Spaces:
Sleeping
Sleeping
Commit ·
38098b4
1
Parent(s): 30aa031
feat: Add multi-platform social media analysis with 7 platform-specific endpoints
Browse files- Add 7 new platform analyzers: YouTube, Instagram, LinkedIn, Facebook, X/Twitter, TikTok, Pinterest
- Create shared model loader with .env configurable MODEL_ID (0.5B/1.5B/7B)
- Refactor services with shared JSON repair and validation utilities
- Update schemas with 7 platform-specific Pydantic response models
- Add temperature tuning per platform (precise 0.3 to creative 0.45)
- Implement professional error handling with safe_analyze wrapper
- Add .env.example for model configuration documentation
- Keep backward-compatible original /analyze-seo route
- .env.example +6 -0
- .gitignore +2 -0
- main.py +100 -23
- models/schemas.py +133 -0
- services/analyzer.py +42 -153
- services/facebook_analyzer.py +50 -0
- services/instagram_analyzer.py +54 -0
- services/linkedin_analyzer.py +50 -0
- services/model_loader.py +43 -0
- services/pinterest_analyzer.py +48 -0
- services/tiktok_analyzer.py +48 -0
- services/twitter_analyzer.py +47 -0
- services/utils.py +52 -0
- services/youtube_analyzer.py +52 -0
.env.example
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Model Configuration
|
| 2 |
+
# Options:
|
| 3 |
+
# Qwen/Qwen2.5-0.5B-Instruct (Nano - ~1GB, fastest, CPU-friendly)
|
| 4 |
+
# Qwen/Qwen2.5-1.5B-Instruct (Goldilocks - ~3GB, recommended for GPU)
|
| 5 |
+
# Qwen/Qwen2.5-7B-Instruct (Smartest - needs 4-bit quantization + GPU)
|
| 6 |
+
MODEL_ID=Qwen/Qwen2.5-0.5B-Instruct
|
.gitignore
CHANGED
|
@@ -3,3 +3,5 @@ __pycache__/
|
|
| 3 |
.env
|
| 4 |
venv/
|
| 5 |
.DS_Store
|
|
|
|
|
|
|
|
|
| 3 |
.env
|
| 4 |
venv/
|
| 5 |
.DS_Store
|
| 6 |
+
*.safetensors
|
| 7 |
+
*.bin
|
main.py
CHANGED
|
@@ -1,38 +1,115 @@
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
-
from models.schemas import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
from services.analyzer import analyze_seo_content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
import uvicorn
|
| 5 |
-
import
|
| 6 |
|
| 7 |
app = FastAPI(
|
| 8 |
-
title="
|
| 9 |
-
description="Professional
|
| 10 |
-
version="
|
| 11 |
)
|
| 12 |
|
| 13 |
-
|
| 14 |
-
def read_root():
|
| 15 |
-
return {"status": "active", "service": "SEO Analyzer API"}
|
| 16 |
|
| 17 |
-
|
| 18 |
-
def analyze_seo(request: SEORequest):
|
| 19 |
-
"""
|
| 20 |
-
Analyzes the provided content and returns a detailed SEO strategy.
|
| 21 |
-
"""
|
| 22 |
try:
|
| 23 |
-
|
| 24 |
-
result
|
| 25 |
-
|
| 26 |
-
if "error" in result and result["error"]:
|
| 27 |
-
print(f"❌ Logic Error: {result['error']}")
|
| 28 |
raise HTTPException(status_code=500, detail=str(result["error"]))
|
| 29 |
-
|
| 30 |
return result
|
|
|
|
|
|
|
| 31 |
except Exception as e:
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
if __name__ == "__main__":
|
| 38 |
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
+
from models.schemas import (
|
| 3 |
+
SEORequest, SEOResponse,
|
| 4 |
+
YouTubeResponse, InstagramResponse, LinkedInResponse,
|
| 5 |
+
FacebookResponse, TwitterResponse, TikTokResponse, PinterestResponse
|
| 6 |
+
)
|
| 7 |
from services.analyzer import analyze_seo_content
|
| 8 |
+
from services.youtube_analyzer import analyze_youtube
|
| 9 |
+
from services.instagram_analyzer import analyze_instagram
|
| 10 |
+
from services.linkedin_analyzer import analyze_linkedin
|
| 11 |
+
from services.facebook_analyzer import analyze_facebook
|
| 12 |
+
from services.twitter_analyzer import analyze_twitter
|
| 13 |
+
from services.tiktok_analyzer import analyze_tiktok
|
| 14 |
+
from services.pinterest_analyzer import analyze_pinterest
|
| 15 |
import uvicorn
|
| 16 |
+
import traceback
|
| 17 |
|
| 18 |
app = FastAPI(
|
| 19 |
+
title="Social Media SEO Analyzer API",
|
| 20 |
+
description="Professional social media content analysis using Generative AI. Covers YouTube, Instagram, LinkedIn, Facebook, X/Twitter, TikTok, and Pinterest.",
|
| 21 |
+
version="2.0.0"
|
| 22 |
)
|
| 23 |
|
| 24 |
+
ERROR_MASK = "An internal error occurred. Please try again later."
|
|
|
|
|
|
|
| 25 |
|
| 26 |
+
def safe_analyze(analyze_fn, content: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
try:
|
| 28 |
+
result = analyze_fn(content)
|
| 29 |
+
if isinstance(result, dict) and result.get("error"):
|
|
|
|
|
|
|
|
|
|
| 30 |
raise HTTPException(status_code=500, detail=str(result["error"]))
|
|
|
|
| 31 |
return result
|
| 32 |
+
except HTTPException:
|
| 33 |
+
raise
|
| 34 |
except Exception as e:
|
| 35 |
+
print(f"CRITICAL SERVER ERROR: {e}\n{traceback.format_exc()}")
|
| 36 |
+
raise HTTPException(status_code=500, detail=ERROR_MASK)
|
| 37 |
+
|
| 38 |
+
# ---------------------------------------------------------------------------
|
| 39 |
+
# Root
|
| 40 |
+
# ---------------------------------------------------------------------------
|
| 41 |
+
|
| 42 |
+
@app.get("/")
|
| 43 |
+
def read_root():
|
| 44 |
+
return {"status": "active", "service": "Social Media SEO Analyzer API", "version": "2.0.0"}
|
| 45 |
+
|
| 46 |
+
# ---------------------------------------------------------------------------
|
| 47 |
+
# Generic SEO
|
| 48 |
+
# ---------------------------------------------------------------------------
|
| 49 |
+
|
| 50 |
+
@app.post("/analyze-seo", response_model=SEOResponse)
|
| 51 |
+
def analyze_seo(request: SEORequest):
|
| 52 |
+
return safe_analyze(analyze_seo_content, request.content)
|
| 53 |
+
|
| 54 |
+
# ---------------------------------------------------------------------------
|
| 55 |
+
# YouTube
|
| 56 |
+
# ---------------------------------------------------------------------------
|
| 57 |
+
|
| 58 |
+
@app.post("/analyze/youtube", response_model=YouTubeResponse)
|
| 59 |
+
def analyze_youtube_route(request: SEORequest):
|
| 60 |
+
return safe_analyze(analyze_youtube, request.content)
|
| 61 |
+
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
# Instagram
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
|
| 66 |
+
@app.post("/analyze/instagram", response_model=InstagramResponse)
|
| 67 |
+
def analyze_instagram_route(request: SEORequest):
|
| 68 |
+
return safe_analyze(analyze_instagram, request.content)
|
| 69 |
+
|
| 70 |
+
# ---------------------------------------------------------------------------
|
| 71 |
+
# LinkedIn
|
| 72 |
+
# ---------------------------------------------------------------------------
|
| 73 |
+
|
| 74 |
+
@app.post("/analyze/linkedin", response_model=LinkedInResponse)
|
| 75 |
+
def analyze_linkedin_route(request: SEORequest):
|
| 76 |
+
return safe_analyze(analyze_linkedin, request.content)
|
| 77 |
+
|
| 78 |
+
# ---------------------------------------------------------------------------
|
| 79 |
+
# Facebook
|
| 80 |
+
# ---------------------------------------------------------------------------
|
| 81 |
+
|
| 82 |
+
@app.post("/analyze/facebook", response_model=FacebookResponse)
|
| 83 |
+
def analyze_facebook_route(request: SEORequest):
|
| 84 |
+
return safe_analyze(analyze_facebook, request.content)
|
| 85 |
+
|
| 86 |
+
# ---------------------------------------------------------------------------
|
| 87 |
+
# X / Twitter
|
| 88 |
+
# ---------------------------------------------------------------------------
|
| 89 |
+
|
| 90 |
+
@app.post("/analyze/twitter", response_model=TwitterResponse)
|
| 91 |
+
def analyze_twitter_route(request: SEORequest):
|
| 92 |
+
return safe_analyze(analyze_twitter, request.content)
|
| 93 |
+
|
| 94 |
+
# ---------------------------------------------------------------------------
|
| 95 |
+
# TikTok
|
| 96 |
+
# ---------------------------------------------------------------------------
|
| 97 |
+
|
| 98 |
+
@app.post("/analyze/tiktok", response_model=TikTokResponse)
|
| 99 |
+
def analyze_tiktok_route(request: SEORequest):
|
| 100 |
+
return safe_analyze(analyze_tiktok, request.content)
|
| 101 |
+
|
| 102 |
+
# ---------------------------------------------------------------------------
|
| 103 |
+
# Pinterest
|
| 104 |
+
# ---------------------------------------------------------------------------
|
| 105 |
+
|
| 106 |
+
@app.post("/analyze/pinterest", response_model=PinterestResponse)
|
| 107 |
+
def analyze_pinterest_route(request: SEORequest):
|
| 108 |
+
return safe_analyze(analyze_pinterest, request.content)
|
| 109 |
+
|
| 110 |
+
# ---------------------------------------------------------------------------
|
| 111 |
+
# Entrypoint
|
| 112 |
+
# ---------------------------------------------------------------------------
|
| 113 |
|
| 114 |
if __name__ == "__main__":
|
| 115 |
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
|
models/schemas.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
from pydantic import BaseModel
|
| 2 |
from typing import List, Optional
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
class KeywordData(BaseModel):
|
| 5 |
keyword: str
|
| 6 |
search_volume: str
|
|
@@ -22,3 +26,132 @@ class SEOResponse(BaseModel):
|
|
| 22 |
content_titles: List[str]
|
| 23 |
target_audience: str
|
| 24 |
error: Optional[str] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from pydantic import BaseModel
|
| 2 |
from typing import List, Optional
|
| 3 |
|
| 4 |
+
# ---------------------------------------------------------------------------
|
| 5 |
+
# Existing generic SEO
|
| 6 |
+
# ---------------------------------------------------------------------------
|
| 7 |
+
|
| 8 |
class KeywordData(BaseModel):
|
| 9 |
keyword: str
|
| 10 |
search_volume: str
|
|
|
|
| 26 |
content_titles: List[str]
|
| 27 |
target_audience: str
|
| 28 |
error: Optional[str] = None
|
| 29 |
+
|
| 30 |
+
# ---------------------------------------------------------------------------
|
| 31 |
+
# YouTube
|
| 32 |
+
# ---------------------------------------------------------------------------
|
| 33 |
+
|
| 34 |
+
class VideoTitle(BaseModel):
|
| 35 |
+
title: str
|
| 36 |
+
expected_ctr: str
|
| 37 |
+
|
| 38 |
+
class YouTubeResponse(BaseModel):
|
| 39 |
+
video_titles: List[VideoTitle]
|
| 40 |
+
tags: List[str]
|
| 41 |
+
description_template: str
|
| 42 |
+
thumbnail_ideas: List[str]
|
| 43 |
+
best_posting_time: str
|
| 44 |
+
engagement_strategies: List[str]
|
| 45 |
+
competition_gap_analysis: str
|
| 46 |
+
error: Optional[str] = None
|
| 47 |
+
|
| 48 |
+
# ---------------------------------------------------------------------------
|
| 49 |
+
# Instagram
|
| 50 |
+
# ---------------------------------------------------------------------------
|
| 51 |
+
|
| 52 |
+
class CaptionData(BaseModel):
|
| 53 |
+
caption: str
|
| 54 |
+
tone: str
|
| 55 |
+
|
| 56 |
+
class HashtagSets(BaseModel):
|
| 57 |
+
small: List[str]
|
| 58 |
+
medium: List[str]
|
| 59 |
+
large: List[str]
|
| 60 |
+
|
| 61 |
+
class ContentIdeas(BaseModel):
|
| 62 |
+
reels: List[str]
|
| 63 |
+
carousels: List[str]
|
| 64 |
+
stories: List[str]
|
| 65 |
+
|
| 66 |
+
class InstagramResponse(BaseModel):
|
| 67 |
+
captions: List[CaptionData]
|
| 68 |
+
hashtag_sets: HashtagSets
|
| 69 |
+
content_ideas: ContentIdeas
|
| 70 |
+
best_posting_time: str
|
| 71 |
+
growth_strategies: List[str]
|
| 72 |
+
error: Optional[str] = None
|
| 73 |
+
|
| 74 |
+
# ---------------------------------------------------------------------------
|
| 75 |
+
# LinkedIn
|
| 76 |
+
# ---------------------------------------------------------------------------
|
| 77 |
+
|
| 78 |
+
class PostDraft(BaseModel):
|
| 79 |
+
headline: str
|
| 80 |
+
body: str
|
| 81 |
+
hook: str
|
| 82 |
+
|
| 83 |
+
class LinkedInResponse(BaseModel):
|
| 84 |
+
post_drafts: List[PostDraft]
|
| 85 |
+
hashtags: List[str]
|
| 86 |
+
article_topics: List[str]
|
| 87 |
+
thought_leadership_angles: List[str]
|
| 88 |
+
best_posting_time: str
|
| 89 |
+
industry_insights: str
|
| 90 |
+
error: Optional[str] = None
|
| 91 |
+
|
| 92 |
+
# ---------------------------------------------------------------------------
|
| 93 |
+
# Facebook
|
| 94 |
+
# ---------------------------------------------------------------------------
|
| 95 |
+
|
| 96 |
+
class PostIdea(BaseModel):
|
| 97 |
+
type: str
|
| 98 |
+
content: str
|
| 99 |
+
|
| 100 |
+
class FacebookResponse(BaseModel):
|
| 101 |
+
post_ideas: List[PostIdea]
|
| 102 |
+
engagement_hooks: List[str]
|
| 103 |
+
hashtags: List[str]
|
| 104 |
+
ad_copy_suggestions: List[str]
|
| 105 |
+
best_posting_time: str
|
| 106 |
+
page_growth_tips: List[str]
|
| 107 |
+
error: Optional[str] = None
|
| 108 |
+
|
| 109 |
+
# ---------------------------------------------------------------------------
|
| 110 |
+
# X / Twitter
|
| 111 |
+
# ---------------------------------------------------------------------------
|
| 112 |
+
|
| 113 |
+
class TweetThread(BaseModel):
|
| 114 |
+
tweets: List[str]
|
| 115 |
+
theme: str
|
| 116 |
+
|
| 117 |
+
class TwitterResponse(BaseModel):
|
| 118 |
+
tweet_threads: List[TweetThread]
|
| 119 |
+
viral_hooks: List[str]
|
| 120 |
+
hashtags: List[str]
|
| 121 |
+
best_posting_time: str
|
| 122 |
+
engagement_tactics: List[str]
|
| 123 |
+
error: Optional[str] = None
|
| 124 |
+
|
| 125 |
+
# ---------------------------------------------------------------------------
|
| 126 |
+
# TikTok
|
| 127 |
+
# ---------------------------------------------------------------------------
|
| 128 |
+
|
| 129 |
+
class VideoConcept(BaseModel):
|
| 130 |
+
hook: str
|
| 131 |
+
script_snippet: str
|
| 132 |
+
sound_suggestion: str
|
| 133 |
+
|
| 134 |
+
class TikTokResponse(BaseModel):
|
| 135 |
+
video_concepts: List[VideoConcept]
|
| 136 |
+
trending_angles: List[str]
|
| 137 |
+
hashtags: List[str]
|
| 138 |
+
best_posting_time: str
|
| 139 |
+
viral_strategies: List[str]
|
| 140 |
+
error: Optional[str] = None
|
| 141 |
+
|
| 142 |
+
# ---------------------------------------------------------------------------
|
| 143 |
+
# Pinterest
|
| 144 |
+
# ---------------------------------------------------------------------------
|
| 145 |
+
|
| 146 |
+
class PinIdea(BaseModel):
|
| 147 |
+
title: str
|
| 148 |
+
description: str
|
| 149 |
+
keyword_focus: str
|
| 150 |
+
|
| 151 |
+
class PinterestResponse(BaseModel):
|
| 152 |
+
pin_ideas: List[PinIdea]
|
| 153 |
+
board_organization: List[str]
|
| 154 |
+
seo_keywords: List[str]
|
| 155 |
+
best_posting_time: str
|
| 156 |
+
traffic_strategies: List[str]
|
| 157 |
+
error: Optional[str] = None
|
services/analyzer.py
CHANGED
|
@@ -1,160 +1,49 @@
|
|
| 1 |
-
import
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
try:
|
| 18 |
-
# Initialize Local Pipeline
|
| 19 |
-
# optimization: torch_dtype=torch.float16 reduces RAM usage significantly
|
| 20 |
-
# device_map="auto" uses CPU or GPU if available
|
| 21 |
-
pipe = pipeline(
|
| 22 |
-
"text-generation",
|
| 23 |
-
model=MODEL_ID,
|
| 24 |
-
torch_dtype=torch.bfloat16, # Efficient for CPU
|
| 25 |
-
device_map="auto",
|
| 26 |
-
trust_remote_code=True
|
| 27 |
-
)
|
| 28 |
-
print("✅ Model Loaded Successfully!")
|
| 29 |
-
except Exception as e:
|
| 30 |
-
print(f"❌ Model Load Failed: {e}")
|
| 31 |
-
pipe = None
|
| 32 |
-
|
| 33 |
-
def clean_json_string(text: str) -> str:
|
| 34 |
-
"""Helper to extract JSON from markdown or raw text."""
|
| 35 |
-
if "```json" in text:
|
| 36 |
-
text = text.split("```json")[1].split("```")[0]
|
| 37 |
-
elif "```" in text:
|
| 38 |
-
text = text.split("```")[1].split("```")[0]
|
| 39 |
-
return text.strip()
|
| 40 |
-
|
| 41 |
-
def repair_json(json_str: str) -> str:
|
| 42 |
-
"""Attempt to repair truncated JSON by closing open brackets/braces."""
|
| 43 |
-
json_str = json_str.strip()
|
| 44 |
-
|
| 45 |
-
# Remove potentially invalid trailing content that isn't a closer
|
| 46 |
-
# (e.g. if it ends with "keyword": "val", -> we need to remove comma)
|
| 47 |
-
json_str = json_str.rstrip(", ")
|
| 48 |
-
|
| 49 |
-
# Balance brackets
|
| 50 |
-
open_braces = json_str.count("{")
|
| 51 |
-
close_braces = json_str.count("}")
|
| 52 |
-
open_brackets = json_str.count("[")
|
| 53 |
-
close_brackets = json_str.count("]")
|
| 54 |
-
|
| 55 |
-
if open_braces > close_braces:
|
| 56 |
-
json_str += "}" * (open_braces - close_braces)
|
| 57 |
-
|
| 58 |
-
if open_brackets > close_brackets:
|
| 59 |
-
json_str += "]" * (open_brackets - close_brackets)
|
| 60 |
-
|
| 61 |
-
return json_str
|
| 62 |
-
|
| 63 |
-
def validate_and_fill_data(data: dict) -> dict:
|
| 64 |
-
"""Ensure all required fields exist in the response."""
|
| 65 |
-
defaults = {
|
| 66 |
-
"core_keywords": [],
|
| 67 |
-
"related_phrases": [],
|
| 68 |
-
"viral_hashtags": [],
|
| 69 |
-
"content_titles": ["Content Strategy Guide", "SEO Optimization Tips", "Viral Content Ideas"], # Fallbacks
|
| 70 |
-
"strategy_tips": ["Focus on user intent.", "Optimize for long-tail keywords.", "Improve meta tags."], # Fallbacks
|
| 71 |
-
"target_audience": "General Audience interested in this topic."
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
for key, default_val in defaults.items():
|
| 75 |
-
if key not in data or data[key] is None:
|
| 76 |
-
data[key] = default_val
|
| 77 |
-
|
| 78 |
-
return data
|
| 79 |
|
| 80 |
def analyze_seo_content(content: str) -> dict:
|
| 81 |
-
"""
|
| 82 |
-
Analyzes content using LOCAL Qwen-0.5B.
|
| 83 |
-
"""
|
| 84 |
-
if pipe is None:
|
| 85 |
-
return {"error": "Model failed to load on server startup. Check logs."}
|
| 86 |
-
|
| 87 |
-
print(f"🧠 Running Local Inference on: {content[:50]}...")
|
| 88 |
-
|
| 89 |
-
# Qwen Chat Template
|
| 90 |
messages = [
|
| 91 |
-
{
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
"core_keywords": [
|
| 114 |
-
{{"keyword": "...", "search_volume": "...", "competition": "...", "relevance": 90}}, ... (Total 30 items)
|
| 115 |
-
],
|
| 116 |
-
"related_phrases": ["..."],
|
| 117 |
-
"viral_hashtags": [{{"tag": "#...", "post_count": "..."}}],
|
| 118 |
-
"content_titles": ["..."],
|
| 119 |
-
"strategy_tips": ["..."],
|
| 120 |
-
"target_audience": "..."
|
| 121 |
-
}}
|
| 122 |
-
"""
|
| 123 |
-
}
|
| 124 |
]
|
| 125 |
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
outputs = pipe(
|
| 129 |
-
messages,
|
| 130 |
-
max_new_tokens=1500,
|
| 131 |
-
do_sample=True,
|
| 132 |
-
temperature=0.3, # Low temp for strict JSON
|
| 133 |
-
top_p=0.9
|
| 134 |
-
)
|
| 135 |
-
|
| 136 |
-
content_text = outputs[0]["generated_text"][-1]["content"]
|
| 137 |
-
cleaned_json = clean_json_string(content_text)
|
| 138 |
-
|
| 139 |
-
try:
|
| 140 |
-
data = json.loads(cleaned_json)
|
| 141 |
-
except json.JSONDecodeError:
|
| 142 |
-
print("⚠️ JSON Parse Error. Attempting repair...")
|
| 143 |
-
cleaned_json = repair_json(cleaned_json)
|
| 144 |
-
try:
|
| 145 |
-
data = json.loads(cleaned_json)
|
| 146 |
-
except json.JSONDecodeError as final_err:
|
| 147 |
-
print(f"❌ Repair Failed: {final_err}")
|
| 148 |
-
return {
|
| 149 |
-
"error": "Optimization Failed. The model generated invalid JSON.",
|
| 150 |
-
"raw_output": content_text[:500] + "..." # Debug info
|
| 151 |
-
}
|
| 152 |
-
|
| 153 |
-
# Ensure Schema Compliance
|
| 154 |
-
data = validate_and_fill_data(data)
|
| 155 |
-
|
| 156 |
return data
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
return {"error": f"Local AI Failed: {str(e)}"}
|
|
|
|
| 1 |
+
from services.utils import run_analysis, validate_and_fill_data_defaults
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a World-Class SEO Strategist. Return ONLY valid JSON.
|
| 4 |
+
RULES:
|
| 5 |
+
1. Estimate 'search_volume' (High/Medium/Low).
|
| 6 |
+
2. 'competition' (Hard/Medium/Easy).
|
| 7 |
+
3. 'relevance' (0-100)."""
|
| 8 |
+
|
| 9 |
+
DEFAULTS = {
|
| 10 |
+
"core_keywords": [],
|
| 11 |
+
"related_phrases": [],
|
| 12 |
+
"viral_hashtags": [],
|
| 13 |
+
"content_titles": ["Content Strategy Guide", "SEO Optimization Tips", "Viral Content Ideas"],
|
| 14 |
+
"strategy_tips": ["Focus on user intent.", "Optimize for long-tail keywords.", "Improve meta tags."],
|
| 15 |
+
"target_audience": "General Audience interested in this topic."
|
| 16 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
def analyze_seo_content(content: str) -> dict:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
messages = [
|
| 20 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 21 |
+
{"role": "user", "content": f"""Analyze topic: "{content[:2000]}"
|
| 22 |
+
|
| 23 |
+
REQUIREMENTS:
|
| 24 |
+
- **Core Keywords**: 30 high-value keywords.
|
| 25 |
+
- **Related Phrases**: 10 variations.
|
| 26 |
+
- **Viral Hashtags**: 8 tags.
|
| 27 |
+
- **Content Titles**: 3 titles.
|
| 28 |
+
- **Strategy Tips**: 3 tips.
|
| 29 |
+
- **Target Audience**: Profile.
|
| 30 |
+
|
| 31 |
+
JSON OUTPUT STRUCTURE:
|
| 32 |
+
{{
|
| 33 |
+
"core_keywords": [
|
| 34 |
+
{{"keyword": "...", "search_volume": "...", "competition": "...", "relevance": 90}}, ... (Total 30 items)
|
| 35 |
+
],
|
| 36 |
+
"related_phrases": ["..."],
|
| 37 |
+
"viral_hashtags": [{{"tag": "#...", "post_count": "..."}}],
|
| 38 |
+
"content_titles": ["..."],
|
| 39 |
+
"strategy_tips": ["..."],
|
| 40 |
+
"target_audience": "..."
|
| 41 |
+
}}"""}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
]
|
| 43 |
|
| 44 |
+
data = run_analysis(messages, temperature=0.3)
|
| 45 |
+
if isinstance(data, dict) and "error" in data:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
return data
|
| 47 |
|
| 48 |
+
# Apply defaults for missing fields
|
| 49 |
+
return validate_and_fill_data_defaults(data, DEFAULTS)
|
|
|
services/facebook_analyzer.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from services.utils import run_analysis, validate_and_fill_data_defaults
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a Facebook Marketing Expert specializing in organic reach, Facebook Groups, Page growth, and advertising psychology. You understand the Facebook algorithm, meaningful interactions, and ad copy optimization. Return ONLY valid JSON.
|
| 4 |
+
|
| 5 |
+
RULES:
|
| 6 |
+
1. Post ideas must specify content type: text, image, video, link, poll, live
|
| 7 |
+
2. Engagement hooks must target emotional triggers (curiosity, outrage, inspiration, humor, nostalgia)
|
| 8 |
+
3. Ad copy must follow AIDA framework (Attention, Interest, Desire, Action)
|
| 9 |
+
4. Hashtags: 3-5 relevant tags maximum
|
| 10 |
+
5. Facebook algorithm prioritizes meaningful interactions (comments > shares > reactions)
|
| 11 |
+
6. Native video gets 10x more reach than external links"""
|
| 12 |
+
|
| 13 |
+
DEFAULTS = {
|
| 14 |
+
"post_ideas": [],
|
| 15 |
+
"engagement_hooks": [],
|
| 16 |
+
"hashtags": [],
|
| 17 |
+
"ad_copy_suggestions": [],
|
| 18 |
+
"best_posting_time": "1-3 PM EST on weekdays",
|
| 19 |
+
"page_growth_tips": ["Post consistently", "Engage in relevant Groups", "Go live weekly"]
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
def analyze_facebook(content: str) -> dict:
|
| 23 |
+
messages = [
|
| 24 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 25 |
+
{"role": "user", "content": f"""Analyze topic for Facebook strategy: "{content[:2000]}"
|
| 26 |
+
|
| 27 |
+
REQUIREMENTS:
|
| 28 |
+
- **Post Ideas**: 8 post ideas with type (text/image/video/link/poll/live) and full content
|
| 29 |
+
- **Engagement Hooks**: 10 hooks targeting different emotional triggers
|
| 30 |
+
- **Hashtags**: 5 optimized hashtags
|
| 31 |
+
- **Ad Copy Suggestions**: 3 ad copies following AIDA framework
|
| 32 |
+
- **Best Posting Time**: Optimal day and time
|
| 33 |
+
- **Page Growth Tips**: 5 actionable tips
|
| 34 |
+
|
| 35 |
+
JSON OUTPUT STRUCTURE:
|
| 36 |
+
{{
|
| 37 |
+
"post_ideas": [
|
| 38 |
+
{{"type": "video", "content": "..."}}, ... (Total 8 items)
|
| 39 |
+
],
|
| 40 |
+
"engagement_hooks": ["..."],
|
| 41 |
+
"hashtags": ["..."],
|
| 42 |
+
"ad_copy_suggestions": ["..."],
|
| 43 |
+
"best_posting_time": "...",
|
| 44 |
+
"page_growth_tips": ["..."]
|
| 45 |
+
}}"""}
|
| 46 |
+
]
|
| 47 |
+
data = run_analysis(messages, temperature=0.35, max_new_tokens=2000)
|
| 48 |
+
if isinstance(data, dict) and "error" in data:
|
| 49 |
+
return data
|
| 50 |
+
return validate_and_fill_data_defaults(data, DEFAULTS)
|
services/instagram_analyzer.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from services.utils import run_analysis, validate_and_fill_data_defaults
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a top Instagram Growth Strategist and Content Creator. You specialize in the Instagram algorithm, visual aesthetics, Reels, Carousels, Stories, and viral marketing. Return ONLY valid JSON.
|
| 4 |
+
|
| 5 |
+
RULES:
|
| 6 |
+
1. Captions must have a strong hook in first line, value in middle, CTA at end
|
| 7 |
+
2. Hash tag sets must be tiered: small (10k-50k posts), medium (50k-500k), large (500k+)
|
| 8 |
+
3. Content ideas must specify format: Reel (<90s), Carousel (5-10 slides), Story (interactive)
|
| 9 |
+
4. Growth strategies focused on: collaboration, trending audio, consistent posting, engagement pods
|
| 10 |
+
5. Instagram algorithm prioritizes: saves > shares > comments > likes > reach"""
|
| 11 |
+
|
| 12 |
+
DEFAULTS = {
|
| 13 |
+
"captions": [],
|
| 14 |
+
"hashtag_sets": {"small": [], "medium": [], "large": []},
|
| 15 |
+
"content_ideas": {"reels": [], "carousels": [], "stories": []},
|
| 16 |
+
"best_posting_time": "9-11 AM EST on weekdays",
|
| 17 |
+
"growth_strategies": ["Post consistently", "Use trending audio", "Engage with niche community"]
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
def analyze_instagram(content: str) -> dict:
|
| 21 |
+
messages = [
|
| 22 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 23 |
+
{"role": "user", "content": f"""Analyze topic for Instagram strategy: "{content[:2000]}"
|
| 24 |
+
|
| 25 |
+
REQUIREMENTS:
|
| 26 |
+
- **Captions**: 5 full captions with different tones (educational, entertaining, inspirational, behind-the-scenes, promotional)
|
| 27 |
+
- **Hashtag Sets**: 10 small-tier, 10 medium-tier, 10 large-tier hashtags
|
| 28 |
+
- **Content Ideas**: 4 Reel concepts, 4 Carousel ideas, 4 Story concepts
|
| 29 |
+
- **Best Posting Time**: Optimal day and time
|
| 30 |
+
- **Growth Strategies**: 5 actionable strategies
|
| 31 |
+
|
| 32 |
+
JSON OUTPUT STRUCTURE:
|
| 33 |
+
{{
|
| 34 |
+
"captions": [
|
| 35 |
+
{{"caption": "...", "tone": "..."}}, ... (Total 5 items)
|
| 36 |
+
],
|
| 37 |
+
"hashtag_sets": {{
|
| 38 |
+
"small": ["#...", "#..."],
|
| 39 |
+
"medium": ["#...", "#..."],
|
| 40 |
+
"large": ["#...", "#..."]
|
| 41 |
+
}},
|
| 42 |
+
"content_ideas": {{
|
| 43 |
+
"reels": ["..."],
|
| 44 |
+
"carousels": ["..."],
|
| 45 |
+
"stories": ["..."]
|
| 46 |
+
}},
|
| 47 |
+
"best_posting_time": "...",
|
| 48 |
+
"growth_strategies": ["..."]
|
| 49 |
+
}}"""}
|
| 50 |
+
]
|
| 51 |
+
data = run_analysis(messages, temperature=0.4, max_new_tokens=2000)
|
| 52 |
+
if isinstance(data, dict) and "error" in data:
|
| 53 |
+
return data
|
| 54 |
+
return validate_and_fill_data_defaults(data, DEFAULTS)
|
services/linkedin_analyzer.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from services.utils import run_analysis, validate_and_fill_data_defaults
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a distinguished LinkedIn Content Strategist and Personal Branding Expert. You specialize in B2B networking, professional storytelling, thought leadership, and the LinkedIn algorithm. Return ONLY valid JSON.
|
| 4 |
+
|
| 5 |
+
RULES:
|
| 6 |
+
1. Post drafts must follow proven LinkedIn frameworks (hook → story → insight → CTA)
|
| 7 |
+
2. Headlines must be curiosity-driven or contrarian within professional bounds
|
| 8 |
+
3. Hashtags: max 5, mix of broad industry + niche specific
|
| 9 |
+
4. Article topics should position the author as a thought leader
|
| 10 |
+
5. Best post length: 900-1200 characters
|
| 11 |
+
6. LinkedIn algorithm favors: dwell time > comments > reactions > shares"""
|
| 12 |
+
|
| 13 |
+
DEFAULTS = {
|
| 14 |
+
"post_drafts": [],
|
| 15 |
+
"hashtags": [],
|
| 16 |
+
"article_topics": [],
|
| 17 |
+
"thought_leadership_angles": [],
|
| 18 |
+
"best_posting_time": "7-9 AM EST on weekdays (Tue-Thu best)",
|
| 19 |
+
"industry_insights": "Focus on providing unique data-driven perspectives in your niche."
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
def analyze_linkedin(content: str) -> dict:
|
| 23 |
+
messages = [
|
| 24 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 25 |
+
{"role": "user", "content": f"""Analyze topic for LinkedIn strategy: "{content[:2000]}"
|
| 26 |
+
|
| 27 |
+
REQUIREMENTS:
|
| 28 |
+
- **Post Drafts**: 5 full LinkedIn posts with headline, body, and hook
|
| 29 |
+
- **Hashtags**: 5 optimized hashtags
|
| 30 |
+
- **Article Topics**: 5 long-form article topic ideas
|
| 31 |
+
- **Thought Leadership Angles**: 5 unique perspectives/angles
|
| 32 |
+
- **Best Posting Time**: Optimal day and time
|
| 33 |
+
- **Industry Insights**: 1 paragraph of key industry observations
|
| 34 |
+
|
| 35 |
+
JSON OUTPUT STRUCTURE:
|
| 36 |
+
{{
|
| 37 |
+
"post_drafts": [
|
| 38 |
+
{{"headline": "...", "body": "...", "hook": "..."}}, ... (Total 5 items)
|
| 39 |
+
],
|
| 40 |
+
"hashtags": ["..."],
|
| 41 |
+
"article_topics": ["..."],
|
| 42 |
+
"thought_leadership_angles": ["..."],
|
| 43 |
+
"best_posting_time": "...",
|
| 44 |
+
"industry_insights": "..."
|
| 45 |
+
}}"""}
|
| 46 |
+
]
|
| 47 |
+
data = run_analysis(messages, temperature=0.3, max_new_tokens=2000)
|
| 48 |
+
if isinstance(data, dict) and "error" in data:
|
| 49 |
+
return data
|
| 50 |
+
return validate_and_fill_data_defaults(data, DEFAULTS)
|
services/model_loader.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import torch
|
| 3 |
+
from transformers import pipeline
|
| 4 |
+
from dotenv import load_dotenv
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
env_path = Path(__file__).resolve().parent.parent / ".env"
|
| 8 |
+
load_dotenv(dotenv_path=env_path)
|
| 9 |
+
|
| 10 |
+
MODEL_ID = os.getenv("MODEL_ID", "Qwen/Qwen2.5-0.5B-Instruct")
|
| 11 |
+
|
| 12 |
+
_pipe = None
|
| 13 |
+
|
| 14 |
+
def get_pipe():
|
| 15 |
+
global _pipe
|
| 16 |
+
if _pipe is None:
|
| 17 |
+
print(f"⏳ Loading Local Model {MODEL_ID}...")
|
| 18 |
+
try:
|
| 19 |
+
_pipe = pipeline(
|
| 20 |
+
"text-generation",
|
| 21 |
+
model=MODEL_ID,
|
| 22 |
+
torch_dtype=torch.bfloat16,
|
| 23 |
+
device_map="auto",
|
| 24 |
+
trust_remote_code=True
|
| 25 |
+
)
|
| 26 |
+
print("✅ Model Loaded Successfully!")
|
| 27 |
+
except Exception as e:
|
| 28 |
+
print(f"❌ Model Load Failed: {e}")
|
| 29 |
+
_pipe = None
|
| 30 |
+
return _pipe
|
| 31 |
+
|
| 32 |
+
def generate_text(messages, temperature=0.3, max_new_tokens=2000):
|
| 33 |
+
pipe = get_pipe()
|
| 34 |
+
if pipe is None:
|
| 35 |
+
return None
|
| 36 |
+
outputs = pipe(
|
| 37 |
+
messages,
|
| 38 |
+
max_new_tokens=max_new_tokens,
|
| 39 |
+
do_sample=True,
|
| 40 |
+
temperature=temperature,
|
| 41 |
+
top_p=0.9
|
| 42 |
+
)
|
| 43 |
+
return outputs[0]["generated_text"][-1]["content"]
|
services/pinterest_analyzer.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from services.utils import run_analysis, validate_and_fill_data_defaults
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a Pinterest SEO Specialist and Visual Marketing Expert who understands how to optimize Pins for search discovery, board strategy, and traffic generation. Return ONLY valid JSON.
|
| 4 |
+
|
| 5 |
+
RULES:
|
| 6 |
+
1. Pin ideas must have keyword-optimized titles and descriptions
|
| 7 |
+
2. Board organization: categorize by theme, audience, and funnel stage
|
| 8 |
+
3. SEO keywords: focus on long-tail, search-intent matching keywords
|
| 9 |
+
4. Pinterest is a VISUAL SEARCH ENGINE, not social media
|
| 10 |
+
5. Vertical 2:3 aspect ratio (1000 x 1500px) performs best
|
| 11 |
+
6. Keyword-rich descriptions in every Pin
|
| 12 |
+
7. Fresh content prioritized by algorithm"""
|
| 13 |
+
|
| 14 |
+
DEFAULTS = {
|
| 15 |
+
"pin_ideas": [],
|
| 16 |
+
"board_organization": [],
|
| 17 |
+
"seo_keywords": [],
|
| 18 |
+
"best_posting_time": "8-11 PM EST on weekends",
|
| 19 |
+
"traffic_strategies": ["Create Rich Pins", "Join group boards", "Use Idea Pins", "Seasonal planning"]
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
def analyze_pinterest(content: str) -> dict:
|
| 23 |
+
messages = [
|
| 24 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 25 |
+
{"role": "user", "content": f"""Analyze topic for Pinterest strategy: "{content[:2000]}"
|
| 26 |
+
|
| 27 |
+
REQUIREMENTS:
|
| 28 |
+
- **Pin Ideas**: 10 pins with keyword-optimized title, description, and keyword focus
|
| 29 |
+
- **Board Organization**: 5 board name suggestions with descriptions
|
| 30 |
+
- **SEO Keywords**: 20 long-tail keywords
|
| 31 |
+
- **Best Posting Time**: Optimal day and time
|
| 32 |
+
- **Traffic Strategies**: 5 strategies for driving traffic from Pinterest
|
| 33 |
+
|
| 34 |
+
JSON OUTPUT STRUCTURE:
|
| 35 |
+
{{
|
| 36 |
+
"pin_ideas": [
|
| 37 |
+
{{"title": "...", "description": "...", "keyword_focus": "..."}}, ... (Total 10 items)
|
| 38 |
+
],
|
| 39 |
+
"board_organization": ["..."],
|
| 40 |
+
"seo_keywords": ["..."],
|
| 41 |
+
"best_posting_time": "...",
|
| 42 |
+
"traffic_strategies": ["..."]
|
| 43 |
+
}}"""}
|
| 44 |
+
]
|
| 45 |
+
data = run_analysis(messages, temperature=0.3, max_new_tokens=2000)
|
| 46 |
+
if isinstance(data, dict) and "error" in data:
|
| 47 |
+
return data
|
| 48 |
+
return validate_and_fill_data_defaults(data, DEFAULTS)
|
services/tiktok_analyzer.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from services.utils import run_analysis, validate_and_fill_data_defaults
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a Viral TikTok Strategist who deeply understands the For You Page (FYP) algorithm, trend culture, and short-form video psychology. Return ONLY valid JSON.
|
| 4 |
+
|
| 5 |
+
RULES:
|
| 6 |
+
1. Video concepts must specify: hook (first 3 seconds), script snippet, and trending sound suggestion
|
| 7 |
+
2. Trending angles must reference current content patterns (day in life, tutorials, POV, commentary, transitions)
|
| 8 |
+
3. Hashtags: 3-5 mix of trending, niche, and broad
|
| 9 |
+
4. Hooks should create curiosity gaps with 80%+ estimated retention rates
|
| 10 |
+
5. TikTok algorithm: completion rate > rewatches > shares > comments > likes
|
| 11 |
+
6. Vertical 9:16 video, 21-60 seconds optimal length
|
| 12 |
+
7. First 2-3 seconds are CRITICAL for retention"""
|
| 13 |
+
|
| 14 |
+
DEFAULTS = {
|
| 15 |
+
"video_concepts": [],
|
| 16 |
+
"trending_angles": [],
|
| 17 |
+
"hashtags": [],
|
| 18 |
+
"best_posting_time": "7-9 AM or 7-10 PM local time",
|
| 19 |
+
"viral_strategies": ["Participate in trending challenges", "Use trending sounds", "Post 1-3x daily"]
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
def analyze_tiktok(content: str) -> dict:
|
| 23 |
+
messages = [
|
| 24 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 25 |
+
{"role": "user", "content": f"""Analyze topic for TikTok strategy: "{content[:2000]}"
|
| 26 |
+
|
| 27 |
+
REQUIREMENTS:
|
| 28 |
+
- **Video Concepts**: 8 concepts with hook, script snippet, and sound suggestion
|
| 29 |
+
- **Trending Angles**: 5 trending content angles for this topic
|
| 30 |
+
- **Hashtags**: 5 optimized hashtags (trending + niche + broad)
|
| 31 |
+
- **Best Posting Time**: Optimal times
|
| 32 |
+
- **Viral Strategies**: 5 strategies specific to this topic/niche
|
| 33 |
+
|
| 34 |
+
JSON OUTPUT STRUCTURE:
|
| 35 |
+
{{
|
| 36 |
+
"video_concepts": [
|
| 37 |
+
{{"hook": "...", "script_snippet": "...", "sound_suggestion": "..."}}, ... (Total 8 items)
|
| 38 |
+
],
|
| 39 |
+
"trending_angles": ["..."],
|
| 40 |
+
"hashtags": ["..."],
|
| 41 |
+
"best_posting_time": "...",
|
| 42 |
+
"viral_strategies": ["..."]
|
| 43 |
+
}}"""}
|
| 44 |
+
]
|
| 45 |
+
data = run_analysis(messages, temperature=0.45, max_new_tokens=2000)
|
| 46 |
+
if isinstance(data, dict) and "error" in data:
|
| 47 |
+
return data
|
| 48 |
+
return validate_and_fill_data_defaults(data, DEFAULTS)
|
services/twitter_analyzer.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from services.utils import run_analysis, validate_and_fill_data_defaults
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a Viral Twitter/X Strategist who understands the platform's fast-paced, text-first nature. You specialize in thread writing, engagement hacking, and building followings through value-driven content. Return ONLY valid JSON.
|
| 4 |
+
|
| 5 |
+
RULES:
|
| 6 |
+
1. Tweet threads must have a compelling hook tweet followed by value tweets and a strong CTA
|
| 7 |
+
2. Viral hooks must fit in 280 characters and create curiosity gaps
|
| 8 |
+
3. Hashtags: 1-2 max, used sparingly
|
| 9 |
+
4. Threads should have 5-10 tweets each
|
| 10 |
+
5. X algorithm prioritizes: replies > retweets > likes > views
|
| 11 |
+
6. Engaging with others' content = 40% of growth strategy"""
|
| 12 |
+
|
| 13 |
+
DEFAULTS = {
|
| 14 |
+
"tweet_threads": [],
|
| 15 |
+
"viral_hooks": [],
|
| 16 |
+
"hashtags": [],
|
| 17 |
+
"best_posting_time": "7-9 AM EST on weekdays",
|
| 18 |
+
"engagement_tactics": ["Reply to industry leaders", "Quote tweet with your take", "Use polls"]
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
def analyze_twitter(content: str) -> dict:
|
| 22 |
+
messages = [
|
| 23 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 24 |
+
{"role": "user", "content": f"""Analyze topic for X/Twitter strategy: "{content[:2000]}"
|
| 25 |
+
|
| 26 |
+
REQUIREMENTS:
|
| 27 |
+
- **Tweet Threads**: 3 complete threads (each 5-10 tweets) with different themes/angles
|
| 28 |
+
- **Viral Hooks**: 10 single-tweet hooks (under 280 chars each)
|
| 29 |
+
- **Hashtags**: 2 optimized hashtags
|
| 30 |
+
- **Best Posting Time**: Optimal day and time
|
| 31 |
+
- **Engagement Tactics**: 5 specific tactics
|
| 32 |
+
|
| 33 |
+
JSON OUTPUT STRUCTURE:
|
| 34 |
+
{{
|
| 35 |
+
"tweet_threads": [
|
| 36 |
+
{{"tweets": ["..."], "theme": "..."}}, ... (Total 3 items)
|
| 37 |
+
],
|
| 38 |
+
"viral_hooks": ["..."],
|
| 39 |
+
"hashtags": ["..."],
|
| 40 |
+
"best_posting_time": "...",
|
| 41 |
+
"engagement_tactics": ["..."]
|
| 42 |
+
}}"""}
|
| 43 |
+
]
|
| 44 |
+
data = run_analysis(messages, temperature=0.35, max_new_tokens=2000)
|
| 45 |
+
if isinstance(data, dict) and "error" in data:
|
| 46 |
+
return data
|
| 47 |
+
return validate_and_fill_data_defaults(data, DEFAULTS)
|
services/utils.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
def clean_json_string(text: str) -> str:
|
| 4 |
+
if "```json" in text:
|
| 5 |
+
text = text.split("```json")[1].split("```")[0]
|
| 6 |
+
elif "```" in text:
|
| 7 |
+
text = text.split("```")[1].split("```")[0]
|
| 8 |
+
return text.strip()
|
| 9 |
+
|
| 10 |
+
def repair_json(json_str: str) -> str:
|
| 11 |
+
json_str = json_str.strip()
|
| 12 |
+
json_str = json_str.rstrip(", ")
|
| 13 |
+
open_braces = json_str.count("{")
|
| 14 |
+
close_braces = json_str.count("}")
|
| 15 |
+
open_brackets = json_str.count("[")
|
| 16 |
+
close_brackets = json_str.count("]")
|
| 17 |
+
if open_braces > close_braces:
|
| 18 |
+
json_str += "}" * (open_braces - close_braces)
|
| 19 |
+
if open_brackets > close_brackets:
|
| 20 |
+
json_str += "]" * (open_brackets - close_brackets)
|
| 21 |
+
return json_str
|
| 22 |
+
|
| 23 |
+
def parse_and_repair(raw_text: str, max_preview=500):
|
| 24 |
+
cleaned = clean_json_string(raw_text)
|
| 25 |
+
try:
|
| 26 |
+
return json.loads(cleaned), None
|
| 27 |
+
except json.JSONDecodeError:
|
| 28 |
+
print("⚠️ JSON Parse Error. Attempting repair...")
|
| 29 |
+
repaired = repair_json(cleaned)
|
| 30 |
+
try:
|
| 31 |
+
return json.loads(repaired), None
|
| 32 |
+
except json.JSONDecodeError as e:
|
| 33 |
+
return None, {
|
| 34 |
+
"error": "Optimization Failed. The model generated invalid JSON.",
|
| 35 |
+
"raw_output": raw_text[:max_preview]
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
def run_analysis(messages, temperature=0.3, max_new_tokens=2000):
|
| 39 |
+
from services.model_loader import generate_text
|
| 40 |
+
raw = generate_text(messages, temperature, max_new_tokens)
|
| 41 |
+
if raw is None:
|
| 42 |
+
return {"error": "Model failed to load on server startup. Check logs."}
|
| 43 |
+
data, err = parse_and_repair(raw)
|
| 44 |
+
if err:
|
| 45 |
+
return err
|
| 46 |
+
return data
|
| 47 |
+
|
| 48 |
+
def validate_and_fill_data_defaults(data: dict, defaults: dict) -> dict:
|
| 49 |
+
for key, default_val in defaults.items():
|
| 50 |
+
if key not in data or data[key] is None:
|
| 51 |
+
data[key] = default_val
|
| 52 |
+
return data
|
services/youtube_analyzer.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from services.utils import run_analysis, validate_and_fill_data_defaults
|
| 2 |
+
|
| 3 |
+
SYSTEM_PROMPT = """You are a world-class YouTube SEO Strategist and Content Consultant. You know the YouTube algorithm inside out — CTR optimization, watch-time retention, keyword ranking, thumbnail psychology, and community engagement. Return ONLY valid JSON.
|
| 4 |
+
|
| 5 |
+
RULES:
|
| 6 |
+
1. Video titles must be click-worthy with high CTR potential (use numbers, power words, curiosity gaps, brackets)
|
| 7 |
+
2. Tags must include a mix of broad, mid-tail, and long-tail keywords
|
| 8 |
+
3. Description templates must include keyword placement, timestamps, and call-to-action
|
| 9 |
+
4. Thumbnail ideas should reference proven design principles (contrast, faces with emotion, text overlay)
|
| 10 |
+
5. Engagement strategies focus on: comments, polls, community tab, end screens, cards"""
|
| 11 |
+
|
| 12 |
+
DEFAULTS = {
|
| 13 |
+
"video_titles": [],
|
| 14 |
+
"tags": [],
|
| 15 |
+
"description_template": "In this video we explore [TOPIC]. Make sure to like and subscribe!",
|
| 16 |
+
"thumbnail_ideas": ["Close-up expressive face with bold text overlay"],
|
| 17 |
+
"best_posting_time": "2-4 PM EST on weekdays",
|
| 18 |
+
"engagement_strategies": ["Pin a comment with a question", "Use end screens to suggest next video"],
|
| 19 |
+
"competition_gap_analysis": "Focus on underserved long-tail subtopics within this niche."
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
def analyze_youtube(content: str) -> dict:
|
| 23 |
+
messages = [
|
| 24 |
+
{"role": "system", "content": SYSTEM_PROMPT},
|
| 25 |
+
{"role": "user", "content": f"""Analyze topic for YouTube strategy: "{content[:2000]}"
|
| 26 |
+
|
| 27 |
+
REQUIREMENTS:
|
| 28 |
+
- **Video Titles**: 15 high-CTR titles with expected CTR percentage
|
| 29 |
+
- **Tags**: 20 keywords (broad, mid-tail, long-tail)
|
| 30 |
+
- **Description Template**: 1 full SEO-optimized description template
|
| 31 |
+
- **Thumbnail Ideas**: 5 specific thumbnail concepts
|
| 32 |
+
- **Best Posting Time**: Optimal day and time
|
| 33 |
+
- **Engagement Strategies**: 5 tactics
|
| 34 |
+
- **Competition Gap Analysis**: 1 paragraph
|
| 35 |
+
|
| 36 |
+
JSON OUTPUT STRUCTURE:
|
| 37 |
+
{{
|
| 38 |
+
"video_titles": [
|
| 39 |
+
{{"title": "...", "expected_ctr": "..."}}, ... (Total 15 items)
|
| 40 |
+
],
|
| 41 |
+
"tags": ["..."],
|
| 42 |
+
"description_template": "...",
|
| 43 |
+
"thumbnail_ideas": ["..."],
|
| 44 |
+
"best_posting_time": "...",
|
| 45 |
+
"engagement_strategies": ["..."],
|
| 46 |
+
"competition_gap_analysis": "..."
|
| 47 |
+
}}"""}
|
| 48 |
+
]
|
| 49 |
+
data = run_analysis(messages, temperature=0.3, max_new_tokens=2000)
|
| 50 |
+
if isinstance(data, dict) and "error" in data:
|
| 51 |
+
return data
|
| 52 |
+
return validate_and_fill_data_defaults(data, DEFAULTS)
|