from __future__ import annotations import asyncio import os import tempfile from pathlib import Path from yuxi.utils.logging_config import logger class Transcoder: def __init__(self, ffmpeg_path: str = "ffmpeg"): self._ffmpeg = ffmpeg_path async def to_ogg_opus(self, input_path: str, sample_rate: int = 16000) -> str: output_path = input_path + ".ogg" cmd = [ self._ffmpeg, "-y", "-i", input_path, "-c:a", "libopus", "-b:a", "24k", "-ar", str(sample_rate), "-ac", "1", "-vbr", "on", "-application", "voip", "-frame_duration", "20", output_path, ] try: process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() if process.returncode != 0: err_msg = stderr.decode()[:500] if stderr else "unknown error" raise RuntimeError(f"FFmpeg transcode failed (exit {process.returncode}): {err_msg}") logger.debug(f"Transcoded {input_path} → {output_path}") return output_path except FileNotFoundError: raise RuntimeError(f"FFmpeg not found at '{self._ffmpeg}'. Install FFmpeg to use TTS voice notes.") async def wav_to_ogg_opus(self, wav_data: bytes) -> bytes: with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: f.write(wav_data) tmp_in = f.name try: tmp_out = await self.to_ogg_opus(tmp_in) try: return Path(tmp_out).read_bytes() finally: if os.path.exists(tmp_out): os.unlink(tmp_out) finally: if os.path.exists(tmp_in): os.unlink(tmp_in) async def check_available(self) -> bool: try: proc = await asyncio.create_subprocess_exec( self._ffmpeg, "-version", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) await proc.communicate() return proc.returncode == 0 except FileNotFoundError: return False transcoder = Transcoder()