This commit is contained in:
JiriUhlir
2026-07-10 10:08:51 +02:00
parent 57faf12d97
commit d55089b1c8
5 changed files with 105 additions and 37 deletions
+37 -3
View File
@@ -21,12 +21,19 @@ async def transcribe(
diarize: bool,
smart_format: bool,
) -> str:
"""Přepíše audio Deepgramem a vrátí prostý transkript (channels[0].alternatives[0].transcript)."""
"""Přepíše audio Deepgramem.
Když je zapnutá diarizace, vrátí přepis se štítky mluvčích (z `utterances`),
např. `Mluvčí 0: ...`. Bez diarizace vrátí plochý transkript.
"""
params = {
"model": model,
"language": language,
"diarize": "true" if diarize else "false",
"smart_format": "true" if smart_format else "false",
# utterances rozseká přepis na promluvy s přiřazeným mluvčím — nutné, aby
# v textu bylo vidět rozlišení mluvčích (samotný `transcript` je plochý).
"utterances": "true" if diarize else "false",
}
headers = {
"Authorization": f"Token {api_key}",
@@ -48,9 +55,36 @@ async def transcribe(
data = resp.json()
try:
channels = data["results"]["channels"]
alternative = channels[0]["alternatives"][0]
results = data["results"]
# 1) Preferuj diarizovaný výstup z utterances (se štítky mluvčích).
diarized = _format_utterances(results.get("utterances"))
if diarized:
return diarized
# 2) Fallback na plochý transkript.
alternative = results["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
def _format_utterances(utterances) -> str:
"""Sloučí Deepgram utterances do přepisu se štítky mluvčích (`Mluvčí N: ...`).
Po sobě jdoucí promluvy stejného mluvčího spojí do jednoho řádku.
"""
if not utterances:
return ""
lines: list[str] = []
prev_speaker = None
for utt in utterances:
text = (utt.get("transcript") or "").strip()
if not text:
continue
speaker = utt.get("speaker")
if speaker != prev_speaker:
lines.append(f"Mluvčí {speaker}: {text}")
prev_speaker = speaker
else:
lines[-1] += " " + text
return "\n".join(lines)
+2 -1
View File
@@ -20,7 +20,8 @@ async def transcribe(
"""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}
# temperature=0 => deterministický přepis, méně halucinací na tichu/šumu.
form = {"model": model, "temperature": "0"}
if language:
form["language"] = language
url = f"{OPENAI_BASE_URL}/audio/transcriptions"
+37 -25
View File
@@ -1,31 +1,43 @@
"""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.
Vychází z původní .NET implementace, ale je upravený tak, aby:
- zachoval VŠECHNY repliky obou zdrojů (sjednocení, ne průnik),
- využil rozlišení mluvčích z Deepgramu (Text1 chodí se štítky `Mluvčí N:`),
- ignoroval typické halucinace Whisperu na tichu/šumu.
Volající může poslat vlastní `combine_prompt` v POST; jinak se použije 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 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."
"Jsi \"Czech Transcript Merger\". Ze dvou přepisů TÉHOŽ českého hovoru — *Text1* "
"(Deepgram, obsahuje rozlišení mluvčích ve tvaru „Mluvčí 0:\", „Mluvčí 1:\") a "
"*Text2* (OpenAI Whisper, plynulejší, ale bez mluvčích a někdy s halucinacemi) — "
"vytvoř jediný, co nejpřesnější a ÚPLNÝ přepis.\n"
"\n"
"PRAVIDLA:\n"
"1) ÚPLNOST je priorita: zachovej VŠECHNY repliky z obou zdrojů (sjednocení). "
"Nikdy nevynechávej reakce mluvčích jen proto, že jsou jen v jednom zdroji — typicky "
"krátké odpovědi volajícího („Ne, teď to nejde.\", „Ano.\", „Děkuji.\") bývají jen "
"v jednom přepisu, přesto je zařaď.\n"
"2) Rozlišení mluvčích: použij strukturu mluvčích z Text1. Každou repliku uveď "
"štítkem na začátku řádku. Když je z kontextu jasné, kdo je kdo, přejmenuj mluvčí na "
"„Asistent:\" (volá bot/firma) a „Volající:\"; jinak ponech „Mluvčí 0:\" / „Mluvčí 1:\". "
"Když začátek není jistý, první mluví Asistent.\n"
"3) Plynulost & běžná mluva: upřednostňuj znění z Text1; Text2 použij pro doplnění "
"chybějících pasáží a upřesnění.\n"
"4) Strukturované údaje (NEZKRESLUJ, NEVYMÝŠLEJ) — jména/íjmení, firmy/brand, SPZ, "
"čísla (tel., částky, datum/čas), e-maily, URL/domény, adresy, čísla objednávek, kódy: "
"vyber tu variantu, která je formátově i významově věrohodnější (často Text2). "
"Oprav zjevné chyby rozpoznání (např. „CS Technoloužís\"/„CS Technologys\" "
"„CS Technologies\", „bojce bota\" → „voicebota\", „I služby\" → „e-služby\"). "
"Zachovej diakritiku a přirozenou češtinu.\n"
"5) Ignoruj zjevné HALUCINACE Whisperu: nesmyslné či nesouvisející útržky (zvlášť na "
"konci hovoru na tichu/šumu, opakující se nebo mimo kontext) do výstupu NEDÁVEJ. "
"V pochybnostech se opři o Text1.\n"
"6) Odstraň duplicity, záseky a šum; opakování zkrať na přirozené.\n"
"7) Nepřidávej obsah, který není ani v jednom zdroji. Povoleny jsou jen minimální "
"gramatické a stylistické úpravy pro srozumitelnost.\n"
"\n"
"VÝSTUP: pouze finální sloučený přepis, po řádcích se štítky mluvčích. Žádné "
"vysvětlení, žádné poznámky, žádný JSON."
)
+15 -4
View File
@@ -31,9 +31,16 @@ router = APIRouter(tags=["transcribe"])
class TranscribeResult(BaseModel):
gpt: str = Field(..., description="Přepis z OpenAI (model whisper-1).")
deepgram: str = Field(..., description="Přepis z Deepgramu.")
merge: str = Field(..., description="Sloučený, co nejpřesnější výsledný přepis.")
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.",
)
async def _read_upload(file: UploadFile) -> tuple[bytes, str, str]:
@@ -65,7 +72,11 @@ async def transcribe(
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)."),
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: