Skill: shawnshenopeninterx/creator-screening (version 1.1.0)

--- name: creator-screening description: Screen and evaluate social media creators/influencers using configurable quality frameworks. Analyzes Instagram, TikTok, YouTube creators using Memories.ai V2 Video Understanding API — fetches profile metadata, runs MAI visual+audio AI analysis on videos, and scores against production quality, audio, delivery, and positioning criteria. Use when asked to vet creators, screen influencers, evaluate content quality, or generate creator reports. metadata: openclaw: emoji: "🎯" --- # Creator Screening Skill Automated creator/influencer screening powered by **Memories.ai V2 Video Understanding API**. ## Parameters | Parameter | Default | Description | |-----------|---------|-------------| | `videos_per_creator` | 5 | Number of top videos to analyze per creator | | `video_seconds` | 30 | First N seconds of each video to analyze | | `platforms` | instagram | Supported: instagram, tiktok, youtube | | `analysis_mode` | `mai` | `mai` (visual+audio AI analysis) or `transcript` (audio-only fallback) | | `framework` | default | Screening framework to apply. See `references/frameworks/` | | `output_format` | discord | `discord`, `pdf` (Google Doc→PDF), or `json` | | `batch_size` | 10 | Max creators per batch run | ## Quick Start ``` Screen these creators: @anshmehra.in, @nishkarshsharmaa Parameters: videos_per_creator=3, framework=cac-crusher ``` ## Workflow ### Step 1: Parse Input Accept creator URLs in any format: - Profile: `https://www.instagram.com/username/` - Individual reel: `https://www.instagram.com/reel/SHORTCODE/` - YouTube: `https://www.youtube.com/@channel` or `/shorts/ID` ### Step 2: Get Profile & Video Metadata **Memories.ai V2**: `POST /instagram/video/metadata` ```bash python3 scripts/scrape_profiles.py --urls "reel_url1,reel_url2" --channel rapid ``` Returns per video ($0.01/video): - **Owner profile**: username, full_name, followers, verified, profile_pic - **Video stats**: views, play_count, duration, caption, comments, dimensions, audio info ### Step 3: Video Understanding (MAI) **Memories.ai V2 MAI**: `POST /instagram/video/mai/transcript` This is the core analysis step. MAI provides: - **Visual scene descriptions**: lighting quality, framing, environment, clothing, production value - **Audio transcription**: speech-to-text with timestamps - **Content understanding**: topic classification, delivery style, structure ```bash python3 scripts/analyze_videos.py --mode mai --videos_per_creator 5 --urls "url1,url2" ``` Each video returns visual + audio AI analysis. Use this to evaluate: - Section 2.1: Look & Feel (lighting, environment, framing) — from **visual scenes** - Section 2.2: Audio Quality (clarity, echo, consistency) — from **audio analysis** - Section 3.x: Delivery & Content (structure, fluency, maturity) — from **transcript text** - Section 4: Positioning (tone, energy, brand safety) — from **combined analysis** **Fallback**: If MAI is unavailable, use `--mode transcript` for audio-only analysis: ```bash python3 scripts/analyze_videos.py --mode transcript --videos_per_creator 5 --urls "url1,url2" ``` ### Step 4: Apply Framework Score against the selected screening framework: ```bash python3 scripts/score_creator.py --framework cac-crusher --profile profile.json --transcripts transcripts.json ``` Frameworks live in `references/frameworks/`: - `cac-crusher.md` — CAC Crusher Creator Screening Framework (Talking Head + Skit categories) - `default.md` — Generic quality screening (5 dimensions, weighted scoring) - `template.md` — Template for creating custom frameworks ### Step 5: Generate Report Output per-creator screening cards with: - Profile stats (followers, verified, engagement) - Per-section scores (PASS/FAIL/FLAG) - Transcript excerpts as evidence - Visual quality notes from MAI - Final verdict (APPROVED / REJECTED / CONDITIONAL) ## Setup Set the following environment variables before use: ```bash export MEMORIES_API_KEY="your-memories-ai-v2-api-key" # Required — Memories.ai V2 API key export APIFY_API_KEY="your-apify-key" # Optional — fallback scraper for profiles ``` Get your API key at https://api-tools.memories.ai ## Memories.ai V2 API Reference All endpoints use: `Authorization: <API_KEY>` header (no Bearer prefix). Base URL: `https://mavi-backend.memories.ai/serve/api/v2` | Endpoint | Method | Use | Cost | |----------|--------|-----|------| | `/{platform}/video/metadata` | POST | Profile + video stats | $0.01/video | | `/{platform}/video/mai/transcript` | POST | Visual + audio AI analysis | ~$0.11/video | | `/{platform}/video/transcript` | POST | Audio transcription only | ~$0.01/video | Platforms: `instagram`, `tiktok`, `youtube`, `twitter` **Request body**: `{"video_url": "...", "channel": "rapid"}` **MAI response**: `{"data": {"task_id": "..."}}` (async, results via webhook) **URL format**: Instagram must use `/reel/SHORTCODE/` (not `/p/` or `/reels/`) ## Error Handling - Add 0.5s delay between API calls - Retry failed requests once, then skip - If MAI webhook not received, fall back to transcript mode - Always normalize Instagram URLs: `/p/` → `/reel/`, `/reels/` → `/reel/`

