68d29ae532
- Fix mobile nav: all 3 dropdowns now get click handlers (was only first) - Remove Our Work from nav - Add Google Maps embed to homepage footer - Update title and meta description/keywords/canonical - Unique hero image per page (14 pages updated) - Remove technician clip from hero reel - Add .cpanel.yml for cPanel Git deployment - Add hero image generation script (ComfyUI SDXL) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
86 lines
3.0 KiB
Python
86 lines
3.0 KiB
Python
"""
|
|
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
|
|
"v3-shot-03", # 3 - dirty stained carpet close-up
|
|
"v3-shot-06", # 4 - living room clean carpet pan
|
|
"v3-shot-07", # 5 - restaurant carpet glide
|
|
"v3-shot-05", # 6 - office lobby carpet pan
|
|
"v2-shot-05-clean-stairs", # 7 - clean bright staircase
|
|
"v2-shot-07-restaurant", # 8 - restaurant carpet
|
|
"v2-shot-06-office", # 9 - bright office carpet
|
|
"shot-01-wide-room", # 10 - wide room establishing
|
|
"shot-05-clean-reveal", # 11 - clean reveal
|
|
"shot-04-extraction-carpet",# 12 - final reveal
|
|
]
|
|
|
|
# Duration for each clip
|
|
DURATIONS = [
|
|
4.5, # 1 wine spill — cut at 4.5s
|
|
5.0, # 2
|
|
4.0, # 3 --- building pace ---
|
|
4.0, # 4
|
|
4.0, # 5
|
|
3.5, # 6
|
|
3.5, # 7
|
|
2.5, # 8 --- rapid ---
|
|
2.5, # 9
|
|
2.5, # 10
|
|
2.5, # 11
|
|
6.0, # 12 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:]}")
|