67 lines
2.3 KiB
Python
67 lines
2.3 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SUPPORTED_AUDIO_FORMATS = frozenset({
|
|
"aac", "aiff", "alac", "amr", "caf", "flac",
|
|
"m4a", "mp3", "oga", "wav", "webm", "wma",
|
|
})
|
|
|
|
NATIVE_OPUS_EXTS = frozenset({".opus", ".ogg"})
|
|
|
|
FFMPEG_OPUS_CMD = (
|
|
"ffmpeg -i {input_path} -vn -ar 48000 -ac 1 -c:a libopus -b:a 64k {output_path}"
|
|
)
|
|
|
|
|
|
def is_native_opus(filename: str) -> bool:
|
|
ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
|
|
return f".{ext}" in NATIVE_OPUS_EXTS
|
|
|
|
|
|
def requires_transcoding(filename: str) -> bool:
|
|
ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else ""
|
|
return ext in SUPPORTED_AUDIO_FORMATS and not is_native_opus(filename)
|
|
|
|
|
|
def build_transcode_command(input_path: str, output_path: str) -> str:
|
|
return FFMPEG_OPUS_CMD.format(input_path=input_path, output_path=output_path)
|
|
|
|
|
|
async def transcode_to_opus(
|
|
input_path: str,
|
|
output_path: str,
|
|
max_duration_secs: int = 300,
|
|
) -> dict:
|
|
import asyncio
|
|
|
|
if not requires_transcoding(input_path):
|
|
return {"success": True, "path": input_path, "transcoded": False}
|
|
|
|
cmd = build_transcode_command(input_path, output_path)
|
|
|
|
try:
|
|
proc = await asyncio.create_subprocess_shell(
|
|
cmd,
|
|
stdout=asyncio.subprocess.PIPE,
|
|
stderr=asyncio.subprocess.PIPE,
|
|
)
|
|
stdout, stderr = await asyncio.wait_for(
|
|
proc.communicate(), timeout=max_duration_secs + 10
|
|
)
|
|
|
|
if proc.returncode == 0:
|
|
return {"success": True, "path": output_path, "transcoded": True}
|
|
else:
|
|
error_msg = stderr.decode("utf-8", errors="replace")[:500]
|
|
logger.error("FFmpeg transcoding failed: %s", error_msg)
|
|
return {"success": False, "error": error_msg, "path": None, "transcoded": False}
|
|
except asyncio.TimeoutError:
|
|
return {"success": False, "error": "Transcoding timed out", "path": None, "transcoded": False}
|
|
except FileNotFoundError:
|
|
return {"success": False, "error": "ffmpeg not found in PATH", "path": None, "transcoded": False}
|
|
except Exception:
|
|
logger.exception("Voice transcoding error")
|
|
return {"success": False, "error": "Internal error during transcoding", "path": None, "transcoded": False} |