path: references/frameworks/cac-crusher.md
content: # CAC Crusher — Creator Screening Framework

Source: Sankalp Tipatle ([REDACTED_SECRET]), Feb 27 2026

## 1. Creator Categories

**A. Talking Head Creators**: Seated, direct-to-camera, structured explanation / video essay
**B. Skit / Drama / Couple Creators**: Performance-based, dialogue-driven, individual or couple-led

## 2. Universal Production Standards (Non-Negotiable)

### 2.1 Look & Feel (Lighting, Environment, Clothing, Framing)

Overall visual must feel **modern, clean, intentional, premium** — comparable to tier-1 digital creators.

**PASS criteria:**
- Indoor controlled lighting
- Evenly lit face (no harsh shadows or blown highlights)
- Balanced, soft lighting
- Clean, well-maintained interiors
- No clutter or distracting elements
- Neat, intentional dress
- Well-groomed (hair, beard, makeup)
- Stable, centered, well-balanced framing
- Proper headroom and side spacing
- High-resolution, sharp footage
- Proper color balance

**FAIL criteria (any one = reject):**
- Dim, uneven, or poorly exposed lighting
- Rural/farm-like or visibly unpolished environments
- Muddy, damaged, stained, or distracting wall backgrounds
- Casually unkempt or poorly styled
- Wrinkled or careless clothing
- Extreme, distracting, or unnaturally colored hairstyles
- Blurry, grainy, or low-resolution cameras
- Awkward cropping or careless composition

### 2.2 Audio Quality

**PASS criteria:**
- Clear, crisp, distortion-free voice
- Balanced and consistent volume
- Minimal echo or room noise
- Clean microphone output

**FAIL criteria:**
- Muffled or boxy audio
- Echo-heavy or noisy environments
- Over-processed or distorted sound
- Inconsistent volume levels

## 3. Format-Specific Filters

### 3.1 Talking Head (Higher Maturity Standard)

**PASS criteria:**
- Appears mature and composed
- Speaks fluently with strong vocabulary
- Explains concepts clearly and confidently
- Steady delivery (minimal fumbling/stuttering)
- Natural, controlled hand gestures
- Sits and speaks directly to camera
- Centered, intentional framing
- Demonstrates confidence and structured thought

**FAIL criteria:**
- Visibly childish tone or expressions
- Exaggerated, meme-style, or theatrically loud delivery
- Frequent fumbling or rhythm breaking
- Lack of clarity in articulation
- Poor camera sharpness or color grading
- Standing casual monologue-style videos
- Background music with text overlays only (no speech)
- Silent reaction-style videos
- Trending audio without meaningful verbal explanation
- Lip-sync to audio

### 3.2 Skit / Drama / Couple

**PASS criteria:**
- Clear, intentional dialogues
- Music as background support (not primary content)
- Confident line delivery
- Strong screen presence
- Well-groomed and composed
- Structured, scenario-driven content

**FAIL criteria:**
- Primarily dance or lip-sync without dialogue
- Entirely dependent on trending music formats
- Exaggerated emotional drama reels
- Dramatic background music with over-the-top acting
- Theatrically immature
- Visibly unpolished or rural-style settings
- Low-resolution or unstable footage
- Trend-dependent, impulsive reels without structured narrative
- Lip-sync/reaction reels using devotional, motivational, or politically-charged audio as primary content

