fix
This commit is contained in:
@@ -21,12 +21,19 @@ async def transcribe(
|
|||||||
diarize: bool,
|
diarize: bool,
|
||||||
smart_format: bool,
|
smart_format: bool,
|
||||||
) -> str:
|
) -> 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 = {
|
params = {
|
||||||
"model": model,
|
"model": model,
|
||||||
"language": language,
|
"language": language,
|
||||||
"diarize": "true" if diarize else "false",
|
"diarize": "true" if diarize else "false",
|
||||||
"smart_format": "true" if smart_format 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 = {
|
headers = {
|
||||||
"Authorization": f"Token {api_key}",
|
"Authorization": f"Token {api_key}",
|
||||||
@@ -48,9 +55,36 @@ async def transcribe(
|
|||||||
|
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
try:
|
try:
|
||||||
channels = data["results"]["channels"]
|
results = data["results"]
|
||||||
alternative = channels[0]["alternatives"][0]
|
# 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 ""
|
return alternative.get("transcript", "") or ""
|
||||||
except (KeyError, IndexError, TypeError) as exc:
|
except (KeyError, IndexError, TypeError) as exc:
|
||||||
log.error("Unexpected Deepgram response shape: %s", exc)
|
log.error("Unexpected Deepgram response shape: %s", exc)
|
||||||
raise UpstreamError("Neočekávaná struktura odpovědi z Deepgramu.") from 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)
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ async def transcribe(
|
|||||||
"""Přepíše audio přes OpenAI Whisper (/audio/transcriptions) a vrátí text."""
|
"""Přepíše audio přes OpenAI Whisper (/audio/transcriptions) a vrátí text."""
|
||||||
headers = {"Authorization": f"Bearer {api_key}"}
|
headers = {"Authorization": f"Bearer {api_key}"}
|
||||||
files = {"file": (filename or "audio.mp3", audio, content_type or "application/octet-stream")}
|
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:
|
if language:
|
||||||
form["language"] = language
|
form["language"] = language
|
||||||
url = f"{OPENAI_BASE_URL}/audio/transcriptions"
|
url = f"{OPENAI_BASE_URL}/audio/transcriptions"
|
||||||
|
|||||||
+37
-25
@@ -1,31 +1,43 @@
|
|||||||
"""Defaultní prompt pro slučování přepisů (Czech Transcript Merger).
|
"""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
|
Vychází z původní .NET implementace, ale je upravený tak, aby:
|
||||||
vlastní `combine_prompt` v POST; když ho nepošle, použije se tento.
|
- 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 = (
|
DEFAULT_COMBINE_PROMPT = (
|
||||||
"Jsi \"Czech Transcript Merger\". Tvým úkolem je z *Text1* (Deepgram) a *Text2* "
|
"Jsi \"Czech Transcript Merger\". Ze dvou přepisů TÉHOŽ českého hovoru — *Text1* "
|
||||||
"(OpenAI) vytvořit jediný, co nejpřesnější český přepis téhož hovoru."
|
"(Deepgram, obsahuje rozlišení mluvčích ve tvaru „Mluvčí 0:\", „Mluvčí 1:\") a "
|
||||||
"PRAVIDLA SLOUČENÍ"
|
"*Text2* (OpenAI Whisper, plynulejší, ale bez mluvčích a někdy s halucinacemi) — "
|
||||||
"1) Plynulost & běžná mluva: upřednostňuj Text1."
|
"vytvoř jediný, co nejpřesnější a ÚPLNÝ přepis.\n"
|
||||||
"2) Strukturované údaje (NEZKRESLUJ, NEVYMÝŠLEJ): upřednostňuj Text2 pro: jména a "
|
"\n"
|
||||||
"příjmení, firmy/brand, SPZ, čísla (tel., částky, datum/čas), e-maily, URL/domény, "
|
"PRAVIDLA:\n"
|
||||||
"adresy, čísla objednávek, kódy."
|
"1) ÚPLNOST je priorita: zachovej VŠECHNY repliky z obou zdrojů (sjednocení). "
|
||||||
"3) Pokud si Text1 a Text2 odporují:"
|
"Nikdy nevynechávej reakce mluvčích jen proto, že jsou jen v jednom zdroji — typicky "
|
||||||
" - pro údaje z bodu (2) zvol Text2, *pokud* je formátově i významově věrohodný; "
|
"krátké odpovědi volajícího („Ne, teď to nejde.\", „Ano.\", „Děkuji.\") bývají jen "
|
||||||
"jinak použij lépe vypadající variantu z Text1 nebo bezpečně oprav na gramaticky "
|
"v jednom přepisu, přesto je zařaď.\n"
|
||||||
"správnou podobu beze změny významu."
|
"2) Rozlišení mluvčích: použij strukturu mluvčích z Text1. Každou repliku uveď "
|
||||||
" - pro běžné věty a obraty preferuj Text1."
|
"štítkem na začátku řádku. Když je z kontextu jasné, kdo je kdo, přejmenuj mluvčí na "
|
||||||
"4) Oprav zjevné chyby rozpoznání (např. „panešance“ → „pane Švance“, "
|
"„Asistent:\" (volá bot/firma) a „Volající:\"; jinak ponech „Mluvčí 0:\" / „Mluvčí 1:\". "
|
||||||
"„CS Technologys“ → „CS Technologies“, „Samalepa.cz“ → „Samolepak.cz“). Zachovej "
|
"Když začátek není jistý, první mluví Asistent.\n"
|
||||||
"diakritiku a přirozenou češtinu."
|
"3) Plynulost & běžná mluva: upřednostňuj znění z Text1; Text2 použij pro doplnění "
|
||||||
"5) Odstraň duplicity, záseky a šum (např. náhodná slova typu „Přiším.“, opakování "
|
"chybějících pasáží a upřesnění.\n"
|
||||||
"„slyšíme se, slyšíme se“ zkrať na přirozené)."
|
"4) Strukturované údaje (NEZKRESLUJ, NEVYMÝŠLEJ) — jména/příjmení, firmy/brand, SPZ, "
|
||||||
"6) Nepřidávej obsah, který není v žádném zdroji. Dovoleny jsou jen minimální "
|
"čísla (tel., částky, datum/čas), e-maily, URL/domény, adresy, čísla objednávek, kódy: "
|
||||||
"gramatické a stylistické úpravy pro srozumitelnost."
|
"vyber tu variantu, která je formátově i významově věrohodnější (často Text2). "
|
||||||
"7) Když je to z kontextu jasné, můžeš rozlišit mluvčí „Asistent:“ / „Volající:“. "
|
"Oprav zjevné chyby rozpoznání (např. „CS Technoloužís\"/„CS Technologys\" → "
|
||||||
"Není-li to jisté, ponech bez štítků."
|
"„CS Technologies\", „bojce bota\" → „voicebota\", „I služby\" → „e-služby\"). "
|
||||||
"😎 VÝSTUP: *Pouze finální sloučený text* (žádné vysvětlení, žádné značky, žádný "
|
"Zachovej diakritiku a přirozenou češtinu.\n"
|
||||||
"JSON, žádné kódy). Když nevíš, kým začít, tak na prvním místě je asistent."
|
"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."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,9 +31,16 @@ router = APIRouter(tags=["transcribe"])
|
|||||||
|
|
||||||
|
|
||||||
class TranscribeResult(BaseModel):
|
class TranscribeResult(BaseModel):
|
||||||
gpt: str = Field(..., description="Přepis z OpenAI (model whisper-1).")
|
gpt: str = Field(..., description="Přepis z OpenAI (model whisper-1), plynulý, bez mluvčích.")
|
||||||
deepgram: str = Field(..., description="Přepis z Deepgramu.")
|
deepgram: str = Field(
|
||||||
merge: str = Field(..., description="Sloučený, co nejpřesnější výsledný přepis.")
|
...,
|
||||||
|
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]:
|
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."),
|
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í."),
|
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)."),
|
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."),
|
smart_format: bool = Form(True, description="Deepgram smart formatting."),
|
||||||
creds: Credentials = Depends(get_credentials),
|
creds: Credentials = Depends(get_credentials),
|
||||||
) -> TranscribeResult:
|
) -> TranscribeResult:
|
||||||
|
|||||||
@@ -23,18 +23,28 @@ jsou form fields v POST. API klíče jsou v X- hlavičkách. Výstup je vždy JS
|
|||||||
| `whisper_model` | text | `whisper-1` | OpenAI přepisový model |
|
| `whisper_model` | text | `whisper-1` | OpenAI přepisový model |
|
||||||
| `chat_model` | text | `gpt-4o` | OpenAI chat model pro sloučení |
|
| `chat_model` | text | `gpt-4o` | OpenAI chat model pro sloučení |
|
||||||
| `language` | text | `cs` | Jazyk (ISO kód) |
|
| `language` | text | `cs` | Jazyk (ISO kód) |
|
||||||
| `diarize` | bool | `true` | Deepgram diarizace |
|
| `diarize` | bool | `true` | Deepgram diarizace (rozlišení mluvčích) |
|
||||||
| `smart_format` | bool | `true` | Deepgram smart formatting |
|
| `smart_format` | bool | `true` | Deepgram smart formatting |
|
||||||
|
|
||||||
Nezadá-li se `combine_prompt`, použije se výchozí prompt z `app/prompts.py`.
|
Nezadá-li se `combine_prompt`, použije se výchozí prompt z `app/prompts.py`.
|
||||||
|
|
||||||
|
### Rozlišení mluvčích a kvalita
|
||||||
|
|
||||||
|
- Při `diarize=true` (výchozí) Deepgram interně zapne i `utterances` a přepis `deepgram`
|
||||||
|
se vrací se štítky mluvčích, např. `Mluvčí 0: ...` / `Mluvčí 1: ...`. Plochý přepis bez
|
||||||
|
mluvčích (samotné `alternatives[0].transcript`) se použije jen jako fallback.
|
||||||
|
- `gpt` (Whisper) je plynulý, ale **bez mluvčích** a občas halucinuje na tichu/šumu —
|
||||||
|
proto se volá s `temperature=0`.
|
||||||
|
- `merge` sjednotí oba zdroje: použije strukturu mluvčích z Deepgramu, doplní chybějící
|
||||||
|
repliky z Whisperu a ignoruje zjevné halucinace (řídí výchozí `combine_prompt`).
|
||||||
|
|
||||||
### Odpověď
|
### Odpověď
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"gpt": "přepis z OpenAI (whisper-1)",
|
"gpt": "Dobrý den, tady je asistentka ...",
|
||||||
"deepgram": "přepis z Deepgramu",
|
"deepgram": "Mluvčí 0: Dobrý den, tady je asistentka ...\nMluvčí 1: Ne, teď to nejde.",
|
||||||
"merge": "sloučený, co nejpřesnější výsledný přepis"
|
"merge": "Asistent: Dobrý den, tady je asistentka ...\nVolající: Ne, teď to nejde."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user