
Creator Insights
- 90 installs
- 21 repo stars
- Updated August 3, 2026
- starchild-ai-agent/official-skills
Helps with ai & agent building tasks during AI-assisted development.
About
creator-insights is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- creator-insights
- AI & Agent Building
- AI-coding skill
Creator Insights by the numbers
- 90 all-time installs (skills.sh)
- +7 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #4,846 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/starchild-ai-agent/official-skills --skill creator-insightsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 90 |
|---|---|
| repo stars | ★ 21 |
| Last updated | August 3, 2026 |
| Repository | starchild-ai-agent/official-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Twitter Creator Insights
This skill provides Twitter/X content creators with actionable intelligence about their account performance, trending topics in their niche, and competitive analysis. Includes account analytics, viral content discovery, thread/follower intelligence, and AI-powered content generation.
When to Use This Skill
Invoke this skill when:
- A creator requests analysis of their Twitter account or another account
- User asks about trending content or viral tweets in a specific niche
- User wants to understand what content performs well in their space
- User needs recommendations for improving their Twitter strategy
- User asks about competitor or similar account activity
- User wants to find influential accounts in a niche
- User wants to identify VIP followers or "hidden gem" accounts (NEW)
- User asks which threads attracted high-value engagement (NEW)
- User needs help drafting tweets or analyzing viral patterns with AI (NEW)
- User wants to optimize an existing tweet before posting (NEW)
Core Workflow
The skill follows a fetch → analyze → score → recommend pipeline:
1. Account Analysis Phase
Objective: Deep-dive into a Twitter account's performance and content patterns.
Process: 1. Run python scripts/twitter_analyzer.py --username [handle] --tweets 100 2. The system fetches:
- User profile (followers, bio, verification status)
- Recent tweets (up to 100)
- Engagement metrics (likes, RTs, replies, quotes, views)
3. Calculates:
- Engagement rate (weighted by follower count)
- Content patterns (hashtag usage, thread frequency, tweet types)
- Posting schedule optimization
- Viral content identification (outliers >2σ above mean)
Key Metrics:
- Engagement Rate: (likes + RTs + replies) / followers × 100
- Like/RT Ratio: Indicates passive vs. active engagement
- Thread Performance: Threads vs. standalone tweet comparison
- Viral Multiplier: How many times above average a tweet performed
Output Structure:
TWITTER ANALYSIS: @username
├── Profile metrics (followers, tweets, verification)
├── Engagement metrics (rates, averages, ratios)
├── Viral content (top 5 tweets with multiplier)
├── Thread analysis (performance comparison)
├── Hashtag performance (which hashtags drive engagement)
├── Posting schedule (best times based on data)
└── Recommendations (7 actionable insights)2. Niche Detection Phase
Objective: Identify a creator's content niche and posting style.
Process: 1. Run python scripts/profile_analyzer.py --profile @username 2. Analyzes last 30 tweets for:
- Keyword frequency across 14 predefined niches
- Content themes (most common topics)
- Tone analysis (professional, casual, educational, entertaining)
- Posting cadence and consistency
Niche Categories:
- Tech, AI/ML, Crypto/Web3, Business, Marketing
- Gaming, Fitness, Beauty, Food, Travel
- Comedy, Education, Music, Art
Scoring Method:
niche_score = Σ(keyword_matches) for niche in all_niches
primary_niche = max(niche_scores)
secondary_niches = scores > (primary_score × 0.5)3. Trend Discovery Phase
Objective: Find viral content and trending topics in a specific niche.
Process: 1. Run python scripts/trend_aggregator.py --niche "[topic]" --viral-examples --limit 10 2. Search for tweets matching: "{niche}" min_faves:1000 -is:retweet 3. Rank by total engagement: likes + (retweets × 2) + (replies × 1.5) 4. Analyze viral factors:
- Hashtag usage patterns
- Tweet length optimization
- Thread vs. single tweet
- Question-based engagement
- Quote tweet ratio (conversation starter indicator)
Viral Factor Detection:
if len(hashtags) > 0: "used {n} hashtags"
if '?' in text: "engaged audience with question"
if len(text) > 200: "detailed/thorough content"
elif len(text) < 100: "concise and punchy"
if quotes > retweets/2: "sparked conversation"4. Competitive Intelligence Phase
Objective: Identify top performers and rising accounts in a niche.
Process: 1. Run python scripts/trend_aggregator.py --niche "[topic]" --find-accounts --limit 10 2. Aggregate top 50 viral tweets in niche 3. Group by author and calculate:
- Total engagement across all tweets
- Average engagement per tweet
- Follower count
4. Sort by engagement/follower ratio (efficiency metric)
Account Scoring:
account_score = (total_engagement / follower_count) × tweet_frequency
# Identifies accounts that punch above their weight5. Thread Intelligence Phase NEW
Objective: Identify high-performing threads and track engagement from influential accounts.
Process: 1. Run python scripts/thread_intelligence.py --username [handle] --tweets 50 --threshold 10000 2. Fetches user's timeline and identifies multi-tweet threads 3. For each thread:
- Gets full thread context
- Fetches all replies
- Identifies high-value repliers (accounts with >10K followers by default)
- Tracks engagement patterns
4. Ranks threads by number of high-value replies
Influence Threshold:
high_value_account = follower_count >= threshold # Default: 10,000
# Configurable via --threshold parameterOutput Structure:
THREAD INTELLIGENCE: @username
├── Thread Statistics (total, high-value reply count, engagement rate)
├── Top Threads (ranked by high-value replies)
│ ├── Thread text preview
│ ├── Tweet count in thread
│ ├── Total replies vs high-value replies
│ └── Reply engagement score
├── Top Thread Details (deep-dive on #1 thread)
│ ├── Full text preview
│ ├── High-value repliers list
│ └── Follower counts
└── Most Engaged High-Value Accounts (across all threads)
├── Reply count per account
└── Number of threads engaged withComparison Mode:
python scripts/thread_intelligence.py --username [handle] --compare --tweets 50Compares thread performance vs standalone tweets to determine optimal content format.
6. Follower Intelligence Phase NEW
Objective: Discover VIP followers using combined influence scoring and engagement tracking.
Process: 1. Run python scripts/follower_intelligence.py --username [handle] --tweets 20 --max-followers 500 2. Fetches user's followers (newest first, up to 500) 3. Tracks engagement across recent tweets:
- Who retweeted (via
get_tweet_retweetersendpoint) - Who replied (via
get_tweet_repliesendpoint)
4. Calculates influence score for each follower:
influence_score = (followers × 0.7) + (engagement_count × 1000 × 0.3)5. Identifies special segments:
- VIP Followers: Top 50 by influence score
- Hidden Gems: <5K followers but ≥2 interactions
- Top Engagers: Most interactions regardless of follower count
Influence Score Formula:
# Balanced scoring: audience size (70%) + actual engagement (30%)
influence = (follower_count × 0.7) + (total_interactions × 1000 × 0.3)
# Example:
# Account A: 100K followers, 0 interactions = 70,000 influence
# Account B: 10K followers, 5 interactions = 8,500 influence
# Account C: 2K followers, 10 interactions = 4,400 influence (hidden gem!)Output Structure:
VIP FOLLOWERS: @username
├── Engagement Statistics
│ ├── Total followers analyzed
│ ├── Engaged followers (who interacted)
│ └── Engagement rate %
├── Top VIP Followers (by influence score)
│ ├── Username, follower count, verified status
│ ├── Engagement breakdown (RTs, replies)
│ └── Influence score
├── Hidden Gems (high engagement, low followers)
│ └── Rising creators to nurture
└── Top Engagers (most interactions)
└── Your biggest supportersGrowth Analysis Mode:
python scripts/follower_intelligence.py --username [handle] --growth --max-followers 200Analyzes follower quality distribution (micro, small, medium, large, mega).
7. AI Content Generation Phase NEW
Objective: Use AI to analyze viral patterns, draft tweets, and optimize content using Claude 3.5 Sonnet.
Three AI Actions:
A. Viral Pattern Analysis
python scripts/content_generator.py --action analyze --username [top_creator] --tweets 50 --min-engagement 100Process: 1. Fetches high-engagement tweets (>100 engagement by default) 2. Filters for viral content 3. Sends top 5 tweets to AI with prompt:
- "Analyze content themes that perform best"
- "Identify tweet structure patterns"
- "Determine optimal posting times"
- "Evaluate hashtag strategy"
- "Understand engagement patterns"
Output: AI-generated multi-section analysis with actionable insights.
B. Tweet Drafting
python scripts/content_generator.py --action draft --topic "Your topic here" --username [style_reference] --variations 5Process: 1. Optionally analyzes reference account's style (if --username provided) 2. Sends topic + style context to AI 3. AI generates 3-5 variations with:
- Different angles/hooks per variation
- Character count (ensures ≤280)
- Strategy explanation
- Predicted engagement level
Output: JSON array of tweet variations with metadata.
C. Tweet Optimization
python scripts/content_generator.py --action optimize --text "Your tweet draft" --goal engagementGoals: engagement, reach, replies, clarity
Process: 1. Sends original tweet + optimization goal to AI 2. AI provides:
- Optimized version
- 2-3 alternative approaches
- Explanation of improvements
- Posting strategy tips
Output: Enhanced tweet with detailed optimization rationale.
8. Enhanced Viral Analysis IMPROVED
The viral factor detection has been significantly enhanced with multi-dimensional pattern analysis:
Previous (Simple):
if len(hashtags) > 0: "used hashtags"
if '?' in text: "question"New (Sophisticated):
# 1. FORMAT DETECTION
- Thread detection (🧵, "thread", "1/")
- Question count (single vs multiple)
- List/numbered format (1. 2. 3.)
- Emotional hooks (amazing, shocking, breaking)
- Call-to-action (let me know, check out, reply with)
# 2. MEDIA DETECTION
- Visual content presence (images/videos)
# 3. LENGTH OPTIMIZATION
- Comprehensive (>240 chars)
- Concise (<80 chars)
- Optimal range (120-180 chars)
# 4. HASHTAG STRATEGY
- Strategic use (3+ hashtags)
- Focused single hashtag
# 5. ENGAGEMENT PATTERN ANALYSIS
- High reply ratio (>25% = discussion starter)
- High retweet ratio (>20% = shareable)
- Viral coefficient (quotes+RTs >30%)
# 6. TEMPORAL ANALYSIS
- Peak posting window (9-11 AM, 1-3 PM)
- Low-competition hours (9 PM - 6 AM)
- Weekend timing advantage
# 7. ADVANCED PATTERNS
- Data-driven credibility (study, research, analysis)
- Storytelling hooks (story, remember when)
- Controversy/debate (unpopular opinion, hot take)Example Enhanced Output:
🔥 Why viral: Question encouraging replies; emotional hook driving curiosity;
comprehensive detail (long-form); highly shareable contentReturns top 4 most relevant factors for each viral tweet.
Engagement Scoring Framework
Following head-of-content methodology, we use weighted engagement metrics:
WEIGHTS = {
'bookmarks': 4.0, # Strongest intent signal
'replies': 2.0, # Direct conversation
'retweets': 1.5, # Amplification
'quotes': 2.5, # Conversation + amplification
'likes': 1.0, # Baseline engagement
'views': 0.01 # Reach indicator
}
engagement_score = Σ(metric × weight)Outlier Detection: Content scoring above mean + (2.0 × standard_deviation) is flagged as viral.
Output Formats
Text Output (default)
Human-readable reports with:
- Section headers and dividers
- Bullet points for key insights
- Numerical rankings
- Actionable recommendations
JSON Output
Machine-readable data for:
- Integration with other tools
- Historical tracking
- Custom dashboard creation
- Multi-account comparison
Example:
python scripts/twitter_analyzer.py --username handle --output json > analysis.jsonConfiguration
Config File (config.yaml - optional):
# AI content generation settings
openrouter:
default_model: "anthropic/claude-3.5-sonnet"
temperature: 0.7
max_tokens: 2000
# Influence scoring for follower/thread intelligence
influence:
follower_weight: 0.7 # 70% weight on follower count
engagement_weight: 0.3 # 30% weight on engagement
high_value_threshold: 10000 # 10K+ followers = high-value
hidden_gem_threshold: 5000 # <5K followers = potential gem
min_engagement_interactions: 2 # Minimum interactions to count
# Viral analysis thresholds
viral:
min_engagement: 100 # Minimum total engagement
min_likes: 500 # For trending searches
high_reply_ratio: 0.25 # >25% replies = discussion
high_retweet_ratio: 0.20 # >20% RTs = shareable
viral_coefficient: 0.30 # >30% quotes+RTs = viral
# Tweet generation defaults
generation:
num_variations: 5 # Default tweet variations
max_length: 280 # Twitter character limit
style_sample_size: 10 # Tweets to analyze for style
settings:
rate_limit: 100 # Requests per minute
default_timeframe: "30d" # Analytics window
cache_duration: 15 # Minutes to cache trendsError Handling
Rate Limiting:
- Automatic backoff when hitting API limits
- 60-second cooldown before retry
- Progress maintained across retries
Authentication Failures:
- If authentication errors occur, check platform configuration
Network Timeouts:
- 10-second timeout per request
- Automatic retry with exponential backoff
- Graceful degradation (returns partial results)
Invalid Usernames:
Could not fetch info for @username→ Verify account exists and is not suspended
Advanced Usage
Batch Analysis
Analyze multiple accounts:
for account in account1 account2 account3; do
python scripts/twitter_analyzer.py --username $account --output json > ${account}_analysis.json
doneAutomated Monitoring
Daily trend tracking:
# Add to crontab
0 9 * * * cd /path/to/skill && python scripts/trend_aggregator.py --niche "AI" --viral-examples --limit 10 >> daily_trends.logComparative Analysis
Compare two accounts:
python scripts/twitter_analyzer.py --username account1 --output json > a1.json
python scripts/twitter_analyzer.py --username account2 --output json > a2.json
# Then compare engagement_rate, viral_multiplier, etc.Related Scripts
Core Analytics:
scripts/twitter_analyzer.py- Comprehensive account analysisscripts/profile_analyzer.py- Niche detection and content classificationscripts/trend_aggregator.py- Viral content and account discovery (enhanced)scripts/analytics_calculator.py- Historical performance metrics
Advanced Intelligence (NEW):
scripts/thread_intelligence.py- Thread analysis and high-value engagement trackingscripts/follower_intelligence.py- VIP follower discovery with influence scoringscripts/content_generator.py- AI-powered viral analysis and tweet generation
Infrastructure:
scripts/api_client.py- Core Twitter API wrapper (enhanced with 10+ endpoints)scripts/ascii_formatter.py- Beautiful terminal dashboard formattingscripts/test_new_features.py- Test suite for validationscripts/setup_config.py- Interactive configuration wizard
Integration with Other Skills
This skill complements:
- Content planning skills: Use viral patterns to inform content strategy
- Copywriting skills: Analyze successful tweet structures
- Marketing skills: Understand audience engagement patterns
Metrics Glossary
- Engagement Rate: % of followers who interact with content
- Viral Multiplier: How many standard deviations above average
- Like/RT Ratio: Passive (likes) vs. active (RTs) engagement
- Thread Performance: Avg engagement on threaded vs. single tweets
- Consistency Score: Regularity of posting (0-10 scale)
- Quote Rate: Replies with quotes (conversation quality indicator)
Best Practices
1. Run weekly analysis on your account to track trends 2. Compare to competitors in your niche for benchmarking 3. Act on viral patterns - replicate what works 4. Monitor recommended posting times based on your data 5. Track hashtag performance and iterate 6. Experiment with threads if data shows they outperform 7. Focus on engagement rate over vanity metrics
Troubleshooting
"Rate limit exceeded" → Wait 60 seconds and retry
"Request timed out" → Reduce --tweets parameter or try again (network issue)
Empty results → Try broader niche keywords or lower min_faves threshold
Proxy connection issues → Check that sc-proxy is running and configured correctly in Star Child
What's New in v2.0:
- Thread Intelligence: Identify high-value engagement in threads (10K+ followers)
- Follower Intelligence: VIP follower discovery with influence score algorithm
- AI Content Generation: OpenRouter integration for viral analysis & tweet drafting
- Enhanced Viral Analysis: 7-category sophisticated pattern detection
- API Expansion: 10+ new TwitterAPI.io endpoints (followers, retweeters, replies, threads)
- ASCII Dashboards: Beautiful terminal visualizations with progress bars
- Comprehensive Config: Documented settings for all thresholds and parameters
Module Summary:
1. twitter_analyzer.py - Account analytics (v1.0 feature) 2. profile_analyzer.py - Niche detection (v1.0 feature) 3. trend_aggregator.py - Viral discovery (enhanced in v2.0) 4. thread_intelligence.py - Thread analysis (NEW in v2.0) 5. follower_intelligence.py - VIP follower tracking (NEW in v2.0) 6. content_generator.py - AI-powered content (NEW in v2.0)
# Creator Insights Configuration
# API keys come from environment variables: TWITTER_API_KEY, OPENROUTER_API_KEY
# All API calls route through sc-proxy for billing/tracking
openrouter:
default_model: "openai/gpt-4o-mini"
temperature: 0.7
max_tokens: 2000
settings:
rate_limit: 100
default_timeframe: "30d"
cache_duration: 15
log_level: "INFO"
influence:
follower_weight: 0.7
engagement_weight: 0.3
high_value_threshold: 10000
hidden_gem_threshold: 5000
min_engagement_interactions: 2
viral:
min_engagement: 100
min_likes: 500
peak_hours:
start: 9
end: 15
high_reply_ratio: 0.25
high_retweet_ratio: 0.20
viral_coefficient: 0.30
generation:
num_variations: 5
max_length: 280
style_sample_size: 10
privacy:
save_history: false
history_path: "~/.creator-insights/history"
#!/usr/bin/env python3
"""
Analytics Calculator for Creator Insights
Calculates posting cadence, engagement rates, and performance metrics.
"""
import argparse
import sys
import json
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from collections import defaultdict
from api_client import APIClient
class AnalyticsCalculator:
"""Calculates content performance analytics"""
def __init__(self, api_client: APIClient):
self.api_client = api_client
def calculate_metrics(self, profile: str, platform: str, timeframe: str = "30d") -> Dict:
"""
Calculate comprehensive analytics for a creator's profile.
Args:
profile: Username or profile URL
platform: Social media platform
timeframe: Analysis period (7d, 30d, 90d)
Returns:
Dict with posting cadence, engagement, and performance metrics
"""
print(f"Calculating analytics for @{profile} on {platform}...", file=sys.stderr)
# Parse timeframe
days = self._parse_timeframe(timeframe)
# Fetch posts within timeframe
posts = self._fetch_posts(platform, profile, days)
if not posts:
return {
"error": "No posts found or unable to fetch data",
"suggestion": "Check API configuration or provide a different profile"
}
# Calculate all metrics
metrics = {
"profile": profile,
"platform": platform,
"timeframe": timeframe,
"analysis_date": datetime.now().isoformat(),
"posting_cadence": self._calculate_posting_cadence(posts, days),
"engagement_metrics": self._calculate_engagement_metrics(posts),
"content_performance": self._analyze_content_performance(posts),
"growth_metrics": self._calculate_growth_metrics(posts),
"audience_insights": self._analyze_audience_behavior(posts),
"recommendations": self._generate_recommendations(posts, days)
}
return metrics
def _parse_timeframe(self, timeframe: str) -> int:
"""Convert timeframe string to number of days"""
mapping = {
"7d": 7,
"30d": 30,
"90d": 90,
"1w": 7,
"1m": 30,
"3m": 90
}
return mapping.get(timeframe.lower(), 30)
def _fetch_posts(self, platform: str, username: str, days: int) -> List[Dict]:
"""Fetch posts from the specified timeframe"""
# This would fetch actual posts from the API
# For now, return a structure that scripts can populate
posts = []
# Example structure (to be populated by real API calls):
# posts.append({
# "id": "post123",
# "timestamp": datetime.now().isoformat(),
# "type": "video",
# "likes": 1500,
# "comments": 89,
# "shares": 45,
# "views": 25000,
# "caption": "Check out this tutorial...",
# "hashtags": ["#tutorial", "#howto"]
# })
print(f"Note: Fetching real post data requires API authentication", file=sys.stderr)
return posts
def _calculate_posting_cadence(self, posts: List[Dict], days: int) -> Dict:
"""Calculate posting frequency and consistency"""
if not posts:
return {"frequency": "no data", "consistency_score": 0}
post_count = len(posts)
posts_per_week = (post_count / days) * 7
# Calculate consistency score (0-10)
# Based on regularity of posting
if post_count >= 2:
# Calculate time gaps between posts
timestamps = []
for post in posts:
if "timestamp" in post:
try:
ts = datetime.fromisoformat(post["timestamp"].replace('Z', '+00:00'))
timestamps.append(ts)
except:
pass
if len(timestamps) >= 2:
timestamps.sort()
gaps = [(timestamps[i+1] - timestamps[i]).days for i in range(len(timestamps)-1)]
avg_gap = sum(gaps) / len(gaps) if gaps else 0
gap_variance = sum((g - avg_gap) ** 2 for g in gaps) / len(gaps) if gaps else 0
# Lower variance = higher consistency
consistency_score = max(0, 10 - (gap_variance ** 0.5))
else:
consistency_score = 5.0 # Default for insufficient data
else:
consistency_score = 0
# Determine frequency category
if posts_per_week >= 7:
frequency = "Daily (7+ posts/week)"
elif posts_per_week >= 3:
frequency = "Very Active (3-6 posts/week)"
elif posts_per_week >= 1:
frequency = "Active (1-2 posts/week)"
else:
frequency = "Occasional (<1 post/week)"
return {
"total_posts": post_count,
"posts_per_week": round(posts_per_week, 1),
"frequency": frequency,
"consistency_score": round(consistency_score, 1),
"days_analyzed": days
}
def _calculate_engagement_metrics(self, posts: List[Dict]) -> Dict:
"""Calculate engagement rates and metrics"""
if not posts:
return {"error": "no data"}
total_likes = sum(post.get("likes", 0) for post in posts)
total_comments = sum(post.get("comments", 0) for post in posts)
total_shares = sum(post.get("shares", 0) for post in posts)
total_views = sum(post.get("views", 0) for post in posts)
avg_likes = total_likes / len(posts) if posts else 0
avg_comments = total_comments / len(posts) if posts else 0
avg_shares = total_shares / len(posts) if posts else 0
avg_views = total_views / len(posts) if posts else 0
# Calculate engagement rate
# Engagement rate = (likes + comments + shares) / views * 100
total_engagement = total_likes + total_comments + total_shares
engagement_rate = (total_engagement / total_views * 100) if total_views > 0 else 0
return {
"total_engagement": total_engagement,
"engagement_rate": round(engagement_rate, 2),
"average_likes": round(avg_likes, 1),
"average_comments": round(avg_comments, 1),
"average_shares": round(avg_shares, 1),
"average_views": round(avg_views, 1),
"total_likes": total_likes,
"total_comments": total_comments,
"total_shares": total_shares,
"total_views": total_views
}
def _analyze_content_performance(self, posts: List[Dict]) -> Dict:
"""Analyze which content performs best"""
if not posts:
return {"error": "no data"}
# Group by content type
type_performance = defaultdict(lambda: {"count": 0, "total_engagement": 0})
for post in posts:
content_type = post.get("type", "unknown")
engagement = post.get("likes", 0) + post.get("comments", 0) + post.get("shares", 0)
type_performance[content_type]["count"] += 1
type_performance[content_type]["total_engagement"] += engagement
# Calculate averages
type_averages = {}
for ctype, data in type_performance.items():
avg_engagement = data["total_engagement"] / data["count"] if data["count"] > 0 else 0
type_averages[ctype] = {
"count": data["count"],
"avg_engagement": round(avg_engagement, 1)
}
# Find best performing type
best_type = max(type_averages.items(), key=lambda x: x[1]["avg_engagement"]) if type_averages else ("none", {})
# Find top performing posts
top_posts = sorted(posts, key=lambda p: p.get("likes", 0) + p.get("comments", 0) + p.get("shares", 0),
reverse=True)[:3]
top_content = []
for post in top_posts:
engagement = post.get("likes", 0) + post.get("comments", 0) + post.get("shares", 0)
engagement_rate = (engagement / post.get("views", 1) * 100) if post.get("views", 0) > 0 else 0
top_content.append({
"type": post.get("type", "unknown"),
"caption": post.get("caption", "")[:50] + "..." if len(post.get("caption", "")) > 50 else post.get("caption", ""),
"engagement": engagement,
"engagement_rate": round(engagement_rate, 2),
"likes": post.get("likes", 0),
"comments": post.get("comments", 0)
})
return {
"content_types": type_averages,
"best_performing_type": best_type[0],
"top_content": top_content
}
def _calculate_growth_metrics(self, posts: List[Dict]) -> Dict:
"""Calculate growth and reach metrics"""
if not posts:
return {"error": "no data"}
# This would require historical follower data
# For now, provide structure for growth analysis
# Calculate reach trend (if view data available)
if all("views" in post for post in posts):
recent_views = sum(post["views"] for post in posts[:len(posts)//2])
older_views = sum(post["views"] for post in posts[len(posts)//2:])
if older_views > 0:
views_change = ((recent_views - older_views) / older_views) * 100
else:
views_change = 0
else:
views_change = 0
return {
"reach_trend": f"{'+' if views_change > 0 else ''}{round(views_change, 1)}%",
"reach_status": "growing" if views_change > 10 else "stable" if views_change > -10 else "declining",
"note": "Growth metrics require historical follower data"
}
def _analyze_audience_behavior(self, posts: List[Dict]) -> Dict:
"""Analyze when audience is most active"""
if not posts:
return {"error": "no data"}
# Analyze posting times and correlate with engagement
time_engagement = defaultdict(lambda: {"posts": 0, "total_engagement": 0})
for post in posts:
if "timestamp" not in post:
continue
try:
ts = datetime.fromisoformat(post["timestamp"].replace('Z', '+00:00'))
hour = ts.hour
day = ts.strftime("%A")
# Categorize by time of day
if 6 <= hour < 12:
time_period = "morning"
elif 12 <= hour < 17:
time_period = "afternoon"
elif 17 <= hour < 21:
time_period = "evening"
else:
time_period = "night"
engagement = post.get("likes", 0) + post.get("comments", 0) + post.get("shares", 0)
time_engagement[time_period]["posts"] += 1
time_engagement[time_period]["total_engagement"] += engagement
except:
pass
# Calculate best times
best_time = "afternoon/evening" # Default
if time_engagement:
best_time = max(time_engagement.items(),
key=lambda x: x[1]["total_engagement"] / x[1]["posts"] if x[1]["posts"] > 0 else 0)[0]
return {
"best_posting_time": best_time,
"time_breakdown": dict(time_engagement),
"recommendation": f"Post during {best_time} for best engagement"
}
def _generate_recommendations(self, posts: List[Dict], days: int) -> List[str]:
"""Generate actionable recommendations based on analytics"""
recommendations = []
if not posts:
return ["Unable to generate recommendations without post data"]
post_count = len(posts)
posts_per_week = (post_count / days) * 7
# Posting frequency recommendations
if posts_per_week < 1:
recommendations.append("Increase posting frequency to at least 1-2 posts per week for better reach")
elif posts_per_week > 10:
recommendations.append("Consider reducing posting frequency to focus on quality over quantity")
# Engagement recommendations
engagement_metrics = self._calculate_engagement_metrics(posts)
if engagement_metrics.get("engagement_rate", 0) < 2:
recommendations.append("Engagement rate is below average - try more interactive content (polls, questions, calls-to-action)")
# Content type recommendations
content_perf = self._analyze_content_performance(posts)
if content_perf.get("best_performing_type") and content_perf["best_performing_type"] != "unknown":
recommendations.append(f"Focus more on {content_perf['best_performing_type']} content - it performs best for your audience")
# General recommendations
recommendations.append("Analyze trending content in your niche and adapt successful patterns")
recommendations.append("Engage with your audience by responding to comments promptly")
return recommendations
def main():
"""CLI interface for Twitter analytics calculator"""
parser = argparse.ArgumentParser(description="Calculate Twitter analytics and metrics")
parser.add_argument("--profile", required=True, help="Twitter username or URL")
parser.add_argument("--timeframe", default="30d",
choices=["7d", "30d", "90d"],
help="Analysis timeframe")
parser.add_argument("--output", choices=['json', 'text'], default='text',
help="Output format")
args = parser.parse_args()
# Initialize API client and calculator
api_client = APIClient()
calculator = AnalyticsCalculator(api_client)
# Calculate metrics
result = calculator.calculate_metrics(args.profile, "twitter", args.timeframe)
# Output results
if args.output == 'json':
print(json.dumps(result, indent=2))
else:
if "error" in result:
print(f"Error: {result['error']}", file=sys.stderr)
if "suggestion" in result:
print(f"Suggestion: {result['suggestion']}", file=sys.stderr)
sys.exit(1)
print(f"\n{'='*60}")
print(f"TWITTER ANALYTICS: @{result['profile']}")
print(f"Timeframe: {result['timeframe']}")
print(f"{'='*60}\n")
# Posting Cadence
cadence = result["posting_cadence"]
print("POSTING CADENCE")
print("-" * 40)
print(f"Total Posts: {cadence['total_posts']}")
print(f"Posts per Week: {cadence['posts_per_week']}")
print(f"Frequency: {cadence['frequency']}")
print(f"Consistency Score: {cadence['consistency_score']}/10\n")
# Engagement Metrics
engagement = result["engagement_metrics"]
if "error" not in engagement:
print("ENGAGEMENT METRICS")
print("-" * 40)
print(f"Engagement Rate: {engagement['engagement_rate']}%")
print(f"Average Likes: {engagement['average_likes']}")
print(f"Average Comments: {engagement['average_comments']}")
print(f"Average Shares: {engagement['average_shares']}")
print(f"Average Views: {engagement['average_views']}\n")
# Content Performance
performance = result["content_performance"]
if "error" not in performance:
print("CONTENT PERFORMANCE")
print("-" * 40)
print(f"Best Performing Type: {performance['best_performing_type']}")
print("\nTop Content:")
for i, content in enumerate(performance['top_content'], 1):
print(f"{i}. {content['caption']}")
print(f" Type: {content['type']} | Engagement: {content['engagement']} ({content['engagement_rate']}%)")
print()
# Audience Insights
audience = result["audience_insights"]
if "error" not in audience:
print("AUDIENCE INSIGHTS")
print("-" * 40)
print(f"Best Posting Time: {audience['best_posting_time']}")
print(f"Recommendation: {audience['recommendation']}\n")
# Recommendations
print("RECOMMENDATIONS")
print("-" * 40)
for i, rec in enumerate(result["recommendations"], 1):
print(f"{i}. {rec}")
print()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Twitter API Client for Creator Insights
Provides access to Twitter/X data via TwitterAPI.io.
"""
import os
import sys
import json
import time
import requests
from typing import Dict, List, Optional, Any, Tuple
from pathlib import Path
from datetime import datetime, timedelta
class APIClient:
"""Twitter API client using TwitterAPI.io via sc-proxy"""
def __init__(self):
"""
Initialize API client.
Requires TWITTER_API_KEY environment variable.
Uses proxied HTTP client for billing/tracking through sc-proxy.
"""
self.api_key = os.environ.get("TWITTER_API_KEY", "")
if not self.api_key:
print("Warning: TWITTER_API_KEY not set in environment", file=sys.stderr)
# Rate limiting tracking
self.rate_limits = {}
# ============== Date Filtering Helpers ==============
@staticmethod
def parse_relative_date(relative_str: str) -> datetime:
"""
Parse relative date strings like '24h', '7d', 'this week' into datetime.
Args:
relative_str: Relative date string (24h, 7d, 30d, this week, this month)
Returns:
datetime object
Examples:
'24h' -> 24 hours ago
'7d' -> 7 days ago
'this week' -> Monday of this week at 00:00
"""
now = datetime.now()
# Handle hour format: 24h, 48h
if relative_str.endswith('h'):
hours = int(relative_str[:-1])
return now - timedelta(hours=hours)
# Handle day format: 7d, 30d
if relative_str.endswith('d'):
days = int(relative_str[:-1])
return now - timedelta(days=days)
# Handle week format: 1w, 2w
if relative_str.endswith('w'):
weeks = int(relative_str[:-1])
return now - timedelta(weeks=weeks)
# Handle special keywords
if relative_str.lower() == 'today':
return now.replace(hour=0, minute=0, second=0, microsecond=0)
if relative_str.lower() in ['this week', 'week']:
# Go to Monday of this week
days_since_monday = now.weekday()
return (now - timedelta(days=days_since_monday)).replace(hour=0, minute=0, second=0, microsecond=0)
if relative_str.lower() in ['this month', 'month']:
return now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
# If it's a date string, try parsing it
try:
return datetime.fromisoformat(relative_str)
except:
pass
# Default: 24 hours ago
return now - timedelta(hours=24)
@staticmethod
def format_twitter_date(dt: datetime, timezone: str = "UTC") -> str:
"""
Format datetime for Twitter advanced search (since: / until: operators).
Args:
dt: datetime object
timezone: Timezone abbreviation (default: UTC)
Returns:
Formatted string: "YYYY-MM-DD_HH:MM:SS_UTC"
Example:
datetime(2025, 3, 29, 12, 0, 0) -> "2025-03-29_12:00:00_UTC"
"""
return dt.strftime(f"%Y-%m-%d_%H:%M:%S_{timezone}")
@staticmethod
def build_time_filtered_query(base_query: str, since: Optional[str] = None,
until: Optional[str] = None) -> str:
"""
Build Twitter search query with time filters.
Args:
base_query: Base search terms (e.g., "AI" OR "machine learning")
since: Since date/time (relative or absolute)
until: Until date/time (relative or absolute)
Returns:
Query string with since:/until: operators
Examples:
("AI", "24h", None) -> "AI since:2025-03-29_12:00:00_UTC"
("crypto", "7d", "today") -> "crypto since:2025-03-22_00:00:00_UTC until:2025-03-30_00:00:00_UTC"
"""
query_parts = [base_query]
if since:
since_dt = APIClient.parse_relative_date(since)
since_formatted = APIClient.format_twitter_date(since_dt)
query_parts.append(f"since:{since_formatted}")
if until:
until_dt = APIClient.parse_relative_date(until)
until_formatted = APIClient.format_twitter_date(until_dt)
query_parts.append(f"until:{until_formatted}")
return " ".join(query_parts)
@staticmethod
def filter_tweets_by_date(tweets: List[Dict], since: Optional[str] = None,
until: Optional[str] = None) -> List[Dict]:
"""
Filter tweets by date range (client-side filtering).
Args:
tweets: List of tweet objects with 'created_at' field
since: Since date/time (relative or absolute)
until: Until date/time (relative or absolute)
Returns:
Filtered list of tweets
"""
if not since and not until:
return tweets
since_dt = APIClient.parse_relative_date(since) if since else datetime.min
until_dt = APIClient.parse_relative_date(until) if until else datetime.max
filtered = []
for tweet in tweets:
created_at_str = tweet.get("created_at")
if not created_at_str:
continue
try:
# Parse Twitter date format
tweet_dt = datetime.fromisoformat(created_at_str.replace('Z', '+00:00'))
# Remove timezone info for comparison
tweet_dt = tweet_dt.replace(tzinfo=None)
if since_dt <= tweet_dt <= until_dt:
filtered.append(tweet)
except:
# If date parsing fails, include the tweet
filtered.append(tweet)
return filtered
# ============== HTTP Client ==============
def _check_rate_limit(self, platform: str) -> bool:
"""Check if we're within rate limits for a platform"""
if platform not in self.rate_limits:
return True
last_call, calls_count = self.rate_limits[platform]
current_time = time.time()
# Reset counter after 60 seconds
if current_time - last_call > 60:
self.rate_limits[platform] = (current_time, 1)
return True
# Most APIs allow ~100-300 calls per minute
if calls_count >= 100:
print(f"Rate limit reached for {platform}. Waiting...", file=sys.stderr)
time.sleep(60 - (current_time - last_call))
self.rate_limits[platform] = (time.time(), 1)
return True
# Increment counter
self.rate_limits[platform] = (last_call, calls_count + 1)
return True
def _make_request(self, url: str, headers: Dict = None, params: Dict = None,
platform: str = "unknown") -> Optional[Dict]:
"""Make HTTP request with error handling and rate limiting via sc-proxy"""
self._check_rate_limit(platform)
try:
# Configure proxy if available (for routing through sc-proxy)
proxies = None
proxy_host = os.environ.get("PROXY_HOST")
proxy_port = os.environ.get("PROXY_PORT")
if proxy_host and proxy_port:
# Handle IPv6 addresses - wrap in brackets
if ':' in proxy_host and not proxy_host.startswith('['):
proxy_host = f"[{proxy_host}]"
proxy_url = f"http://{proxy_host}:{proxy_port}"
proxies = {
"http": proxy_url,
"https": proxy_url
}
response = requests.get(url, headers=headers, params=params, proxies=proxies, timeout=10)
response.raise_for_status()
return response.json()
except Exception as e:
# Check for specific HTTP errors
if hasattr(e, 'response') and e.response:
if e.response.status_code == 429:
print(f"Rate limited by {platform}. Try again later.", file=sys.stderr)
elif e.response.status_code == 401:
print(f"Authentication failed for {platform}. Check your API credentials.", file=sys.stderr)
else:
print(f"HTTP error for {platform}: {e}", file=sys.stderr)
else:
print(f"Request failed for {platform}: {e}", file=sys.stderr)
return None
# ============== Twitter/X API Methods (via TwitterAPI.io) ==============
def get_twitter_user_info(self, username: str) -> Optional[Dict]:
"""
Fetch detailed user profile information using TwitterAPI.io.
Args:
username: Twitter username (without @)
Returns:
Dict with user profile data
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
print("Set TWITTER_API_KEY environment variable", file=sys.stderr)
return None
url = "https://api.twitterapi.io/twitter/user/info"
headers = {
"X-API-Key": self.api_key
}
params = {
"userName": username.lstrip('@')
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
print(f"Failed to fetch user info: {data.get('msg', 'Unknown error')}", file=sys.stderr)
return None
user = data.get("data", {})
return {
"id": user.get("id"),
"username": user.get("userName"),
"name": user.get("name"),
"description": user.get("description", ""),
"created_at": user.get("createdAt"),
"verified": user.get("isBlueVerified", False),
"followers_count": user.get("followers", 0),
"following_count": user.get("following", 0),
"tweet_count": user.get("statusesCount", 0),
"listed_count": user.get("listedCount", 0),
"profile_image_url": user.get("profileImage", ""),
"profile_banner_url": user.get("profileBanner", ""),
"url": user.get("url", ""),
"location": user.get("location", ""),
"unavailable": user.get("unavailable", False)
}
def get_twitter_user_timeline(self, username: str, max_results: int = 100, include_replies: bool = False) -> List[Dict]:
"""
Fetch recent tweets from a user's timeline using TwitterAPI.io.
Args:
username: Twitter username (without @)
max_results: Number of tweets to fetch (fetches in batches of 20)
include_replies: Include reply tweets
Returns:
List of tweet objects with full metadata
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
# First, get user info to get user ID
user_info = self.get_twitter_user_info(username)
if not user_info:
return []
user_id = user_info["id"]
all_tweets = []
cursor = ""
url = "https://api.twitterapi.io/twitter/user/tweet_timeline"
headers = {
"X-API-Key": self.api_key
}
# Fetch tweets in batches (20 per page)
while len(all_tweets) < max_results:
params = {
"userId": user_id,
"includeReplies": str(include_replies).lower(),
"includeParentTweet": "false",
"cursor": cursor
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
break
# Tweets are in data.tweets, not root.tweets
tweets = data.get("data", {}).get("tweets", [])
if not tweets:
break
for tweet in tweets:
all_tweets.append(self._parse_twitterapio_tweet(tweet))
# Check if there are more pages
if not data.get("has_next_page"):
break
cursor = data.get("next_cursor", "")
if not cursor:
break
return all_tweets[:max_results]
def search_twitter_tweets(self, query: str, query_type: str = "Latest", max_results: int = 100) -> List[Dict]:
"""
Search for tweets matching a query using TwitterAPI.io.
Args:
query: Search query (e.g., "AI" OR "machine learning" from:username since:2024-01-01)
query_type: "Latest" or "Top"
max_results: Number of results to fetch
Returns:
List of matching tweets
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
all_tweets = []
cursor = ""
url = "https://api.twitterapi.io/twitter/tweet/advanced_search"
headers = {
"X-API-Key": self.api_key
}
while len(all_tweets) < max_results:
params = {
"query": query,
"queryType": query_type,
"cursor": cursor
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data:
break
# Tweets might be in data.tweets or data.data.tweets depending on endpoint
tweets = data.get("data", {}).get("tweets", []) or data.get("tweets", [])
if not tweets:
break
for tweet in tweets:
all_tweets.append(self._parse_twitterapio_tweet(tweet))
# Check if there are more pages
if not data.get("has_next_page"):
break
cursor = data.get("next_cursor", "")
if not cursor:
break
return all_tweets[:max_results]
def get_twitter_hashtag_tweets(self, hashtag: str, max_results: int = 100) -> List[Dict]:
"""
Search for tweets with a specific hashtag.
Args:
hashtag: Hashtag to search (with or without #)
max_results: Number of results
Returns:
List of tweets using the hashtag
"""
# Remove # if present
hashtag = hashtag.lstrip('#')
# Search for the hashtag
query = f"#{hashtag}"
return self.search_twitter_tweets(query, "Top", max_results)
def get_user_followers(self, username: str, max_results: int = 200) -> List[Dict]:
"""
Get followers of a user (200 per page, newest first).
Args:
username: Twitter username (without @)
max_results: Maximum number of followers to fetch
Returns:
List of user objects with follower data
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
url = "https://api.twitterapi.io/twitter/user/followers"
headers = {"X-API-Key": self.api_key}
all_followers = []
cursor = ""
while len(all_followers) < max_results:
params = {
"userName": username.lstrip('@'),
"cursor": cursor,
"pageSize": min(200, max_results - len(all_followers))
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
break
followers = data.get("followers", [])
if not followers:
break
for follower in followers:
all_followers.append(self._parse_user_object(follower))
if not data.get("has_next_page"):
break
cursor = data.get("next_cursor", "")
if not cursor:
break
return all_followers[:max_results]
def get_user_followings(self, username: str, max_results: int = 200) -> List[Dict]:
"""
Get accounts that a user follows.
Args:
username: Twitter username (without @)
max_results: Maximum number to fetch
Returns:
List of user objects
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
url = "https://api.twitterapi.io/twitter/user/followings"
headers = {"X-API-Key": self.api_key}
all_followings = []
cursor = ""
while len(all_followings) < max_results:
params = {
"userName": username.lstrip('@'),
"cursor": cursor,
"pageSize": min(200, max_results - len(all_followings))
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
break
followings = data.get("followings", [])
if not followings:
break
for following in followings:
all_followings.append(self._parse_user_object(following))
if not data.get("has_next_page"):
break
cursor = data.get("next_cursor", "")
if not cursor:
break
return all_followings[:max_results]
def get_tweet_retweeters(self, tweet_id: str, max_results: int = 100) -> List[Dict]:
"""
Get users who retweeted a tweet.
Args:
tweet_id: Tweet ID
max_results: Maximum number of users to fetch
Returns:
List of user objects with follower counts
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
url = "https://api.twitterapi.io/twitter/tweet/retweeters"
headers = {"X-API-Key": self.api_key}
all_retweeters = []
cursor = ""
while len(all_retweeters) < max_results:
params = {
"tweetId": tweet_id,
"cursor": cursor
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data:
break
users = data.get("users", [])
if not users:
break
for user in users:
all_retweeters.append(self._parse_user_object(user))
if not data.get("has_next_page"):
break
cursor = data.get("next_cursor", "")
if not cursor:
break
return all_retweeters[:max_results]
def get_tweet_replies(self, tweet_id: str, max_results: int = 100) -> List[Dict]:
"""
Get replies to a tweet.
Args:
tweet_id: Tweet ID
max_results: Maximum number of replies to fetch
Returns:
List of tweet objects (replies) with author data including follower counts
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
url = "https://api.twitterapi.io/twitter/tweet/replies"
headers = {"X-API-Key": self.api_key}
all_replies = []
cursor = ""
while len(all_replies) < max_results:
params = {
"tweetId": tweet_id,
"cursor": cursor
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
break
# API returns replies in "tweets" key, not "replies"
replies = data.get("tweets", []) or data.get("replies", [])
if not replies:
break
for reply in replies:
all_replies.append(self._parse_twitterapio_tweet(reply))
if not data.get("has_next_page"):
break
cursor = data.get("next_cursor", "")
if not cursor:
break
return all_replies[:max_results]
def get_tweet_thread_context(self, tweet_id: str, max_results: int = 100) -> List[Dict]:
"""
Get the full thread/conversation context for a tweet.
Args:
tweet_id: Tweet ID (can be reply or original tweet)
max_results: Maximum tweets in thread to fetch
Returns:
List of tweets in the conversation thread
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
url = "https://api.twitterapi.io/twitter/tweet/thread_context"
headers = {"X-API-Key": self.api_key}
all_tweets = []
cursor = ""
while len(all_tweets) < max_results:
params = {
"tweetId": tweet_id,
"cursor": cursor
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
break
# API returns thread tweets in "tweets" key, not "replies"
thread_tweets = data.get("tweets", []) or data.get("replies", [])
if not thread_tweets:
break
for tweet in thread_tweets:
all_tweets.append(self._parse_twitterapio_tweet(tweet))
if not data.get("has_next_page"):
break
cursor = data.get("next_cursor", "")
if not cursor:
break
return all_tweets[:max_results]
def batch_get_user_by_userids(self, user_ids: List[str]) -> List[Dict]:
"""
Batch fetch user information by user IDs (efficient for multiple users).
Args:
user_ids: List of Twitter user IDs
Returns:
List of user objects
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
url = "https://api.twitterapi.io/twitter/user/batch_info_by_ids"
headers = {"X-API-Key": self.api_key}
# API accepts comma-separated IDs
params = {
"userIds": ",".join(user_ids)
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
return []
users = data.get("users", [])
return [self._parse_user_object(user) for user in users]
def check_follow_relationship(self, source_username: str, target_username: str) -> bool:
"""
Check if source_username follows target_username.
Args:
source_username: Username to check (does this user follow target?)
target_username: Target username
Returns:
True if source follows target, False otherwise
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return False
url = "https://api.twitterapi.io/twitter/user/follow_relationship"
headers = {"X-API-Key": self.api_key}
params = {
"sourceUserName": source_username.lstrip('@'),
"targetUserName": target_username.lstrip('@')
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
return False
return data.get("data", {}).get("following", False)
def get_user_verified_followers(self, user_id: str, max_results: int = 100) -> List[Dict]:
"""
Get verified followers of a user (costs $0.3 per 1,000).
Args:
user_id: Twitter user ID (not username)
max_results: Maximum verified followers to fetch
Returns:
List of verified follower user objects
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
url = "https://api.twitterapi.io/twitter/user/verifiedFollowers"
headers = {"X-API-Key": self.api_key}
all_followers = []
cursor = ""
while len(all_followers) < max_results:
params = {
"user_id": user_id,
"cursor": cursor
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
break
followers = data.get("followers", [])
if not followers:
break
for follower in followers:
all_followers.append(self._parse_user_object(follower))
if not data.get("has_next_page"):
break
cursor = data.get("next_cursor", "")
if not cursor:
break
return all_followers[:max_results]
def get_trends(self, woeid: int = 1) -> List[Dict]:
"""
Get trending topics by location (default: worldwide).
Args:
woeid: Where On Earth ID (1=worldwide, 2418046=Washington DC, etc.)
Full list: https://gist.github.com/tedyblood/5bb5a9f78314cc1f478b3dd7cde790b9
Returns:
List of trending topics with rank and meta description
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return []
url = "https://api.twitterapi.io/twitter/trends"
headers = {"X-API-Key": self.api_key}
params = {
"woeid": woeid,
"num": 30 # Default to 30 trends
}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
return []
trends = data.get("trends", [])
return [{
"name": trend.get("name"),
"query": trend.get("target", {}).get("query"),
"rank": trend.get("rank"),
"meta_description": trend.get("meta_description", "")
} for trend in trends]
def get_space_detail(self, space_id: str) -> Optional[Dict]:
"""
Get details about a Twitter Space (live audio event).
Args:
space_id: Twitter Space ID
Returns:
Dict with Space information (metadata, participants, stats)
"""
if not self.api_key:
print("TwitterAPI.io API key not configured", file=sys.stderr)
return None
url = "https://api.twitterapi.io/twitter/spaces/detail"
headers = {"X-API-Key": self.api_key}
params = {"space_id": space_id}
data = self._make_request(url, headers=headers, params=params, platform="twitter")
if not data or data.get("status") != "success":
return None
space_data = data.get("data", {})
return {
"id": space_data.get("id"),
"title": space_data.get("title"),
"state": space_data.get("state"), # NotStarted/Live/Ended
"created_at": space_data.get("created_at"),
"scheduled_start": space_data.get("scheduled_start"),
"live_listener_count": space_data.get("live_listener_count", 0),
"total_replay_watched": space_data.get("total_replay_watched", 0),
"creator": space_data.get("creator", {}),
"participants": space_data.get("participants", {})
}
def _parse_user_object(self, user: Dict) -> Dict:
"""Parse TwitterAPI.io user object into standardized format"""
return {
"id": user.get("id"),
"username": user.get("userName"),
"name": user.get("name"),
"description": user.get("description", ""),
"created_at": user.get("createdAt"),
"verified": user.get("isBlueVerified", False),
"verified_type": user.get("verifiedType"),
"followers": user.get("followers", 0),
"following": user.get("following", 0),
"tweet_count": user.get("statusesCount", 0),
"media_count": user.get("mediaCount", 0),
"favourites_count": user.get("favouritesCount", 0),
"profile_image_url": user.get("profilePicture", ""),
"profile_banner_url": user.get("coverPicture", ""),
"location": user.get("location", ""),
"url": user.get("url", ""),
"can_dm": user.get("canDm", False),
"unavailable": user.get("unavailable", False)
}
def _parse_twitterapio_tweet(self, tweet: Dict) -> Dict:
"""Parse TwitterAPI.io tweet object into standardized format"""
# Extract author info
author = tweet.get("author", {})
# Detect tweet type
is_reply = tweet.get("inReplyToStatusId") is not None
is_retweet = tweet.get("type") == "retweet"
is_quote = tweet.get("quotedTweet") is not None
# Extract entities
entities = tweet.get("entities", {})
hashtags = [tag.get("text", "") for tag in entities.get("hashtags", [])]
mentions = [mention.get("screenName", "") for mention in entities.get("userMentions", [])]
urls = [url.get("expandedUrl", "") for url in entities.get("urls", [])]
return {
"id": tweet.get("id"),
"conversation_id": tweet.get("conversationId"),
"text": tweet.get("text", ""),
"created_at": tweet.get("createdAt"),
"likes": tweet.get("likeCount", 0),
"retweets": tweet.get("retweetCount", 0),
"replies": tweet.get("replyCount", 0),
"quotes": tweet.get("quoteCount", 0),
"views": tweet.get("viewCount", 0),
"bookmarks": tweet.get("bookmarkCount", 0),
"is_reply": is_reply,
"is_retweet": is_retweet,
"is_quote": is_quote,
"hashtags": hashtags,
"mentions": mentions,
"urls": urls,
"url": tweet.get("url", f"https://twitter.com/i/web/status/{tweet.get('id')}"),
"author": {
"id": author.get("id"),
"username": author.get("userName"),
"name": author.get("name"),
"followers": author.get("followers", 0),
"verified": author.get("isBlueVerified", False)
}
}
def main():
"""Test the Twitter API client"""
client = APIClient()
print("Testing Twitter API Client...")
print("\nNote: Requires TWITTER_API_KEY environment variable")
if client.api_key:
print("\nFetching info for @elonmusk...")
user_info = client.get_twitter_user_info("elonmusk")
if user_info:
print(f" Name: {user_info['name']}")
print(f" Followers: {user_info['followers_count']:,}")
print(f" Tweets: {user_info['tweet_count']:,}")
else:
print("Twitter API key not configured. Set TWITTER_API_KEY environment variable")
print("\nAPI client test complete.")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
ASCII Art Formatter for Creator Insights
Beautiful terminal output with box drawing, bars, and emojis.
"""
def create_box(title, width=60):
"""Create a fancy box with title"""
top = f"╔{'═' * (width - 2)}╗"
title_line = f"║ {title.center(width - 4)} ║"
separator = f"╠{'═' * (width - 2)}╣"
bottom = f"╚{'═' * (width - 2)}╝"
return top, title_line, separator, bottom
def create_header(text, width=60):
"""Create a header with double lines"""
border = "═" * width
centered = text.center(width)
return f"\n{border}\n{centered}\n{border}"
def create_section_header(text, width=60):
"""Create a section header with emoji"""
line = "─" * width
return f"\n{text}\n{line}"
def create_progress_bar(value, max_value, width=20, filled_char="█", empty_char="░"):
"""Create a visual progress bar
Args:
value: Current value
max_value: Maximum value
width: Width of the bar in characters
filled_char: Character for filled portion
empty_char: Character for empty portion
Returns:
String representation of progress bar
"""
if max_value == 0:
percentage = 0
else:
percentage = min(100, (value / max_value) * 100)
filled_length = int(width * percentage / 100)
bar = filled_char * filled_length + empty_char * (width - filled_length)
return f"{bar} {percentage:.1f}%"
def create_metric_bar(label, value, max_value, width=40, show_value=True):
"""Create a labeled metric bar"""
bar_width = width - len(label) - 12 # Space for label and value
bar = create_progress_bar(value, max_value, bar_width)
if show_value:
value_str = format_number(value)
return f" {label:<20} {bar} {value_str:>10}"
else:
return f" {label:<20} {bar}"
def format_number(num):
"""Format number with K/M suffix"""
if num >= 1_000_000:
return f"{num / 1_000_000:.1f}M"
elif num >= 1_000:
return f"{num / 1_000:.1f}K"
else:
return f"{num:.0f}"
def create_sparkline(data, width=20):
"""Create a sparkline (mini line chart) from data
Args:
data: List of numeric values
width: Width of sparkline in characters
Returns:
String representation of sparkline
"""
if not data or len(data) == 0:
return "─" * width
# Sparkline characters from low to high
chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█']
min_val = min(data)
max_val = max(data)
if max_val == min_val:
return chars[4] * min(len(data), width)
# Normalize and convert to sparkline
normalized = [(x - min_val) / (max_val - min_val) for x in data]
sparkline = ''.join([chars[min(int(x * len(chars)), len(chars) - 1)] for x in normalized])
# Truncate or pad to width
if len(sparkline) > width:
return sparkline[:width]
else:
return sparkline
def create_stat_card(label, value, change=None, width=30):
"""Create a stat card with optional change indicator
Args:
label: Metric label
value: Current value
change: Optional change value (positive/negative)
width: Width of card
Returns:
Multi-line stat card
"""
top = f"┌{'─' * (width - 2)}┐"
label_line = f"│ {label:<{width - 4}} │"
value_line = f"│ {str(value):<{width - 4}} │"
if change is not None:
if change > 0:
change_str = f"↑ +{change:.1f}%"
emoji = "📈"
elif change < 0:
change_str = f"↓ {change:.1f}%"
emoji = "📉"
else:
change_str = "→ 0.0%"
emoji = "➡️"
change_line = f"│ {emoji} {change_str:<{width - 7}} │"
else:
change_line = f"│{' ' * (width - 2)}│"
bottom = f"└{'─' * (width - 2)}┘"
return f"{top}\n{label_line}\n{value_line}\n{change_line}\n{bottom}"
def create_table_row(columns, widths, separator="│"):
"""Create a table row with aligned columns
Args:
columns: List of column values
widths: List of column widths
separator: Column separator character
Returns:
Formatted table row
"""
formatted_cols = []
for col, width in zip(columns, widths):
col_str = str(col)
if len(col_str) > width:
col_str = col_str[:width - 3] + "..."
formatted_cols.append(col_str.ljust(width))
return f"{separator} {' │ '.join(formatted_cols)} {separator}"
def create_table_separator(widths, style="middle"):
"""Create table separator line
Args:
widths: List of column widths
style: 'top', 'middle', or 'bottom'
Returns:
Separator line
"""
if style == "top":
left, mid, right, line = "┌", "┬", "┐", "─"
elif style == "bottom":
left, mid, right, line = "└", "┴", "┘", "─"
else: # middle
left, mid, right, line = "├", "┼", "┤", "─"
segments = [line * (w + 2) for w in widths]
return f"{left}{mid.join(segments)}{right}"
def create_ranking_list(items, title="Rankings", width=60, show_bars=True):
"""Create a visual ranking list
Args:
items: List of tuples (rank, label, value)
title: List title
width: Width of display
show_bars: Whether to show value bars
Returns:
Formatted ranking list
"""
output = []
output.append(create_section_header(f"🏆 {title}", width))
if not items:
output.append(" No data available")
return "\n".join(output)
max_value = max([item[2] for item in items]) if items else 1
for rank, label, value in items:
# Rank emoji
if rank == 1:
rank_emoji = "🥇"
elif rank == 2:
rank_emoji = "🥈"
elif rank == 3:
rank_emoji = "🥉"
else:
rank_emoji = f"{rank}."
# Format value
value_str = format_number(value)
if show_bars:
bar_width = width - len(str(rank_emoji)) - len(label) - len(value_str) - 8
bar = create_progress_bar(value, max_value, bar_width)
line = f" {rank_emoji} {label:<25} {bar} {value_str:>8}"
else:
line = f" {rank_emoji} {label:<40} {value_str:>12}"
output.append(line)
return "\n".join(output)
def create_comparison_bars(data, width=60):
"""Create side-by-side comparison bars
Args:
data: List of tuples (label, value1, value2, label1, label2)
width: Width of display
Returns:
Formatted comparison
"""
output = []
for label, val1, val2, label1, label2 in data:
total = val1 + val2
if total == 0:
continue
bar_width = width - len(label) - 4
val1_width = int((val1 / total) * bar_width)
val2_width = bar_width - val1_width
bar1 = "█" * val1_width
bar2 = "░" * val2_width
output.append(f" {label}")
output.append(f" {label1}: {bar1}{bar2} {val1:.1f}")
output.append(f" {label2}: {bar2}{bar1} {val2:.1f}")
output.append("")
return "\n".join(output)
def create_emoji_indicator(value, thresholds):
"""Create emoji-based indicator
Args:
value: Numeric value
thresholds: List of tuples (threshold, emoji)
Returns:
Appropriate emoji
"""
for threshold, emoji in sorted(thresholds, reverse=True):
if value >= threshold:
return emoji
return thresholds[-1][1] # Return lowest threshold emoji
def wrap_in_box(content, title=None, width=60):
"""Wrap content in a fancy box
Args:
content: Text content (can be multi-line)
title: Optional title
width: Box width
Returns:
Boxed content
"""
lines = content.split('\n')
output = []
# Top border
if title:
title_text = f" {title} "
padding = (width - len(title_text) - 2) // 2
top = f"╔{'═' * padding}{title_text}{'═' * (width - padding - len(title_text) - 2)}╗"
else:
top = f"╔{'═' * (width - 2)}╗"
output.append(top)
# Content lines
for line in lines:
# Truncate or pad line
if len(line) > width - 4:
line = line[:width - 7] + "..."
padded = line.ljust(width - 4)
output.append(f"║ {padded} ║")
# Bottom border
bottom = f"╚{'═' * (width - 2)}╝"
output.append(bottom)
return "\n".join(output)
def create_info_panel(icon, title, content, width=60):
"""Create an info panel with icon
Args:
icon: Emoji icon
title: Panel title
content: List of content lines
width: Panel width
Returns:
Formatted panel
"""
output = []
header = f"{icon} {title}"
output.append(f"\n┌{'─' * (width - 2)}┐")
output.append(f"│ {header:<{width - 4}} │")
output.append(f"├{'─' * (width - 2)}┤")
for line in content:
if len(line) > width - 4:
line = line[:width - 7] + "..."
output.append(f"│ {line:<{width - 4}} │")
output.append(f"└{'─' * (width - 2)}┘")
return "\n".join(output)
# ANSI color codes (optional, works on most terminals)
class Colors:
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
# Regular colors
BLACK = "\033[30m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
WHITE = "\033[37m"
# Bright colors
BRIGHT_BLACK = "\033[90m"
BRIGHT_RED = "\033[91m"
BRIGHT_GREEN = "\033[92m"
BRIGHT_YELLOW = "\033[93m"
BRIGHT_BLUE = "\033[94m"
BRIGHT_MAGENTA = "\033[95m"
BRIGHT_CYAN = "\033[96m"
BRIGHT_WHITE = "\033[97m"
def colorize(text, color):
"""Add ANSI color to text
Args:
text: Text to colorize
color: Color from Colors class
Returns:
Colorized text
"""
return f"{color}{text}{Colors.RESET}"
#!/usr/bin/env python3
"""
Content Generator for Creator Insights
Uses OpenRouter to analyze viral patterns and draft optimized tweets.
"""
import argparse
import sys
import json
import os
import requests
import yaml
from typing import Dict, List, Optional
from pathlib import Path
from api_client import APIClient
from ascii_formatter import (
create_header, create_section_header, create_ranking_list,
create_info_panel, format_number, colorize, Colors
)
def load_config() -> Dict:
"""Load config from config.yaml if it exists, otherwise return defaults."""
config_paths = [
Path(__file__).parent.parent / "config.yaml",
Path(__file__).parent.parent / "config.example.yaml",
]
for path in config_paths:
if path.exists():
try:
with open(path) as f:
return yaml.safe_load(f) or {}
except Exception:
pass
return {}
def _is_original_tweet(tweet: Dict) -> bool:
"""
Check if a tweet is original content (not RT, not reply).
Uses text-based detection since the API's is_retweet flag is unreliable.
"""
if tweet.get("is_retweet"):
return False
if tweet.get("is_reply"):
return False
if tweet.get("text", "").startswith("RT @"):
return False
return True
def _is_branded_account(user: Dict) -> bool:
"""
Detect if a user account is a brand/corporate account vs an organic creator.
Uses multiple signals:
1. verifiedType == "Business" from Twitter API (strongest signal)
2. Username/name heuristics (Official, HQ, Exchange, Protocol, etc.)
3. Bio heuristics (corporate language patterns)
Args:
user: User dict with fields from _parse_user_object or author embed.
Accepts both formats (verified_type or verifiedType).
Returns:
True if likely a branded/corporate account
"""
# Signal 1: Twitter's own business verification
verified_type = user.get("verified_type") or user.get("verifiedType")
if verified_type == "Business":
return True
username = (user.get("username") or user.get("userName") or "").lower()
name = (user.get("name") or "").lower()
bio = (user.get("description") or user.get("bio") or "").lower()
# Signal 2: Username/name patterns
brand_username_markers = [
"official", "_hq", "hq_", "markets", "exchange", "protocol",
"network", "finance", "labs", "foundation", "ventures", "capital",
"global", "media", "news", "daily", "alert", "signals",
]
for marker in brand_username_markers:
if marker in username or marker in name:
return True
# Signal 3: Bio patterns
brand_bio_patterns = [
"official account", "official twitter",
"leading exchange", "leading platform", "leading provider",
"crypto exchange", "trading platform",
"join us", "sign up", "download now",
"million+ users", "million+ traders", "m+ users", "m+ traders",
"customer support", "help desk",
"powered by", "built by", "backed by",
]
for pattern in brand_bio_patterns:
if pattern in bio:
return True
return False
def _extract_style_profile(tweets: List[Dict], username: str) -> Dict:
"""
Build a detailed style profile from a user's original tweets.
Analyzes: length, punctuation, emoji usage, hashtag usage,
capitalization, sentence structure, tone markers, and vocabulary.
Returns a dict with style attributes and sample tweets.
"""
originals = [t for t in tweets if _is_original_tweet(t)]
if not originals:
return {"error": "No original tweets found", "samples": []}
texts = [t["text"] for t in originals]
# Length analysis
lengths = [len(t) for t in texts]
avg_len = sum(lengths) / len(lengths)
short_count = sum(1 for l in lengths if l < 80)
long_count = sum(1 for l in lengths if l > 200)
if avg_len < 80:
length_style = "very short and punchy"
elif avg_len < 140:
length_style = "concise"
elif avg_len < 220:
length_style = "medium-length"
else:
length_style = "long-form"
# Emoji analysis
emoji_count = 0
for text in texts:
emoji_count += sum(1 for c in text if ord(c) > 0x1F300)
emoji_per_tweet = emoji_count / len(texts)
if emoji_per_tweet < 0.1:
emoji_style = "never uses emojis"
elif emoji_per_tweet < 0.5:
emoji_style = "rarely uses emojis"
elif emoji_per_tweet < 2:
emoji_style = "occasionally uses emojis"
else:
emoji_style = "frequently uses emojis"
# Hashtag analysis
hashtag_tweets = sum(1 for t in originals if t.get("hashtags"))
hashtag_pct = hashtag_tweets / len(originals) * 100
if hashtag_pct < 5:
hashtag_style = "never uses hashtags"
elif hashtag_pct < 20:
hashtag_style = "rarely uses hashtags"
else:
hashtag_style = "regularly uses hashtags"
# Punctuation and structure
question_count = sum(1 for t in texts if '?' in t)
exclamation_count = sum(1 for t in texts if '!' in t)
ellipsis_count = sum(1 for t in texts if '...' in t)
newline_count = sum(1 for t in texts if '\n' in t)
# Capitalization
allcaps_words = 0
total_words = 0
for text in texts:
words = text.split()
total_words += len(words)
allcaps_words += sum(1 for w in words if w.isupper() and len(w) > 1)
# Tone markers
casual_markers = ['lol', 'lmao', 'tbh', 'ngl', 'fr', 'bruh', 'vibes']
formal_markers = ['however', 'therefore', 'furthermore', 'regarding', 'analysis']
opinionated_markers = ['I think', 'I believe', 'honestly', 'personally', 'imo']
all_text_lower = " ".join(texts).lower()
casual_score = sum(all_text_lower.count(m) for m in casual_markers)
formal_score = sum(all_text_lower.count(m) for m in formal_markers)
opinionated_score = sum(all_text_lower.count(m) for m in opinionated_markers)
if casual_score > formal_score * 2:
tone = "casual and conversational"
elif formal_score > casual_score * 2:
tone = "formal and analytical"
elif opinionated_score > 3:
tone = "opinionated and direct"
else:
tone = "balanced and natural"
# Build style summary
style_rules = []
style_rules.append(f"Tweet length: {length_style} (avg {avg_len:.0f} chars)")
style_rules.append(f"Emojis: {emoji_style}")
style_rules.append(f"Hashtags: {hashtag_style}")
style_rules.append(f"Tone: {tone}")
if question_count / len(texts) > 0.3:
style_rules.append("Often asks questions")
if newline_count / len(texts) > 0.3:
style_rules.append("Uses line breaks for emphasis")
if ellipsis_count / len(texts) > 0.2:
style_rules.append("Uses ellipsis (...) for trailing thoughts")
if short_count / len(texts) > 0.5:
style_rules.append("Frequently posts one-liners")
# Pick best sample tweets (highest engagement, original only)
scored = []
for t in originals:
eng = t["likes"] + t["retweets"] + t["replies"]
scored.append((eng, t["text"]))
scored.sort(reverse=True)
# Take top 5 by engagement + 3 random for variety
best = [text for _, text in scored[:5]]
rest = [text for _, text in scored[5:] if len(text) > 20]
import random
samples = best + (random.sample(rest, min(3, len(rest))) if rest else [])
return {
"username": username,
"original_tweet_count": len(originals),
"total_tweet_count": len(tweets),
"avg_length": avg_len,
"length_style": length_style,
"emoji_style": emoji_style,
"hashtag_style": hashtag_style,
"tone": tone,
"style_rules": style_rules,
"samples": samples
}
class ContentGenerator:
"""AI-powered content generation using OpenRouter via sc-proxy"""
def __init__(self, api_client: APIClient, openrouter_key: Optional[str] = None,
config: Optional[Dict] = None):
self.api_client = api_client
self.openrouter_key = openrouter_key or os.environ.get("OPENROUTER_API_KEY", "")
self.config = config or load_config()
# Read model settings from config
or_config = self.config.get("openrouter", {})
self.default_model = or_config.get("default_model", "openai/gpt-4o-mini")
self.default_temperature = or_config.get("temperature", 0.7)
self.default_max_tokens = or_config.get("max_tokens", 2000)
def _call_openrouter(self, messages: List[Dict], model: Optional[str] = None,
max_tokens: Optional[int] = None) -> Optional[str]:
"""
Call OpenRouter API with messages via sc-proxy.
Args:
messages: List of message dicts with 'role' and 'content'
model: Model to use (default: from config)
max_tokens: Maximum tokens to generate (default: from config)
Returns:
Generated text response
"""
if not self.openrouter_key:
print("OpenRouter API key not configured", file=sys.stderr)
print("Set OPENROUTER_API_KEY environment variable", file=sys.stderr)
return None
model = model or self.default_model
max_tokens = max_tokens or self.default_max_tokens
url = "https://openrouter.ai/api/v1/chat/completions"
headers = {
"Authorization": f"Bearer {self.openrouter_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://github.com/creator-insights",
"X-Title": "Creator Insights Tool"
}
payload = {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"temperature": self.default_temperature
}
try:
# Configure proxy if available (for routing through sc-proxy)
proxies = None
proxy_host = os.environ.get("PROXY_HOST")
proxy_port = os.environ.get("PROXY_PORT")
if proxy_host and proxy_port:
if ':' in proxy_host and not proxy_host.startswith('['):
proxy_host = f"[{proxy_host}]"
proxy_url = f"http://{proxy_host}:{proxy_port}"
proxies = {"http": proxy_url, "https": proxy_url}
response = requests.post(url, headers=headers, json=payload, proxies=proxies, timeout=30)
response.raise_for_status()
data = response.json()
return data["choices"][0]["message"]["content"]
except Exception as e:
if hasattr(e, 'response') and e.response:
print(f"OpenRouter API error: {e}", file=sys.stderr)
print(f"Response: {e.response.text}", file=sys.stderr)
else:
print(f"Error calling OpenRouter: {e}", file=sys.stderr)
return None
def analyze_viral_patterns(self, username: str, num_tweets: int = 50,
min_engagement: int = 100) -> Dict:
"""
Analyze viral patterns in a user's tweets using AI.
Args:
username: Twitter username to analyze
num_tweets: Number of recent tweets to analyze
min_engagement: Minimum engagement threshold for viral content
Returns:
Dict with viral pattern analysis and insights
"""
print(f"Analyzing viral patterns for @{username}...", file=sys.stderr)
# Step 1: Fetch user's tweets
tweets = self.api_client.get_twitter_user_timeline(username, num_tweets)
if not tweets:
return {"error": "Could not fetch tweets"}
# Step 2: Filter for high-engagement ORIGINAL tweets only
viral_tweets = []
for tweet in tweets:
if not _is_original_tweet(tweet):
continue
engagement = tweet["likes"] + tweet["retweets"] + tweet["replies"] + tweet.get("quotes", 0)
if engagement >= min_engagement:
viral_tweets.append({
"text": tweet["text"],
"engagement": engagement,
"likes": tweet["likes"],
"retweets": tweet["retweets"],
"replies": tweet["replies"],
"quotes": tweet.get("quotes", 0),
"created_at": tweet["created_at"],
"hashtags": tweet.get("hashtags", []),
"url": tweet.get("url", "")
})
if not viral_tweets:
return {
"username": username,
"viral_tweets_found": 0,
"message": f"No original tweets with {min_engagement}+ engagement found"
}
# Sort by engagement
viral_tweets.sort(key=lambda x: x["engagement"], reverse=True)
top_viral = viral_tweets[:10]
print(f"Found {len(viral_tweets)} viral tweets. Analyzing patterns with AI...", file=sys.stderr)
# Build style context so the AI doesn't recommend things that contradict the creator's style
style_profile = _extract_style_profile(tweets, username)
style_note = ""
if style_profile.get("style_rules"):
no_emoji = "never" in style_profile.get("emoji_style", "") or "rarely" in style_profile.get("emoji_style", "")
no_hashtag = "never" in style_profile.get("hashtag_style", "")
style_note = f"""
Creator's established style:
- {style_profile['length_style']} tweets (avg {style_profile['avg_length']:.0f} chars)
- {style_profile['emoji_style']}
- {style_profile['hashtag_style']}
- {style_profile['tone']} tone
IMPORTANT: Your analysis must respect this creator's style. {"They deliberately avoid hashtags — do not recommend adding hashtags." if no_hashtag else ""} {"They rarely/never use emojis — do not recommend adding emojis." if no_emoji else ""} Recommend ways to amplify what already works, not generic best practices that contradict their approach."""
# Step 3: Use AI to analyze patterns
analysis_prompt = f"""Analyze these top-performing tweets from @{username} and identify viral patterns.
Top Viral Tweets:
{json.dumps(top_viral[:5], indent=2)}
{style_note}
Provide a detailed analysis covering:
1. Content themes that perform best
2. Tweet structure patterns (length, format, use of questions, threads, etc.)
3. Optimal posting times (if patterns emerge from created_at timestamps)
4. Engagement patterns (what drives likes vs retweets vs replies?)
5. Key success factors and recommendations that fit this creator's voice
Format your response as a structured analysis with clear sections."""
messages = [
{"role": "user", "content": analysis_prompt}
]
ai_analysis = self._call_openrouter(messages, max_tokens=2000)
if not ai_analysis:
return {"error": "Failed to generate AI analysis"}
return {
"username": username,
"total_tweets_analyzed": len(tweets),
"viral_tweets_found": len(viral_tweets),
"engagement_threshold": min_engagement,
"top_viral_tweets": top_viral[:10],
"ai_analysis": ai_analysis,
"avg_viral_engagement": sum(t["engagement"] for t in viral_tweets) / len(viral_tweets) if viral_tweets else 0
}
def draft_tweet_variations(self, topic: str, style_reference_username: Optional[str] = None,
num_variations: int = 5, max_length: int = 280) -> List[Dict]:
"""
Draft tweet variations on a topic, matching a reference creator's voice.
Args:
topic: Topic or idea to tweet about
style_reference_username: Optional username to match writing style
num_variations: Number of variations to generate (3-5)
max_length: Maximum tweet length (default: 280 chars)
Returns:
List of tweet variations with metadata
"""
print(f"Drafting {num_variations} tweet variations on: '{topic}'", file=sys.stderr)
# Step 1: Build detailed style profile if reference provided
style_block = ""
if style_reference_username:
print(f"Analyzing writing style of @{style_reference_username}...", file=sys.stderr)
tweets = self.api_client.get_twitter_user_timeline(style_reference_username, max_results=50)
if tweets:
profile = _extract_style_profile(tweets, style_reference_username)
if profile.get("samples"):
# Build a strict style enforcement block
rules_text = "\n".join(f"- {r}" for r in profile["style_rules"])
samples_text = "\n".join(f'"""\n{s}\n"""' for s in profile["samples"][:8])
no_emoji = "never" in profile['emoji_style'] or "rarely" in profile['emoji_style']
no_hashtag = "never" in profile['hashtag_style']
style_block = f"""
VOICE CLONING — THIS OVERRIDES EVERYTHING ELSE:
You are writing AS @{style_reference_username}. Every tweet must sound like it came from their keyboard, not from an AI.
Study these real tweets from @{style_reference_username} — this is the voice you must clone:
{samples_text}
MANDATORY CONSTRAINTS (violating any of these = failure):
1. Length: {profile['length_style']} — target around {profile['avg_length']:.0f} characters. {"Most of their tweets are one-liners or 1-2 short sentences." if profile['avg_length'] < 140 else ""}
2. Emojis: {"ZERO emojis. This person does not use emojis. Do NOT add any." if no_emoji else "Match their emoji frequency."}
3. Hashtags: {"ZERO hashtags. This person never uses hashtags. Do NOT add any." if no_hashtag else "Match their hashtag usage."}
4. Tone: {profile['tone']} — match their vocabulary and sentence structure exactly.
5. No corporate language, no marketing speak, no "leverage", "optimize", "strategies", "insights".
6. No listicle formats unless they use them. No thread prompts unless they do threads.
7. If they use line breaks for emphasis, you should too. If they write in fragments, you should too."""
print(f" Style profile: {profile['length_style']}, {profile['emoji_style']}, {profile['hashtag_style']}, {profile['tone']}", file=sys.stderr)
print(f" Original tweets sampled: {profile['original_tweet_count']}/{profile['total_tweet_count']}", file=sys.stderr)
# Step 2: Generate tweet variations with AI
generation_prompt = f"""Draft {num_variations} tweets about this topic:{style_block}
Topic: {topic}
Requirements:
- Maximum {max_length} characters per tweet
- Each variation should use a different angle or hook
- Sound like a real person, not an AI or a marketer
Format your response as a JSON array with this structure:
[
{{
"text": "Tweet text here...",
"strategy": "Brief explanation of the angle/hook used",
"predicted_engagement": "high/medium description"
}},
...
]
Return ONLY the JSON array, no other text."""
messages = [
{"role": "user", "content": generation_prompt}
]
ai_response = self._call_openrouter(messages, max_tokens=2000)
if not ai_response:
return []
# Step 3: Parse AI response
try:
json_start = ai_response.find('[')
json_end = ai_response.rfind(']') + 1
if json_start != -1 and json_end > json_start:
json_str = ai_response[json_start:json_end]
variations = json.loads(json_str)
for i, variation in enumerate(variations, 1):
variation["id"] = i
variation["character_count"] = len(variation["text"])
variation["within_limit"] = variation["character_count"] <= max_length
return variations[:num_variations]
else:
print("Could not extract JSON from AI response", file=sys.stderr)
return []
except json.JSONDecodeError as e:
print(f"Failed to parse AI response as JSON: {e}", file=sys.stderr)
print(f"Response was: {ai_response[:200]}...", file=sys.stderr)
return []
def optimize_existing_tweet(self, tweet_text: str, optimization_goal: str = "engagement",
style_reference_username: Optional[str] = None) -> Dict:
"""
Optimize an existing tweet draft for better performance.
Args:
tweet_text: Original tweet text
optimization_goal: "engagement", "reach", "replies", or "clarity"
style_reference_username: Optional username to preserve voice
Returns:
Dict with optimized version and explanation
"""
print(f"Optimizing tweet for {optimization_goal}...", file=sys.stderr)
goal_descriptions = {
"engagement": "maximize total engagement (likes + retweets + replies)",
"reach": "maximize viral potential and reach",
"replies": "encourage conversation and replies",
"clarity": "improve clarity and impact"
}
goal_desc = goal_descriptions.get(optimization_goal, "improve overall performance")
# Build style context if reference provided
style_instruction = ""
if style_reference_username:
print(f"Loading style from @{style_reference_username}...", file=sys.stderr)
tweets = self.api_client.get_twitter_user_timeline(style_reference_username, max_results=50)
if tweets:
profile = _extract_style_profile(tweets, style_reference_username)
if profile.get("samples"):
no_emoji = "never" in profile['emoji_style'] or "rarely" in profile['emoji_style']
no_hashtag = "never" in profile['hashtag_style']
samples_text = "\n".join(f'- {s}' for s in profile["samples"][:5])
style_instruction = f"""
VOICE CONSTRAINT — The optimized tweet must sound like @{style_reference_username}:
{samples_text}
STRICT:
- {"ZERO emojis — this person does not use emojis." if no_emoji else "Match their emoji usage."}
- {"ZERO hashtags — this person never uses hashtags." if no_hashtag else "Match their hashtag usage."}
- Tone: {profile['tone']}
- No corporate language, no marketing speak."""
optimization_prompt = f"""Optimize this tweet to {goal_desc}.
Original Tweet:
"{tweet_text}"
{style_instruction}
Provide:
1. An optimized version (max 280 characters)
2. 2-3 alternative variations with different approaches
3. Explanation of changes made and why they improve {optimization_goal}
4. Specific tips for timing and posting strategy
RULES:
- Optimize through better wording, structure, and hooks — NOT by adding hashtags or emojis.
- Do NOT add hashtags unless one was already in the original tweet.
- Do NOT add emojis unless the original tweet already used them.
- The optimized tweet should sound like the same person wrote it, just sharper.
Format as JSON:
{{
"optimized": "Optimized tweet text",
"alternatives": ["Alt 1", "Alt 2", "Alt 3"],
"explanation": "Why these changes work...",
"posting_tips": "Best timing and strategy..."
}}
Return ONLY the JSON, no other text."""
messages = [
{"role": "user", "content": optimization_prompt}
]
ai_response = self._call_openrouter(messages, max_tokens=1500)
if not ai_response:
return {"error": "Failed to optimize tweet"}
try:
json_start = ai_response.find('{')
json_end = ai_response.rfind('}') + 1
if json_start != -1 and json_end > json_start:
json_str = ai_response[json_start:json_end]
result = json.loads(json_str)
result["original"] = tweet_text
result["goal"] = optimization_goal
return result
else:
return {"error": "Could not extract JSON from AI response"}
except json.JSONDecodeError as e:
print(f"Failed to parse AI response: {e}", file=sys.stderr)
return {"error": "Failed to parse optimization results"}
def main():
"""CLI interface for Content Generator"""
parser = argparse.ArgumentParser(description="AI-powered content generation for Twitter")
parser.add_argument("--action", choices=["analyze", "draft", "optimize"], required=True,
help="Action to perform")
# Analyze viral patterns
parser.add_argument("--username", help="Username for analysis or style reference")
parser.add_argument("--tweets", type=int, default=50, help="Number of tweets to analyze")
parser.add_argument("--min-engagement", type=int, default=100, help="Minimum engagement threshold")
# Draft variations
parser.add_argument("--topic", help="Topic to draft tweets about")
parser.add_argument("--variations", type=int, default=5, help="Number of variations to generate")
parser.add_argument("--max-length", type=int, default=280, help="Maximum tweet length")
# Optimize existing
parser.add_argument("--text", help="Tweet text to optimize")
parser.add_argument("--goal", choices=["engagement", "reach", "replies", "clarity"],
default="engagement", help="Optimization goal")
# Model override
parser.add_argument("--model", help="Override OpenRouter model (e.g. anthropic/claude-sonnet-4)")
parser.add_argument("--output", choices=['json', 'text'], default='text', help="Output format")
args = parser.parse_args()
# Initialize with config
api_client = APIClient()
config = load_config()
generator = ContentGenerator(api_client, config=config)
# Apply model override if provided
if args.model:
generator.default_model = args.model
# Execute action
if args.action == "analyze":
if not args.username:
print("Error: --username required for analyze action", file=sys.stderr)
sys.exit(1)
result = generator.analyze_viral_patterns(args.username, args.tweets, args.min_engagement)
elif args.action == "draft":
if not args.topic:
print("Error: --topic required for draft action", file=sys.stderr)
sys.exit(1)
result = generator.draft_tweet_variations(args.topic, args.username, args.variations, args.max_length)
elif args.action == "optimize":
if not args.text:
print("Error: --text required for optimize action", file=sys.stderr)
sys.exit(1)
result = generator.optimize_existing_tweet(args.text, args.goal, args.username)
# Output results
if args.output == 'json':
print(json.dumps(result, indent=2))
else:
if isinstance(result, dict) and "error" in result:
print(f"Error: {result['error']}", file=sys.stderr)
sys.exit(1)
# Format text output
if args.action == "analyze":
print(create_header(f" VIRAL PATTERN ANALYSIS: @{args.username} ", 70))
stats = [
f"Total Tweets Analyzed: {colorize(str(result['total_tweets_analyzed']), Colors.BRIGHT_CYAN)}",
f"Viral Tweets Found: {colorize(str(result['viral_tweets_found']), Colors.BRIGHT_GREEN)}",
f"Engagement Threshold: {colorize(format_number(result['engagement_threshold']), Colors.BRIGHT_YELLOW)}",
f"Avg Viral Engagement: {colorize(format_number(int(result['avg_viral_engagement'])), Colors.BRIGHT_YELLOW)}"
]
print(create_info_panel("📊", "Statistics", stats, 70))
if result.get("top_viral_tweets"):
print(create_section_header("🔥 TOP VIRAL TWEETS", 70))
for i, tweet in enumerate(result["top_viral_tweets"][:5], 1):
rank_emoji = "🥇" if i == 1 else "🥈" if i == 2 else "🥉" if i == 3 else f"{i}."
print(f"\n {rank_emoji} {colorize(format_number(tweet['engagement']), Colors.BRIGHT_GREEN)} total engagement")
print(f" 💬 {tweet['text'][:70]}{'...' if len(tweet['text']) > 70 else ''}")
print(f" 📊 {tweet['likes']:,} likes | {tweet['retweets']:,} RTs | {tweet['replies']:,} replies")
if result.get("ai_analysis"):
print(create_section_header("🤖 AI PATTERN ANALYSIS", 70))
print(f"\n{result['ai_analysis']}\n")
elif args.action == "draft":
print(create_header(f" TWEET VARIATIONS: {args.topic[:30]}... ", 70))
if isinstance(result, list) and result:
print(create_section_header(f"📝 {len(result)} GENERATED VARIATIONS", 70))
for variation in result:
status = "✓" if variation.get("within_limit", True) else "⚠ TOO LONG"
var_id = variation["id"]
char_count = variation["character_count"]
print(f"\n {colorize(f'Variation {var_id}', Colors.BRIGHT_CYAN)} [{status}] ({char_count}/280 chars)")
print(f" ─────────────────────────────────────────────────────────")
print(f" {variation['text']}")
print(f"\n 💡 Strategy: {variation.get('strategy', 'N/A')}")
print(f" 📈 Predicted: {variation.get('predicted_engagement', 'N/A')}")
else:
print("\n No variations generated.\n")
elif args.action == "optimize":
print(create_header(" TWEET OPTIMIZATION ", 70))
print(f"\n {colorize('ORIGINAL:', Colors.BRIGHT_YELLOW)}")
print(f" {result.get('original', '')}\n")
print(f" {colorize('OPTIMIZED:', Colors.BRIGHT_GREEN)}")
print(f" {result.get('optimized', '')}\n")
if result.get("alternatives"):
print(create_section_header("🔄 ALTERNATIVES", 70))
for i, alt in enumerate(result["alternatives"], 1):
print(f" {i}. {alt}")
if result.get("explanation"):
print(create_section_header("💡 WHY THIS WORKS", 70))
print(f" {result['explanation']}\n")
if result.get("posting_tips"):
print(create_section_header("⏰ POSTING TIPS", 70))
print(f" {result['posting_tips']}\n")
print()
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Follower Intelligence Module for Creator Insights
Engager-first approach: discovers who actually interacts with your content,
then enriches with follower status and influence scoring.
"""
import argparse
import sys
import json
from typing import Dict, List, Optional, Tuple
from collections import defaultdict
from api_client import APIClient
from ascii_formatter import (
create_header, create_section_header, create_ranking_list,
create_info_panel, format_number, colorize, Colors
)
class FollowerIntelligence:
"""Analyzes engagement and influence using an engager-first approach"""
def __init__(self, api_client: APIClient):
self.api_client = api_client
def analyze_vip_followers(self, username: str, num_tweets: int = 20,
max_followers: int = 500) -> Dict:
"""
Discover who engages with your content and rank by influence.
Pipeline: collect engagers from tweets → fetch followers for tagging →
calculate influence scores → categorize.
Args:
username: Twitter username to analyze
num_tweets: Number of recent tweets to analyze for engagement
max_followers: Maximum followers to fetch for is_follower tagging
Returns:
Dict with engager analysis, hidden gems, and follower overlap
"""
print(f"Analyzing engagement for @{username}...", file=sys.stderr)
# Step 1: Fetch recent tweets
print(f"Fetching last {num_tweets} tweets...", file=sys.stderr)
tweets = self.api_client.get_twitter_user_timeline(username, num_tweets)
if not tweets:
return {"error": "Could not fetch tweets"}
# Step 2: Collect all engagers (engager-first approach)
print(f"Collecting engagers from {len(tweets)} tweets...", file=sys.stderr)
engager_map = {} # str(user_id) -> profile + engagement counts
for i, tweet in enumerate(tweets[:num_tweets], 1):
print(f" Scanning tweet {i}/{min(num_tweets, len(tweets))}...", file=sys.stderr, end='\r')
tweet_id = tweet["id"]
# Get retweeters — each comes with full user profile
retweeters = self.api_client.get_tweet_retweeters(tweet_id, max_results=100)
for user in retweeters:
uid = str(user["id"])
if uid not in engager_map:
engager_map[uid] = {
"username": user["username"],
"name": user["name"],
"followers": user["followers"],
"verified": user["verified"],
"retweets": 0,
"replies": 0,
"total_interactions": 0
}
engager_map[uid]["retweets"] += 1
engager_map[uid]["total_interactions"] += 1
# Get repliers — author data embedded in each reply
replies = self.api_client.get_tweet_replies(tweet_id, max_results=100)
for reply in replies:
author = reply.get("author", {})
uid = str(author.get("id"))
if not uid or uid == "None":
continue
if uid not in engager_map:
engager_map[uid] = {
"username": author.get("username", "Unknown"),
"name": author.get("name", "Unknown"),
"followers": author.get("followers", 0),
"verified": author.get("verified", False),
"retweets": 0,
"replies": 0,
"total_interactions": 0
}
engager_map[uid]["replies"] += 1
engager_map[uid]["total_interactions"] += 1
print(f"\nFound {len(engager_map)} unique engagers", file=sys.stderr)
if not engager_map:
return {
"username": username,
"stats": {
"total_engagers": 0,
"engagers_who_follow": 0,
"unique_retweeters": 0,
"unique_repliers": 0
},
"message": "No engagers found on recent tweets. The account may have low engagement or tweets may be too new."
}
# Step 3: Fetch followers for is_follower tagging
print(f"Fetching up to {max_followers} followers for follower tagging...", file=sys.stderr)
followers = self.api_client.get_user_followers(username, max_followers)
follower_ids = {str(f["id"]) for f in followers} if followers else set()
# Step 4: Build results — score and categorize
all_engagers = []
for uid, data in engager_map.items():
is_follower = uid in follower_ids
influence_score = (data["followers"] * 0.7) + (data["total_interactions"] * 1000 * 0.3)
all_engagers.append({
"username": data["username"],
"name": data["name"],
"followers": data["followers"],
"verified": data["verified"],
"is_follower": is_follower,
"engagement": {
"retweets": data["retweets"],
"replies": data["replies"],
"total_interactions": data["total_interactions"]
},
"influence_score": int(influence_score),
"profile_url": f"https://twitter.com/{data['username']}"
})
# Sort by influence score
all_engagers.sort(key=lambda x: x["influence_score"], reverse=True)
# Calculate statistics
total_engagers = len(all_engagers)
engagers_who_follow = len([e for e in all_engagers if e["is_follower"]])
unique_retweeters = len([uid for uid, d in engager_map.items() if d["retweets"] > 0])
unique_repliers = len([uid for uid, d in engager_map.items() if d["replies"] > 0])
return {
"username": username,
"stats": {
"total_engagers": total_engagers,
"engagers_who_follow": engagers_who_follow,
"follow_rate": (engagers_who_follow / total_engagers * 100) if total_engagers > 0 else 0,
"unique_retweeters": unique_retweeters,
"unique_repliers": unique_repliers,
"tweets_analyzed": min(num_tweets, len(tweets)),
"followers_checked": len(follower_ids)
},
"top_engagers": all_engagers[:50],
"hidden_gems": self._find_hidden_gems(all_engagers),
"most_active": self._find_most_active(all_engagers),
"follower_engagers": self._find_follower_engagers(all_engagers)
}
def _find_hidden_gems(self, engagers: List[Dict], max_followers: int = 5000) -> List[Dict]:
"""
Find 'hidden gems' - engagers with low follower count but repeated engagement.
Args:
engagers: List of engager data
max_followers: Maximum follower count to be considered a "gem"
Returns:
List of hidden gem engagers
"""
gems = [
e for e in engagers
if e["followers"] < max_followers and e["engagement"]["total_interactions"] >= 2
]
gems.sort(key=lambda x: x["engagement"]["total_interactions"], reverse=True)
return gems[:10]
def _find_most_active(self, engagers: List[Dict]) -> List[Dict]:
"""
Find most active engagers by raw interaction count.
Args:
engagers: List of engager data
Returns:
List of most active engagers
"""
active = sorted(engagers, key=lambda x: x["engagement"]["total_interactions"], reverse=True)
return active[:20]
def _find_follower_engagers(self, engagers: List[Dict]) -> List[Dict]:
"""
Find engagers who are also followers — your most loyal audience.
Args:
engagers: List of engager data
Returns:
List of engagers who follow the account
"""
follower_engagers = [e for e in engagers if e["is_follower"]]
follower_engagers.sort(key=lambda x: x["influence_score"], reverse=True)
return follower_engagers[:20]
def analyze_follower_growth(self, username: str, sample_size: int = 200) -> Dict:
"""
Analyze follower quality and growth patterns.
Args:
username: Twitter username
sample_size: Number of recent followers to analyze
Returns:
Dict with follower growth analysis
"""
print(f"Analyzing follower growth for @{username}...", file=sys.stderr)
# Get recent followers (newest first)
followers = self.api_client.get_user_followers(username, sample_size)
if not followers:
return {"error": "Could not fetch followers"}
# Enrich follower profiles — the followers endpoint returns 0 for follower counts
print(f"Enriching {len(followers)} follower profiles...", file=sys.stderr)
follower_ids = [str(f["id"]) for f in followers if f.get("id")]
# Batch in chunks of 100 (API pricing is better at 100+)
for i in range(0, len(follower_ids), 100):
chunk = follower_ids[i:i+100]
enriched = self.api_client.batch_get_user_by_userids(chunk)
if enriched:
enriched_map = {str(u["id"]): u for u in enriched}
for follower in followers:
uid = str(follower["id"])
if uid in enriched_map:
follower.update(enriched_map[uid])
print(f" Enriched batch {i//100 + 1} ({len(enriched)} profiles)", file=sys.stderr)
# Analyze follower quality
total_followers = len(followers)
verified_count = len([f for f in followers if f["verified"]])
avg_followers = sum(f["followers"] for f in followers) / total_followers if total_followers > 0 else 0
# Categorize followers by size
categories = {
"micro": 0, # < 1K
"small": 0, # 1K-10K
"medium": 0, # 10K-100K
"large": 0, # 100K-1M
"mega": 0 # > 1M
}
for follower in followers:
count = follower["followers"]
if count < 1000:
categories["micro"] += 1
elif count < 10000:
categories["small"] += 1
elif count < 100000:
categories["medium"] += 1
elif count < 1000000:
categories["large"] += 1
else:
categories["mega"] += 1
return {
"username": username,
"total_analyzed": total_followers,
"verified_followers": verified_count,
"verified_percentage": (verified_count / total_followers * 100) if total_followers > 0 else 0,
"avg_follower_count": int(avg_followers),
"categories": categories,
"category_percentages": {
cat: (count / total_followers * 100) if total_followers > 0 else 0
for cat, count in categories.items()
},
"recent_followers": followers[:20]
}
def main():
"""CLI interface for Follower Intelligence"""
parser = argparse.ArgumentParser(description="Analyze engagement and follower influence")
parser.add_argument("--username", required=True, help="Twitter username to analyze (without @)")
parser.add_argument("--tweets", type=int, default=20, help="Number of recent tweets to analyze for engagement")
parser.add_argument("--max-followers", type=int, default=500, help="Maximum followers to fetch for tagging")
parser.add_argument("--growth", action="store_true", help="Analyze follower growth and quality")
parser.add_argument("--output", choices=['json', 'text'], default='text', help="Output format")
args = parser.parse_args()
# Initialize
api_client = APIClient()
analyzer = FollowerIntelligence(api_client)
# Execute analysis
if args.growth:
result = analyzer.analyze_follower_growth(args.username, args.max_followers)
else:
result = analyzer.analyze_vip_followers(args.username, args.tweets, args.max_followers)
# Output results
if args.output == 'json':
print(json.dumps(result, indent=2))
else:
if "error" in result:
print(f"Error: {result['error']}", file=sys.stderr)
sys.exit(1)
if args.growth:
# Follower growth analysis (unchanged)
print(create_header(f" FOLLOWER GROWTH: @{args.username} ", 70))
stats = [
f"Total Analyzed: {colorize(str(result['total_analyzed']), Colors.BRIGHT_CYAN)}",
f"Verified Followers: {colorize(str(result['verified_followers']), Colors.BRIGHT_GREEN)} ({result['verified_percentage']:.1f}%)",
f"Avg Follower Count: {colorize(format_number(result['avg_follower_count']), Colors.BRIGHT_YELLOW)}"
]
print(create_info_panel("📊", "Growth Statistics", stats, 70))
print(create_section_header("👥 FOLLOWER CATEGORIES", 70))
categories = result["categories"]
category_items = []
for i, (cat, count) in enumerate(categories.items(), 1):
label = f"{cat.capitalize()} ({count} followers)"
value = count
category_items.append((i, label, value))
print(create_ranking_list(category_items, "", 70, show_bars=True))
else:
# Engager-first analysis
print(create_header(f" ENGAGEMENT INTELLIGENCE: @{args.username} ", 70))
stats_data = result["stats"]
stats = [
f"Unique Engagers: {colorize(str(stats_data['total_engagers']), Colors.BRIGHT_CYAN)}",
f"Retweeters: {colorize(str(stats_data['unique_retweeters']), Colors.BRIGHT_GREEN)} | Repliers: {colorize(str(stats_data['unique_repliers']), Colors.BRIGHT_GREEN)}",
f"Engagers Who Follow: {colorize(str(stats_data['engagers_who_follow']), Colors.BRIGHT_YELLOW)} ({stats_data['follow_rate']:.1f}%)",
f"Tweets Analyzed: {stats_data['tweets_analyzed']} | Followers Checked: {format_number(stats_data['followers_checked'])}"
]
print(create_info_panel("📊", "Engagement Statistics", stats, 70))
# Top engagers by influence
if result["top_engagers"]:
engager_items = []
for i, engager in enumerate(result["top_engagers"][:10], 1):
verified_badge = "✓ " if engager["verified"] else ""
follow_badge = " [F]" if engager["is_follower"] else ""
label = f"{verified_badge}@{engager['username']}{follow_badge} ({format_number(engager['followers'])})"
value = engager["influence_score"]
engager_items.append((i, label, value))
print("\n" + create_ranking_list(engager_items, "TOP ENGAGERS (BY INFLUENCE)", 70, show_bars=True))
# Detail on #1 engager
top = result["top_engagers"][0]
follow_text = "follows you" if top["is_follower"] else "does not follow you"
print(f"\n {'Top Engager Details:':^70}")
print(f" {'-' * 68}")
top_username = top["username"]
print(f" 👤 {colorize(f'@{top_username}', Colors.BRIGHT_CYAN)} ({top['name']})")
print(f" 👥 Followers: {colorize(format_number(top['followers']), Colors.BRIGHT_GREEN)} | {follow_text}")
eng = top["engagement"]
print(f" 💬 {eng['retweets']} RTs | {eng['replies']} replies | {eng['total_interactions']} total")
print(f" 🏆 Influence Score: {colorize(format_number(top['influence_score']), Colors.BRIGHT_YELLOW)}")
# Hidden gems
if result["hidden_gems"]:
print(create_section_header("💎 HIDDEN GEMS (High Engagement, Low Followers)", 70))
gem_items = []
for i, gem in enumerate(result["hidden_gems"][:5], 1):
follow_badge = " [F]" if gem["is_follower"] else ""
label = f"@{gem['username']}{follow_badge} ({format_number(gem['followers'])})"
value = gem["engagement"]["total_interactions"] * 100
gem_items.append((i, label, value))
print(create_ranking_list(gem_items, "", 70, show_bars=True))
# Most active
if result["most_active"]:
print(create_section_header("🔥 MOST ACTIVE (By Interactions)", 70))
for i, engager in enumerate(result["most_active"][:5], 1):
eng = engager["engagement"]
follow_badge = " [F]" if engager["is_follower"] else ""
print(f" {i}. @{engager['username']}{follow_badge}")
print(f" {eng['retweets']} RTs | {eng['replies']} replies | {eng['total_interactions']} total")
# Follower engagers
if result["follower_engagers"]:
print(create_section_header("🤝 LOYAL FOLLOWERS (Follow + Engage)", 70))
for i, engager in enumerate(result["follower_engagers"][:5], 1):
eng = engager["engagement"]
verified_badge = "✓ " if engager["verified"] else ""
print(f" {i}. {verified_badge}@{engager['username']} ({format_number(engager['followers'])} followers)")
print(f" {eng['total_interactions']} interactions | Influence: {format_number(engager['influence_score'])}")
print()
if __name__ == "__main__":
main()