diff --git a/.cpanel.yml b/.cpanel.yml index e88f51e..a072119 100644 --- a/.cpanel.yml +++ b/.cpanel.yml @@ -7,7 +7,6 @@ deployment: - /bin/cp -r commercial "$DEPLOYPATH" - /bin/cp -r contact "$DEPLOYPATH" - /bin/cp -r locations "$DEPLOYPATH" - - /bin/cp -r our-work "$DEPLOYPATH" - /bin/cp -r reviews "$DEPLOYPATH" - /bin/cp -r service-area "$DEPLOYPATH" - /bin/cp -r services "$DEPLOYPATH" @@ -16,3 +15,4 @@ deployment: - /bin/cp 500.html "$DEPLOYPATH" - /bin/cp robots.txt "$DEPLOYPATH" - /bin/cp sitemap.xml "$DEPLOYPATH" + - /bin/cp .htaccess "$DEPLOYPATH" diff --git a/.gitignore b/.gitignore index 8383823..fed7c46 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ node_modules/ build/ .DS_Store *.log +__pycache__/ +*.pyc diff --git a/.htaccess b/.htaccess new file mode 100644 index 0000000..b34eefa --- /dev/null +++ b/.htaccess @@ -0,0 +1,14 @@ +Options -Indexes +RewriteEngine On + +# Deny sensitive files + + Order allow,deny + Deny from all + + +# Deny tools directory +RewriteRule ^tools/ - [F,L] + +ErrorDocument 404 /404.html +ErrorDocument 500 /500.html diff --git a/assets/images/hero/hero-about.jpg b/assets/images/hero/hero-about.jpg deleted file mode 100644 index df4b8a8..0000000 Binary files a/assets/images/hero/hero-about.jpg and /dev/null differ diff --git a/assets/images/hero/hero-about.webp b/assets/images/hero/hero-about.webp index 3dc0e26..beed56d 100644 Binary files a/assets/images/hero/hero-about.webp and b/assets/images/hero/hero-about.webp differ diff --git a/assets/images/hero/hero-add-ons.jpg b/assets/images/hero/hero-add-ons.jpg deleted file mode 100644 index 0a82a20..0000000 Binary files a/assets/images/hero/hero-add-ons.jpg and /dev/null differ diff --git a/assets/images/hero/hero-add-ons.webp b/assets/images/hero/hero-add-ons.webp index 74ee5bc..22e91a6 100644 Binary files a/assets/images/hero/hero-add-ons.webp and b/assets/images/hero/hero-add-ons.webp differ diff --git a/assets/images/hero/hero-area-rugs.jpg b/assets/images/hero/hero-area-rugs.jpg deleted file mode 100644 index 19addbe..0000000 Binary files a/assets/images/hero/hero-area-rugs.jpg and /dev/null differ diff --git a/assets/images/hero/hero-area-rugs.webp b/assets/images/hero/hero-area-rugs.webp index f3b571b..70945dc 100644 Binary files a/assets/images/hero/hero-area-rugs.webp and b/assets/images/hero/hero-area-rugs.webp differ diff --git a/assets/images/hero/hero-before-after.webp b/assets/images/hero/hero-before-after.webp deleted file mode 100644 index ccef775..0000000 Binary files a/assets/images/hero/hero-before-after.webp and /dev/null differ diff --git a/assets/images/hero/hero-carpet-cleaning.webp b/assets/images/hero/hero-carpet-cleaning.webp index 22401d7..affe8f1 100644 Binary files a/assets/images/hero/hero-carpet-cleaning.webp and b/assets/images/hero/hero-carpet-cleaning.webp differ diff --git a/assets/images/hero/hero-clean-result.jpg b/assets/images/hero/hero-clean-result.jpg deleted file mode 100644 index f56e0f5..0000000 Binary files a/assets/images/hero/hero-clean-result.jpg and /dev/null differ diff --git a/assets/images/hero/hero-clean-result.webp b/assets/images/hero/hero-clean-result.webp index b729a20..3c87030 100644 Binary files a/assets/images/hero/hero-clean-result.webp and b/assets/images/hero/hero-clean-result.webp differ diff --git a/assets/images/hero/hero-commercial.jpg b/assets/images/hero/hero-commercial.jpg deleted file mode 100644 index 1972305..0000000 Binary files a/assets/images/hero/hero-commercial.jpg and /dev/null differ diff --git a/assets/images/hero/hero-commercial.webp b/assets/images/hero/hero-commercial.webp index ee737f3..6dba9c8 100644 Binary files a/assets/images/hero/hero-commercial.webp and b/assets/images/hero/hero-commercial.webp differ diff --git a/assets/images/hero/hero-floors.jpg b/assets/images/hero/hero-floors.jpg deleted file mode 100644 index 0e0ea17..0000000 Binary files a/assets/images/hero/hero-floors.jpg and /dev/null differ diff --git a/assets/images/hero/hero-floors.webp b/assets/images/hero/hero-floors.webp index 9b7eeb0..db281bb 100644 Binary files a/assets/images/hero/hero-floors.webp and b/assets/images/hero/hero-floors.webp differ diff --git a/assets/images/hero/hero-hotels.jpg b/assets/images/hero/hero-hotels.jpg deleted file mode 100644 index cbc7263..0000000 Binary files a/assets/images/hero/hero-hotels.jpg and /dev/null differ diff --git a/assets/images/hero/hero-hotels.webp b/assets/images/hero/hero-hotels.webp index 4739bd7..6f1fbfe 100644 Binary files a/assets/images/hero/hero-hotels.webp and b/assets/images/hero/hero-hotels.webp differ diff --git a/assets/images/hero/hero-living-room.jpg b/assets/images/hero/hero-living-room.jpg deleted file mode 100644 index 5024663..0000000 Binary files a/assets/images/hero/hero-living-room.jpg and /dev/null differ diff --git a/assets/images/hero/hero-living-room.webp b/assets/images/hero/hero-living-room.webp index 358ea90..e12e590 100644 Binary files a/assets/images/hero/hero-living-room.webp and b/assets/images/hero/hero-living-room.webp differ diff --git a/assets/images/hero/hero-offices.jpg b/assets/images/hero/hero-offices.jpg deleted file mode 100644 index 9c1a8be..0000000 Binary files a/assets/images/hero/hero-offices.jpg and /dev/null differ diff --git a/assets/images/hero/hero-offices.webp b/assets/images/hero/hero-offices.webp index e43f8e6..2283d23 100644 Binary files a/assets/images/hero/hero-offices.webp and b/assets/images/hero/hero-offices.webp differ diff --git a/assets/images/hero/hero-property-management.jpg b/assets/images/hero/hero-property-management.jpg deleted file mode 100644 index 12dcba9..0000000 Binary files a/assets/images/hero/hero-property-management.jpg and /dev/null differ diff --git a/assets/images/hero/hero-property-management.webp b/assets/images/hero/hero-property-management.webp index de0b601..97cbee5 100644 Binary files a/assets/images/hero/hero-property-management.webp and b/assets/images/hero/hero-property-management.webp differ diff --git a/assets/images/hero/hero-retail.jpg b/assets/images/hero/hero-retail.jpg deleted file mode 100644 index bba82dc..0000000 Binary files a/assets/images/hero/hero-retail.jpg and /dev/null differ diff --git a/assets/images/hero/hero-retail.webp b/assets/images/hero/hero-retail.webp index 368f477..c109b45 100644 Binary files a/assets/images/hero/hero-retail.webp and b/assets/images/hero/hero-retail.webp differ diff --git a/assets/images/hero/hero-service-area.jpg b/assets/images/hero/hero-service-area.jpg deleted file mode 100644 index 9eb2341..0000000 Binary files a/assets/images/hero/hero-service-area.jpg and /dev/null differ diff --git a/assets/images/hero/hero-service-area.webp b/assets/images/hero/hero-service-area.webp index 3d3a9cf..8d8e5b6 100644 Binary files a/assets/images/hero/hero-service-area.webp and b/assets/images/hero/hero-service-area.webp differ diff --git a/assets/images/hero/hero-stairs.webp b/assets/images/hero/hero-stairs.webp index d4e3413..aa1d18f 100644 Binary files a/assets/images/hero/hero-stairs.webp and b/assets/images/hero/hero-stairs.webp differ diff --git a/assets/images/hero/hero-technician.webp b/assets/images/hero/hero-technician.webp deleted file mode 100644 index 5a917ed..0000000 Binary files a/assets/images/hero/hero-technician.webp and /dev/null differ diff --git a/assets/images/hero/hero-upholstery.jpg b/assets/images/hero/hero-upholstery.jpg deleted file mode 100644 index de9b4a6..0000000 Binary files a/assets/images/hero/hero-upholstery.jpg and /dev/null differ diff --git a/assets/images/hero/hero-upholstery.webp b/assets/images/hero/hero-upholstery.webp index 749a36f..d0abb15 100644 Binary files a/assets/images/hero/hero-upholstery.webp and b/assets/images/hero/hero-upholstery.webp differ diff --git a/assets/images/hero/hero-vacation-rentals.jpg b/assets/images/hero/hero-vacation-rentals.jpg deleted file mode 100644 index b7b5945..0000000 Binary files a/assets/images/hero/hero-vacation-rentals.jpg and /dev/null differ diff --git a/assets/images/hero/hero-vacation-rentals.webp b/assets/images/hero/hero-vacation-rentals.webp index 3735e3e..bc24d6f 100644 Binary files a/assets/images/hero/hero-vacation-rentals.webp and b/assets/images/hero/hero-vacation-rentals.webp differ diff --git a/assets/images/services/add-ons.webp b/assets/images/services/add-ons.webp index d4cb2f9..367e1c5 100644 Binary files a/assets/images/services/add-ons.webp and b/assets/images/services/add-ons.webp differ diff --git a/assets/images/services/area-rug-cleaning.webp b/assets/images/services/area-rug-cleaning.webp index f72d6dc..020634d 100644 Binary files a/assets/images/services/area-rug-cleaning.webp and b/assets/images/services/area-rug-cleaning.webp differ diff --git a/assets/images/services/carpet-cleaning.webp b/assets/images/services/carpet-cleaning.webp index 0f62e36..ce3468d 100644 Binary files a/assets/images/services/carpet-cleaning.webp and b/assets/images/services/carpet-cleaning.webp differ diff --git a/assets/images/services/commercial-overview.webp b/assets/images/services/commercial-overview.webp index c70b9b9..aad5d0c 100644 Binary files a/assets/images/services/commercial-overview.webp and b/assets/images/services/commercial-overview.webp differ diff --git a/assets/images/services/floor-cleaning.webp b/assets/images/services/floor-cleaning.webp index 88c93fe..e798494 100644 Binary files a/assets/images/services/floor-cleaning.webp and b/assets/images/services/floor-cleaning.webp differ diff --git a/assets/images/services/hotels-inns.webp b/assets/images/services/hotels-inns.webp index 6d9caf5..a075983 100644 Binary files a/assets/images/services/hotels-inns.webp and b/assets/images/services/hotels-inns.webp differ diff --git a/assets/images/services/office-spaces.webp b/assets/images/services/office-spaces.webp index 4aefec9..fff3b29 100644 Binary files a/assets/images/services/office-spaces.webp and b/assets/images/services/office-spaces.webp differ diff --git a/assets/images/services/property-management.webp b/assets/images/services/property-management.webp index f4b173e..c0f2dda 100644 Binary files a/assets/images/services/property-management.webp and b/assets/images/services/property-management.webp differ diff --git a/assets/images/services/retail-showrooms.webp b/assets/images/services/retail-showrooms.webp index 06f666d..cd0e62c 100644 Binary files a/assets/images/services/retail-showrooms.webp and b/assets/images/services/retail-showrooms.webp differ diff --git a/assets/images/services/stairs-cleaning.webp b/assets/images/services/stairs-cleaning.webp index bbb0deb..4664c43 100644 Binary files a/assets/images/services/stairs-cleaning.webp and b/assets/images/services/stairs-cleaning.webp differ diff --git a/assets/images/services/upholstery-cleaning.webp b/assets/images/services/upholstery-cleaning.webp index c418ad9..f57139e 100644 Binary files a/assets/images/services/upholstery-cleaning.webp and b/assets/images/services/upholstery-cleaning.webp differ diff --git a/assets/images/services/vacation-rentals.webp b/assets/images/services/vacation-rentals.webp index afbdc54..0805045 100644 Binary files a/assets/images/services/vacation-rentals.webp and b/assets/images/services/vacation-rentals.webp differ diff --git a/tools/__pycache__/build-paced-reel.cpython-313.pyc b/tools/__pycache__/build-paced-reel.cpython-313.pyc deleted file mode 100644 index 6772f0b..0000000 Binary files a/tools/__pycache__/build-paced-reel.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/build-reel-server.cpython-313.pyc b/tools/__pycache__/build-reel-server.cpython-313.pyc deleted file mode 100644 index 7dd4cf6..0000000 Binary files a/tools/__pycache__/build-reel-server.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/convert-to-webp.cpython-313.pyc b/tools/__pycache__/convert-to-webp.cpython-313.pyc deleted file mode 100644 index 3bdcd84..0000000 Binary files a/tools/__pycache__/convert-to-webp.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/gen-hero-images.cpython-313.pyc b/tools/__pycache__/gen-hero-images.cpython-313.pyc deleted file mode 100644 index fc2d3d3..0000000 Binary files a/tools/__pycache__/gen-hero-images.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/gen-images-comfyui.cpython-313.pyc b/tools/__pycache__/gen-images-comfyui.cpython-313.pyc deleted file mode 100644 index 8e7d197..0000000 Binary files a/tools/__pycache__/gen-images-comfyui.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/gen-images.cpython-313.pyc b/tools/__pycache__/gen-images.cpython-313.pyc deleted file mode 100644 index 08c8fe8..0000000 Binary files a/tools/__pycache__/gen-images.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/gen-locations.cpython-313.pyc b/tools/__pycache__/gen-locations.cpython-313.pyc deleted file mode 100644 index bfcaae2..0000000 Binary files a/tools/__pycache__/gen-locations.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/gen-missing-2.cpython-313.pyc b/tools/__pycache__/gen-missing-2.cpython-313.pyc deleted file mode 100644 index 44be8c2..0000000 Binary files a/tools/__pycache__/gen-missing-2.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/gen-service-images.cpython-313.pyc b/tools/__pycache__/gen-service-images.cpython-313.pyc deleted file mode 100644 index 423f70e..0000000 Binary files a/tools/__pycache__/gen-service-images.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/gen-video.cpython-313.pyc b/tools/__pycache__/gen-video.cpython-313.pyc deleted file mode 100644 index 785ef37..0000000 Binary files a/tools/__pycache__/gen-video.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-3shots.cpython-313.pyc b/tools/__pycache__/regen-3shots.cpython-313.pyc deleted file mode 100644 index dccf22c..0000000 Binary files a/tools/__pycache__/regen-3shots.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-commercial-overview.cpython-313.pyc b/tools/__pycache__/regen-commercial-overview.cpython-313.pyc deleted file mode 100644 index c89ae30..0000000 Binary files a/tools/__pycache__/regen-commercial-overview.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-full-reel.cpython-313.pyc b/tools/__pycache__/regen-full-reel.cpython-313.pyc deleted file mode 100644 index 07fd19a..0000000 Binary files a/tools/__pycache__/regen-full-reel.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-images-targeted.cpython-313.pyc b/tools/__pycache__/regen-images-targeted.cpython-313.pyc deleted file mode 100644 index 9f93eee..0000000 Binary files a/tools/__pycache__/regen-images-targeted.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-industrial.cpython-313.pyc b/tools/__pycache__/regen-industrial.cpython-313.pyc deleted file mode 100644 index fb01125..0000000 Binary files a/tools/__pycache__/regen-industrial.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-shot.cpython-313.pyc b/tools/__pycache__/regen-shot.cpython-313.pyc deleted file mode 100644 index fe56598..0000000 Binary files a/tools/__pycache__/regen-shot.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-shot02.cpython-313.pyc b/tools/__pycache__/regen-shot02.cpython-313.pyc deleted file mode 100644 index 16c1d39..0000000 Binary files a/tools/__pycache__/regen-shot02.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-shot04.cpython-313.pyc b/tools/__pycache__/regen-shot04.cpython-313.pyc deleted file mode 100644 index a909bcf..0000000 Binary files a/tools/__pycache__/regen-shot04.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-v3-shot04.cpython-313.pyc b/tools/__pycache__/regen-v3-shot04.cpython-313.pyc deleted file mode 100644 index ce701ac..0000000 Binary files a/tools/__pycache__/regen-v3-shot04.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-v3.cpython-313.pyc b/tools/__pycache__/regen-v3.cpython-313.pyc deleted file mode 100644 index bbcf67e..0000000 Binary files a/tools/__pycache__/regen-v3.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-v4.cpython-313.pyc b/tools/__pycache__/regen-v4.cpython-313.pyc deleted file mode 100644 index 74ca987..0000000 Binary files a/tools/__pycache__/regen-v4.cpython-313.pyc and /dev/null differ diff --git a/tools/__pycache__/regen-veo31.cpython-313.pyc b/tools/__pycache__/regen-veo31.cpython-313.pyc deleted file mode 100644 index d714ea5..0000000 Binary files a/tools/__pycache__/regen-veo31.cpython-313.pyc and /dev/null differ diff --git a/tools/gen-hero-images.py b/tools/gen-hero-images.py deleted file mode 100644 index f293de7..0000000 --- a/tools/gen-hero-images.py +++ /dev/null @@ -1,244 +0,0 @@ -""" -Generate unique hero images for every page via ComfyUI SDXL. -No people. No machines. Residential = warm home scenes. Commercial = professional spaces. -Run in tmux: python3 tools/gen-hero-images.py -""" -import json, time, urllib.request, os, random - -COMFY = "http://localhost:8188" -OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "hero") -CKPT = "sd_xl_base_1.0.safetensors" - -NEG = ( - "people, person, human, hands, feet, boots, shoes, worker, cleaner, technician, " - "machine, vacuum cleaner, equipment, steam, city skyline, apartment building exterior, " - "urban, dirty, stain, text, watermark, logo, blurry, low quality, cartoon, anime, " - "illustration, render, CGI" -) - -IMAGES = [ - { - "filename": "hero-carpet-cleaning.jpg", - "positive": ( - "wide shot of a bright residential living room in an upstate New York home, " - "thick plush beige carpet throughout, warm afternoon sunlight through large windows, " - "comfortable couch and coffee table, Finger Lakes farmhouse style interior, " - "no people, ultra-realistic architectural photography, 16:9 wide" - ), - }, - { - "filename": "hero-stairs.jpg", - "positive": ( - "wide shot of a clean carpeted staircase inside a suburban American home, " - "light grey carpet runner on dark wood stairs, white painted banister, " - "bright natural light from above, no people, no equipment, " - "ultra-realistic interior photography, 16:9" - ), - }, - { - "filename": "hero-upholstery.jpg", - "positive": ( - "bright cozy living room in a residential home, large comfortable fabric sofa " - "in warm neutral tones, clean armchair beside it, plush carpet beneath, " - "afternoon light, Finger Lakes countryside visible through window, " - "no people, ultra-realistic interior photography, 16:9" - ), - }, - { - "filename": "hero-floors.jpg", - "positive": ( - "wide shot of a clean hardwood floor hallway in a spacious suburban American home, " - "light oak floors gleaming, white walls, natural light streaming through windows, " - "no people, no equipment, ultra-realistic interior photography, 16:9" - ), - }, - { - "filename": "hero-area-rugs.jpg", - "positive": ( - "beautiful oriental area rug centered in a bright residential living room, " - "rich warm tones of deep red and gold, hardwood floor surrounding the rug, " - "cozy farmhouse interior, natural light, Finger Lakes region home decor, " - "no people, ultra-realistic interior photography, 16:9" - ), - }, - { - "filename": "hero-add-ons.jpg", - "positive": ( - "bright clean residential bedroom interior, freshly cleaned light beige carpet, " - "white walls, large window with sheer curtains, simple wooden bed frame, " - "crisp natural morning light, Finger Lakes home style, " - "no people, no machines, ultra-realistic interior photography, 16:9" - ), - }, - { - "filename": "hero-commercial.jpg", - "positive": ( - "wide shot of a modern commercial office building lobby interior, " - "clean dark grey commercial carpet throughout, professional corporate space, " - "glass entrance doors, white walls, recessed lighting, " - "no people, no equipment, architectural photography, ultra-realistic, 16:9" - ), - }, - { - "filename": "hero-offices.jpg", - "positive": ( - "modern open-plan corporate office interior, clean grey carpet tiles, " - "rows of empty desks, glass partitions, professional overhead lighting, " - "large windows with daylight, no people, no equipment, " - "architectural photography, ultra-realistic, 16:9" - ), - }, - { - "filename": "hero-vacation-rentals.jpg", - "positive": ( - "bright cozy vacation rental cottage living room interior, Finger Lakes region style, " - "clean beige carpet, wooden ceiling beams, stone fireplace, comfortable furniture, " - "large window with lake view in distance, warm inviting atmosphere, " - "no people, ultra-realistic interior photography, 16:9" - ), - }, - { - "filename": "hero-hotels.jpg", - "positive": ( - "long elegant hotel corridor interior, clean patterned burgundy carpet runner, " - "warm wall sconce lighting along white walls, numbered wooden room doors, " - "soft warm glow, upscale hospitality interior, no people, no equipment, " - "professional architectural photography, ultra-realistic, 16:9" - ), - }, - { - "filename": "hero-retail.jpg", - "positive": ( - "upscale retail showroom interior, clean light grey carpet flooring, " - "modern minimalist display fixtures, bright track lighting overhead, " - "white walls, large storefront windows with natural light, " - "no people, no equipment, architectural photography, ultra-realistic, 16:9" - ), - }, - { - "filename": "hero-property-management.jpg", - "positive": ( - "clean move-in ready apartment unit living room, fresh neutral carpet, " - "white walls, bright windows, empty space ready for tenants, " - "no furniture, no people, no equipment, " - "real estate photography style, ultra-realistic, 16:9" - ), - }, - { - "filename": "hero-about.jpg", - "positive": ( - "warm exterior view of a classic upstate New York suburban home, " - "green lawn, mature trees, clear blue sky, Finger Lakes region, " - "inviting residential property, no people, no vehicles, " - "professional real estate photography, ultra-realistic, 16:9" - ), - }, - { - "filename": "hero-service-area.jpg", - "positive": ( - "scenic Finger Lakes region landscape, rolling green hills, vineyards in distance, " - "calm lake water reflecting sky, late afternoon golden hour light, " - "upstate New York countryside, no people, no vehicles, " - "professional landscape photography, ultra-realistic, 16:9" - ), - }, - { - "filename": "hero-living-room.jpg", - "positive": ( - "spacious bright residential living room in a Finger Lakes area home, " - "plush clean light grey carpet, white walls, large sectional sofa, " - "afternoon sunlight through bay windows, warm cozy family atmosphere, " - "no people, ultra-realistic interior photography, 16:9" - ), - }, - { - "filename": "hero-clean-result.jpg", - "positive": ( - "close-up wide angle of immaculate freshly cleaned residential carpet, " - "uniform plush beige pile, bright natural light raking across the surface, " - "showing deep clean texture, no people, no machines, " - "ultra-realistic macro photography, 16:9" - ), - }, -] - - -def build_workflow(positive, seed=None): - if seed is None: - seed = random.randint(0, 2**32) - return { - "3": {"class_type": "KSampler", "inputs": { - "cfg": 7.5, "denoise": 1.0, - "latent_image": ["5", 0], "model": ["4", 0], - "negative": ["7", 0], "positive": ["6", 0], - "sampler_name": "dpmpp_2m", "scheduler": "karras", - "seed": seed, "steps": 30, - }}, - "4": {"class_type": "CheckpointLoaderSimple", "inputs": {"ckpt_name": CKPT}}, - "5": {"class_type": "EmptyLatentImage", "inputs": {"batch_size": 1, "height": 576, "width": 1024}}, - "6": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["4", 1], "text": positive}}, - "7": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["4", 1], "text": NEG}}, - "8": {"class_type": "VAEDecode", "inputs": {"samples": ["3", 0], "vae": ["4", 2]}}, - "9": {"class_type": "SaveImage", "inputs": {"filename_prefix": "hero_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=900): - 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_out in outputs.values(): - if "images" in node_out: - return node_out["images"] - except Exception: - pass - 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") - url = f"{COMFY}/view?filename={fname}&subfolder={subfolder}&type={img_type}" - with urllib.request.urlopen(url) as resp: - data = resp.read() - try: - from PIL import Image - import io - img = Image.open(io.BytesIO(data)).convert("RGB") - img.save(out_path, "JPEG", quality=92) - print(f" OK: {os.path.basename(out_path)} ({os.path.getsize(out_path)//1024}KB)", flush=True) - except ImportError: - png_path = out_path.replace(".jpg", ".png") - with open(png_path, "wb") as f: - f.write(data) - print(f" OK (PNG): {png_path}", flush=True) - - -total = len(IMAGES) -for i, spec in enumerate(IMAGES): - out_path = os.path.join(OUT_DIR, spec["filename"]) - print(f"\n[{i+1}/{total}] {spec['filename']}", flush=True) - workflow = build_workflow(spec["positive"]) - prompt_id = queue_prompt(workflow) - print(f" queued {prompt_id[:8]}...", flush=True) - images = wait_for_result(prompt_id) - if images: - download_image(images[0], out_path) - else: - print(f" FAILED (timeout)", flush=True) - -print("\nAll done.", flush=True) diff --git a/tools/gen-images-comfyui.py b/tools/gen-images-comfyui.py deleted file mode 100644 index c9865bb..0000000 --- a/tools/gen-images-comfyui.py +++ /dev/null @@ -1,186 +0,0 @@ -"""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.") diff --git a/tools/gen-images-flux.py b/tools/gen-images-flux.py new file mode 100644 index 0000000..26fdbaf --- /dev/null +++ b/tools/gen-images-flux.py @@ -0,0 +1,292 @@ +""" +Generate all site images via FLUX.1 Schnell GGUF through ComfyUI. +FLUX Schnell: 4 steps, cfg=1.0, no negative prompt, photorealistic. +Run after ComfyUI restart: python3 tools/gen-images-flux.py +""" +import json, time, urllib.request, os, random, io + +COMFY = "http://localhost:8188" +HERO_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "hero") +SVC_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "services") + +IMAGES = [ + # --- HERO IMAGES --- + {"filename": "hero-carpet-cleaning.jpg", "dir": HERO_DIR, "prompt": ( + "low-angle 35mm lens perspective looking across thick plush cream carpet in an upstate New York living room, " + "carpet fibers razor sharp in foreground, couch and coffee table receding into shallow bokeh background, " + "warm afternoon window light raking across carpet texture, Finger Lakes farmhouse interior, " + "no people, ultra-realistic architectural photography, 16:9" + )}, + {"filename": "hero-stairs.jpg", "dir": HERO_DIR, "prompt": ( + "dramatic low 35mm angle looking up a clean carpeted staircase from floor level, " + "light grey carpet runner sharp and textured in foreground steps, wood banister receding diagonally, " + "bright daylight flooding from above, shallow depth of field, " + "no people, ultra-realistic interior photography, 16:9" + )}, + {"filename": "hero-upholstery.jpg", "dir": HERO_DIR, "prompt": ( + "50mm lens low corner angle across a bright residential living room, " + "plush linen fabric sofa arm sharp in near foreground, clean armchair and window receding with bokeh, " + "afternoon countryside light through window, shallow depth of field, " + "no people, ultra-realistic interior photography, 16:9" + )}, + {"filename": "hero-floors.jpg", "dir": HERO_DIR, "prompt": ( + "low 24mm angle pressed to gleaming light oak hardwood floor, " + "floor grain razor sharp in extreme foreground receding to hallway vanishing point, " + "white walls, natural light streaming in, shallow depth of field, " + "no people, ultra-realistic interior photography, 16:9" + )}, + {"filename": "hero-area-rugs.jpg", "dir": HERO_DIR, "prompt": ( + "low 35mm angle looking across a hand-knotted oriental rug from floor level, " + "rich red and gold rug fibers sharp in foreground, hardwood floor and room receding into bokeh, " + "cozy farmhouse living room, warm natural light, shallow depth of field, " + "no people, ultra-realistic interior photography, 16:9" + )}, + {"filename": "hero-add-ons.jpg", "dir": HERO_DIR, "prompt": ( + "low 35mm angle across a clean beige bedroom carpet, " + "carpet pile sharp and detailed in near foreground, wooden bed frame and sheer curtained window receding, " + "crisp morning light, shallow depth of field, " + "no people, no machines, ultra-realistic interior photography, 16:9" + )}, + {"filename": "hero-commercial.jpg", "dir": HERO_DIR, "prompt": ( + "low 24mm wide-angle lens across a modern corporate lobby floor, " + "dark charcoal commercial carpet sharp in extreme foreground receding to glass entrance doors, " + "recessed ceiling lights creating depth, strong vanishing point perspective, " + "no people, ultra-realistic architectural photography, 16:9" + )}, + {"filename": "hero-offices.jpg", "dir": HERO_DIR, "prompt": ( + "low 24mm angle across clean grey carpet tiles in a modern open-plan office, " + "carpet tile seams sharp in foreground receding to rows of empty desks and glass partitions, " + "professional overhead lighting, strong linear perspective, " + "no people, ultra-realistic architectural photography, 16:9" + )}, + {"filename": "hero-vacation-rentals.jpg", "dir": HERO_DIR, "prompt": ( + "low 35mm angle across clean beige carpet in a Finger Lakes cottage living room, " + "carpet fibers sharp in foreground, stone fireplace and lake-view window receding with bokeh, " + "wooden ceiling beams, warm inviting light, shallow depth of field, " + "no people, ultra-realistic interior photography, 16:9" + )}, + {"filename": "hero-hotels.jpg", "dir": HERO_DIR, "prompt": ( + "low 24mm lens looking down a long hotel corridor from floor level, " + "patterned burgundy carpet runner sharp in extreme foreground receding to vanishing point, " + "warm wall sconces lining white walls, numbered doors converging in perspective, " + "no people, ultra-realistic hospitality photography, 16:9" + )}, + {"filename": "hero-retail.jpg", "dir": HERO_DIR, "prompt": ( + "low 35mm diagonal angle across clean light grey carpet in an upscale retail showroom, " + "carpet surface sharp in foreground, minimalist display fixtures and storefront windows receding with bokeh, " + "bright track lighting overhead, shallow depth of field, " + "no people, ultra-realistic architectural photography, 16:9" + )}, + {"filename": "hero-property-management.jpg", "dir": HERO_DIR, "prompt": ( + "low 35mm angle across fresh neutral carpet in an empty move-in ready apartment, " + "carpet texture sharp in foreground, bare white walls and bright windows receding, " + "clean real estate photography perspective, shallow depth of field, " + "no people, ultra-realistic real estate photography, 16:9" + )}, + {"filename": "hero-about.jpg", "dir": HERO_DIR, "prompt": ( + "low 35mm angle from lawn level looking up at a classic upstate New York suburban home, " + "green grass blades sharp in extreme foreground, inviting house facade receding upward, " + "mature trees and clear blue sky, warm summer afternoon, " + "no people, ultra-realistic real estate photography, 16:9" + )}, + {"filename": "hero-service-area.jpg", "dir": HERO_DIR, "prompt": ( + "low horizon 24mm wide-angle Finger Lakes landscape, " + "green vineyard vines sharp in foreground receding to rolling hills and calm lake, " + "golden hour light casting long shadows, strong depth and distance, " + "no people, ultra-realistic landscape photography, 16:9" + )}, + {"filename": "hero-living-room.jpg", "dir": HERO_DIR, "prompt": ( + "low 35mm corner angle across a spacious residential living room, " + "plush light grey carpet sharp and textured in foreground, large sectional sofa and bay windows receding with bokeh, " + "warm afternoon sunlight, shallow depth of field, " + "no people, ultra-realistic interior photography, 16:9" + )}, + {"filename": "hero-clean-result.jpg", "dir": HERO_DIR, "prompt": ( + "extreme low 50mm macro angle pressed to immaculate freshly cleaned residential carpet, " + "individual carpet fibers razor sharp in foreground, pile receding into soft bokeh, " + "raking natural light revealing deep clean texture and uniform pile height, " + "no people, ultra-realistic macro carpet photography, 16:9" + )}, + # --- SERVICE CARD IMAGES --- + {"filename": "carpet-cleaning.jpg", "dir": SVC_DIR, "prompt": ( + "low 35mm angle looking across plush clean beige carpet in a residential living room, " + "carpet fibers sharp in foreground, couch and window receding into bokeh, " + "warm afternoon light, shallow depth of field, no people, ultra-realistic interior photography" + )}, + {"filename": "stairs-cleaning.jpg", "dir": SVC_DIR, "prompt": ( + "low 35mm angle looking up clean grey carpeted stairs from bottom step, " + "carpet texture sharp on nearest step, stairs receding diagonally upward, " + "wood banister, bright light from above, no people, ultra-realistic interior photography" + )}, + {"filename": "upholstery-cleaning.jpg", "dir": SVC_DIR, "prompt": ( + "low 50mm angle across a clean plush linen fabric sofa arm, " + "fabric weave sharp in foreground, living room receding with bokeh, " + "warm light, shallow depth of field, no people, ultra-realistic interior photography" + )}, + {"filename": "floor-cleaning.jpg", "dir": SVC_DIR, "prompt": ( + "low 24mm angle pressed to gleaming light oak hardwood floor, " + "wood grain razor sharp in extreme foreground receding down hallway, " + "natural light, no people, ultra-realistic interior photography" + )}, + {"filename": "area-rug-cleaning.jpg", "dir": SVC_DIR, "prompt": ( + "low 35mm angle across a vibrant clean oriental rug from floor level, " + "rug fibers and pattern sharp in foreground, hardwood floor and room receding, " + "warm light, shallow depth of field, no people, ultra-realistic interior photography" + )}, + {"filename": "add-ons.jpg", "dir": SVC_DIR, "prompt": ( + "low 35mm angle across clean beige bedroom carpet, " + "carpet pile sharp in foreground, bed frame and curtained window receding with bokeh, " + "morning light, no people, ultra-realistic interior photography" + )}, + {"filename": "commercial-overview.jpg", "dir": SVC_DIR, "prompt": ( + "low 24mm angle across dark commercial carpet in a corporate lobby, " + "carpet surface sharp in foreground receding to glass entrance, " + "strong vanishing point, no people, ultra-realistic architectural photography" + )}, + {"filename": "vacation-rentals.jpg", "dir": SVC_DIR, "prompt": ( + "low 35mm angle across clean carpet in a Finger Lakes cottage living room, " + "carpet sharp in foreground, stone fireplace and window receding with bokeh, " + "rustic warm decor, no people, ultra-realistic interior photography" + )}, + {"filename": "office-spaces.jpg", "dir": SVC_DIR, "prompt": ( + "low 24mm angle across grey carpet tiles in a modern open office, " + "tile seams sharp in foreground, empty desks receding with linear perspective, " + "professional lighting, no people, ultra-realistic architectural photography" + )}, + {"filename": "hotels-inns.jpg", "dir": SVC_DIR, "prompt": ( + "low 24mm angle down a hotel corridor, patterned carpet runner sharp in foreground, " + "corridor receding to vanishing point, warm wall sconces, " + "no people, ultra-realistic hospitality photography" + )}, + {"filename": "retail-showrooms.jpg", "dir": SVC_DIR, "prompt": ( + "low 35mm diagonal angle across light grey carpet in an upscale retail showroom, " + "carpet sharp in foreground, display fixtures and track lighting receding with bokeh, " + "no people, ultra-realistic architectural photography" + )}, + {"filename": "property-management.jpg", "dir": SVC_DIR, "prompt": ( + "low 35mm angle across fresh neutral carpet in an empty clean apartment, " + "carpet texture sharp in foreground, white walls and windows receding, " + "no people, ultra-realistic real estate photography" + )}, +] + + +def build_workflow(prompt, seed=None): + if seed is None: + seed = random.randint(0, 2**32) + return { + "1": { + "class_type": "UnetLoaderGGUF", + "inputs": {"unet_name": "flux1-schnell-Q8_0.gguf"}, + }, + "2": { + "class_type": "DualCLIPLoader", + "inputs": { + "clip_name1": "t5xxl_fp8_e4m3fn.safetensors", + "clip_name2": "clip_l.safetensors", + "type": "flux", + }, + }, + "3": { + "class_type": "VAELoader", + "inputs": {"vae_name": "ae.safetensors"}, + }, + "4": { + "class_type": "CLIPTextEncode", + "inputs": {"clip": ["2", 0], "text": prompt}, + }, + "5": { + "class_type": "EmptyLatentImage", + "inputs": {"batch_size": 1, "height": 576, "width": 1024}, + }, + "6": { + "class_type": "KSampler", + "inputs": { + "cfg": 1.0, + "denoise": 1.0, + "latent_image": ["5", 0], + "model": ["1", 0], + "negative": ["4", 0], + "positive": ["4", 0], + "sampler_name": "euler", + "scheduler": "simple", + "seed": seed, + "steps": 4, + }, + }, + "7": { + "class_type": "VAEDecode", + "inputs": {"samples": ["6", 0], "vae": ["3", 0]}, + }, + "8": { + "class_type": "SaveImage", + "inputs": {"filename_prefix": "flux_lahr", "images": ["7", 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: + entry = hist[prompt_id] + status = entry.get("status", {}).get("status_str", "") + if status == "error": + msgs = entry.get("status", {}).get("messages", []) + print(f" COMFYUI ERROR: {msgs}", flush=True) + return None + for node_out in entry.get("outputs", {}).values(): + if "images" in node_out: + return node_out["images"] + except Exception: + pass + 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") + url = f"{COMFY}/view?filename={fname}&subfolder={subfolder}&type={img_type}" + with urllib.request.urlopen(url) as resp: + data = resp.read() + try: + from PIL import Image + img = Image.open(io.BytesIO(data)).convert("RGB") + img.save(out_path, "JPEG", quality=92) + print(f" OK: {os.path.basename(out_path)} ({os.path.getsize(out_path)//1024}KB)", flush=True) + except ImportError: + png_path = out_path.replace(".jpg", ".png") + with open(png_path, "wb") as f: + f.write(data) + print(f" OK (PNG): {png_path}", flush=True) + + +total = len(IMAGES) +for i, spec in enumerate(IMAGES): + out_path = os.path.join(spec["dir"], spec["filename"]) + print(f"\n[{i+1}/{total}] {spec['filename']}", flush=True) + workflow = build_workflow(spec["prompt"]) + prompt_id = queue_prompt(workflow) + print(f" queued {prompt_id[:8]}...", flush=True) + images = wait_for_result(prompt_id) + if images: + download_image(images[0], out_path) + else: + print(f" FAILED (timeout)", flush=True) + +print("\nAll done.", flush=True) diff --git a/tools/gen-images.py b/tools/gen-images.py deleted file mode 100644 index a80529f..0000000 --- a/tools/gen-images.py +++ /dev/null @@ -1,106 +0,0 @@ -""" -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/") diff --git a/tools/gen-missing-2.py b/tools/gen-missing-2.py deleted file mode 100644 index f29a23b..0000000 --- a/tools/gen-missing-2.py +++ /dev/null @@ -1,60 +0,0 @@ -"""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.") diff --git a/tools/gen-video-wan.py b/tools/gen-video-wan.py new file mode 100644 index 0000000..5c46300 --- /dev/null +++ b/tools/gen-video-wan.py @@ -0,0 +1,223 @@ +""" +Generate carpet cleaning reel clips via Wan 2.2 TI2V (text+image to video) through ComfyUI. +Uses FLUX-generated hero stills as input frames, animates each into a 3-5 second clip. +Run after gen-images-flux.py completes and images are converted to webp. + +Usage: + python3 tools/gen-video-wan.py + +Output: assets/videos/clips/*.mp4 — stitch with ffmpeg into final reel. +""" +import json, time, urllib.request, os, random, io + +COMFY = "http://localhost:8188" +HERO_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "hero") +OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "videos", "clips") +os.makedirs(OUT_DIR, exist_ok=True) + +WAN_MODEL = "Wan2.2-TI2V-5B-Q4_K_M.gguf" + +# Each clip: input still + motion prompt → 3-5 sec animated clip +# Order matches the reel sequence +CLIPS = [ + { + "filename": "clip-01-carpet.mp4", + "image": "hero-carpet-cleaning.webp", + "prompt": "slow dolly forward across clean plush carpet, gentle camera push toward the far wall, warm afternoon light, cinematic, smooth motion", + "frames": 49, # ~4 seconds at ~12fps + }, + { + "filename": "clip-02-stairs.mp4", + "image": "hero-stairs.webp", + "prompt": "slow pan upward along clean carpeted staircase, camera tilts up following the banister, soft natural light, cinematic motion", + "frames": 49, + }, + { + "filename": "clip-03-upholstery.mp4", + "image": "hero-upholstery.webp", + "prompt": "gentle push in toward clean linen sofa, shallow depth of field, warm light, slow cinematic camera movement", + "frames": 49, + }, + { + "filename": "clip-04-commercial.mp4", + "image": "hero-commercial.webp", + "prompt": "slow tracking shot moving forward down a clean corporate lobby, receding vanishing point, professional lighting, cinematic", + "frames": 49, + }, + { + "filename": "clip-05-floors.mp4", + "image": "hero-floors.webp", + "prompt": "floor-level drift forward along gleaming hardwood, camera slides smoothly down the hallway, natural light", + "frames": 49, + }, + { + "filename": "clip-06-clean-result.mp4", + "image": "hero-clean-result.webp", + "prompt": "slow rack focus across clean carpet fibers, foreground to background, raking natural light, macro detail, cinematic", + "frames": 49, + }, +] + + +def load_image_as_base64(image_path): + import base64 + with open(image_path, "rb") as f: + return base64.b64encode(f.read()).decode("utf-8") + + +def upload_image(image_path): + """Upload image to ComfyUI and return the filename it assigned.""" + import base64 + fname = os.path.basename(image_path) + with open(image_path, "rb") as f: + img_data = f.read() + boundary = "----FormBoundary" + str(random.randint(100000, 999999)) + body = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="image"; filename="{fname}"\r\n' + f"Content-Type: image/webp\r\n\r\n" + ).encode() + img_data + f"\r\n--{boundary}--\r\n".encode() + req = urllib.request.Request( + f"{COMFY}/upload/image", + data=body, + headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, + ) + with urllib.request.urlopen(req) as resp: + result = json.loads(resp.read()) + return result["name"] + + +def build_workflow(image_name, prompt, frames, seed=None): + if seed is None: + seed = random.randint(0, 2**32) + return { + "1": { + "class_type": "UnetLoaderGGUF", + "inputs": {"unet_name": WAN_MODEL}, + }, + "2": { + "class_type": "CLIPLoader", + "inputs": { + "clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", + "type": "wan", + }, + }, + "3": { + "class_type": "VAELoader", + "inputs": {"vae_name": "wan_2.1_vae.safetensors"}, + }, + "4": { + "class_type": "LoadImage", + "inputs": {"image": image_name}, + }, + "5": { + "class_type": "CLIPTextEncode", + "inputs": {"clip": ["2", 0], "text": prompt}, + }, + "6": { + "class_type": "CLIPTextEncode", + "inputs": {"clip": ["2", 0], "text": "blur, low quality, distortion, text, watermark, people, faces"}, + }, + "7": { + "class_type": "WanImageToVideo", + "inputs": { + "model": ["1", 0], + "clip": ["2", 0], + "vae": ["3", 0], + "image": ["4", 0], + "positive": ["5", 0], + "negative": ["6", 0], + "width": 832, + "height": 480, + "length": frames, + "batch_size": 1, + "seed": seed, + "steps": 20, + "cfg": 6.0, + "sampler_name": "uni_pc", + "scheduler": "simple", + "denoise": 1.0, + }, + }, + "8": { + "class_type": "SaveAnimatedWEBP", + "inputs": { + "images": ["7", 0], + "filename_prefix": "wan_lahr", + "fps": 16, + "lossless": False, + "quality": 85, + "method": "default", + }, + }, + } + + +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=1800): + 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: + entry = hist[prompt_id] + if entry.get("status", {}).get("status_str") == "error": + print(f" ERROR: {entry['status'].get('messages', '')}", flush=True) + return None + for node_out in entry.get("outputs", {}).values(): + if "images" in node_out: + return node_out["images"] + if "gifs" in node_out: + return node_out["gifs"] + except Exception: + pass + time.sleep(8) + print(" waiting...", flush=True) + return None + + +def download_video(vid_info, out_path): + fname = vid_info["filename"] + subfolder = vid_info.get("subfolder", "") + img_type = vid_info.get("type", "output") + url = f"{COMFY}/view?filename={fname}&subfolder={subfolder}&type={img_type}" + with urllib.request.urlopen(url) as resp: + data = resp.read() + with open(out_path, "wb") as f: + f.write(data) + print(f" saved: {os.path.basename(out_path)} ({len(data)//1024}KB)", flush=True) + + +total = len(CLIPS) +for i, clip in enumerate(CLIPS): + print(f"\n[{i+1}/{total}] {clip['filename']}", flush=True) + image_path = os.path.join(HERO_DIR, clip["image"]) + if not os.path.exists(image_path): + print(f" SKIP: {image_path} not found", flush=True) + continue + print(f" uploading {clip['image']}...", flush=True) + image_name = upload_image(image_path) + workflow = build_workflow(image_name, clip["prompt"], clip["frames"]) + prompt_id = queue_prompt(workflow) + print(f" queued {prompt_id[:8]}...", flush=True) + results = wait_for_result(prompt_id) + if results: + out_path = os.path.join(OUT_DIR, clip["filename"]) + # rename .webp output to .mp4 for compatibility — or save as webp animation + out_path_webp = out_path.replace(".mp4", ".webp") + download_video(results[0], out_path_webp) + else: + print(f" FAILED", flush=True) + +print("\nAll clips done. Stitch with:") +print(f" ffmpeg -f concat -safe 0 -i tools/clip-list.txt -c copy assets/videos/hero-reel-flux.mp4") diff --git a/tools/pipeline.html b/tools/pipeline.html new file mode 100644 index 0000000..3cdc88f --- /dev/null +++ b/tools/pipeline.html @@ -0,0 +1,151 @@ + + + + +Image Gen Pipeline + + + + +

