Major: smart selection pipeline (analyze.py) + audio fade + multi-lang auto-detect

- New analyze.py: full transcript + energy + structural analysis
- Smart clip range: includes pre-chorus, can exceed 30s up to max_duration (default 45s)
- Audio fade in/out: auto-detected from vocal boundaries
- Instrumental detection: auto-disables subs if vocals < 10% of duration
- Multi-language: auto-detect via Whisper or explicit (DE/SL/HR/BS/SR/EN/IT/ES/FR)
- Frontend: cleaner UX, added bs language, smart selection description
- reframe.py: --fade-in --fade-out args
- clip.py: propagates fade params
- app/main.py: replaces find_chorus.py call with analyze.py
This commit is contained in:
2026-04-29 06:21:35 +00:00
parent 81edd24ca3
commit 8512076b91
5 changed files with 572 additions and 36 deletions
+56 -24
View File
@@ -154,37 +154,57 @@ def process_job(job_id):
else:
input_path = Path(job["input_path"])
# ── 2. Find chorus (če auto) ──────────────────────────
# ── 2. Smart analysis (če auto_chorus) ──────────────────────────
if job.get("auto_chorus"):
update_job(job_id, current_step="Iščem refren (Whisper + energy)")
update_job(job_id, current_step="Analiza pesmi (transkript + energija)")
analysis_path = OUTPUT_DIR / f"{job_id}.analysis.json"
cmd = [
"python3", str(SCRIPTS_DIR / "find_chorus.py"),
"python3", str(SCRIPTS_DIR / "analyze.py"),
str(input_path),
"--duration", str(job.get("duration", 30)),
"--json",
"--target-duration", str(job.get("duration", 30)),
"--max-duration", str(job.get("max_duration", 45)),
"--min-duration", str(job.get("min_duration", 20)),
"--output", str(analysis_path),
]
if job.get("lang"):
# lang: če None ali 'auto', pusti analyze.py auto-detect
if job.get("lang") and job["lang"] not in ("auto", ""):
cmd += ["--lang", job["lang"]]
cmd += ["--model", job.get("whisper_model", "small")]
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode == 0:
if proc.returncode == 0 and analysis_path.exists():
try:
chorus = json.loads(proc.stdout)
if chorus.get("candidates"):
best = chorus["candidates"][0]
update_job(
job_id,
chorus_detection=chorus,
start=best["start"],
duration=best["duration"],
)
# KLJUČNO: reload local job dict, da nove vrednosti pridejo v reframe call
job = load_job(job_id)
except json.JSONDecodeError:
update_job(job_id, chorus_error="JSON decode failed")
with open(analysis_path, "r", encoding="utf-8") as f:
analysis = json.load(f)
cr = analysis["clip_range"]
fade = analysis["fade"]
update_job(
job_id,
analysis_summary={
"language": analysis["language"],
"language_probability": analysis["language_probability"],
"instrumental": analysis["instrumental"],
"clip_range": cr,
"fade": fade,
"chorus_preview": analysis["chorus"]["best"]["text_preview"]
if analysis.get("chorus") and analysis["chorus"].get("best") else None,
},
start=cr["start"],
duration=cr["duration"],
fade_in=fade["fade_in"],
fade_out=fade["fade_out"],
detected_language=analysis["language"],
is_instrumental=analysis["instrumental"],
)
# Auto-disable subs za instrumental
if analysis["instrumental"] and not job.get("no_subs"):
update_job(job_id, no_subs=True, auto_disabled_subs=True)
# Reload local dict
job = load_job(job_id)
except (json.JSONDecodeError, KeyError) as e:
update_job(job_id, chorus_error=f"Analysis parse: {e}")
else:
update_job(job_id, chorus_error=proc.stderr[-300:])
update_job(job_id, chorus_error=(proc.stderr or "")[-500:])
# ── 3. Reframe + subtitles (clip.py orchestrator) ─────
output_path = OUTPUT_DIR / f"{job_id}.mp4"
@@ -201,8 +221,16 @@ def process_job(job_id):
cmd += ["--start", str(job["start"])]
if job.get("duration") is not None:
cmd += ["--duration", str(job["duration"])]
if job.get("lang"):
cmd += ["--lang", job["lang"]]
if job.get("fade_in", 0) > 0:
cmd += ["--fade-in", str(job["fade_in"])]
if job.get("fade_out", 0) > 0:
cmd += ["--fade-out", str(job["fade_out"])]
# lang: prefer detected_language če auto
chosen_lang = job.get("lang")
if chosen_lang in (None, "auto", ""):
chosen_lang = job.get("detected_language")
if chosen_lang:
cmd += ["--lang", chosen_lang]
if job.get("no_subs"):
cmd += ["--no-subs"]
cmd += ["--model", job.get("whisper_model", "small")]
@@ -269,10 +297,12 @@ class YouTubeJobIn(BaseModel):
class StartJobIn(BaseModel):
job_id: str
mode: str = "track"
lang: Optional[str] = None
lang: Optional[str] = None # None/auto = Whisper auto-detect
auto_chorus: bool = True
start: Optional[float] = None
duration: Optional[float] = 30
max_duration: Optional[float] = 45 # Smart selection lahko gre do 45s
min_duration: Optional[float] = 20
no_subs: bool = False
subtitle_style: str = "reels"
whisper_model: str = "small"
@@ -373,6 +403,8 @@ async def start_processing(
auto_chorus=payload.auto_chorus,
start=payload.start,
duration=payload.duration,
max_duration=payload.max_duration,
min_duration=payload.min_duration,
no_subs=payload.no_subs,
subtitle_style=payload.subtitle_style,
whisper_model=payload.whisper_model,