This commit is contained in:
JiriUhlir
2026-07-10 09:46:49 +02:00
parent 429cb84461
commit 7a3f61c125
16 changed files with 762 additions and 16 deletions
+30 -1
View File
@@ -1,3 +1,32 @@
# audio-transcription
Generated by AppFactory.
Stateless FastAPI služba (AppFactory) pro přepis audia. Přijme audio soubor + parametry
v POST a vrátí JSON. Přepis běží paralelně dvěma engine — **Deepgram** a **OpenAI Whisper**
— a volitelně se oba texty sloučí přes OpenAI Chat do jednoho co nejpřesnějšího přepisu.
Port původní .NET implementace `CallCenterController` (dual/combined přepis); napojení na
Twilio bylo vynecháno — audio se posílá přímo jako proměnná (upload).
## Endpointy
- `GET /health` — health check
- `GET /version` — verze + root_path
- `POST /transcribe/dual` — Deepgram + Whisper (dva nezávislé texty)
- `POST /transcribe/combined` — dual + sloučený `merged`
Swagger: `/docs`.
## Přihlašovací údaje (hlavičky)
- `X-Deepgram-Api-Key`
- `X-OpenAI-Api-Key`
## Lokální běh
```bash
pip install -r requirements.txt
uvicorn app.main:app --host 0.0.0.0 --port 8000
```
Podrobná dokumentace: [documentation/overview.md](documentation/overview.md),
[documentation/transcribe.md](documentation/transcribe.md).
View File
+60
View File
@@ -0,0 +1,60 @@
"""Klient pro Deepgram pre-recorded přepis (REST, přes httpx).
Nepoužíváme těžké SDK — voláme REST endpoint přímo, aby nebyl závislý na verzích.
"""
import httpx
from ..config import DEEPGRAM_BASE_URL, UPSTREAM_TIMEOUT_SECONDS
from ..errors import UpstreamError
from ..logging_config import get_logger
log = get_logger("audio-transcription.deepgram")
async def transcribe(
*,
api_key: str,
audio: bytes,
content_type: str,
model: str,
language: str,
diarize: bool,
smart_format: bool,
) -> str:
"""Přepíše audio Deepgramem a vrátí prostý transkript (channels[0].alternatives[0].transcript)."""
params = {
"model": model,
"language": language,
"diarize": "true" if diarize else "false",
"smart_format": "true" if smart_format else "false",
}
headers = {
"Authorization": f"Token {api_key}",
"Content-Type": content_type or "application/octet-stream",
}
url = f"{DEEPGRAM_BASE_URL}/listen"
try:
async with httpx.AsyncClient(timeout=UPSTREAM_TIMEOUT_SECONDS) as client:
resp = await client.post(url, params=params, headers=headers, content=audio)
except httpx.HTTPError as exc:
log.error("Deepgram request failed: %s", exc)
raise UpstreamError(f"Deepgram request se nezdařil: {exc}") from exc
if resp.status_code >= 400:
# Deepgram vrací chybu v JSON; nikdy nelogujeme klíč (ten je jen v hlavičce).
snippet = resp.text[:500]
log.warning("Deepgram returned %s: %s", resp.status_code, snippet)
raise UpstreamError(
f"Deepgram vrátil chybu {resp.status_code}.",
detail=snippet,
)
data = resp.json()
try:
channels = data["results"]["channels"]
alternative = channels[0]["alternatives"][0]
return alternative.get("transcript", "") or ""
except (KeyError, IndexError, TypeError) as exc:
log.error("Unexpected Deepgram response shape: %s", exc)
raise UpstreamError("Neočekávaná struktura odpovědi z Deepgramu.") from exc
+87
View File
@@ -0,0 +1,87 @@
"""Klient pro OpenAI — Whisper přepis + Chat Completions (slučování), přes httpx."""
import httpx
from ..config import OPENAI_BASE_URL, UPSTREAM_TIMEOUT_SECONDS
from ..errors import UpstreamError
from ..logging_config import get_logger
log = get_logger("audio-transcription.openai")
async def transcribe(
*,
api_key: str,
audio: bytes,
filename: str,
content_type: str,
model: str,
language: str | None = None,
) -> str:
"""Přepíše audio přes OpenAI Whisper (/audio/transcriptions) a vrátí text."""
headers = {"Authorization": f"Bearer {api_key}"}
files = {"file": (filename or "audio.mp3", audio, content_type or "application/octet-stream")}
form = {"model": model}
if language:
form["language"] = language
url = f"{OPENAI_BASE_URL}/audio/transcriptions"
try:
async with httpx.AsyncClient(timeout=UPSTREAM_TIMEOUT_SECONDS) as client:
resp = await client.post(url, headers=headers, data=form, files=files)
except httpx.HTTPError as exc:
log.error("OpenAI transcription request failed: %s", exc)
raise UpstreamError(f"OpenAI Whisper request se nezdařil: {exc}") from exc
if resp.status_code >= 400:
snippet = resp.text[:500]
log.warning("OpenAI transcription returned %s: %s", resp.status_code, snippet)
raise UpstreamError(
f"OpenAI Whisper vrátil chybu {resp.status_code}.",
detail=snippet,
)
data = resp.json()
return data.get("text", "") or ""
async def merge_transcripts(
*,
api_key: str,
model: str,
system_prompt: str,
text_deepgram: str,
text_whisper: str,
) -> str:
"""Sloučí dva přepisy do jednoho přes Chat Completions dle system promptu."""
headers = {"Authorization": f"Bearer {api_key}"}
payload = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"Text z Deepgram: {text_deepgram}"},
{"role": "user", "content": f"Text z OpenAI Whisper: {text_whisper}"},
],
}
url = f"{OPENAI_BASE_URL}/chat/completions"
try:
async with httpx.AsyncClient(timeout=UPSTREAM_TIMEOUT_SECONDS) as client:
resp = await client.post(url, headers=headers, json=payload)
except httpx.HTTPError as exc:
log.error("OpenAI chat request failed: %s", exc)
raise UpstreamError(f"OpenAI chat (merge) request se nezdařil: {exc}") from exc
if resp.status_code >= 400:
snippet = resp.text[:500]
log.warning("OpenAI chat returned %s: %s", resp.status_code, snippet)
raise UpstreamError(
f"OpenAI chat (merge) vrátil chybu {resp.status_code}.",
detail=snippet,
)
data = resp.json()
try:
return data["choices"][0]["message"]["content"] or ""
except (KeyError, IndexError, TypeError) as exc:
log.error("Unexpected OpenAI chat response shape: %s", exc)
raise UpstreamError("Neočekávaná struktura odpovědi z OpenAI chatu.") from exc
+23
View File
@@ -0,0 +1,23 @@
"""Konfigurace čtená z environment variables (AppFactory runtime .env).
Žádné secrets zde nejsou — API klíče se předávají per-request přes X- hlavičky
(viz app/credentials.py). Zde jsou pouze veřejné defaulty a nastavení proxy.
"""
import os
APP_NAME = os.getenv("APP_NAME", "audio-transcription")
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
ROOT_PATH = os.getenv("ROOT_PATH", "")
# Upstream base URL adresy (přepsatelné přes env, kdyby se změnily).
DEEPGRAM_BASE_URL = os.getenv("DEEPGRAM_BASE_URL", "https://api.deepgram.com/v1")
OPENAI_BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
# Defaultní modely — přepsatelné v POST parametrech requestu.
DEFAULT_DEEPGRAM_MODEL = os.getenv("DEFAULT_DEEPGRAM_MODEL", "nova-2")
DEFAULT_WHISPER_MODEL = os.getenv("DEFAULT_WHISPER_MODEL", "whisper-1")
DEFAULT_CHAT_MODEL = os.getenv("DEFAULT_CHAT_MODEL", "gpt-4o")
DEFAULT_LANGUAGE = os.getenv("DEFAULT_LANGUAGE", "cs")
# Timeout pro upstream volání (přepis dlouhého audia může trvat).
UPSTREAM_TIMEOUT_SECONDS = float(os.getenv("UPSTREAM_TIMEOUT_SECONDS", "300"))
+44
View File
@@ -0,0 +1,44 @@
"""Extrakce per-request přihlašovacích údajů z X- hlaviček.
Secrets (API klíče) chodí VÝHRADNĚ v hlavičkách, nikdy v těle requestu ani v URL.
Nic se neukládá — služba je stateless.
"""
from dataclasses import dataclass
from fastapi import Header
from .errors import CredentialsError
@dataclass
class Credentials:
deepgram_api_key: str | None
openai_api_key: str | None
def require_deepgram(self) -> str:
if not self.deepgram_api_key or not self.deepgram_api_key.strip():
raise CredentialsError("Chybí hlavička X-Deepgram-Api-Key.")
return self.deepgram_api_key.strip()
def require_openai(self) -> str:
if not self.openai_api_key or not self.openai_api_key.strip():
raise CredentialsError("Chybí hlavička X-OpenAI-Api-Key.")
return self.openai_api_key.strip()
def get_credentials(
x_deepgram_api_key: str | None = Header(
default=None,
alias="X-Deepgram-Api-Key",
description="Deepgram API klíč (secret). Nutný pro Deepgram přepis.",
),
x_openai_api_key: str | None = Header(
default=None,
alias="X-OpenAI-Api-Key",
description="OpenAI API klíč (secret). Nutný pro Whisper přepis a slučovací chat.",
),
) -> Credentials:
return Credentials(
deepgram_api_key=x_deepgram_api_key,
openai_api_key=x_openai_api_key,
)
+70
View File
@@ -0,0 +1,70 @@
"""Typované výjimky + centrální exception handlery.
Platí pravidlo: žádná tichá selhání — každá chyba se loguje (bez secrets).
"""
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from .logging_config import get_logger
log = get_logger("audio-transcription.errors")
class TranscriptionError(Exception):
"""Základní chyba služby s HTTP status kódem."""
status_code = 500
def __init__(self, message: str, status_code: int | None = None, detail=None):
super().__init__(message)
self.message = message
if status_code is not None:
self.status_code = status_code
self.detail = detail
class CredentialsError(TranscriptionError):
"""Chybějící / neplatné přihlašovací údaje (X- hlavičky)."""
status_code = 401
class BadRequestError(TranscriptionError):
"""Neplatný vstup (chybí audio, špatné parametry)."""
status_code = 400
class UpstreamError(TranscriptionError):
"""Chyba při volání upstream API (Deepgram / OpenAI / stažení audia)."""
status_code = 502
def register_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(TranscriptionError)
async def _handle_transcription_error(request: Request, exc: TranscriptionError):
log.warning(
"%s on %s: %s",
exc.__class__.__name__,
request.url.path,
exc.message,
)
body = {"error": exc.__class__.__name__, "message": exc.message}
if exc.detail is not None:
body["detail"] = exc.detail
return JSONResponse(status_code=exc.status_code, content=body)
@app.exception_handler(Exception)
async def _handle_unexpected(request: Request, exc: Exception):
# Nelogujeme celý stack s možnými secrets ve vstupu; logujeme typ + zprávu.
log.error(
"Unhandled %s on %s: %s",
exc.__class__.__name__,
request.url.path,
exc,
)
return JSONResponse(
status_code=500,
content={"error": "InternalError", "message": "Neočekávaná chyba serveru."},
)
+16
View File
@@ -0,0 +1,16 @@
"""Centrální logging. Nikdy nelogujeme secrets (API klíče, hlavičky s creds)."""
import logging
import os
_LEVEL = os.getenv("LOG_LEVEL", "INFO").upper()
def configure_logging() -> None:
logging.basicConfig(
level=_LEVEL,
format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
)
def get_logger(name: str) -> logging.Logger:
return logging.getLogger(name)
+47 -15
View File
@@ -1,25 +1,57 @@
"""Vstupní bod aplikace audio-transcription.
Stateless FastAPI služba běžící v AppFactory za reverse proxy `/apps/<app-id>`.
Přijímá audio + parametry v POST, přepisuje ho paralelně (Deepgram + OpenAI Whisper)
a volitelně slučuje do jednoho co nejlepšího přepisu. Výstup je vždy JSON.
API klíče se předávají per-request v X- hlavičkách, nikdy se neukládají ani nelogují.
"""
import os
from fastapi import FastAPI
APP_NAME = os.getenv("APP_NAME", "audio-transcription")
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
ROOT_PATH = os.getenv("ROOT_PATH", "")
from .config import APP_NAME, APP_VERSION, ROOT_PATH
from .errors import register_exception_handlers
from .logging_config import configure_logging
from .routers import meta, transcribe
configure_logging()
DESCRIPTION = """
Přepis hovorů / audia dvěma engine (Deepgram + OpenAI Whisper) a jejich AI sloučení.
### Přihlašovací údaje (hlavičky)
Secrets se předávají v hlavičkách u každého requestu — nikdy v těle ani v URL:
- `X-Deepgram-Api-Key` — Deepgram API klíč (Deepgram Console → API Keys)
- `X-OpenAI-Api-Key` — OpenAI API klíč (platform.openai.com → API keys)
### Endpointy
- `POST /transcribe/dual` — vrátí dva nezávislé přepisy (`text1` = Deepgram, `text2` = Whisper)
- `POST /transcribe/combined` — dual + `merged` (sloučený výsledek dle `combine_prompt`)
Audio se posílá jako proměnná `file` (multipart/form-data). Ostatní parametry
(modely, jazyk, prompt) jsou form fields v POST.
"""
app = FastAPI(
title=APP_NAME,
version=APP_VERSION,
root_path=ROOT_PATH
description=DESCRIPTION,
root_path=ROOT_PATH,
)
@app.get("/health")
def health():
return {"status": "ok"}
register_exception_handlers(app)
@app.get("/version")
def version():
return {
"app": APP_NAME,
"version": APP_VERSION,
"language": "python",
"root_path": ROOT_PATH
}
app.include_router(meta.router)
app.include_router(transcribe.router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(
"app.main:app",
host="0.0.0.0",
port=int(os.getenv("PORT", "8000")),
)
+31
View File
@@ -0,0 +1,31 @@
"""Defaultní prompt pro slučování přepisů (Czech Transcript Merger).
Přebráno z původní .NET implementace CallCenterController. Volající může poslat
vlastní `combine_prompt` v POST; když ho nepošle, použije se tento.
"""
DEFAULT_COMBINE_PROMPT = (
"Jsi \"Czech Transcript Merger\". Tvým úkolem je z *Text1* (Deepgram) a *Text2* "
"(OpenAI) vytvořit jediný, co nejpřesnější český přepis téhož hovoru."
"PRAVIDLA SLOUČENÍ"
"1) Plynulost & běžná mluva: upřednostňuj Text1."
"2) Strukturované údaje (NEZKRESLUJ, NEVYMÝŠLEJ): upřednostňuj Text2 pro: jména a "
"příjmení, firmy/brand, SPZ, čísla (tel., částky, datum/čas), e-maily, URL/domény, "
"adresy, čísla objednávek, kódy."
"3) Pokud si Text1 a Text2 odporují:"
" - pro údaje z bodu (2) zvol Text2, *pokud* je formátově i významově věrohodný; "
"jinak použij lépe vypadající variantu z Text1 nebo bezpečně oprav na gramaticky "
"správnou podobu beze změny významu."
" - pro běžné věty a obraty preferuj Text1."
"4) Oprav zjevné chyby rozpoznání (např. „panešance“ → „pane Švance“, "
"„CS Technologys“ → „CS Technologies“, „Samalepa.cz“ → „Samolepak.cz“). Zachovej "
"diakritiku a přirozenou češtinu."
"5) Odstraň duplicity, záseky a šum (např. náhodná slova typu „Přiším.“, opakování "
"„slyšíme se, slyšíme se“ zkrať na přirozené)."
"6) Nepřidávej obsah, který není v žádném zdroji. Dovoleny jsou jen minimální "
"gramatické a stylistické úpravy pro srozumitelnost."
"7) Když je to z kontextu jasné, můžeš rozlišit mluvčí „Asistent:“ / „Volající:“. "
"Není-li to jisté, ponech bez štítků."
"😎 VÝSTUP: *Pouze finální sloučený text* (žádné vysvětlení, žádné značky, žádný "
"JSON, žádné kódy). Když nevíš, kým začít, tak na prvním místě je asistent."
)
View File
+21
View File
@@ -0,0 +1,21 @@
"""Povinné meta endpointy: /health a /version."""
from fastapi import APIRouter
from ..config import APP_NAME, APP_VERSION, ROOT_PATH
router = APIRouter(tags=["meta"])
@router.get("/health", summary="Health check")
def health():
return {"status": "ok"}
@router.get("/version", summary="Verze a runtime informace")
def version():
return {
"app": APP_NAME,
"version": APP_VERSION,
"language": "python",
"root_path": ROOT_PATH,
}
+168
View File
@@ -0,0 +1,168 @@
"""Endpointy pro přepis audia.
- POST /transcribe/dual → paralelní přepis Deepgram + OpenAI Whisper (dva texty)
- POST /transcribe/combined → dual + sloučení do jednoho co nejlepšího přepisu (merge)
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.
"""
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 DualResult(BaseModel):
text1: str = Field(..., description="Přepis z Deepgramu.")
text2: str = Field(..., description="Přepis z OpenAI Whisper.")
deepgram_model: str
whisper_model: str
language: str
class CombinedResult(DualResult):
merged: str = Field(..., description="Sloučený, co nejpřesnější výsledný přepis.")
chat_model: str
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
async def _transcribe_dual(
creds: Credentials,
audio: bytes,
filename: str,
content_type: str,
deepgram_model: str,
whisper_model: str,
language: str,
diarize: bool,
smart_format: bool,
) -> tuple[str, str]:
"""Spustí oba přepisy paralelně; vrátí (deepgram_text, whisper_text)."""
deepgram_key = creds.require_deepgram()
openai_key = creds.require_openai()
text1, text2 = 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 text1, text2
@router.post(
"/transcribe/dual",
response_model=DualResult,
summary="Paralelní přepis Deepgram + OpenAI Whisper",
description="Přijme audio soubor a vrátí dva nezávislé přepisy: "
"`text1` z Deepgramu a `text2` z OpenAI Whisper.",
)
async def transcribe_dual(
file: UploadFile = File(..., description="Audio soubor k přepisu (proměnná)."),
deepgram_model: str = Form(DEFAULT_DEEPGRAM_MODEL, description="Deepgram model."),
whisper_model: str = Form(DEFAULT_WHISPER_MODEL, description="OpenAI přepisový model."),
language: str = Form(DEFAULT_LANGUAGE, description="Jazyk audia (ISO kód, např. cs)."),
diarize: bool = Form(True, description="Deepgram diarizace (rozlišení mluvčích)."),
smart_format: bool = Form(True, description="Deepgram smart formatting."),
creds: Credentials = Depends(get_credentials),
) -> DualResult:
audio, filename, content_type = await _read_upload(file)
text1, text2 = await _transcribe_dual(
creds, audio, filename, content_type,
deepgram_model, whisper_model, language, diarize, smart_format,
)
return DualResult(
text1=text1,
text2=text2,
deepgram_model=deepgram_model,
whisper_model=whisper_model,
language=language,
)
@router.post(
"/transcribe/combined",
response_model=CombinedResult,
summary="Dual přepis + AI sloučení do nejlepšího přepisu",
description="Přijme audio soubor, vytvoří přepis z Deepgramu i Whisperu a poté je "
"sloučí přes OpenAI Chat podle `combine_prompt` do jediného, co nejpřesnějšího "
"českého přepisu. Vrací `text1`, `text2` i výsledný `merged`.",
)
async def transcribe_combined(
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)."),
smart_format: bool = Form(True, description="Deepgram smart formatting."),
creds: Credentials = Depends(get_credentials),
) -> CombinedResult:
audio, filename, content_type = await _read_upload(file)
text1, text2 = await _transcribe_dual(
creds, audio, filename, content_type,
deepgram_model, whisper_model, language, diarize, smart_format,
)
prompt = combine_prompt.strip() if combine_prompt and combine_prompt.strip() else DEFAULT_COMBINE_PROMPT
merged = await openai_client.merge_transcripts(
api_key=creds.require_openai(),
model=chat_model,
system_prompt=prompt,
text_deepgram=text1,
text_whisper=text2,
)
return CombinedResult(
text1=text1,
text2=text2,
merged=merged,
deepgram_model=deepgram_model,
whisper_model=whisper_model,
chat_model=chat_model,
language=language,
)
+66
View File
@@ -0,0 +1,66 @@
# audio-transcription — přehled
Stateless FastAPI služba v AppFactory. Přijme audio + parametry v POST a vrátí JSON
s přepisy. Přepis běží dvěma engine paralelně (Deepgram + OpenAI Whisper) a volitelně
se oba texty sloučí přes OpenAI Chat do jednoho co nejpřesnějšího přepisu.
Vychází z původní .NET implementace `CallCenterController` (dual/combined přepis).
Napojení na Twilio bylo vynecháno — audio se posílá přímo jako proměnná (upload).
## Architektura
```
app/
main.py # FastAPI app, root_path, Swagger, exception handlers
config.py # env konfigurace + defaulty (žádné secrets)
logging_config.py # logging (bez secrets)
errors.py # typované výjimky + handlery (žádná tichá selhání)
credentials.py # X- hlavičky → přihlašovací údaje (stateless)
prompts.py # výchozí slučovací prompt (Czech Transcript Merger)
clients/
deepgram_client.py # Deepgram pre-recorded REST (httpx)
openai_client.py # OpenAI Whisper + Chat Completions (httpx)
routers/
meta.py # /health, /version
transcribe.py # /transcribe/dual, /transcribe/combined
```
Žádné SDK — upstream API se volají přímo přes `httpx`, aby služba nebyla závislá na
verzích knihoven. Nic se neukládá; služba je multi-tenant a stateless.
## Reverse proxy
Běží za `/apps/audio-transcription`. `root_path` se čte z env `ROOT_PATH`, takže
OpenAPI `servers` i Swagger „Try it out“ míří na správný prefix.
## Endpointy
| Metoda | Cesta | Popis |
|--------|-------|-------|
| GET | `/health` | Health check (200) |
| GET | `/version` | Verze + root_path |
| POST | `/transcribe/dual` | Deepgram + Whisper (dva texty) |
| POST | `/transcribe/combined` | dual + sloučený `merged` |
Detaily viz [transcribe.md](transcribe.md).
## Přihlašovací údaje
Secrets se předávají v hlavičkách u každého requestu (nikdy v těle/URL, nikdy se
nelogují ani neukládají):
- `X-Deepgram-Api-Key`
- `X-OpenAI-Api-Key`
## Konfigurace (env)
| Proměnná | Default | Popis |
|----------|---------|-------|
| `ROOT_PATH` | `""` | Prefix reverse proxy (`/apps/audio-transcription`) |
| `DEFAULT_DEEPGRAM_MODEL` | `nova-2` | Výchozí Deepgram model |
| `DEFAULT_WHISPER_MODEL` | `whisper-1` | Výchozí OpenAI přepisový model |
| `DEFAULT_CHAT_MODEL` | `gpt-4o` | Výchozí chat model pro sloučení |
| `DEFAULT_LANGUAGE` | `cs` | Výchozí jazyk |
| `UPSTREAM_TIMEOUT_SECONDS` | `300` | Timeout upstream volání |
| `DEEPGRAM_BASE_URL` | `https://api.deepgram.com/v1` | Base URL Deepgram |
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | Base URL OpenAI |
+97
View File
@@ -0,0 +1,97 @@
# Endpointy přepisu
Audio se posílá jako proměnná `file` (multipart/form-data). Všechny ostatní parametry
jsou form fields v POST. API klíče jsou v X- hlavičkách. Výstup je vždy JSON.
## Hlavičky (secrets)
| Hlavička | Nutná | Popis |
|----------|-------|-------|
| `X-Deepgram-Api-Key` | ano | Deepgram API klíč |
| `X-OpenAI-Api-Key` | ano | OpenAI API klíč (Whisper + slučovací chat) |
## POST /transcribe/dual
Paralelní přepis dvěma engine. Vrací dva nezávislé texty.
### Form parametry
| Pole | Typ | Default | Popis |
|------|-----|---------|-------|
| `file` | soubor | — | Audio k přepisu (povinné) |
| `deepgram_model` | text | `nova-2` | Deepgram model |
| `whisper_model` | text | `whisper-1` | OpenAI přepisový model |
| `language` | text | `cs` | Jazyk (ISO kód) |
| `diarize` | bool | `true` | Deepgram diarizace |
| `smart_format` | bool | `true` | Deepgram smart formatting |
### Odpověď
```json
{
"text1": "přepis z Deepgramu",
"text2": "přepis z OpenAI Whisper",
"deepgram_model": "nova-2",
"whisper_model": "whisper-1",
"language": "cs"
}
```
### Příklad (curl)
```bash
curl -X POST https://services.csbot.cz/apps/audio-transcription/transcribe/dual \
-H "X-Deepgram-Api-Key: <DEEPGRAM_KEY>" \
-H "X-OpenAI-Api-Key: <OPENAI_KEY>" \
-F "file=@nahravka.mp3" \
-F "language=cs"
```
## POST /transcribe/combined
Jako `dual`, navíc oba přepisy sloučí přes OpenAI Chat do jednoho výsledku (`merged`).
### Form parametry
Vše z `dual`, plus:
| Pole | Typ | Default | Popis |
|------|-----|---------|-------|
| `combine_prompt` | text | výchozí „Czech Transcript Merger“ | System prompt pro sloučení |
| `chat_model` | text | `gpt-4o` | OpenAI chat model pro sloučení |
Nezadá-li se `combine_prompt`, použije se výchozí prompt z `app/prompts.py`.
### Odpověď
```json
{
"text1": "přepis z Deepgramu",
"text2": "přepis z OpenAI Whisper",
"merged": "sloučený, co nejpřesnější výsledný přepis",
"deepgram_model": "nova-2",
"whisper_model": "whisper-1",
"chat_model": "gpt-4o",
"language": "cs"
}
```
### Příklad (curl)
```bash
curl -X POST https://services.csbot.cz/apps/audio-transcription/transcribe/combined \
-H "X-Deepgram-Api-Key: <DEEPGRAM_KEY>" \
-H "X-OpenAI-Api-Key: <OPENAI_KEY>" \
-F "file=@nahravka.mp3"
```
## Chybové stavy
| HTTP | Kdy |
|------|-----|
| 400 | Chybí / prázdný audio soubor |
| 401 | Chybí `X-Deepgram-Api-Key` nebo `X-OpenAI-Api-Key` |
| 502 | Chyba upstream API (Deepgram / OpenAI) |
| 500 | Neočekávaná chyba serveru |
Všechny chyby se logují (bez secrets), žádné tiché selhání.
+2
View File
@@ -1,2 +1,4 @@
fastapi
uvicorn[standard]
httpx
python-multipart