Spaces:
Runtime error
Runtime error
deploy live demo
Browse files- migrations/009_gen_note.sql +6 -0
- pipeline/flow.py +1 -1
- pipeline/veo.py +16 -3
- pipeline/webapp/app.py +3 -2
- pipeline/webapp/templates/video.html +5 -3
migrations/009_gen_note.sql
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- 009_gen_note.sql — a transient, human-readable generation status note surfaced
|
| 2 |
+
-- live in the approval UI (e.g. "Veo blockade klippet — byter seed (försök 2/3)…").
|
| 3 |
+
-- Cleared at the start of each generation and when a segment finally succeeds. It is
|
| 4 |
+
-- NOT a creative attribute and never feeds the strategist (kept out of videos.tags).
|
| 5 |
+
|
| 6 |
+
alter table videos add column if not exists gen_note text;
|
pipeline/flow.py
CHANGED
|
@@ -136,7 +136,7 @@ def generate_pending(video_id: str, kinds: tuple[str, ...] | None = None,
|
|
| 136 |
raise RuntimeError(f"video {video_id} has no segments to generate")
|
| 137 |
|
| 138 |
generated: list[str] = []
|
| 139 |
-
db.execute("update videos set status = 'generating', error = null where id = %s", (video_id,))
|
| 140 |
for seg in rows:
|
| 141 |
done = (seg["file_path"] and seg["status"] != "qc_fail"
|
| 142 |
and (storage.is_remote(seg["file_path"]) or Path(seg["file_path"]).exists()))
|
|
|
|
| 136 |
raise RuntimeError(f"video {video_id} has no segments to generate")
|
| 137 |
|
| 138 |
generated: list[str] = []
|
| 139 |
+
db.execute("update videos set status = 'generating', error = null, gen_note = null where id = %s", (video_id,))
|
| 140 |
for seg in rows:
|
| 141 |
done = (seg["file_path"] and seg["status"] != "qc_fail"
|
| 142 |
and (storage.is_remote(seg["file_path"]) or Path(seg["file_path"]).exists()))
|
pipeline/veo.py
CHANGED
|
@@ -361,12 +361,22 @@ def _rephrase_segment_line(segment_id: str) -> str | None:
|
|
| 361 |
return new_text
|
| 362 |
|
| 363 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
def _handle_safety_block(segment_id: str, first_exc: VeoSafetyBlock) -> str:
|
| 365 |
"""On a Veo safety block (§12.5): Veo's RAI filter is non-deterministic and usually
|
| 366 |
trips on the rendered VIDEO, not the words — so we retry on a **fresh seed** (the words
|
| 367 |
and startframe stay identical), looping up to VEO_RAI_MAX_RETRIES times. A new seed
|
| 368 |
-
almost always escapes the false positive.
|
| 369 |
-
|
|
|
|
| 370 |
s = get_settings()
|
| 371 |
if not s.veo_rai_autoretry:
|
| 372 |
raise first_exc
|
|
@@ -374,6 +384,8 @@ def _handle_safety_block(segment_id: str, first_exc: VeoSafetyBlock) -> str:
|
|
| 374 |
for attempt in range(1, max(1, s.veo_rai_max_retries) + 1):
|
| 375 |
new_seed = duration.random_seed()
|
| 376 |
db.execute("update segments set seed = %s where id = %s", (new_seed, segment_id))
|
|
|
|
|
|
|
| 377 |
print(f"[RAI] segment {segment_id}: safety block — auto-retry {attempt}/{s.veo_rai_max_retries} "
|
| 378 |
f"on a fresh seed {new_seed} (same words + startframe).")
|
| 379 |
op_name = generate_segment(segment_id)
|
|
@@ -383,7 +395,7 @@ def _handle_safety_block(segment_id: str, first_exc: VeoSafetyBlock) -> str:
|
|
| 383 |
last_exc = e2
|
| 384 |
continue
|
| 385 |
print(f"[RAI] segment {segment_id}: still blocked after {s.veo_rai_max_retries} fresh seeds — surfacing "
|
| 386 |
-
f"(
|
| 387 |
raise last_exc
|
| 388 |
|
| 389 |
|
|
@@ -410,4 +422,5 @@ def generate_and_download(segment_id: str) -> str:
|
|
| 410 |
path = _handle_safety_block(segment_id, exc)
|
| 411 |
file_path = _maybe_upload_clip(segment_id, path)
|
| 412 |
db.execute("update segments set file_path = %s where id = %s", (file_path, segment_id))
|
|
|
|
| 413 |
return file_path
|
|
|
|
| 361 |
return new_text
|
| 362 |
|
| 363 |
|
| 364 |
+
def _set_gen_note(segment_id: str, note: str | None) -> None:
|
| 365 |
+
"""Surface a transient generation status to the UI (e.g. RAI seed-swap progress).
|
| 366 |
+
Stored on videos.gen_note — not a creative attribute, never in videos.tags."""
|
| 367 |
+
db.execute(
|
| 368 |
+
"update videos set gen_note = %s where id = (select video_id from segments where id = %s)",
|
| 369 |
+
(note, str(segment_id)),
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
|
| 373 |
def _handle_safety_block(segment_id: str, first_exc: VeoSafetyBlock) -> str:
|
| 374 |
"""On a Veo safety block (§12.5): Veo's RAI filter is non-deterministic and usually
|
| 375 |
trips on the rendered VIDEO, not the words — so we retry on a **fresh seed** (the words
|
| 376 |
and startframe stay identical), looping up to VEO_RAI_MAX_RETRIES times. A new seed
|
| 377 |
+
almost always escapes the false positive. The retry runs **automatically**; its progress
|
| 378 |
+
is surfaced live via videos.gen_note. Only when the loop is exhausted does it surface as
|
| 379 |
+
failed — and only then does the UI show a 'Försök igen' button."""
|
| 380 |
s = get_settings()
|
| 381 |
if not s.veo_rai_autoretry:
|
| 382 |
raise first_exc
|
|
|
|
| 384 |
for attempt in range(1, max(1, s.veo_rai_max_retries) + 1):
|
| 385 |
new_seed = duration.random_seed()
|
| 386 |
db.execute("update segments set seed = %s where id = %s", (new_seed, segment_id))
|
| 387 |
+
_set_gen_note(segment_id,
|
| 388 |
+
f"Veo blockade klippet — byter seed (försök {attempt}/{s.veo_rai_max_retries})…")
|
| 389 |
print(f"[RAI] segment {segment_id}: safety block — auto-retry {attempt}/{s.veo_rai_max_retries} "
|
| 390 |
f"on a fresh seed {new_seed} (same words + startframe).")
|
| 391 |
op_name = generate_segment(segment_id)
|
|
|
|
| 395 |
last_exc = e2
|
| 396 |
continue
|
| 397 |
print(f"[RAI] segment {segment_id}: still blocked after {s.veo_rai_max_retries} fresh seeds — surfacing "
|
| 398 |
+
f"(the UI now shows 'Försök igen' for a fresh {s.veo_rai_max_retries}-retry loop).")
|
| 399 |
raise last_exc
|
| 400 |
|
| 401 |
|
|
|
|
| 422 |
path = _handle_safety_block(segment_id, exc)
|
| 423 |
file_path = _maybe_upload_clip(segment_id, path)
|
| 424 |
db.execute("update segments set file_path = %s where id = %s", (file_path, segment_id))
|
| 425 |
+
_set_gen_note(segment_id, None) # clear any RAI seed-swap note — this segment is done
|
| 426 |
return file_path
|
pipeline/webapp/app.py
CHANGED
|
@@ -185,7 +185,7 @@ def _video_view(video_id: str) -> dict[str, Any] | None:
|
|
| 185 |
def _set_failed(video_id: str, exc: Exception) -> None:
|
| 186 |
msg = f"{type(exc).__name__}: {exc}"
|
| 187 |
try:
|
| 188 |
-
db.execute("update videos set status = 'failed', error = %s where id = %s",
|
| 189 |
(msg[:2000], video_id))
|
| 190 |
except Exception:
|
| 191 |
pass
|
|
@@ -353,7 +353,8 @@ def video_status(video_id: str):
|
|
| 353 |
v = view["video"]
|
| 354 |
return JSONResponse({
|
| 355 |
"id": video_id, "stage": view["stage"], "status": v["status"], "seed": v["seed"],
|
| 356 |
-
"error": v.get("error"), "
|
|
|
|
| 357 |
"progress": view["progress"],
|
| 358 |
"segments": [
|
| 359 |
{"idx": s["idx"], "take": s["take"], "kind": s["kind"],
|
|
|
|
| 185 |
def _set_failed(video_id: str, exc: Exception) -> None:
|
| 186 |
msg = f"{type(exc).__name__}: {exc}"
|
| 187 |
try:
|
| 188 |
+
db.execute("update videos set status = 'failed', error = %s, gen_note = null where id = %s",
|
| 189 |
(msg[:2000], video_id))
|
| 190 |
except Exception:
|
| 191 |
pass
|
|
|
|
| 353 |
v = view["video"]
|
| 354 |
return JSONResponse({
|
| 355 |
"id": video_id, "stage": view["stage"], "status": v["status"], "seed": v["seed"],
|
| 356 |
+
"error": v.get("error"), "gen_note": v.get("gen_note"),
|
| 357 |
+
"inflight": view["inflight"], "final_url": view["final_url"],
|
| 358 |
"progress": view["progress"],
|
| 359 |
"segments": [
|
| 360 |
{"idx": s["idx"], "take": s["take"], "kind": s["kind"],
|
pipeline/webapp/templates/video.html
CHANGED
|
@@ -52,6 +52,7 @@
|
|
| 52 |
<form method="post" action="/videos/{{ video.id }}/generate-hooks"><button class="btn">Generera hook-tagningar</button></form>
|
| 53 |
{% elif stage == "generating_hooks" %}
|
| 54 |
<p class="muted" style="margin-top:12px">Genererar {{ hooks|length }} tagningar… sidan uppdaterar sig själv.</p>
|
|
|
|
| 55 |
{% elif stage == "pick_seed" %}
|
| 56 |
<p class="muted" style="margin-top:12px">Samma replik, två seeds. Välj den där avataren ser bäst ut — den låses för hela videon.</p>
|
| 57 |
<div class="pickgrid">
|
|
@@ -79,6 +80,7 @@
|
|
| 79 |
<form method="post" action="/videos/{{ video.id }}/finish" style="margin-top:12px"><button class="btn">Generera resten & klipp ihop</button></form>
|
| 80 |
{% elif stage in ("generating_body","assembling") %}
|
| 81 |
<p class="muted" style="margin-top:12px">{{ progress.done }}/{{ progress.total }} klipp klara. Sidan uppdaterar sig själv.</p>
|
|
|
|
| 82 |
{% elif stage == "need_assemble" %}
|
| 83 |
<form method="post" action="/videos/{{ video.id }}/assemble" style="margin-top:12px"><button class="btn">Klipp ihop videon</button></form>
|
| 84 |
{% elif stage == "done" %}
|
|
@@ -119,11 +121,11 @@
|
|
| 119 |
</div>
|
| 120 |
|
| 121 |
<script>
|
| 122 |
-
const STAGE={{ stage|tojson }}, VID={{ video.id|tojson }}, DONE={{ progress.done }};
|
| 123 |
if(["generating_hooks","generating_body","assembling"].includes(STAGE)){
|
| 124 |
setInterval(async()=>{try{const d=await(await fetch(`/videos/${VID}/status`)).json();
|
| 125 |
-
// reload on
|
| 126 |
-
if(d.stage!==STAGE || (d.progress && d.progress.done!==DONE)) location.reload();
|
| 127 |
}catch(e){}},2000);
|
| 128 |
}
|
| 129 |
</script>
|
|
|
|
| 52 |
<form method="post" action="/videos/{{ video.id }}/generate-hooks"><button class="btn">Generera hook-tagningar</button></form>
|
| 53 |
{% elif stage == "generating_hooks" %}
|
| 54 |
<p class="muted" style="margin-top:12px">Genererar {{ hooks|length }} tagningar… sidan uppdaterar sig själv.</p>
|
| 55 |
+
{% if video.gen_note %}<p class="pill warn" style="margin-top:10px;display:inline-block">⟳ {{ video.gen_note }}</p>{% endif %}
|
| 56 |
{% elif stage == "pick_seed" %}
|
| 57 |
<p class="muted" style="margin-top:12px">Samma replik, två seeds. Välj den där avataren ser bäst ut — den låses för hela videon.</p>
|
| 58 |
<div class="pickgrid">
|
|
|
|
| 80 |
<form method="post" action="/videos/{{ video.id }}/finish" style="margin-top:12px"><button class="btn">Generera resten & klipp ihop</button></form>
|
| 81 |
{% elif stage in ("generating_body","assembling") %}
|
| 82 |
<p class="muted" style="margin-top:12px">{{ progress.done }}/{{ progress.total }} klipp klara. Sidan uppdaterar sig själv.</p>
|
| 83 |
+
{% if video.gen_note %}<p class="pill warn" style="margin-top:10px;display:inline-block">⟳ {{ video.gen_note }}</p>{% endif %}
|
| 84 |
{% elif stage == "need_assemble" %}
|
| 85 |
<form method="post" action="/videos/{{ video.id }}/assemble" style="margin-top:12px"><button class="btn">Klipp ihop videon</button></form>
|
| 86 |
{% elif stage == "done" %}
|
|
|
|
| 121 |
</div>
|
| 122 |
|
| 123 |
<script>
|
| 124 |
+
const STAGE={{ stage|tojson }}, VID={{ video.id|tojson }}, DONE={{ progress.done }}, NOTE={{ (video.gen_note or '')|tojson }};
|
| 125 |
if(["generating_hooks","generating_body","assembling"].includes(STAGE)){
|
| 126 |
setInterval(async()=>{try{const d=await(await fetch(`/videos/${VID}/status`)).json();
|
| 127 |
+
// reload on stage change, a finished clip, or a new retry note (live seed-swap status)
|
| 128 |
+
if(d.stage!==STAGE || (d.progress && d.progress.done!==DONE) || ((d.gen_note||'')!==NOTE)) location.reload();
|
| 129 |
}catch(e){}},2000);
|
| 130 |
}
|
| 131 |
</script>
|