Lahr Carpet Cleaning — Image Generation Pipeline

+ +

Model Stack

+
+
+
Prompt
+
gen-images-flux.py
+
28 images (16 hero + 12 svc)
+
+
+
+
API
+
ComfyUI
+
localhost:8188
+
+
+
+
UNet (model)
+
FLUX.1 Schnell
+
Q8_0 GGUF · 12GB · 12B params
+
+
+
+
Sampler
+
KSampler
+
4 steps · euler · cfg=1.0
+
+
+
+
Decode
+
FLUX AE
+
ae.safetensors · 108MB
+
+
+
+
Output
+
JPEG → WebP
+
1024×576 · q92 → q80
+
+
+ +

Text Encoders

+
+
+
CLIP-L
+
clip_l.safetensors
+
235MB · short prompts
+
+
+
+
+
T5-XXL fp8
+
t5xxl_fp8_e4m3fn
+
4.6GB · long prompt understanding
+
+
+
+
Node
+
DualCLIPLoader
+
type: flux
+
+
+ +

Hardware

+
+
GPU
AMD Radeon (2GB VRAM)
+
Execution
CPU only (VRAM too small)
+
Speed
~4 min / image
+
Total ETA
~1h50m for 28 images
+
+ +

Model Files on Disk

+
+
UNet
flux1-schnell-Q8_0.gguf · 12GB
+
T5-XXL
t5xxl_fp8_e4m3fn.safetensors · 4.6GB
+
CLIP-L
clip_l.safetensors · 235MB
+
VAE
ae.safetensors · 108MB (official BFL)
+
+ +

