Maia/backend/app/services/chunk_service.py

89 lines
3.5 KiB
Python

import os
import subprocess
import glob
import logging
from app.config import settings
logger = logging.getLogger(__name__)
def get_chunks_dir(meeting_id: str) -> str:
path = os.path.join(settings.AUDIO_STORAGE_PATH, "chunks", meeting_id)
os.makedirs(path, exist_ok=True)
return path
def save_chunk(chunk_data: bytes, meeting_id: str, chunk_index: int, ext: str = ".webm") -> str:
chunks_dir = get_chunks_dir(meeting_id)
filename = f"chunk_{chunk_index:06d}{ext}"
path = os.path.join(chunks_dir, filename)
with open(path, "wb") as f:
f.write(chunk_data)
return path
def assemble_chunks(meeting_id: str) -> str:
"""
Assemble all chunks into a single WAV file for Whisper processing.
Timeslice WebM chunks (from a single continuous MediaRecorder session) form a
valid WebM stream when binary-concatenated in order. We concatenate them and
then convert to 16kHz mono WAV with ffmpeg.
Falls back to ffmpeg concat demuxer if binary concat fails.
"""
chunks_dir = get_chunks_dir(meeting_id)
chunk_files = sorted(glob.glob(os.path.join(chunks_dir, "chunk_*")))
if not chunk_files:
raise ValueError(f"No chunks found for meeting {meeting_id}")
output_path = os.path.join(settings.AUDIO_STORAGE_PATH, f"{meeting_id}.wav")
if len(chunk_files) == 1:
result = subprocess.run(
["ffmpeg", "-y", "-i", chunk_files[0],
"-ar", "16000", "-ac", "1", "-sample_fmt", "s16", output_path],
capture_output=True, text=True, timeout=300,
)
if result.returncode != 0:
logger.error(f"ffmpeg single-chunk convert failed: {result.stderr}")
raise RuntimeError(f"Failed to convert audio chunk: {result.stderr}")
else:
# Binary-concatenate timeslice WebM chunks (they are sequential fragments
# of the same MediaRecorder stream — binary concat = valid WebM stream)
raw_concat = os.path.join(chunks_dir, "concat_raw.webm")
with open(raw_concat, "wb") as out:
for cf in chunk_files:
with open(cf, "rb") as inp:
out.write(inp.read())
result = subprocess.run(
["ffmpeg", "-y", "-i", raw_concat,
"-ar", "16000", "-ac", "1", "-sample_fmt", "s16", output_path],
capture_output=True, text=True, timeout=300,
)
if result.returncode != 0:
logger.warning(f"Binary concat failed ({result.stderr[:200]}), trying concat demuxer...")
concat_list = os.path.join(chunks_dir, "concat.txt")
with open(concat_list, "w") as f:
for cf in chunk_files:
f.write(f"file '{cf}'\n")
result2 = subprocess.run(
["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_list,
"-ar", "16000", "-ac", "1", "-sample_fmt", "s16", output_path],
capture_output=True, text=True, timeout=300,
)
if result2.returncode != 0:
logger.error(f"Both assembly methods failed: {result2.stderr}")
raise RuntimeError(f"Failed to assemble audio chunks: {result2.stderr}")
logger.info(f"Assembled {len(chunk_files)} chunks -> {output_path}")
return output_path
def cleanup_chunks(meeting_id: str):
"""Remove chunk files after successful assembly."""
import shutil
chunks_dir = get_chunks_dir(meeting_id)
if os.path.exists(chunks_dir):
shutil.rmtree(chunks_dir)