diff --git a/tools/transcription_audio.py b/tools/transcription_audio.py index 069a3d957b..15413f370a 100644 --- a/tools/transcription_audio.py +++ b/tools/transcription_audio.py @@ -64,7 +64,7 @@ _STT_M4A_ENCODE_ARGS = ("-vn", "-ac", "1", "-ar", "16000", "-c:a", "aac", "-b:a" def _run_ffmpeg_stt_encode(ffmpeg: str, input_path: str, output_path: str, *, audio_filter: Optional[str] = None) -> None: - """Run the shared STT m4a encode, optionally with an ``-af`` filter. Raises on failure (callers own the semantics).""" + """Run the shared STT m4a encode, optionally with an ``-af`` filter. Raises on failure; callers own the semantics.""" command = [ffmpeg, "-y", "-i", input_path] if audio_filter: command += ["-af", audio_filter] @@ -236,7 +236,9 @@ def _probe_audio_duration(file_path: str) -> Optional[float]: ffprobe = _find_ffprobe_binary() if not ffprobe: return None - command = [ffprobe, "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", file_path] + command = [ + ffprobe, "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", file_path, + ] try: return float(_run_quiet(command, timeout=30).stdout.strip()) except Exception: # noqa: BLE001 - probe is best-effort diff --git a/tools/transcription_cloud.py b/tools/transcription_cloud.py index 0148afa7e7..a1d20f9d51 100644 --- a/tools/transcription_cloud.py +++ b/tools/transcription_cloud.py @@ -1,10 +1,10 @@ """Cloud STT providers. -OpenAI-SDK-shaped backends (groq, openai, deepinfra), Mistral Voxtral, the REST -multipart backends (xAI, ElevenLabs), and OpenAI audio credential resolution -(config > keyless local server > env > managed Nous gateway). Every name is -re-imported by ``tools/transcription_tools.py`` (patch surface), which is -imported lazily here so origin patches still intercept. +OpenAI-SDK-shaped backends (groq, openai, deepinfra), Mistral Voxtral, REST multipart +backends (xAI, ElevenLabs), and OpenAI audio credential resolution (config > keyless +local server > env > managed Nous gateway). Every name is re-imported by +``tools/transcription_tools.py`` (patch surface), imported lazily here so origin +patches still intercept. """ from __future__ import annotations @@ -35,11 +35,7 @@ def _has_xai_stt_credentials() -> bool: def _with_openai_client(api_key: str, base_url: Optional[str], file_path: str, log_label: str, body): - """Run ``body(client)`` against a fresh OpenAI SDK client (30s timeout, no SDK retries). - - Always closes the client; any exception maps to the shared envelope via - :func:`_openai_sdk_failure`. - """ + """Run ``body(client)`` on a fresh OpenAI SDK client (30s timeout, no retries); always closed, errors -> envelope.""" try: from openai import OpenAI client = OpenAI(api_key=api_key, base_url=base_url, timeout=30, max_retries=0) @@ -64,8 +60,8 @@ def _cloud_failure(exc: BaseException, file_path: str, label: str, detail: Optio def _openai_sdk_failure(exc: BaseException, file_path: str, log_label: str) -> Dict[str, Any]: """Map an OpenAI-SDK-shaped exception to the shared error envelope. - Order matters: APIConnectionError is checked before APITimeoutError (its - subclass) so timeouts report as connection errors, as they always have. + APIConnectionError is checked before APITimeoutError (its subclass) so timeouts + report as connection errors, as they always have. """ try: from openai import APIError, APIConnectionError, APITimeoutError @@ -93,11 +89,7 @@ def _sdk_prompt_kwargs(language: Optional[str], prompt: Optional[str]) -> Dict[s def _transcribe_groq( file_path: str, model_name: str, *, language: Optional[str] = None, prompt: Optional[str] = None ) -> Dict[str, Any]: - """Transcribe using Groq Whisper API (free tier available). - - Language: hook override > ``stt.groq.language`` > ``stt.language`` > env; - otherwise Groq auto-detects. - """ + """Transcribe via the Groq Whisper API; language: hook > ``stt.groq.language`` > ``stt.language`` > env > auto.""" from tools.transcription_tools import _HAS_OPENAI, _resolve_provider_key, _resolve_stt_language api_key = _resolve_provider_key("GROQ_API_KEY", "groq") if not api_key: @@ -131,9 +123,8 @@ def _transcribe_openai( ) -> Dict[str, Any]: """Transcribe via the OpenAI ``audio.transcriptions.create`` SDK shape. - Shared backend for every OpenAI-compatible STT endpoint (DeepInfra etc.): - callers pass explicit ``api_key``/``base_url`` to skip the OpenAI-only auth - chain and a ``provider_label`` for the response's ``provider``. + Shared by every OpenAI-compatible endpoint (DeepInfra etc.): explicit ``api_key``/ + ``base_url`` skip the OpenAI-only auth chain; ``provider_label`` names the response's provider. """ from tools.transcription_tools import _HAS_OPENAI, _resolve_openai_audio_client_config, _resolve_stt_language if api_key is None: @@ -149,8 +140,7 @@ def _transcribe_openai( if not _HAS_OPENAI: return _error_result("openai package not installed") - # Auto-correct a Groq-only model on the native OpenAI path only — - # third-party endpoints may legitimately serve a whisper-large-v3 variant. + # Auto-correct a Groq-only model on the native OpenAI path only (third-party endpoints may serve it). if provider_label == "openai" and model_name in GROQ_MODELS: logger.info("Model %s not available on OpenAI, using %s", model_name, DEFAULT_STT_MODEL) model_name = DEFAULT_STT_MODEL @@ -167,14 +157,12 @@ def _transcribe_openai( } if language: if model_name == "gpt-transcribe": - # gpt-transcribe replaces ``language`` with a ``languages`` - # list and rejects requests sending the legacy field. + # gpt-transcribe takes a ``languages`` list and rejects the legacy field. create_kwargs["extra_body"] = {"languages": [language]} else: create_kwargs["language"] = language logger.debug("Using language hint '%s' for OpenAI STT", language) - if prompt: - # Only sent when set so the no-hook, no-config request stays byte-identical. + if prompt: # only when set so the bare request stays byte-identical create_kwargs["prompt"] = prompt return client.audio.transcriptions.create(**create_kwargs) @@ -185,8 +173,7 @@ def _transcribe_openai( message = str(exc).lower() if not any(k in message for k in ("unsupported", "corrupted", "invalid file")): raise - # Newer models reject some containers whisper-1 accepted - # (notably Ogg/Opus voice notes): transcode to m4a, retry once. + # Newer models reject containers whisper-1 accepted (Ogg/Opus voice notes): transcode, retry once. converted_path, transcode_error = _transcode_audio_for_stt(file_path, work_dir) if transcode_error: return _error_result(transcode_error) @@ -255,11 +242,10 @@ def _post_audio_multipart(url: str, headers: Dict[str, str], file_path: str, dat def _rest_transcript(response, label: str, extract_detail, extract_text): - """Turn a multipart STT response into ``(text, body, None)`` or ``(None, None, error_envelope)``. + """Multipart STT response -> ``(text, body, None)`` or ``(None, None, error_envelope)``. - Non-200 -> ``"