Major: Claude post-processing of Whisper transcript
- Claude now corrects transcription errors (Slavic languages, dialects, mixed langs) - Returns corrected_segments with same timestamps but cleaner text - Pipeline generates SRT from Claude-corrected transcript and passes to subtitle.py via --srt - subtitle.py supports --srt to skip Whisper re-transcription on the trimmed clip - clip.py propagates --srt through to subtitle.py - Whisper still runs once (in analyze.py); subtitle.py reuses corrected output instead of re-running - This means: Whisper's mistakes (mixed langs, hallucinations, wrong words) are fixed by Claude before becoming visible subtitles
This commit is contained in:
+91
@@ -111,6 +111,75 @@ def list_jobs():
|
||||
return out
|
||||
|
||||
|
||||
def generate_srt_from_segments(segments, clip_start, clip_end, output_path):
|
||||
"""Generira SRT samo za dele, ki spadajo v [clip_start, clip_end].
|
||||
|
||||
Timestamp-i so re-mapirani na 0-based (kot je v trim-anem videu).
|
||||
Razdeli dolge segmente (>2.5s) na enake kose za hiter pacing v reels stilu.
|
||||
Vse besedilo VELIKE TISKANE ČRKE.
|
||||
"""
|
||||
MAX_CHUNK_DURATION = 2.5
|
||||
|
||||
def fmt_ts(s):
|
||||
h = int(s // 3600)
|
||||
m = int((s % 3600) // 60)
|
||||
sec = s % 60
|
||||
return f"{h:02d}:{m:02d}:{sec:06.3f}".replace(".", ",")
|
||||
|
||||
lines = []
|
||||
idx = 1
|
||||
|
||||
for seg in segments:
|
||||
s_start = float(seg["start"])
|
||||
s_end = float(seg["end"])
|
||||
text = str(seg["text"]).strip()
|
||||
|
||||
# Filter v range
|
||||
if s_end <= clip_start or s_start >= clip_end:
|
||||
continue
|
||||
# Klipni
|
||||
s_start = max(s_start, clip_start)
|
||||
s_end = min(s_end, clip_end)
|
||||
if s_end - s_start < 0.2:
|
||||
continue
|
||||
|
||||
# Re-mapraj na 0-based
|
||||
rel_start = s_start - clip_start
|
||||
rel_end = s_end - clip_start
|
||||
|
||||
if not text:
|
||||
continue
|
||||
text_upper = text.upper()
|
||||
|
||||
# Razdeli na chunk-e če je predolg
|
||||
duration = rel_end - rel_start
|
||||
if duration <= MAX_CHUNK_DURATION:
|
||||
lines.append(f"{idx}\n{fmt_ts(rel_start)} --> {fmt_ts(rel_end)}\n{text_upper}\n")
|
||||
idx += 1
|
||||
else:
|
||||
# Razdeli na N enakih kosov; če ima Whisper word-timing, jih lahko razdelimo bolje,
|
||||
# ampak za zdaj enako razdelimo
|
||||
n_parts = int(duration / MAX_CHUNK_DURATION) + 1
|
||||
words = text_upper.split()
|
||||
words_per_part = max(1, len(words) // n_parts)
|
||||
chunk_dur = duration / n_parts
|
||||
for i in range(n_parts):
|
||||
cs = rel_start + i * chunk_dur
|
||||
ce = rel_start + (i + 1) * chunk_dur
|
||||
# Vzemi pripadajoče besede
|
||||
wstart = i * words_per_part
|
||||
wend = (i + 1) * words_per_part if i < n_parts - 1 else len(words)
|
||||
chunk_text = " ".join(words[wstart:wend]) if wstart < len(words) else text_upper
|
||||
if not chunk_text.strip():
|
||||
chunk_text = text_upper
|
||||
lines.append(f"{idx}\n{fmt_ts(cs)} --> {fmt_ts(ce)}\n{chunk_text.strip()}\n")
|
||||
idx += 1
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
f.write("\n".join(lines))
|
||||
return output_path
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
# Pipeline runner (background task)
|
||||
# ────────────────────────────────────────────────────────────────
|
||||
@@ -174,12 +243,29 @@ def process_job(job_id):
|
||||
cmd += ["--model", job.get("whisper_model", "small")]
|
||||
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
srt_from_claude = None # Pot do SRT iz Claude-popravljenega transcript-a
|
||||
if proc.returncode == 0 and analysis_path.exists():
|
||||
try:
|
||||
with open(analysis_path, "r", encoding="utf-8") as f:
|
||||
analysis = json.load(f)
|
||||
cr = analysis["clip_range"]
|
||||
fade = analysis["fade"]
|
||||
|
||||
# Generiraj SRT iz transcript-a TRIM-ANEGA na clip_range
|
||||
# (Claude je morda popravil besedilo — uporabi popravljeno)
|
||||
if analysis.get("transcript", {}).get("segments"):
|
||||
srt_path_out = OUTPUT_DIR / f"{job_id}.subtitles.srt"
|
||||
try:
|
||||
generate_srt_from_segments(
|
||||
analysis["transcript"]["segments"],
|
||||
cr["start"], cr["end"],
|
||||
srt_path_out,
|
||||
)
|
||||
srt_from_claude = str(srt_path_out)
|
||||
print(f"📝 Generated SRT from Claude transcript: {srt_path_out}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ SRT generation failed: {e}")
|
||||
|
||||
update_job(
|
||||
job_id,
|
||||
analysis_summary={
|
||||
@@ -193,6 +279,7 @@ def process_job(job_id):
|
||||
"video_duration": analysis.get("video_duration"),
|
||||
"candidates": analysis["chorus"].get("all_candidates", [])[:5]
|
||||
if analysis.get("chorus") else [],
|
||||
"claude_corrected_text": analysis.get("claude_corrected_text", False),
|
||||
},
|
||||
# Cel transkript shranimo za UI prikaz
|
||||
full_transcript=[
|
||||
@@ -205,6 +292,7 @@ def process_job(job_id):
|
||||
fade_out=fade["fade_out"],
|
||||
detected_language=analysis["language"],
|
||||
is_instrumental=analysis["instrumental"],
|
||||
claude_srt_path=srt_from_claude,
|
||||
)
|
||||
# Auto-disable subs za instrumental
|
||||
if analysis["instrumental"] and not job.get("no_subs"):
|
||||
@@ -235,6 +323,9 @@ def process_job(job_id):
|
||||
cmd += ["--fade-in", str(job["fade_in"])]
|
||||
if job.get("fade_out", 0) > 0:
|
||||
cmd += ["--fade-out", str(job["fade_out"])]
|
||||
# SRT iz Claude (boljše besedilo) — preda direktno v subtitle.py
|
||||
if job.get("claude_srt_path") and Path(job["claude_srt_path"]).exists() and not job.get("no_subs"):
|
||||
cmd += ["--srt", job["claude_srt_path"]]
|
||||
# lang: prefer detected_language če auto
|
||||
chosen_lang = job.get("lang")
|
||||
if chosen_lang in (None, "auto", ""):
|
||||
|
||||
Reference in New Issue
Block a user