## 4. Positioning Standard (Core Intent)

Across all formats, creators must reflect:
- Maturity in tone
- Premium but relatable presence
- Structured communication
- Controlled energy (not chaotic or loud)
- Intentional production quality
- Dialogue-driven content (not passive trend mimicry)

**Reject signals:**
- Childlike energy
- Over-dramatic emotional storytelling
- Trend-chasing formats
- Loud theatrical exaggeration
- Rural or visibly low-production aesthetics

## 5. Final Screening Rule

A creator moves forward ONLY if:
1. Universal production standards are met
2. Format-specific standards are satisfied
3. Maturity and positioning standards align
4. No strong red flags exist

**If a single major red flag appears in lighting, environment, grooming, maturity, delivery, or audio quality — REJECT.**

## Scoring Template

For each creator, evaluate and score:

```
Profile: @username
Category: A (Talking Head) / B (Skit/Couple)
Followers: N | Verified: Y/N | Avg Views: N | Engagement: N%

Section 2.1 Look & Feel:    PASS / FAIL / FLAG — [notes]
Section 2.2 Audio Quality:   PASS / FAIL / FLAG — [notes]
Section 3.x Format-Specific: PASS / FAIL / FLAG — [notes]
Section 4 Positioning:       PASS / FAIL / FLAG — [notes]
Red Flags:                   None / [list]

VERDICT: APPROVED / REJECTED / CONDITIONAL
```


path: references/frameworks/default.md
content: # Default Creator Screening Framework

Generic quality screening for any brand partnership evaluation.

## Scoring Dimensions

### 1. Production Quality (Weight: 30%)
- **Lighting**: Studio/ring light vs natural/uncontrolled
- **Resolution**: 1080p+ vs sub-HD
- **Framing**: Centered, stable vs shaky, off-center
- **Environment**: Clean, intentional background vs cluttered/messy
- **Color**: Balanced color grading vs oversaturated/dull

Score: 1-5 (1=amateur, 5=broadcast quality)

### 2. Audio Quality (Weight: 20%)
- **Clarity**: External mic vs phone mic
- **Consistency**: Even volume vs fluctuating
- **Background**: Clean vs noisy/echo
- **Post-processing**: Professional vs raw

Score: 1-5

### 3. Content & Delivery (Weight: 25%)
- **Structure**: Organized, clear flow vs rambling
- **Confidence**: Composed vs nervous/fumbling
- **Vocabulary**: Strong, appropriate vs limited
- **Originality**: Original content vs trend copies
- **Value**: Educational/entertaining vs filler

Score: 1-5

### 4. Brand Safety (Weight: 15%)
- **Tone**: Professional vs controversial
- **Language**: Clean vs explicit
- **Topics**: Brand-safe vs risky
- **Associations**: Reputable vs questionable

Score: 1-5

### 5. Engagement & Reach (Weight: 10%)
- **Views-to-followers ratio**: >10% good, >50% great
- **Engagement rate**: >1% good, >3% great
- **Comment quality**: Real discussion vs spam/bots
- **Growth trend**: Rising vs stagnant vs declining

Score: 1-5

## Verdict Thresholds

- **APPROVED**: Weighted score >= 3.5/5
- **CONDITIONAL**: Weighted score 2.5-3.4
- **REJECTED**: Weighted score < 2.5


path: references/frameworks/template.md
content: # [Framework Name] — Creator Screening Framework

Source: [Client/Company name]

## Creator Categories
Define the content format categories relevant to this framework.

## Universal Standards
List non-negotiable production requirements (lighting, audio, etc.)

## Format-Specific Filters
For each category, define PASS and FAIL criteria.

## Positioning Standard
Define the brand alignment and tone requirements.

## Scoring Template
```
Profile: @username
Category: [format type]
[Dimension 1]: PASS / FAIL / FLAG — [notes]
[Dimension 2]: PASS / FAIL / FLAG — [notes]
...
VERDICT: APPROVED / REJECTED / CONDITIONAL
```


