fix: add transcription provider fallback

This commit is contained in:
2026-06-10 11:47:47 +00:00
parent 6670b5adec
commit 53889da8f6
4 changed files with 183 additions and 7 deletions
+90 -4
View File
@@ -19,10 +19,15 @@ load_dotenv()
logger = logging.getLogger(__name__)
OPENROUTER_BASE = "https://openrouter.ai/api/v1"
OPENAI_BASE = "https://api.openai.com/v1"
TRANSCRIPTION_MODELS = [
"openai/gpt-4o-mini-transcribe",
"openai/whisper-large-v3",
]
DIRECT_OPENAI_TRANSCRIPTION_MODELS = [
"gpt-4o-mini-transcribe",
"whisper-1",
]
TRANSCRIPTION_CHUNK_SECONDS = 600
MAX_TRANSCRIPTION_FILE_BYTES = 25 * 1024 * 1024
SUMMARY_TRANSCRIPT_CHAR_LIMIT = 12000
@@ -32,6 +37,10 @@ def _api_key() -> str:
return os.getenv("OPENROUTER_API_KEY", "")
def _openai_api_key() -> str:
return os.getenv("OPENAI_API_KEY", "")
def _auth_headers() -> dict[str, str]:
api_key = _api_key()
if not api_key:
@@ -177,7 +186,7 @@ async def _split_audio_for_transcription(audio_path: str) -> list[str]:
return chunk_paths
async def _transcribe_chunk(client: httpx.AsyncClient, audio_path: str, headers: dict[str, str]) -> str:
async def _transcribe_chunk_with_openrouter(client: httpx.AsyncClient, audio_path: str, headers: dict[str, str]) -> str:
failures: list[str] = []
for model in TRANSCRIPTION_MODELS:
resp = await client.post(
@@ -193,7 +202,7 @@ async def _transcribe_chunk(client: httpx.AsyncClient, audio_path: str, headers:
suffix = f"; generation_id={generation_id}" if generation_id else ""
failures.append(f"{model}: HTTP {resp.status_code}: {detail}{suffix}")
logger.warning(
"Transcription model failed: model=%s chunk=%s status=%s detail=%s generation_id=%s",
"Transcription model failed: provider=openrouter model=%s chunk=%s status=%s detail=%s generation_id=%s",
model,
Path(audio_path).name,
resp.status_code,
@@ -206,7 +215,7 @@ async def _transcribe_chunk(client: httpx.AsyncClient, audio_path: str, headers:
text = data.get("text", "")
if text.strip():
logger.info(
"Transcription chunk completed: chunk=%s model=%s transcript_chars=%s",
"Transcription chunk completed: provider=openrouter chunk=%s model=%s transcript_chars=%s",
Path(audio_path).name,
model,
len(text.strip()),
@@ -215,7 +224,7 @@ async def _transcribe_chunk(client: httpx.AsyncClient, audio_path: str, headers:
failures.append(f"{model}: returned empty text")
logger.warning(
"Transcription model returned empty text: model=%s chunk=%s",
"Transcription model returned empty text: provider=openrouter model=%s chunk=%s",
model,
Path(audio_path).name,
)
@@ -223,6 +232,83 @@ async def _transcribe_chunk(client: httpx.AsyncClient, audio_path: str, headers:
raise RuntimeError("OpenRouter transcription failed across all models: " + " | ".join(failures))
async def _transcribe_chunk_with_openai(client: httpx.AsyncClient, audio_path: str) -> str:
api_key = _openai_api_key()
if not api_key:
raise RuntimeError("OPENAI_API_KEY not set")
failures: list[str] = []
headers = {"Authorization": f"Bearer {api_key}"}
for model in DIRECT_OPENAI_TRANSCRIPTION_MODELS:
with open(audio_path, "rb") as audio_file:
files = {"file": (Path(audio_path).name, audio_file, "audio/wav")}
data = {"model": model}
resp = await client.post(
f"{OPENAI_BASE}/audio/transcriptions",
headers=headers,
data=data,
files=files,
)
try:
resp.raise_for_status()
except httpx.HTTPStatusError:
detail = summarize_error(_safe_json(resp), fallback=resp.text)
request_id = resp.headers.get("x-request-id")
suffix = f"; request_id={request_id}" if request_id else ""
failures.append(f"{model}: HTTP {resp.status_code}: {detail}{suffix}")
logger.warning(
"Transcription model failed: provider=openai model=%s chunk=%s status=%s detail=%s request_id=%s",
model,
Path(audio_path).name,
resp.status_code,
detail,
request_id,
)
continue
data = resp.json()
text = data.get("text", "")
if text.strip():
logger.info(
"Transcription chunk completed: provider=openai chunk=%s model=%s transcript_chars=%s",
Path(audio_path).name,
model,
len(text.strip()),
)
return text.strip()
failures.append(f"{model}: returned empty text")
logger.warning(
"Transcription model returned empty text: provider=openai model=%s chunk=%s",
model,
Path(audio_path).name,
)
raise RuntimeError("Direct OpenAI transcription failed across all models: " + " | ".join(failures))
async def _transcribe_chunk(client: httpx.AsyncClient, audio_path: str, headers: dict[str, str]) -> str:
failures: list[str] = []
try:
return await _transcribe_chunk_with_openrouter(client, audio_path, headers)
except RuntimeError as exc:
failures.append(str(exc))
logger.warning(
"OpenRouter transcription provider failed for chunk=%s; checking direct OpenAI fallback",
Path(audio_path).name,
)
if _openai_api_key():
try:
return await _transcribe_chunk_with_openai(client, audio_path)
except RuntimeError as exc:
failures.append(str(exc))
else:
failures.append("direct OpenAI fallback skipped: OPENAI_API_KEY not set")
raise RuntimeError("Transcription failed across providers: " + " | ".join(failures))
async def transcribe(audio_path: str) -> str:
"""Send audio to OpenRouter STT models and return transcript text."""
logger.info(