Generation Progress

+
+
4 / 28
+
14% — reload page to update
+
+ +

Prompt Strategy

+
+
Low-angle perspective (35mm / 24mm lens specified in prompt)
+
Carpet/floor texture sharp in foreground — subject recedes into bokeh
+
Shallow depth of field + vanishing point for depth cues
+
No people, no machines, no equipment
+
Finger Lakes / upstate NY context for residential scenes
+
+
Previous model: RealVisXL V5.0 fp16 (SDXL 3.5B) — rejected: flat angles, poor depth
+
Current model: FLUX.1 Schnell (12B transformer) — better spatial understanding
+
+ + + diff --git a/tools/regen-3shots.py b/tools/regen-3shots.py deleted file mode 100644 index 8916696..0000000 --- a/tools/regen-3shots.py +++ /dev/null @@ -1,142 +0,0 @@ -""" -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.") diff --git a/tools/regen-commercial-overview.py b/tools/regen-commercial-overview.py deleted file mode 100644 index 672f119..0000000 --- a/tools/regen-commercial-overview.py +++ /dev/null @@ -1,55 +0,0 @@ -"""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.") diff --git a/tools/regen-full-reel.py b/tools/regen-full-reel.py deleted file mode 100644 index cbecc11..0000000 --- a/tools/regen-full-reel.py +++ /dev/null @@ -1,163 +0,0 @@ -""" -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.") diff --git a/tools/regen-images-targeted.py b/tools/regen-images-targeted.py deleted file mode 100644 index 58f2ff2..0000000 --- a/tools/regen-images-targeted.py +++ /dev/null @@ -1,64 +0,0 @@ -"""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.") diff --git a/tools/regen-industrial.py b/tools/regen-industrial.py deleted file mode 100644 index d459148..0000000 --- a/tools/regen-industrial.py +++ /dev/null @@ -1,169 +0,0 @@ -"""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.") diff --git a/tools/regen-shot.py b/tools/regen-shot.py deleted file mode 100644 index 9ed4894..0000000 --- a/tools/regen-shot.py +++ /dev/null @@ -1,87 +0,0 @@ -""" -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:]}") diff --git a/tools/regen-shot02.py b/tools/regen-shot02.py deleted file mode 100644 index 48c9237..0000000 --- a/tools/regen-shot02.py +++ /dev/null @@ -1,102 +0,0 @@ -"""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.") diff --git a/tools/regen-shot04.py b/tools/regen-shot04.py deleted file mode 100644 index 566a1e1..0000000 --- a/tools/regen-shot04.py +++ /dev/null @@ -1,95 +0,0 @@ -"""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.") diff --git a/tools/regen-v3-shot04.py b/tools/regen-v3-shot04.py deleted file mode 100644 index 35459c8..0000000 --- a/tools/regen-v3-shot04.py +++ /dev/null @@ -1,98 +0,0 @@ -"""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.") diff --git a/tools/regen-v3.py b/tools/regen-v3.py deleted file mode 100644 index 3f3cb1a..0000000 --- a/tools/regen-v3.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -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.") diff --git a/tools/regen-v4.py b/tools/regen-v4.py deleted file mode 100644 index b5e1288..0000000 --- a/tools/regen-v4.py +++ /dev/null @@ -1,154 +0,0 @@ -"""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.") diff --git a/tools/regen-veo31.py b/tools/regen-veo31.py deleted file mode 100644 index 67b2377..0000000 --- a/tools/regen-veo31.py +++ /dev/null @@ -1,167 +0,0 @@ -""" -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.") diff --git a/tools/review-all.html b/tools/review-all.html new file mode 100644 index 0000000..55b29cc --- /dev/null +++ b/tools/review-all.html @@ -0,0 +1,117 @@ + + + + +Image Review — All 28 + + + + +
+