path: scripts/analyze_videos.py
content: #!/usr/bin/env python3
"""
Analyze creator videos via Memories.ai API.
Supports two modes:
  - transcript: Audio-only transcription (sync, fast ~2s/video)
  - mai: Full visual+audio AI analysis (async, ~30s/video, needs webhook)

Usage:
  python3 analyze_videos.py --mode transcript --videos_per_creator 5 --urls "url1,url2"
  python3 analyze_videos.py --mode mai --video_seconds 30 --urls "url1,url2"
"""
import argparse, json, urllib.request, sys, os, time

MAVI_KEY = os.environ.get('MEMORIES_API_KEY', '')
MAVI_BASE = 'https://mavi-backend.memories.ai/serve/api/v2'

def detect_platform(url):
    """Detect platform from URL."""
    if 'instagram.com' in url: return 'instagram'
    if 'tiktok.com' in url: return 'tiktok'
    if 'youtube.com' in url or 'youtu.be' in url: return 'youtube'
    return 'instagram'

def normalize_url(url):
    """Normalize Instagram URLs to /reel/ format."""
    if 'instagram.com' in url:
        url = url.replace('/p/', '/reel/')
        url = url.replace('/reels/', '/reel/')
    return url.strip()

def get_transcript(video_url):
    """Get audio transcript (sync, fast)."""
    platform = detect_platform(video_url)
    url = f'{MAVI_BASE}/{platform}/video/transcript'
    body = {'video_url': normalize_url(video_url), 'channel': 'rapid'}
    data = json.dumps(body).encode()
    req = urllib.request.Request(url, data=data, method='POST')
    req.add_header('Authorization', MAVI_KEY)
    req.add_header('Content-Type', 'application/json')
    
    try:
        resp = urllib.request.urlopen(req, timeout=120)
        result = json.loads(resp.read())
        if result.get('code') == '0000':
            d = result.get('data', {})
            transcripts = d.get('transcripts', [])
            if transcripts:
                return {'text': transcripts[0].get('text', ''), 'success': True}
            # YouTube format
            content = d.get('content', [])
            if content:
                full = ' '.join(c.get('text', '') for c in content)
                return {'text': full, 'success': True}
        return {'text': '', 'success': False, 'error': result.get('msg')}
    except Exception as e:
        return {'text': '', 'success': False, 'error': str(e)}

def submit_mai(video_url):
    """Submit for full MAI visual+audio analysis (async)."""
    platform = detect_platform(video_url)
    url = f'{MAVI_BASE}/{platform}/video/mai/transcript'
    body = {'video_url': normalize_url(video_url)}
    data = json.dumps(body).encode()
    req = urllib.request.Request(url, data=data, method='POST')
    req.add_header('Authorization', MAVI_KEY)
    req.add_header('Content-Type', 'application/json')
    
    try:
        resp = urllib.request.urlopen(req, timeout=120)
        result = json.loads(resp.read())
        if result.get('code') == '0000':
            return {'task_id': result['data']['task_id'], 'success': True}
        return {'task_id': None, 'success': False, 'error': result.get('msg')}
    except Exception as e:
        return {'task_id': None, 'success': False, 'error': str(e)}

def main():
    parser = argparse.ArgumentParser(description='Analyze creator videos')
    parser.add_argument('--mode', default='mai', choices=['transcript', 'mai'])
    parser.add_argument('--videos_per_creator', type=int, default=5)
    parser.add_argument('--video_seconds', type=int, default=30, help='Seconds to analyze (MAI mode)')
    parser.add_argument('--urls', required=True, help='Comma-separated video URLs')
    parser.add_argument('--output', default='-', help='Output file (- for stdout)')
    args = parser.parse_args()
    
    urls = [u.strip() for u in args.urls.split(',') if u.strip()]
    urls = urls[:args.videos_per_creator]
    
    results = []
    for url in urls:
        print(f"Analyzing: {url}", file=sys.stderr, flush=True)
        
        if args.mode == 'transcript':
            r = get_transcript(url)
            results.append({'url': url, 'mode': 'transcript', **r})
        else:
            r = submit_mai(url)
            results.append({'url': url, 'mode': 'mai', **r})
        
        time.sleep(1)  # Rate limiting
    
    output = json.dumps(results, indent=2, default=str)
    if args.output == '-':
        print(output)
    else:
        with open(args.output, 'w') as f:
            f.write(output)

