diff --git a/.cpanel.yml b/.cpanel.yml new file mode 100644 index 0000000..ee4822b --- /dev/null +++ b/.cpanel.yml @@ -0,0 +1,18 @@ +--- +deployment: + tasks: + - export DEPLOYPATH=/home/dev1communitypro/public_html/ + - /bin/cp -r assets "$DEPLOYPATH" + - /bin/cp -r about "$DEPLOYPATH" + - /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" + - /bin/cp index.html "$DEPLOYPATH" + - /bin/cp 404.html "$DEPLOYPATH" + - /bin/cp 500.html "$DEPLOYPATH" + - /bin/cp robots.txt "$DEPLOYPATH" + - /bin/cp sitemap.xml "$DEPLOYPATH" diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b3ae507 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +.git +.env +.planning +.claude +*.Zone.Identifier +node_modules diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..08eeee3 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +RESEND_API_KEY= +FROM_ADDRESS=noreply@lahrcarpetcleaning.com +ALLOWED_ORIGINS=lahrcarpetcleaning.com +LAHR_INBOX_TO=lahrcarpet@gmail.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8383823 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.env +*.Zone.Identifier +node_modules/ +build/ +.DS_Store +*.log diff --git a/.planning/SITEMAP_CANONICAL.md b/.planning/SITEMAP_CANONICAL.md new file mode 100644 index 0000000..4079eae --- /dev/null +++ b/.planning/SITEMAP_CANONICAL.md @@ -0,0 +1,124 @@ +# SITEMAP_CANONICAL.md — lahrcarpetcleaning.com + +## Nav Structure +Home · Residential · Commercial · Our Work · About · Company + +--- + +## Pages + +### / — Home +Status: EXISTS +File: index.html + +--- + +### /residential/ — Residential Services (landing) +Status: TODO — new page +Nav: Residential (parent) +Purpose: Overview of all residential services with cards linking to each sub-page. + +#### /residential/carpet-cleaning/ +Status: EXISTS (currently at /services/carpet-cleaning/) — needs folder rename +Hero image: hero-technician.jpg + +#### /residential/stairs/ +Status: EXISTS (currently at /services/stairs/) — needs folder rename +Hero image: hero-stairs.jpg + +#### /residential/upholstery/ +Status: EXISTS (currently at /services/upholstery/) — needs folder rename +Hero image: hero-clean-result.jpg + +#### /residential/floors/ +Status: EXISTS (currently at /services/floors/) — needs folder rename +Hero image: hero-living-room.jpg + +#### /residential/area-rugs/ +Status: EXISTS (currently at /services/area-rugs/) — needs folder rename +Hero image: hero-clean-result.jpg + +#### /residential/add-ons/ +Status: EXISTS (currently at /services/add-ons/) — needs folder rename +Hero image: hero-before-after.jpg + +--- + +### /commercial/ — Commercial Cleaning +Status: EXISTS (currently at /services/commercial/) — needs folder move to root +Hero image: AdobeStock commercial images (keep as-is) + +--- + +### /our-work/ — Our Work (Before/After Gallery) +Status: TODO — new page +Nav: Our Work (standalone) +Purpose: Grid of before/after job photos. Real client work images. +Images: assets/images/our-work/ (folder needed) + +--- + +### /about/ — About +Status: EXISTS +Nav: About (standalone) + +--- + +### /company/ — Company (dropdown parent, no landing needed) +Nav label: Company + +#### /contact/ — Contact +Status: EXISTS + +#### /service-area/ — Service Area +Status: TODO — new page +Purpose: Towns served — Waterloo, Seneca Falls, Geneva, Canandaigua, Auburn, Finger Lakes region. +SEO: local landing content per town area. + +#### /reviews/ — Reviews +Status: TODO — new page +Purpose: Google review embeds / static testimonials. + +--- + +## Folder Rename Plan + +| Current path | New path | +|----------------------------|-------------------------------| +| /services/carpet-cleaning/ | /residential/carpet-cleaning/ | +| /services/stairs/ | /residential/stairs/ | +| /services/upholstery/ | /residential/upholstery/ | +| /services/floors/ | /residential/floors/ | +| /services/area-rugs/ | /residential/area-rugs/ | +| /services/add-ons/ | /residential/add-ons/ | +| /services/commercial/ | /commercial/ | + +All internal nav links and footer links update site-wide on rename. + +--- + +## sitemap.xml URLs (target) + +/ +/residential/ +/residential/carpet-cleaning/ +/residential/stairs/ +/residential/upholstery/ +/residential/floors/ +/residential/area-rugs/ +/residential/add-ons/ +/commercial/ +/our-work/ +/about/ +/contact/ +/service-area/ +/reviews/ + +--- + +## Notes +- No pricing on any page (pricing flyer is internal reference only) +- All hero images: Imagen 4 generated (assets/images/hero/) +- Service area focus: Upstate NY — Waterloo, Seneca Falls, Geneva, Finger Lakes region +- Phone: 315-719-1218 | Email: lahrcarpet@gmail.com +- Address: 1076 Waterloo/Geneva Road, Waterloo, NY diff --git a/.planning/lahr-carpet-cleaning.jpeg b/.planning/lahr-carpet-cleaning.jpeg new file mode 100644 index 0000000..f25662b Binary files /dev/null and b/.planning/lahr-carpet-cleaning.jpeg differ diff --git a/404.html b/404.html new file mode 100644 index 0000000..f1f0375 --- /dev/null +++ b/404.html @@ -0,0 +1,38 @@ + + + + + + Page Not Found | Lahr Carpet Cleaning + + + + + + +
+
+

404

+

Page Not Found

+

The page you are looking for does not exist or has been moved.

+ Back to Home +
+
+ + + + diff --git a/500.html b/500.html new file mode 100644 index 0000000..045232b --- /dev/null +++ b/500.html @@ -0,0 +1,38 @@ + + + + + + Server Error | Lahr Carpet Cleaning + + + + + + +
+
+

500

+

Server Error

+

Something went wrong on our end. Please try again or contact us directly.

+ Back to Home +
+
+ + + + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3a11f7d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM nginx:alpine +COPY infra/nginx.conf /etc/nginx/conf.d/default.conf +COPY . /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/about/index.html b/about/index.html new file mode 100644 index 0000000..b0bc0c3 --- /dev/null +++ b/about/index.html @@ -0,0 +1,119 @@ + + + + + + About Lahr Carpet Cleaning | Waterloo NY + + + + + + +
+
+
+ Waterloo, NY — Finger Lakes Region +

About Lahr Carpet Cleaning

+

Local, reliable carpet care that treats your home with respect.

+ Book Now +
+
+
+ +
+
+
+
+ Lahr Carpet Cleaning technician at work +
+
+ +

A Local Business Built on Trust

+

Lahr Carpet Cleaning is a family-owned company based in Waterloo, NY. We serve homeowners and businesses throughout the Finger Lakes region. Our focus has always been simple: show up on time, do the work right, and leave every space cleaner than we found it.

+

Dirty carpets are more than an eyesore. They hold allergens, pet dander, and bacteria that regular vacuuming cannot remove. We use professional hot water extraction equipment to reach deep into carpet fibers and pull out what has built up over time.

+

Every home we enter gets our full attention. We protect your furniture, your floors, and your time. When we finish, you will see the difference immediately.

+
+
+
+
+ +
+
+
+ +

Our Approach to Every Job

+

We treat every home the way we would want ours treated.

+
+
+
+
+

Reliability

+

We arrive when scheduled. No vague windows. No last-minute cancellations. Your time matters.

+
+
+
+

Attention to Detail

+

We inspect high-traffic areas, pre-treat stains, and check our work before we leave.

+
+
+
+

Respect for Your Home

+

We protect baseboards, handle furniture carefully, and clean up after ourselves every time.

+
+
+
+

Safe Products

+

Our cleaning solutions are safe for children, pets, and the environment.

+
+
+
+

Honest Pricing

+

No hidden fees. We tell you the cost before we begin, and that is what you pay.

+
+
+
+

Quality Results

+

We do not leave until the job meets our standard. Your satisfaction is the measure of our work.

+
+
+
+
+ +
+
+
+ +

Proudly Serving the Finger Lakes

+

We are your neighbors. We live and work in the same communities you do.

+
+
+
+

Lahr Carpet Cleaning is based in Waterloo, NY and serves the broader Finger Lakes region. That includes Seneca Falls, Geneva, Canandaigua, Cayuga, Ovid, Romulus, Lodi, Dundee, Penn Yan, and the communities that surround them.

+

Being a local business means we are invested in doing right by our neighbors. When you call us, you are not dealing with a call center. You are talking to the people who will actually show up at your door.

+

We serve homes, rental properties, offices, and commercial spaces throughout Seneca, Ontario, Schuyler, and Yates counties. If you are not sure we cover your area, give us a call at 315-719-1218.

+
+
+ Clean living room carpet in Finger Lakes home +
+
+
+
+ +
+
+

Ready to See the Difference?

+

Call us or fill out our contact form. Free estimates included with every booking.

