Retranscribe feature: ponovi STT z drugim providerjem v Edit modalu
PROBLEM: STT (Soniox/Scribe) včasih popolnoma narobe slišije besedilo —
npr. 'BILA JE LJUBEZEN PRVA' postane 'BILAL JO ME ZANPRLA' (TRIO ŠUBIC).
Edit modal je do zdaj zahteval popoln recut z novim clip range.
NEW: '🔁 Ponovi transkript' gumb v Edit modalu z dropdown za provider:
- Scribe (ElevenLabs) — default, najboljši za nemščino + slovenščino
- Soniox — slovenski default v auto routing
- Whisper local (faster-whisper)
Backend:
POST /api/jobs/{id}/retranscribe { provider, auto_upload?, lang? }
- Briše stari MP4 + analysis.json + .srt + .ass (lokal + S3)
- Re-queue job z whisper_provider override
- Ohrani isti clip range — analyze.py si naredi svežo analizo
- retranscribe_count števec za sledenje poskusov
UX: confirm dialog → 1.5s status → auto-close modal → watchJob za live progress.
Brez auto_upload v Nextcloud — preveri rezultat preden re-uploadaš.
This commit is contained in:
parent
2abd9daae1
commit
79f611ba73
80
app/main.py
80
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/)
|
# Nextcloud upload (folxspeed/REELS/)
|
||||||
# ────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -1365,6 +1365,51 @@
|
|||||||
refreshJobs();
|
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 ─────────────────────────────────────
|
// ─── EDIT MODAL ─────────────────────────────────────
|
||||||
function formatTime(sec) {
|
function formatTime(sec) {
|
||||||
if (!isFinite(sec) || sec < 0) sec = 0;
|
if (!isFinite(sec) || sec < 0) sec = 0;
|
||||||
@ -1509,8 +1554,16 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="preview-status" style="margin-top:6px; font-size:12px; color:var(--muted); text-align:right;"></div>
|
<div id="preview-status" style="margin-top:6px; font-size:12px; color:var(--muted); text-align:right;"></div>
|
||||||
|
|
||||||
<div class="modal-actions" style="margin-top:18px;">
|
<div class="modal-actions" style="margin-top:18px; display:flex; gap:8px; flex-wrap:wrap;">
|
||||||
<button class="primary" id="edit-save-btn">✅ Shrani in re-render</button>
|
<button class="primary" id="edit-save-btn">✅ Shrani in re-render</button>
|
||||||
|
<div style="display:flex; gap:6px; align-items:center; margin-left:auto;">
|
||||||
|
<select id="retranscribe-provider" style="padding:6px 8px; font-size:12px; background:var(--panel); color:var(--text); border:1px solid #444; border-radius:4px;" title="STT provider za ponovni transkript">
|
||||||
|
<option value="elevenlabs">Scribe (ElevenLabs)</option>
|
||||||
|
<option value="soniox">Soniox</option>
|
||||||
|
<option value="local">Whisper (lokalno)</option>
|
||||||
|
</select>
|
||||||
|
<button class="small ghost" id="retranscribe-btn" onclick="retranscribeJob('${jobId}')" title="Ponovi STT z izbranim providerjem in re-renderaj. Ohrani isti izrez." style="border-color:#ffd700; color:#ffd700;">🔁 Ponovi transkript</button>
|
||||||
|
</div>
|
||||||
<button onclick="closeModal()">Prekliči</button>
|
<button onclick="closeModal()">Prekliči</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="edit-status" style="margin-top:10px; font-size:12px; color:var(--muted);"></div>
|
<div id="edit-status" style="margin-top:10px; font-size:12px; color:var(--muted);"></div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user