if __name__ == '__main__':
    main()


path: scripts/score_creator.py
content: #!/usr/bin/env python3
"""
Score a creator against a screening framework.
Uses profile metrics + video transcripts to evaluate each framework dimension.

Usage:
  python3 score_creator.py --framework cac-crusher --profile profile.json --transcripts transcripts.json
"""
import argparse, json, sys

# Signals detected from transcript text
REJECT_SIGNALS = {
    'lip_sync': ['lip sync', 'lipsync', 'trending audio', 'trending sound'],
    'trend_dependent': ['tag your', 'mention your', 'comment and win', 'follow for'],
    'emotional_drama': ['surprise visit', 'wait for it', 'transformation', 'emotional'],
    'product_spam': ['comment goat', 'comment link', 'dm for', 'link in bio'],
    'devotional_political': ['bhajan', 'sermon', 'political', 'motivational speech'],
}

QUALITY_SIGNALS = {
    'structured': ['step 1', 'step 2', 'here\'s how', 'let me explain', 'in this video'],
    'educational': ['learn', 'course', 'training', 'tutorial', 'guide'],
    'professional': ['enterprise', 'business', 'strategy', 'advisor', 'expert'],
}

def detect_signals(text):
    """Detect quality and reject signals from transcript text."""
    text_lower = text.lower()
    
    found_reject = {}
    for signal, keywords in REJECT_SIGNALS.items():
        for kw in keywords:
            if kw in text_lower:
                found_reject[signal] = kw
                break
    
    found_quality = {}
    for signal, keywords in QUALITY_SIGNALS.items():
        for kw in keywords:
            if kw in text_lower:
                found_quality[signal] = kw
                break
    
    return found_reject, found_quality

def classify_category(profile, transcripts):
    """Classify creator into Talking Head or Skit/Couple."""
    bio = (profile.get('bio') or '').lower()
    captions = ' '.join(t.get('caption', '') for t in transcripts).lower()
    
    skit_signals = ['actor', 'couple', 'comedy', 'skit', 'drama', 'biwi', 'husband', 'wife']
    talking_head_signals = ['expert', 'advisor', 'coach', 'educator', 'ca ', 'mba', 'course', 'training', 'business']
    
    skit_score = sum(1 for s in skit_signals if s in bio or s in captions)
    th_score = sum(1 for s in talking_head_signals if s in bio or s in captions)
    
    return 'B (Skit/Couple)' if skit_score > th_score else 'A (Talking Head)'

def score_cac_crusher(profile, transcripts):
    """Score against CAC Crusher framework."""
    scores = {}
    red_flags = []
    
    # Combine all transcript text
    all_text = ' '.join(t.get('text', '') for t in transcripts if t.get('text'))
    reject_signals, quality_signals = detect_signals(all_text)
    
    category = classify_category(profile, transcripts)
    
    # 2.1 Look & Feel — scored via MAI visual analysis or inferred from metrics
    # Without MAI, we infer from engagement and follower quality
    followers = profile.get('followers', 0)
    verified = profile.get('verified', False)
    avg_views = profile.get('avgViews', 0)
    eng_rate = profile.get('engagementRate', 0)
    
    look_feel = 'PASS' if verified else 'FLAG'
    if avg_views < 1000 and followers > 50000:
        look_feel = 'FAIL'
        red_flags.append('Dead engagement — possible fake followers')
    scores['look_feel'] = look_feel
    
    # 2.2 Audio — inferred from transcript quality
    has_transcript = bool(all_text.strip())
    transcript_length = len(all_text)
    scores['audio'] = 'PASS' if has_transcript and transcript_length > 50 else 'FLAG'
    
    # 3.x Format-specific
    if 'Talking Head' in category:
        if quality_signals:
            scores['format'] = 'PASS'
        elif reject_signals:
            scores['format'] = 'FAIL'
            for sig, kw in reject_signals.items():
                red_flags.append(f'Talking Head reject: {sig} ("{kw}")')
        else:
            scores['format'] = 'FLAG'
    else:  # Skit
        if 'lip_sync' in reject_signals or 'trend_dependent' in reject_signals:
            scores['format'] = 'FAIL'
            red_flags.append(f'Skit reject: lip-sync/trend-dependent content')
        elif 'emotional_drama' in reject_signals:
            scores['format'] = 'FAIL'
            red_flags.append('Skit reject: exaggerated emotional drama')
        else:
            scores['format'] = 'FLAG'
    
    # 4. Positioning
    if quality_signals and not reject_signals:
        scores['positioning'] = 'PASS'
    elif reject_signals:
        scores['positioning'] = 'FAIL'
    else:
        scores['positioning'] = 'FLAG'
    
    # Final verdict
    fails = sum(1 for v in scores.values() if v == 'FAIL')
    flags = sum(1 for v in scores.values() if v == 'FLAG')
    
    if fails > 0:
        verdict = 'REJECTED'
    elif flags >= 2:
        verdict = 'CONDITIONAL'
    else:
        verdict = 'APPROVED'
    
    return {
        'username': profile.get('username', '?'),
        'category': category,
        'followers': followers,
        'verified': verified,
        'avgViews': avg_views,
        'engagementRate': eng_rate,
        'scores': scores,
        'redFlags': red_flags,
        'qualitySignals': list(quality_signals.keys()),
        'rejectSignals': list(reject_signals.keys()),
        'verdict': verdict,
    }

