139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
import asyncio
|
|
import base64
|
|
import logging
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
|
|
|
import openrouter_client
|
|
from openrouter_client import (
|
|
SUMMARY_TRANSCRIPT_CHAR_LIMIT,
|
|
TRANSCRIPTION_MODELS,
|
|
_audio_format,
|
|
_build_transcription_payload,
|
|
_chunk_text,
|
|
_combined_summary_prompt,
|
|
_summary_prompt,
|
|
summarize,
|
|
transcribe,
|
|
)
|
|
|
|
|
|
def test_audio_format_defaults_to_wav_when_missing_suffix(tmp_path: Path):
|
|
path = tmp_path / "recording"
|
|
path.write_bytes(b"abc")
|
|
assert _audio_format(str(path)) == "wav"
|
|
|
|
|
|
def test_build_transcription_payload_uses_base64_json_shape(tmp_path: Path):
|
|
path = tmp_path / "meeting.wav"
|
|
path.write_bytes(b"RIFFdemo")
|
|
|
|
payload = _build_transcription_payload(str(path), TRANSCRIPTION_MODELS[0])
|
|
|
|
assert payload["model"] == "openai/gpt-4o-mini-transcribe"
|
|
assert payload["input_audio"]["format"] == "wav"
|
|
assert payload["input_audio"]["data"] == base64.b64encode(b"RIFFdemo").decode("ascii")
|
|
|
|
|
|
def test_chunk_text_splits_long_text_under_limit():
|
|
text = ("alpha beta gamma\n" * 1500).strip()
|
|
chunks = _chunk_text(text, limit=SUMMARY_TRANSCRIPT_CHAR_LIMIT)
|
|
|
|
assert len(chunks) > 1
|
|
assert all(len(chunk) <= SUMMARY_TRANSCRIPT_CHAR_LIMIT for chunk in chunks)
|
|
assert "\n".join(chunks) == text
|
|
|
|
|
|
def test_transcribe_combines_chunk_transcripts_and_logs_chunking(tmp_path: Path, monkeypatch, caplog):
|
|
source = tmp_path / "meeting.wav"
|
|
normalized = tmp_path / "normalized.wav"
|
|
chunk1 = tmp_path / "chunk-000.wav"
|
|
chunk2 = tmp_path / "chunk-001.wav"
|
|
source.write_bytes(b"source")
|
|
normalized.write_bytes(b"normalized")
|
|
chunk1.write_bytes(b"c1")
|
|
chunk2.write_bytes(b"c2")
|
|
|
|
async def fake_normalize(audio_path: str) -> str:
|
|
assert audio_path == str(source)
|
|
return str(normalized)
|
|
|
|
async def fake_split(audio_path: str) -> list[str]:
|
|
assert audio_path == str(normalized)
|
|
return [str(chunk1), str(chunk2)]
|
|
|
|
seen = []
|
|
|
|
async def fake_transcribe_chunk(_client, audio_path: str, _headers: dict[str, str]) -> str:
|
|
seen.append(Path(audio_path).name)
|
|
return {
|
|
"chunk-000.wav": "hello there",
|
|
"chunk-001.wav": "general kenobi",
|
|
}[Path(audio_path).name]
|
|
|
|
monkeypatch.setattr(openrouter_client, "_normalize_audio_for_transcription", fake_normalize)
|
|
monkeypatch.setattr(openrouter_client, "_split_audio_for_transcription", fake_split)
|
|
monkeypatch.setattr(openrouter_client, "_transcribe_chunk", fake_transcribe_chunk)
|
|
monkeypatch.setattr(openrouter_client, "_auth_headers", lambda: {"Authorization": "Bearer test"})
|
|
|
|
class DummyClient:
|
|
async def __aenter__(self):
|
|
return object()
|
|
|
|
async def __aexit__(self, exc_type, exc, tb):
|
|
return False
|
|
|
|
monkeypatch.setattr(openrouter_client.httpx, "AsyncClient", lambda timeout: DummyClient())
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
result = asyncio.run(transcribe(str(source)))
|
|
|
|
assert result == "hello there\n\ngeneral kenobi"
|
|
assert seen == ["chunk-000.wav", "chunk-001.wav"]
|
|
assert "Split normalized audio into 2 chunk(s)" in caplog.text
|
|
|
|
|
|
def test_summarize_uses_multi_stage_pipeline_for_long_transcript(monkeypatch, caplog):
|
|
long_transcript = ("decision item follow-up\n" * 4000).strip()
|
|
prompts = []
|
|
|
|
async def fake_chat_completion(prompt: str, *, timeout: int = 180) -> str:
|
|
prompts.append(prompt)
|
|
if "Partial meeting transcript chunk" in prompt:
|
|
return f"INTERMEDIATE-{len(prompts)}"
|
|
return "FINAL SUMMARY"
|
|
|
|
monkeypatch.setattr(openrouter_client, "_chat_completion", fake_chat_completion)
|
|
|
|
with caplog.at_level(logging.INFO):
|
|
result = asyncio.run(summarize(long_transcript))
|
|
|
|
assert result == "FINAL SUMMARY"
|
|
assert len(prompts) >= 2
|
|
assert any("Partial meeting transcript chunk" in prompt for prompt in prompts[:-1])
|
|
assert "Combined chunk summaries" in prompts[-1]
|
|
assert "Transcript exceeds single-pass summary limit" in caplog.text
|
|
|
|
|
|
def test_summary_prompt_requires_requested_sections():
|
|
prompt = _summary_prompt("Alice: finished API work")
|
|
|
|
assert "## What Was Done" in prompt
|
|
assert "## What Will Be Done Next" in prompt
|
|
assert "## Action Items" in prompt
|
|
assert "## Decisions" in prompt
|
|
assert "all dev team members' reports" in prompt
|
|
|
|
|
|
def test_combined_summary_prompt_requires_requested_sections():
|
|
prompt = _combined_summary_prompt(["Chunk 1 summary", "Chunk 2 summary"])
|
|
|
|
assert "## What Was Done" in prompt
|
|
assert "## What Will Be Done Next" in prompt
|
|
assert "## Action Items" in prompt
|
|
assert "## Decisions" in prompt
|
|
assert "all dev team members' reports" in prompt
|