afa67 commited on
Commit
a9439f8
·
verified ·
1 Parent(s): 275fddc

deploy live demo

Browse files
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. Still blocked after that → surface; the UI
369
- 'Försök igen' button restarts a fresh loop."""
 
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"(click 'Försök igen' to start a fresh {s.veo_rai_max_retries}-retry loop).")
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"), "inflight": view["inflight"], "final_url": view["final_url"],
 
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 a stage change OR when another clip finishes (live progress)
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>