""" Lahr Carpet Cleaning — Veo hero video generator. 5 shots x 4s = 20s reel. Concatenated by ffmpeg into hero-reel.mp4. Saves clips to: assets/videos/hero/clips/ Saves final to: assets/videos/hero/hero-reel.mp4 Run: python3 tools/gen-video.py """ import os import sys import time import subprocess try: from google import genai from google.genai import types except ImportError: print("Installing google-genai...") 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__)) OUT_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") os.makedirs(OUT_DIR, exist_ok=True) client = genai.Client(api_key=API_KEY) SHOTS = [ { "name": "shot-01-door-opens", "prompt": ( "Cinematic low-angle wide shot. A solid wood front door of an upstate New York home opens " "inward smoothly. Bright golden afternoon sunlight pours through the doorway onto a carpeted " "entryway floor. Camera is at floor level, looking toward the door. The door swings open " "fully revealing light. No people visible. Photorealistic, warm inviting light, slow motion." ), }, { "name": "shot-02-pan-to-stains", "prompt": ( "Slow cinematic camera pan from the front door entryway across a residential living room carpet " "in an upstate New York home. The carpet shows visible dirt tracks, pet stains, and soiling " "from daily use. Natural light. No people. Camera moves fluidly across the room revealing " "the stained carpet. Photorealistic." ), }, { "name": "shot-03-stain-closeup", "prompt": ( "Close-up shot of a stained beige carpet with visible pet stains, mud, and dark soiling. " "Camera slowly pushes in on the dirty area. Dramatic side lighting emphasises the stain depth " "and texture. Slow motion. Ultra-realistic macro photography style." ), }, { "name": "shot-04-extraction-carpet", "prompt": ( "Cinematic slow-motion wide shot: a large industrial stand-up hot water extraction machine " "being pushed steadily forward across a beige residential carpet. The machine is a tall " "professional-grade upright extractor — heavy-duty, commercial size, on wheels, with a wide " "cleaning head at the base and an upright handle. No steam, no spraying water, no visible " "liquid anywhere on the machine exterior. The carpet behind the machine transitions from dirty " "and matted to bright, clean, and fluffy as it passes. Warm natural room light. Photorealistic." ), }, { "name": "shot-05-extraction-couch", "prompt": ( "Close-up cinematic shot of a professional technician's gloved hand holding a small flat " "upholstery cleaning attachment tool, pressing it firmly against a dirty grey sofa cushion " "and sliding it slowly across the fabric. The fabric visibly brightens and lifts as the tool " "moves. No water pours out — suction draws moisture into the tool. Slow motion, natural light. " "Photorealistic." ), }, { "name": "shot-06-extraction-stairs", "prompt": ( "Cinematic shot of a professional technician's hands using a compact portable upright carpet " "cleaner on a carpeted staircase — pushing the machine up a stair tread step by step. Each " "tread brightens and looks freshly cleaned as the machine passes. No water pours out. Clean " "bright carpet revealed on each step. Slow motion, warm interior light. Photorealistic." ), }, { "name": "shot-07-office-entryway", "prompt": ( "Wide cinematic shot of a clean professional office building entryway with commercial grade " "carpet. Modern corporate interior, glass doors, professional lighting. No people. Camera " "slowly pushes forward through the entry. Photorealistic." ), }, { "name": "shot-08-showroom", "prompt": ( "Wide cinematic shot of an upscale retail showroom or winery tasting room in the Finger Lakes " "region. Rich carpet throughout, warm interior lighting, product displays. No people. Camera " "glides forward through the space. Photorealistic, luxurious atmosphere." ), }, { "name": "shot-09-technician-unloading", "prompt": ( "Wide shot of a professional carpet cleaning technician wearing a plain black shirt with no logo, " "rolling a large industrial stand-up hot water extraction machine out of a white service van " "parked in a residential driveway in upstate New York. The machine is a heavy commercial-grade " "upright extractor on wheels — tall, industrial size. Autumn trees in background, bright day. " "Technician shown from side or behind, no face visible. Photorealistic." ), }, ] MODELS = [ "veo-2.0-generate-001", "veo-3.0-generate-001", ] def poll(operation, timeout=420): elapsed = 0 while not operation.done: if elapsed >= timeout: print(" Timed out.") return None print(f" Waiting... ({elapsed}s)") time.sleep(15) elapsed += 15 operation = client.operations.get(operation) return operation def download_video(video, out_path): video_bytes = None try: video_bytes = client.files.download(file=video) except Exception: pass if video_bytes: with open(out_path, "wb") as f: f.write(video_bytes) return True if hasattr(video, "uri") and video.uri: import urllib.request uri = video.uri + ("&" if "?" in video.uri else "?") + f"key={API_KEY}" print(f" Fetching via URI...") urllib.request.urlretrieve(uri, out_path) return True return False def generate(): saved = [] for item in SHOTS: out_path = os.path.join(OUT_DIR, f"{item['name']}.mp4") print(f"\n[{SHOTS.index(item)+1}/{len(SHOTS)}] Generating {item['name']}...") done = False for model in MODELS: try: print(f" Model: {model}") 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: continue if op.response and op.response.generated_videos: vid = op.response.generated_videos[0].video if download_video(vid, out_path): size_kb = os.path.getsize(out_path) // 1024 print(f" Saved {out_path} ({size_kb}KB)") saved.append(out_path) done = True break else: print(f" Download failed for {model}") else: print(f" No video from {model}") except Exception as e: print(f" Error with {model}: {e}") if not done: print(f" FAILED: {item['name']}") return saved def concat(clips): if len(clips) < 2: print("Not enough clips to concatenate.") return list_file = os.path.join(OUT_DIR, "concat.txt") with open(list_file, "w") as f: for c in clips: f.write(f"file '{c}'\n") print(f"\nConcatenating {len(clips)} clips into hero-reel.mp4...") result = subprocess.run( ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file, "-c:v", "libx264", "-crf", "22", "-preset", "fast", "-movflags", "+faststart", REEL_OUT], capture_output=True, text=True ) if result.returncode == 0: size_kb = os.path.getsize(REEL_OUT) // 1024 print(f" Saved {REEL_OUT} ({size_kb}KB)") else: print(f" ffmpeg error: {result.stderr[-300:]}") if __name__ == "__main__": clips = generate() if clips: concat(clips) print(f"\nDone. {len(clips)}/5 clips generated.") if len(clips) == 5: print("Hero reel ready: assets/videos/hero/hero-reel.mp4")