+ Book Now +
+
+ + + + + + diff --git a/assets/css/styles.css b/assets/css/styles.css new file mode 100644 index 0000000..6119d77 --- /dev/null +++ b/assets/css/styles.css @@ -0,0 +1,1521 @@ +/* ===== Google Fonts ===== */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap'); + +/* ===== Design Tokens ===== */ +:root { + --color-bg: #0a0a0b; + --color-surface: #111113; + --color-surface-2: #18181b; + --color-surface-3: #242428; + --color-border: rgba(255,255,255,0.08); + --color-border-hover:rgba(255,255,255,0.18); + --color-accent: #e8291b; + --color-accent-dark: #b81e13; + --color-accent-glow: rgba(232,41,27,0.18); + --color-white: #ffffff; + --color-text-1: #f4f4f5; + --color-text-2: #a1a1aa; + --color-text-3: #71717a; + --color-star: #f59e0b; + + /* keep legacy names for backward compat */ + --color-primary: #0a0a0b; + --color-black: #0a0a0b; + --color-text: #f4f4f5; + --color-text-light: #a1a1aa; + --color-text-muted: #71717a; + --color-gray: #27272a; + --color-gray-light: #18181b; + --color-drop-bg: #18181b; + --color-border-v2: rgba(255,255,255,0.08); + + --font: 'Inter', system-ui, sans-serif; + --font-heading: 'Inter', system-ui, sans-serif; + --font-body: 'Inter', system-ui, sans-serif; + + --radius-sm: 10px; + --radius-md: 18px; + --radius-lg: 28px; + --radius-pill: 50px; + + --shadow-sm: 0 1px 3px rgba(0,0,0,0.4); + --shadow-md: 0 8px 24px rgba(0,0,0,0.4); + --shadow-lg: 0 20px 60px rgba(0,0,0,0.5); + + --transition: all 0.2s cubic-bezier(0.4,0,0.2,1); + --transition-slow: all 0.4s cubic-bezier(0.4,0,0.2,1); +} + +/* ===== Reset ===== */ +*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; } +html { scroll-behavior:smooth; font-size:16px; } +body { + font-family: var(--font); + background: var(--color-bg); + color: var(--color-text-1); + line-height: 1.65; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +img { max-width:100%; height:auto; display:block; } +a { text-decoration:none; color:inherit; transition:var(--transition); } + +/* ===== Container ===== */ +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 32px; +} + +/* ===== Typography ===== */ +h1,h2,h3,h4 { + font-family: var(--font); + font-weight: 700; + line-height: 1.15; + letter-spacing: -0.02em; +} + +.section-label { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--color-accent); + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + margin-bottom: 12px; +} +.section-label::before { + content: ''; + display: block; + width: 18px; + height: 2px; + background: var(--color-accent); + border-radius: 2px; +} + +.section-title { + font-size: clamp(1.6rem, 3vw, 2.4rem); + color: var(--color-white); + font-weight: 900; + letter-spacing: -0.045em; + line-height: 1.0; + text-align: center; +} +.section-title.dark { color: var(--color-bg); } + +.section-header { + text-align: center; + max-width: 720px; + margin: 0 auto 64px; +} +.section-header h2 { + font-size: clamp(1.6rem, 3vw, 2.4rem); + font-weight: 900; + letter-spacing: -0.045em; + line-height: 1.0; + color: var(--color-white); + margin-bottom: 16px; +} +.section-header p { + font-size: 1rem; + color: var(--color-text-2); + line-height: 1.7; + max-width: 560px; + margin: 0 auto; +} + +.section-subtitle { + color: var(--color-text-2); + font-size: 1.05rem; + line-height: 1.75; + margin-top: 14px; + text-align: center; +} +.section-subtitle.dark { color: var(--color-text-2); } + +/* ===== Buttons ===== */ +.btn, .button { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 13px 28px; + font-family: var(--font); + font-size: 0.875rem; + font-weight: 600; + letter-spacing: 0.02em; + cursor: pointer; + border: none; + border-radius: var(--radius-pill); + transition: var(--transition); + white-space: nowrap; +} +.btn-primary, .btn.btn-primary { + background: var(--color-accent); + color: #fff; +} +.btn-primary:hover { + background: var(--color-accent-dark); + transform: translateY(-1px); + box-shadow: 0 6px 20px var(--color-accent-glow); +} +.btn-ghost, .btn-outline { + background: transparent; + color: var(--color-white); + border: 1px solid var(--color-border-hover); +} +.btn-ghost:hover, .btn-outline:hover { + border-color: var(--color-accent); + color: var(--color-accent); + background: var(--color-accent-glow); +} +.btn-large { padding: 16px 36px; font-size: 1rem; } +.btn-full { width: 100%; justify-content: center; padding: 15px; font-size: 1rem; } +.btn-light { + background: var(--color-white); + color: var(--color-bg); +} +.btn-light:hover { background: #e4e4e7; transform: translateY(-1px); } + +/* ===== Header / Nav ===== */ +.header { + position: fixed; + top: 0; + left: 0; + width: 100%; + z-index: 1000; + padding: 14px 0; + background: rgba(10,10,11,0.82); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid var(--color-border); + transition: var(--transition); +} +.navbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 32px; +} +.logo, .logo-text { + font-size: 1.35rem; + font-weight: 800; + color: var(--color-white); + letter-spacing: -0.03em; +} +.logo-highlight { color: var(--color-accent); } + +.nav-links { + display: flex; + list-style: none; + gap: 4px; + align-items: center; +} +.nav-links a { + color: var(--color-text-2); + font-size: 0.9rem; + font-weight: 500; + padding: 6px 14px; + border-radius: var(--radius-sm); + transition: var(--transition); +} +.nav-links a:hover { color: var(--color-white); background: var(--color-surface-2); } + +/* Dropdown */ +.nav-dropdown { position: relative; } +.dropdown-label { + color: var(--color-text-2); + font-size: 0.9rem; + font-weight: 500; + padding: 6px 14px; + display: flex; + align-items: center; + gap: 5px; + border-radius: var(--radius-sm); + cursor: pointer; + transition: var(--transition); +} +.dropdown-label:hover, .dropdown-label:hover i { color: var(--color-white); background: var(--color-surface-2); } +.dropdown-label i { font-size: 0.7rem; transition: transform 0.2s; } +.nav-dropdown:hover .dropdown-label { background: var(--color-surface-2); color: var(--color-white); } +.nav-dropdown:hover .dropdown-label i { transform: rotate(180deg); } + +.dropdown-menu { + display: none; + position: absolute; + top: 100%; + left: 0; + padding: 12px 6px 6px; + background: var(--color-surface-2); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + min-width: 240px; + box-shadow: var(--shadow-lg); + z-index: 1001; +} +.nav-dropdown:hover .dropdown-menu, +.dropdown-menu:hover { display: block; } +.nav-dropdown.open .dropdown-menu { display: block; } + +.dropdown-menu a { + display: block; + color: var(--color-text-2); + font-size: 0.875rem; + font-weight: 500; + padding: 9px 14px; + border-radius: var(--radius-sm); + transition: var(--transition); + white-space: nowrap; +} +.dropdown-menu a:hover { color: var(--color-white); background: var(--color-surface-3); } + +.nav-contact { + display: flex; + align-items: center; + gap: 12px; +} +.social-icon { + width: 34px; + height: 34px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + color: var(--color-text-2); + font-size: 0.9rem; + background: var(--color-surface-2); + border: 1px solid var(--color-border); + transition: var(--transition); +} +.social-icon:hover { color: var(--color-white); border-color: var(--color-accent); background: var(--color-accent-glow); } + +/* Mobile toggle */ +.mobile-menu-toggle { + display: none; + flex-direction: column; + gap: 5px; + background: none; + border: none; + cursor: pointer; + padding: 4px; +} +.mobile-menu-toggle span { + display: block; + width: 22px; + height: 2px; + background: var(--color-white); + border-radius: 2px; + transition: var(--transition); +} +.mobile-menu-toggle.active span:nth-child(1) { transform: rotate(45deg) translate(5px,5px); } +.mobile-menu-toggle.active span:nth-child(2) { opacity: 0; } +.mobile-menu-toggle.active span:nth-child(3) { transform: rotate(-45deg) translate(5px,-5px); } + +/* ===== Hero ===== */ +.hero { + position: relative; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: flex-end; + background-size: cover; + background-position: center; + overflow: hidden; +} +/* Video background */ +.hero-video { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + z-index: 0; +} + +.hero-overlay { + position: absolute; + inset: 0; + background: + radial-gradient(rgba(255,255,255,0.04) 1px, transparent 1px), + linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.48) 50%, rgba(0,0,0,0.15) 100%), + linear-gradient(to right, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0.0) 65%); + background-size: 28px 28px, 100% 100%, 100% 100%; + z-index: 0; +} +.hero .container { + position: relative; + z-index: 1; + padding-top: 140px; + padding-bottom: 72px; + flex: 1; + display: flex; + align-items: flex-end; + max-width: none; + margin: 0; + padding-left: max(40px, 6vw); + padding-right: max(40px, 6vw); +} +.hero-content { + max-width: 580px; + text-align: left; + text-shadow: 0 2px 12px rgba(0,0,0,0.6); +} +.hero-title { text-align: left; } +.hero-content .btn { text-shadow: none; } +.hero-eyebrow { + display: inline-flex; + align-items: center; + gap: 14px; + color: rgba(255,255,255,0.45); + font-size: 0.68rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.22em; + margin-bottom: 32px; +} +.hero-eyebrow::before { + content: ''; + display: block; + width: 40px; + height: 1px; + background: var(--color-accent); + flex-shrink: 0; +} +.hero-eyebrow i { display: none; } + +.hero-title { + font-size: clamp(2.4rem, 5.5vw, 5rem); + font-weight: 900; + color: var(--color-white); + letter-spacing: -0.055em; + line-height: 0.92; + margin-bottom: 32px; +} + +.hero-sub, .hero-description { + font-size: 1rem; + color: rgba(255,255,255,0.5); + line-height: 1.65; + max-width: 420px; + margin-bottom: 44px; + font-weight: 400; + letter-spacing: 0.01em; +} + +.hero-actions { + display: flex; + gap: 14px; + flex-wrap: wrap; + align-items: center; +} +.hero-actions .btn-large { padding: 15px 32px; } + +/* Hero paragraph / legacy */ +.paragraph { + color: rgba(255,255,255,0.78); + font-size: 1.1rem; + margin-bottom: 24px; + line-height: 1.7; +} + +/* Hero info strip */ +.hero-strip { + position: relative; + z-index: 1; + background: rgba(0,0,0,0.7); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border-top: 1px solid var(--color-border); +} + +/* Segmented CTA strip */ +.hero-cta-strip { + display: grid; + grid-template-columns: repeat(3, 1fr); +} +.cta-strip-item { + display: flex; + align-items: center; + gap: 16px; + padding: 22px 28px; + color: var(--color-text-1); + border-right: 1px solid var(--color-border); + transition: var(--transition); + text-decoration: none; +} +.cta-strip-item:last-child { border-right: none; } +.cta-strip-item:hover { background: rgba(255,255,255,0.05); } +.cta-strip-item:hover .cta-strip-arrow { transform: translateX(4px); color: var(--color-accent); } +.cta-strip-icon { + width: 44px; + height: 44px; + background: var(--color-surface-2); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-accent); + font-size: 1.1rem; + flex-shrink: 0; +} +.cta-strip-text { flex: 1; } +.cta-strip-label { + display: block; + font-size: 0.95rem; + font-weight: 700; + color: var(--color-white); + letter-spacing: -0.01em; +} +.cta-strip-sub { + display: block; + font-size: 0.75rem; + color: var(--color-text-2); + margin-top: 2px; +} +.cta-strip-arrow { + color: var(--color-text-3); + font-size: 0.85rem; + transition: transform 0.2s, color 0.2s; + flex-shrink: 0; +} +.cta-strip-primary .cta-strip-icon { + background: var(--color-accent); + border-color: var(--color-accent); + color: #fff; +} +.cta-strip-primary .cta-strip-label { color: var(--color-accent); } +.cta-strip-primary:hover { background: rgba(232,41,27,0.08); } + +@media (max-width: 768px) { + .hero-cta-strip { grid-template-columns: 1fr; } + .cta-strip-item { border-right: none; border-bottom: 1px solid var(--color-border); padding: 16px 20px; } + .cta-strip-item:last-child { border-bottom: none; } +} +.hero-strip-inner { + display: flex; + align-items: center; + gap: 0; + padding: 18px 0; + overflow-x: auto; +} +.strip-item { + display: flex; + align-items: center; + gap: 10px; + color: rgba(255,255,255,0.8); + font-size: 0.875rem; + font-weight: 500; + padding: 0 28px; + white-space: nowrap; + border-right: 1px solid var(--color-border); +} +.strip-item:last-child { border-right: none; } +.strip-item i { color: var(--color-accent); font-size: 1rem; } + +/* Page Hero (inner pages) */ +.page-hero { + position: relative; + min-height: auto; + padding: 180px 0 100px; + display: flex; + align-items: flex-end; + justify-content: flex-start; + background-size: cover; + background-position: center; + overflow: hidden; +} +.page-hero::before { + content: ''; + position: absolute; + inset: 0; + background: + linear-gradient(to top, rgba(0,0,0,0.90) 0%, rgba(0,0,0,0.5) 50%, rgba(0,0,0,0.2) 100%), + linear-gradient(to right, rgba(0,0,0,0.7) 0%, transparent 65%); +} +.page-hero .container { + position: relative; + z-index: 1; + max-width: none; + margin: 0; + padding-left: max(40px, 6vw); + padding-right: max(40px, 6vw); +} +.page-hero-content { max-width: 680px; } +.page-hero-content .hero-eyebrow { margin-bottom: 20px; } +.page-hero .hero-title, +.page-hero-content h1 { + font-size: clamp(1.8rem, 4vw, 3.2rem); + font-weight: 900; + color: var(--color-white); + letter-spacing: -0.05em; + line-height: 0.94; + margin-bottom: 24px; + text-align: left; + text-shadow: none; +} +.page-hero-content p { + font-size: 1rem; + color: rgba(255,255,255,0.5); + line-height: 1.65; + max-width: 420px; + margin-bottom: 36px; + font-weight: 400; +} +.page-hero .paragraph { text-align: left; } + +/* ===== Services Grid ===== */ +.services-overview { + padding: 100px 0; + background: var(--color-bg); +} +.services-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 18px; +} +.service-card { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + overflow: hidden; + transition: var(--transition); + display: flex; + flex-direction: column; + text-align: left; +} +.service-card:hover { + border-color: var(--color-accent); + transform: translateY(-4px); + box-shadow: 0 20px 40px rgba(0,0,0,0.4), 0 0 0 1px var(--color-accent); +} +.service-card .service-image { + width: 100%; + height: 200px; + overflow: hidden; + display: block; + margin: 0; + border-radius: 0; + position: relative; +} +.service-card .service-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.4s ease; +} +.service-card:hover .service-image img { transform: scale(1.06); } + +.service-card .service-icon { + width: 44px; + height: 44px; + background: var(--color-accent-glow); + border: 1px solid var(--color-accent); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-accent); + font-size: 1.1rem; + margin-bottom: 14px; +} + +/* card text area */ +.service-card > *:not(.service-image) { padding: 0 24px; } +.service-card > *:first-child:not(.service-image) { padding-top: 24px; } +.service-card > .btn { margin: 0 24px 24px; } + +.service-card h3, +.heading-3 { + font-size: 1.2rem; + font-weight: 700; + color: var(--color-white) !important; + margin: 16px 0 6px; + letter-spacing: -0.02em; +} +.text-block-4 { + font-size: 0.75rem; + font-weight: 700; + color: var(--color-accent); + text-transform: uppercase; + letter-spacing: 0.1em; + margin-bottom: 10px; +} +.paragraph-2 { + color: var(--color-text-2); + font-size: 0.9rem; + line-height: 1.65; + margin-bottom: 18px; +} +.service-card .btn { + align-self: flex-start; + margin: auto 24px 24px; + font-size: 0.8rem; + padding: 10px 18px; +} +.service-link { + color: var(--color-accent); + font-weight: 600; + font-size: 0.85rem; + display: inline-flex; + align-items: center; + gap: 6px; +} +.service-link i { transition: transform 0.2s; } +.service-link:hover i { transform: translateX(4px); } + +/* ===== Advantages / Why Choose ===== */ +.advantages-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} +.advantage { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 28px; + text-align: center; + transition: var(--transition); +} +.advantage:hover { + border-color: var(--color-accent); + transform: translateY(-3px); + background: var(--color-surface-2); +} +.advantage-icon { + width: 54px; + height: 54px; + background: var(--color-accent-glow); + border: 1px solid rgba(232,41,27,0.3); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto 16px; + color: var(--color-accent); + font-size: 1.3rem; +} +.advantage h3 { + font-size: 1rem; + font-weight: 700; + color: var(--color-white); + margin-bottom: 8px; + letter-spacing: -0.01em; +} +.advantage p { + color: var(--color-text-2); + font-size: 0.875rem; + line-height: 1.65; +} + +/* ===== Why Choose Section ===== */ +.why-choose { + padding: 0 0 100px; + background: var(--color-bg); +} +.why-choose-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0; + align-items: stretch; + max-width: 1200px; + margin: 0 auto 60px; + border-radius: var(--radius-lg); + overflow: hidden; + border: 1px solid var(--color-border); +} +.why-choose-image { + min-height: 420px; + background: #1a1a1a; + background-size: cover; + background-position: center; +} +.why-choose-content { + padding: 60px 48px; + background: var(--color-surface); + display: flex; + flex-direction: column; + justify-content: center; + gap: 16px; +} +.why-choose-content .section-label { text-align: left; } +.why-choose-content .section-title { text-align: left; margin-bottom: 0; } +.why-choose-desc { + color: var(--color-text-2); + font-size: 0.95rem; + line-height: 1.75; +} +.why-choose .advantages-grid { max-width: 1200px; margin: 0 auto; } +.why-choose .advantage { background: var(--color-surface); } + +/* ===== CTA Banner ===== */ +.cta-banner { + padding: 100px 20px; + background: var(--color-white); + text-align: center; + position: relative; + overflow: hidden; +} +.cta-banner::before { + content: ''; + position: absolute; + top: -40%; + left: 50%; + transform: translateX(-50%); + width: 600px; + height: 600px; + background: radial-gradient(circle, rgba(232,41,27,0.06) 0%, transparent 70%); + pointer-events: none; +} +.cta-content { + position: relative; + z-index: 1; + max-width: 640px; + margin: 0 auto; +} +.cta-content h2, +.cta-banner .heading-4 { + font-size: clamp(1.8rem, 3vw, 2.5rem); + font-weight: 800; + color: var(--color-bg); + margin-bottom: 14px; + letter-spacing: -0.03em; + margin-top: 0; + text-align: center; +} +.cta-banner .text-accent { color: var(--color-accent); } +.cta-content p, +.paragraph-4 { + color: #52525b; + font-size: 1.05rem; + margin-bottom: 28px; + line-height: 1.7; + text-align: center; +} +.cta-banner .btn-primary { + padding: 16px 40px; + font-size: 1rem; + font-weight: 700; +} + +/* ===== Service Details ===== */ +.service-details { + padding: 80px 0 100px; + background: var(--color-bg); +} +.service-detail-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 60px; + align-items: center; + max-width: 1100px; + margin: 0 auto; +} +.service-detail-row.reverse { direction: rtl; } +.service-detail-row.reverse > * { direction: ltr; } + +.service-detail-content h2 { + font-size: clamp(1.6rem, 3vw, 2.5rem); + font-weight: 800; + color: var(--color-white); + margin-bottom: 20px; + letter-spacing: -0.03em; +} +.service-detail-desc { + color: var(--color-text-2); + font-size: 1rem; + line-height: 1.75; + margin-bottom: 20px; +} + +/* Service Details Alt (light bg) */ +.service-details-alt { + padding: 80px 0; + background: var(--color-surface); + border-top: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); +} +.service-details-alt .section-title { color: var(--color-white); margin-bottom: 16px; } +.service-details-alt .section-subtitle { color: var(--color-text-2); } +.service-details-alt .advantage { + background: var(--color-surface-2); + border-color: var(--color-border); +} + +/* ===== Image Gallery ===== */ +.image-gallery { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + margin: 40px auto; + max-width: 960px; + border-radius: var(--radius-md); + overflow: hidden; +} +.image-gallery img { + width: 100%; + height: 240px; + object-fit: cover; + transition: transform 0.35s ease; +} +.image-gallery img:hover { transform: scale(1.04); } + +/* ===== What's Included ===== */ +.whats-included { + padding: 80px 0; + background: var(--color-bg); + border-top: 1px solid var(--color-border); +} +.whats-included .section-title { text-align: center; margin-bottom: 48px; } +.checklist { + list-style: none; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 12px; +} +.checklist li { + display: flex; + align-items: center; + gap: 14px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + padding: 16px 20px; + color: var(--color-text-1); + font-size: 0.9rem; + font-weight: 600; + transition: var(--transition); +} +.checklist li:hover { + border-color: var(--color-accent); + background: var(--color-surface-2); + transform: translateX(4px); +} +.checklist li i { + color: var(--color-accent); + font-size: 0.9rem; + flex-shrink: 0; + width: 16px; +} + +/* ===== Reviews ===== */ +.reviews { + padding: 80px 0; + background: var(--color-white); +} +.reviews .heading-4.black, +.reviews h2 { + font-size: clamp(1.8rem, 3vw, 2.5rem); + font-weight: 800; + color: var(--color-bg); + text-align: center; + margin-bottom: 48px; + margin-top: 0; + letter-spacing: -0.03em; +} +.reviews-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 18px; +} +.review-card { + background: #fafafa; + border: 1px solid #e4e4e7; + border-radius: var(--radius-md); + padding: 28px; + transition: var(--transition); +} +.review-card:hover { box-shadow: var(--shadow-md); transform: translateY(-2px); } +.review-heading { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 14px; +} +.reviewer-name { + font-weight: 700; + font-size: 0.9rem; + color: var(--color-bg); +} +.stars { color: var(--color-star); font-size: 0.8rem; letter-spacing: 2px; } +.review-text { + color: #3f3f46; + font-size: 0.9rem; + line-height: 1.7; + margin-bottom: 12px; + font-style: italic; +} +.reviewer-location { color: #a1a1aa; font-size: 0.8rem; } + +/* ===== Contact ===== */ +.contact { + padding: 100px 0; + background: var(--color-surface); +} +.contact-wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 80px; + align-items: start; + max-width: 1100px; + margin: 0 auto; +} +.contact-info h2 { + font-size: clamp(1.8rem, 3vw, 2.4rem); + font-weight: 800; + color: var(--color-white); + margin-bottom: 14px; + letter-spacing: -0.03em; +} +.contact-info .section-label { margin-bottom: 10px; } +.contact-info > p { + color: var(--color-text-2); + font-size: 1rem; + line-height: 1.7; + margin-bottom: 32px; +} +.contact-details { display: flex; flex-direction: column; gap: 14px; margin-bottom: 32px; } +.contact-item { + display: flex; + align-items: center; + gap: 14px; + color: var(--color-text-1); + font-size: 0.9rem; +} +.contact-item i { + width: 38px; + height: 38px; + background: var(--color-surface-2); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-accent); + flex-shrink: 0; + font-size: 0.85rem; +} +.contact-item a:hover { color: var(--color-accent); } +.social-links { display: flex; gap: 10px; } +.social-links a { + width: 40px; + height: 40px; + background: var(--color-surface-2); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-text-2); + font-size: 0.95rem; + transition: var(--transition); +} +.social-links a:hover { color: var(--color-white); border-color: var(--color-accent); background: var(--color-accent-glow); } + +.contact-form { + background: var(--color-surface-2); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 40px; +} +.contact-form h3 { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-white); + margin-bottom: 28px; + letter-spacing: -0.02em; +} +.contact-form input, +.contact-form select, +.contact-form textarea { + width: 100%; + padding: 12px 16px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + color: var(--color-text-1); + font-family: var(--font); + font-size: 0.9rem; + margin-bottom: 14px; + transition: var(--transition); + outline: none; +} +.contact-form input::placeholder, +.contact-form textarea::placeholder { color: var(--color-text-3); } +.contact-form input:focus, +.contact-form select:focus, +.contact-form textarea:focus { + border-color: var(--color-accent); + background: var(--color-surface-3); +} +.contact-form select { color: var(--color-text-1); } +.contact-form select option { background: var(--color-surface-2); } +.contact-form textarea { min-height: 110px; resize: vertical; } + +/* ===== Footer ===== */ +.footer { + background: var(--color-surface); + border-top: 1px solid var(--color-border); + padding: 60px 0 0; +} +.footer-grid { + display: grid; + grid-template-columns: 2fr 1.2fr 1.2fr 1.2fr; + gap: 48px; + padding-bottom: 60px; + border-bottom: 1px solid var(--color-border); + max-width: 1200px; + margin: 0 auto; +} +.footer-logo { + font-size: 1.35rem; + font-weight: 800; + color: var(--color-white); + letter-spacing: -0.03em; + margin-bottom: 14px; + display: inline-flex; + align-items: center; +} +.footer-brand p { + color: var(--color-text-2); + font-size: 0.875rem; + line-height: 1.7; + max-width: 280px; +} +.footer-links h4 { + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--color-text-3); + margin-bottom: 18px; +} +.footer-links ul { list-style: none; } +.footer-links li { + margin-bottom: 10px; + font-size: 0.875rem; + display: flex; + align-items: flex-start; + gap: 8px; +} +.footer-links a { color: var(--color-text-2); } +.footer-links a:hover { color: var(--color-accent); } +.footer-links li i { color: var(--color-accent); margin-top: 3px; font-size: 0.8rem; flex-shrink: 0; } +.footer-bottom { + padding: 20px 0; + text-align: center; + color: var(--color-text-3); + font-size: 0.8rem; + background: var(--color-bg); +} + +/* ===== Legacy / Utility ===== */ +.text-accent { color: var(--color-accent); } +.heading-4 { + font-size: clamp(1.8rem, 3vw, 2.5rem); + font-weight: 800; + color: var(--color-white); + text-align: center; + margin: 0 0 40px; + letter-spacing: -0.03em; +} +.heading-4.black { color: var(--color-bg); } +.heading-detail { + font-size: 1.8rem; + color: var(--color-white); + font-weight: 800; + margin-bottom: 24px; + letter-spacing: -0.03em; +} +.services-overview .section-label, +.services-overview .section-title, +.services-overview .section-subtitle { color: var(--color-white) !important; } + +/* Dropdown list (service page accordion) */ +.dropdown-list { display: flex; flex-direction: column; gap: 0; border-top: 1px solid var(--color-border); } +.service-dropdown { border-bottom: 1px solid var(--color-border); } +.dropdown-toggle { + background: var(--color-surface); + padding: 14px 18px; + display: flex; + align-items: center; + gap: 10px; + cursor: pointer; + color: var(--color-text-1); + font-weight: 600; + font-size: 0.9rem; +} +.dropdown-toggle i { font-size: 0.7rem; transition: transform 0.2s; } +.dropdown-toggle.active i { transform: rotate(180deg); } +.dropdown-content { + background: var(--color-surface-2); + padding: 14px 18px; + display: none; + font-size: 0.9rem; + color: var(--color-text-2); + line-height: 1.65; +} +.dropdown-content.active { display: block; } + +/* phone-link */ +.phone-link { font-weight: 600; color: var(--color-accent); font-size: 0.875rem; } + +/* ===== Responsive ===== */ +@media (max-width: 1024px) { + .services-grid { grid-template-columns: repeat(2, 1fr); } + .advantages-grid { grid-template-columns: repeat(2, 1fr); } + .reviews-grid { grid-template-columns: repeat(2, 1fr); } + .footer-grid { grid-template-columns: 1fr 1fr; gap: 32px; } + .why-choose-wrapper { grid-template-columns: 1fr; } + .why-choose-image { min-height: 300px; } + .service-detail-row { grid-template-columns: 1fr; gap: 40px; } + .image-gallery { grid-template-columns: repeat(2,1fr); } +} + +@media (max-width: 768px) { + .container { padding: 0 20px; } + + /* Nav */ + .nav-links { + display: none; + position: absolute; + top: 100%; + left: 0; + width: 100%; + background: var(--color-surface); + border-bottom: 1px solid var(--color-border); + flex-direction: column; + padding: 16px; + gap: 4px; + z-index: 999; + } + .nav-links.active { display: flex; } + .nav-links a { text-align: left; } + .nav-contact { + display: none; + position: absolute; + top: calc(100% + 260px); + left: 0; + width: 100%; + background: var(--color-surface); + flex-direction: row; + padding: 14px 20px; + border-bottom: 1px solid var(--color-border); + gap: 12px; + z-index: 999; + } + .nav-contact.active { display: flex; } + .mobile-menu-toggle { display: flex; } + + /* Nav dropdown mobile */ + .nav-dropdown { width: 100%; } + .dropdown-label { width: 100%; padding: 8px 14px; } + .dropdown-menu { + position: static; + display: none; + border: none; + border-radius: 0; + box-shadow: none; + background: var(--color-surface-2); + padding: 4px 8px; + margin-top: 4px; + } + .nav-dropdown:hover .dropdown-menu { display: none; } + .nav-dropdown.open .dropdown-menu { display: block; } + + /* Hero */ + .hero { min-height: 100vh; } + .hero .container { padding-top: 100px; padding-bottom: 0; } + .hero-title { font-size: clamp(2.2rem, 8vw, 3.5rem); } + .hero-actions { flex-direction: column; } + .hero-actions .btn { width: 100%; justify-content: center; } + .hero-strip-inner { gap: 0; flex-wrap: wrap; } + .strip-item { padding: 10px 16px; flex: 1 1 auto; border-right: none; border-bottom: 1px solid var(--color-border); } + + /* Sections */ + .services-grid, + .advantages-grid, + .reviews-grid { grid-template-columns: 1fr; } + .section-title { font-size: 1.7rem; } + .why-choose-content { padding: 36px 24px; } + .contact-wrapper { grid-template-columns: 1fr; gap: 48px; } + .footer-grid { grid-template-columns: 1fr; gap: 32px; } + .checklist { grid-template-columns: 1fr; } + .image-gallery { grid-template-columns: 1fr; } + .image-gallery img { height: 200px; } + .contact-form { padding: 28px 20px; } +} + +@media (max-width: 480px) { + .hero-title { font-size: 2rem; } + .section-title { font-size: 1.5rem; } + .cta-content h2 { font-size: 1.6rem; } +} + +/* ===== Generic Section Layout ===== */ +.section { + position: relative; + padding: 120px 0; + background: var(--color-bg); +} +.section-alt { + position: relative; + padding: 120px 0; + background: var(--color-surface); + overflow: hidden; +} +/* Noise grain texture on alt sections */ +.section-alt::after { + content: ''; + position: absolute; + inset: 0; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='300' height='300' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E"); + pointer-events: none; + z-index: 0; +} +.section-alt > .container { position: relative; z-index: 1; } + +/* ===== Features Grid ===== */ +.features-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 20px; +} +.feature-item { + background: var(--color-surface-2); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + padding: 32px 28px; + transition: var(--transition); +} +.feature-item:hover { + border-color: rgba(232,41,27,0.3); + transform: translateY(-3px); + box-shadow: 0 12px 32px rgba(0,0,0,0.3); +} +.feature-icon { + width: 48px; + height: 48px; + background: var(--color-accent-glow); + border: 1px solid rgba(232,41,27,0.25); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + color: var(--color-accent); + font-size: 1.1rem; + margin-bottom: 20px; + flex-shrink: 0; +} +.feature-item h3 { + font-size: 1rem; + font-weight: 700; + color: var(--color-white); + letter-spacing: -0.02em; + margin-bottom: 10px; + line-height: 1.3; +} +.feature-item p { + color: var(--color-text-2); + font-size: 0.875rem; + line-height: 1.72; +} + +/* ===== Why Grid (two-column split) ===== */ +.why-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 80px; + align-items: center; +} +.why-text { + display: flex; + flex-direction: column; + gap: 20px; +} +.why-text h2 { + font-size: clamp(2rem, 4vw, 3.2rem); + font-weight: 900; + letter-spacing: -0.045em; + line-height: 1.0; + color: var(--color-white); + margin: 0; +} +.why-text p { + color: var(--color-text-2); + font-size: 0.95rem; + line-height: 1.78; + margin: 0; +} +.why-grid .why-choose-image { + min-height: 480px; + background: var(--color-surface-2); + background-size: cover; + background-position: center; + border-radius: var(--radius-lg); + border: 1px solid var(--color-border); +} + +/* ===== CTA Section ===== */ +.cta-section { + position: relative; + padding: 120px 0; + background: var(--color-surface); + overflow: hidden; +} +.cta-section::before { + content: ''; + position: absolute; + inset: 0; + background-image: radial-gradient(rgba(232,41,27,0.07) 1px, transparent 1px); + background-size: 32px 32px; + pointer-events: none; +} +.cta-content { + text-align: center; + max-width: 640px; + margin: 0 auto; + position: relative; + z-index: 1; +} +.cta-content h2 { + font-size: clamp(2rem, 4vw, 3.4rem); + font-weight: 900; + letter-spacing: -0.045em; + line-height: 1.0; + color: var(--color-white); + margin-bottom: 16px; +} +.cta-content p { + color: var(--color-text-2); + font-size: 1rem; + margin-bottom: 40px; + line-height: 1.7; +} +.cta-actions { + display: flex; + gap: 14px; + justify-content: center; + flex-wrap: wrap; +} + +/* ===== How It Works / Process Steps ===== */ +.how-it-works { + padding: 100px 0; + background: var(--color-surface); + position: relative; + overflow: hidden; +} +.how-it-works::after { + content: ''; + position: absolute; + inset: 0; + background: + radial-gradient(rgba(232,41,27,0.03) 1px, transparent 1px); + background-size: 32px 32px; + pointer-events: none; +} +.process-track { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0; + position: relative; + margin-top: 72px; +} +.process-connector { + position: absolute; + top: 27px; + left: calc(16.66% + 28px); + right: calc(16.66% + 28px); + height: 1px; + background: var(--color-border); + z-index: 0; + overflow: hidden; +} +.process-connector-fill { + height: 100%; + width: 0; + background: linear-gradient(to right, var(--color-accent), rgba(232,41,27,0.4)); + transition: width 1.2s cubic-bezier(0.4, 0, 0.2, 1); +} +.process-track.animated .process-connector-fill { + width: 100%; +} +.process-step { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 0 40px; + position: relative; + z-index: 1; + opacity: 0; + transform: translateY(24px); + transition: opacity 0.5s ease, transform 0.5s ease; +} +.process-step.visible { + opacity: 1; + transform: translateY(0); +} +.process-num { + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--color-bg); + border: 1px solid var(--color-border); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.1rem; + font-weight: 800; + color: var(--color-text-3); + letter-spacing: -0.03em; + margin-bottom: 28px; + transition: border-color 0.5s ease, color 0.5s ease, background 0.5s ease, box-shadow 0.5s ease; +} +.process-step.visible .process-num { + border-color: var(--color-accent); + color: var(--color-accent); + background: var(--color-accent-glow); + box-shadow: 0 0 0 6px rgba(232,41,27,0.06); +} +.process-step h3 { + font-size: 1.1rem; + font-weight: 700; + color: var(--color-white); + letter-spacing: -0.025em; + margin-bottom: 12px; + line-height: 1.2; +} +.process-step p { + font-size: 0.9rem; + color: var(--color-text-2); + line-height: 1.7; + max-width: 260px; +} + +@media (max-width: 768px) { + .process-track { + grid-template-columns: 1fr; + gap: 48px; + margin-top: 48px; + } + .process-connector { display: none; } + .process-step { padding: 0 20px; } +} + +/* ===== Responsive overrides ===== */ +@media (max-width: 1024px) { + .features-grid { grid-template-columns: repeat(2, 1fr); } +} +@media (max-width: 768px) { + .section, .section-alt { padding: 80px 0; } + .features-grid { grid-template-columns: 1fr; } + .why-grid { grid-template-columns: 1fr; gap: 40px; } + .why-grid .why-choose-image { min-height: 260px; order: -1; } + .cta-section { padding: 80px 0; } +} + diff --git a/assets/images/add-ons/AdobeStock_109112537_Preview.jpeg b/assets/images/add-ons/AdobeStock_109112537_Preview.jpeg new file mode 100644 index 0000000..4ba5f7e Binary files /dev/null and b/assets/images/add-ons/AdobeStock_109112537_Preview.jpeg differ diff --git a/assets/images/add-ons/AdobeStock_1844489241_Preview.jpeg b/assets/images/add-ons/AdobeStock_1844489241_Preview.jpeg new file mode 100644 index 0000000..8067abe Binary files /dev/null and b/assets/images/add-ons/AdobeStock_1844489241_Preview.jpeg differ diff --git a/assets/images/add-ons/AdobeStock_1844493920_Preview.jpeg b/assets/images/add-ons/AdobeStock_1844493920_Preview.jpeg new file mode 100644 index 0000000..6a8bc85 Binary files /dev/null and b/assets/images/add-ons/AdobeStock_1844493920_Preview.jpeg differ diff --git a/assets/images/add-ons/AdobeStock_1844496776_Preview.jpeg b/assets/images/add-ons/AdobeStock_1844496776_Preview.jpeg new file mode 100644 index 0000000..ae3b57a Binary files /dev/null and b/assets/images/add-ons/AdobeStock_1844496776_Preview.jpeg differ diff --git a/assets/images/add-ons/AdobeStock_85679649_Preview.jpeg b/assets/images/add-ons/AdobeStock_85679649_Preview.jpeg new file mode 100644 index 0000000..3ce6725 Binary files /dev/null and b/assets/images/add-ons/AdobeStock_85679649_Preview.jpeg differ diff --git a/assets/images/add-ons/AdobeStock_927963367_Preview.jpeg b/assets/images/add-ons/AdobeStock_927963367_Preview.jpeg new file mode 100644 index 0000000..b746bd8 Binary files /dev/null and b/assets/images/add-ons/AdobeStock_927963367_Preview.jpeg differ diff --git a/assets/images/area-rugs/AdobeStock_127441811_Preview.jpeg b/assets/images/area-rugs/AdobeStock_127441811_Preview.jpeg new file mode 100644 index 0000000..bca9b90 Binary files /dev/null and b/assets/images/area-rugs/AdobeStock_127441811_Preview.jpeg differ diff --git a/assets/images/area-rugs/AdobeStock_1807708661_Preview.jpeg b/assets/images/area-rugs/AdobeStock_1807708661_Preview.jpeg new file mode 100644 index 0000000..bc6e0be Binary files /dev/null and b/assets/images/area-rugs/AdobeStock_1807708661_Preview.jpeg differ diff --git a/assets/images/area-rugs/AdobeStock_1929234088_Preview.jpeg b/assets/images/area-rugs/AdobeStock_1929234088_Preview.jpeg new file mode 100644 index 0000000..3adf572 Binary files /dev/null and b/assets/images/area-rugs/AdobeStock_1929234088_Preview.jpeg differ diff --git a/assets/images/area-rugs/AdobeStock_2009290610_Preview.jpeg b/assets/images/area-rugs/AdobeStock_2009290610_Preview.jpeg new file mode 100644 index 0000000..200dfeb Binary files /dev/null and b/assets/images/area-rugs/AdobeStock_2009290610_Preview.jpeg differ diff --git a/assets/images/area-rugs/AdobeStock_491752943_Preview.jpeg b/assets/images/area-rugs/AdobeStock_491752943_Preview.jpeg new file mode 100644 index 0000000..8cdee3f Binary files /dev/null and b/assets/images/area-rugs/AdobeStock_491752943_Preview.jpeg differ diff --git a/assets/images/carpet-cleaning/AdobeStock_1844496142_Preview.jpeg b/assets/images/carpet-cleaning/AdobeStock_1844496142_Preview.jpeg new file mode 100644 index 0000000..263a43b Binary files /dev/null and b/assets/images/carpet-cleaning/AdobeStock_1844496142_Preview.jpeg differ diff --git a/assets/images/carpet-cleaning/AdobeStock_1942695256_Preview.jpeg b/assets/images/carpet-cleaning/AdobeStock_1942695256_Preview.jpeg new file mode 100644 index 0000000..0819399 Binary files /dev/null and b/assets/images/carpet-cleaning/AdobeStock_1942695256_Preview.jpeg differ diff --git a/assets/images/carpet-cleaning/AdobeStock_1976068673_Preview.jpeg b/assets/images/carpet-cleaning/AdobeStock_1976068673_Preview.jpeg new file mode 100644 index 0000000..1660520 Binary files /dev/null and b/assets/images/carpet-cleaning/AdobeStock_1976068673_Preview.jpeg differ diff --git a/assets/images/carpet-cleaning/AdobeStock_403353983_Preview.jpeg b/assets/images/carpet-cleaning/AdobeStock_403353983_Preview.jpeg new file mode 100644 index 0000000..5eea5f6 Binary files /dev/null and b/assets/images/carpet-cleaning/AdobeStock_403353983_Preview.jpeg differ diff --git a/assets/images/carpet-cleaning/AdobeStock_491752943_Preview.jpeg b/assets/images/carpet-cleaning/AdobeStock_491752943_Preview.jpeg new file mode 100644 index 0000000..8cdee3f Binary files /dev/null and b/assets/images/carpet-cleaning/AdobeStock_491752943_Preview.jpeg differ diff --git a/assets/images/commercial/AdobeStock_169381335_Preview.jpeg b/assets/images/commercial/AdobeStock_169381335_Preview.jpeg new file mode 100644 index 0000000..1537c01 Binary files /dev/null and b/assets/images/commercial/AdobeStock_169381335_Preview.jpeg differ diff --git a/assets/images/commercial/AdobeStock_1827877985_Preview.jpeg b/assets/images/commercial/AdobeStock_1827877985_Preview.jpeg new file mode 100644 index 0000000..b3a9d1a Binary files /dev/null and b/assets/images/commercial/AdobeStock_1827877985_Preview.jpeg differ diff --git a/assets/images/commercial/AdobeStock_1893842152_Preview.jpeg b/assets/images/commercial/AdobeStock_1893842152_Preview.jpeg new file mode 100644 index 0000000..a4700c7 Binary files /dev/null and b/assets/images/commercial/AdobeStock_1893842152_Preview.jpeg differ diff --git a/assets/images/commercial/AdobeStock_1922957325_Preview.jpeg b/assets/images/commercial/AdobeStock_1922957325_Preview.jpeg new file mode 100644 index 0000000..9ce9438 Binary files /dev/null and b/assets/images/commercial/AdobeStock_1922957325_Preview.jpeg differ diff --git a/assets/images/commercial/AdobeStock_216501111_Preview.jpeg b/assets/images/commercial/AdobeStock_216501111_Preview.jpeg new file mode 100644 index 0000000..8546b37 Binary files /dev/null and b/assets/images/commercial/AdobeStock_216501111_Preview.jpeg differ diff --git a/assets/images/hero/hero-before-after.jpg b/assets/images/hero/hero-before-after.jpg new file mode 100644 index 0000000..5d1b460 Binary files /dev/null and b/assets/images/hero/hero-before-after.jpg differ diff --git a/assets/images/hero/hero-clean-result.jpg b/assets/images/hero/hero-clean-result.jpg new file mode 100644 index 0000000..2dc3f2a Binary files /dev/null and b/assets/images/hero/hero-clean-result.jpg differ diff --git a/assets/images/hero/hero-living-room.jpg b/assets/images/hero/hero-living-room.jpg new file mode 100644 index 0000000..b6aa4f6 Binary files /dev/null and b/assets/images/hero/hero-living-room.jpg differ diff --git a/assets/images/hero/hero-stairs.jpg b/assets/images/hero/hero-stairs.jpg new file mode 100644 index 0000000..a7ca08b Binary files /dev/null and b/assets/images/hero/hero-stairs.jpg differ diff --git a/assets/images/hero/hero-technician.jpg b/assets/images/hero/hero-technician.jpg new file mode 100644 index 0000000..2dc3f2a Binary files /dev/null and b/assets/images/hero/hero-technician.jpg differ diff --git a/assets/images/services/add-ons.jpg b/assets/images/services/add-ons.jpg new file mode 100644 index 0000000..a0c6f5e Binary files /dev/null and b/assets/images/services/add-ons.jpg differ diff --git a/assets/images/services/area-rug-cleaning.jpg b/assets/images/services/area-rug-cleaning.jpg new file mode 100644 index 0000000..519a4ef Binary files /dev/null and b/assets/images/services/area-rug-cleaning.jpg differ diff --git a/assets/images/services/carpet-cleaning.jpg b/assets/images/services/carpet-cleaning.jpg new file mode 100644 index 0000000..ebbded8 Binary files /dev/null and b/assets/images/services/carpet-cleaning.jpg differ diff --git a/assets/images/services/commercial-overview.jpg b/assets/images/services/commercial-overview.jpg new file mode 100644 index 0000000..b2a3ad8 Binary files /dev/null and b/assets/images/services/commercial-overview.jpg differ diff --git a/assets/images/services/floor-cleaning.jpg b/assets/images/services/floor-cleaning.jpg new file mode 100644 index 0000000..0861aaf Binary files /dev/null and b/assets/images/services/floor-cleaning.jpg differ diff --git a/assets/images/services/hotels-inns.jpg b/assets/images/services/hotels-inns.jpg new file mode 100644 index 0000000..f8b6399 Binary files /dev/null and b/assets/images/services/hotels-inns.jpg differ diff --git a/assets/images/services/office-spaces.jpg b/assets/images/services/office-spaces.jpg new file mode 100644 index 0000000..129f621 Binary files /dev/null and b/assets/images/services/office-spaces.jpg differ diff --git a/assets/images/services/property-management.jpg b/assets/images/services/property-management.jpg new file mode 100644 index 0000000..ce97d18 Binary files /dev/null and b/assets/images/services/property-management.jpg differ diff --git a/assets/images/services/retail-showrooms.jpg b/assets/images/services/retail-showrooms.jpg new file mode 100644 index 0000000..33a9f5d Binary files /dev/null and b/assets/images/services/retail-showrooms.jpg differ diff --git a/assets/images/services/stairs-cleaning.jpg b/assets/images/services/stairs-cleaning.jpg new file mode 100644 index 0000000..410628f Binary files /dev/null and b/assets/images/services/stairs-cleaning.jpg differ diff --git a/assets/images/services/upholstery-cleaning.jpg b/assets/images/services/upholstery-cleaning.jpg new file mode 100644 index 0000000..6b62744 Binary files /dev/null and b/assets/images/services/upholstery-cleaning.jpg differ diff --git a/assets/images/services/vacation-rentals.jpg b/assets/images/services/vacation-rentals.jpg new file mode 100644 index 0000000..e7bb8f2 Binary files /dev/null and b/assets/images/services/vacation-rentals.jpg differ diff --git a/assets/images/upholstery/AdobeStock_178752004_Preview.jpeg b/assets/images/upholstery/AdobeStock_178752004_Preview.jpeg new file mode 100644 index 0000000..6ac093d Binary files /dev/null and b/assets/images/upholstery/AdobeStock_178752004_Preview.jpeg differ diff --git a/assets/images/upholstery/AdobeStock_4862794_Preview.jpeg b/assets/images/upholstery/AdobeStock_4862794_Preview.jpeg new file mode 100644 index 0000000..c70f733 Binary files /dev/null and b/assets/images/upholstery/AdobeStock_4862794_Preview.jpeg differ diff --git a/assets/images/upholstery/AdobeStock_601638841_Preview.jpeg b/assets/images/upholstery/AdobeStock_601638841_Preview.jpeg new file mode 100644 index 0000000..802e134 Binary files /dev/null and b/assets/images/upholstery/AdobeStock_601638841_Preview.jpeg differ diff --git a/assets/js/components.js b/assets/js/components.js new file mode 100644 index 0000000..e012203 --- /dev/null +++ b/assets/js/components.js @@ -0,0 +1,143 @@ +(function () { + var NAV = ``; + + var FOOTER = ``; + + var PROCESS = `
+
+
+ +

Getting Started Is Simple

+

Three steps from first contact to clean carpets.

+
+
+
+
+
1
+

Contact Us

+

Call or submit the form online. Tell us what you need cleaned and we will confirm availability for your area.

+
+
+
2
+

We Arrive and Clean

+

Our technician arrives on time with professional equipment. We pre-treat, extract, and inspect before we leave.

+
+
+
3
+

Enjoy the Results

+

Your carpets dry in hours. You will see and smell the difference the same day we finish.

+
+
+
+
`; + + var navEl = document.getElementById('site-nav'); + var footerEl = document.getElementById('site-footer'); + var processEl = document.getElementById('site-process'); + if (navEl) navEl.innerHTML = NAV; + if (footerEl) footerEl.innerHTML = FOOTER; + if (processEl) processEl.outerHTML = PROCESS; +})(); diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..426b35b --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,68 @@ +const mobileToggle = document.querySelector('.mobile-menu-toggle'); +const navLinks = document.querySelector('.nav-links'); +const navContact = document.querySelector('.nav-contact'); +mobileToggle.addEventListener('click', () => { + navLinks.classList.toggle('active'); + navContact.classList.toggle('active'); + mobileToggle.classList.toggle('active'); +}); +const dropdownLabel = document.querySelector('.dropdown-label'); +const navDropdown = document.querySelector('.nav-dropdown'); +if (dropdownLabel && navDropdown) { + dropdownLabel.addEventListener('click', function(e) { + if (window.innerWidth <= 768) { + e.preventDefault(); + e.stopPropagation(); + navDropdown.classList.toggle('open'); + } + }); + document.addEventListener('click', function(e) { + if (!navDropdown.contains(e.target)) { + navDropdown.classList.remove('open'); + } + }); +} +document.querySelectorAll('.dropdown-toggle').forEach(toggle => { + toggle.addEventListener('click', function() { + this.classList.toggle('active'); + this.nextElementSibling.classList.toggle('active'); + }); +}); +const form = document.getElementById('quoteForm'); +if (form) { + form.addEventListener('submit', function(e) { + e.preventDefault(); + alert('Thank you for your quote request! We will contact you shortly.'); + this.reset(); + }); +} + +// How It Works — scroll-triggered step reveal +(function () { + function initProcess() { + var track = document.querySelector('.process-track'); + if (!track) return; + var steps = track.querySelectorAll('.process-step'); + var observer = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + track.classList.add('animated'); + steps.forEach(function (step, i) { + setTimeout(function () { + step.classList.add('visible'); + }, i * 220); + }); + observer.disconnect(); + } + }); + }, { threshold: 0.25 }); + observer.observe(track); + } + // components.js runs before main.js, but process section is injected synchronously + // so it's available immediately + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initProcess); + } else { + initProcess(); + } +}()); diff --git a/assets/screenshot-home.png b/assets/screenshot-home.png new file mode 100644 index 0000000..c2c16ac Binary files /dev/null and b/assets/screenshot-home.png differ diff --git a/assets/videos/hero/clips/concat-browser.txt b/assets/videos/hero/clips/concat-browser.txt new file mode 100644 index 0000000..c51cd82 --- /dev/null +++ b/assets/videos/hero/clips/concat-browser.txt @@ -0,0 +1,16 @@ +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-01.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-04.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-02.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-03-stain-on-chair.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-05-extraction-couch.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-03-technician.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-06.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-07.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-03.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-05-clean-stairs.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-05.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-07-restaurant.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-06-office.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-01-wide-room.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-05-clean-reveal.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-04-extraction-carpet.mp4' diff --git a/assets/videos/hero/clips/concat-v2.txt b/assets/videos/hero/clips/concat-v2.txt new file mode 100644 index 0000000..28cd9f8 --- /dev/null +++ b/assets/videos/hero/clips/concat-v2.txt @@ -0,0 +1,7 @@ +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-01-door-entry.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-02-mud-on-carpet.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-03-stain-on-chair.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-04-extraction-carpet.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-05-clean-stairs.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-06-office.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v2-shot-07-restaurant.mp4' diff --git a/assets/videos/hero/clips/concat-v3.txt b/assets/videos/hero/clips/concat-v3.txt new file mode 100644 index 0000000..ed1d20d --- /dev/null +++ b/assets/videos/hero/clips/concat-v3.txt @@ -0,0 +1,7 @@ +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-01.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-02.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-03.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-04.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-05.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-06.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/v3-shot-07.mp4' diff --git a/assets/videos/hero/clips/concat.txt b/assets/videos/hero/clips/concat.txt new file mode 100644 index 0000000..232d4f3 --- /dev/null +++ b/assets/videos/hero/clips/concat.txt @@ -0,0 +1,9 @@ +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-01-door-opens-trimmed.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-02-pan-to-stains.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-03-stain-closeup.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-04-extraction-carpet.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-05-extraction-couch.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-06-extraction-stairs.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-07-office-entryway.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-08-showroom.mp4' +file '/home/sirdrez/arisingmedia-websites/lahrcarpetcleaning.com/assets/videos/hero/clips/shot-09-technician-unloading.mp4' diff --git a/assets/videos/hero/clips/shot-01-door-opens-trimmed.mp4 b/assets/videos/hero/clips/shot-01-door-opens-trimmed.mp4 new file mode 100644 index 0000000..b863143 Binary files /dev/null and b/assets/videos/hero/clips/shot-01-door-opens-trimmed.mp4 differ diff --git a/assets/videos/hero/clips/shot-01-door-opens.mp4 b/assets/videos/hero/clips/shot-01-door-opens.mp4 new file mode 100644 index 0000000..686b8e3 Binary files /dev/null and b/assets/videos/hero/clips/shot-01-door-opens.mp4 differ diff --git a/assets/videos/hero/clips/shot-01-wide-room.mp4 b/assets/videos/hero/clips/shot-01-wide-room.mp4 new file mode 100644 index 0000000..90a3328 Binary files /dev/null and b/assets/videos/hero/clips/shot-01-wide-room.mp4 differ diff --git a/assets/videos/hero/clips/shot-02-pan-to-stains.mp4 b/assets/videos/hero/clips/shot-02-pan-to-stains.mp4 new file mode 100644 index 0000000..bb3c255 Binary files /dev/null and b/assets/videos/hero/clips/shot-02-pan-to-stains.mp4 differ diff --git a/assets/videos/hero/clips/shot-02-staircase.mp4 b/assets/videos/hero/clips/shot-02-staircase.mp4 new file mode 100644 index 0000000..ed17fa1 Binary files /dev/null and b/assets/videos/hero/clips/shot-02-staircase.mp4 differ diff --git a/assets/videos/hero/clips/shot-03-stain-closeup.mp4 b/assets/videos/hero/clips/shot-03-stain-closeup.mp4 new file mode 100644 index 0000000..f653221 Binary files /dev/null and b/assets/videos/hero/clips/shot-03-stain-closeup.mp4 differ diff --git a/assets/videos/hero/clips/shot-03-technician.mp4 b/assets/videos/hero/clips/shot-03-technician.mp4 new file mode 100644 index 0000000..b625607 Binary files /dev/null and b/assets/videos/hero/clips/shot-03-technician.mp4 differ diff --git a/assets/videos/hero/clips/shot-04-extraction-carpet.mp4 b/assets/videos/hero/clips/shot-04-extraction-carpet.mp4 new file mode 100644 index 0000000..23424a1 Binary files /dev/null and b/assets/videos/hero/clips/shot-04-extraction-carpet.mp4 differ diff --git a/assets/videos/hero/clips/shot-04-extraction-closeup.mp4 b/assets/videos/hero/clips/shot-04-extraction-closeup.mp4 new file mode 100644 index 0000000..9c8d764 Binary files /dev/null and b/assets/videos/hero/clips/shot-04-extraction-closeup.mp4 differ diff --git a/assets/videos/hero/clips/shot-05-clean-reveal.mp4 b/assets/videos/hero/clips/shot-05-clean-reveal.mp4 new file mode 100644 index 0000000..b8b357f Binary files /dev/null and b/assets/videos/hero/clips/shot-05-clean-reveal.mp4 differ diff --git a/assets/videos/hero/clips/shot-05-extraction-couch.mp4 b/assets/videos/hero/clips/shot-05-extraction-couch.mp4 new file mode 100644 index 0000000..ded7f11 Binary files /dev/null and b/assets/videos/hero/clips/shot-05-extraction-couch.mp4 differ diff --git a/assets/videos/hero/clips/shot-06-extraction-stairs.mp4 b/assets/videos/hero/clips/shot-06-extraction-stairs.mp4 new file mode 100644 index 0000000..28380c5 Binary files /dev/null and b/assets/videos/hero/clips/shot-06-extraction-stairs.mp4 differ diff --git a/assets/videos/hero/clips/shot-07-office-entryway.mp4 b/assets/videos/hero/clips/shot-07-office-entryway.mp4 new file mode 100644 index 0000000..56819d2 Binary files /dev/null and b/assets/videos/hero/clips/shot-07-office-entryway.mp4 differ diff --git a/assets/videos/hero/clips/shot-08-showroom.mp4 b/assets/videos/hero/clips/shot-08-showroom.mp4 new file mode 100644 index 0000000..3369482 Binary files /dev/null and b/assets/videos/hero/clips/shot-08-showroom.mp4 differ diff --git a/assets/videos/hero/clips/shot-09-technician-unloading.mp4 b/assets/videos/hero/clips/shot-09-technician-unloading.mp4 new file mode 100644 index 0000000..79fc8d2 Binary files /dev/null and b/assets/videos/hero/clips/shot-09-technician-unloading.mp4 differ diff --git a/assets/videos/hero/clips/v2-shot-01-door-entry.mp4 b/assets/videos/hero/clips/v2-shot-01-door-entry.mp4 new file mode 100644 index 0000000..c06dc0a Binary files /dev/null and b/assets/videos/hero/clips/v2-shot-01-door-entry.mp4 differ diff --git a/assets/videos/hero/clips/v2-shot-02-mud-on-carpet.mp4 b/assets/videos/hero/clips/v2-shot-02-mud-on-carpet.mp4 new file mode 100644 index 0000000..c99bd17 Binary files /dev/null and b/assets/videos/hero/clips/v2-shot-02-mud-on-carpet.mp4 differ diff --git a/assets/videos/hero/clips/v2-shot-03-stain-on-chair.mp4 b/assets/videos/hero/clips/v2-shot-03-stain-on-chair.mp4 new file mode 100644 index 0000000..c7509c0 Binary files /dev/null and b/assets/videos/hero/clips/v2-shot-03-stain-on-chair.mp4 differ diff --git a/assets/videos/hero/clips/v2-shot-04-extraction-carpet.mp4 b/assets/videos/hero/clips/v2-shot-04-extraction-carpet.mp4 new file mode 100644 index 0000000..89699c1 Binary files /dev/null and b/assets/videos/hero/clips/v2-shot-04-extraction-carpet.mp4 differ diff --git a/assets/videos/hero/clips/v2-shot-05-clean-stairs.mp4 b/assets/videos/hero/clips/v2-shot-05-clean-stairs.mp4 new file mode 100644 index 0000000..093caf4 Binary files /dev/null and b/assets/videos/hero/clips/v2-shot-05-clean-stairs.mp4 differ diff --git a/assets/videos/hero/clips/v2-shot-06-office.mp4 b/assets/videos/hero/clips/v2-shot-06-office.mp4 new file mode 100644 index 0000000..68991d1 Binary files /dev/null and b/assets/videos/hero/clips/v2-shot-06-office.mp4 differ diff --git a/assets/videos/hero/clips/v2-shot-07-restaurant.mp4 b/assets/videos/hero/clips/v2-shot-07-restaurant.mp4 new file mode 100644 index 0000000..48ff393 Binary files /dev/null and b/assets/videos/hero/clips/v2-shot-07-restaurant.mp4 differ diff --git a/assets/videos/hero/clips/v3-shot-01.mp4 b/assets/videos/hero/clips/v3-shot-01.mp4 new file mode 100644 index 0000000..4c39bad Binary files /dev/null and b/assets/videos/hero/clips/v3-shot-01.mp4 differ diff --git a/assets/videos/hero/clips/v3-shot-02.mp4 b/assets/videos/hero/clips/v3-shot-02.mp4 new file mode 100644 index 0000000..4f23eb6 Binary files /dev/null and b/assets/videos/hero/clips/v3-shot-02.mp4 differ diff --git a/assets/videos/hero/clips/v3-shot-03.mp4 b/assets/videos/hero/clips/v3-shot-03.mp4 new file mode 100644 index 0000000..a7dac0c Binary files /dev/null and b/assets/videos/hero/clips/v3-shot-03.mp4 differ diff --git a/assets/videos/hero/clips/v3-shot-04.mp4 b/assets/videos/hero/clips/v3-shot-04.mp4 new file mode 100644 index 0000000..532213a Binary files /dev/null and b/assets/videos/hero/clips/v3-shot-04.mp4 differ diff --git a/assets/videos/hero/clips/v3-shot-05.mp4 b/assets/videos/hero/clips/v3-shot-05.mp4 new file mode 100644 index 0000000..31c5f26 Binary files /dev/null and b/assets/videos/hero/clips/v3-shot-05.mp4 differ diff --git a/assets/videos/hero/clips/v3-shot-06.mp4 b/assets/videos/hero/clips/v3-shot-06.mp4 new file mode 100644 index 0000000..112854f Binary files /dev/null and b/assets/videos/hero/clips/v3-shot-06.mp4 differ diff --git a/assets/videos/hero/clips/v3-shot-07.mp4 b/assets/videos/hero/clips/v3-shot-07.mp4 new file mode 100644 index 0000000..aa7e5c8 Binary files /dev/null and b/assets/videos/hero/clips/v3-shot-07.mp4 differ diff --git a/assets/videos/hero/hero-clean.mp4 b/assets/videos/hero/hero-clean.mp4 new file mode 100644 index 0000000..26db62a Binary files /dev/null and b/assets/videos/hero/hero-clean.mp4 differ diff --git a/assets/videos/hero/hero-main.mp4 b/assets/videos/hero/hero-main.mp4 new file mode 100644 index 0000000..2f2d87f Binary files /dev/null and b/assets/videos/hero/hero-main.mp4 differ diff --git a/assets/videos/hero/hero-reel.mp4 b/assets/videos/hero/hero-reel.mp4 new file mode 100644 index 0000000..d019724 Binary files /dev/null and b/assets/videos/hero/hero-reel.mp4 differ diff --git a/commercial/hotels-inns/index.html b/commercial/hotels-inns/index.html new file mode 100644 index 0000000..a85cc7b --- /dev/null +++ b/commercial/hotels-inns/index.html @@ -0,0 +1,92 @@ + + + + + + Hotel and Inn Carpet Cleaning | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Hotel and Inn Carpet Cleaning in the Finger Lakes

+

Guests write reviews based on what they see and smell. Worn, stained carpet is the detail that costs you stars.

+ Book Now +
+
+
+ +
+
+
+ +

Consistent Carpet Care That Keeps Your Property Competitive

+

Hospitality carpet absorbs daily punishment from luggage wheels, food spills, and heavy foot traffic. Without a consistent cleaning program, hallways and guest rooms develop an odor and appearance that no amount of fresh linens can overcome. Lahr Carpet Cleaning serves inns, hotels, and bed and breakfasts throughout the Finger Lakes region.

+
+
+
+
+

Guest Rooms

+

Room carpet carries odors and stains that surface cleaning cannot fix. We extract embedded soil and treat stains at the fiber level. Guests notice clean rooms and say so in reviews.

+
+
+
+

Hallways and Common Areas

+

Hallway carpet shows traffic lanes faster than any other area of a property. We restore pile and color so the path from the lobby to the room sets the right tone for every guest.

+
+
+
+

Review Score Protection

+

Cleanliness is one of the top factors guests cite in online reviews for lodging properties. A consistent cleaning program is one of the lowest-cost investments you can make to protect your rating and occupancy rate.

+
+
+
+

Scheduled Programs for Lodging Properties

+

We build cleaning schedules around your occupancy calendar. High season and low season have different demands. We accommodate both so your property is always ready for the next arrival.

+
+
+
+
+ +
+
+
+
+ +

We Understand the Pace of Wine Country Hospitality

+

Finger Lakes lodging properties face compressed busy seasons and demanding guests who compare properties online before they arrive. Your property needs to look its best during peak periods, not just between them. We work with innkeepers and property managers to build cleaning programs that keep up with the season.

+

Bed and breakfasts, small inns, and boutique hotels all receive the same professional extraction service we bring to larger commercial accounts. No property is too small to benefit from clean carpet.

+ Book Now +
+
+
+
+
+ +
+
+
+

Protect Your Property Rating With Clean Carpet

+

Book a cleaning program built around your occupancy schedule.

+ +
+
+
+ + + + + + diff --git a/commercial/offices/index.html b/commercial/offices/index.html new file mode 100644 index 0000000..82d7bb0 --- /dev/null +++ b/commercial/offices/index.html @@ -0,0 +1,92 @@ + + + + + + Office Carpet Cleaning Waterloo NY | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Office Carpet Cleaning in Waterloo, NY

+

Worn office carpet tells clients you do not care about the details. We clean after hours so your space looks sharp every morning.

+ Book Now +
+
+
+ +
+
+
+ +

Clean Offices Win Client Confidence Before You Speak

+

High-traffic office carpet shows wear faster than almost any other commercial surface. Matted pile, traffic lanes, and embedded soil build up over months and communicate neglect to everyone who walks in. Lahr Carpet Cleaning restores office carpet without costing you a single hour of business time.

+
+
+
+
+

After-Hours and Weekend Service

+

We work when your office is empty. Evening and weekend appointments mean your team arrives to clean carpet without any disruption to their day. No wet floors, no equipment in the way during business hours.

+
+
+
+

First Impressions for Clients

+

Clients notice the carpet before they notice the conference table. A clean, maintained workspace signals professionalism from the lobby to the back office. We keep your space ready for whoever walks through the door.

+
+
+
+

Healthier Air for Your Team

+

Office carpet traps allergens, dust, and bacteria at desk level all day long. Professional extraction removes what vacuuming leaves behind. Your team breathes better in a properly cleaned workspace.

+
+
+
+

Recurring Service Programs

+

One-time cleanings help. Scheduled programs keep your office looking professional year-round. We set up a cleaning cadence that fits your traffic volume and budget, then show up reliably each time.

+
+
+
+
+ +
+
+
+
+ +

No Disruption. No Excuses. Just Clean Carpet.

+

Office managers need vendors who show up on schedule, communicate clearly, and do not require hand-holding. We have built our commercial business in Waterloo and the surrounding area on exactly that kind of reliability.

+

We use truck-mounted hot water extraction that pulls deep soil out of office carpet rather than pushing it around. The result lasts longer and dries faster than portable equipment. Your office is ready for the morning crew, not still damp when they arrive.

+ Book Now +
+
+
+
+
+ +
+
+
+

Your Office Should Look As Professional As You Are

+

Book after-hours cleaning and give your clients the right first impression.

+ +
+
+
+ + + + + + diff --git a/commercial/property-management/index.html b/commercial/property-management/index.html new file mode 100644 index 0000000..c2c2e6b --- /dev/null +++ b/commercial/property-management/index.html @@ -0,0 +1,92 @@ + + + + + + Carpet Cleaning for Property Managers | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Carpet Cleaning for Property Managers in Upstate NY

+

Managing multiple units means you need carpet vendors who show up, do it right, and get out of your way.

+ Book Now +
+
+
+ +
+
+
+ +

One Reliable Vendor for Your Entire Portfolio

+

Tenant turnover is time-sensitive and expensive. Carpet that is not cleaned between tenants gets replaced sooner, deposits get disputed, and new tenants move in to a unit that does not feel fresh. Lahr Carpet Cleaning serves residential and commercial property portfolios across Seneca, Ontario, and Wayne counties with fast, consistent turn cleaning.

+
+
+
+
+

Multi-Unit Turn Cleaning

+

We handle back-to-back unit turnovers without the coordination headaches. Call us with your turnover schedule and we slot into your move-out and move-in window. No chasing vendors, no last-minute scrambles.

+
+
+
+

Consistent Vendor Relationship

+

Property managers need vendors who perform the same way every time. We document each job with condition notes so you have a record for deposit disputes or owner reporting. Consistent service protects your portfolio and your reputation.

+
+
+
+

Seneca, Ontario, and Wayne County Coverage

+

We serve properties across the three-county area surrounding Waterloo. Whether your portfolio is concentrated in one town or spread across multiple communities, we have the coverage to be your single carpet cleaning vendor.

+
+
+
+

Extend Carpet Life Between Replacements

+

Professional extraction removes the abrasive soil that cuts carpet fiber and causes premature wear. Regular cleaning between tenants stretches the replacement cycle and reduces capital expenses across your portfolio.

+
+
+
+
+ +
+
+
+
+ +

We Work at the Speed Your Turnovers Demand

+

A unit sitting empty costs money. We understand that turnover timelines are tight and that carpet cleaning cannot be the bottleneck that delays a new lease. Lahr Carpet Cleaning prioritizes commercial accounts with committed scheduling so your units move through the process on time.

+

We work with both residential and commercial property managers. From single-family rental homes to multi-unit apartment buildings and commercial office suites, we bring the same professional extraction process and reliable communication to every job in your portfolio.

+ Book Now +
+
+
+
+
+ +
+
+
+

Stop Chasing Carpet Vendors Between Tenants

+

Set up a portfolio relationship with one reliable local cleaner and move on.

+ +
+
+
+ + + + + + diff --git a/commercial/retail-showrooms/index.html b/commercial/retail-showrooms/index.html new file mode 100644 index 0000000..7db9f95 --- /dev/null +++ b/commercial/retail-showrooms/index.html @@ -0,0 +1,92 @@ + + + + + + Retail and Showroom Carpet Cleaning | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Retail and Showroom Carpet Cleaning in the Finger Lakes

+

Worn showroom carpet undermines your product before a customer reaches for it. We restore what foot traffic takes away.

+ Book Now +
+
+
+ +
+
+
+ +

Your Showroom Floor Is Part of Your Product Presentation

+

Shoppers and visitors form an impression of your business the moment they step inside. Matted, soiled carpet at the entry or along traffic paths signals that quality is not a priority here. Lahr Carpet Cleaning serves wineries, galleries, and retail businesses throughout the Finger Lakes with professional extraction that restores color and pile to working condition.

+
+
+
+
+

Entry and High-Traffic Zones

+

Entry carpet wears and soils faster than anywhere else in a retail space. We target these zones with deep extraction that removes embedded grit before it damages fiber. Your entry sets the tone for every visit.

+
+
+
+

Color and Pile Restoration

+

Professional extraction lifts matted pile and pulls out the soil that dulls carpet color over time. The result is carpet that looks closer to new without the cost of replacement. Most showroom carpet has more life in it than it appears.

+
+
+
+

Winery and Tasting Room Service

+

Wine spills are a fact of life in tasting rooms. We treat wine staining quickly and thoroughly using professional spotting agents designed for carpet fiber. Finger Lakes wineries trust us to keep their tasting rooms guest-ready.

+
+
+
+

After-Hours Availability

+

We clean when your doors are closed. Morning, evening, and weekend appointments mean your showroom opens clean every time with no disruption to customers or staff.

+
+
+
+
+ +
+
+
+
+ +

The Finger Lakes Market Expects a Premium Experience

+

Visitors to this region come to taste fine wine, browse galleries, and shop in spaces that feel curated. A poorly maintained floor undercuts everything else you have built. Retail and showroom clients in this market hold their venues to a higher standard because their customers do.

+

We serve businesses from Waterloo to Seneca Falls and throughout the wine country corridor. Our scheduling is flexible enough to work around tasting room hours, gallery events, and seasonal traffic spikes.

+ Book Now +
+
+
+
+
+ +
+
+
+

Let Your Showroom Floor Reflect the Quality You Sell

+

Book after-hours cleaning and give every customer the right first step.

+ +
+
+
+ + + + + + diff --git a/commercial/vacation-rentals/index.html b/commercial/vacation-rentals/index.html new file mode 100644 index 0000000..c7b38ab --- /dev/null +++ b/commercial/vacation-rentals/index.html @@ -0,0 +1,92 @@ + + + + + + Carpet Cleaning for Vacation Rentals | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Carpet Cleaning for Vacation Rentals in the Finger Lakes

+

One stained carpet can cost you a five-star review. We turn over your property fast so the next guests see it at its best.

+ Book Now +
+
+
+ +
+
+
+ +

Between-Guest Carpet Care That Protects Your Rating

+

Short-term guests bring wine, mud, and pet hair. Those stains go straight into your reviews if they are not handled before the next check-in. Lahr Carpet Cleaning serves Airbnb and VRBO hosts throughout the Finger Lakes wine country with fast, reliable turnaround cleaning.

+
+
+
+
+

Protect Your Review Score

+

Guests notice dirty carpets immediately and say so in reviews. A clean property commands higher nightly rates and repeat bookings. We remove the stains before the next guest walks through the door.

+
+
+
+

Fast Turnaround Available

+

Back-to-back bookings leave little time. We offer flexible scheduling around your check-out and check-in windows. Call us with your turnaround window and we will make it work.

+
+
+
+

Finger Lakes Wine Country Coverage

+

We serve rental properties along Seneca Lake, Cayuga Lake, and throughout the surrounding wine country. Local availability means shorter response times and reliable scheduling for your property calendar.

+
+
+
+

Pet and Stain Treatment

+

Pet-friendly rentals face the toughest carpet challenges. We treat pet odor and staining at the source using professional extraction, not surface masking. Guests and their noses will not know the difference.

+
+
+
+
+ +
+
+
+
+ +

A Cleaning Partner You Can Actually Count On

+

Managing a short-term rental from a distance is stressful enough. Your cleaning vendors need to show up on time and do the job right without you standing there. Lahr Carpet Cleaning has worked with Finger Lakes hosts long enough to understand what that reliability means.

+

We communicate clearly, arrive when scheduled, and send confirmation when the job is done. Set up a recurring program and take carpet cleaning off your mental list entirely.

+ Book Now +
+
+
+
+
+ +
+
+
+

Keep Your Rental Property Guest-Ready

+

Book between-guest carpet cleaning and protect the rating your property deserves.

+ +
+
+
+ + + + + + diff --git a/contact/index.html b/contact/index.html new file mode 100644 index 0000000..d949e7a --- /dev/null +++ b/contact/index.html @@ -0,0 +1,287 @@ + + + + + + Contact | Lahr Carpet Cleaning + + + + + + + +
+
+
+ +
+ +

Contact Lahr Carpet Cleaning

+

Your carpets should not have to wait. Call or fill out the form and we will get you scheduled.

+ +
+
+
+ Phone + 315-719-1218 +
+
+ +
+
+
+ Email + lahrcarpet@gmail.com +
+
+ +
+
+
+ Address + 1076 Waterloo/Geneva Road
Waterloo, NY
+
+
+ +
+
+
+ Service Area + Waterloo, Seneca Falls, Geneva, Canandaigua, Penn Yan and surrounding Finger Lakes communities +
+
+ +
+ + +
+
+ +
+

Book Your Cleaning

+

Fill out the form and we will be in touch to confirm your appointment.

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ +
+
+
+ + + + + + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4a69f9e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,6 @@ +services: + web: + build: . + ports: + - "8005:80" + restart: unless-stopped diff --git a/index.html b/index.html new file mode 100644 index 0000000..5609178 --- /dev/null +++ b/index.html @@ -0,0 +1,286 @@ + + + + + + Lahr Carpet Cleaning | Professional Carpet & Upholstery Cleaning in Waterloo, NY + + + + + + +
+ +
+ +
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Deeply
Clean
Carpets.

+

Professional carpet and upholstery cleaning for homes and businesses across Upstate New York.

+ +
+
+
+ +
+
+ +
+ + +
+
+
+ +

Home Cleaning Services

+

Professional cleaning for every surface in your home, from carpets and stairs to upholstery and hard floors.

+
+
+
+
Carpet Cleaning
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing to restore them.

+ Learn More +
+
+
Upholstery Cleaning
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue behind.

+ Learn More +
+
+
Floor Cleaning
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ + +
+
+
+ +

Business Cleaning Solutions

+

We keep offices, vacation rentals, hotels, and retail spaces clean without disrupting your operations.

+
+
+
+
Vacation Rentals
+

Vacation Rentals

+
Finger Lakes Rentals
+

Fast turnaround cleaning between guest stays. We keep your property ready so every arrival walks into a fresh space.

+ Learn More +
+
+
Office Spaces
+

Office Spaces

+
Professional Environments
+

Clean office carpets tell your clients you care about the details. We schedule around your business hours.

+ Learn More +
+
+
Hotels and Inns
+

Hotels & Inns

+
Hospitality Cleaning
+

Guests notice clean carpet. We work overnight or between checkouts to keep every room and corridor looking sharp.

+ Learn More +
+
+
Retail and Showrooms
+

Retail & Showrooms

+
Customer-Facing Spaces
+

Your showroom floor is what customers see first. High-traffic carpet and hard-surface cleaning that maintains your image.

+ Learn More +
+
+
Property Management
+

Property Management

+
Multi-Unit Properties
+

Managing multiple units means coordinating multiple vendors. We handle carpet and floor cleaning across your entire portfolio.

+ Learn More +
+
+
Commercial Overview
+

Commercial Overview

+
All Business Types
+

See everything we offer for commercial clients or contact us to discuss your specific property and schedule.

+ Learn More +
+
+
+
+ +
+
+

Book Your Cleaning Today!

+

Get a free estimate for your home or business. We serve Waterloo, Geneva, and the Finger Lakes region.

+ Book Now +
+
+ +
+
+

Reviews

+ +
+
+
+ +
+
+
+ +
+
+ +

Why Choose Us

+

We show up on time, use professional truck-mount equipment, and leave your carpets dry in hours. We work in Waterloo, Geneva, Seneca Falls, and the surrounding Finger Lakes area. Every job is done by an experienced technician who knows how to handle different fabrics without causing damage. You will see the difference the same day we finish.

+
+
+
+
+
+
+

Experienced Professionals

+

Years of hands-on experience with all types of carpets, rugs, and upholstery fabrics.

+
+
+
+

Eco-Friendly Products

+

Safe for children, pets, and the environment. No harsh chemicals left behind.

+
+
+
+

Fast Drying Times

+

Advanced equipment means your carpets dry in hours, not days.

+
+
+
+
+ +
+
+
+
+ +

Ready to Refresh Your Home?

+

Contact us today for a free estimate. We serve Waterloo, Geneva, and the surrounding Finger Lakes area.

+
+
+ + 315-719-1218 +
+ +
+ + 1076 Waterloo/Geneva Road, Waterloo, NY +
+
+ +
+
+
+

Request a Free Quote

+ + + + + + + +
+
+
+
+
+ + + + + + + + diff --git a/infra/nginx.conf b/infra/nginx.conf new file mode 100644 index 0000000..bb8bc79 --- /dev/null +++ b/infra/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name lahrcarpetcleaning.com www.lahrcarpetcleaning.com; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ $uri/index.html =404; + } + + error_page 404 /404.html; + error_page 500 502 503 504 /500.html; + + location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf)$ { + expires 30d; + add_header Cache-Control "public, immutable"; + } + + location ~ /\. { + deny all; + } + + location ~* \.(env|Dockerfile|dockerignore|yml|yaml|md|planning)$ { + deny all; + } +} diff --git a/locations/canandaigua-ny/index.html b/locations/canandaigua-ny/index.html new file mode 100644 index 0000000..558e7b1 --- /dev/null +++ b/locations/canandaigua-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Canandaigua, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Canandaigua,
NY

+

Lakefront homes, rentals, and businesses along Canandaigua Lake.

+ +
+
+
+ +
+
+
+ +

Services in Canandaigua

+

We serve Canandaigua and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Canandaigua, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Canandaigua, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Canandaigua, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Canandaigua, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Canandaigua, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Canandaigua, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Canandaigua

+

Call 315-719-1218 or submit the form for a free estimate in Canandaigua, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/clifton-springs-ny/index.html b/locations/clifton-springs-ny/index.html new file mode 100644 index 0000000..13dddd5 --- /dev/null +++ b/locations/clifton-springs-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Clifton Springs, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Clifton Springs,
NY

+

Residential and commercial cleaning throughout Clifton Springs.

+ +
+
+
+ +
+
+
+ +

Services in Clifton Springs

+

We serve Clifton Springs and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Clifton Springs, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Clifton Springs, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Clifton Springs, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Clifton Springs, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Clifton Springs, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Clifton Springs, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Clifton Springs

+

Call 315-719-1218 or submit the form for a free estimate in Clifton Springs, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/clyde-ny/index.html b/locations/clyde-ny/index.html new file mode 100644 index 0000000..c42d638 --- /dev/null +++ b/locations/clyde-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Clyde, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Wayne County — Finger Lakes +

Clyde,
NY

+

Carpet, upholstery, and floor cleaning for homes and businesses in Clyde.

+ +
+
+
+ +
+
+
+ +

Services in Clyde

+

We serve Clyde and the surrounding Wayne County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Clyde, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Clyde, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Clyde, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Clyde, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Clyde, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Clyde, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Clyde

+

Call 315-719-1218 or submit the form for a free estimate in Clyde, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/east-bloomfield-ny/index.html b/locations/east-bloomfield-ny/index.html new file mode 100644 index 0000000..f5a8aa6 --- /dev/null +++ b/locations/east-bloomfield-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in East Bloomfield, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

East Bloomfield,
NY

+

Serving homes and properties in East Bloomfield and surrounding areas.

+ +
+
+
+ +
+
+
+ +

Services in East Bloomfield

+

We serve East Bloomfield and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in East Bloomfield, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in East Bloomfield, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in East Bloomfield, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in East Bloomfield, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in East Bloomfield, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in East Bloomfield, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving East Bloomfield

+

Call 315-719-1218 or submit the form for a free estimate in East Bloomfield, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/farmington-ny/index.html b/locations/farmington-ny/index.html new file mode 100644 index 0000000..1d10123 --- /dev/null +++ b/locations/farmington-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Farmington, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Farmington,
NY

+

Residential and commercial carpet cleaning throughout Farmington.

+ +
+
+
+ +
+
+
+ +

Services in Farmington

+

We serve Farmington and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Farmington, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Farmington, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Farmington, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Farmington, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Farmington, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Farmington, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Farmington

+

Call 315-719-1218 or submit the form for a free estimate in Farmington, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/finger-lakes-ny/index.html b/locations/finger-lakes-ny/index.html new file mode 100644 index 0000000..c849e92 --- /dev/null +++ b/locations/finger-lakes-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Finger Lakes, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Region — Finger Lakes +

Finger Lakes,
NY

+

Serving vacation rentals, wineries, and homes across the Finger Lakes region.

+ +
+
+
+ +
+
+
+ +

Services in Finger Lakes

+

We serve Finger Lakes and the surrounding Region communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Finger Lakes, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Finger Lakes, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Finger Lakes, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Finger Lakes, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Finger Lakes, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Finger Lakes, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Finger Lakes

+

Call 315-719-1218 or submit the form for a free estimate in Finger Lakes, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/geneva-ny/index.html b/locations/geneva-ny/index.html new file mode 100644 index 0000000..8523a62 --- /dev/null +++ b/locations/geneva-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Geneva, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Geneva,
NY

+

Full residential and commercial services throughout Geneva.

+ +
+
+
+ +
+
+
+ +

Services in Geneva

+

We serve Geneva and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Geneva, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Geneva, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Geneva, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Geneva, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Geneva, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Geneva, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Geneva

+

Call 315-719-1218 or submit the form for a free estimate in Geneva, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/gorham-ny/index.html b/locations/gorham-ny/index.html new file mode 100644 index 0000000..9f3e0fc --- /dev/null +++ b/locations/gorham-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Gorham, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Gorham,
NY

+

Carpet and floor cleaning for homes and properties in Gorham.

+ +
+
+
+ +
+
+
+ +

Services in Gorham

+

We serve Gorham and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Gorham, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Gorham, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Gorham, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Gorham, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Gorham, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Gorham, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Gorham

+

Call 315-719-1218 or submit the form for a free estimate in Gorham, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/himrod-ny/index.html b/locations/himrod-ny/index.html new file mode 100644 index 0000000..21aa628 --- /dev/null +++ b/locations/himrod-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Himrod, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Yates County — Finger Lakes +

Himrod,
NY

+

Carpet and floor cleaning for homes and rentals in the Himrod area.

+ +
+
+
+ +
+
+
+ +

Services in Himrod

+

We serve Himrod and the surrounding Yates County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Himrod, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Himrod, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Himrod, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Himrod, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Himrod, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Himrod, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Himrod

+

Call 315-719-1218 or submit the form for a free estimate in Himrod, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/index.html b/locations/index.html new file mode 100644 index 0000000..23fc016 --- /dev/null +++ b/locations/index.html @@ -0,0 +1,201 @@ + + + + + + Service Areas | Lahr Carpet Cleaning | Finger Lakes, NY + + + + + + + +
+
+
+ Finger Lakes Region +

Service
Areas

+

We clean carpets, upholstery, rugs, and hard floors across 21 cities in Upstate New York. Select your city below.

+ +
+
+
+ +
+
+
+ +

Cities We Serve

+

Based in Waterloo, NY. We travel throughout Seneca, Ontario, Yates, Wayne, and Cayuga counties.

+
+
+
+
Waterloo NY
+

Waterloo, NY

+
Seneca County
+

Our home base. Fastest response times in the area.

+ View Services +
+
+
Geneva NY
+

Geneva, NY

+
Ontario County
+

Full residential and commercial services throughout Geneva.

+ View Services +
+
+
Seneca Falls NY
+

Seneca Falls, NY

+
Seneca County
+

Serving homes, vacation rentals, and businesses in Seneca Falls.

+ View Services +
+
+
Canandaigua NY
+

Canandaigua, NY

+
Ontario County
+

Lakefront homes, rentals, and businesses along Canandaigua Lake.

+ View Services +
+
+
Penn Yan NY
+

Penn Yan, NY

+
Yates County
+

Homes, wineries, and short-term rentals in the Penn Yan area.

+ View Services +
+
+
Newark NY
+

Newark, NY

+
Wayne County
+

Carpet and upholstery cleaning for homes and businesses in Newark.

+ View Services +
+
+
Clifton Springs NY
+

Clifton Springs, NY

+
Ontario County
+

Residential and commercial cleaning throughout Clifton Springs.

+ View Services +
+
+
Lodi NY
+

Lodi, NY

+
Seneca County
+

Serving homes and vacation properties in Lodi and surrounding areas.

+ View Services +
+
+
Himrod NY
+

Himrod, NY

+
Yates County
+

Carpet and floor cleaning for homes and rentals in the Himrod area.

+ View Services +
+
+
Phelps NY
+

Phelps, NY

+
Ontario County
+

Residential carpet and upholstery cleaning throughout Phelps.

+ View Services +
+
+
Shortsville NY
+

Shortsville, NY

+
Ontario County
+

Home and business cleaning services in Shortsville, NY.

+ View Services +
+
+
Victor NY
+

Victor, NY

+
Ontario County
+

Residential and commercial carpet cleaning throughout Victor.

+ View Services +
+
+
Naples NY
+

Naples, NY

+
Ontario County
+

Serving homes, wineries, and vacation rentals in the Naples area.

+ View Services +
+
+
Gorham NY
+

Gorham, NY

+
Ontario County
+

Carpet and floor cleaning for homes and properties in Gorham.

+ View Services +
+
+
Manchester NY
+

Manchester, NY

+
Ontario County
+

Residential and commercial cleaning services in Manchester, NY.

+ View Services +
+
+
Ovid NY
+

Ovid, NY

+
Seneca County
+

Serving homes and rental properties throughout Ovid.

+ View Services +
+
+
Clyde NY
+

Clyde, NY

+
Wayne County
+

Carpet, upholstery, and floor cleaning for homes and businesses in Clyde.

+ View Services +
+
+
Farmington NY
+

Farmington, NY

+
Ontario County
+

Residential and commercial carpet cleaning throughout Farmington.

+ View Services +
+
+
East Bloomfield NY
+

East Bloomfield, NY

+
Ontario County
+

Serving homes and properties in East Bloomfield and surrounding areas.

+ View Services +
+
+
Rushville NY
+

Rushville, NY

+
Yates County
+

Carpet and upholstery cleaning for homes and rentals in Rushville.

+ View Services +
+
+
Finger Lakes NY
+

Finger Lakes, NY

+
Region
+

Serving vacation rentals, wineries, and homes across the Finger Lakes region.

+ View Services +
+
+
+
+ +
+
+

Not sure if we cover your area?

+

Call 315-719-1218 or submit the form and we will confirm availability for your address.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/lodi-ny/index.html b/locations/lodi-ny/index.html new file mode 100644 index 0000000..cd0594b --- /dev/null +++ b/locations/lodi-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Lodi, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Seneca County — Finger Lakes +

Lodi,
NY

+

Serving homes and vacation properties in Lodi and surrounding areas.

+ +
+
+
+ +
+
+
+ +

Services in Lodi

+

We serve Lodi and the surrounding Seneca County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Lodi, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Lodi, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Lodi, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Lodi, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Lodi, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Lodi, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Lodi

+

Call 315-719-1218 or submit the form for a free estimate in Lodi, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/manchester-ny/index.html b/locations/manchester-ny/index.html new file mode 100644 index 0000000..ed3fac7 --- /dev/null +++ b/locations/manchester-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Manchester, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Manchester,
NY

+

Residential and commercial cleaning services in Manchester, NY.

+ +
+
+
+ +
+
+
+ +

Services in Manchester

+

We serve Manchester and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Manchester, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Manchester, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Manchester, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Manchester, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Manchester, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Manchester, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Manchester

+

Call 315-719-1218 or submit the form for a free estimate in Manchester, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/naples-ny/index.html b/locations/naples-ny/index.html new file mode 100644 index 0000000..84680b9 --- /dev/null +++ b/locations/naples-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Naples, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Naples,
NY

+

Serving homes, wineries, and vacation rentals in the Naples area.

+ +
+
+
+ +
+
+
+ +

Services in Naples

+

We serve Naples and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Naples, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Naples, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Naples, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Naples, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Naples, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Naples, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Naples

+

Call 315-719-1218 or submit the form for a free estimate in Naples, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/newark-ny/index.html b/locations/newark-ny/index.html new file mode 100644 index 0000000..9e1db59 --- /dev/null +++ b/locations/newark-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Newark, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Wayne County — Finger Lakes +

Newark,
NY

+

Carpet and upholstery cleaning for homes and businesses in Newark.

+ +
+
+
+ +
+
+
+ +

Services in Newark

+

We serve Newark and the surrounding Wayne County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Newark, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Newark, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Newark, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Newark, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Newark, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Newark, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Newark

+

Call 315-719-1218 or submit the form for a free estimate in Newark, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/ovid-ny/index.html b/locations/ovid-ny/index.html new file mode 100644 index 0000000..5274e06 --- /dev/null +++ b/locations/ovid-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Ovid, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Seneca County — Finger Lakes +

Ovid,
NY

+

Serving homes and rental properties throughout Ovid.

+ +
+
+
+ +
+
+
+ +

Services in Ovid

+

We serve Ovid and the surrounding Seneca County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Ovid, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Ovid, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Ovid, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Ovid, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Ovid, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Ovid, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Ovid

+

Call 315-719-1218 or submit the form for a free estimate in Ovid, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/penn-yan-ny/index.html b/locations/penn-yan-ny/index.html new file mode 100644 index 0000000..a13e788 --- /dev/null +++ b/locations/penn-yan-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Penn Yan, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Yates County — Finger Lakes +

Penn Yan,
NY

+

Homes, wineries, and short-term rentals in the Penn Yan area.

+ +
+
+
+ +
+
+
+ +

Services in Penn Yan

+

We serve Penn Yan and the surrounding Yates County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Penn Yan, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Penn Yan, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Penn Yan, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Penn Yan, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Penn Yan, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Penn Yan, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Penn Yan

+

Call 315-719-1218 or submit the form for a free estimate in Penn Yan, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/phelps-ny/index.html b/locations/phelps-ny/index.html new file mode 100644 index 0000000..065ef67 --- /dev/null +++ b/locations/phelps-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Phelps, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Phelps,
NY

+

Residential carpet and upholstery cleaning throughout Phelps.

+ +
+
+
+ +
+
+
+ +

Services in Phelps

+

We serve Phelps and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Phelps, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Phelps, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Phelps, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Phelps, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Phelps, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Phelps, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Phelps

+

Call 315-719-1218 or submit the form for a free estimate in Phelps, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/rushville-ny/index.html b/locations/rushville-ny/index.html new file mode 100644 index 0000000..0e0b307 --- /dev/null +++ b/locations/rushville-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Rushville, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Yates County — Finger Lakes +

Rushville,
NY

+

Carpet and upholstery cleaning for homes and rentals in Rushville.

+ +
+
+
+ +
+
+
+ +

Services in Rushville

+

We serve Rushville and the surrounding Yates County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Rushville, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Rushville, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Rushville, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Rushville, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Rushville, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Rushville, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Rushville

+

Call 315-719-1218 or submit the form for a free estimate in Rushville, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/seneca-falls-ny/index.html b/locations/seneca-falls-ny/index.html new file mode 100644 index 0000000..2ce971e --- /dev/null +++ b/locations/seneca-falls-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Seneca Falls, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Seneca County — Finger Lakes +

Seneca Falls,
NY

+

Serving homes, vacation rentals, and businesses in Seneca Falls.

+ +
+
+
+ +
+
+
+ +

Services in Seneca Falls

+

We serve Seneca Falls and the surrounding Seneca County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Seneca Falls, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Seneca Falls, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Seneca Falls, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Seneca Falls, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Seneca Falls, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Seneca Falls, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Seneca Falls

+

Call 315-719-1218 or submit the form for a free estimate in Seneca Falls, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/shortsville-ny/index.html b/locations/shortsville-ny/index.html new file mode 100644 index 0000000..45d3411 --- /dev/null +++ b/locations/shortsville-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Shortsville, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Shortsville,
NY

+

Home and business cleaning services in Shortsville, NY.

+ +
+
+
+ +
+
+
+ +

Services in Shortsville

+

We serve Shortsville and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Shortsville, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Shortsville, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Shortsville, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Shortsville, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Shortsville, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Shortsville, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Shortsville

+

Call 315-719-1218 or submit the form for a free estimate in Shortsville, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/victor-ny/index.html b/locations/victor-ny/index.html new file mode 100644 index 0000000..0221aee --- /dev/null +++ b/locations/victor-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Victor, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Ontario County — Finger Lakes +

Victor,
NY

+

Residential and commercial carpet cleaning throughout Victor.

+ +
+
+
+ +
+
+
+ +

Services in Victor

+

We serve Victor and the surrounding Ontario County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Victor, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Victor, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Victor, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Victor, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Victor, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Victor, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Victor

+

Call 315-719-1218 or submit the form for a free estimate in Victor, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/locations/waterloo-ny/index.html b/locations/waterloo-ny/index.html new file mode 100644 index 0000000..55f5894 --- /dev/null +++ b/locations/waterloo-ny/index.html @@ -0,0 +1,96 @@ + + + + + + Carpet Cleaning in Waterloo, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Seneca County — Finger Lakes +

Waterloo,
NY

+

Our home base. Fastest response times in the area.

+ +
+
+
+ +
+
+
+ +

Services in Waterloo

+

We serve Waterloo and the surrounding Seneca County communities. Call to confirm availability for your address.

+
+
+
+
Carpet Cleaning in Waterloo, NY
+

Carpet Cleaning

+
In-Home Service
+

Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home.

+ Learn More +
+
+
Stairs Cleaning in Waterloo, NY
+

Stairs Cleaning

+
Step by Step
+

Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing.

+ Learn More +
+
+
Upholstery Cleaning in Waterloo, NY
+

Upholstery Cleaning

+
Furniture Refresh
+

Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue.

+ Learn More +
+
+
Floor Cleaning in Waterloo, NY
+

Floor Cleaning

+
Hard Surface Care
+

Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition.

+ Learn More +
+
+
Area Rug Cleaning in Waterloo, NY
+

Area Rug Cleaning

+
Delicate Care
+

Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt.

+ Learn More +
+
+
Add-On Services in Waterloo, NY
+

Add-On Services

+
Extra Care
+

Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service.

+ Learn More +
+
+
+
+ +
+
+

Serving Waterloo

+

Call 315-719-1218 or submit the form for a free estimate in Waterloo, NY.

+ Get a Free Estimate +
+
+ + + + + + diff --git a/our-work/index.html b/our-work/index.html new file mode 100644 index 0000000..0b368f4 --- /dev/null +++ b/our-work/index.html @@ -0,0 +1,158 @@ + + + + + + Our Work | Lahr Carpet Cleaning + + + + + + + +
+
+
+ Results from the Finger Lakes Region +

Our Work

+

Real cleaning jobs. Real results. See what professional carpet care looks like.

+ Book Now +
+
+
+ +
+
+ +

The Results Speak for Themselves

+

We clean carpets, stairs, upholstery, area rugs, hard floors, and commercial spaces throughout the Finger Lakes region. Every job gets the same equipment and the same standard of care. The types of results below reflect our everyday work for homeowners and businesses in Waterloo, Geneva, Seneca Falls, and surrounding communities.

+
+
+ +
+
+
+ +

What We Achieve on Every Job

+

From pet-stained carpet to commercial office floors, our equipment reaches what household cleaning cannot.

+
+
+
+
+

Carpet Cleaning

+

We remove deep-seated dirt, allergens, and stains from carpet fibers using hot water extraction. High-traffic areas and heavily soiled rooms get pre-treatment before the main clean.

+
+
+
+

Upholstery Cleaning

+

Sofas, chairs, and mattresses hold more than you expect. Our upholstery cleaning restores color and removes odors without damaging fabric.

+
+
+
+

Stair Cleaning

+

Stairs are the most heavily trafficked surface in any home. We clean every tread and riser with the same thoroughness we bring to open floor areas.

+
+
+
+

Commercial Spaces

+

We work in offices, rental properties, and retail spaces. Commercial cleaning is scheduled around your business hours to minimize disruption.

+
+
+
+
+ + + +
+
+

Want Results Like These?

+

Schedule your cleaning today. We serve the entire Finger Lakes region. Free estimates included.

+ Book Now +
+
+ + + + + + diff --git a/reviews/index.html b/reviews/index.html new file mode 100644 index 0000000..8a26d6d --- /dev/null +++ b/reviews/index.html @@ -0,0 +1,100 @@ + + + + + + Customer Reviews | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, NY — Finger Lakes Region +

What Our Customers Say

+

Real reviews from your neighbors throughout the Finger Lakes.

+ Book Now +
+
+
+ +
+
+ +

Reviews from the Finger Lakes Community

+

We let our work speak for itself. Homeowners and businesses across Waterloo, Seneca Falls, Geneva, and the surrounding region have shared their experiences online. Read what your neighbors are saying and see why they keep calling Lahr Carpet Cleaning back.

+
+
+ + + +
+
+
+ +

Built on Word of Mouth

+

Most of our business comes from referrals and repeat customers. That tells us something important.

+
+
+
+ Clean carpet results after professional cleaning +
+
+

When your carpets and upholstery look genuinely clean, people notice. They ask who you used. That is how Lahr Carpet Cleaning grows in this region.

+

We do not rely on gimmicks or pressure tactics. We show up, do the job properly, and let the results do the talking. That approach has earned us a loyal customer base throughout Seneca, Ontario, Schuyler, and Yates counties.

+

If you have had us in your home and want to share your experience, we would be grateful for a review on Google or Facebook. It helps other homeowners in the Finger Lakes find reliable carpet care.

+
+ Leave a Google Review + Leave a Facebook Review +
+
+
+
+
+ +
+
+

Ready to Join Our Satisfied Customers?

+

Schedule your cleaning today and experience the Lahr difference firsthand.

+ Book Now +
+
+ + + + + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..1437b99 --- /dev/null +++ b/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://lahrcarpetcleaning.com/sitemap.xml diff --git a/service-area/index.html b/service-area/index.html new file mode 100644 index 0000000..11fb645 --- /dev/null +++ b/service-area/index.html @@ -0,0 +1,159 @@ + + + + + + Service Area | Lahr Carpet Cleaning + + + + + + +
+
+
+ Seneca, Ontario, Schuyler & Yates Counties +

Our Service Area

+

Professional carpet and upholstery cleaning throughout the Finger Lakes region.

+ Book Now +
+
+
+ +
+
+
+ +

Serving the Finger Lakes Region

+

We cover a wide area across Upstate New York. If your town is not listed below, call us and we will let you know.

+
+
+

Lahr Carpet Cleaning is based in Waterloo, NY. We travel to homes and businesses throughout the surrounding counties. Our service area spans four counties and dozens of communities across the Finger Lakes. We bring the same professional equipment and attention to detail to every address we visit.

+
+
+
+ +
+
+
+ +

Towns & Villages

+

Find your community below. We serve all of these areas and the rural routes between them.

+
+
+
+
+

Waterloo

+

Seneca County

+
+
+
+

Seneca Falls

+

Seneca County

+
+
+
+

Ovid

+

Seneca County

+
+
+
+

Romulus

+

Seneca County

+
+
+
+

Cayuga

+

Seneca County

+
+
+
+

Geneva

+

Ontario County

+
+
+
+

Canandaigua

+

Ontario County

+
+
+
+

Lodi

+

Schuyler County

+
+
+
+

Dundee

+

Schuyler County

+
+
+
+

Penn Yan

+

Yates County

+
+
+
+
+ +
+
+
+ +

Four-County Coverage

+

We serve homes and businesses in all four of these Finger Lakes counties.

+
+
+
+
+

Seneca County

+

Our home base. We serve every community in Seneca County with full residential and commercial services.

+
+
+
+

Ontario County

+

Covering Geneva, Canandaigua, and the communities along Seneca and Canandaigua lakes.

+
+
+
+

Schuyler County

+

Serving the towns and villages along the western Finger Lakes corridor, including Watkins Glen and surrounding areas.

+
+
+
+

Yates County

+

Reaching Penn Yan, Dundee, and the communities along Keuka Lake.

+
+
+
+
+ +
+
+ +

Don't See Your Town?

+

Our coverage area continues to grow. If your address is not listed above, give us a call. We will let you know whether we can reach you and when we have availability.

+
+ 315-719-1218 + Book Now +
+
+
+ +
+
+

Ready to Schedule Your Cleaning?

+

We serve your neighborhood. Free estimates available on every appointment.

+ Book Now +
+
+ + + + + + diff --git a/services/add-ons/index.html b/services/add-ons/index.html new file mode 100644 index 0000000..d3c36c7 --- /dev/null +++ b/services/add-ons/index.html @@ -0,0 +1,92 @@ + + + + + + Carpet Cleaning Add-On Services | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Carpet Cleaning Add-On Services

+

Standard cleaning handles most situations. Some homes need more. These optional treatments are available at booking and extend the results of your service.

+ Book Now +
+
+
+ +
+
+
+ +

Your Home May Need More Than a Standard Clean

+

Pet ownership, heavy foot traffic, and years of accumulation create problems that a basic extraction pass does not fully resolve. These add-ons target specific issues so you get the most out of your appointment.

+
+
+
+
+

Pet Odor Treatment

+

Pet urine odor persists because the source is in the padding and subfloor, not just the carpet surface. A targeted odor treatment neutralizes the compounds that cause the smell rather than masking them with fragrance. Available as an add-on at booking.

+
+
+
+

Fabric Protector Application

+

Fabric protector creates a barrier on carpet fibers that slows the absorption of spills. Liquid sits on the surface longer, giving you more time to blot it before it sets. Applied after cleaning when the carpet is freshest.

+
+
+
+

Deodorizer Treatment

+

General household odors accumulate in carpet over time without a single identifiable source. A deodorizer treatment applied after extraction neutralizes residual odors and leaves the room smelling clean. Available for any room in the home.

+
+
+
+

Heavily Soiled Area Treatment

+

High-traffic zones and areas that have not been professionally cleaned in years often need extra pre-treatment and additional extraction passes. This add-on covers the time and product required to bring those areas to the same standard as the rest of the carpet.

+
+
+
+
+ +
+
+
+
+ +

Tell Us What You Are Dealing With When You Book

+

When you contact us to schedule, describe the specific problems you want addressed. Pet odors, heavy soiling, or a request for protector are all things we factor into the job before arriving. Nothing is added without your knowledge.

+

These treatments are optional. Not every home needs them. We will tell you honestly at the time of booking whether an add-on makes sense for your situation.

+ Book Now +
+
+
+
+
+ +
+
+
+

Get the Full Treatment Your Home Needs

+

Serving Waterloo, Seneca Falls, and the Finger Lakes region.

+ +
+
+
+ + + + + + diff --git a/services/area-rugs/index.html b/services/area-rugs/index.html new file mode 100644 index 0000000..b147b16 --- /dev/null +++ b/services/area-rugs/index.html @@ -0,0 +1,92 @@ + + + + + + Area Rug Cleaning Waterloo NY | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Area Rug Cleaning in Waterloo, NY

+

Area rugs trap allergens, pet dander, and dust mites deep in the fibers. Gentle but thorough cleaning removes what regular vacuuming cannot reach.

+ Book Now +
+
+
+ +
+
+
+ +

The Dirt in Your Rug Goes Deeper Than You Think

+

Area rugs act as air filters for the room. Allergens, pet dander, and fine soil settle into the base of the pile where vacuuming barely reaches. Over time that buildup affects the air quality in the room and the feel of the rug underfoot.

+
+
+
+
+

Allergens and Dander Removed

+

Dust mites and pet dander accumulate in rug fibers over months of use. Vacuuming pulls out some surface debris but leaves the rest packed into the pile. Professional cleaning extracts what has settled deep in the fiber.

+
+
+
+

Safe for Wool and Delicate Fibers

+

Wool rugs, natural fiber rugs, and hand-woven pieces require a different approach than synthetic broadloom. We identify the fiber type before cleaning begins. The process is matched to the rug, not the other way around.

+
+
+
+

Synthetic Rugs Cleaned Thoroughly

+

Machine-made synthetic rugs tolerate a more thorough extraction process. They collect just as much debris as natural fiber rugs. A complete clean restores the color and softens the pile.

+
+
+
+

Stains and Odors Addressed

+

Pet accidents and spills on area rugs soak through to the backing and the floor below. Pre-treatment targets those areas before the extraction pass. Most stains and odors respond well to professional treatment.

+
+
+
+
+ +
+
+
+
+ +

Wool, Synthetic, and Delicate Rugs Welcome

+

We clean area rugs of all common fiber types including wool, polypropylene, nylon, cotton, and blended synthetics. Each rug is inspected for fiber type and condition before cleaning begins. Delicate or hand-woven pieces receive a gentler approach.

+

If your rug smells, looks flat, or has visible staining, professional cleaning will make a clear difference. A rug that looks too far gone often surprises its owner after a thorough clean.

+ Book Now +
+
+
+
+
+ +
+
+
+

Give Your Rugs a Real Clean

+

Serving Waterloo, Seneca Falls, and the Finger Lakes region.

+ +
+
+
+ + + + + + diff --git a/services/carpet-cleaning/index.html b/services/carpet-cleaning/index.html new file mode 100644 index 0000000..234aede --- /dev/null +++ b/services/carpet-cleaning/index.html @@ -0,0 +1,92 @@ + + + + + + Residential Carpet Cleaning in Waterloo NY | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Residential Carpet Cleaning in Waterloo, NY

+

Your carpets hold more dirt than you can see. Hot water extraction pulls out what vacuuming leaves behind.

+ Book Now +
+
+
+ +
+
+
+ +

Clean Carpets Start With the Right Process

+

Vacuuming removes surface debris. Ground-in dirt, pet stains, and allergens require professional hot water extraction to actually leave your carpets clean.

+
+
+
+
+

Pet Stains Removed at the Source

+

Pet accidents soak through fibers and into the padding below. Surface cleaning masks the odor temporarily. Hot water extraction reaches the source and removes it completely.

+
+
+
+

Allergen Reduction for Your Home

+

Dust mites, pet dander, and pollen collect deep in carpet fibers. Every step kicks them back into the air you breathe. A thorough cleaning removes what accumulates over months of use.

+
+
+
+

Ground-In Dirt Lifted Out

+

Foot traffic grinds soil into the base of fibers where vacuums cannot reach. Over time this darkens traffic lanes and wears down the carpet. Professional extraction lifts embedded grit before it causes permanent damage.

+
+
+
+

Fast-Drying Results

+

Hot water extraction leaves carpets damp, not soaking wet. Most residential carpets are dry and walkable within a few hours. The result is clean fiber from the surface to the base.

+
+
+
+
+ +
+
+
+
+ +

Local Service You Can Count On

+

Lahr Carpet Cleaning serves Waterloo, Seneca Falls, and the surrounding Finger Lakes region. When you call, you reach the person doing the work. There are no subcontractors and no surprises.

+

Every job follows the same approach: assess the carpet, pre-treat problem areas, extract thoroughly, and leave the space cleaner than we found it. That consistency is what keeps customers coming back.

+ Book Now +
+
+
+
+
+ +
+
+
+

Ready for Truly Clean Carpets?

+

Serving Waterloo, Seneca Falls, and the Finger Lakes region.

+ +
+
+
+ + + + + + diff --git a/services/commercial/index.html b/services/commercial/index.html new file mode 100644 index 0000000..35e5e8b --- /dev/null +++ b/services/commercial/index.html @@ -0,0 +1,92 @@ + + + + + + Commercial Carpet Cleaning Waterloo NY | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Commercial Carpet Cleaning in Waterloo, NY

+

Your business floors see heavy traffic every day. We restore them without disrupting your operations.

+ Book Now +
+
+
+ +
+
+
+ +

Professional Cleaning Built Around Your Business

+

Dirty carpets send a message to clients and guests before a word is spoken. Lahr Carpet Cleaning serves offices, rental properties, and hospitality businesses across the Finger Lakes with scheduling that works around your hours.

+
+
+
+
+

Office Buildings

+

High-traffic office carpet wears and soils fast. We clean after hours or on weekends so your team arrives to fresh floors. No disruption during business hours.

+
+
+
+

Rental Properties

+

Tenant turnover leaves carpets looking rough. We work with property managers across Seneca, Ontario, and Wayne counties for consistent, timely cleaning between tenants.

+
+
+
+

Hospitality and Lodging

+

Guest rooms and hallways take daily abuse in any lodging property. Clean carpets protect your review scores and guest satisfaction. We serve inns, hotels, and bed and breakfasts throughout the Finger Lakes region.

+
+
+
+

Flexible Scheduling

+

We schedule around your operation, not ours. Early morning, evening, and weekend appointments are available. One call sets a recurring program so clean floors are never an afterthought.

+
+
+
+
+ +
+
+
+
+ +

Local Knowledge. Professional Results.

+

We have served the Waterloo and Finger Lakes commercial market for years. We understand the seasonal pace of this region and the demands placed on properties serving wine country visitors, year-round residents, and business clients alike.

+

Every job uses professional hot water extraction equipment. No rental-grade machines, no shortcuts. You get clean carpets the first time, with a before and after walk-through so you know exactly what was done.

+ Book Now +
+
+
+
+
+ +
+
+
+

Ready to Protect Your Business Image?

+

Call or book online and we will schedule around your hours.

+ +
+
+
+ + + + + + diff --git a/services/floors/index.html b/services/floors/index.html new file mode 100644 index 0000000..b9fe845 --- /dev/null +++ b/services/floors/index.html @@ -0,0 +1,92 @@ + + + + + + Floor Cleaning Services Waterloo NY | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Floor Cleaning Services in Waterloo, NY

+

Mopping pushes grime around grout lines without removing it. Professional agitation and extraction restores the color your floors used to have.

+ Book Now +
+
+
+ +
+
+
+ +

Grout Lines Hold the Grime Mopping Leaves Behind

+

Tile, grout, vinyl, and LVP all accumulate buildup that household mops cannot remove. Professional cleaning uses agitation and extraction together to pull embedded soil out of the surface rather than spreading it around.

+
+
+
+
+

Tile and Grout Restored

+

Grout is porous and absorbs dirt with every pass of a mop. Over time it darkens and discolors even if you clean regularly. Professional agitation and extraction pulls embedded grime out and restores the original grout color.

+
+
+
+

Vinyl and LVP Cleaned Safely

+

Vinyl plank and luxury vinyl plank floors require cleaning solutions that do not damage the wear layer or cause edge swelling. We use methods appropriate for LVP so the floor looks clean without being harmed in the process.

+
+
+
+

Grout Lines Get Individual Attention

+

The lines between tiles are where household cleaning fails most visibly. We work those channels directly rather than cleaning across the surface only. The difference in grout color after cleaning is often striking.

+
+
+
+

No Chemical Residue Left Behind

+

Cleaning solutions that stay on the floor attract new dirt faster. Extraction removes both the soil and the cleaning solution together. The floor stays cleaner longer because there is nothing left to hold onto new debris.

+
+
+
+
+ +
+
+
+
+ +

Professional Results on Multiple Hard Floor Types

+

We work on tile, ceramic, porcelain, vinyl, and LVP. Each surface type is assessed before cleaning begins to confirm the right approach. Grout sealing is available as an add-on after cleaning to help maintain the results.

+

If your floors look dull or your grout lines have darkened despite regular mopping, professional cleaning will show you the difference. Book the service and see what the floor actually looks like clean.

+ Book Now +
+
+
+
+
+ +
+
+
+

Restore Your Hard Floors

+

Serving Waterloo, Seneca Falls, and the Finger Lakes region.

+ +
+
+
+ + + + + + diff --git a/services/stairs/index.html b/services/stairs/index.html new file mode 100644 index 0000000..c4dd887 --- /dev/null +++ b/services/stairs/index.html @@ -0,0 +1,92 @@ + + + + + + Stair Carpet Cleaning Waterloo NY | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Stair Carpet Cleaning in Waterloo, NY

+

Stairs collect more dirt per square inch than any flat surface in your home. Compact equipment reaches every tread and riser for a dramatic before-and-after result.

+ Book Now +
+
+
+ +
+
+
+ +

Your Stairs Take More Abuse Than Any Other Surface

+

Every family member and pet travels those treads multiple times a day. Dirt and debris pack into the edges of each step where vacuuming cannot follow. Professional cleaning restores what daily use takes away.

+
+
+
+
+

Compact Equipment for Tight Spaces

+

Standard carpet cleaning wands do not work well on stairs. Dedicated stair tools reach every tread, riser, and edge corner. Nothing gets skipped because of the geometry.

+
+
+
+

High-Traffic Dirt Removed Thoroughly

+

Stairs see more concentrated foot traffic than any hallway or room. Ground-in grit damages fibers faster on stairs than anywhere else. Extraction cleaning stops that cycle of wear.

+
+
+
+

Stain Pre-Treatment on Every Step

+

Pet accidents, food spills, and tracked-in mud all show on stair carpet. Each tread is pre-treated before extraction begins. Stubborn stains get individual attention before the main pass.

+
+
+
+

Dramatic Before-and-After Contrast

+

The improvement on stairs is visible immediately after cleaning. Restored color and texture make the whole staircase look refreshed. The results are among the most noticeable of any surface we clean.

+
+
+
+
+ +
+
+
+
+ +

Every Step Gets Individual Attention

+

We work tread by tread from the top of the staircase down. Each step is vacuumed, pre-treated, and extracted before moving to the next. Risers and the edges where carpet meets the wall get cleaned too, not just the walking surface.

+

The process takes longer per square foot than open room cleaning, but the result justifies it. Stairs that looked permanently stained often clean up completely with this approach.

+ Book Now +
+
+
+
+
+ +
+
+
+

Clean Every Step in Your Home

+

Serving Waterloo, Seneca Falls, and the Finger Lakes region.

+ +
+
+
+ + + + + + diff --git a/services/upholstery/index.html b/services/upholstery/index.html new file mode 100644 index 0000000..d63e58d --- /dev/null +++ b/services/upholstery/index.html @@ -0,0 +1,92 @@ + + + + + + Upholstery Cleaning Waterloo NY | Lahr Carpet Cleaning + + + + + + +
+
+
+ Waterloo, Seneca Falls & the Finger Lakes +

Upholstery Cleaning in Waterloo, NY

+

Your sofa absorbs pet hair, body oils, and food odors every single day. Fabric-safe extraction removes what vacuuming and spot sprays leave behind.

+ Book Now +
+
+
+ +
+
+
+ +

Your Furniture Holds More Than You Realize

+

Sofas, chairs, and sectionals collect allergens, pet dander, and body oils deep in the fabric where surface cleaning cannot reach. Professional extraction cleans the fiber from the inside out.

+
+
+
+
+

Sofas, Chairs, and Sectionals

+

We clean all upholstered residential furniture including sectionals, loveseats, accent chairs, and ottomans. Each piece is assessed before cleaning begins. No two fabrics are treated exactly the same way.

+
+
+
+

Pet Hair and Dander Removed

+

Pet hair weaves into fabric fibers and resists normal vacuuming. Pet dander triggers allergies and accumulates invisibly over time. Extraction cleaning removes both from the upholstery where your family sits every day.

+
+
+
+

Food Odors Neutralized

+

Body oils and food residue break down inside fabric over time. The result is an odor that builds gradually and becomes impossible to ignore. Cleaning removes the source rather than covering it with fragrance.

+
+
+
+

Fabric-Safe Extraction Process

+

Not every fabric tolerates the same cleaning method. We identify the fabric type before selecting the approach. The goal is a thorough clean without damage to the weave or color.

+
+
+
+
+ +
+
+
+
+ +

Residential Upholstered Furniture of All Types

+

We service standard residential upholstered pieces: sofas, loveseats, sectionals, armchairs, recliners, and dining chairs with fabric seats. Each piece is vacuumed, spot pre-treated, and then extracted using equipment sized for furniture rather than floors.

+

If your furniture smells, looks dull, or shows visible soil on the armrests and seat cushions, cleaning will make a visible difference. Book the appointment and see the result for yourself.

+ Book Now +
+
+
+
+
+ +
+
+
+

Fresher Furniture Starts Here

+

Serving Waterloo, Seneca Falls, and the Finger Lakes region.

+ +
+
+
+ + + + + + diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..35ac054 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,13 @@ + + + https://lahrcarpetcleaning.com/1.0 + https://lahrcarpetcleaning.com/about/0.8 + https://lahrcarpetcleaning.com/contact/0.9 + https://lahrcarpetcleaning.com/services/carpet-cleaning/0.9 + https://lahrcarpetcleaning.com/services/stairs/0.8 + https://lahrcarpetcleaning.com/services/upholstery/0.8 + https://lahrcarpetcleaning.com/services/floors/0.8 + https://lahrcarpetcleaning.com/services/add-ons/0.7 + https://lahrcarpetcleaning.com/services/area-rugs/0.8 + https://lahrcarpetcleaning.com/services/commercial/0.8 + diff --git a/tools/__pycache__/build-paced-reel.cpython-313.pyc b/tools/__pycache__/build-paced-reel.cpython-313.pyc new file mode 100644 index 0000000..79d5484 Binary files /dev/null and b/tools/__pycache__/build-paced-reel.cpython-313.pyc differ diff --git a/tools/__pycache__/build-reel-server.cpython-313.pyc b/tools/__pycache__/build-reel-server.cpython-313.pyc new file mode 100644 index 0000000..7dd4cf6 Binary files /dev/null and b/tools/__pycache__/build-reel-server.cpython-313.pyc differ diff --git a/tools/__pycache__/gen-images-comfyui.cpython-313.pyc b/tools/__pycache__/gen-images-comfyui.cpython-313.pyc new file mode 100644 index 0000000..8e7d197 Binary files /dev/null and b/tools/__pycache__/gen-images-comfyui.cpython-313.pyc differ diff --git a/tools/__pycache__/gen-images.cpython-313.pyc b/tools/__pycache__/gen-images.cpython-313.pyc new file mode 100644 index 0000000..08c8fe8 Binary files /dev/null and b/tools/__pycache__/gen-images.cpython-313.pyc differ diff --git a/tools/__pycache__/gen-locations.cpython-313.pyc b/tools/__pycache__/gen-locations.cpython-313.pyc new file mode 100644 index 0000000..bfcaae2 Binary files /dev/null and b/tools/__pycache__/gen-locations.cpython-313.pyc differ diff --git a/tools/__pycache__/gen-missing-2.cpython-313.pyc b/tools/__pycache__/gen-missing-2.cpython-313.pyc new file mode 100644 index 0000000..44be8c2 Binary files /dev/null and b/tools/__pycache__/gen-missing-2.cpython-313.pyc differ diff --git a/tools/__pycache__/gen-service-images.cpython-313.pyc b/tools/__pycache__/gen-service-images.cpython-313.pyc new file mode 100644 index 0000000..423f70e Binary files /dev/null and b/tools/__pycache__/gen-service-images.cpython-313.pyc differ diff --git a/tools/__pycache__/gen-video.cpython-313.pyc b/tools/__pycache__/gen-video.cpython-313.pyc new file mode 100644 index 0000000..785ef37 Binary files /dev/null and b/tools/__pycache__/gen-video.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-3shots.cpython-313.pyc b/tools/__pycache__/regen-3shots.cpython-313.pyc new file mode 100644 index 0000000..dccf22c Binary files /dev/null and b/tools/__pycache__/regen-3shots.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-commercial-overview.cpython-313.pyc b/tools/__pycache__/regen-commercial-overview.cpython-313.pyc new file mode 100644 index 0000000..c89ae30 Binary files /dev/null and b/tools/__pycache__/regen-commercial-overview.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-full-reel.cpython-313.pyc b/tools/__pycache__/regen-full-reel.cpython-313.pyc new file mode 100644 index 0000000..07fd19a Binary files /dev/null and b/tools/__pycache__/regen-full-reel.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-images-targeted.cpython-313.pyc b/tools/__pycache__/regen-images-targeted.cpython-313.pyc new file mode 100644 index 0000000..9f93eee Binary files /dev/null and b/tools/__pycache__/regen-images-targeted.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-industrial.cpython-313.pyc b/tools/__pycache__/regen-industrial.cpython-313.pyc new file mode 100644 index 0000000..fb01125 Binary files /dev/null and b/tools/__pycache__/regen-industrial.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-shot.cpython-313.pyc b/tools/__pycache__/regen-shot.cpython-313.pyc new file mode 100644 index 0000000..fe56598 Binary files /dev/null and b/tools/__pycache__/regen-shot.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-shot02.cpython-313.pyc b/tools/__pycache__/regen-shot02.cpython-313.pyc new file mode 100644 index 0000000..16c1d39 Binary files /dev/null and b/tools/__pycache__/regen-shot02.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-shot04.cpython-313.pyc b/tools/__pycache__/regen-shot04.cpython-313.pyc new file mode 100644 index 0000000..a909bcf Binary files /dev/null and b/tools/__pycache__/regen-shot04.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-v3-shot04.cpython-313.pyc b/tools/__pycache__/regen-v3-shot04.cpython-313.pyc new file mode 100644 index 0000000..ce701ac Binary files /dev/null and b/tools/__pycache__/regen-v3-shot04.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-v3.cpython-313.pyc b/tools/__pycache__/regen-v3.cpython-313.pyc new file mode 100644 index 0000000..bbcf67e Binary files /dev/null and b/tools/__pycache__/regen-v3.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-v4.cpython-313.pyc b/tools/__pycache__/regen-v4.cpython-313.pyc new file mode 100644 index 0000000..74ca987 Binary files /dev/null and b/tools/__pycache__/regen-v4.cpython-313.pyc differ diff --git a/tools/__pycache__/regen-veo31.cpython-313.pyc b/tools/__pycache__/regen-veo31.cpython-313.pyc new file mode 100644 index 0000000..d714ea5 Binary files /dev/null and b/tools/__pycache__/regen-veo31.cpython-313.pyc differ diff --git a/tools/build-paced-reel.py b/tools/build-paced-reel.py new file mode 100644 index 0000000..92a33a3 --- /dev/null +++ b/tools/build-paced-reel.py @@ -0,0 +1,87 @@ +""" +Build paced hero reel from the browser-ordered clip list. +- Shot 1 (family entry): trimmed to 3s — cuts before mud pan +- Shots 2-7: full 6s (establish the story) +- Shots 8-12: trimmed to 3.5s (building pace) +- Shots 13-15: trimmed to 2.5s (rapid) +- Shot 16 (final reveal): full 6s (hold on clean result) +""" +import os, subprocess, shutil + +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +TMP_DIR = os.path.join(VID_DIR, "paced-tmp") +os.makedirs(TMP_DIR, exist_ok=True) + +# Clip order: wine spill > extraction > stain > technician, stairs later +CLIPS = [ + "v3-shot-02", # 1 - wine spill on sofa + "shot-05-extraction-couch",# 2 - extraction couch + "shot-03-technician", # 3 - technician + "v3-shot-03", # 4 - dirty stained carpet close-up + "v3-shot-06", # 5 - living room clean carpet pan + "v3-shot-07", # 6 - restaurant carpet glide + "v3-shot-05", # 7 - office lobby carpet pan + "v2-shot-05-clean-stairs", # 8 - clean bright staircase + "v2-shot-07-restaurant", # 9 - restaurant carpet + "v2-shot-06-office", # 10 - bright office carpet + "shot-01-wide-room", # 11 - wide room establishing + "shot-05-clean-reveal", # 12 - clean reveal + "shot-04-extraction-carpet",# 13 - final reveal +] + +# Duration for each clip +DURATIONS = [ + 4.5, # 1 wine spill — cut at 4.5s + 5.0, # 2 + 5.0, # 3 + 4.0, # 4 --- building pace --- + 4.0, # 5 + 4.0, # 6 + 3.5, # 7 + 3.5, # 8 + 2.5, # 9 --- rapid --- + 2.5, # 10 + 2.5, # 11 + 2.5, # 12 + 6.0, # 13 final reveal — hold +] + +paced_clips = [] +for i, (name, dur) in enumerate(zip(CLIPS, DURATIONS)): + src = os.path.join(VID_DIR, f"{name}.mp4") + out = os.path.join(TMP_DIR, f"{i:02d}-{name}.mp4") + if not os.path.exists(src): + print(f" MISSING: {src}") + continue + print(f"[{i+1:02d}] {name} → {dur}s") + result = subprocess.run( + ["ffmpeg", "-y", "-i", src, "-t", str(dur), + "-c:v", "libx264", "-crf", "22", "-preset", "fast", out], + capture_output=True, text=True + ) + if result.returncode != 0: + print(f" ffmpeg error: {result.stderr[-200:]}") + else: + paced_clips.append(out) + +print(f"\n{len(paced_clips)}/{len(CLIPS)} clips trimmed") + +concat_file = os.path.join(TMP_DIR, "concat.txt") +with open(concat_file, "w") as f: + for p in paced_clips: + f.write(f"file '{p}'\n") + +result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True +) +if result.returncode == 0: + print(f"Reel saved: {REEL_OUT} ({os.path.getsize(REEL_OUT)//1024}KB)") + # Clean up tmp + shutil.rmtree(TMP_DIR) + print("Done.") +else: + print(f"ffmpeg error: {result.stderr[-400:]}") diff --git a/tools/build-reel-server.py b/tools/build-reel-server.py new file mode 100644 index 0000000..5f8736e --- /dev/null +++ b/tools/build-reel-server.py @@ -0,0 +1,84 @@ +"""Local server: serves clip-browser.html + handles POST /build-reel to run ffmpeg.""" +import http.server, json, os, subprocess, urllib.parse + +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +TOOLS_DIR = os.path.dirname(__file__) + +class Handler(http.server.BaseHTTPRequestHandler): + def log_message(self, fmt, *args): pass + + def do_GET(self): + path = self.path.split('?')[0] + if path in ('/', '/tools/clip-browser.html'): + self._serve_file(os.path.join(TOOLS_DIR, 'clip-browser.html'), 'text/html') + elif path.startswith('/assets/videos/'): + rel = path.lstrip('/') + fpath = os.path.join(BASE_DIR, rel) + if os.path.exists(fpath): + self._serve_file(fpath, 'video/mp4') + else: + self._404() + else: + self._404() + + def do_POST(self): + if self.path == '/tools/build-reel-api.py': + length = int(self.headers.get('Content-Length', 0)) + body = json.loads(self.rfile.read(length)) + clips = body.get('clips', []) + if not clips: + self._json({'message': 'No clips provided.'}, 400) + return + concat_file = os.path.join(VID_DIR, 'concat-browser.txt') + missing = [] + with open(concat_file, 'w') as f: + for name in clips: + p = os.path.join(VID_DIR, f'{name}.mp4') + if not os.path.exists(p): + missing.append(name) + else: + f.write(f"file '{p}'\n") + if missing: + self._json({'message': f'Missing clips: {missing}'}, 400) + return + result = subprocess.run( + ['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', concat_file, + '-c:v', 'libx264', '-crf', '22', '-preset', 'fast', + '-movflags', '+faststart', REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + size = os.path.getsize(REEL_OUT) // 1024 + self._json({'message': f'Reel built: hero-reel.mp4 ({size}KB). Now rebuild Docker: docker compose up -d --build'}) + else: + self._json({'message': f'ffmpeg error: {result.stderr[-300:]}'}, 500) + else: + self._404() + + def _serve_file(self, path, ctype): + with open(path, 'rb') as f: + data = f.read() + self.send_response(200) + self.send_header('Content-Type', ctype) + self.send_header('Content-Length', len(data)) + self.end_headers() + self.wfile.write(data) + + def _json(self, obj, code=200): + data = json.dumps(obj).encode() + self.send_response(code) + self.send_header('Content-Type', 'application/json') + self.send_header('Content-Length', len(data)) + self.end_headers() + self.wfile.write(data) + + def _404(self): + self.send_response(404) + self.end_headers() + +if __name__ == '__main__': + port = 8088 + print(f'Clip browser running at http://localhost:{port}/') + http.server.HTTPServer(('', port), Handler).serve_forever() diff --git a/tools/clip-browser.html b/tools/clip-browser.html new file mode 100644 index 0000000..094b149 --- /dev/null +++ b/tools/clip-browser.html @@ -0,0 +1,230 @@ + + + + +Lahr Clip Browser + + + + +

Lahr Clip Browser

+

Drag clips to reorder. Click Preview to watch. Remove clips from reel. Build Reel when ready.

+ +
+
+
+
+

Available (not in reel)

+
+
+
+ +
+ + + + diff --git a/tools/gen-images-comfyui.py b/tools/gen-images-comfyui.py new file mode 100644 index 0000000..c9865bb --- /dev/null +++ b/tools/gen-images-comfyui.py @@ -0,0 +1,186 @@ +"""Generate replacement service images via ComfyUI SDXL (local, no API key needed).""" +import json, time, urllib.request, urllib.error, os, sys + +COMFY = "http://localhost:8188" +OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "services") +CKPT = "sd_xl_base_1.0.safetensors" + +IMAGES = [ + { + "filename": "vacation-rentals.jpg", + "positive": ( + "bright cozy vacation rental living room interior, clean beige carpet, " + "comfortable furniture, large windows with natural light, Finger Lakes " + "style decor, warm inviting atmosphere, no people, no equipment, " + "professional interior photography, ultra-realistic" + ), + "negative": ( + "people, person, human, worker, machine, vacuum, equipment, dirty, stain, " + "text, watermark, blurry, low quality, cartoon, dark" + ), + }, + { + "filename": "office-spaces.jpg", + "positive": ( + "modern corporate office interior, clean dark grey commercial carpet tiles, " + "open plan workspace, white desks, professional lighting, glass partitions, " + "no people, no equipment, architectural photography, ultra-realistic" + ), + "negative": ( + "people, person, human, worker, machine, vacuum, equipment, dirty, stain, " + "text, watermark, blurry, low quality, cartoon" + ), + }, + { + "filename": "hotels-inns.jpg", + "positive": ( + "elegant hotel corridor interior, clean patterned carpet runner, warm wall " + "sconce lighting, white walls, numbered room doors along hallway, " + "hospitality interior design, no people, no equipment, " + "professional photography, ultra-realistic" + ), + "negative": ( + "people, person, human, worker, machine, vacuum, equipment, dirty, stain, " + "text, watermark, blurry, low quality, cartoon" + ), + }, + { + "filename": "retail-showrooms.jpg", + "positive": ( + "upscale retail showroom interior, clean light grey carpet flooring, " + "modern display shelving, bright overhead track lighting, white walls, " + "customer-facing professional space, no people, no equipment, " + "architectural photography, ultra-realistic" + ), + "negative": ( + "people, person, human, worker, machine, vacuum, equipment, dirty, stain, " + "text, watermark, blurry, low quality, cartoon" + ), + }, + { + "filename": "property-management.jpg", + "positive": ( + "clean apartment unit interior, fresh beige carpet throughout living room, " + "neutral walls, bright windows, move-in ready condition, residential " + "property management style, no people, no furniture, no equipment, " + "real estate photography, ultra-realistic" + ), + "negative": ( + "people, person, human, worker, machine, vacuum, equipment, dirty, stain, " + "text, watermark, blurry, low quality, cartoon" + ), + }, +] + +def build_workflow(positive, negative, seed=None): + import random + if seed is None: + seed = random.randint(0, 2**32) + return { + "3": { + "class_type": "KSampler", + "inputs": { + "cfg": 7.0, + "denoise": 1.0, + "latent_image": ["5", 0], + "model": ["4", 0], + "negative": ["7", 0], + "positive": ["6", 0], + "sampler_name": "euler", + "scheduler": "normal", + "seed": seed, + "steps": 25, + }, + }, + "4": { + "class_type": "CheckpointLoaderSimple", + "inputs": {"ckpt_name": CKPT}, + }, + "5": { + "class_type": "EmptyLatentImage", + "inputs": {"batch_size": 1, "height": 768, "width": 1024}, + }, + "6": { + "class_type": "CLIPTextEncode", + "inputs": {"clip": ["4", 1], "text": positive}, + }, + "7": { + "class_type": "CLIPTextEncode", + "inputs": {"clip": ["4", 1], "text": negative}, + }, + "8": { + "class_type": "VAEDecode", + "inputs": {"samples": ["3", 0], "vae": ["4", 2]}, + }, + "9": { + "class_type": "SaveImage", + "inputs": {"filename_prefix": "lahr_gen", "images": ["8", 0]}, + }, + } + + +def queue_prompt(workflow): + data = json.dumps({"prompt": workflow}).encode() + req = urllib.request.Request( + f"{COMFY}/prompt", + data=data, + headers={"Content-Type": "application/json"}, + ) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read())["prompt_id"] + + +def wait_for_result(prompt_id, timeout=600): + start = time.time() + while time.time() - start < timeout: + try: + with urllib.request.urlopen(f"{COMFY}/history/{prompt_id}") as resp: + hist = json.loads(resp.read()) + if prompt_id in hist: + outputs = hist[prompt_id].get("outputs", {}) + for node_id, node_out in outputs.items(): + if "images" in node_out: + return node_out["images"] + except Exception: + pass + print(" waiting...", flush=True) + time.sleep(5) + return None + + +def download_image(img_info, out_path): + fname = img_info["filename"] + subfolder = img_info.get("subfolder", "") + img_type = img_info.get("type", "output") + params = f"filename={fname}&subfolder={subfolder}&type={img_type}" + url = f"{COMFY}/view?{params}" + with urllib.request.urlopen(url) as resp: + data = resp.read() + # Convert PNG to JPEG via PIL if available + try: + from PIL import Image + import io + img = Image.open(io.BytesIO(data)).convert("RGB") + img.save(out_path, "JPEG", quality=90) + print(f" Saved JPEG ({len(data)//1024}KB raw -> {os.path.getsize(out_path)//1024}KB)") + except ImportError: + # Save as-is (PNG), rename accordingly + png_path = out_path.replace(".jpg", ".png") + with open(png_path, "wb") as f: + f.write(data) + print(f" Saved PNG (PIL not available): {png_path}") + + +for spec in IMAGES: + out_path = os.path.join(OUT_DIR, spec["filename"]) + print(f"\nGenerating: {spec['filename']}") + workflow = build_workflow(spec["positive"], spec["negative"]) + prompt_id = queue_prompt(workflow) + print(f" Queued: {prompt_id}") + images = wait_for_result(prompt_id) + if images: + download_image(images[0], out_path) + else: + print(" FAILED: no output after timeout") + +print("\nDone.") diff --git a/tools/gen-images.py b/tools/gen-images.py new file mode 100644 index 0000000..a80529f --- /dev/null +++ b/tools/gen-images.py @@ -0,0 +1,106 @@ +""" +Lahr Carpet Cleaning — Gemini Imagen hero image generator. +Generates: hero living room, cleaning in progress, clean result. +Saves to: assets/images/hero/ +Run: python3 tools/gen-images.py +""" +import os +import sys + +try: + from google import genai + from google.genai import types +except ImportError: + print("Installing google-genai...") + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "hero") +os.makedirs(OUT_DIR, exist_ok=True) + +client = genai.Client(api_key=API_KEY) + +IMAGES = [ + { + "name": "hero-living-room", + "prompt": ( + "Luxurious modern living room with spotlessly clean cream carpet, " + "natural light streaming through large windows, contemporary furniture, " + "professional interior photography, warm inviting atmosphere, no people, " + "ultra-realistic, 8K quality" + ), + "aspect": "16:9", + }, + { + "name": "hero-clean-result", + "prompt": ( + "Pristine white plush carpet in a beautiful upscale residential home, " + "dramatic before-after transformation, deeply cleaned carpet with " + "vacuum lines visible, natural light, professional photography, no people" + ), + "aspect": "16:9", + }, + { + "name": "hero-technician", + "prompt": ( + "Professional carpet cleaning technician pushing a large upright hot water " + "extraction machine across residential carpet in a bright modern home interior. " + "Machine resembles an oversized upright vacuum cleaner with a cylindrical body. " + "No steam visible anywhere, no water spraying, no hoses visible, completely dry. " + "Technician shown from behind or side, no face, plain black shirt, no logo. " + "High-end professional photography." + ), + "aspect": "16:9", + }, + { + "name": "hero-before-after", + "prompt": ( + "Side-by-side residential living room carpet: left half heavily soiled with mud " + "and dark stains, right half same carpet after professional hot water extraction " + "cleaning, bright and pristine. No steam, no water, no machines visible anywhere. " + "Dramatic before-after contrast. Professional photography, no people." + ), + "aspect": "16:9", + }, + { + "name": "hero-stairs", + "prompt": ( + "Beautiful staircase with freshly cleaned plush carpet on stairs, " + "modern home interior, natural light from above, professional result, " + "no people, architectural photography, high-end residential" + ), + "aspect": "3:4", + }, +] + +def generate(): + for item in IMAGES: + out_path = os.path.join(OUT_DIR, f"{item['name']}.jpg") + print(f"Generating {item['name']}...") + try: + resp = client.models.generate_images( + model="imagen-4.0-generate-001", + prompt=item["prompt"], + config=types.GenerateImagesConfig( + number_of_images=1, + aspect_ratio=item["aspect"], + output_mime_type="image/jpeg", + safety_filter_level="block_low_and_above", + ), + ) + if resp.generated_images: + image_bytes = resp.generated_images[0].image.image_bytes + with open(out_path, "wb") as f: + f.write(image_bytes) + size_kb = len(image_bytes) // 1024 + print(f" Saved {out_path} ({size_kb}KB)") + else: + print(f" No image returned for {item['name']}") + except Exception as e: + print(f" Error on {item['name']}: {e}") + +if __name__ == "__main__": + generate() + print("\nDone. Images in assets/images/hero/") diff --git a/tools/gen-locations.py b/tools/gen-locations.py new file mode 100644 index 0000000..566618f --- /dev/null +++ b/tools/gen-locations.py @@ -0,0 +1,226 @@ +""" +Lahr Carpet Cleaning — Location page generator. +Creates /locations//index.html for each city. +Run: python3 tools/gen-locations.py +""" +import os + +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +LOC_DIR = os.path.join(BASE_DIR, "locations") + +CITIES = [ + {"name": "Waterloo", "slug": "waterloo-ny", "county": "Seneca County", "note": "Our home base. Fastest response times in the area."}, + {"name": "Geneva", "slug": "geneva-ny", "county": "Ontario County", "note": "Full residential and commercial services throughout Geneva."}, + {"name": "Seneca Falls", "slug": "seneca-falls-ny", "county": "Seneca County", "note": "Serving homes, vacation rentals, and businesses in Seneca Falls."}, + {"name": "Canandaigua", "slug": "canandaigua-ny", "county": "Ontario County", "note": "Lakefront homes, rentals, and businesses along Canandaigua Lake."}, + {"name": "Penn Yan", "slug": "penn-yan-ny", "county": "Yates County", "note": "Homes, wineries, and short-term rentals in the Penn Yan area."}, + {"name": "Newark", "slug": "newark-ny", "county": "Wayne County", "note": "Carpet and upholstery cleaning for homes and businesses in Newark."}, + {"name": "Clifton Springs", "slug": "clifton-springs-ny", "county": "Ontario County", "note": "Residential and commercial cleaning throughout Clifton Springs."}, + {"name": "Lodi", "slug": "lodi-ny", "county": "Seneca County", "note": "Serving homes and vacation properties in Lodi and surrounding areas."}, + {"name": "Himrod", "slug": "himrod-ny", "county": "Yates County", "note": "Carpet and floor cleaning for homes and rentals in the Himrod area."}, + {"name": "Phelps", "slug": "phelps-ny", "county": "Ontario County", "note": "Residential carpet and upholstery cleaning throughout Phelps."}, + {"name": "Shortsville", "slug": "shortsville-ny", "county": "Ontario County", "note": "Home and business cleaning services in Shortsville, NY."}, + {"name": "Victor", "slug": "victor-ny", "county": "Ontario County", "note": "Residential and commercial carpet cleaning throughout Victor."}, + {"name": "Naples", "slug": "naples-ny", "county": "Ontario County", "note": "Serving homes, wineries, and vacation rentals in the Naples area."}, + {"name": "Gorham", "slug": "gorham-ny", "county": "Ontario County", "note": "Carpet and floor cleaning for homes and properties in Gorham."}, + {"name": "Manchester", "slug": "manchester-ny", "county": "Ontario County", "note": "Residential and commercial cleaning services in Manchester, NY."}, + {"name": "Ovid", "slug": "ovid-ny", "county": "Seneca County", "note": "Serving homes and rental properties throughout Ovid."}, + {"name": "Clyde", "slug": "clyde-ny", "county": "Wayne County", "note": "Carpet, upholstery, and floor cleaning for homes and businesses in Clyde."}, + {"name": "Farmington", "slug": "farmington-ny", "county": "Ontario County", "note": "Residential and commercial carpet cleaning throughout Farmington."}, + {"name": "East Bloomfield", "slug": "east-bloomfield-ny", "county": "Ontario County", "note": "Serving homes and properties in East Bloomfield and surrounding areas."}, + {"name": "Rushville", "slug": "rushville-ny", "county": "Yates County", "note": "Carpet and upholstery cleaning for homes and rentals in Rushville."}, + {"name": "Finger Lakes", "slug": "finger-lakes-ny", "county": "Region", "note": "Serving vacation rentals, wineries, and homes across the Finger Lakes region."}, +] + +SERVICES = [ + {"name": "Carpet Cleaning", "slug": "/services/carpet-cleaning/", "img": "/assets/images/services/carpet-cleaning.jpg", "sub": "In-Home Service", "desc": "Hot water extraction removes deep-seated dirt, allergens, and stains from carpet fibers throughout your home."}, + {"name": "Stairs Cleaning", "slug": "/services/stairs/", "img": "/assets/images/services/stairs-cleaning.jpg", "sub": "Step by Step", "desc": "Stairs collect more dirt per square inch than any flat surface. We clean every tread, riser, and landing."}, + {"name": "Upholstery Cleaning","slug": "/services/upholstery/", "img": "/assets/images/services/upholstery-cleaning.jpg","sub": "Furniture Refresh", "desc": "Safe, effective cleaning for sofas, chairs, and mattresses. We work with all fabric types and leave no residue."}, + {"name": "Floor Cleaning", "slug": "/services/floors/", "img": "/assets/images/services/floor-cleaning.jpg", "sub": "Hard Surface Care", "desc": "Wood floor cleaning and tile and grout restoration that brings hard surfaces back to their original condition."}, + {"name": "Area Rug Cleaning", "slug": "/services/area-rugs/", "img": "/assets/images/services/area-rug-cleaning.jpg", "sub": "Delicate Care", "desc": "Gentle, specialized cleaning for oriental, Persian, and delicate rugs that restores color and removes embedded dirt."}, + {"name": "Add-On Services", "slug": "/services/add-ons/", "img": "/assets/images/services/add-ons.jpg", "sub": "Extra Care", "desc": "Furniture moving, pet hair removal, odor treatment, and heavily soiled area care available alongside any service."}, +] + +HERO_IMAGES = [ + "/assets/images/hero/hero-living-room.jpg", + "/assets/images/hero/hero-clean-result.jpg", + "/assets/images/hero/hero-technician.jpg", + "/assets/images/hero/hero-before-after.jpg", + "/assets/images/hero/hero-stairs.jpg", +] + + +def service_card(svc, city_name): + accent_word, rest = svc["name"].split(" ", 1) if " " in svc["name"] else (svc["name"], "") + h3 = f'{accent_word} {rest}'.strip() + return f"""
+
{svc['name']} in {city_name}, NY
+

{h3}

+
{svc['sub']}
+

{svc['desc']}

+ Learn More +
""" + + +def page_html(city, idx): + hero_img = HERO_IMAGES[idx % len(HERO_IMAGES)] + cards = "\n".join(service_card(s, city["name"]) for s in SERVICES) + name = city["name"] + county = city["county"] + note = city["note"] + slug = city["slug"] + + return f""" + + + + + Carpet Cleaning in {name}, NY | Lahr Carpet Cleaning + + + + + + + +
+
+
+ {county} — Finger Lakes +

{name},
NY

+

{note}

+ +
+
+
+ +
+
+
+ +

Services in {name}

+

We serve {name} and the surrounding {county} communities. Call to confirm availability for your address.

+
+
+{cards} +
+
+
+ +
+
+

Serving {name}

+

Call 315-719-1218 or submit the form for a free estimate in {name}, NY.

+ Get a Free Estimate +
+
+ + + + + + +""" + + +def locations_index(): + city_cards = [] + for i, city in enumerate(CITIES): + img = HERO_IMAGES[i % len(HERO_IMAGES)] + name = city["name"] + county = city["county"] + note = city["note"] + slug = city["slug"] + if " " in name: + accent_word, rest = name.split(" ", 1) + h3 = f'{accent_word} {rest}, NY' + else: + h3 = f'{name}, NY' + city_cards.append(f"""
+
{name} NY
+

{h3}

+
{county}
+

{note}

+ View Services +
""") + + cards_html = "\n".join(city_cards) + return f""" + + + + + Service Areas | Lahr Carpet Cleaning | Finger Lakes, NY + + + + + + + +
+
+
+ Finger Lakes Region +

Service
Areas

+

We clean carpets, upholstery, rugs, and hard floors across 21 cities in Upstate New York. Select your city below.

+ +
+
+
+ +
+
+
+ +

Cities We Serve

+

Based in Waterloo, NY. We travel throughout Seneca, Ontario, Yates, Wayne, and Cayuga counties.

+
+
+{cards_html} +
+
+
+ +
+
+

Not sure if we cover your area?

+

Call 315-719-1218 or submit the form and we will confirm availability for your address.

+ Get a Free Estimate +
+
+ + + + + + +""" + + +if __name__ == "__main__": + # Write locations index + with open(os.path.join(LOC_DIR, "index.html"), "w") as f: + f.write(locations_index()) + print("Wrote locations/index.html") + + # Write each city page + for i, city in enumerate(CITIES): + city_dir = os.path.join(LOC_DIR, city["slug"]) + os.makedirs(city_dir, exist_ok=True) + out_path = os.path.join(city_dir, "index.html") + with open(out_path, "w") as f: + f.write(page_html(city, i)) + print(f"Wrote locations/{city['slug']}/index.html") + + print(f"\nDone. {len(CITIES)} city pages + index generated.") diff --git a/tools/gen-missing-2.py b/tools/gen-missing-2.py new file mode 100644 index 0000000..f29a23b --- /dev/null +++ b/tools/gen-missing-2.py @@ -0,0 +1,60 @@ +"""Generate the 2 missing service images.""" +import os, sys +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "services") +client = genai.Client(api_key=API_KEY) + +TARGETS = [ + { + "name": "property-management", + "prompt": ( + "View across three clean empty apartment living rooms, each with spotlessly clean " + "beige carpet showing fresh extraction lines after professional hot water extraction cleaning. " + "Bright neutral interiors ready for new tenants. Natural light, no furniture, " + "no people, no equipment. Professional real estate photography, ultra-realistic." + ), + }, + { + "name": "commercial-overview", + "prompt": ( + "Professional carpet cleaning technician in a plain black shirt, shown from the side, " + "pushing a large upright extraction machine through a bright commercial building lobby. " + "Clean bright carpet behind the machine. No steam, no water spraying, no face visible. " + "Professional editorial photography, ultra-realistic." + ), + }, +] + +for item in TARGETS: + out_path = os.path.join(OUT_DIR, f"{item['name']}.jpg") + print(f"Generating {item['name']}...") + try: + resp = client.models.generate_images( + model="imagen-4.0-generate-001", + prompt=item["prompt"], + config=types.GenerateImagesConfig( + number_of_images=1, + aspect_ratio="4:3", + output_mime_type="image/jpeg", + safety_filter_level="block_low_and_above", + ), + ) + if resp.generated_images: + b = resp.generated_images[0].image.image_bytes + with open(out_path, "wb") as f: + f.write(b) + print(f" Saved ({len(b)//1024}KB)") + else: + print(f" No image returned") + except Exception as e: + print(f" Error: {e}") + +print("Done.") diff --git a/tools/gen-service-images.py b/tools/gen-service-images.py new file mode 100644 index 0000000..7e60816 --- /dev/null +++ b/tools/gen-service-images.py @@ -0,0 +1,179 @@ +""" +Lahr Carpet Cleaning — Service card image generator. +Generates 12 unique images for residential and commercial service cards. +Saves to: assets/images/services/ +Run: python3 tools/gen-service-images.py +""" +import os +import sys + +try: + from google import genai + from google.genai import types +except ImportError: + print("Installing google-genai...") + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "services") +os.makedirs(OUT_DIR, exist_ok=True) + +client = genai.Client(api_key=API_KEY) + +IMAGES = [ + # ── Residential ────────────────────────────────────────────────────────── + { + "name": "carpet-cleaning", + "prompt": ( + "Wide shot of a large industrial stand-up hot water extraction machine being pushed across " + "a plush beige residential carpet. The machine is a heavy commercial-grade upright extractor " + "on wheels — tall, wide cleaning head at the base, long upright handle. " + "The carpet behind it transitions from dirty and matted to clean, bright, and fluffy. " + "Completely dry machine exterior, no steam, no water spraying anywhere. " + "Warm natural interior light. Ultra-realistic professional photography." + ), + }, + { + "name": "stairs-cleaning", + "prompt": ( + "Wide cinematic shot of a carpeted residential staircase. Each step has clean, " + "bright plush carpet that looks freshly cleaned with visible extraction lines. " + "Modern upstate New York home interior, natural light from above, " + "warm wood banisters, no people, no equipment visible. " + "Professional interior photography, ultra-realistic." + ), + }, + { + "name": "upholstery-cleaning", + "prompt": ( + "Close-up of a clean grey linen sofa cushion showing bright, lifted fabric texture " + "after professional upholstery cleaning. Half the cushion shows the before " + "(slightly soiled, flat fabric) and half shows the cleaned result (bright, fluffy, refreshed). " + "Natural window light, residential living room in background, no people, no equipment. " + "Ultra-realistic product photography." + ), + }, + { + "name": "floor-cleaning", + "prompt": ( + "Wide shot of a gleaming hardwood floor in a modern residential home after professional " + "cleaning. The floor reflects soft natural window light, showing deep grain detail. " + "Contemporary furniture in background, no people, no cleaning equipment visible. " + "Professional interior photography, ultra-realistic." + ), + }, + { + "name": "area-rug-cleaning", + "prompt": ( + "Overhead flat-lay shot of a large vibrant Persian or oriental area rug " + "with rich red, navy, and cream geometric patterns, looking freshly cleaned — " + "colors vivid, fibers lifted and bright. Hardwood floor beneath. " + "No people, no equipment, no water. Professional product photography, ultra-realistic." + ), + }, + { + "name": "add-ons", + "prompt": ( + "Close-up macro shot of clean carpet fibers being lifted by a professional " + "grooming brush after hot water extraction cleaning. The fibers are bright, " + "fluffy, and standing upright. Warm light catches the texture. " + "No steam, no water, no people. Ultra-realistic macro photography." + ), + }, + # ── Commercial ─────────────────────────────────────────────────────────── + { + "name": "vacation-rentals", + "prompt": ( + "Bright, airy vacation rental living room in the Finger Lakes region of upstate New York. " + "Spotlessly clean cream carpet, contemporary furniture, large windows with lake views, " + "warm natural afternoon light. Inviting and fresh. No people, no equipment. " + "Professional real estate photography, ultra-realistic." + ), + }, + { + "name": "office-spaces", + "prompt": ( + "Wide shot of a clean modern corporate office with freshly cleaned dark charcoal carpet " + "throughout. Open plan workspace, glass partitions, professional lighting. " + "Carpet shows neat vacuum lines indicating recent professional cleaning. " + "No people, no equipment. Professional architectural photography, ultra-realistic." + ), + }, + { + "name": "hotels-inns", + "prompt": ( + "Elegant hotel corridor in a boutique upstate New York inn. Clean, plush patterned " + "carpet runner down the hallway with fresh vacuum lines. Warm sconce lighting, " + "wood paneling, framed art on walls. No people, no equipment. " + "Professional hospitality photography, ultra-realistic." + ), + }, + { + "name": "retail-showrooms", + "prompt": ( + "Wide shot of an upscale retail showroom or winery tasting room in the Finger Lakes. " + "Clean, rich carpet throughout, warm lighting, product displays on shelves. " + "Carpet looks freshly extracted — bright and spotless. " + "No people, no equipment. Professional commercial interior photography, ultra-realistic." + ), + }, + { + "name": "property-management", + "prompt": ( + "View across three clean apartment living rooms in sequence, each showing " + "spotlessly clean beige carpet with fresh vacuum lines after professional cleaning. " + "Bright, neutral interiors ready for new tenants. Natural light, no furniture, " + "no people, no equipment. Professional real estate photography, ultra-realistic." + ), + }, + { + "name": "commercial-overview", + "prompt": ( + "Professional carpet cleaning technician in a plain black shirt, shown from the side, " + "pushing a large industrial stand-up hot water extraction machine through a bright commercial " + "building lobby. The machine is a heavy commercial-grade upright extractor on wheels — " + "tall, wide cleaning head, long handle. Clean carpet visible. No steam, no water spraying, " + "no face visible. Professional editorial photography, ultra-realistic." + ), + }, +] + + +def generate(): + saved = [] + total = len(IMAGES) + for i, item in enumerate(IMAGES, 1): + out_path = os.path.join(OUT_DIR, f"{item['name']}.jpg") + print(f"[{i}/{total}] Generating {item['name']}...") + try: + resp = client.models.generate_images( + model="imagen-4.0-generate-001", + prompt=item["prompt"], + config=types.GenerateImagesConfig( + number_of_images=1, + aspect_ratio="4:3", + output_mime_type="image/jpeg", + safety_filter_level="block_low_and_above", + ), + ) + if resp.generated_images: + img_bytes = resp.generated_images[0].image.image_bytes + with open(out_path, "wb") as f: + f.write(img_bytes) + print(f" Saved {out_path} ({len(img_bytes)//1024}KB)") + saved.append(item["name"]) + else: + print(f" No image returned for {item['name']}") + except Exception as e: + print(f" Error on {item['name']}: {e}") + + return saved + + +if __name__ == "__main__": + saved = generate() + print(f"\nDone. {len(saved)}/{len(IMAGES)} images saved to assets/images/services/") + if saved: + print("Generated:", ", ".join(saved)) diff --git a/tools/gen-video.py b/tools/gen-video.py new file mode 100644 index 0000000..e4f864e --- /dev/null +++ b/tools/gen-video.py @@ -0,0 +1,220 @@ +""" +Lahr Carpet Cleaning — Veo hero video generator. +5 shots x 4s = 20s reel. Concatenated by ffmpeg into hero-reel.mp4. +Saves clips to: assets/videos/hero/clips/ +Saves final to: assets/videos/hero/hero-reel.mp4 +Run: python3 tools/gen-video.py +""" +import os +import sys +import time +import subprocess + +try: + from google import genai + from google.genai import types +except ImportError: + print("Installing google-genai...") + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +OUT_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +os.makedirs(OUT_DIR, exist_ok=True) + +client = genai.Client(api_key=API_KEY) + +SHOTS = [ + { + "name": "shot-01-door-opens", + "prompt": ( + "Cinematic low-angle wide shot. A solid wood front door of an upstate New York home opens " + "inward smoothly. Bright golden afternoon sunlight pours through the doorway onto a carpeted " + "entryway floor. Camera is at floor level, looking toward the door. The door swings open " + "fully revealing light. No people visible. Photorealistic, warm inviting light, slow motion." + ), + }, + { + "name": "shot-02-pan-to-stains", + "prompt": ( + "Slow cinematic camera pan from the front door entryway across a residential living room carpet " + "in an upstate New York home. The carpet shows visible dirt tracks, pet stains, and soiling " + "from daily use. Natural light. No people. Camera moves fluidly across the room revealing " + "the stained carpet. Photorealistic." + ), + }, + { + "name": "shot-03-stain-closeup", + "prompt": ( + "Close-up shot of a stained beige carpet with visible pet stains, mud, and dark soiling. " + "Camera slowly pushes in on the dirty area. Dramatic side lighting emphasises the stain depth " + "and texture. Slow motion. Ultra-realistic macro photography style." + ), + }, + { + "name": "shot-04-extraction-carpet", + "prompt": ( + "Cinematic slow-motion wide shot: a large industrial stand-up hot water extraction machine " + "being pushed steadily forward across a beige residential carpet. The machine is a tall " + "professional-grade upright extractor — heavy-duty, commercial size, on wheels, with a wide " + "cleaning head at the base and an upright handle. No steam, no spraying water, no visible " + "liquid anywhere on the machine exterior. The carpet behind the machine transitions from dirty " + "and matted to bright, clean, and fluffy as it passes. Warm natural room light. Photorealistic." + ), + }, + { + "name": "shot-05-extraction-couch", + "prompt": ( + "Close-up cinematic shot of a professional technician's gloved hand holding a small flat " + "upholstery cleaning attachment tool, pressing it firmly against a dirty grey sofa cushion " + "and sliding it slowly across the fabric. The fabric visibly brightens and lifts as the tool " + "moves. No water pours out — suction draws moisture into the tool. Slow motion, natural light. " + "Photorealistic." + ), + }, + { + "name": "shot-06-extraction-stairs", + "prompt": ( + "Cinematic shot of a professional technician's hands using a compact portable upright carpet " + "cleaner on a carpeted staircase — pushing the machine up a stair tread step by step. Each " + "tread brightens and looks freshly cleaned as the machine passes. No water pours out. Clean " + "bright carpet revealed on each step. Slow motion, warm interior light. Photorealistic." + ), + }, + { + "name": "shot-07-office-entryway", + "prompt": ( + "Wide cinematic shot of a clean professional office building entryway with commercial grade " + "carpet. Modern corporate interior, glass doors, professional lighting. No people. Camera " + "slowly pushes forward through the entry. Photorealistic." + ), + }, + { + "name": "shot-08-showroom", + "prompt": ( + "Wide cinematic shot of an upscale retail showroom or winery tasting room in the Finger Lakes " + "region. Rich carpet throughout, warm interior lighting, product displays. No people. Camera " + "glides forward through the space. Photorealistic, luxurious atmosphere." + ), + }, + { + "name": "shot-09-technician-unloading", + "prompt": ( + "Wide shot of a professional carpet cleaning technician wearing a plain black shirt with no logo, " + "rolling a large industrial stand-up hot water extraction machine out of a white service van " + "parked in a residential driveway in upstate New York. The machine is a heavy commercial-grade " + "upright extractor on wheels — tall, industrial size. Autumn trees in background, bright day. " + "Technician shown from side or behind, no face visible. Photorealistic." + ), + }, +] + +MODELS = [ + "veo-2.0-generate-001", + "veo-3.0-generate-001", +] + +def poll(operation, timeout=420): + elapsed = 0 + while not operation.done: + if elapsed >= timeout: + print(" Timed out.") + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + operation = client.operations.get(operation) + return operation + +def download_video(video, out_path): + video_bytes = None + try: + video_bytes = client.files.download(file=video) + except Exception: + pass + if video_bytes: + with open(out_path, "wb") as f: + f.write(video_bytes) + return True + if hasattr(video, "uri") and video.uri: + import urllib.request + uri = video.uri + ("&" if "?" in video.uri else "?") + f"key={API_KEY}" + print(f" Fetching via URI...") + urllib.request.urlretrieve(uri, out_path) + return True + return False + +def generate(): + saved = [] + for item in SHOTS: + out_path = os.path.join(OUT_DIR, f"{item['name']}.mp4") + print(f"\n[{SHOTS.index(item)+1}/{len(SHOTS)}] Generating {item['name']}...") + + done = False + for model in MODELS: + try: + print(f" Model: {model}") + op = client.models.generate_videos( + model=model, + prompt=item["prompt"], + config=types.GenerateVideosConfig( + aspect_ratio="16:9", + resolution="720p", + duration_seconds=6, + number_of_videos=1, + ), + ) + op = poll(op) + if op is None: + continue + if op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + if download_video(vid, out_path): + size_kb = os.path.getsize(out_path) // 1024 + print(f" Saved {out_path} ({size_kb}KB)") + saved.append(out_path) + done = True + break + else: + print(f" Download failed for {model}") + else: + print(f" No video from {model}") + except Exception as e: + print(f" Error with {model}: {e}") + + if not done: + print(f" FAILED: {item['name']}") + + return saved + +def concat(clips): + if len(clips) < 2: + print("Not enough clips to concatenate.") + return + list_file = os.path.join(OUT_DIR, "concat.txt") + with open(list_file, "w") as f: + for c in clips: + f.write(f"file '{c}'\n") + print(f"\nConcatenating {len(clips)} clips into hero-reel.mp4...") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", + "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + size_kb = os.path.getsize(REEL_OUT) // 1024 + print(f" Saved {REEL_OUT} ({size_kb}KB)") + else: + print(f" ffmpeg error: {result.stderr[-300:]}") + +if __name__ == "__main__": + clips = generate() + if clips: + concat(clips) + print(f"\nDone. {len(clips)}/5 clips generated.") + if len(clips) == 5: + print("Hero reel ready: assets/videos/hero/hero-reel.mp4") diff --git a/tools/regen-3shots.py b/tools/regen-3shots.py new file mode 100644 index 0000000..8916696 --- /dev/null +++ b/tools/regen-3shots.py @@ -0,0 +1,142 @@ +""" +Regen shot-04, shot-06, shot-07 with corrected scenes. +shot-04: carpet before/after reveal, no machine +shot-06: clean bright staircase, no machine +shot-07: bright modern office, no dark tones +""" +import os, sys, time, subprocess + +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +client = genai.Client(api_key=API_KEY) + +SHOTS = [ + { + "name": "shot-04-extraction-carpet", + "prompt": ( + "Cinematic slow-motion wide shot. Camera glides low across a residential living room carpet. " + "The left half of the carpet is visibly dirty, stained, and matted. " + "The right half is bright, clean, fluffy, and freshly extracted. " + "The boundary between dirty and clean is sharp and dramatic. " + "Warm natural afternoon light. No people. No machines. No equipment. Photorealistic." + ), + }, + { + "name": "shot-06-extraction-stairs", + "prompt": ( + "Cinematic slow-motion shot looking up a bright residential carpeted staircase. " + "Each step has clean, bright, plush beige carpet with fresh extraction lines. " + "Warm natural light from above illuminates the stairs. Wood banisters on each side. " + "No people. No machines. No equipment anywhere in frame. Photorealistic." + ), + }, + { + "name": "shot-07-office-entryway", + "prompt": ( + "Wide cinematic shot of a bright modern commercial office building lobby. " + "Large windows let in abundant natural daylight. Clean beige or grey commercial carpet throughout. " + "White walls, professional lighting, glass doors, contemporary furniture. " + "The carpet looks spotlessly clean with neat vacuum lines. " + "No people. No machines. No dark tones — the space is bright and well-lit. Photorealistic." + ), + }, +] + +MODEL = "veo-3.1-generate-preview" + +def poll(op, timeout=600): + elapsed = 0 + while not op.done: + if elapsed >= timeout: + print(f" Timed out after {timeout}s.") + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + op = client.operations.get(op) + return op + +saved = [] +for item in SHOTS: + out_path = os.path.join(VID_DIR, f"{item['name']}.mp4") + print(f"\n[VID] Generating {item['name']}...") + try: + op = client.models.generate_videos( + model=MODEL, + prompt=item["prompt"], + config=types.GenerateVideosConfig( + aspect_ratio="16:9", + resolution="720p", + duration_seconds=6, + number_of_videos=1, + ), + ) + op = poll(op) + if op is None: + print(f" FAILED (timeout)") + continue + if op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + video_bytes = client.files.download(file=vid) + if video_bytes: + with open(out_path, "wb") as f: + f.write(video_bytes) + print(f" Saved ({os.path.getsize(out_path)//1024}KB)") + saved.append(item["name"]) + else: + try: + vid.save(out_path) + print(f" Saved via .save() ({os.path.getsize(out_path)//1024}KB)") + saved.append(item["name"]) + except Exception as e2: + print(f" Download failed: {e2}") + else: + print(f" No video returned") + except Exception as e: + print(f" Error: {e}") + +print(f"\n{len(saved)}/{len(SHOTS)} shots saved: {saved}") + +ORDER = [ + "shot-01-door-opens-trimmed", + "shot-02-pan-to-stains", + "shot-03-stain-closeup", + "shot-04-extraction-carpet", + "shot-05-extraction-couch", + "shot-06-extraction-stairs", + "shot-07-office-entryway", + "shot-08-showroom", + "shot-09-technician-unloading", +] + +missing = [n for n in ORDER if not os.path.exists(os.path.join(VID_DIR, f"{n}.mp4"))] +if missing: + print(f"\nSkipping reconcat — missing: {missing}") +else: + print("\nReconcatenating hero-reel.mp4...") + concat_file = os.path.join(VID_DIR, "concat.txt") + with open(concat_file, "w") as f: + for name in ORDER: + f.write(f"file '{os.path.join(VID_DIR, name)}.mp4'\n") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", + "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" Reel saved ({os.path.getsize(REEL_OUT)//1024}KB)") + else: + print(f" ffmpeg error: {result.stderr[-400:]}") + +print("\nDone.") diff --git a/tools/regen-commercial-overview.py b/tools/regen-commercial-overview.py new file mode 100644 index 0000000..672f119 --- /dev/null +++ b/tools/regen-commercial-overview.py @@ -0,0 +1,55 @@ +"""Regenerate commercial-overview.jpg with a clear commercial carpet scene.""" +import os, sys +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "services") +client = genai.Client(api_key=API_KEY) + +PROMPTS = [ + ( + "Wide shot of a modern commercial office building lobby with clean grey carpet throughout. " + "Professional corporate interior, glass doors, white walls, overhead lighting. " + "The carpet is spotless and freshly cleaned — uniform, well-maintained. " + "No people. No machines. No equipment. Professional architectural photography, ultra-realistic." + ), + ( + "Wide interior shot of a bright commercial building corridor with clean, dark grey commercial carpet. " + "Modern office environment, glass partitions, professional lighting. " + "The carpet looks freshly cleaned and spotless. " + "No people, no equipment. Professional photography, ultra-realistic." + ), +] + +out_path = os.path.join(OUT_DIR, "commercial-overview.jpg") + +for i, prompt in enumerate(PROMPTS): + print(f"Attempt {i+1}...") + try: + resp = client.models.generate_images( + model="imagen-4.0-generate-001", + prompt=prompt, + config=types.GenerateImagesConfig( + number_of_images=1, aspect_ratio="4:3", + output_mime_type="image/jpeg", + safety_filter_level="block_low_and_above", + ), + ) + if resp.generated_images: + b = resp.generated_images[0].image.image_bytes + with open(out_path, "wb") as f: + f.write(b) + print(f"Saved ({len(b)//1024}KB)") + break + else: + print("No image returned") + except Exception as e: + print(f"Error: {e}") + +print("Done.") diff --git a/tools/regen-full-reel.py b/tools/regen-full-reel.py new file mode 100644 index 0000000..cbecc11 --- /dev/null +++ b/tools/regen-full-reel.py @@ -0,0 +1,163 @@ +""" +Full hero reel regeneration — 7-shot narrative arc. +1. Door opens, muddy boots run in +2. Mud tracked across carpet +3. Stain on upholstered chair +4. Carpet cleaning machine extracting dirt +5. Clean bright staircase +6. Office building wide carpet +7. Restaurant with carpet +""" +import os, sys, time, subprocess + +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +os.makedirs(VID_DIR, exist_ok=True) +client = genai.Client(api_key=API_KEY) + +SHOTS = [ + { + "name": "v2-shot-01-door-entry", + "prompt": ( + "Cinematic slow-motion wide shot. A wooden front door of an upstate New York home swings open. " + "A child and an adult walk inside wearing muddy boots. Camera stays low at floor level. " + "The boots leave dark muddy tracks across the beige carpet in the entryway with each step. " + "Warm afternoon light pours through the open door. Photorealistic." + ), + }, + { + "name": "v2-shot-02-mud-on-carpet", + "prompt": ( + "Extreme close-up slow-motion shot at carpet level. Muddy boot soles press into clean beige carpet, " + "leaving dark brown mud stains and wet footprints with each step. " + "Camera is low, tight on the boots and the mud soaking into carpet fibers. " + "Dramatic side lighting. No faces visible. Photorealistic." + ), + }, + { + "name": "v2-shot-03-stain-on-chair", + "prompt": ( + "Close-up cinematic shot of a light grey upholstered armchair. " + "A visible dark stain spreads across one cushion. " + "Camera slowly pushes in on the stain, showing the soiled fabric texture. " + "Warm natural light from a window. No people. No equipment. Photorealistic." + ), + }, + { + "name": "v2-shot-04-extraction-carpet", + "prompt": ( + "Cinematic slow-motion wide shot. A technician pushes a Rug Doctor style carpet cleaning machine " + "steadily forward across a beige living room carpet. The machine is a tall upright unit with a handle " + "and flat rectangular cleaning head — like a large upright vacuum cleaner. " + "The carpet behind the machine is visibly brighter and cleaner than the carpet ahead of it. " + "No steam. No water spraying. Warm room light. Photorealistic." + ), + }, + { + "name": "v2-shot-05-clean-stairs", + "prompt": ( + "Cinematic slow-motion shot looking up a bright residential carpeted staircase. " + "Each step has clean, fresh, plush beige carpet. Warm natural light from above. " + "Wood banisters on the sides. The carpet looks spotless and freshly cleaned. " + "No people. No machines. Photorealistic." + ), + }, + { + "name": "v2-shot-06-office", + "prompt": ( + "Wide cinematic shot of a bright modern commercial office lobby. " + "Large windows, abundant natural daylight. Clean grey commercial carpet covers the entire floor. " + "White walls, glass partitions, professional lighting. Carpet looks spotlessly clean. " + "No people. No machines. Photorealistic." + ), + }, + { + "name": "v2-shot-07-restaurant", + "prompt": ( + "Wide cinematic shot of an upscale restaurant dining room with carpeted floors. " + "Warm ambient lighting, white tablecloths, wood accents. " + "The carpet is clean, rich, and well-maintained throughout the space. " + "No people. No machines. Photorealistic, luxurious atmosphere." + ), + }, +] + +MODEL = "veo-3.1-generate-preview" + +def poll(op, timeout=600): + elapsed = 0 + while not op.done: + if elapsed >= timeout: + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + op = client.operations.get(op) + return op + +saved = [] +for item in SHOTS: + out_path = os.path.join(VID_DIR, f"{item['name']}.mp4") + print(f"\n[{SHOTS.index(item)+1}/{len(SHOTS)}] {item['name']}...") + try: + op = client.models.generate_videos( + model=MODEL, + prompt=item["prompt"], + config=types.GenerateVideosConfig( + aspect_ratio="16:9", resolution="720p", + duration_seconds=6, number_of_videos=1, + ), + ) + op = poll(op) + if op and op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + video_bytes = client.files.download(file=vid) + if video_bytes: + with open(out_path, "wb") as f: + f.write(video_bytes) + print(f" Saved ({os.path.getsize(out_path)//1024}KB)") + saved.append(out_path) + else: + try: + vid.save(out_path) + print(f" Saved via .save()") + saved.append(out_path) + except Exception as e2: + print(f" Download failed: {e2}") + else: + print(f" No video returned") + except Exception as e: + print(f" Error: {e}") + +print(f"\n{len(saved)}/{len(SHOTS)} shots saved") + +if len(saved) < 2: + print("Not enough clips — skipping reconcat.") +else: + order = [os.path.join(VID_DIR, f"{s['name']}.mp4") for s in SHOTS + if os.path.exists(os.path.join(VID_DIR, f"{s['name']}.mp4"))] + concat_file = os.path.join(VID_DIR, "concat-v2.txt") + with open(concat_file, "w") as f: + for p in order: + f.write(f"file '{p}'\n") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" Reel saved ({os.path.getsize(REEL_OUT)//1024}KB)") + else: + print(f" ffmpeg error: {result.stderr[-300:]}") + +print("\nDone.") diff --git a/tools/regen-images-targeted.py b/tools/regen-images-targeted.py new file mode 100644 index 0000000..58f2ff2 --- /dev/null +++ b/tools/regen-images-targeted.py @@ -0,0 +1,64 @@ +"""Regenerate only hero-technician and hero-before-after images.""" +import os, sys +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +OUT_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets", "images", "hero") +client = genai.Client(api_key=API_KEY) + +TARGETS = [ + { + "name": "hero-technician", + "aspect": "16:9", + "prompt": ( + "Professional carpet cleaning technician pushing a large upright hot water " + "extraction machine across residential carpet in a bright modern home interior. " + "Machine resembles an oversized upright vacuum cleaner with a cylindrical body. " + "No steam visible anywhere, no water spraying, no hoses visible, completely dry. " + "Technician shown from behind or side, no face, plain black shirt, no logo. " + "High-end professional photography." + ), + }, + { + "name": "hero-before-after", + "aspect": "16:9", + "prompt": ( + "Side-by-side residential living room carpet: left half heavily soiled with mud " + "and dark stains, right half same carpet after professional hot water extraction " + "cleaning, bright and pristine. No steam, no water, no machines visible anywhere. " + "Dramatic before-after contrast. Professional photography, no people." + ), + }, +] + +for item in TARGETS: + out_path = os.path.join(OUT_DIR, f"{item['name']}.jpg") + print(f"Generating {item['name']}...") + try: + resp = client.models.generate_images( + model="imagen-4.0-generate-001", + prompt=item["prompt"], + config=types.GenerateImagesConfig( + number_of_images=1, + aspect_ratio=item["aspect"], + output_mime_type="image/jpeg", + safety_filter_level="block_low_and_above", + ), + ) + if resp.generated_images: + img_bytes = resp.generated_images[0].image.image_bytes + with open(out_path, "wb") as f: + f.write(img_bytes) + print(f" Saved {out_path} ({len(img_bytes)//1024}KB)") + else: + print(f" No image returned for {item['name']}") + except Exception as e: + print(f" Error: {e}") + +print("\nDone.") diff --git a/tools/regen-industrial.py b/tools/regen-industrial.py new file mode 100644 index 0000000..d459148 --- /dev/null +++ b/tools/regen-industrial.py @@ -0,0 +1,169 @@ +"""Regenerate carpet-cleaning and commercial-overview service images with industrial extractor prompts.""" +import os, sys, time, subprocess +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +IMG_DIR = os.path.join(BASE_DIR, "assets", "images", "services") +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +client = genai.Client(api_key=API_KEY) + +# ── Service images ──────────────────────────────────────────────────────────── +IMAGES = [ + { + "name": "carpet-cleaning", + "prompt": ( + "Wide shot of a large industrial stand-up hot water extraction machine being pushed across " + "a plush beige residential carpet. The machine is a heavy commercial-grade upright extractor " + "on wheels — tall, wide cleaning head at the base, long upright handle. " + "The carpet behind it transitions from dirty and matted to clean, bright, and fluffy. " + "Completely dry machine exterior, no steam, no water spraying anywhere. " + "Warm natural interior light. Ultra-realistic professional photography." + ), + }, + { + "name": "commercial-overview", + "prompt": ( + "Professional carpet cleaning technician in a plain black shirt, shown from the side, " + "pushing a large industrial stand-up hot water extraction machine through a bright commercial " + "building lobby. The machine is a heavy commercial-grade upright extractor on wheels — " + "tall, wide cleaning head, long handle. Clean carpet visible. No steam, no water spraying, " + "no face visible. Professional editorial photography, ultra-realistic." + ), + }, +] + +for item in IMAGES: + out_path = os.path.join(IMG_DIR, f"{item['name']}.jpg") + print(f"[IMG] Generating {item['name']}...") + try: + resp = client.models.generate_images( + model="imagen-4.0-generate-001", + prompt=item["prompt"], + config=types.GenerateImagesConfig( + number_of_images=1, aspect_ratio="4:3", + output_mime_type="image/jpeg", + safety_filter_level="block_low_and_above", + ), + ) + if resp.generated_images: + b = resp.generated_images[0].image.image_bytes + with open(out_path, "wb") as f: + f.write(b) + print(f" Saved {out_path} ({len(b)//1024}KB)") + else: + print(f" No image returned") + except Exception as e: + print(f" Error: {e}") + +# ── Video shots ─────────────────────────────────────────────────────────────── +SHOTS = [ + { + "name": "shot-04-extraction-carpet", + "prompt": ( + "Cinematic slow-motion wide shot: a large industrial stand-up hot water extraction machine " + "being pushed steadily forward across a beige residential carpet. The machine is a tall " + "professional-grade upright extractor — heavy-duty, commercial size, on wheels, with a wide " + "cleaning head at the base and an upright handle. No steam, no spraying water, no visible " + "liquid anywhere on the machine exterior. The carpet behind the machine transitions from dirty " + "and matted to bright, clean, and fluffy as it passes. Warm natural room light. Photorealistic." + ), + }, + { + "name": "shot-09-technician-unloading", + "prompt": ( + "Wide shot of a professional carpet cleaning technician wearing a plain black shirt with no logo, " + "rolling a large industrial stand-up hot water extraction machine out of a white service van " + "parked in a residential driveway in upstate New York. The machine is a heavy commercial-grade " + "upright extractor on wheels — tall, industrial size. Autumn trees in background, bright day. " + "Technician shown from side or behind, no face visible. Photorealistic." + ), + }, +] + +MODELS = ["veo-2.0-generate-001", "veo-3.0-generate-001"] + +def poll(op, timeout=420): + elapsed = 0 + while not op.done: + if elapsed >= timeout: + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + op = client.operations.get(op) + return op + +saved_clips = [] +for item in SHOTS: + out_path = os.path.join(VID_DIR, f"{item['name']}.mp4") + print(f"\n[VID] Generating {item['name']}...") + done = False + for model in MODELS: + try: + print(f" Model: {model}") + op = client.models.generate_videos( + model=model, prompt=item["prompt"], + config=types.GenerateVideosConfig( + aspect_ratio="16:9", resolution="720p", + duration_seconds=6, number_of_videos=1, + ), + ) + op = poll(op) + if op and op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + try: + b = client.files.download(file=vid) + except Exception: + b = None + if b: + with open(out_path, "wb") as f: + f.write(b) + print(f" Saved ({os.path.getsize(out_path)//1024}KB)") + saved_clips.append(item["name"]) + done = True + break + except Exception as e: + print(f" Error with {model}: {e}") + if not done: + print(f" FAILED: {item['name']}") + +# ── Reconcat reel if both shots regenerated ─────────────────────────────────── +if len(saved_clips) == 2: + print("\nReconcatenating reel...") + concat_file = os.path.join(VID_DIR, "concat.txt") + order = [ + "shot-01-door-opens-trimmed", + "shot-02-pan-to-stains", + "shot-03-stain-closeup", + "shot-04-extraction-carpet", + "shot-05-extraction-couch", + "shot-06-extraction-stairs", + "shot-07-office-entryway", + "shot-08-showroom", + "shot-09-technician-unloading", + ] + with open(concat_file, "w") as f: + for name in order: + f.write(f"file '{os.path.join(VID_DIR, name)}.mp4'\n") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", + "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" Reel saved ({os.path.getsize(REEL_OUT)//1024}KB)") + else: + print(f" ffmpeg error: {result.stderr[-300:]}") +else: + print(f"\nOnly {len(saved_clips)}/2 video shots regenerated — skipping reconcat.") + +print("\nDone.") diff --git a/tools/regen-shot.py b/tools/regen-shot.py new file mode 100644 index 0000000..9ed4894 --- /dev/null +++ b/tools/regen-shot.py @@ -0,0 +1,87 @@ +""" +Regenerate one specific shot and re-concatenate the hero reel. +Usage: python3 tools/regen-shot.py shot-02-staircase +""" +import os, sys, time, subprocess +from google import genai +from google.genai import types +import urllib.request + +API_KEY = os.environ.get("GEMINI_API_KEY", "") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +CLIPS_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") + +if not API_KEY: + print("Set GEMINI_API_KEY env var"); sys.exit(1) + +# Import SHOTS from gen-video.py (dash in name requires importlib) +import importlib.util +spec = importlib.util.spec_from_file_location( + "gen_video", os.path.join(os.path.dirname(__file__), "gen-video.py") +) +_mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(_mod) +SHOTS = _mod.SHOTS + +shot_name = sys.argv[1] if len(sys.argv) > 1 else None +target = next((s for s in SHOTS if s["name"] == shot_name), None) +if not target: + print(f"Shot '{shot_name}' not found. Available: {[s['name'] for s in SHOTS]}") + sys.exit(1) + +client = genai.Client(api_key=API_KEY) +out_path = os.path.join(CLIPS_DIR, f"{target['name']}.mp4") + +print(f"Regenerating {target['name']}...") +op = client.models.generate_videos( + model="veo-3.0-generate-001", + prompt=target["prompt"], + config=types.GenerateVideosConfig( + aspect_ratio="16:9", resolution="720p", + duration_seconds=6, number_of_videos=1, + ), +) +elapsed = 0 +while not op.done: + print(f" Waiting... ({elapsed}s)") + time.sleep(15); elapsed += 15 + op = client.operations.get(op) + +if not (op.response and op.response.generated_videos): + print("No video returned"); sys.exit(1) + +vid = op.response.generated_videos[0].video +video_bytes = None +try: + video_bytes = client.files.download(file=vid) +except Exception: + pass +if video_bytes: + with open(out_path, "wb") as f: f.write(video_bytes) +elif hasattr(vid, "uri") and vid.uri: + uri = vid.uri + ("&" if "?" in vid.uri else "?") + f"key={API_KEY}" + urllib.request.urlretrieve(uri, out_path) +else: + print("Download failed"); sys.exit(1) + +print(f"Saved {out_path} ({os.path.getsize(out_path)//1024}KB)") + +# Re-concat in shot order +clips = [os.path.join(CLIPS_DIR, f"{s['name']}.mp4") for s in SHOTS + if os.path.exists(os.path.join(CLIPS_DIR, f"{s['name']}.mp4"))] +list_file = os.path.join(CLIPS_DIR, "concat.txt") +with open(list_file, "w") as f: + for c in clips: f.write(f"file '{c}'\n") + +print(f"Concatenating {len(clips)} clips...") +r = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", list_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", + "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True +) +if r.returncode == 0: + print(f"Reel updated: {REEL_OUT} ({os.path.getsize(REEL_OUT)//1024}KB)") +else: + print(f"ffmpeg error: {r.stderr[-200:]}") diff --git a/tools/regen-shot02.py b/tools/regen-shot02.py new file mode 100644 index 0000000..48c9237 --- /dev/null +++ b/tools/regen-shot02.py @@ -0,0 +1,102 @@ +"""Generate shot-02 replacement: kid runs in with muddy shoes, focus on feet and mud tracks.""" +import os, sys, time, subprocess + +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +client = genai.Client(api_key=API_KEY) + +PROMPTS = [ + ( + "Slow cinematic shot at floor level, looking across a beige residential carpet. " + "A child's feet in muddy sneakers run into frame from the front door and across the carpet, " + "leaving clear muddy footprints with each step. Then adult feet in boots walk in behind, " + "adding more dirt and mud tracks. Camera stays low, focused on the feet and the mud on the carpet. " + "Warm indoor light. Photorealistic, slow motion." + ), + ( + "Low cinematic camera angle at carpet level inside a home entryway. " + "A child runs in wearing muddy shoes, close-up on their feet stomping mud into the beige carpet. " + "Adult legs follow behind, tracking in more dirt. " + "The carpet shows fresh mud and dirty footprints after each step. " + "Camera stays at floor level throughout. Warm natural light. Slow motion. Photorealistic." + ), +] + +MODEL = "veo-3.1-generate-preview" +out_path = os.path.join(VID_DIR, "shot-02-pan-to-stains.mp4") + +def poll(op, timeout=600): + elapsed = 0 + while not op.done: + if elapsed >= timeout: + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + op = client.operations.get(op) + return op + +saved = False +for i, prompt in enumerate(PROMPTS): + print(f"\n[VID] shot-02 attempt {i+1}...") + try: + op = client.models.generate_videos( + model=MODEL, + prompt=prompt, + config=types.GenerateVideosConfig( + aspect_ratio="16:9", resolution="720p", + duration_seconds=6, number_of_videos=1, + ), + ) + op = poll(op) + if op and op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + video_bytes = client.files.download(file=vid) + if video_bytes: + with open(out_path, "wb") as f: + f.write(video_bytes) + print(f" Saved ({os.path.getsize(out_path)//1024}KB)") + saved = True + break + else: + print(f" No video returned, trying next prompt...") + except Exception as e: + print(f" Error: {e}") + +if not saved: + print("All attempts failed — original shot-02 kept.") +else: + ORDER = [ + "shot-01-door-opens-trimmed", "shot-02-pan-to-stains", "shot-03-stain-closeup", + "shot-04-extraction-carpet", "shot-05-extraction-couch", "shot-06-extraction-stairs", + "shot-07-office-entryway", "shot-08-showroom", "shot-09-technician-unloading", + ] + missing = [n for n in ORDER if not os.path.exists(os.path.join(VID_DIR, f"{n}.mp4"))] + if missing: + print(f"Skipping reconcat — missing: {missing}") + else: + concat_file = os.path.join(VID_DIR, "concat.txt") + with open(concat_file, "w") as f: + for name in ORDER: + f.write(f"file '{os.path.join(VID_DIR, name)}.mp4'\n") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" Reel saved ({os.path.getsize(REEL_OUT)//1024}KB)") + else: + print(f" ffmpeg error: {result.stderr[-300:]}") + +print("\nDone.") diff --git a/tools/regen-shot04.py b/tools/regen-shot04.py new file mode 100644 index 0000000..566a1e1 --- /dev/null +++ b/tools/regen-shot04.py @@ -0,0 +1,95 @@ +"""Retry shot-04 with a simple clean carpet scene.""" +import os, sys, time, subprocess + +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +client = genai.Client(api_key=API_KEY) + +PROMPTS = [ + ( + "Cinematic slow dolly shot moving forward through a bright residential living room. " + "Clean, plush beige carpet fills the floor. Warm afternoon sunlight comes through large windows. " + "Contemporary furniture, neutral walls. The carpet looks freshly cleaned — bright, fluffy, spotless. " + "No people. No machines. Photorealistic." + ), + ( + "Slow cinematic camera pan across a clean beige residential carpet in a bright living room. " + "The carpet fibers are lifted and bright after professional cleaning. " + "Warm natural light. Modern home interior. No people, no equipment. Photorealistic." + ), +] + +MODEL = "veo-3.1-generate-preview" +out_path = os.path.join(VID_DIR, "shot-04-extraction-carpet.mp4") + +def poll(op, timeout=600): + elapsed = 0 + while not op.done: + if elapsed >= timeout: + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + op = client.operations.get(op) + return op + +saved = False +for i, prompt in enumerate(PROMPTS): + print(f"\n[VID] shot-04 attempt {i+1}...") + try: + op = client.models.generate_videos( + model=MODEL, + prompt=prompt, + config=types.GenerateVideosConfig( + aspect_ratio="16:9", resolution="720p", + duration_seconds=6, number_of_videos=1, + ), + ) + op = poll(op) + if op and op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + video_bytes = client.files.download(file=vid) + if video_bytes: + with open(out_path, "wb") as f: + f.write(video_bytes) + print(f" Saved ({os.path.getsize(out_path)//1024}KB)") + saved = True + break + else: + print(f" No video returned, trying next prompt...") + except Exception as e: + print(f" Error: {e}") + +if not saved: + print("All attempts failed.") +else: + ORDER = [ + "shot-01-door-opens-trimmed", "shot-02-pan-to-stains", "shot-03-stain-closeup", + "shot-04-extraction-carpet", "shot-05-extraction-couch", "shot-06-extraction-stairs", + "shot-07-office-entryway", "shot-08-showroom", "shot-09-technician-unloading", + ] + concat_file = os.path.join(VID_DIR, "concat.txt") + with open(concat_file, "w") as f: + for name in ORDER: + f.write(f"file '{os.path.join(VID_DIR, name)}.mp4'\n") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" Reel saved ({os.path.getsize(REEL_OUT)//1024}KB)") + else: + print(f" ffmpeg error: {result.stderr[-300:]}") + +print("\nDone.") diff --git a/tools/regen-v3-shot04.py b/tools/regen-v3-shot04.py new file mode 100644 index 0000000..35459c8 --- /dev/null +++ b/tools/regen-v3-shot04.py @@ -0,0 +1,98 @@ +"""Replace v3-shot-04 with a clean sofa result — no cleaning action, no equipment, no steam.""" +import os, sys, time, subprocess + +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +client = genai.Client(api_key=API_KEY) + +PROMPTS = [ + ( + "Close-up cinematic shot slowly pulling back from a clean, bright grey upholstered sofa cushion. " + "The fabric is fresh, fluffy, and spotless. Warm natural window light. " + "No people. No machines. No equipment of any kind. No steam. No water. " + "Just a beautiful clean sofa in a bright living room. Photorealistic." + ), + ( + "Slow cinematic pan across a clean living room. A light grey sofa with pristine cushions. " + "Bright clean beige carpet on the floor. Warm afternoon light. " + "No people. No machines. No equipment. Photorealistic." + ), +] + +MODEL = "veo-3.1-generate-preview" +out_path = os.path.join(VID_DIR, "v3-shot-04.mp4") + +def poll(op, timeout=600): + elapsed = 0 + while not op.done: + if elapsed >= timeout: + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + op = client.operations.get(op) + return op + +saved = False +for i, prompt in enumerate(PROMPTS): + print(f"\n[v3-shot-04] attempt {i+1}...") + try: + op = client.models.generate_videos( + model=MODEL, + prompt=prompt, + config=types.GenerateVideosConfig( + aspect_ratio="16:9", resolution="720p", + duration_seconds=6, number_of_videos=1, + ), + ) + op = poll(op) + if op and op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + video_bytes = client.files.download(file=vid) + if video_bytes: + with open(out_path, "wb") as f: + f.write(video_bytes) + print(f" Saved ({os.path.getsize(out_path)//1024}KB)") + saved = True + break + else: + print(f" No video returned") + except Exception as e: + print(f" Error: {e}") + +if saved: + clips = [ + "v3-shot-01", "v3-shot-02", "v3-shot-03", "v3-shot-04", + "v3-shot-05", "v3-shot-06", "v3-shot-07", + ] + missing = [n for n in clips if not os.path.exists(os.path.join(VID_DIR, f"{n}.mp4"))] + if missing: + print(f"Skipping reconcat — missing: {missing}") + else: + concat_file = os.path.join(VID_DIR, "concat-v3.txt") + with open(concat_file, "w") as f: + for n in clips: + f.write(f"file '{os.path.join(VID_DIR, n)}.mp4'\n") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" Reel saved ({os.path.getsize(REEL_OUT)//1024}KB)") + else: + print(f" ffmpeg error: {result.stderr[-300:]}") +else: + print("Failed — shot-04 unchanged.") + +print("\nDone.") diff --git a/tools/regen-v3.py b/tools/regen-v3.py new file mode 100644 index 0000000..3f3cb1a --- /dev/null +++ b/tools/regen-v3.py @@ -0,0 +1,153 @@ +""" +Hero reel v3 — 7 shots with corrected prompts. +""" +import os, sys, time, subprocess + +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +os.makedirs(VID_DIR, exist_ok=True) +client = genai.Client(api_key=API_KEY) + +SHOTS = [ + { + "name": "v3-shot-01", + "prompt": ( + "Medium shot. A family of four — two adults and two children — walks through the front door " + "of a warm residential home. Camera is inside the home facing them as they enter. " + "After they walk in, the camera slowly pans down to the carpet, revealing dirty footprints " + "and mud tracked onto the beige carpet. Warm natural light. Photorealistic." + ), + }, + { + "name": "v3-shot-02", + "prompt": ( + "Medium shot, slow zoom in. A glass of red wine has spilled onto a light grey upholstered sofa. " + "The camera starts wide on the sofa then slowly zooms in on the dark wine stain spreading " + "across the fabric. The stain is clearly visible, soaked into the cushion. " + "Warm living room light. No people. Photorealistic." + ), + }, + { + "name": "v3-shot-03", + "prompt": ( + "Medium shot at low angle, camera near floor level. Inside a bright commercial office entryway. " + "A person wearing work boots walks through the entry door toward the camera and past it. " + "The camera is low, showing the boots and lower legs as they walk across the carpet past the lens. " + "Office carpet visible, natural light from glass doors behind. Photorealistic." + ), + }, + { + "name": "v3-shot-04", + "prompt": ( + "Close-up cinematic shot. A technician's gloved hand holds a small upholstery extraction wand — " + "a flat rectangular handheld tool. The technician presses the wand firmly onto a wine-stained " + "sofa cushion and draws it slowly across the fabric. The stain visibly lifts as the wand moves. " + "Suction only — no water sprays out. Slow motion. Natural light. Photorealistic." + ), + }, + { + "name": "v3-shot-05", + "prompt": ( + "Wide cinematic shot. Camera slowly pans across the entrance of a modern commercial office building. " + "Clean grey commercial carpet stretches across the lobby floor. Large windows, glass doors, " + "professional lighting. The carpet looks freshly cleaned and spotless. " + "No people. No machines. Photorealistic." + ), + }, + { + "name": "v3-shot-06", + "prompt": ( + "Wide cinematic shot. Camera slowly pans across a bright, clean residential living room. " + "Plush clean beige carpet throughout. Comfortable furniture — sofa, armchairs, coffee table. " + "Warm natural afternoon light through large windows. The room looks fresh and inviting. " + "No people. No cleaning equipment. Photorealistic." + ), + }, + { + "name": "v3-shot-07", + "prompt": ( + "Wide cinematic shot. Camera moves slowly forward through an upscale restaurant dining room. " + "Rich carpet covers the floor. White tablecloths, warm ambient lighting, wood accents. " + "The carpet looks clean, deep, and well-maintained as the camera glides through the space. " + "No people. Photorealistic, luxurious atmosphere." + ), + }, +] + +MODEL = "veo-3.1-generate-preview" + +def poll(op, timeout=600): + elapsed = 0 + while not op.done: + if elapsed >= timeout: + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + op = client.operations.get(op) + return op + +saved = [] +for i, item in enumerate(SHOTS): + out_path = os.path.join(VID_DIR, f"{item['name']}.mp4") + print(f"\n[{i+1}/{len(SHOTS)}] {item['name']}...") + try: + op = client.models.generate_videos( + model=MODEL, + prompt=item["prompt"], + config=types.GenerateVideosConfig( + aspect_ratio="16:9", resolution="720p", + duration_seconds=6, number_of_videos=1, + ), + ) + op = poll(op) + if op and op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + video_bytes = client.files.download(file=vid) + if video_bytes: + with open(out_path, "wb") as f: + f.write(video_bytes) + print(f" Saved ({os.path.getsize(out_path)//1024}KB)") + saved.append(out_path) + else: + try: + vid.save(out_path) + print(f" Saved via .save()") + saved.append(out_path) + except Exception as e2: + print(f" Download failed: {e2}") + else: + print(f" No video returned") + except Exception as e: + print(f" Error: {e}") + +print(f"\n{len(saved)}/{len(SHOTS)} shots saved") + +if len(saved) >= 2: + concat_file = os.path.join(VID_DIR, "concat-v3.txt") + clips = [os.path.join(VID_DIR, f"{s['name']}.mp4") for s in SHOTS + if os.path.exists(os.path.join(VID_DIR, f"{s['name']}.mp4"))] + with open(concat_file, "w") as f: + for p in clips: + f.write(f"file '{p}'\n") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" Reel saved ({os.path.getsize(REEL_OUT)//1024}KB)") + else: + print(f" ffmpeg error: {result.stderr[-300:]}") + +print("\nDone.") diff --git a/tools/regen-v4.py b/tools/regen-v4.py new file mode 100644 index 0000000..b5e1288 --- /dev/null +++ b/tools/regen-v4.py @@ -0,0 +1,154 @@ +"""Hero reel v4 — 6 precise shots.""" +import os, sys, time, subprocess + +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +os.makedirs(VID_DIR, exist_ok=True) +client = genai.Client(api_key=API_KEY) + +SHOTS = [ + { + "name": "v4-shot-01", + "prompt": ( + "Medium cinematic shot. A family — two adults and two children — walks through a front door " + "into a residential home. The camera follows their feet as they step onto the beige carpet " + "in the entryway, then slowly pans down to show their shoes leaving dirty tracks on the carpet. " + "Warm afternoon light. Photorealistic, slow motion." + ), + }, + { + "name": "v4-shot-02", + "prompt": ( + "Slow-motion close-up cinematic shot. A wine glass tips over on a light grey fabric sofa cushion. " + "Red wine pours out of the glass and spreads across the sofa cushion, soaking into the fabric. " + "The dark red stain expands slowly across the grey upholstery. " + "Warm living room light. No people visible. Photorealistic, dramatic slow motion." + ), + }, + { + "name": "v4-shot-03", + "prompt": ( + "Cinematic close-up shot slowly pushing in on a section of heavily soiled residential carpet. " + "The beige carpet has multiple visible stains — dark spots, discoloration, pet stains, " + "and general dirt buildup embedded in the fibers. " + "Dramatic side lighting emphasizes the depth of the stains. No people. No equipment. Photorealistic." + ), + }, + { + "name": "v4-shot-04", + "prompt": ( + "Close-up cinematic shot. A carpet cleaning technician in a plain black shirt pushes " + "a large upright carpet cleaning machine — like a Rug Doctor — across a dirty beige carpet. " + "Tight shot focused on the wide flat cleaning head at the base of the machine pressing against " + "the carpet and moving forward. The carpet behind the machine looks visibly cleaner and brighter. " + "The machine only pulls dirt and moisture INTO itself — nothing comes out. " + "No steam. No liquid leaving the machine. Photorealistic slow motion." + ), + }, + { + "name": "v4-shot-05", + "prompt": ( + "Wide cinematic shot slowly pushing forward through the main entrance of a modern commercial " + "office building. Clean grey carpet covers the entire lobby floor. Glass doors, white walls, " + "professional overhead lighting. The carpet is the visual centerpiece — clean, uniform, well-maintained. " + "No people. No machines. Photorealistic." + ), + }, + { + "name": "v4-shot-06", + "prompt": ( + "Cinematic wide shot inside a bright clean residential living room. " + "The camera slowly pans upward from the clean plush beige carpet to reveal the whole room. " + "A family — two adults and a child — walks in at different moments and relaxes on the sofa. " + "Everyone is wearing socks. The carpet is spotless and fluffy. Warm natural light. " + "Comfortable, inviting atmosphere. Photorealistic." + ), + }, +] + +MODELS = ["veo-3.1-generate-preview", "veo-2.0-generate-001"] + +def poll(op, timeout=600): + elapsed = 0 + while not op.done: + if elapsed >= timeout: + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + op = client.operations.get(op) + return op + +saved = [] +for i, item in enumerate(SHOTS): + out_path = os.path.join(VID_DIR, f"{item['name']}.mp4") + print(f"\n[{i+1}/{len(SHOTS)}] {item['name']}...") + done = False + for model in MODELS: + try: + print(f" Trying {model}...") + op = client.models.generate_videos( + model=model, + prompt=item["prompt"], + config=types.GenerateVideosConfig( + aspect_ratio="16:9", resolution="720p", + duration_seconds=6, number_of_videos=1, + ), + ) + op = poll(op) + if op and op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + video_bytes = client.files.download(file=vid) + if video_bytes: + with open(out_path, "wb") as f: + f.write(video_bytes) + print(f" Saved ({os.path.getsize(out_path)//1024}KB)") + saved.append(out_path) + done = True + break + else: + try: + vid.save(out_path) + print(f" Saved via .save()") + saved.append(out_path) + done = True + break + except Exception as e2: + print(f" Download failed: {e2}") + else: + print(f" No video returned from {model}") + except Exception as e: + print(f" Error with {model}: {e}") + if not done: + print(f" FAILED: {item['name']}") + +print(f"\n{len(saved)}/{len(SHOTS)} shots saved") + +if len(saved) >= 2: + clips = [os.path.join(VID_DIR, f"{s['name']}.mp4") for s in SHOTS + if os.path.exists(os.path.join(VID_DIR, f"{s['name']}.mp4"))] + concat_file = os.path.join(VID_DIR, "concat-v4.txt") + with open(concat_file, "w") as f: + for p in clips: + f.write(f"file '{p}'\n") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" Reel saved ({os.path.getsize(REEL_OUT)//1024}KB)") + else: + print(f" ffmpeg error: {result.stderr[-300:]}") + +print("\nDone.") diff --git a/tools/regen-veo31.py b/tools/regen-veo31.py new file mode 100644 index 0000000..67b2377 --- /dev/null +++ b/tools/regen-veo31.py @@ -0,0 +1,167 @@ +""" +Regenerate carpet extraction shots using Veo 3.1. +Fixes: buffer/rotary/steamer machines replaced with explicit stand-up extractor descriptions. +Reconcats hero-reel.mp4 when done. +Run: python3 tools/regen-veo31.py +""" +import os, sys, time, subprocess + +try: + from google import genai + from google.genai import types +except ImportError: + os.system(f"{sys.executable} -m pip install google-genai --quiet") + from google import genai + from google.genai import types + +API_KEY = os.environ.get("GEMINI_API_KEY", "AIzaSyB_1p8KvaT_rdNJGPs8HKk8bKsvUlcL6Kg") +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +VID_DIR = os.path.join(BASE_DIR, "assets", "videos", "hero", "clips") +REEL_OUT = os.path.join(BASE_DIR, "assets", "videos", "hero", "hero-reel.mp4") +client = genai.Client(api_key=API_KEY) + +# Machine description block used across all shots +MACHINE = ( + "The carpet cleaning machine looks exactly like a Rug Doctor or Bissell Big Green carpet cleaner — " + "the large upright machines you rent at grocery stores. Tall rectangular body, upright handle, " + "wide flat cleaning foot at the base. It is pushed forward in straight lines like a vacuum cleaner. " + "NOT a floor buffer. NOT a rotary disc scrubber. NOT a steam cleaner. NOT circular. " + "NO spinning parts. NO steam. NO water spraying out. The carpet is cleaned by suction alone." +) + +SHOTS = [ + { + "name": "shot-04-extraction-carpet", + "prompt": ( + f"Cinematic slow-motion wide shot. A man pushes a Rug Doctor style carpet cleaning machine " + f"steadily forward across a beige residential carpet in a living room. {MACHINE} " + f"The carpet behind the machine transitions from dirty and matted to bright, clean, and fluffy. " + f"Warm natural room light. Photorealistic professional video." + ), + }, + { + "name": "shot-05-extraction-couch", + "prompt": ( + "Close-up cinematic slow-motion shot. A professional technician's gloved hand holds a small " + "flat handheld upholstery cleaning attachment — a rectangular suction wand, flat on the bottom, " + "no moving parts, no spinning disc. " + "The technician presses it firmly against a dirty grey sofa cushion and slides it slowly across. " + "The fabric visibly brightens as the wand moves across it. " + "NO rotary machine anywhere in frame. NO spinning. NO steam. NO water pouring out. " + "Natural light. Photorealistic." + ), + }, + { + "name": "shot-06-extraction-stairs", + "prompt": ( + "Cinematic slow-motion shot. A professional technician pushes a Rug Doctor style upright carpet " + "cleaning machine up a carpeted residential staircase, one stair tread at a time. " + "The machine is a tall rectangular upright unit with a handle and flat cleaning head — " + "NOT a buffer, NOT circular, NOT rotating. " + "Each stair tread brightens and looks freshly cleaned as the machine passes over it. " + "NO steam, NO water visible. Warm interior light. Photorealistic." + ), + }, + { + "name": "shot-09-technician-unloading", + "prompt": ( + f"Wide cinematic shot. A professional carpet cleaning technician in a plain black shirt " + f"rolls a Rug Doctor style carpet cleaning machine down a ramp out of a white service van " + f"parked in a residential driveway in upstate New York. {MACHINE} " + f"Autumn trees in background, bright daylight. " + f"Technician shown from the side or behind, no face visible. Photorealistic." + ), + }, +] + +MODEL = "veo-3.1-generate-preview" + +def poll(op, timeout=600): + elapsed = 0 + while not op.done: + if elapsed >= timeout: + print(f" Timed out after {timeout}s.") + return None + print(f" Waiting... ({elapsed}s)") + time.sleep(15) + elapsed += 15 + op = client.operations.get(op) + return op + +saved = [] +for item in SHOTS: + out_path = os.path.join(VID_DIR, f"{item['name']}.mp4") + print(f"\n[VID] Generating {item['name']} with {MODEL}...") + try: + op = client.models.generate_videos( + model=MODEL, + prompt=item["prompt"], + config=types.GenerateVideosConfig( + aspect_ratio="16:9", + resolution="720p", + duration_seconds=6, + number_of_videos=1, + ), + ) + op = poll(op) + if op is None: + print(f" FAILED (timeout): {item['name']}") + continue + if op.response and op.response.generated_videos: + vid = op.response.generated_videos[0].video + # Veo 3.1 download pattern + video_bytes = client.files.download(file=vid) + if video_bytes: + with open(out_path, "wb") as f: + f.write(video_bytes) + print(f" Saved {out_path} ({os.path.getsize(out_path)//1024}KB)") + saved.append(item["name"]) + else: + # fallback: try .save() method + try: + vid.save(out_path) + print(f" Saved via .save() ({os.path.getsize(out_path)//1024}KB)") + saved.append(item["name"]) + except Exception as e2: + print(f" Download failed: {e2}") + else: + print(f" No video returned for {item['name']}") + except Exception as e: + print(f" Error: {e}") + +print(f"\n{len(saved)}/{len(SHOTS)} shots saved: {saved}") + +# Reconcat full reel +ORDER = [ + "shot-01-door-opens-trimmed", + "shot-02-pan-to-stains", + "shot-03-stain-closeup", + "shot-04-extraction-carpet", + "shot-05-extraction-couch", + "shot-06-extraction-stairs", + "shot-07-office-entryway", + "shot-08-showroom", + "shot-09-technician-unloading", +] + +missing = [n for n in ORDER if not os.path.exists(os.path.join(VID_DIR, f"{n}.mp4"))] +if missing: + print(f"\nSkipping reconcat — missing clips: {missing}") +else: + print("\nReconcatenating hero-reel.mp4...") + concat_file = os.path.join(VID_DIR, "concat.txt") + with open(concat_file, "w") as f: + for name in ORDER: + f.write(f"file '{os.path.join(VID_DIR, name)}.mp4'\n") + result = subprocess.run( + ["ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", concat_file, + "-c:v", "libx264", "-crf", "22", "-preset", "fast", + "-movflags", "+faststart", REEL_OUT], + capture_output=True, text=True + ) + if result.returncode == 0: + print(f" Reel saved: {REEL_OUT} ({os.path.getsize(REEL_OUT)//1024}KB)") + else: + print(f" ffmpeg error: {result.stderr[-400:]}") + +print("\nDone.")