Anti-hallucination: filename hint to LLM + beam search + silence threshold

When Whisper hallucinates (generates fake lyrics not matching the audio),
LLM can now use the original filename as a hint to recognize the song
and override the false transcript with the actual lyrics.

Pipeline:
1. Pass filename (e.g. 'Ben Zucker - Bonnie und Clyde') as hint
2. Whisper transcribes (may hallucinate)
3. Claude/Gemini reads filename + transcript:
   - Recognizes song from filename hint
   - Compares Whisper output to known lyrics
   - Replaces hallucinated text with real lyrics (preserves timestamps)
   - If can't fix, removes segment (better silent than wrong)

Also added Whisper anti-hallucination params:
- beam_size=5 (more careful decoding vs greedy)
- hallucination_silence_threshold=2.0 (skip text in long silences)
This commit is contained in:
Sebastjan Artič 2026-04-29 10:48:55 +00:00
parent 05fb0081c6
commit 60765ad84c
2 changed files with 65 additions and 29 deletions

View File

@ -242,6 +242,11 @@ def process_job(job_id):
cmd += ["--llm-provider", job["llm_provider"]] cmd += ["--llm-provider", job["llm_provider"]]
if job.get("llm_model"): if job.get("llm_model"):
cmd += ["--llm-model", job["llm_model"]] cmd += ["--llm-model", job["llm_model"]]
# Filename hint = original filename (Claude lahko prepozna pesem)
if job.get("filename"):
# Brez extension
fn_hint = Path(job["filename"]).stem
cmd += ["--filename-hint", fn_hint]
# lang: če None ali 'auto', pusti analyze.py auto-detect # lang: če None ali 'auto', pusti analyze.py auto-detect
if job.get("lang") and job["lang"] not in ("auto", ""): if job.get("lang") and job["lang"] not in ("auto", ""):
cmd += ["--lang", job["lang"]] cmd += ["--lang", job["lang"]]

View File