def main():
    parser = argparse.ArgumentParser(description='Score creator against framework')
    parser.add_argument('--framework', default='cac-crusher')
    parser.add_argument('--profile', required=True, help='Profile JSON file')
    parser.add_argument('--transcripts', required=True, help='Transcripts JSON file')
    parser.add_argument('--output', default='-')
    args = parser.parse_args()
    
    profile = json.load(open(args.profile))
    transcripts = json.load(open(args.transcripts))
    
    if args.framework == 'cac-crusher':
        result = score_cac_crusher(profile, transcripts)
    else:
        print(f"Framework {args.framework} not implemented", file=sys.stderr)
        sys.exit(1)
    
    output = json.dumps(result, indent=2, default=str)
    if args.output == '-':
        print(output)
    else:
        with open(args.output, 'w') as f:
            f.write(output)

if __name__ == '__main__':
    main()



path: scripts/scrape_profiles.py
content: #!/usr/bin/env python3
"""
Scrape Instagram creator profiles via Memories.ai Video Metadata API.
Falls back to Apify if APIFY_API_KEY is set and Memories.ai fails.

Usage: python3 scrape_profiles.py --usernames "user1,user2" --platform instagram
       python3 scrape_profiles.py --urls "https://instagram.com/reel/ABC,https://instagram.com/reel/DEF"
Output: JSON with profile data + video metrics for each creator.
"""
import argparse, json, urllib.request, sys, os, time

MAVI_KEY = os.environ.get('MEMORIES_API_KEY', '')
MAVI_BASE = 'https://mavi-backend.memories.ai/serve/api/v2'
APIFY_KEY = os.environ.get('APIFY_API_KEY', '')

def mavi_metadata(video_url, channel='rapid'):
    """Get video metadata + owner profile via Memories.ai."""
    url = f'{MAVI_BASE}/instagram/video/metadata'
    body = {'video_url': video_url, 'channel': channel}
    data = json.dumps(body).encode()
    req = urllib.request.Request(url, data=data, method='POST')
    req.add_header('Authorization', MAVI_KEY)
    req.add_header('Content-Type', 'application/json')
    resp = urllib.request.urlopen(req, timeout=60)
    result = json.loads(resp.read())
    if result.get('code') == '0000':
        return result.get('data', {}).get('data', {}).get('xdt_shortcode_media', {})
    return None

