- Stealth mode: playwright-stealth, random fingerprints, human delays - Retry logic: exponential backoff (3 attempts) - Logging: rotating logs to /root/.hermes/logs/gmb/ - Validation: phone/website/rating validation + dedup - Pain detection: 12 signals, scoring, service matching - Review scraper: extract reviews + pain keyword detection - Website health: SSL, speed, mobile, contact form checks - Pitch generator: Apex pitches (SMS, email, call, Gumtree) - Docker containerization - .env for secrets (no hardcoded API keys) - Integration with Pipecat voice dialer (gmb_to_voice.py)
276 lines
11 KiB
Python
276 lines
11 KiB
Python
"""
|
|
Apex Pitch Generator Module
|
|
============================
|
|
Generate personalized cold outreach pitches based on pain signals.
|
|
Focus: Lead Generation as highest-margin service.
|
|
"""
|
|
|
|
from .logger import get_logger
|
|
|
|
|
|
# Pitch templates by pain signal
|
|
PITCH_TEMPLATES = {
|
|
'missed_calls': {
|
|
'hook': "I noticed {count} recent reviews mentioning people couldn't reach {business} by phone",
|
|
'problem': "Every missed call is a potential customer going to your competitor",
|
|
'solution': "I help businesses like yours capture every lead with smart call routing and instant follow-up",
|
|
'proof': "My last client recovered $12K/month in lost leads within 30 days",
|
|
'cta': "Can I show you how in a quick 10-minute call?",
|
|
},
|
|
'no_website': {
|
|
'hook': "I noticed {business} doesn't have a website yet",
|
|
'problem': "In 2026, 87% of customers search online before choosing a local business",
|
|
'solution': "I build fast, mobile-friendly websites that actually generate leads (not just look pretty)",
|
|
'proof': "Average client sees 15-20 new inquiries per month within 60 days",
|
|
'cta': "Want to see some examples of sites I've built for {industry} businesses?",
|
|
},
|
|
'broken_website': {
|
|
'hook': "I checked {business}'s website and noticed {issue}",
|
|
'problem': "This is likely costing you customers right now — Google penalizes broken sites in search rankings",
|
|
'solution': "I can fix this in 48 hours and get you back in Google's good books",
|
|
'proof': "Fixed 23 sites this year with avg 40% traffic increase within 2 weeks",
|
|
'cta': "Want me to send you a quick video showing exactly what's broken?",
|
|
},
|
|
'low_rating': {
|
|
'hook': "I noticed {business} has a {rating}★ rating with some concerning recent reviews",
|
|
'problem': "Anything under 4 stars is actively pushing customers to competitors",
|
|
'solution': "I help businesses rebuild their online reputation and respond professionally to negative reviews",
|
|
'proof': "Took a Joondalup dentist from 3.2★ to 4.6★ in 90 days with zero fake reviews",
|
|
'cta': "Can I share the exact system I use?",
|
|
},
|
|
'recent_1star': {
|
|
'hook': "I saw {business} got {count} one-star reviews in the last month",
|
|
'problem': "Unaddressed negative reviews stay on Google forever and scare away new customers",
|
|
'solution': "I help business owners respond professionally and turn critics into advocates",
|
|
'proof': "One client recovered from 8 bad reviews to 4.8★ rating in 60 days",
|
|
'cta': "Want to see the response templates that actually work?",
|
|
},
|
|
'unclaimed_gmb': {
|
|
'hook': "I noticed {business}'s Google Business profile appears unclaimed",
|
|
'problem': "Unclaimed profiles can't be optimized, so you're missing out on free local search traffic",
|
|
'solution': "I can claim and optimize your profile in 24 hours — it's the easiest SEO win available",
|
|
'proof': "Optimized profiles typically see 30-50% more calls within 30 days",
|
|
'cta': "Want me to walk you through the process?",
|
|
},
|
|
'few_reviews': {
|
|
'hook': "I noticed {business} only has {count} reviews on Google",
|
|
'problem': "Businesses with fewer than 20 reviews are invisible to most customers",
|
|
'solution': "I run ethical review generation campaigns that get real customers to leave real reviews",
|
|
'proof': "One client went from 12 to 87 reviews in 90 days — all genuine",
|
|
'cta': "Want to see the system I use?",
|
|
},
|
|
'no_contact_form': {
|
|
'hook': "I noticed {business}'s website doesn't have a contact form",
|
|
'problem': "You're relying 100% on phone calls, which means you're missing 60% of leads who prefer to fill forms",
|
|
'solution': "I add smart contact forms that capture leads 24/7 and send instant SMS notifications",
|
|
'proof': "Added forms to 15 sites this quarter — average 22 new leads/month per site",
|
|
'cta': "Can I mock up what it would look like on your site?",
|
|
},
|
|
'slow_website': {
|
|
'hook': "I tested {business}'s website and it took {load_time} seconds to load",
|
|
'problem': "Google's threshold is 3 seconds — anything slower loses 40% of visitors instantly",
|
|
'solution': "I optimize websites to load in under 2 seconds without rebuilding them",
|
|
'proof': "Average optimization takes 4 hours and improves load time by 60%",
|
|
'cta': "Want me to send you a speed report with specific fixes?",
|
|
},
|
|
'not_mobile_friendly': {
|
|
'hook': "I checked {business}'s website on my phone and it's not mobile-friendly",
|
|
'problem': "78% of local searches happen on mobile — Google actually hides non-mobile sites from phone users",
|
|
'solution': "I make existing websites mobile-friendly without a full rebuild",
|
|
'proof': "Mobile optimization typically recovers 30-40% of lost mobile traffic",
|
|
'cta': "Want me to show you what your site looks like on a phone right now?",
|
|
},
|
|
}
|
|
|
|
# Service pricing (for context, not mentioned in pitch)
|
|
SERVICE_PRICING = {
|
|
'Lead Generation + Call Tracking': {'setup': '$1,500', 'monthly': '$500/mo'},
|
|
'Website Development': {'setup': '$1,500-$3,000', 'monthly': '$150/mo hosting'},
|
|
'Website Maintenance': {'setup': '$500', 'monthly': '$300/mo'},
|
|
'Reputation Management': {'setup': '$800', 'monthly': '$400/mo'},
|
|
'Review Response Service': {'setup': '$300', 'monthly': '$200/mo'},
|
|
'GMB Optimization': {'setup': '$500', 'monthly': '$150/mo'},
|
|
'Review Generation Campaign': {'setup': '$500', 'monthly': '$300/mo'},
|
|
'Lead Capture Optimization': {'setup': '$600', 'monthly': '$100/mo'},
|
|
'Website Performance': {'setup': '$400', 'monthly': '$0'},
|
|
'Mobile Optimization': {'setup': '$500', 'monthly': '$0'},
|
|
}
|
|
|
|
|
|
def generate_apex_pitch(lead, pain_data, channel='sms'):
|
|
"""
|
|
Generate a personalized apex pitch for a lead.
|
|
|
|
Args:
|
|
lead: Business data dictionary
|
|
pain_data: Pain detection results from detect_pain_signals()
|
|
channel: 'sms', 'email', 'call', or 'gumtree'
|
|
|
|
Returns:
|
|
Dictionary with pitch components
|
|
"""
|
|
logger = get_logger()
|
|
|
|
if not pain_data or not pain_data.get('signals'):
|
|
return None
|
|
|
|
# Get primary signal (highest pain)
|
|
signals = pain_data['signals']
|
|
primary_key = max(signals.keys(), key=lambda k: signals[k].get('signal_info', {}).get('weight', 0))
|
|
primary_signal = signals[primary_key]
|
|
|
|
# Get template
|
|
template = PITCH_TEMPLATES.get(primary_key)
|
|
if not template:
|
|
# Fallback to generic
|
|
template = {
|
|
'hook': f"I noticed {lead.get('name', 'your business')} has some opportunities to improve online presence",
|
|
'problem': "These issues are likely costing you customers every day",
|
|
'solution': "I help local businesses fix these problems and generate more leads",
|
|
'proof': "Working with Perth businesses for 5+ years",
|
|
'cta': "Can I show you how?",
|
|
}
|
|
|
|
# Build context
|
|
context = {
|
|
'business': lead.get('name', 'your business'),
|
|
'industry': lead.get('category', 'local'),
|
|
'rating': lead.get('rating', 0),
|
|
'count': primary_signal.get('count', 1),
|
|
'load_time': '',
|
|
'issue': '',
|
|
}
|
|
|
|
# Add website-specific context
|
|
if 'slow_website' in signals:
|
|
details = signals['slow_website'].get('details', {})
|
|
context['load_time'] = f"{details.get('load_time', 4)}"
|
|
|
|
if 'broken_website' in signals:
|
|
details = signals['broken_website'].get('details', {})
|
|
issues = details.get('issues', [])
|
|
context['issue'] = issues[0] if issues else "some technical issues"
|
|
|
|
# Fill template
|
|
try:
|
|
hook = template['hook'].format(**context)
|
|
problem = template['problem'].format(**context)
|
|
solution = template['solution'].format(**context)
|
|
proof = template['proof'].format(**context)
|
|
cta = template['cta'].format(**context)
|
|
except KeyError as e:
|
|
logger.warning(f"Missing context for pitch template: {e}")
|
|
hook = f"I've been looking at {lead.get('name', 'your business')} online"
|
|
problem = template['problem']
|
|
solution = template['solution']
|
|
proof = template['proof']
|
|
cta = template['cta']
|
|
|
|
# Format for channel
|
|
if channel == 'sms':
|
|
# Short, punchy, under 160 chars ideally (but up to 320 OK)
|
|
pitch = f"{hook}. {cta}"
|
|
if len(pitch) > 160:
|
|
pitch = f"{hook[:80]}... {cta}"
|
|
|
|
elif channel == 'email':
|
|
# Full pitch with all components
|
|
pitch = f"""Hi,
|
|
|
|
{hook}.
|
|
|
|
{problem}.
|
|
|
|
{solution}. {proof}.
|
|
|
|
{cta}
|
|
|
|
Cheers,
|
|
Zul
|
|
Darwisyah Digital Media
|
|
0405 022 460"""
|
|
|
|
elif channel == 'call':
|
|
# Conversational script
|
|
pitch = f"""OPENING:
|
|
"Hi, is this {lead.get('name', 'the business')}? This is Zul — I'm a local business owner in Perth. I'll be quick.
|
|
|
|
{hook}. Is that something you've noticed yourself?"
|
|
|
|
PROBE:
|
|
"How has that been affecting your business?"
|
|
|
|
PITCH:
|
|
"{solution}. {proof}."
|
|
|
|
CLOSE:
|
|
"{cta}"
|
|
|
|
OBJECTION HANDLING:
|
|
- "Not interested": "Totally understand. Can I send you a quick 2-minute video showing what I found? No pressure either way."
|
|
- "How much?": "Depends on what you need — happy to give you a ballpark if you tell me more about what's not working."
|
|
- "Send info": "Will do — what's the best email? And quick question — what's your biggest challenge right now with [problem area]?"
|
|
"""
|
|
|
|
elif channel == 'gumtree':
|
|
# Casual, local tone
|
|
pitch = f"""Hi there,
|
|
|
|
I came across {lead.get('name', 'your business')} online and noticed {hook.lower()}.
|
|
|
|
{problem}.
|
|
|
|
I'm Zul, a local Perth guy who helps businesses fix exactly these kinds of issues. {solution}. {proof}.
|
|
|
|
{cta}
|
|
|
|
Happy to chat — no hard sell.
|
|
|
|
Cheers,
|
|
Zul
|
|
0405 022 460"""
|
|
|
|
else:
|
|
pitch = f"{hook}. {problem}. {solution}. {proof}. {cta}"
|
|
|
|
result = {
|
|
'pitch': pitch,
|
|
'channel': channel,
|
|
'primary_service': pain_data.get('primary_service'),
|
|
'pain_score': pain_data.get('pain_score'),
|
|
'hook': hook,
|
|
'problem': problem,
|
|
'solution': solution,
|
|
'proof': proof,
|
|
'cta': cta,
|
|
'pricing': SERVICE_PRICING.get(pain_data.get('primary_service'), {}),
|
|
}
|
|
|
|
logger.info(f"Generated {channel} pitch for '{lead.get('name')}': pain_score={pain_data.get('pain_score')}")
|
|
|
|
return result
|
|
|
|
|
|
def generate_batch_pitches(leads_with_pain, channel='sms'):
|
|
"""
|
|
Generate pitches for multiple leads.
|
|
|
|
Args:
|
|
leads_with_pain: List of (lead, pain_data) tuples
|
|
channel: Pitch channel
|
|
|
|
Returns:
|
|
List of pitch dictionaries
|
|
"""
|
|
pitches = []
|
|
|
|
for lead, pain_data in leads_with_pain:
|
|
if pain_data and pain_data.get('pain_score', 0) > 0:
|
|
pitch = generate_apex_pitch(lead, pain_data, channel)
|
|
if pitch:
|
|
pitches.append({
|
|
'lead': lead,
|
|
'pitch': pitch,
|
|
})
|
|
|
|
return pitches
|