Add speaker-attributed meeting transcription

This commit is contained in:
2026-06-12 12:18:35 +00:00
parent 7516e0ad61
commit 8fd5e5b464
7 changed files with 571 additions and 13 deletions
+71 -2
View File
@@ -1,6 +1,7 @@
from __future__ import annotations
import asyncio
import json
import logging
import os
import uuid
@@ -14,7 +15,7 @@ from dotenv import load_dotenv
import config
from helpers import chunk_message, command_channel_error
from openrouter_client import summarize, transcribe
from openrouter_client import summarize, transcribe, transcribe_tracks
from voice import MeetingRecorder
logging.basicConfig(
@@ -187,6 +188,74 @@ async def _save_retry_state(guild_id: int, file_path: str, source_channel_id: in
)
def _load_speaker_tracks(file_path: str) -> list[dict]:
"""Return per-speaker track entries for a recording, or [] if none.
Looks for a ``tracks/tracks.json`` manifest next to the mixed archive WAV.
Track paths in the manifest are stored relative to the app working
directory; they are resolved against the manifest location when needed so
the lookup works regardless of absolute vs relative archive paths.
"""
manifest_path = Path(file_path).parent / "tracks" / "tracks.json"
if not manifest_path.exists():
return []
try:
manifest = json.loads(manifest_path.read_text())
except (json.JSONDecodeError, OSError) as exc:
logger.warning("Failed reading speaker-track manifest path=%s error=%s", manifest_path, exc)
return []
tracks = manifest.get("tracks")
if not isinstance(tracks, list):
return []
resolved: list[dict] = []
for track in tracks:
if not isinstance(track, dict):
continue
path = track.get("path")
display_name = track.get("display_name") or "Unknown User"
if not path:
continue
candidate = Path(path)
if not candidate.exists():
# Manifest paths may be relative to the app cwd; also try resolving
# the file name against the manifest's own directory.
alt = manifest_path.parent / candidate.name
if alt.exists():
candidate = alt
else:
logger.warning("Speaker track file missing: display_name=%s path=%s", display_name, path)
continue
resolved.append({"display_name": display_name, "path": str(candidate)})
return resolved
async def _transcribe_recording(file_path: str, guild_id: int) -> str:
"""Transcribe using per-speaker tracks when available, else the mixed file."""
tracks = _load_speaker_tracks(file_path)
if tracks:
logger.info(
"Using speaker-attributed transcription: guild_id=%s speakers=%s",
guild_id,
len(tracks),
)
try:
return await transcribe_tracks(tracks)
except Exception as exc: # noqa: BLE001 - fall back to mixed file
logger.warning(
"Speaker-track transcription failed; falling back to mixed archive: guild_id=%s error=%s",
guild_id,
exc,
)
logger.info("Using single-file transcription: guild_id=%s path=%s", guild_id, file_path)
return await transcribe(file_path)
async def process_recording(
file_path: str,
guild_id: int,
@@ -237,7 +306,7 @@ async def process_recording(
os.path.getsize(file_path) / 1024 / 1024,
)
transcript = await transcribe(file_path)
transcript = await _transcribe_recording(file_path, guild_id)
logger.info("Transcription complete for guild_id=%s transcript_chars=%s", guild_id, len(transcript))
summary = await summarize(transcript)
logger.info("Summarization complete for guild_id=%s summary_chars=%s", guild_id, len(summary))