diff --git a/app/main.py b/app/main.py index d0928e0..995e342 100644 --- a/app/main.py +++ b/app/main.py @@ -2435,6 +2435,86 @@ async def recut_job(job_id: str, payload: RecutRequest, user: str = Depends(chec } +# ──────────────────────────────────────────────────────────────── +# Retranscribe — ponovi STT z drugim providerjem (npr. Soniox→Scribe) +# Ohrani isti clip range, samo regenerira transkript + podnapise + render. +# ──────────────────────────────────────────────────────────────── +class RetranscribeRequest(BaseModel): + provider: str = "elevenlabs" # 'elevenlabs' (Scribe) | 'local' (faster-whisper) | 'soniox' | 'auto' + lang: Optional[str] = None # ostane original če None + auto_upload: bool = False # po končanem renderu naloži v Nextcloud (overwrites obstoječ) + + +@app.post("/api/jobs/{job_id}/retranscribe") +async def retranscribe_job(job_id: str, payload: RetranscribeRequest, user: str = Depends(check_auth)): + """Ponovi transkripcijo z drugim STT providerjem in re-renderaj. + + Ohrani isti clip range (start/end iz obstoječe analize) — uporabnik + ne potrebuje ponovno pozicionirati IN/OUT. Samo besedilo se popravi. + + Pogosto: Soniox je narobe slišal v slovenskem folkloru, gremo na Scribe. + """ + job = load_job(job_id) + if not job: + raise HTTPException(404, "Ne obstaja") + if job.get("status") in ("queued", "processing", "downloading"): + raise HTTPException(409, "Job je že v obdelavi") + + # Validate provider + valid_providers = {"elevenlabs", "local", "soniox", "auto"} + if payload.provider not in valid_providers: + raise HTTPException(400, f"Neveljaven provider. Dovoljen: {sorted(valid_providers)}") + + # Original input mora obstajati (lokalno ali v S3) + src = job.get("input_path") + if not src: + raise HTTPException(400, "Original video manjka") + _ensure_local(src, "upload") + if not Path(src).exists(): + raise HTTPException(400, "Original video ne obstaja niti lokalno niti v S3") + + # Briši stari output + analysis (ker bo regen) + out_mp4 = OUTPUT_DIR / f"{job_id}.mp4" + if out_mp4.exists(): + out_mp4.unlink() + _delete_from_s3(out_mp4.name, "output") + for ext in ("srt", "ass"): + p = OUTPUT_DIR / f"{job_id}.subtitles.{ext}" + if p.exists(): + p.unlink() + _delete_from_s3(p.name, "output") + # Briši tudi analysis.json — zaženemo svežo analizo + analysis_path = OUTPUT_DIR / f"{job_id}.analysis.json" + if analysis_path.exists(): + analysis_path.unlink() + _delete_from_s3(analysis_path.name, "output") + + # Re-queue job — pomembno: NE postavi custom_clip=True (pustimo polno re-analizo) + # whisper_provider override poskrbi, da bo tokrat drug STT + updates = { + "status": "queued", + "current_step": f"V vrsti za retranscribe ({payload.provider})", + "whisper_provider": payload.provider, + "auto_upload_to_nextcloud": payload.auto_upload, + "hidden_after_upload": False, + "nextcloud_status": "retranscribing", + "error": None, + "chorus_error": None, + "custom_clip": False, + "retranscribe_count": (job.get("retranscribe_count", 0) or 0) + 1, + } + if payload.lang and payload.lang not in ("auto", ""): + updates["lang"] = payload.lang + update_job(job_id, **updates) + + return { + "status": "queued", + "job_id": job_id, + "provider": payload.provider, + "retranscribe_count": updates["retranscribe_count"], + } + + # ──────────────────────────────────────────────────────────────── # Nextcloud upload (folxspeed/REELS/) # ──────────────────────────────────────────────────────────────── diff --git a/templates/index.html b/templates/index.html index 3e05d8a..12dde51 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1365,6 +1365,51 @@ refreshJobs(); } + async function retranscribeJob(id) { + const sel = document.getElementById("retranscribe-provider"); + const provider = sel ? sel.value : "elevenlabs"; + const providerLabels = { + elevenlabs: "Scribe (ElevenLabs)", + soniox: "Soniox", + local: "Whisper local", + }; + const lbl = providerLabels[provider] || provider; + if (!confirm(`Ponovim transkript z "${lbl}"?\n\nObstoječi izrez (start/end) ostane enak. Trenutni MP4 + podnapisi se zbrišejo in regenerirajo.`)) return; + const btn = document.getElementById("retranscribe-btn"); + const status = document.getElementById("edit-status"); + if (btn) { btn.disabled = true; btn.textContent = "⏳ V vrsti…"; } + if (status) { status.textContent = `Pošiljam zahtevo (${lbl})…`; status.style.color = "#ffd700"; } + try { + const r = await fetch(`/api/jobs/${id}/retranscribe`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider, auto_upload: false }), + }); + if (!r.ok) { + const err = await r.json().catch(() => ({})); + throw new Error(err.detail || `HTTP ${r.status}`); + } + const data = await r.json(); + if (status) { + status.textContent = `✅ Job dodan v vrsto (poskus #${data.retranscribe_count}). Modal lahko zapreš — napredek bo viden v listi.`; + status.style.color = "#4ade80"; + } + // Auto-close modal po 1.5s + refresh + setTimeout(() => { + closeModal(); + refreshJobs(); + // Watch job + if (typeof watchJob === "function") watchJob(id); + }, 1500); + } catch (e) { + if (status) { + status.textContent = `❌ Napaka: ${e.message}`; + status.style.color = "#ff6b6b"; + } + if (btn) { btn.disabled = false; btn.textContent = "🔁 Ponovi transkript"; } + } + } + // ─── EDIT MODAL ───────────────────────────────────── function formatTime(sec) { if (!isFinite(sec) || sec < 0) sec = 0; @@ -1509,8 +1554,16 @@
-