""" Regenerate carpet extraction shots using Veo 3.1. Fixes: buffer/rotary/steamer machines replaced with explicit stand-up extractor descriptions. Reconcats hero-reel.mp4 when done. Run: python3 tools/regen-veo31.py """ import os, sys, time, subprocess try: from google import genai from google.genai import types except ImportError: os.system(f"{sys.executable} -m pip install google-genai --quiet") from google import genai from google.genai import types API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") BASE_DIR = os.path.dirname(os.path.dirname(__file__)) VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") client = genai.Client(api_key=API_KEY) # Machine description block used across all shots MACHINE = ( "The carpet cleaning machine looks exactly like a Rug Doctor or Bissell Big Green carpet cleaner — " "the large upright machines you rent at grocery stores. Tall rectangular body, upright handle, " "wide flat cleaning foot at the base. It is pushed forward in straight lines like a vacuum cleaner. " "NOT a floor buffer. NOT a rotary disc scrubber. NOT a steam cleaner. NOT circular. " "NO spinning parts. NO steam. NO water spraying out. The carpet is cleaned by suction alone." ) SHOTS = [ { "name": "shot-04-extraction-carpet", "prompt": ( f"Cinematic slow-motion wide shot. A man pushes a Rug Doctor style carpet cleaning machine " f"steadily forward across a beige residential carpet in a living room. {MACHINE} " f"The carpet behind the machine transitions from dirty and matted to bright, clean, and fluffy. " f"Warm natural room light. Photorealistic professional video." ), }, { "name": "shot-05-extraction-couch", "prompt": ( "Close-up cinematic slow-motion shot. A professional technician's gloved hand holds a small " "flat handheld upholstery cleaning attachment — a rectangular suction wand, flat on the bottom, " "no moving parts, no spinning disc. " "The technician presses it firmly against a dirty grey sofa cushion and slides it slowly across. " "The fabric visibly brightens as the wand moves across it. " "NO rotary machine anywhere in frame. NO spinning. NO steam. NO water pouring out. " "Natural light. Photorealistic." ), }, { "name": "shot-06-extraction-stairs", "prompt": ( "Cinematic slow-motion shot. A professional technician pushes a Rug Doctor style upright carpet " "cleaning machine up a carpeted residential staircase, one stair tread at a time. " "The machine is a tall rectangular upright unit with a handle and flat cleaning head — " "NOT a buffer, NOT circular, NOT rotating. " "Each stair tread brightens and looks freshly cleaned as the machine passes over it. " "NO steam, NO water visible. Warm interior light. Photorealistic." ), }, { "name": "shot-09-technician-unloading", "prompt": ( f"Wide cinematic shot. A professional carpet cleaning technician in a plain black shirt " f"rolls a Rug Doctor style carpet cleaning machine down a ramp out of a white service van " f"parked in a residential driveway in upstate New York. {MACHINE} " f"Autumn trees in background, bright daylight. " f"Technician shown from the side or behind, no face visible. Photorealistic." ), }, ] MODEL = "veo-3.1-generate-preview" def poll(op, timeout=600): elapsed = 0 while not op.done: if elapsed >= timeout: print(f" Timed out after {timeout}s.") return None print(f" Waiting... ({elapsed}s)") time.sleep(15) elapsed += 15 op = client.operations.get(op) return op saved = [] for item in SHOTS: out_path = os.path.join(VID_DIR, f"{item['name']}.mp4") print(f"\n[VID] Generating {item['name']} with {MODEL}...") try: op = client.models.generate_videos( model=MODEL, prompt=item["prompt"], config=types.GenerateVideosConfig( aspect_ratio="16:9", resolution="720p", duration_seconds=6, number_of_videos=1, ), ) op = poll(op) if op is None: print(f" FAILED (timeout): {item['name']}") continue if op.response and op.response.generated_videos: vid = op.response.generated_videos[0].video # Veo 3.1 download pattern video_bytes = client.files.download(file=vid) if video_bytes: with open(out_path, "wb") as f: f.write(video_bytes) print(f" Saved {out_path} ({os.path.getsize(out_path)//1024}KB)") saved.append(item["name"]) else: # fallback: try .save() method try: vid.save(out_path) print(f" Saved via .save() ({os.path.getsize(out_path)//1024}KB)") saved.append(item["name"]) except Exception as e2: print(f" Download failed: {e2}") else: print(f" No video returned for {item['name']}") except Exception as e: print(f" Error: {e}") print(f"\n{len(saved)}/{len(SHOTS)} shots saved: {saved}") # Reconcat full reel ORDER = [ "shot-01-door-opens-trimmed", "shot-02-pan-to-stains", "shot-03-stain-closeup", "shot-04-extraction-carpet", "shot-05-extraction-couch", "shot-06-extraction-stairs", "shot-07-office-entryway", "shot-08-showroom", "shot-09-technician-unloading", ] missing = [n for n in ORDER if not os.path.exists(os.path.join(VID_DIR, f"{n}.mp4"))] if missing: print(f"\nSkipping reconcat — missing clips: {missing}") else: print("\nReconcatenating hero-reel.mp4...") concat_file = os.path.join(VID_DIR, "concat.txt") with open(concat_file, "w") as f: for name in ORDER: f.write(f"file '{os.path.join(VID_DIR, name)}.mp4'\n") result = subprocess.run( ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, "-c:v", "libx264", "-crf", "22", "-preset", "fast", "-movflags", "+faststart", REEL_OUT], capture_output=True, text=True ) if result.returncode == 0: print(f" Reel saved: {REEL_OUT} ({os.path.getsize(REEL_OUT)//1024}KB)") else: print(f" ffmpeg error: {result.stderr[-400:]}") print("\nDone.")