This commit is contained in:
Concept Agent
2026-05-15 18:02:38 +02:00
parent 72016728e2
commit 307e452251
175 changed files with 9316 additions and 0 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+87
View File
@@ -0,0 +1,87 @@
"""
Build paced hero reel from the browser-ordered clip list.
- Shot 1 (family entry): trimmed to 3s — cuts before mud pan
- Shots 2-7: full 6s (establish the story)
- Shots 8-12: trimmed to 3.5s (building pace)
- Shots 13-15: trimmed to 2.5s (rapid)
- Shot 16 (final reveal): full 6s (hold on clean result)
"""
import os, subprocess, shutil
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")
TMP_DIR = os.path.join(VID_DIR, "paced-tmp")
os.makedirs(TMP_DIR, exist_ok=True)
# Clip order: wine spill > extraction > stain > technician, stairs later
CLIPS = [
"v3-shot-02", # 1 - wine spill on sofa
"shot-05-extraction-couch",# 2 - extraction couch
"shot-03-technician", # 3 - technician
"v3-shot-03", # 4 - dirty stained carpet close-up
"v3-shot-06", # 5 - living room clean carpet pan
"v3-shot-07", # 6 - restaurant carpet glide
"v3-shot-05", # 7 - office lobby carpet pan
"v2-shot-05-clean-stairs", # 8 - clean bright staircase
"v2-shot-07-restaurant", # 9 - restaurant carpet
"v2-shot-06-office", # 10 - bright office carpet
"shot-01-wide-room", # 11 - wide room establishing
"shot-05-clean-reveal", # 12 - clean reveal
"shot-04-extraction-carpet",# 13 - final reveal
]
# Duration for each clip
DURATIONS = [
4.5, # 1 wine spill — cut at 4.5s
5.0, # 2
5.0, # 3
4.0, # 4 --- building pace ---
4.0, # 5
4.0, # 6
3.5, # 7
3.5, # 8
2.5, # 9 --- rapid ---
2.5, # 10
2.5, # 11
2.5, # 12
6.0, # 13 final reveal — hold
]
paced_clips = []
for i, (name, dur) in enumerate(zip(CLIPS, DURATIONS)):
src = os.path.join(VID_DIR, f"{name}.mp4")
out = os.path.join(TMP_DIR, f"{i:02d}-{name}.mp4")
if not os.path.exists(src):
print(f" MISSING: {src}")
continue
print(f"[{i+1:02d}] {name}{dur}s")
result = subprocess.run(
["ffmpeg", "-y", "-i", src, "-t", str(dur),
"-c:v", "libx264", "-crf", "22", "-preset", "fast", out],
capture_output=True, text=True
)
if result.returncode != 0:
print(f" ffmpeg error: {result.stderr[-200:]}")
else:
paced_clips.append(out)
print(f"\n{len(paced_clips)}/{len(CLIPS)} clips trimmed")
concat_file = os.path.join(TMP_DIR, "concat.txt")
with open(concat_file, "w") as f:
for p in paced_clips:
f.write(f"file '{p}'\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)")
# Clean up tmp
shutil.rmtree(TMP_DIR)
print("Done.")
else:
print(f"ffmpeg error: {result.stderr[-400:]}")
+84
View File
@@ -0,0 +1,84 @@
"""Local server: serves clip-browser.html + handles POST /build-reel to run ffmpeg."""
import http.server, json, os, subprocess, urllib.parse
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")
TOOLS_DIR = os.path.dirname(__file__)
class Handler(http.server.BaseHTTPRequestHandler):
def log_message(self, fmt, *args): pass
def do_GET(self):
path = self.path.split('?')[0]
if path in ('/', '/tools/clip-browser.html'):
self._serve_file(os.path.join(TOOLS_DIR, 'clip-browser.html'), 'text/html')
elif path.startswith('/assets/videos/'):
rel = path.lstrip('/')
fpath = os.path.join(BASE_DIR, rel)
if os.path.exists(fpath):
self._serve_file(fpath, 'video/mp4')
else:
self._404()
else:
self._404()
def do_POST(self):
if self.path == '/tools/build-reel-api.py':
length = int(self.headers.get('Content-Length', 0))
body = json.loads(self.rfile.read(length))
clips = body.get('clips', [])
if not clips:
self._json({'message': 'No clips provided.'}, 400)
return
concat_file = os.path.join(VID_DIR, 'concat-browser.txt')
missing = []
with open(concat_file, 'w') as f:
for name in clips:
p = os.path.join(VID_DIR, f'{name}.mp4')
if not os.path.exists(p):
missing.append(name)
else:
f.write(f"file '{p}'\n")
if missing:
self._json({'message': f'Missing clips: {missing}'}, 400)
return
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:
size = os.path.getsize(REEL_OUT) // 1024
self._json({'message': f'Reel built: hero-reel.mp4 ({size}KB). Now rebuild Docker: docker compose up -d --build'})
else:
self._json({'message': f'ffmpeg error: {result.stderr[-300:]}'}, 500)
else:
self._404()
def _serve_file(self, path, ctype):
with open(path, 'rb') as f:
data = f.read()
self.send_response(200)
self.send_header('Content-Type', ctype)
self.send_header('Content-Length', len(data))
self.end_headers()
self.wfile.write(data)
def _json(self, obj, code=200):
data = json.dumps(obj).encode()
self.send_response(code)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', len(data))
self.end_headers()
self.wfile.write(data)
def _404(self):
self.send_response(404)
self.end_headers()
if __name__ == '__main__':
port = 8088
print(f'Clip browser running at http://localhost:{port}/')
http.server.HTTPServer(('', port), Handler).serve_forever()
+230
View File
@@ -0,0 +1,230 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lahr Clip Browser</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #0a0a0b; color: #eee; font-family: Inter, sans-serif; padding: 24px; }
h1 { font-size: 20px; color: #e8291b; margin-bottom: 6px; }
p.sub { color: #888; font-size: 13px; margin-bottom: 24px; }
.layout { display: grid; grid-template-columns: 1fr 340px; gap: 24px; }
#clip-list { display: flex; flex-direction: column; gap: 10px; }
.clip-item {
display: flex; align-items: center; gap: 12px;
background: #161618; border: 1px solid #2a2a2e; border-radius: 8px;
padding: 10px 12px; cursor: grab; user-select: none;
transition: border-color 0.15s;
}
.clip-item:hover { border-color: #e8291b; }
.clip-item.dragging { opacity: 0.4; }
.clip-item.drag-over { border-color: #e8291b; background: #1f1012; }
.drag-handle { color: #555; font-size: 18px; flex-shrink: 0; cursor: grab; }
.clip-thumb { width: 120px; height: 68px; object-fit: cover; border-radius: 4px; flex-shrink: 0; background: #222; }
.clip-info { flex: 1; min-width: 0; }
.clip-name { font-size: 13px; font-weight: 600; color: #ddd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.clip-size { font-size: 11px; color: #666; margin-top: 2px; }
.clip-actions { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
.btn-preview { background: #1e1e22; border: 1px solid #333; color: #ccc; padding: 5px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; }
.btn-preview:hover { border-color: #e8291b; color: #e8291b; }
.btn-remove { background: transparent; border: 1px solid #333; color: #555; padding: 5px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; }
.btn-remove:hover { border-color: #e8291b; color: #e8291b; }
.sidebar { position: sticky; top: 24px; }
.preview-box { background: #161618; border: 1px solid #2a2a2e; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
.preview-box h3 { font-size: 13px; color: #888; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
#preview-video { width: 100%; border-radius: 6px; background: #000; }
#preview-name { font-size: 12px; color: #666; margin-top: 8px; text-align: center; }
.reel-box { background: #161618; border: 1px solid #2a2a2e; border-radius: 8px; padding: 16px; }
.reel-box h3 { font-size: 13px; color: #888; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
#reel-order { font-size: 11px; color: #666; line-height: 1.8; margin-bottom: 14px; max-height: 220px; overflow-y: auto; }
.btn-build { width: 100%; background: #e8291b; color: #fff; border: none; padding: 12px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; }
.btn-build:hover { background: #c72216; }
#build-output { margin-top: 12px; font-size: 11px; color: #888; background: #0d0d0f; border-radius: 4px; padding: 10px; display: none; white-space: pre-wrap; }
.unused-section { margin-top: 32px; }
.unused-section h2 { font-size: 14px; color: #555; margin-bottom: 12px; }
#unused-list { display: flex; flex-direction: column; gap: 8px; }
.unused-item { display: flex; align-items: center; gap: 12px; background: #111; border: 1px solid #1e1e22; border-radius: 8px; padding: 8px 12px; }
.unused-item .clip-name { font-size: 12px; color: #666; }
.btn-add { background: #1e1e22; border: 1px solid #333; color: #888; padding: 5px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; margin-left: auto; }
.btn-add:hover { border-color: #4a9; color: #4a9; }
</style>
</head>
<body>
<h1>Lahr Clip Browser</h1>
<p class="sub">Drag clips to reorder. Click Preview to watch. Remove clips from reel. Build Reel when ready.</p>
<div class="layout">
<div>
<div id="clip-list"></div>
<div class="unused-section">
<h2>Available (not in reel)</h2>
<div id="unused-list"></div>
</div>
</div>
<div class="sidebar">
<div class="preview-box">
<h3>Preview</h3>
<video id="preview-video" controls></video>
<div id="preview-name">Click Preview on any clip</div>
</div>
<div class="reel-box">
<h3>Current Reel Order</h3>
<div id="reel-order"></div>
<button class="btn-build" onclick="buildReel()">Build Reel</button>
<div id="build-output"></div>
</div>
</div>
</div>
<script>
const BASE = '/assets/videos/hero/clips/';
const ALL_CLIPS = [
{ name: 'v3-shot-01', label: 'v3 · Family enters door, pans to carpet', size: '5.2MB' },
{ name: 'v3-shot-02', label: 'v3 · Wine spill on sofa close-up', size: '6.0MB' },
{ name: 'v3-shot-03', label: 'v3 · Dirty stained carpet close-up', size: '3.3MB' },
{ name: 'v3-shot-04', label: 'v3 · Clean bright sofa pullback', size: '5.6MB' },
{ name: 'v3-shot-05', label: 'v3 · Office lobby carpet pan', size: '4.7MB' },
{ name: 'v3-shot-06', label: 'v3 · Living room clean carpet pan', size: '4.9MB' },
{ name: 'v3-shot-07', label: 'v3 · Restaurant carpet glide', size: '3.1MB' },
{ name: 'v2-shot-01-door-entry', label: 'v2 · Door entry muddy boots', size: '2.2MB' },
{ name: 'v2-shot-02-mud-on-carpet', label: 'v2 · Mud boots on carpet floor level', size: '4.1MB' },
{ name: 'v2-shot-03-stain-on-chair', label: 'v2 · Stain on chair close-up', size: '3.3MB' },
{ name: 'v2-shot-04-extraction-carpet', label: 'v2 · Extraction machine on carpet', size: '4.2MB' },
{ name: 'v2-shot-05-clean-stairs', label: 'v2 · Clean bright staircase', size: '5.6MB' },
{ name: 'v2-shot-06-office', label: 'v2 · Bright office carpet', size: '4.7MB' },
{ name: 'v2-shot-07-restaurant', label: 'v2 · Restaurant carpet', size: '4.7MB' },
{ name: 'shot-01-door-opens-trimmed', label: 'v1 · Door opens (trimmed 2.5s)', size: '525KB' },
{ name: 'shot-01-wide-room', label: 'v1 · Wide room establishing', size: '2.6MB' },
{ name: 'shot-02-pan-to-stains', label: 'v1 · Pan to stains / muddy shoes', size: '2.8MB' },
{ name: 'shot-02-staircase', label: 'v1 · Staircase', size: '1.4MB' },
{ name: 'shot-03-stain-closeup', label: 'v1 · Stain close-up', size: '4.7MB' },
{ name: 'shot-03-technician', label: 'v1 · Technician', size: '1.6MB' },
{ name: 'shot-04-extraction-carpet', label: 'v1 · Extraction carpet (clean reveal)', size: '6.2MB' },
{ name: 'shot-04-extraction-closeup', label: 'v1 · Extraction close-up', size: '2.0MB' },
{ name: 'shot-05-clean-reveal', label: 'v1 · Clean reveal', size: '2.2MB' },
{ name: 'shot-05-extraction-couch', label: 'v1 · Extraction couch', size: '3.5MB' },
{ name: 'shot-06-extraction-stairs', label: 'v1 · Extraction stairs', size: '5.4MB' },
{ name: 'shot-07-office-entryway', label: 'v1 · Office entryway', size: '5.9MB' },
{ name: 'shot-08-showroom', label: 'v1 · Showroom', size: '4.1MB' },
{ name: 'shot-09-technician-unloading', label: 'v1 · Technician unloading van', size: '3.7MB' },
{ name: 'shot-01-door-opens', label: 'v1 · Door opens (full 6s)', size: '1.5MB' },
];
// Default reel = current v3 set
let reelClips = ALL_CLIPS.slice(0, 7).map(c => c.name);
function getClip(name) { return ALL_CLIPS.find(c => c.name === name); }
function render() {
const list = document.getElementById('clip-list');
list.innerHTML = '';
reelClips.forEach((name, idx) => {
const clip = getClip(name);
if (!clip) return;
const div = document.createElement('div');
div.className = 'clip-item';
div.draggable = true;
div.dataset.name = name;
div.innerHTML = `
<span class="drag-handle">⠿</span>
<video class="clip-thumb" src="${BASE}${name}.mp4" muted preload="metadata"></video>
<div class="clip-info">
<div class="clip-name">${idx+1}. ${clip.label}</div>
<div class="clip-size">${clip.size}</div>
</div>
<div class="clip-actions">
<button class="btn-preview" onclick="preview('${name}', '${clip.label}')">Preview</button>
<button class="btn-remove" onclick="remove('${name}')">Remove</button>
</div>`;
div.addEventListener('dragstart', dragStart);
div.addEventListener('dragover', dragOver);
div.addEventListener('drop', drop);
div.addEventListener('dragend', dragEnd);
list.appendChild(div);
});
const unused = ALL_CLIPS.filter(c => !reelClips.includes(c.name));
const ulist = document.getElementById('unused-list');
ulist.innerHTML = '';
unused.forEach(clip => {
const div = document.createElement('div');
div.className = 'unused-item';
div.innerHTML = `
<div class="clip-name">${clip.label} <span style="color:#444">(${clip.size})</span></div>
<button class="btn-add" onclick="addToReel('${clip.name}')">+ Add</button>`;
ulist.appendChild(div);
});
const orderEl = document.getElementById('reel-order');
orderEl.innerHTML = reelClips.map((n,i) => {
const c = getClip(n);
return `<div>${i+1}. ${c ? c.label : n}</div>`;
}).join('');
}
function preview(name, label) {
const v = document.getElementById('preview-video');
v.src = BASE + name + '.mp4';
v.play();
document.getElementById('preview-name').textContent = label;
}
function remove(name) {
reelClips = reelClips.filter(n => n !== name);
render();
}
function addToReel(name) {
reelClips.push(name);
render();
}
let dragSrc = null;
function dragStart(e) { dragSrc = this; this.classList.add('dragging'); }
function dragOver(e) { e.preventDefault(); document.querySelectorAll('.clip-item').forEach(el => el.classList.remove('drag-over')); this.classList.add('drag-over'); }
function drop(e) {
e.preventDefault();
if (dragSrc === this) return;
const fromName = dragSrc.dataset.name;
const toName = this.dataset.name;
const fi = reelClips.indexOf(fromName);
const ti = reelClips.indexOf(toName);
reelClips.splice(fi, 1);
reelClips.splice(ti, 0, fromName);
render();
}
function dragEnd() { document.querySelectorAll('.clip-item').forEach(el => el.classList.remove('dragging','drag-over')); }
async function buildReel() {
const out = document.getElementById('build-output');
out.style.display = 'block';
out.textContent = 'Sending to server...';
const resp = await fetch('/tools/build-reel-api.py', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clips: reelClips })
});
if (!resp.ok) {
// Fallback: show the ffmpeg command to copy
const lines = reelClips.map(n => `file 'assets/videos/hero/clips/${n}.mp4'`).join('\n');
out.textContent = 'Server not available. Run this manually:\n\n' +
'# 1. Save as concat.txt:\n' + lines + '\n\n' +
'# 2. Run ffmpeg:\n' +
'ffmpeg -y -f concat -safe 0 -i concat.txt -c:v libx264 -crf 22 -preset fast -movflags +faststart assets/videos/hero/hero-reel.mp4';
} else {
const data = await resp.json();
out.textContent = data.message || 'Done.';
}
}
render();
</script>
</body>
</html>
+186
View File
@@ -0,0 +1,186 @@
"""Generate replacement service images via ComfyUI SDXL (local, no API key needed)."""
import json, time, urllib.request, urllib.error, os, sys
COMFY = "http://localhost:8188"
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "services")
CKPT = "sd_xl_base_1.0.safetensors"
IMAGES = [
{
"filename": "vacation-rentals.jpg",
"positive": (
"bright cozy vacation rental living room interior, clean beige carpet, "
"comfortable furniture, large windows with natural light, Finger Lakes "
"style decor, warm inviting atmosphere, no people, no equipment, "
"professional interior photography, ultra-realistic"
),
"negative": (
"people, person, human, worker, machine, vacuum, equipment, dirty, stain, "
"text, watermark, blurry, low quality, cartoon, dark"
),
},
{
"filename": "office-spaces.jpg",
"positive": (
"modern corporate office interior, clean dark grey commercial carpet tiles, "
"open plan workspace, white desks, professional lighting, glass partitions, "
"no people, no equipment, architectural photography, ultra-realistic"
),
"negative": (
"people, person, human, worker, machine, vacuum, equipment, dirty, stain, "
"text, watermark, blurry, low quality, cartoon"
),
},
{
"filename": "hotels-inns.jpg",
"positive": (
"elegant hotel corridor interior, clean patterned carpet runner, warm wall "
"sconce lighting, white walls, numbered room doors along hallway, "
"hospitality interior design, no people, no equipment, "
"professional photography, ultra-realistic"
),
"negative": (
"people, person, human, worker, machine, vacuum, equipment, dirty, stain, "
"text, watermark, blurry, low quality, cartoon"
),
},
{
"filename": "retail-showrooms.jpg",
"positive": (
"upscale retail showroom interior, clean light grey carpet flooring, "
"modern display shelving, bright overhead track lighting, white walls, "
"customer-facing professional space, no people, no equipment, "
"architectural photography, ultra-realistic"
),
"negative": (
"people, person, human, worker, machine, vacuum, equipment, dirty, stain, "
"text, watermark, blurry, low quality, cartoon"
),
},
{
"filename": "property-management.jpg",
"positive": (
"clean apartment unit interior, fresh beige carpet throughout living room, "
"neutral walls, bright windows, move-in ready condition, residential "
"property management style, no people, no furniture, no equipment, "
"real estate photography, ultra-realistic"
),
"negative": (
"people, person, human, worker, machine, vacuum, equipment, dirty, stain, "
"text, watermark, blurry, low quality, cartoon"
),
},
]
def build_workflow(positive, negative, seed=None):
import random
if seed is None:
seed = random.randint(0, 2**32)
return {
"3": {
"class_type": "KSampler",
"inputs": {
"cfg": 7.0,
"denoise": 1.0,
"latent_image": ["5", 0],
"model": ["4", 0],
"negative": ["7", 0],
"positive": ["6", 0],
"sampler_name": "euler",
"scheduler": "normal",
"seed": seed,
"steps": 25,
},
},
"4": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": CKPT},
},
"5": {
"class_type": "EmptyLatentImage",
"inputs": {"batch_size": 1, "height": 768, "width": 1024},
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"clip": ["4", 1], "text": positive},
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": {"clip": ["4", 1], "text": negative},
},
"8": {
"class_type": "VAEDecode",
"inputs": {"samples": ["3", 0], "vae": ["4", 2]},
},
"9": {
"class_type": "SaveImage",
"inputs": {"filename_prefix": "lahr_gen", "images": ["8", 0]},
},
}
def queue_prompt(workflow):
data = json.dumps({"prompt": workflow}).encode()
req = urllib.request.Request(
f"{COMFY}/prompt",
data=data,
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())["prompt_id"]
def wait_for_result(prompt_id, timeout=600):
start = time.time()
while time.time() - start < timeout:
try:
with urllib.request.urlopen(f"{COMFY}/history/{prompt_id}") as resp:
hist = json.loads(resp.read())
if prompt_id in hist:
outputs = hist[prompt_id].get("outputs", {})
for node_id, node_out in outputs.items():
if "images" in node_out:
return node_out["images"]
except Exception:
pass
print(" waiting...", flush=True)
time.sleep(5)
return None
def download_image(img_info, out_path):
fname = img_info["filename"]
subfolder = img_info.get("subfolder", "")
img_type = img_info.get("type", "output")
params = f"filename={fname}&subfolder={subfolder}&type={img_type}"
url = f"{COMFY}/view?{params}"
with urllib.request.urlopen(url) as resp:
data = resp.read()
# Convert PNG to JPEG via PIL if available
try:
from PIL import Image
import io
img = Image.open(io.BytesIO(data)).convert("RGB")
img.save(out_path, "JPEG", quality=90)
print(f" Saved JPEG ({len(data)//1024}KB raw -> {os.path.getsize(out_path)//1024}KB)")
except ImportError:
# Save as-is (PNG), rename accordingly
png_path = out_path.replace(".jpg", ".png")
with open(png_path, "wb") as f:
f.write(data)
print(f" Saved PNG (PIL not available): {png_path}")
for spec in IMAGES:
out_path = os.path.join(OUT_DIR, spec["filename"])
print(f"\nGenerating: {spec['filename']}")
workflow = build_workflow(spec["positive"], spec["negative"])
prompt_id = queue_prompt(workflow)
print(f" Queued: {prompt_id}")
images = wait_for_result(prompt_id)
if images:
download_image(images[0], out_path)
else:
print(" FAILED: no output after timeout")
print("\nDone.")
+106
View File
@@ -0,0 +1,106 @@
"""
Lahr Carpet Cleaning — Gemini Imagen hero image generator.
Generates: hero living room, cleaning in progress, clean result.
Saves to: assets/images/hero/
Run: python3 tools/gen-images.py
"""
import os
import sys
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")
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "hero")
os.makedirs(OUT_DIR, exist_ok=True)
client = genai.Client(api_key=API_KEY)
IMAGES = [
{
"name": "hero-living-room",
"prompt": (
"Luxurious modern living room with spotlessly clean cream carpet, "
"natural light streaming through large windows, contemporary furniture, "
"professional interior photography, warm inviting atmosphere, no people, "
"ultra-realistic, 8K quality"
),
"aspect": "16:9",
},
{
"name": "hero-clean-result",
"prompt": (
"Pristine white plush carpet in a beautiful upscale residential home, "
"dramatic before-after transformation, deeply cleaned carpet with "
"vacuum lines visible, natural light, professional photography, no people"
),
"aspect": "16:9",
},
{
"name": "hero-technician",
"prompt": (
"Professional carpet cleaning technician pushing a large upright hot water "
"extraction machine across residential carpet in a bright modern home interior. "
"Machine resembles an oversized upright vacuum cleaner with a cylindrical body. "
"No steam visible anywhere, no water spraying, no hoses visible, completely dry. "
"Technician shown from behind or side, no face, plain black shirt, no logo. "
"High-end professional photography."
),
"aspect": "16:9",
},
{
"name": "hero-before-after",
"prompt": (
"Side-by-side residential living room carpet: left half heavily soiled with mud "
"and dark stains, right half same carpet after professional hot water extraction "
"cleaning, bright and pristine. No steam, no water, no machines visible anywhere. "
"Dramatic before-after contrast. Professional photography, no people."
),
"aspect": "16:9",
},
{
"name": "hero-stairs",
"prompt": (
"Beautiful staircase with freshly cleaned plush carpet on stairs, "
"modern home interior, natural light from above, professional result, "
"no people, architectural photography, high-end residential"
),
"aspect": "3:4",
},
]
def generate():
for item in IMAGES:
out_path = os.path.join(OUT_DIR, f"{item['name']}.jpg")
print(f"Generating {item['name']}...")
try:
resp = client.models.generate_images(
model="imagen-4.0-generate-001",
prompt=item["prompt"],
config=types.GenerateImagesConfig(
number_of_images=1,
aspect_ratio=item["aspect"],
output_mime_type="image/jpeg",
safety_filter_level="block_low_and_above",
),
)
if resp.generated_images:
image_bytes = resp.generated_images[0].image.image_bytes
with open(out_path, "wb") as f:
f.write(image_bytes)
size_kb = len(image_bytes) // 1024
print(f" Saved {out_path} ({size_kb}KB)")
else:
print(f" No image returned for {item['name']}")
except Exception as e:
print(f" Error on {item['name']}: {e}")
if __name__ == "__main__":
generate()
print("\nDone. Images in assets/images/hero/")
+226
View File
@@ -0,0 +1,226 @@
"""
Lahr Carpet Cleaning — Location page generator.
Creates /locations/<slug>/index.html for each city.
Run: python3 tools/gen-locations.py
"""
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
LOC_DIR = os.path.join(BASE_DIR, "locations")
CITIES = [
{"name": "Waterloo", "slug": "waterloo-ny", "county": "Seneca County", "note": "Our home base. Fastest response times in the area."},
{"name": "Geneva", "slug": "geneva-ny", "county": "Ontario County", "note": "Full residential and commercial services throughout Geneva."},
{"name": "Seneca Falls", "slug": "seneca-falls-ny", "county": "Seneca County", "note": "Serving homes, vacation rentals, and businesses in Seneca Falls."},
{"name": "Canandaigua", "slug": "canandaigua-ny", "county": "Ontario County", "note": "Lakefront homes, rentals, and businesses along Canandaigua Lake."},
{"name": "Penn Yan", "slug": "penn-yan-ny", "county": "Yates County", "note": "Homes, wineries, and short-term rentals in the Penn Yan area."},
{"name": "Newark", "slug": "newark-ny", "county": "Wayne County", "note": "Carpet and upholstery cleaning for homes and businesses in Newark."},
{"name": "Clifton Springs", "slug": "clifton-springs-ny", "county": "Ontario County", "note": "Residential and commercial cleaning throughout Clifton Springs."},
{"name": "Lodi", "slug": "lodi-ny", "county": "Seneca County", "note": "Serving homes and vacation properties in Lodi and surrounding areas."},
{"name": "Himrod", "slug": "himrod-ny", "county": "Yates County", "note": "Carpet and floor cleaning for homes and rentals in the Himrod area."},
{"name": "Phelps", "slug": "phelps-ny", "county": "Ontario County", "note": "Residential carpet and upholstery cleaning throughout Phelps."},
{"name": "Shortsville", "slug": "shortsville-ny", "county": "Ontario County", "note": "Home and business cleaning services in Shortsville, NY."},
{"name": "Victor", "slug": "victor-ny", "county": "Ontario County", "note": "Residential and commercial carpet cleaning throughout Victor."},
{"name": "Naples", "slug": "naples-ny", "county": "Ontario County", "note": "Serving homes, wineries, and vacation rentals in the Naples area."},
{"name": "Gorham", "slug": "gorham-ny", "county": "Ontario County", "note": "Carpet and floor cleaning for homes and properties in Gorham."},
{"name": "Manchester", "slug": "manchester-ny", "county": "Ontario County", "note": "Residential and commercial cleaning services in Manchester, NY."},
{"name": "Ovid", "slug": "ovid-ny", "county": "Seneca County", "note": "Serving homes and rental properties throughout Ovid."},
{"name": "Clyde", "slug": "clyde-ny", "county": "Wayne County", "note": "Carpet, upholstery, and floor cleaning for homes and businesses in Clyde."},
{"name": "Farmington", "slug": "farmington-ny", "county": "Ontario County", "note": "Residential and commercial carpet cleaning throughout Farmington."},
{"name": "East Bloomfield", "slug": "east-bloomfield-ny", "county": "Ontario County", "note": "Serving homes and properties in East Bloomfield and surrounding areas."},
{"name": "Rushville", "slug": "rushville-ny", "county": "Yates County", "note": "Carpet and upholstery cleaning for homes and rentals in Rushville."},
{"name": "Finger Lakes", "slug": "finger-lakes-ny", "county": "Region", "note": "Serving vacation rentals, wineries, and homes across the Finger Lakes region."},
]
SERVICES = [
{"name": "Carpet Cleaning", "slug": "/services/carpet-cleaning/", "img": "/assets/images/services/carpet-cleaning.jpg", "sub": "In-Home Service", "desc": "Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home."},
{"name": "Stairs Cleaning", "slug": "/services/stairs/", "img": "/assets/images/services/stairs-cleaning.jpg", "sub": "Step by Step", "desc": "Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing."},
{"name": "Upholstery Cleaning","slug": "/services/upholstery/", "img": "/assets/images/services/upholstery-cleaning.jpg","sub": "Furniture Refresh", "desc": "Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue."},
{"name": "Floor Cleaning", "slug": "/services/floors/", "img": "/assets/images/services/floor-cleaning.jpg", "sub": "Hard Surface Care", "desc": "Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition."},
{"name": "Area Rug Cleaning", "slug": "/services/area-rugs/", "img": "/assets/images/services/area-rug-cleaning.jpg", "sub": "Delicate Care", "desc": "Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt."},
{"name": "Add-On Services", "slug": "/services/add-ons/", "img": "/assets/images/services/add-ons.jpg", "sub": "Extra Care", "desc": "Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service."},
]
HERO_IMAGES = [
"/assets/images/hero/hero-living-room.jpg",
"/assets/images/hero/hero-clean-result.jpg",
"/assets/images/hero/hero-technician.jpg",
"/assets/images/hero/hero-before-after.jpg",
"/assets/images/hero/hero-stairs.jpg",
]
def service_card(svc, city_name):
accent_word, rest = svc["name"].split(" ", 1) if " " in svc["name"] else (svc["name"], "")
h3 = f'<span class="text-accent">{accent_word}</span> {rest}'.strip()
return f""" <div class="service-card">
<div class="service-image"><img src="{svc['img']}" alt="{svc['name']} in {city_name}, NY"></div>
<h3 class="heading-3">{h3}</h3>
<div class="text-block-4">{svc['sub']}</div>
<p class="paragraph-2">{svc['desc']}</p>
<a href="{svc['slug']}" class="btn btn-primary">Learn More</a>
</div>"""
def page_html(city, idx):
hero_img = HERO_IMAGES[idx % len(HERO_IMAGES)]
cards = "\n".join(service_card(s, city["name"]) for s in SERVICES)
name = city["name"]
county = city["county"]
note = city["note"]
slug = city["slug"]
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Carpet Cleaning in {name}, NY | Lahr Carpet Cleaning</title>
<meta name="description" content="Professional carpet, upholstery, and floor cleaning in {name}, NY. Lahr Carpet Cleaning serves {county} and the Finger Lakes region. Call 315-719-1218.">
<link rel="stylesheet" href="/assets/css/styles.css?v=5">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head>
<body>
<header class="header" id="header">
<div class="container"><div id="site-nav"></div></div>
</header>
<section class="page-hero" style="background-image: url('{hero_img}');">
<div class="container">
<div class="page-hero-content">
<span class="hero-eyebrow">{county} &mdash; Finger Lakes</span>
<h1 class="hero-title">{name},<br><span class="text-accent">NY</span></h1>
<p>{note}</p>
<div class="hero-actions">
<a href="/contact/" class="btn btn-primary btn-large">Book Now</a>
<a href="tel:315-719-1218" class="btn btn-ghost btn-large"><i class="fas fa-phone"></i> 315-719-1218</a>
</div>
</div>
</div>
</section>
<section class="services-overview">
<div class="container">
<div class="section-header">
<span class="section-label">{name}, NY</span>
<h2 class="section-title">Services in {name}</h2>
<p class="section-subtitle">We serve {name} and the surrounding {county} communities. Call to confirm availability for your address.</p>
</div>
<div class="services-grid">
{cards}
</div>
</div>
</section>
<section class="cta-banner">
<div class="cta-content">
<h2 class="heading-4"><strong>Serving </strong><span class="text-accent"><strong>{name}</strong></span></h2>
<p class="paragraph-4">Call 315-719-1218 or submit the form for a free estimate in {name}, NY.</p>
<a href="/contact/" class="btn btn-primary">Get a Free Estimate</a>
</div>
</section>
<div id="site-footer"></div>
<script src="/assets/js/components.js"></script>
<script src="/assets/js/main.js"></script>
</body>
</html>
"""
def locations_index():
city_cards = []
for i, city in enumerate(CITIES):
img = HERO_IMAGES[i % len(HERO_IMAGES)]
name = city["name"]
county = city["county"]
note = city["note"]
slug = city["slug"]
if " " in name:
accent_word, rest = name.split(" ", 1)
h3 = f'<span class="text-accent">{accent_word}</span> {rest}, NY'
else:
h3 = f'<span class="text-accent">{name}</span>, NY'
city_cards.append(f""" <div class="service-card">
<div class="service-image"><img src="{img}" alt="{name} NY"></div>
<h3 class="heading-3">{h3}</h3>
<div class="text-block-4">{county}</div>
<p class="paragraph-2">{note}</p>
<a href="/locations/{slug}/" class="btn btn-primary">View Services</a>
</div>""")
cards_html = "\n".join(city_cards)
return f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Service Areas | Lahr Carpet Cleaning | Finger Lakes, NY</title>
<meta name="description" content="Lahr Carpet Cleaning serves Waterloo, Geneva, Seneca Falls, Canandaigua, Penn Yan, and 16 more cities across the Finger Lakes region. Call 315-719-1218.">
<link rel="stylesheet" href="/assets/css/styles.css?v=5">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
</head>
<body>
<header class="header" id="header">
<div class="container"><div id="site-nav"></div></div>
</header>
<section class="page-hero" style="background-image: url('/assets/images/hero/hero-technician.jpg');">
<div class="container">
<div class="page-hero-content">
<span class="hero-eyebrow">Finger Lakes Region</span>
<h1 class="hero-title">Service<br><span class="text-accent">Areas</span></h1>
<p>We clean carpets, upholstery, rugs, and hard floors across 21 cities in Upstate New York. Select your city below.</p>
<div class="hero-actions">
<a href="/contact/" class="btn btn-primary btn-large">Book Now</a>
<a href="tel:315-719-1218" class="btn btn-ghost btn-large"><i class="fas fa-phone"></i> 315-719-1218</a>
</div>
</div>
</div>
</section>
<section class="services-overview">
<div class="container">
<div class="section-header">
<span class="section-label">Where We Work</span>
<h2 class="section-title">Cities We Serve</h2>
<p class="section-subtitle">Based in Waterloo, NY. We travel throughout Seneca, Ontario, Yates, Wayne, and Cayuga counties.</p>
</div>
<div class="services-grid">
{cards_html}
</div>
</div>
</section>
<section class="cta-banner">
<div class="cta-content">
<h2 class="heading-4"><strong>Not sure if we cover your area?</strong></h2>
<p class="paragraph-4">Call 315-719-1218 or submit the form and we will confirm availability for your address.</p>
<a href="/contact/" class="btn btn-primary">Get a Free Estimate</a>
</div>
</section>
<div id="site-footer"></div>
<script src="/assets/js/components.js"></script>
<script src="/assets/js/main.js"></script>
</body>
</html>
"""
if __name__ == "__main__":
# Write locations index
with open(os.path.join(LOC_DIR, "index.html"), "w") as f:
f.write(locations_index())
print("Wrote locations/index.html")
# Write each city page
for i, city in enumerate(CITIES):
city_dir = os.path.join(LOC_DIR, city["slug"])
os.makedirs(city_dir, exist_ok=True)
out_path = os.path.join(city_dir, "index.html")
with open(out_path, "w") as f:
f.write(page_html(city, i))
print(f"Wrote locations/{city['slug']}/index.html")
print(f"\nDone. {len(CITIES)} city pages + index generated.")
+60
View File
@@ -0,0 +1,60 @@
"""Generate the 2 missing service images."""
import os, sys
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")
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "services")
client = genai.Client(api_key=API_KEY)
TARGETS = [
{
"name": "property-management",
"prompt": (
"View across three clean empty apartment living rooms, each with spotlessly clean "
"beige carpet showing fresh extraction lines after professional hot water extraction cleaning. "
"Bright neutral interiors ready for new tenants. Natural light, no furniture, "
"no people, no equipment. Professional real estate photography, ultra-realistic."
),
},
{
"name": "commercial-overview",
"prompt": (
"Professional carpet cleaning technician in a plain black shirt, shown from the side, "
"pushing a large upright extraction machine through a bright commercial building lobby. "
"Clean bright carpet behind the machine. No steam, no water spraying, no face visible. "
"Professional editorial photography, ultra-realistic."
),
},
]
for item in TARGETS:
out_path = os.path.join(OUT_DIR, f"{item['name']}.jpg")
print(f"Generating {item['name']}...")
try:
resp = client.models.generate_images(
model="imagen-4.0-generate-001",
prompt=item["prompt"],
config=types.GenerateImagesConfig(
number_of_images=1,
aspect_ratio="4:3",
output_mime_type="image/jpeg",
safety_filter_level="block_low_and_above",
),
)
if resp.generated_images:
b = resp.generated_images[0].image.image_bytes
with open(out_path, "wb") as f:
f.write(b)
print(f" Saved ({len(b)//1024}KB)")
else:
print(f" No image returned")
except Exception as e:
print(f" Error: {e}")
print("Done.")
+179
View File
@@ -0,0 +1,179 @@
"""
Lahr Carpet Cleaning — Service card image generator.
Generates 12 unique images for residential and commercial service cards.
Saves to: assets/images/services/
Run: python3 tools/gen-service-images.py
"""
import os
import sys
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")
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "services")
os.makedirs(OUT_DIR, exist_ok=True)
client = genai.Client(api_key=API_KEY)
IMAGES = [
# ── Residential ──────────────────────────────────────────────────────────
{
"name": "carpet-cleaning",
"prompt": (
"Wide shot of a large industrial stand-up hot water extraction machine being pushed across "
"a plush beige residential carpet. The machine is a heavy commercial-grade upright extractor "
"on wheels — tall, wide cleaning head at the base, long upright handle. "
"The carpet behind it transitions from dirty and matted to clean, bright, and fluffy. "
"Completely dry machine exterior, no steam, no water spraying anywhere. "
"Warm natural interior light. Ultra-realistic professional photography."
),
},
{
"name": "stairs-cleaning",
"prompt": (
"Wide cinematic shot of a carpeted residential staircase. Each step has clean, "
"bright plush carpet that looks freshly cleaned with visible extraction lines. "
"Modern upstate New York home interior, natural light from above, "
"warm wood banisters, no people, no equipment visible. "
"Professional interior photography, ultra-realistic."
),
},
{
"name": "upholstery-cleaning",
"prompt": (
"Close-up of a clean grey linen sofa cushion showing bright, lifted fabric texture "
"after professional upholstery cleaning. Half the cushion shows the before "
"(slightly soiled, flat fabric) and half shows the cleaned result (bright, fluffy, refreshed). "
"Natural window light, residential living room in background, no people, no equipment. "
"Ultra-realistic product photography."
),
},
{
"name": "floor-cleaning",
"prompt": (
"Wide shot of a gleaming hardwood floor in a modern residential home after professional "
"cleaning. The floor reflects soft natural window light, showing deep grain detail. "
"Contemporary furniture in background, no people, no cleaning equipment visible. "
"Professional interior photography, ultra-realistic."
),
},
{
"name": "area-rug-cleaning",
"prompt": (
"Overhead flat-lay shot of a large vibrant Persian or oriental area rug "
"with rich red, navy, and cream geometric patterns, looking freshly cleaned — "
"colors vivid, fibers lifted and bright. Hardwood floor beneath. "
"No people, no equipment, no water. Professional product photography, ultra-realistic."
),
},
{
"name": "add-ons",
"prompt": (
"Close-up macro shot of clean carpet fibers being lifted by a professional "
"grooming brush after hot water extraction cleaning. The fibers are bright, "
"fluffy, and standing upright. Warm light catches the texture. "
"No steam, no water, no people. Ultra-realistic macro photography."
),
},
# ── Commercial ───────────────────────────────────────────────────────────
{
"name": "vacation-rentals",
"prompt": (
"Bright, airy vacation rental living room in the Finger Lakes region of upstate New York. "
"Spotlessly clean cream carpet, contemporary furniture, large windows with lake views, "
"warm natural afternoon light. Inviting and fresh. No people, no equipment. "
"Professional real estate photography, ultra-realistic."
),
},
{
"name": "office-spaces",
"prompt": (
"Wide shot of a clean modern corporate office with freshly cleaned dark charcoal carpet "
"throughout. Open plan workspace, glass partitions, professional lighting. "
"Carpet shows neat vacuum lines indicating recent professional cleaning. "
"No people, no equipment. Professional architectural photography, ultra-realistic."
),
},
{
"name": "hotels-inns",
"prompt": (
"Elegant hotel corridor in a boutique upstate New York inn. Clean, plush patterned "
"carpet runner down the hallway with fresh vacuum lines. Warm sconce lighting, "
"wood paneling, framed art on walls. No people, no equipment. "
"Professional hospitality photography, ultra-realistic."
),
},
{
"name": "retail-showrooms",
"prompt": (
"Wide shot of an upscale retail showroom or winery tasting room in the Finger Lakes. "
"Clean, rich carpet throughout, warm lighting, product displays on shelves. "
"Carpet looks freshly extracted — bright and spotless. "
"No people, no equipment. Professional commercial interior photography, ultra-realistic."
),
},
{
"name": "property-management",
"prompt": (
"View across three clean apartment living rooms in sequence, each showing "
"spotlessly clean beige carpet with fresh vacuum lines after professional cleaning. "
"Bright, neutral interiors ready for new tenants. Natural light, no furniture, "
"no people, no equipment. Professional real estate photography, ultra-realistic."
),
},
{
"name": "commercial-overview",
"prompt": (
"Professional carpet cleaning technician in a plain black shirt, shown from the side, "
"pushing a large industrial stand-up hot water extraction machine through a bright commercial "
"building lobby. The machine is a heavy commercial-grade upright extractor on wheels — "
"tall, wide cleaning head, long handle. Clean carpet visible. No steam, no water spraying, "
"no face visible. Professional editorial photography, ultra-realistic."
),
},
]
def generate():
saved = []
total = len(IMAGES)
for i, item in enumerate(IMAGES, 1):
out_path = os.path.join(OUT_DIR, f"{item['name']}.jpg")
print(f"[{i}/{total}] Generating {item['name']}...")
try:
resp = client.models.generate_images(
model="imagen-4.0-generate-001",
prompt=item["prompt"],
config=types.GenerateImagesConfig(
number_of_images=1,
aspect_ratio="4:3",
output_mime_type="image/jpeg",
safety_filter_level="block_low_and_above",
),
)
if resp.generated_images:
img_bytes = resp.generated_images[0].image.image_bytes
with open(out_path, "wb") as f:
f.write(img_bytes)
print(f" Saved {out_path} ({len(img_bytes)//1024}KB)")
saved.append(item["name"])
else:
print(f" No image returned for {item['name']}")
except Exception as e:
print(f" Error on {item['name']}: {e}")
return saved
if __name__ == "__main__":
saved = generate()
print(f"\nDone. {len(saved)}/{len(IMAGES)} images saved to assets/images/services/")
if saved:
print("Generated:", ", ".join(saved))
+220
View File
@@ -0,0 +1,220 @@
"""
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")
+142
View File
@@ -0,0 +1,142 @@
"""
Regen shot-04, shot-06, shot-07 with corrected scenes.
shot-04: carpet before/after reveal, no machine
shot-06: clean bright staircase, no machine
shot-07: bright modern office, no dark tones
"""
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)
SHOTS = [
{
"name": "shot-04-extraction-carpet",
"prompt": (
"Cinematic slow-motion wide shot. Camera glides low across a residential living room carpet. "
"The left half of the carpet is visibly dirty, stained, and matted. "
"The right half is bright, clean, fluffy, and freshly extracted. "
"The boundary between dirty and clean is sharp and dramatic. "
"Warm natural afternoon light. No people. No machines. No equipment. Photorealistic."
),
},
{
"name": "shot-06-extraction-stairs",
"prompt": (
"Cinematic slow-motion shot looking up a bright residential carpeted staircase. "
"Each step has clean, bright, plush beige carpet with fresh extraction lines. "
"Warm natural light from above illuminates the stairs. Wood banisters on each side. "
"No people. No machines. No equipment anywhere in frame. Photorealistic."
),
},
{
"name": "shot-07-office-entryway",
"prompt": (
"Wide cinematic shot of a bright modern commercial office building lobby. "
"Large windows let in abundant natural daylight. Clean beige or grey commercial carpet throughout. "
"White walls, professional lighting, glass doors, contemporary furniture. "
"The carpet looks spotlessly clean with neat vacuum lines. "
"No people. No machines. No dark tones — the space is bright and well-lit. 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']}...")
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)")
continue
if op.response and op.response.generated_videos:
vid = op.response.generated_videos[0].video
video_bytes = client.files.download(file=vid)
if video_bytes:
with open(out_path, "wb") as f:
f.write(video_bytes)
print(f" Saved ({os.path.getsize(out_path)//1024}KB)")
saved.append(item["name"])
else:
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")
except Exception as e:
print(f" Error: {e}")
print(f"\n{len(saved)}/{len(SHOTS)} shots saved: {saved}")
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: {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 ({os.path.getsize(REEL_OUT)//1024}KB)")
else:
print(f" ffmpeg error: {result.stderr[-400:]}")
print("\nDone.")
+55
View File
@@ -0,0 +1,55 @@
"""Regenerate commercial-overview.jpg with a clear commercial carpet scene."""
import os, sys
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")
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "services")
client = genai.Client(api_key=API_KEY)
PROMPTS = [
(
"Wide shot of a modern commercial office building lobby with clean grey carpet throughout. "
"Professional corporate interior, glass doors, white walls, overhead lighting. "
"The carpet is spotless and freshly cleaned — uniform, well-maintained. "
"No people. No machines. No equipment. Professional architectural photography, ultra-realistic."
),
(
"Wide interior shot of a bright commercial building corridor with clean, dark grey commercial carpet. "
"Modern office environment, glass partitions, professional lighting. "
"The carpet looks freshly cleaned and spotless. "
"No people, no equipment. Professional photography, ultra-realistic."
),
]
out_path = os.path.join(OUT_DIR, "commercial-overview.jpg")
for i, prompt in enumerate(PROMPTS):
print(f"Attempt {i+1}...")
try:
resp = client.models.generate_images(
model="imagen-4.0-generate-001",
prompt=prompt,
config=types.GenerateImagesConfig(
number_of_images=1, aspect_ratio="4:3",
output_mime_type="image/jpeg",
safety_filter_level="block_low_and_above",
),
)
if resp.generated_images:
b = resp.generated_images[0].image.image_bytes
with open(out_path, "wb") as f:
f.write(b)
print(f"Saved ({len(b)//1024}KB)")
break
else:
print("No image returned")
except Exception as e:
print(f"Error: {e}")
print("Done.")
+163
View File
@@ -0,0 +1,163 @@
"""
Full hero reel regeneration — 7-shot narrative arc.
1. Door opens, muddy boots run in
2. Mud tracked across carpet
3. Stain on upholstered chair
4. Carpet cleaning machine extracting dirt
5. Clean bright staircase
6. Office building wide carpet
7. Restaurant with carpet
"""
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")
os.makedirs(VID_DIR, exist_ok=True)
client = genai.Client(api_key=API_KEY)
SHOTS = [
{
"name": "v2-shot-01-door-entry",
"prompt": (
"Cinematic slow-motion wide shot. A wooden front door of an upstate New York home swings open. "
"A child and an adult walk inside wearing muddy boots. Camera stays low at floor level. "
"The boots leave dark muddy tracks across the beige carpet in the entryway with each step. "
"Warm afternoon light pours through the open door. Photorealistic."
),
},
{
"name": "v2-shot-02-mud-on-carpet",
"prompt": (
"Extreme close-up slow-motion shot at carpet level. Muddy boot soles press into clean beige carpet, "
"leaving dark brown mud stains and wet footprints with each step. "
"Camera is low, tight on the boots and the mud soaking into carpet fibers. "
"Dramatic side lighting. No faces visible. Photorealistic."
),
},
{
"name": "v2-shot-03-stain-on-chair",
"prompt": (
"Close-up cinematic shot of a light grey upholstered armchair. "
"A visible dark stain spreads across one cushion. "
"Camera slowly pushes in on the stain, showing the soiled fabric texture. "
"Warm natural light from a window. No people. No equipment. Photorealistic."
),
},
{
"name": "v2-shot-04-extraction-carpet",
"prompt": (
"Cinematic slow-motion wide shot. A technician pushes a Rug Doctor style carpet cleaning machine "
"steadily forward across a beige living room carpet. The machine is a tall upright unit with a handle "
"and flat rectangular cleaning head — like a large upright vacuum cleaner. "
"The carpet behind the machine is visibly brighter and cleaner than the carpet ahead of it. "
"No steam. No water spraying. Warm room light. Photorealistic."
),
},
{
"name": "v2-shot-05-clean-stairs",
"prompt": (
"Cinematic slow-motion shot looking up a bright residential carpeted staircase. "
"Each step has clean, fresh, plush beige carpet. Warm natural light from above. "
"Wood banisters on the sides. The carpet looks spotless and freshly cleaned. "
"No people. No machines. Photorealistic."
),
},
{
"name": "v2-shot-06-office",
"prompt": (
"Wide cinematic shot of a bright modern commercial office lobby. "
"Large windows, abundant natural daylight. Clean grey commercial carpet covers the entire floor. "
"White walls, glass partitions, professional lighting. Carpet looks spotlessly clean. "
"No people. No machines. Photorealistic."
),
},
{
"name": "v2-shot-07-restaurant",
"prompt": (
"Wide cinematic shot of an upscale restaurant dining room with carpeted floors. "
"Warm ambient lighting, white tablecloths, wood accents. "
"The carpet is clean, rich, and well-maintained throughout the space. "
"No people. No machines. Photorealistic, luxurious atmosphere."
),
},
]
MODEL = "veo-3.1-generate-preview"
def poll(op, timeout=600):
elapsed = 0
while not op.done:
if elapsed >= timeout:
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[{SHOTS.index(item)+1}/{len(SHOTS)}] {item['name']}...")
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 and op.response and op.response.generated_videos:
vid = op.response.generated_videos[0].video
video_bytes = client.files.download(file=vid)
if video_bytes:
with open(out_path, "wb") as f:
f.write(video_bytes)
print(f" Saved ({os.path.getsize(out_path)//1024}KB)")
saved.append(out_path)
else:
try:
vid.save(out_path)
print(f" Saved via .save()")
saved.append(out_path)
except Exception as e2:
print(f" Download failed: {e2}")
else:
print(f" No video returned")
except Exception as e:
print(f" Error: {e}")
print(f"\n{len(saved)}/{len(SHOTS)} shots saved")
if len(saved) < 2:
print("Not enough clips — skipping reconcat.")
else:
order = [os.path.join(VID_DIR, f"{s['name']}.mp4") for s in SHOTS
if os.path.exists(os.path.join(VID_DIR, f"{s['name']}.mp4"))]
concat_file = os.path.join(VID_DIR, "concat-v2.txt")
with open(concat_file, "w") as f:
for p in order:
f.write(f"file '{p}'\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 ({os.path.getsize(REEL_OUT)//1024}KB)")
else:
print(f" ffmpeg error: {result.stderr[-300:]}")
print("\nDone.")
+64
View File
@@ -0,0 +1,64 @@
"""Regenerate only hero-technician and hero-before-after images."""
import os, sys
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")
OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "hero")
client = genai.Client(api_key=API_KEY)
TARGETS = [
{
"name": "hero-technician",
"aspect": "16:9",
"prompt": (
"Professional carpet cleaning technician pushing a large upright hot water "
"extraction machine across residential carpet in a bright modern home interior. "
"Machine resembles an oversized upright vacuum cleaner with a cylindrical body. "
"No steam visible anywhere, no water spraying, no hoses visible, completely dry. "
"Technician shown from behind or side, no face, plain black shirt, no logo. "
"High-end professional photography."
),
},
{
"name": "hero-before-after",
"aspect": "16:9",
"prompt": (
"Side-by-side residential living room carpet: left half heavily soiled with mud "
"and dark stains, right half same carpet after professional hot water extraction "
"cleaning, bright and pristine. No steam, no water, no machines visible anywhere. "
"Dramatic before-after contrast. Professional photography, no people."
),
},
]
for item in TARGETS:
out_path = os.path.join(OUT_DIR, f"{item['name']}.jpg")
print(f"Generating {item['name']}...")
try:
resp = client.models.generate_images(
model="imagen-4.0-generate-001",
prompt=item["prompt"],
config=types.GenerateImagesConfig(
number_of_images=1,
aspect_ratio=item["aspect"],
output_mime_type="image/jpeg",
safety_filter_level="block_low_and_above",
),
)
if resp.generated_images:
img_bytes = resp.generated_images[0].image.image_bytes
with open(out_path, "wb") as f:
f.write(img_bytes)
print(f" Saved {out_path} ({len(img_bytes)//1024}KB)")
else:
print(f" No image returned for {item['name']}")
except Exception as e:
print(f" Error: {e}")
print("\nDone.")
+169
View File
@@ -0,0 +1,169 @@
"""Regenerate carpet-cleaning and commercial-overview service images with industrial extractor prompts."""
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__))
IMG_DIR = os.path.join(BASE_DIR, "assets", "images", "services")
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)
# ── Service images ────────────────────────────────────────────────────────────
IMAGES = [
{
"name": "carpet-cleaning",
"prompt": (
"Wide shot of a large industrial stand-up hot water extraction machine being pushed across "
"a plush beige residential carpet. The machine is a heavy commercial-grade upright extractor "
"on wheels — tall, wide cleaning head at the base, long upright handle. "
"The carpet behind it transitions from dirty and matted to clean, bright, and fluffy. "
"Completely dry machine exterior, no steam, no water spraying anywhere. "
"Warm natural interior light. Ultra-realistic professional photography."
),
},
{
"name": "commercial-overview",
"prompt": (
"Professional carpet cleaning technician in a plain black shirt, shown from the side, "
"pushing a large industrial stand-up hot water extraction machine through a bright commercial "
"building lobby. The machine is a heavy commercial-grade upright extractor on wheels — "
"tall, wide cleaning head, long handle. Clean carpet visible. No steam, no water spraying, "
"no face visible. Professional editorial photography, ultra-realistic."
),
},
]
for item in IMAGES:
out_path = os.path.join(IMG_DIR, f"{item['name']}.jpg")
print(f"[IMG] Generating {item['name']}...")
try:
resp = client.models.generate_images(
model="imagen-4.0-generate-001",
prompt=item["prompt"],
config=types.GenerateImagesConfig(
number_of_images=1, aspect_ratio="4:3",
output_mime_type="image/jpeg",
safety_filter_level="block_low_and_above",
),
)
if resp.generated_images:
b = resp.generated_images[0].image.image_bytes
with open(out_path, "wb") as f:
f.write(b)
print(f" Saved {out_path} ({len(b)//1024}KB)")
else:
print(f" No image returned")
except Exception as e:
print(f" Error: {e}")
# ── Video shots ───────────────────────────────────────────────────────────────
SHOTS = [
{
"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-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(op, timeout=420):
elapsed = 0
while not op.done:
if elapsed >= timeout:
return None
print(f" Waiting... ({elapsed}s)")
time.sleep(15)
elapsed += 15
op = client.operations.get(op)
return op
saved_clips = []
for item in SHOTS:
out_path = os.path.join(VID_DIR, f"{item['name']}.mp4")
print(f"\n[VID] 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 and op.response and op.response.generated_videos:
vid = op.response.generated_videos[0].video
try:
b = client.files.download(file=vid)
except Exception:
b = None
if b:
with open(out_path, "wb") as f:
f.write(b)
print(f" Saved ({os.path.getsize(out_path)//1024}KB)")
saved_clips.append(item["name"])
done = True
break
except Exception as e:
print(f" Error with {model}: {e}")
if not done:
print(f" FAILED: {item['name']}")
# ── Reconcat reel if both shots regenerated ───────────────────────────────────
if len(saved_clips) == 2:
print("\nReconcatenating reel...")
concat_file = os.path.join(VID_DIR, "concat.txt")
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",
]
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 ({os.path.getsize(REEL_OUT)//1024}KB)")
else:
print(f" ffmpeg error: {result.stderr[-300:]}")
else:
print(f"\nOnly {len(saved_clips)}/2 video shots regenerated — skipping reconcat.")
print("\nDone.")
+87
View File
@@ -0,0 +1,87 @@
"""
Regenerate one specific shot and re-concatenate the hero reel.
Usage: python3 tools/regen-shot.py shot-02-staircase
"""
import os, sys, time, subprocess
from google import genai
from google.genai import types
import urllib.request
API_KEY = os.environ.get("GEMINI_API_KEY", "")
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
CLIPS_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips")
REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4")
if not API_KEY:
print("Set GEMINI_API_KEY env var"); sys.exit(1)
# Import SHOTS from gen-video.py (dash in name requires importlib)
import importlib.util
spec = importlib.util.spec_from_file_location(
"gen_video", os.path.join(os.path.dirname(__file__), "gen-video.py")
)
_mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(_mod)
SHOTS = _mod.SHOTS
shot_name = sys.argv[1] if len(sys.argv) > 1 else None
target = next((s for s in SHOTS if s["name"] == shot_name), None)
if not target:
print(f"Shot '{shot_name}' not found. Available: {[s['name'] for s in SHOTS]}")
sys.exit(1)
client = genai.Client(api_key=API_KEY)
out_path = os.path.join(CLIPS_DIR, f"{target['name']}.mp4")
print(f"Regenerating {target['name']}...")
op = client.models.generate_videos(
model="veo-3.0-generate-001",
prompt=target["prompt"],
config=types.GenerateVideosConfig(
aspect_ratio="16:9", resolution="720p",
duration_seconds=6, number_of_videos=1,
),
)
elapsed = 0
while not op.done:
print(f" Waiting... ({elapsed}s)")
time.sleep(15); elapsed += 15
op = client.operations.get(op)
if not (op.response and op.response.generated_videos):
print("No video returned"); sys.exit(1)
vid = op.response.generated_videos[0].video
video_bytes = None
try:
video_bytes = client.files.download(file=vid)
except Exception:
pass
if video_bytes:
with open(out_path, "wb") as f: f.write(video_bytes)
elif hasattr(vid, "uri") and vid.uri:
uri = vid.uri + ("&" if "?" in vid.uri else "?") + f"key={API_KEY}"
urllib.request.urlretrieve(uri, out_path)
else:
print("Download failed"); sys.exit(1)
print(f"Saved {out_path} ({os.path.getsize(out_path)//1024}KB)")
# Re-concat in shot order
clips = [os.path.join(CLIPS_DIR, f"{s['name']}.mp4") for s in SHOTS
if os.path.exists(os.path.join(CLIPS_DIR, f"{s['name']}.mp4"))]
list_file = os.path.join(CLIPS_DIR, "concat.txt")
with open(list_file, "w") as f:
for c in clips: f.write(f"file '{c}'\n")
print(f"Concatenating {len(clips)} clips...")
r = 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 r.returncode == 0:
print(f"Reel updated: {REEL_OUT} ({os.path.getsize(REEL_OUT)//1024}KB)")
else:
print(f"ffmpeg error: {r.stderr[-200:]}")
+102
View File
@@ -0,0 +1,102 @@
"""Generate shot-02 replacement: kid runs in with muddy shoes, focus on feet and mud tracks."""
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)
PROMPTS = [
(
"Slow cinematic shot at floor level, looking across a beige residential carpet. "
"A child's feet in muddy sneakers run into frame from the front door and across the carpet, "
"leaving clear muddy footprints with each step. Then adult feet in boots walk in behind, "
"adding more dirt and mud tracks. Camera stays low, focused on the feet and the mud on the carpet. "
"Warm indoor light. Photorealistic, slow motion."
),
(
"Low cinematic camera angle at carpet level inside a home entryway. "
"A child runs in wearing muddy shoes, close-up on their feet stomping mud into the beige carpet. "
"Adult legs follow behind, tracking in more dirt. "
"The carpet shows fresh mud and dirty footprints after each step. "
"Camera stays at floor level throughout. Warm natural light. Slow motion. Photorealistic."
),
]
MODEL = "veo-3.1-generate-preview"
out_path = os.path.join(VID_DIR, "shot-02-pan-to-stains.mp4")
def poll(op, timeout=600):
elapsed = 0
while not op.done:
if elapsed >= timeout:
return None
print(f" Waiting... ({elapsed}s)")
time.sleep(15)
elapsed += 15
op = client.operations.get(op)
return op
saved = False
for i, prompt in enumerate(PROMPTS):
print(f"\n[VID] shot-02 attempt {i+1}...")
try:
op = client.models.generate_videos(
model=MODEL,
prompt=prompt,
config=types.GenerateVideosConfig(
aspect_ratio="16:9", resolution="720p",
duration_seconds=6, number_of_videos=1,
),
)
op = poll(op)
if op and op.response and op.response.generated_videos:
vid = op.response.generated_videos[0].video
video_bytes = client.files.download(file=vid)
if video_bytes:
with open(out_path, "wb") as f:
f.write(video_bytes)
print(f" Saved ({os.path.getsize(out_path)//1024}KB)")
saved = True
break
else:
print(f" No video returned, trying next prompt...")
except Exception as e:
print(f" Error: {e}")
if not saved:
print("All attempts failed — original shot-02 kept.")
else:
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"Skipping reconcat — missing: {missing}")
else:
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 ({os.path.getsize(REEL_OUT)//1024}KB)")
else:
print(f" ffmpeg error: {result.stderr[-300:]}")
print("\nDone.")
+95
View File
@@ -0,0 +1,95 @@
"""Retry shot-04 with a simple clean carpet scene."""
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)
PROMPTS = [
(
"Cinematic slow dolly shot moving forward through a bright residential living room. "
"Clean, plush beige carpet fills the floor. Warm afternoon sunlight comes through large windows. "
"Contemporary furniture, neutral walls. The carpet looks freshly cleaned — bright, fluffy, spotless. "
"No people. No machines. Photorealistic."
),
(
"Slow cinematic camera pan across a clean beige residential carpet in a bright living room. "
"The carpet fibers are lifted and bright after professional cleaning. "
"Warm natural light. Modern home interior. No people, no equipment. Photorealistic."
),
]
MODEL = "veo-3.1-generate-preview"
out_path = os.path.join(VID_DIR, "shot-04-extraction-carpet.mp4")
def poll(op, timeout=600):
elapsed = 0
while not op.done:
if elapsed >= timeout:
return None
print(f" Waiting... ({elapsed}s)")
time.sleep(15)
elapsed += 15
op = client.operations.get(op)
return op
saved = False
for i, prompt in enumerate(PROMPTS):
print(f"\n[VID] shot-04 attempt {i+1}...")
try:
op = client.models.generate_videos(
model=MODEL,
prompt=prompt,
config=types.GenerateVideosConfig(
aspect_ratio="16:9", resolution="720p",
duration_seconds=6, number_of_videos=1,
),
)
op = poll(op)
if op and op.response and op.response.generated_videos:
vid = op.response.generated_videos[0].video
video_bytes = client.files.download(file=vid)
if video_bytes:
with open(out_path, "wb") as f:
f.write(video_bytes)
print(f" Saved ({os.path.getsize(out_path)//1024}KB)")
saved = True
break
else:
print(f" No video returned, trying next prompt...")
except Exception as e:
print(f" Error: {e}")
if not saved:
print("All attempts failed.")
else:
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",
]
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 ({os.path.getsize(REEL_OUT)//1024}KB)")
else:
print(f" ffmpeg error: {result.stderr[-300:]}")
print("\nDone.")
+98
View File
@@ -0,0 +1,98 @@
"""Replace v3-shot-04 with a clean sofa result — no cleaning action, no equipment, no steam."""
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)
PROMPTS = [
(
"Close-up cinematic shot slowly pulling back from a clean, bright grey upholstered sofa cushion. "
"The fabric is fresh, fluffy, and spotless. Warm natural window light. "
"No people. No machines. No equipment of any kind. No steam. No water. "
"Just a beautiful clean sofa in a bright living room. Photorealistic."
),
(
"Slow cinematic pan across a clean living room. A light grey sofa with pristine cushions. "
"Bright clean beige carpet on the floor. Warm afternoon light. "
"No people. No machines. No equipment. Photorealistic."
),
]
MODEL = "veo-3.1-generate-preview"
out_path = os.path.join(VID_DIR, "v3-shot-04.mp4")
def poll(op, timeout=600):
elapsed = 0
while not op.done:
if elapsed >= timeout:
return None
print(f" Waiting... ({elapsed}s)")
time.sleep(15)
elapsed += 15
op = client.operations.get(op)
return op
saved = False
for i, prompt in enumerate(PROMPTS):
print(f"\n[v3-shot-04] attempt {i+1}...")
try:
op = client.models.generate_videos(
model=MODEL,
prompt=prompt,
config=types.GenerateVideosConfig(
aspect_ratio="16:9", resolution="720p",
duration_seconds=6, number_of_videos=1,
),
)
op = poll(op)
if op and op.response and op.response.generated_videos:
vid = op.response.generated_videos[0].video
video_bytes = client.files.download(file=vid)
if video_bytes:
with open(out_path, "wb") as f:
f.write(video_bytes)
print(f" Saved ({os.path.getsize(out_path)//1024}KB)")
saved = True
break
else:
print(f" No video returned")
except Exception as e:
print(f" Error: {e}")
if saved:
clips = [
"v3-shot-01", "v3-shot-02", "v3-shot-03", "v3-shot-04",
"v3-shot-05", "v3-shot-06", "v3-shot-07",
]
missing = [n for n in clips if not os.path.exists(os.path.join(VID_DIR, f"{n}.mp4"))]
if missing:
print(f"Skipping reconcat — missing: {missing}")
else:
concat_file = os.path.join(VID_DIR, "concat-v3.txt")
with open(concat_file, "w") as f:
for n in clips:
f.write(f"file '{os.path.join(VID_DIR, n)}.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 ({os.path.getsize(REEL_OUT)//1024}KB)")
else:
print(f" ffmpeg error: {result.stderr[-300:]}")
else:
print("Failed — shot-04 unchanged.")
print("\nDone.")
+153
View File
@@ -0,0 +1,153 @@
"""
Hero reel v3 — 7 shots with corrected prompts.
"""
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")
os.makedirs(VID_DIR, exist_ok=True)
client = genai.Client(api_key=API_KEY)
SHOTS = [
{
"name": "v3-shot-01",
"prompt": (
"Medium shot. A family of four — two adults and two children — walks through the front door "
"of a warm residential home. Camera is inside the home facing them as they enter. "
"After they walk in, the camera slowly pans down to the carpet, revealing dirty footprints "
"and mud tracked onto the beige carpet. Warm natural light. Photorealistic."
),
},
{
"name": "v3-shot-02",
"prompt": (
"Medium shot, slow zoom in. A glass of red wine has spilled onto a light grey upholstered sofa. "
"The camera starts wide on the sofa then slowly zooms in on the dark wine stain spreading "
"across the fabric. The stain is clearly visible, soaked into the cushion. "
"Warm living room light. No people. Photorealistic."
),
},
{
"name": "v3-shot-03",
"prompt": (
"Medium shot at low angle, camera near floor level. Inside a bright commercial office entryway. "
"A person wearing work boots walks through the entry door toward the camera and past it. "
"The camera is low, showing the boots and lower legs as they walk across the carpet past the lens. "
"Office carpet visible, natural light from glass doors behind. Photorealistic."
),
},
{
"name": "v3-shot-04",
"prompt": (
"Close-up cinematic shot. A technician's gloved hand holds a small upholstery extraction wand — "
"a flat rectangular handheld tool. The technician presses the wand firmly onto a wine-stained "
"sofa cushion and draws it slowly across the fabric. The stain visibly lifts as the wand moves. "
"Suction only — no water sprays out. Slow motion. Natural light. Photorealistic."
),
},
{
"name": "v3-shot-05",
"prompt": (
"Wide cinematic shot. Camera slowly pans across the entrance of a modern commercial office building. "
"Clean grey commercial carpet stretches across the lobby floor. Large windows, glass doors, "
"professional lighting. The carpet looks freshly cleaned and spotless. "
"No people. No machines. Photorealistic."
),
},
{
"name": "v3-shot-06",
"prompt": (
"Wide cinematic shot. Camera slowly pans across a bright, clean residential living room. "
"Plush clean beige carpet throughout. Comfortable furniture — sofa, armchairs, coffee table. "
"Warm natural afternoon light through large windows. The room looks fresh and inviting. "
"No people. No cleaning equipment. Photorealistic."
),
},
{
"name": "v3-shot-07",
"prompt": (
"Wide cinematic shot. Camera moves slowly forward through an upscale restaurant dining room. "
"Rich carpet covers the floor. White tablecloths, warm ambient lighting, wood accents. "
"The carpet looks clean, deep, and well-maintained as the camera glides through the space. "
"No people. Photorealistic, luxurious atmosphere."
),
},
]
MODEL = "veo-3.1-generate-preview"
def poll(op, timeout=600):
elapsed = 0
while not op.done:
if elapsed >= timeout:
return None
print(f" Waiting... ({elapsed}s)")
time.sleep(15)
elapsed += 15
op = client.operations.get(op)
return op
saved = []
for i, item in enumerate(SHOTS):
out_path = os.path.join(VID_DIR, f"{item['name']}.mp4")
print(f"\n[{i+1}/{len(SHOTS)}] {item['name']}...")
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 and op.response and op.response.generated_videos:
vid = op.response.generated_videos[0].video
video_bytes = client.files.download(file=vid)
if video_bytes:
with open(out_path, "wb") as f:
f.write(video_bytes)
print(f" Saved ({os.path.getsize(out_path)//1024}KB)")
saved.append(out_path)
else:
try:
vid.save(out_path)
print(f" Saved via .save()")
saved.append(out_path)
except Exception as e2:
print(f" Download failed: {e2}")
else:
print(f" No video returned")
except Exception as e:
print(f" Error: {e}")
print(f"\n{len(saved)}/{len(SHOTS)} shots saved")
if len(saved) >= 2:
concat_file = os.path.join(VID_DIR, "concat-v3.txt")
clips = [os.path.join(VID_DIR, f"{s['name']}.mp4") for s in SHOTS
if os.path.exists(os.path.join(VID_DIR, f"{s['name']}.mp4"))]
with open(concat_file, "w") as f:
for p in clips:
f.write(f"file '{p}'\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 ({os.path.getsize(REEL_OUT)//1024}KB)")
else:
print(f" ffmpeg error: {result.stderr[-300:]}")
print("\nDone.")
+154
View File
@@ -0,0 +1,154 @@
"""Hero reel v4 — 6 precise shots."""
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")
os.makedirs(VID_DIR, exist_ok=True)
client = genai.Client(api_key=API_KEY)
SHOTS = [
{
"name": "v4-shot-01",
"prompt": (
"Medium cinematic shot. A family — two adults and two children — walks through a front door "
"into a residential home. The camera follows their feet as they step onto the beige carpet "
"in the entryway, then slowly pans down to show their shoes leaving dirty tracks on the carpet. "
"Warm afternoon light. Photorealistic, slow motion."
),
},
{
"name": "v4-shot-02",
"prompt": (
"Slow-motion close-up cinematic shot. A wine glass tips over on a light grey fabric sofa cushion. "
"Red wine pours out of the glass and spreads across the sofa cushion, soaking into the fabric. "
"The dark red stain expands slowly across the grey upholstery. "
"Warm living room light. No people visible. Photorealistic, dramatic slow motion."
),
},
{
"name": "v4-shot-03",
"prompt": (
"Cinematic close-up shot slowly pushing in on a section of heavily soiled residential carpet. "
"The beige carpet has multiple visible stains — dark spots, discoloration, pet stains, "
"and general dirt buildup embedded in the fibers. "
"Dramatic side lighting emphasizes the depth of the stains. No people. No equipment. Photorealistic."
),
},
{
"name": "v4-shot-04",
"prompt": (
"Close-up cinematic shot. A carpet cleaning technician in a plain black shirt pushes "
"a large upright carpet cleaning machine — like a Rug Doctor — across a dirty beige carpet. "
"Tight shot focused on the wide flat cleaning head at the base of the machine pressing against "
"the carpet and moving forward. The carpet behind the machine looks visibly cleaner and brighter. "
"The machine only pulls dirt and moisture INTO itself — nothing comes out. "
"No steam. No liquid leaving the machine. Photorealistic slow motion."
),
},
{
"name": "v4-shot-05",
"prompt": (
"Wide cinematic shot slowly pushing forward through the main entrance of a modern commercial "
"office building. Clean grey carpet covers the entire lobby floor. Glass doors, white walls, "
"professional overhead lighting. The carpet is the visual centerpiece — clean, uniform, well-maintained. "
"No people. No machines. Photorealistic."
),
},
{
"name": "v4-shot-06",
"prompt": (
"Cinematic wide shot inside a bright clean residential living room. "
"The camera slowly pans upward from the clean plush beige carpet to reveal the whole room. "
"A family — two adults and a child — walks in at different moments and relaxes on the sofa. "
"Everyone is wearing socks. The carpet is spotless and fluffy. Warm natural light. "
"Comfortable, inviting atmosphere. Photorealistic."
),
},
]
MODELS = ["veo-3.1-generate-preview", "veo-2.0-generate-001"]
def poll(op, timeout=600):
elapsed = 0
while not op.done:
if elapsed >= timeout:
return None
print(f" Waiting... ({elapsed}s)")
time.sleep(15)
elapsed += 15
op = client.operations.get(op)
return op
saved = []
for i, item in enumerate(SHOTS):
out_path = os.path.join(VID_DIR, f"{item['name']}.mp4")
print(f"\n[{i+1}/{len(SHOTS)}] {item['name']}...")
done = False
for model in MODELS:
try:
print(f" Trying {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 and op.response and op.response.generated_videos:
vid = op.response.generated_videos[0].video
video_bytes = client.files.download(file=vid)
if video_bytes:
with open(out_path, "wb") as f:
f.write(video_bytes)
print(f" Saved ({os.path.getsize(out_path)//1024}KB)")
saved.append(out_path)
done = True
break
else:
try:
vid.save(out_path)
print(f" Saved via .save()")
saved.append(out_path)
done = True
break
except Exception as e2:
print(f" Download failed: {e2}")
else:
print(f" No video returned from {model}")
except Exception as e:
print(f" Error with {model}: {e}")
if not done:
print(f" FAILED: {item['name']}")
print(f"\n{len(saved)}/{len(SHOTS)} shots saved")
if len(saved) >= 2:
clips = [os.path.join(VID_DIR, f"{s['name']}.mp4") for s in SHOTS
if os.path.exists(os.path.join(VID_DIR, f"{s['name']}.mp4"))]
concat_file = os.path.join(VID_DIR, "concat-v4.txt")
with open(concat_file, "w") as f:
for p in clips:
f.write(f"file '{p}'\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 ({os.path.getsize(REEL_OUT)//1024}KB)")
else:
print(f" ffmpeg error: {result.stderr[-300:]}")
print("\nDone.")
+167
View File
@@ -0,0 +1,167 @@
"""
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.")