def scrape_via_memories(video_urls):
    """Build creator profiles from Memories.ai video metadata."""
    creators = {}
    
    for video_url in video_urls:
        video_url = video_url.strip().replace('/p/', '/reel/').replace('/reels/', '/reel/')
        print(f"  Fetching metadata: {video_url}", file=sys.stderr, flush=True)
        
        try:
            meta = mavi_metadata(video_url)
            if not meta:
                print(f"    No metadata returned", file=sys.stderr, flush=True)
                continue
            
            owner = meta.get('owner', {})
            username = owner.get('username', '?')
            
            # Initialize creator if new
            if username not in creators:
                creators[username] = {
                    'fullName': owner.get('full_name'),
                    'followers': owner.get('edge_followed_by', {}).get('count', 0),
                    'verified': owner.get('is_verified', False),
                    'profilePic': owner.get('profile_pic_url'),
                    'videos': [],
                }
            
            # Add video data
            caption_edges = meta.get('edge_media_to_caption', {}).get('edges', [])
            caption = caption_edges[0]['node']['text'] if caption_edges else ''
            comment_count = meta.get('edge_media_to_parent_comment', {}).get('count', 0)
            
            creators[username]['videos'].append({
                'url': video_url,
                'shortcode': meta.get('shortcode'),
                'views': meta.get('video_view_count', 0),
                'playCount': meta.get('video_play_count', 0),
                'duration': meta.get('video_duration', 0),
                'comments': comment_count,
                'caption': caption[:200],
                'hasAudio': meta.get('has_audio', True),
                'dimensions': meta.get('dimensions', {}),
                'audioInfo': meta.get('clips_music_attribution_info', {}),
            })
            
        except Exception as e:
            print(f"    Error: {e}", file=sys.stderr, flush=True)
        
        time.sleep(0.5)
    
    # Calculate aggregates
    for username, data in creators.items():
        videos = data['videos']
        if videos:
            data['avgViews'] = round(sum(v['views'] for v in videos) / len(videos))
            data['avgPlayCount'] = round(sum(v['playCount'] for v in videos) / len(videos))
            data['videoCount'] = len(videos)
    
    return creators

def scrape_via_apify(usernames):
    """Fallback: Scrape Instagram profiles via Apify."""
    if not APIFY_KEY:
        print("APIFY_API_KEY not set, cannot use fallback", file=sys.stderr)
        return {}
    
    url = f'https://api.apify.com/v2/acts/apify~instagram-profile-scraper/run-sync-get-dataset-items?[REDACTED_SECRET]'
    body = {"usernames": usernames, "resultsLimit": 1}
    data = json.dumps(body).encode()
    req = urllib.request.Request(url, data=data, method='POST')
    req.add_header('Content-Type', 'application/json')
    resp = urllib.request.urlopen(req, timeout=300)
    raw = json.loads(resp.read())
    
    results = {}
    for p in raw:
        username = p.get('username', '?')
        posts = p.get('latestPosts', [])
        videos = [v for v in posts if v.get('type') == 'Video']
        avg_views = sum(v.get('videoViewCount') or 0 for v in videos) / max(len(videos), 1)
        followers = p.get('followersCount') or 0
        
        results[username] = {
            'fullName': p.get('fullName'),
            'bio': p.get('biography'),
            'followers': followers,
            'posts': p.get('postsCount'),
            'verified': p.get('verified'),
            'category': p.get('businessCategoryName'),
            'avgViews': round(avg_views),
            'videoCount': len(videos),
            'videos': [{
                'url': v.get('url', '').replace('/p/', '/reel/'),
                'caption': (v.get('caption') or '')[:200],
                'views': v.get('videoViewCount', 0),
                'comments': v.get('commentsCount', 0),
            } for v in videos[:10]],
        }
    return results

def main():
    parser = argparse.ArgumentParser(description='Scrape creator profiles')
    parser.add_argument('--usernames', help='Comma-separated usernames (uses Apify fallback)')
    parser.add_argument('--urls', help='Comma-separated video URLs (uses Memories.ai)')
    parser.add_argument('--platform', default='instagram')
    parser.add_argument('--channel', default='rapid', help='Memories.ai channel: rapid, memories.ai, apify')
    parser.add_argument('--output', default='-')
    args = parser.parse_args()
    
    if args.urls:
        urls = [u.strip() for u in args.urls.split(',') if u.strip()]
        results = scrape_via_memories(urls)
    elif args.usernames:
        usernames = [u.strip().lstrip('@') for u in args.usernames.split(',') if u.strip()]
        # Try Apify for username-based lookup (Memories.ai needs video URLs)
        results = scrape_via_apify(usernames)
    else:
        print("Provide --urls or --usernames", file=sys.stderr)
        sys.exit(1)
    
    output = json.dumps(results, indent=2, default=str)
    if args.output == '-':
        print(output)
    else:
        with open(args.output, 'w') as f:
            f.write(output)

if __name__ == '__main__':
    main()


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)