Lahr Carpet Cleaning — Image Review

+
Model: FLUX.1 Schnell Q8_0 GGUF  ·  4 steps, cfg=1.0, euler/simple  ·  1024×576 → WebP  ·  28 images total
+
+ +
Hero Images (16)
+
+ +
Service Cards (12)
+
+ +
+ +
+
+
FLUX.1 Schnell · Q8_0 GGUF · 4 steps · euler · cfg=1.0
+
+
+
+
+ + + + diff --git a/tools/review-heroes.html b/tools/review-heroes.html new file mode 100644 index 0000000..6a3bd0e --- /dev/null +++ b/tools/review-heroes.html @@ -0,0 +1,35 @@ + + + + +Hero Image Review + + + +

Hero Images — RealVisXL V5.0 (15 of 16)

+
+

hero-carpet-cleaning

+

hero-upholstery

+

hero-floors

+

hero-area-rugs

+

hero-add-ons

+

hero-commercial

+

hero-offices

+

hero-vacation-rentals

+

hero-hotels

+

hero-retail

+

hero-property-management

+

hero-about

+

hero-service-area

+

hero-living-room

+

hero-clean-result

+
+ + diff --git a/tools/wan-test-v2.py b/tools/wan-test-v2.py new file mode 100644 index 0000000..cb04e57 --- /dev/null +++ b/tools/wan-test-v2.py @@ -0,0 +1,125 @@ +"""Single test clip — corrected WanImageToVideo workflow.""" +import json, time, urllib.request, os, random + +COMFY = "http://localhost:8188" +IMAGE_PATH = "assets/images/hero/hero-carpet-cleaning.webp" +OUT_DIR = "assets/videos/clips" +os.makedirs(OUT_DIR, exist_ok=True) + + +def upload_image(image_path): + fname = os.path.basename(image_path) + with open(image_path, "rb") as f: + img_data = f.read() + boundary = "----FormBoundary123456" + body = ( + f"--{boundary}\r\n" + f'Content-Disposition: form-data; name="image"; filename="{fname}"\r\n' + f"Content-Type: image/webp\r\n\r\n" + ).encode() + img_data + f"\r\n--{boundary}--\r\n".encode() + req = urllib.request.Request( + f"{COMFY}/upload/image", data=body, + headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, + ) + with urllib.request.urlopen(req) as resp: + result = json.loads(resp.read()) + print(f" uploaded: {result['name']}") + return result["name"] + + +def build_workflow(image_name, prompt, frames=25): + # WanImageToVideo is a conditioning node, NOT a sampler. + # outputs: [0]=positive CONDITIONING, [1]=negative CONDITIONING, [2]=latent LATENT + # start_image is optional IMAGE — anchors first frame. + return { + "1": {"class_type": "UnetLoaderGGUF", "inputs": {"unet_name": "Wan2.2-TI2V-5B-Q4_K_M.gguf"}}, + "2": {"class_type": "CLIPLoader", "inputs": {"clip_name": "umt5_xxl_fp8_e4m3fn_scaled.safetensors", "type": "wan"}}, + "3": {"class_type": "VAELoader", "inputs": {"vae_name": "wan_2.1_vae.safetensors"}}, + "4": {"class_type": "LoadImage", "inputs": {"image": image_name}}, + "5": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": prompt}}, + "6": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": "blur, low quality, distortion, text, watermark, people, jitter"}}, + "7": { + "class_type": "WanImageToVideo", + "inputs": { + "positive": ["5", 0], + "negative": ["6", 0], + "vae": ["3", 0], + "start_image": ["4", 0], + "width": 832, "height": 480, "length": frames, "batch_size": 1, + }, + }, + "8": { + "class_type": "KSampler", + "inputs": { + "model": ["1", 0], + "positive": ["7", 0], + "negative": ["7", 1], + "latent_image": ["7", 2], + "seed": 42, "steps": 20, "cfg": 6.0, + "sampler_name": "uni_pc", "scheduler": "simple", "denoise": 1.0, + }, + }, + # VAEDecodeTiled handles video (5D) latents — VAEDecode only handles images (4D) + "9": {"class_type": "VAEDecodeTiled", "inputs": {"samples": ["8", 0], "vae": ["3", 0], "tile_size": 512, "overlap": 64, "temporal_size": 64, "temporal_overlap": 8}}, + "10": { + "class_type": "SaveAnimatedWEBP", + "inputs": {"images": ["9", 0], "filename_prefix": "wan_test", "fps": 12, "lossless": False, "quality": 85, "method": "default"}, + }, + } + + +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=1800): + start = time.time() + while time.time() - start < timeout: + with urllib.request.urlopen(f"{COMFY}/history/{prompt_id}") as resp: + hist = json.loads(resp.read()) + if prompt_id in hist: + entry = hist[prompt_id] + if entry.get("status", {}).get("status_str") == "error": + print(f" ERROR: {entry['status'].get('messages', '')}") + return None + for node_out in entry.get("outputs", {}).values(): + if "gifs" in node_out: + return node_out["gifs"] + if "images" in node_out: + return node_out["images"] + elapsed = int(time.time() - start) + print(f" waiting... {elapsed}s", flush=True) + time.sleep(15) + return None + + +def download_output(vid_info, out_path): + fname = vid_info["filename"] + subfolder = vid_info.get("subfolder", "") + img_type = vid_info.get("type", "output") + url = f"{COMFY}/view?filename={fname}&subfolder={subfolder}&type={img_type}" + with urllib.request.urlopen(url) as resp: + data = resp.read() + with open(out_path, "wb") as f: + f.write(data) + print(f" saved: {out_path} ({len(data)//1024}KB)") + + +print("[TEST v2] WanImageToVideo → KSampler → VAEDecode → SaveAnimatedWEBP") +image_name = upload_image(IMAGE_PATH) +workflow = build_workflow( + image_name, + "slow dolly forward across clean plush cream carpet, gentle camera push toward the far wall, warm afternoon light, cinematic smooth motion", + frames=9, +) +prompt_id = queue_prompt(workflow) +print(f" queued: {prompt_id}") +results = wait_for_result(prompt_id) +if results: + download_output(results[0], f"{OUT_DIR}/test-clip-01.webp") + print("SUCCESS") +else: + print("FAILED")