feat: add chunked meeting transcription and logging
This commit is contained in:
@@ -1,11 +1,22 @@
|
||||
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__)))
|
||||
|
||||
from openrouter_client import TRANSCRIPTION_MODELS, _audio_format, _build_transcription_payload
|
||||
import openrouter_client
|
||||
from openrouter_client import (
|
||||
SUMMARY_TRANSCRIPT_CHAR_LIMIT,
|
||||
TRANSCRIPTION_MODELS,
|
||||
_audio_format,
|
||||
_build_transcription_payload,
|
||||
_chunk_text,
|
||||
summarize,
|
||||
transcribe,
|
||||
)
|
||||
|
||||
|
||||
def test_audio_format_defaults_to_wav_when_missing_suffix(tmp_path: Path):
|
||||
@@ -23,3 +34,83 @@ def test_build_transcription_payload_uses_base64_json_shape(tmp_path: Path):
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user