afa67's picture
deploy live demo
a9439f8 verified
Raw
History Blame
17.6 kB
"""Veo 3.1 on Vertex AI β€” EXACTLY as the MVP donor does it (SPEC_NEW Β§11.2).
Generation goes through the raw Vertex `:predictLongRunning` / `:fetchPredictOperation`
REST endpoints (a direct port of the donor's worker `veo.ts`), NOT the google-genai
SDK. This guarantees the request matches the proven MVP path byte-for-byte:
parameters: aspectRatio 9:16 Β· durationSeconds (dynamic 4/6/8) Β· sampleCount 1 Β·
seed (locked) Β· generateAudio true Β· resolution 720p
instances : [{ prompt, image: { bytesBase64Encoded, mimeType } }] ← the startframe
720p on every clip (owner requirement). The seed is passed explicitly in the request
body, so identity continuity can't be silently dropped by an SDK version.
Guarantees (unchanged):
- MAX_GENERATIONS_PER_DAY enforced against generation_log per UTC day (dry-run and
real counted separately).
- DRY_RUN=true never touches Vertex: the fixture clip is copied; the log records
duration/seed/image so Phase-1 acceptance is provable without creds.
- Safety blocks raise VeoSafetyBlock and are handled by the RAI policy (Β§12.5).
"""
from __future__ import annotations
import base64
import json
import re
import shutil
import time
import uuid
from pathlib import Path
from typing import Any
from urllib.parse import quote
import httpx
from . import db, duration
from .config import FIXTURES_DIR, MEDIA_DIR, get_settings
DEFAULT_SEGMENT_SECONDS = 8
POLL_INTERVAL_S = 15
POLL_TIMEOUT_S = 15 * 60
TRANSPORT_RETRIES = 3
SCOPES = ["https://www.googleapis.com/auth/cloud-platform"]
SEGMENTS_DIR = MEDIA_DIR / "segments"
DRY_RUN_OP_PREFIX = "dry-run/"
FIXTURE_CLIP = FIXTURES_DIR / "good_clip.mp4"
class DailyCapExceeded(RuntimeError):
pass
class VeoSafetyBlock(RuntimeError):
"""Safety/RAI filtering. Handled by the RAI policy in generate_and_download."""
def __init__(self, reasons: list[str]):
self.reasons = reasons
super().__init__(f"Veo safety filter blocked the generation: {reasons}")
def _is_rai_message(msg: str | None) -> bool:
"""A Veo operation error that is actually a safety/RAI block (MVP isRaiError)."""
m = (msg or "").lower()
return any(k in m for k in (
"safety", "usage guidelines", "responsible ai", "blocked", "filtered",
"policy", "violat", "prohibited", "rai",
))
# ── Daily cap ────────────────────────────────────────────────────────────────
def generations_today(dry_run: bool) -> int:
row = db.fetch_one(
"""
select count(*) as n from generation_log
where dry_run = %s
and (started_at at time zone 'utc')::date = (now() at time zone 'utc')::date
""",
(dry_run,),
)
return int(row["n"]) if row else 0
def enforce_daily_cap() -> None:
s = get_settings()
used = generations_today(s.dry_run)
if used >= s.max_generations_per_day:
mode = "dry-run" if s.dry_run else "REAL"
raise DailyCapExceeded(
f"MAX_GENERATIONS_PER_DAY reached: {used}/{s.max_generations_per_day} {mode} "
f"generations already submitted today (UTC). The two-take hook spends "
f"SEED_TAKES extra generations per video (SPEC_NEW Β§10)."
)
def _log_generation(segment_id: str, dry_run: bool, model_id: str | None) -> None:
db.execute(
"insert into generation_log (segment_id, dry_run, model_id) values (%s, %s, %s)",
(segment_id, dry_run, model_id),
)
# ── Vertex REST (port of the donor's veo.ts) ─────────────────────────────────
def _vertex_base() -> str:
s = get_settings()
proj = s.require("GOOGLE_CLOUD_PROJECT")
loc = s.require("VERTEX_LOCATION")
model = s.require("VEO_MODEL_ID")
return (
f"https://{loc}-aiplatform.googleapis.com/v1/projects/{proj}"
f"/locations/{loc}/publishers/google/models/{model}"
)
_creds = None
def _access_token() -> str:
"""Mint a Vertex access token from the service-account / ADC credentials at
GOOGLE_APPLICATION_CREDENTIALS (or application-default credentials)."""
global _creds
import google.auth
from google.auth.transport.requests import Request as GoogleAuthRequest
if _creds is None:
s = get_settings()
path = s.google_application_credentials
if path:
if not Path(path).is_file():
raise RuntimeError(
f"GOOGLE_APPLICATION_CREDENTIALS points to a missing file: {path!r}."
)
_creds, _ = google.auth.load_credentials_from_file(path, scopes=SCOPES)
else:
_creds, _ = google.auth.default(scopes=SCOPES)
if not _creds.valid:
_creds.refresh(GoogleAuthRequest())
return _creds.token
def _auth_headers() -> dict[str, str]:
s = get_settings()
return {
"Authorization": f"Bearer {_access_token()}",
"x-goog-user-project": s.require("GOOGLE_CLOUD_PROJECT"),
"content-type": "application/json",
}
def _has_startframe(ref: str | None) -> bool:
if not ref:
return False
return ref.startswith(("http://", "https://")) or Path(ref).is_file()
def _image_inline(ref: str) -> dict[str, str]:
"""Inline base64 the startframe β€” from a local file OR an http(s) URL (e.g. a
Supabase Storage public URL). Matches the donor, which fetches the image URL."""
if ref.startswith(("http://", "https://")):
r = httpx.get(ref, timeout=60)
if r.status_code >= 400:
raise RuntimeError(f"startframe fetch {r.status_code}: {r.text[:200]}")
mime = (r.headers.get("content-type") or "image/jpeg").split(";")[0].strip()
return {"bytesBase64Encoded": base64.b64encode(r.content).decode(), "mimeType": mime}
p = Path(ref)
mime = {".png": "image/png", ".webp": "image/webp"}.get(p.suffix.lower(), "image/jpeg")
return {"bytesBase64Encoded": base64.b64encode(p.read_bytes()).decode(), "mimeType": mime}
def _submit(segment: dict[str, Any], avatar: dict[str, Any] | None, duration_s: int, seed: int) -> str:
"""POST :predictLongRunning exactly like the donor's veo.ts. Returns the op name."""
s = get_settings()
instance: dict[str, Any] = {"prompt": segment["veo_prompt"]}
ref = avatar.get("ref_image_path") if avatar else None
if _has_startframe(ref):
instance["image"] = _image_inline(ref) # the startframe, inline base64
body = {
"instances": [instance],
"parameters": {
"aspectRatio": "9:16",
"durationSeconds": int(duration_s),
"sampleCount": 1,
"seed": int(seed),
"generateAudio": True,
"resolution": s.veo_resolution, # 720p
},
}
url = f"{_vertex_base()}:predictLongRunning"
last: Exception | None = None
for attempt in range(TRANSPORT_RETRIES):
try:
r = httpx.post(url, headers=_auth_headers(), json=body, timeout=120)
if r.status_code == 429 or r.status_code >= 500:
last = RuntimeError(f"Veo start {r.status_code}: {r.text[:300]}")
time.sleep(2**attempt)
continue
if r.status_code >= 400:
raise RuntimeError(f"Veo start {r.status_code}: {r.text[:500]}")
return r.json()["name"]
except httpx.TransportError as exc:
last = exc
time.sleep(2**attempt)
raise RuntimeError(f"Veo submission failed after retries: {last}")
def _download_gcs(gcs_uri: str) -> bytes:
m = re.match(r"^gs://([^/]+)/(.+)$", gcs_uri)
if not m:
raise RuntimeError(f"bad gcsUri: {gcs_uri}")
url = (
f"https://storage.googleapis.com/storage/v1/b/{m.group(1)}"
f"/o/{quote(m.group(2), safe='')}?alt=media"
)
s = get_settings()
r = httpx.get(
url,
headers={"Authorization": f"Bearer {_access_token()}",
"x-goog-user-project": s.require("GOOGLE_CLOUD_PROJECT")},
timeout=300,
)
if r.status_code >= 400:
raise RuntimeError(f"GCS download {r.status_code}: {r.text[:300]}")
return r.content
# ── Per-segment render facts (duration + seed) ───────────────────────────────
def _resolve_render_facts(seg: dict[str, Any]) -> tuple[int, int]:
s = get_settings()
dur = seg.get("duration_s")
if not dur:
dur = duration.decide_duration(seg["spoken_text"], s.words_per_second, s.tail_budget_s)
seed = seg.get("seed")
if seed is None:
seed = duration.random_seed()
dur, seed = int(dur), int(seed)
db.execute("update segments set duration_s = %s, seed = %s where id = %s", (dur, seed, str(seg["id"])))
return dur, seed
# ── Contract functions ───────────────────────────────────────────────────────
def generate_segment(segment_id: str) -> str:
s = get_settings()
seg = db.fetch_one("select * from segments where id = %s", (segment_id,))
if seg is None:
raise RuntimeError(f"no segment {segment_id}")
avatar = db.fetch_one(
"select a.* from avatars a join videos v on v.avatar_id = a.id where v.id = %s",
(seg["video_id"],),
)
duration_s, seed = _resolve_render_facts(seg)
image_attached = _has_startframe(avatar.get("ref_image_path") if avatar else None)
enforce_daily_cap()
db.execute("update segments set status = 'generating', attempts = attempts + 1 where id = %s", (segment_id,))
if s.dry_run:
op_name = f"{DRY_RUN_OP_PREFIX}{uuid.uuid4()}"
_log_generation(segment_id, True, f"dry-run-fixture 720p dur={duration_s} seed={seed} img={image_attached}")
db.execute("update segments set veo_operation = %s where id = %s", (op_name, segment_id))
return op_name
_log_generation(segment_id, False, s.veo_model_id)
op_name = _submit(seg, avatar, duration_s, seed)
db.execute("update segments set veo_operation = %s where id = %s", (op_name, segment_id))
return op_name
def poll(operation_name: str, segment_id: str | None = None) -> dict[str, Any]:
if operation_name.startswith(DRY_RUN_OP_PREFIX):
seg_id = segment_id or _segment_for_operation(operation_name)
dest = SEGMENTS_DIR / f"{seg_id}.mp4"
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(FIXTURE_CLIP, dest)
return {"status": "done", "file": str(dest)}
r = httpx.post(f"{_vertex_base()}:fetchPredictOperation", headers=_auth_headers(),
json={"operationName": operation_name}, timeout=60)
if r.status_code >= 400:
raise RuntimeError(f"Veo poll {r.status_code}: {r.text[:300]}")
data = r.json()
if not data.get("done"):
return {"status": "running"}
if data.get("error"):
err = data["error"]
msg = err.get("message") if isinstance(err, dict) else str(err)
if _is_rai_message(msg): # safety block delivered as an op error
raise VeoSafetyBlock([str(msg)])
raise RuntimeError(f"Veo operation failed: {err}")
resp = data.get("response") or {}
gen_resp = resp.get("generateVideoResponse") or {}
filtered = resp.get("raiMediaFilteredCount") or gen_resp.get("raiMediaFilteredCount")
if filtered:
reasons = resp.get("raiMediaFilteredReasons") or gen_resp.get("raiMediaFilteredReasons") or ["RAI filtered"]
raise VeoSafetyBlock(list(reasons))
videos = resp.get("videos") or resp.get("generatedSamples") or gen_resp.get("generatedSamples") or []
first = videos[0] if videos else None
if not first:
raise RuntimeError(f"Veo done with no video; response={json.dumps(resp)[:400]}")
seg_id = segment_id or _segment_for_operation(operation_name)
dest = SEGMENTS_DIR / f"{seg_id}.mp4"
dest.parent.mkdir(parents=True, exist_ok=True)
b64 = first.get("bytesBase64Encoded")
if b64:
dest.write_bytes(base64.b64decode(b64))
return {"status": "done", "file": str(dest)}
gcs = first.get("gcsUri") or (first.get("video") or {}).get("uri")
if gcs:
dest.write_bytes(_download_gcs(gcs))
return {"status": "done", "file": str(dest)}
raise RuntimeError("Veo response had neither bytes nor gcsUri")
def _segment_for_operation(operation_name: str) -> str:
row = db.fetch_one("select id from segments where veo_operation = %s", (operation_name,))
if row is None:
raise RuntimeError(f"no segment recorded for operation {operation_name}")
return str(row["id"])
def _poll_until_done(op_name: str, segment_id: str) -> str:
deadline = time.monotonic() + POLL_TIMEOUT_S
while True:
result = poll(op_name, segment_id=segment_id)
if result["status"] == "done":
return result["file"]
if time.monotonic() > deadline:
raise TimeoutError(f"Veo operation {op_name} still running after {POLL_TIMEOUT_S}s")
time.sleep(POLL_INTERVAL_S)
# ── RAI policy (SPEC_NEW Β§12.5) ──────────────────────────────────────────────
def _rephrase_segment_line(segment_id: str) -> str | None:
"""Sonnet rephrases the spoken line (softer/less imperative) and swaps it into
the segment's prompt β€” the scene/startframe stay identical. Returns the new line."""
s = get_settings()
seg = db.fetch_one("select * from segments where id = %s", (segment_id,))
if seg is None:
return None
from . import agents
from .prompts.veo_segment import validate_spoken_text
new_text = agents.rephrase_spoken_line(seg["spoken_text"])
validate_spoken_text(new_text)
old_q = f'"{seg["spoken_text"].strip()}"'
new_prompt = seg["veo_prompt"].replace(old_q, f'"{new_text.strip()}"', 1)
new_dur = duration.decide_duration(new_text, s.words_per_second, s.tail_budget_s)
db.execute(
"update segments set spoken_text = %s, veo_prompt = %s, duration_s = %s where id = %s",
(new_text, new_prompt, new_dur, segment_id),
)
return new_text
def _set_gen_note(segment_id: str, note: str | None) -> None:
"""Surface a transient generation status to the UI (e.g. RAI seed-swap progress).
Stored on videos.gen_note β€” not a creative attribute, never in videos.tags."""
db.execute(
"update videos set gen_note = %s where id = (select video_id from segments where id = %s)",
(note, str(segment_id)),
)
def _handle_safety_block(segment_id: str, first_exc: VeoSafetyBlock) -> str:
"""On a Veo safety block (Β§12.5): Veo's RAI filter is non-deterministic and usually
trips on the rendered VIDEO, not the words β€” so we retry on a **fresh seed** (the words
and startframe stay identical), looping up to VEO_RAI_MAX_RETRIES times. A new seed
almost always escapes the false positive. The retry runs **automatically**; its progress
is surfaced live via videos.gen_note. Only when the loop is exhausted does it surface as
failed β€” and only then does the UI show a 'FΓΆrsΓΆk igen' button."""
s = get_settings()
if not s.veo_rai_autoretry:
raise first_exc
last_exc = first_exc
for attempt in range(1, max(1, s.veo_rai_max_retries) + 1):
new_seed = duration.random_seed()
db.execute("update segments set seed = %s where id = %s", (new_seed, segment_id))
_set_gen_note(segment_id,
f"Veo blockade klippet β€” byter seed (fΓΆrsΓΆk {attempt}/{s.veo_rai_max_retries})…")
print(f"[RAI] segment {segment_id}: safety block β€” auto-retry {attempt}/{s.veo_rai_max_retries} "
f"on a fresh seed {new_seed} (same words + startframe).")
op_name = generate_segment(segment_id)
try:
return _poll_until_done(op_name, segment_id)
except VeoSafetyBlock as e2:
last_exc = e2
continue
print(f"[RAI] segment {segment_id}: still blocked after {s.veo_rai_max_retries} fresh seeds β€” surfacing "
f"(the UI now shows 'FΓΆrsΓΆk igen' for a fresh {s.veo_rai_max_retries}-retry loop).")
raise last_exc
def _maybe_upload_clip(segment_id: str, local_path: str) -> str:
"""Upload a generated clip to Supabase Storage (so nothing lives only locally);
return the public URL, or the local path on failure / dry-run."""
from . import storage
if get_settings().dry_run or not storage.enabled():
return local_path
try:
return storage.upload_bytes("gens", f"segments/{segment_id}.mp4",
Path(local_path).read_bytes(), "video/mp4")
except Exception as exc: # noqa: BLE001 β€” keep local on failure
print(f"[storage] clip upload failed ({exc}); keeping local")
return local_path
def generate_and_download(segment_id: str) -> str:
op_name = generate_segment(segment_id)
try:
path = _poll_until_done(op_name, segment_id)
except VeoSafetyBlock as exc:
path = _handle_safety_block(segment_id, exc)
file_path = _maybe_upload_clip(segment_id, path)
db.execute("update segments set file_path = %s where id = %s", (file_path, segment_id))
_set_gen_note(segment_id, None) # clear any RAI seed-swap note β€” this segment is done
return file_path