Files
audio-transcription/app/routers/transcribe.py
T
2026-07-13 08:54:45 +02:00

156 lines
5.9 KiB
Python

"""Endpoint pro přepis audia.
POST /transcribe — udělá vše najednou: audio přepíše paralelně Deepgramem i OpenAI
(whisper-1) a oba texty sloučí přes OpenAI Chat podle `combine_prompt` do jednoho
co nejlepšího přepisu.
Audio přichází jako proměnná (upload `file` ve form-data). Všechny ostatní parametry
jsou v POST těle (form fields). API klíče jsou v X- hlavičkách (viz credentials.py).
Výstup je vždy JSON: {"gpt": ..., "deepgram": ..., "merge": ..., "errors": [...]}.
Selhání jednoho z přepisů (Deepgram / OpenAI) nebo sloučení neshodí celý request —
jeho část zůstane prázdná a chyba se přidá do pole `errors`.
"""
import asyncio
from fastapi import APIRouter, Depends, File, Form, UploadFile
from pydantic import BaseModel, Field
from ..clients import deepgram_client, openai_client
from ..config import (
DEFAULT_CHAT_MODEL,
DEFAULT_DEEPGRAM_MODEL,
DEFAULT_LANGUAGE,
DEFAULT_WHISPER_MODEL,
)
from ..credentials import Credentials, get_credentials
from ..errors import BadRequestError
from ..logging_config import get_logger
from ..prompts import DEFAULT_COMBINE_PROMPT
log = get_logger("audio-transcription.transcribe")
router = APIRouter(tags=["transcribe"])
class TranscribeResult(BaseModel):
gpt: str = Field(..., description="Přepis z OpenAI (model whisper-1), plynulý, bez mluvčích.")
deepgram: str = Field(
...,
description="Přepis z Deepgramu. Při zapnuté diarizaci se štítky mluvčích "
"(`Mluvčí 0: ...`).",
)
merge: str = Field(
...,
description="Sloučený, co nejpřesnější a úplný přepis se štítky mluvčích.",
)
errors: list[str] = Field(
default_factory=list,
description="Chyby dílčích kroků (Deepgram / OpenAI přepis / sloučení). "
"Prázdné, pokud vše proběhlo v pořádku. Selhavší část je pak prázdná.",
)
async def _read_upload(file: UploadFile) -> tuple[bytes, str, str]:
"""Načte upload do paměti a vrátí (bytes, filename, content_type)."""
audio = await file.read()
if not audio:
raise BadRequestError("Nahraný soubor je prázdný.")
filename = file.filename or "audio.mp3"
content_type = file.content_type or "application/octet-stream"
return audio, filename, content_type
@router.post(
"/dual-with-merge",
response_model=TranscribeResult,
summary="Přepis audia (Deepgram + OpenAI) a AI sloučení — vše najednou",
description="Přijme audio soubor, vytvoří paralelně přepis z Deepgramu i z OpenAI "
"(whisper-1) a poté je sloučí přes OpenAI Chat podle `combine_prompt` do jediného, "
"co nejpřesnějšího českého přepisu. Vrací JSON `{gpt, deepgram, merge}`.",
)
async def transcribe(
file: UploadFile = File(..., description="Audio soubor k přepisu (proměnná)."),
combine_prompt: str = Form(
DEFAULT_COMBINE_PROMPT,
description="System prompt pro sloučení. Nezadáš-li, použije se výchozí "
"'Czech Transcript Merger'.",
),
deepgram_model: str = Form(DEFAULT_DEEPGRAM_MODEL, description="Deepgram model."),
whisper_model: str = Form(DEFAULT_WHISPER_MODEL, description="OpenAI přepisový model."),
chat_model: str = Form(DEFAULT_CHAT_MODEL, description="OpenAI chat model pro sloučení."),
language: str = Form(DEFAULT_LANGUAGE, description="Jazyk audia (ISO kód, např. cs)."),
diarize: bool = Form(
True,
description="Deepgram diarizace — rozlišení mluvčích. Při zapnutí vrací `deepgram` "
"přepis se štítky `Mluvčí N:` (interně zapne i utterances).",
),
smart_format: bool = Form(True, description="Deepgram smart formatting."),
creds: Credentials = Depends(get_credentials),
) -> TranscribeResult:
deepgram_key = creds.require_deepgram()
openai_key = creds.require_openai()
audio, filename, content_type = await _read_upload(file)
errors: list[str] = []
# Oba přepisy paralelně; selhání jednoho neshodí druhý ani celý request.
result_deepgram, result_gpt = await asyncio.gather(
deepgram_client.transcribe(
api_key=deepgram_key,
audio=audio,
content_type=content_type,
model=deepgram_model,
language=language,
diarize=diarize,
smart_format=smart_format,
),
openai_client.transcribe(
api_key=openai_key,
audio=audio,
filename=filename,
content_type=content_type,
model=whisper_model,
language=language,
),
return_exceptions=True,
)
if isinstance(result_deepgram, BaseException):
log.exception("Deepgram přepis selhal", exc_info=result_deepgram)
errors.append(f"Deepgram přepis selhal: {result_deepgram}")
text_deepgram = ""
else:
text_deepgram = result_deepgram
if isinstance(result_gpt, BaseException):
log.exception("OpenAI (whisper) přepis selhal", exc_info=result_gpt)
errors.append(f"OpenAI (whisper) přepis selhal: {result_gpt}")
text_gpt = ""
else:
text_gpt = result_gpt
# Sloučení má smysl jen když je aspoň jeden přepis k dispozici.
merged = ""
if text_deepgram or text_gpt:
prompt = combine_prompt.strip() if combine_prompt and combine_prompt.strip() else DEFAULT_COMBINE_PROMPT
try:
merged = await openai_client.merge_transcripts(
api_key=openai_key,
model=chat_model,
system_prompt=prompt,
text_deepgram=text_deepgram,
text_whisper=text_gpt,
)
except Exception as exc:
log.exception("Sloučení přepisů selhalo")
errors.append(f"Sloučení přepisů selhalo: {exc}")
return TranscribeResult(
gpt=text_gpt,
deepgram=text_deepgram,
merge=merged,
errors=errors,
)