Add speaker-attributed meeting transcription
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user