@ -110,6 +110,10 @@ def transcribe_full(audio_path, lang=None, model_size="small"):
compression_ratio_threshold=2.4, compression_ratio_threshold=2.4,
log_prob_threshold=-1.0, log_prob_threshold=-1.0,
no_speech_threshold=0.6, no_speech_threshold=0.6,
# Beam search namesto greedy = bolj zanesljiv decode (manj halucinacij)
beam_size=5,
# Halucinacija detection: če je tišina dolga, ne pretvarjaj v tekst
hallucination_silence_threshold=2.0,
) )
detected_lang = info.language detected_lang = info.language
detected_prob = float(info.language_probability) detected_prob = float(info.language_probability)
@ -437,7 +441,7 @@ def detect_audio_fade(clip_range, transcript, video_duration=None):
} }
def _build_analysis_prompt(transcript, video_duration, target_duration=30): def _build_analysis_prompt(transcript, video_duration, target_duration=30, filename_hint=None):
"""Pripravi enotni prompt za Claude/Gemini analizo.""" """Pripravi enotni prompt za Claude/Gemini analizo."""
lines = [] lines = []
for seg in transcript["segments"]: for seg in transcript["segments"]:
@ -447,46 +451,67 @@ def _build_analysis_prompt(transcript, video_duration, target_duration=30):
lines.append(f"[{start:6.1f}-{end:6.1f}] {text}") lines.append(f"[{start:6.1f}-{end:6.1f}] {text}")
transcript_text = "\n".join(lines) transcript_text = "\n".join(lines)
hint_block = ""
if filename_hint:
hint_block = f"""
🎵 IME DATOTEKE: "{filename_hint}"
Iz imena datoteke morda lahko prepoznaš naslov pesmi ali izvajalca. Če je tako:
- Uporabi svoje znanje o **dejanskem besedilu** te pesmi
- Če Whisper transkript ne ustreza znanemu besedilu pesmi (halucinacija), POPRAVI besedilo na **dejansko besedilo pesmi**
- Ohrani timestamp-e iz Whisper-ja (časovne meje so pravilne, samo besede so napačne)
"""
return f"""Tu je transcript pesmi iz Whisper modela (timestamp v sekundah, besedilo): return f"""Tu je transcript pesmi iz Whisper modela (timestamp v sekundah, besedilo):
{transcript_text} {transcript_text}
Cela pesem traja {video_duration:.1f}s. Cilj: izrezati ~{target_duration}s odsek za TikTok/Instagram Reel. Cela pesem traja {video_duration:.1f}s. Cilj: izrezati ~{target_duration}s odsek za TikTok/Instagram Reel.{hint_block}
POMEMBNO: Whisper je avtomatski STT in pogosto naredi napake, posebej pri: POMEMBNO: Whisper si IZMIŠLJA besede ko ne razume jasno (HALLUCINACIJA). Posebej:
- slovanskih jezikih (slovenščina, hrvaščina, bosanščina, srbščina) - Ko glasba prevladuje nad vokalom
- narečnih izrazih - Pri narečjih in slovanskih jezikih
- ko glasba prevladuje nad vokalom - Generira "tipičen" tekst (npr. tekst druge pesmi istega izvajalca)
- Lahko vstavi besede ki se POdoBNO slišijo, ampak imajo ČISTO drug pomen
KAKO PREPOZNATI HALUCINACIJO:
- Tekst nima smisla v kontekstu pesmi
- Različni segmenti imajo nepovezane teme (kot da bi bilo več pesmi)
- Refren je v vsakem ponovitvi različen (refren se MORA ponavljati identično)
- Tekst je premalo **glede na trajanje** (več tišine = manj besed, ne več)
PROSIM: PROSIM:
1. Preberi celoten tekst in razumi strukturo (intro / verz / pre-chorus / refren / bridge / outro) 1. Preberi celoten tekst in razumi strukturo (intro / verz / pre-chorus / refren / bridge / outro)
2. POPRAVI očitne napake v transkripciji: 2. POPRAVI očitne halucinacije:
- Če pesem ima refren ki se ponavlja, vse pojavitve refrena POPRAVI da imajo ENAKO besedilo (uporabi najjasnejšo varianto) - Če prepoznaš pesem (po izvajalcu, naslovu, znaku besedila) **uporabi PRAVO besedilo**
- Popravi napačne besede ki nimajo smisla v kontekstu - Če halucinacijo ne moreš popraviti, **odstrani segment** (raje brez podnapisa kot napačen)
- Popravi pomešane jezike (če pesem je slovenska, vse vrstice naj bodo v slovenščini) - Refren MORA imeti vse pojavitve ENAKE
- Popravi pomešane jezike (vse vrstice v enem jeziku)
- Ohrani timestamp-e nespremenjene - Ohrani timestamp-e nespremenjene
3. Prepoznaj REFREN: del besedila, ki se ponavlja v pesmi 3. Prepoznaj REFREN: del besedila ki se PONAVLJA
4. Izberi najboljši odsek za reel: 4. Izberi najboljši odsek za reel:
- Vključi cel refren (cel verz besedila brez prekinitve) - Vključi cel refren (brez prekinitve)
- Če imaš prostor, dodaj pre-chorus build-up tik pred refrenom - Lahko dodaj pre-chorus build-up
- Lahko traja 20-45 sekund (ne strogo 30s) - 20-45 sekund
- Začni in končaj na smiselni meji (konec stavka, ne sredi besede) - Začni in končaj na smiselni meji
5. Če pesem nima jasnega refrena (instrumental, monolog, govor), izberi najbolj dramatičen ali zaključen del 5. Če pesem nima jasnega refrena, izberi najbolj dramatičen ali zaključen del
6. Če Whisper transkript je v večini halucinacija (manj kot 30% smiselnih besed), v "reason" napiši "WHISPER_HALLUCINATION_DETECTED" in vrni najmanj segmentov (samo tisti ki so smiselni)
Odgovori SAMO v JSON formatu (brez markdown, brez razlage): Odgovori SAMO v JSON formatu (brez markdown, brez razlage):
{{ {{
"start": <sekunde>, "start": <sekunde>,
"end": <sekunde>, "end": <sekunde>,
"reason": "<kratka razlaga zakaj ta odsek>", "reason": "<kratka razlaga>",
"chorus_text": "<besedilo refrena ali ključni del>", "chorus_text": "<besedilo refrena>",
"structure": "<1 stavek o strukturi pesmi>", "structure": "<1 stavek o strukturi pesmi>",
"language": "<jezik: sl/de/hr/bs/sr/en/it/es/fr>", "language": "<jezik: sl/de/hr/bs/sr/en/it/es/fr>",
"hallucination_detected": <true/false>,
"corrected_segments": [ "corrected_segments": [
{{"start": <s>, "end": <s>, "text": "<popravljeno besedilo>"}} {{"start": <s>, "end": <s>, "text": "<popravljeno besedilo ALI prazno če halucinacija>"}}
] ]
}} }}
V "corrected_segments" vključi VSE segmente iz inputa s popravljenim besedilom (ohrani timestamp-e).""" V "corrected_segments" vključi VSE segmente iz inputa s popravljenim besedilom. Halucinacije nadomesti s pravim besedilom (če veš) ALI pusti prazno besedilo."""
def _parse_llm_response(text, video_duration): def _parse_llm_response(text, video_duration):
@ -522,10 +547,11 @@ def _parse_llm_response(text, video_duration):
} }
def analyze_with_claude(transcript, video_duration, target_duration=30, model="claude-sonnet-4-6"): def analyze_with_claude(transcript, video_duration, target_duration=30, model="claude-sonnet-4-6", filename_hint=None):
"""Pošlje transkript Claude API-ju (Anthropic). """Pošlje transkript Claude API-ju (Anthropic).
model: claude-sonnet-4-6 (default), claude-haiku-4-5-20251001, claude-opus-4-7 model: claude-sonnet-4-6 (default), claude-haiku-4-5-20251001, claude-opus-4-7
filename_hint: ime datoteke (Claude lahko prepozna pesem in popravi halucinacije)
""" """
api_key = os.environ.get("ANTHROPIC_API_KEY") api_key = os.environ.get("ANTHROPIC_API_KEY")
if not api_key: if not api_key:
@ -535,7 +561,7 @@ def analyze_with_claude(transcript, video_duration, target_duration=30, model="c
if not transcript.get("segments"): if not transcript.get("segments"):
return None return None
prompt = _build_analysis_prompt(transcript, video_duration, target_duration) prompt = _build_analysis_prompt(transcript, video_duration, target_duration, filename_hint=filename_hint)
try: try:
import urllib.request import urllib.request
@ -600,7 +626,7 @@ def analyze_with_claude(transcript, video_duration, target_duration=30, model="c
return None return None
def analyze_with_gemini(transcript, video_duration, target_duration=30, model="gemini-3.1-pro-preview"): def analyze_with_gemini(transcript, video_duration, target_duration=30, model="gemini-3.1-pro-preview", filename_hint=None):
"""Pošlje transkript Gemini API-ju (Google). """Pošlje transkript Gemini API-ju (Google).
Gemini 3.1 Pro ima najboljši multilingual rezultat (MMMLU 92.6%) odličen za SLO/HR/BS. Gemini 3.1 Pro ima najboljši multilingual rezultat (MMMLU 92.6%) odličen za SLO/HR/BS.
@ -613,7 +639,7 @@ def analyze_with_gemini(transcript, video_duration, target_duration=30, model="g
if not transcript.get("segments"): if not transcript.get("segments"):
return None return None
prompt = _build_analysis_prompt(transcript, video_duration, target_duration) prompt = _build_analysis_prompt(transcript, video_duration, target_duration, filename_hint=filename_hint)
try: try:
import urllib.request import urllib.request
@ -705,23 +731,23 @@ def analyze_with_gemini(transcript, video_duration, target_duration=30, model="g
return None return None
def analyze_with_llm(transcript, video_duration, target_duration=30, provider="claude", llm_model=None): def analyze_with_llm(transcript, video_duration, target_duration=30, provider="claude", llm_model=None, filename_hint=None):
"""Glavna funkcija — uporabi izbrano LLM (claude/gemini/auto).""" """Glavna funkcija — uporabi izbrano LLM (claude/gemini/auto)."""
if provider == "gemini": if provider == "gemini":
model = llm_model or "gemini-3.1-pro-preview" model = llm_model or "gemini-3.1-pro-preview"
return analyze_with_gemini(transcript, video_duration, target_duration, model) return analyze_with_gemini(transcript, video_duration, target_duration, model, filename_hint=filename_hint)
elif provider == "claude": elif provider == "claude":
model = llm_model or "claude-sonnet-4-6" model = llm_model or "claude-sonnet-4-6"
return analyze_with_claude(transcript, video_duration, target_duration, model) return analyze_with_claude(transcript, video_duration, target_duration, model, filename_hint=filename_hint)
elif provider == "auto": elif provider == "auto":
# Najprej probaj Claude, fallback na Gemini # Najprej probaj Claude, fallback na Gemini
result = analyze_with_claude(transcript, video_duration, target_duration, result = analyze_with_claude(transcript, video_duration, target_duration,
llm_model or "claude-sonnet-4-6") llm_model or "claude-sonnet-4-6", filename_hint=filename_hint)
if result: if result:
return result return result
print(" 🔄 Claude ni uspel, probam Gemini...", file=sys.stderr) print(" 🔄 Claude ni uspel, probam Gemini...", file=sys.stderr)
return analyze_with_gemini(transcript, video_duration, target_duration, return analyze_with_gemini(transcript, video_duration, target_duration,
llm_model or "gemini-3.1-pro-preview") llm_model or "gemini-3.1-pro-preview", filename_hint=filename_hint)
else: else:
print(f" ⚠️ Neznan LLM provider: {provider}", file=sys.stderr) print(f" ⚠️ Neznan LLM provider: {provider}", file=sys.stderr)
return None return None
@ -760,6 +786,8 @@ def main():
help="Kateri LLM uporabiti za analizo (default: claude)") help="Kateri LLM uporabiti za analizo (default: claude)")
ap.add_argument("--llm-model", default=None, ap.add_argument("--llm-model", default=None,
help="Specifičen model (npr. claude-sonnet-4-6, gemini-3.1-pro-preview)") help="Specifičen model (npr. claude-sonnet-4-6, gemini-3.1-pro-preview)")
ap.add_argument("--filename-hint", default=None,
help="Originalno ime datoteke (Claude lahko prepozna pesem)")
ap.add_argument("--json", action="store_true", help="Output JSON") ap.add_argument("--json", action="store_true", help="Output JSON")
ap.add_argument("--output", help="Path za JSON output") ap.add_argument("--output", help="Path za JSON output")
args = ap.parse_args() args = ap.parse_args()
@ -795,9 +823,12 @@ def main():
if not instrumental and not args.no_claude: if not instrumental and not args.no_claude:
provider = args.llm_provider provider = args.llm_provider
print(f"🤖 Pošiljam transkript {provider}-u za analizo...", file=sys.stderr) print(f"🤖 Pošiljam transkript {provider}-u za analizo...", file=sys.stderr)
# Filename hint = original filename brez extension (Claude lahko prepozna pesem)
fname_hint = args.filename_hint or video.stem
claude_result = analyze_with_llm( claude_result = analyze_with_llm(
transcript, duration, target_duration=args.target_duration, transcript, duration, target_duration=args.target_duration,
provider=provider, llm_model=args.llm_model, provider=provider, llm_model=args.llm_model,
filename_hint=fname_hint,
) )
# 5b. Find chorus lokalno (kot fallback ali za score-jev preview) # 5b. Find chorus lokalno (kot fallback ali za score-jev preview)