Fix 'Žena' word still cut: word-level start extension instead of segment-level

Previous fix used segment boundaries — required segments <3s for type 1
or <4s for type 2. But Žena was in a 4.3s segment ('saj še doma mi več
noč'jo verjet'. Žena me'), so the condition wasn't met and clip start
stayed at 77.7s, exactly at end of word 'Žena' (76.88-77.70s).

New approach: scan word-level timestamps directly:

1. If clip start falls MID-WORD → extend back to word start - 0.15s
2. If a word ends 0-0.5s BEFORE clip start AND next word is at clip start
   → that word is suspect (may be first word of chorus that Scribe put
   in previous segment), extend back to its start - 0.15s

Word-level timestamps are always available from Scribe (timestamps_granularity=word).
Falls back to segment-level for local Whisper without word timing.

This handles arbitrary segment lengths and is universal — works for any
language where the chorus starts on a word that the STT placed in the
previous segment.
This commit is contained in:
2026-04-29 15:04:18 +00:00
parent a5097c5acc
commit 157e6b781e
2 changed files with 103 additions and 35 deletions
+56 -33
View File
@@ -1363,42 +1363,65 @@ def main():
#
# Strategija: če clip start pade SREDI segmenta (ne tik na začetku),
# razširi nazaj na začetek tega segmenta + 0.2s buffer.
# ── EXTEND clip START nazaj če Claude začne sredi besede ali tik za njo ──
# Pesem se pogosto začne na isti besedi v transkriptu, ampak Scribe lahko
# zazna mejo med segmenti **PO** prvi besedi (npr. "Žena me tepe" — "Žena"
# je v prejšnjem segmentu pri 76.88-77.70s, novi segment začne 78.30).
# Claude reže tipično na začetku novega segmenta = odrezana prva beseda.
#
# Strategija: **na ravni besed** — najdi besedo katere konec je
# blizu clip start (±0.5s) IN preveri ali se lahko ta beseda
# "naslanja" na clip (z malo pavze do naslednje besede).
current_start = clip_range["start"]
# Zberi VSE besede z njihovimi timestampi
all_words = []
for seg in corrected_segs:
seg_start = float(seg.get("start", 0))
seg_end = float(seg.get("end", 0))
# Segment ki se prekriva s clip start (start je MED njim)
if seg_start < current_start < seg_end:
# Razširi nazaj — vendar samo če je segment kratek (<3s) in
# se "naslanja" na clip (zadnja beseda lahko vodi v refren)
if (seg_end - seg_start) < 3.0:
new_start = max(0, seg_start - 0.2) # 0.2s buffer pred prvo besedo
if new_start < current_start:
print(f" 🎵 Razširim clip začetek {current_start:.1f}s → {new_start:.1f}s "
f"(prva beseda refrena je v prejšnjem segmentu)", file=sys.stderr)
for w in seg.get("words", []):
if w.get("start") is not None and w.get("end") is not None:
all_words.append({
"start": float(w["start"]),
"end": float(w["end"]),
"text": w.get("text", ""),
})
if all_words:
# Najdi "rob" — beseda kjer končanje zelo blizu clip start
# ALI clip start je sredi besede (besedo bi odrezali)
for i, w in enumerate(all_words):
# Beseda zaobsega clip start (clip reže sredi besede)
if w["start"] < current_start < w["end"]:
new_start = max(0, w["start"] - 0.15)
print(f" 🎵 Razširim clip začetek {current_start:.2f}s → {new_start:.2f}s "
f"(clip rezal sredi besede '{w['text'].strip()}')", file=sys.stderr)
current_start = new_start
break
# Beseda končana TIK pred clip start (do 0.5s pred)
# IN je naslednja beseda PO/blizu clip start
if 0 < (current_start - w["end"]) <= 0.5:
# Preveri naslednjo besedo
next_w = all_words[i + 1] if i + 1 < len(all_words) else None
if next_w and next_w["start"] >= current_start - 0.1:
# Razdalja od te besede do naslednje > 0.3s pomeni mogoče prelom verz/refren
gap_to_next = next_w["start"] - w["end"]
# Razširi nazaj na začetek te besede - 0.15s buffer
new_start = max(0, w["start"] - 0.15)
print(f" 🎵 Razširim clip začetek {current_start:.2f}s → {new_start:.2f}s "
f"(beseda '{w['text'].strip()}' končana {current_start - w['end']:.2f}s pred clip start, "
f"morda začne refren; gap do '{next_w['text'].strip()}' = {gap_to_next:.2f}s)", file=sys.stderr)
current_start = new_start
break
# Segment ki se konča TOČNO ali tik pred clip start (lahko zadnja
# beseda refrena = "Žena" se konča na 78.2 ko clip začne 78.3)
elif current_start - 0.5 <= seg_end <= current_start + 0.1:
# Preveri ali zadnja beseda v segmentu morda **začne refren**
seg_text = seg.get("text", "").strip()
# Če segment kaže novo frazo (z veliko začetnico po pavzi) ali
# vsebuje znake interpunkcije, morda zadnja beseda res začne refren
if seg_text and (seg_end - seg_start) < 4.0:
# Razširi nazaj na začetek POSLEDJE besede tega segmenta
words = seg.get("words", [])
if words:
last_word = words[-1]
new_start = max(0, float(last_word.get("start", seg_start)) - 0.15)
else:
# Brez word-level: vzemi 0.5s nazaj
new_start = max(0, current_start - 0.5)
if new_start < current_start:
print(f" 🎵 Razširim clip začetek {current_start:.1f}s → {new_start:.1f}s "
f"(zadnja beseda prejšnjega segmenta morda začne refren)", file=sys.stderr)
current_start = new_start
break
break
else:
# Fallback: če ni word-level (npr. local Whisper), uporabi segmente kot prej
for seg in corrected_segs:
seg_start = float(seg.get("start", 0))
seg_end = float(seg.get("end", 0))
if seg_start < current_start < seg_end:
new_start = max(0, current_start - 0.5)
print(f" 🎵 Razširim clip začetek {current_start:.2f}s → {new_start:.2f}s "
f"(brez word-level, fallback -0.5s)", file=sys.stderr)
current_start = new_start
break
if current_start < clip_range["start"]:
clip_range["start"] = round(current_start, 2)