#!/usr/bin/env python3 """ Content Transform — Repurpose long-form content into platform-native drafts. Reads content atoms, generates platform-native drafts using Claude API + optional expert panel quality gate. Supports X threads/posts, LinkedIn, YouTube Shorts, and newsletter formats. LLM mode is DEFAULT. Use --template-only for fast template-based drafts (no API needed). Usage: python content-transform.py --atoms atoms.json --top-n 10 python content-transform.py --atoms atoms.json --template-only python content-transform.py --atoms atoms.json --no-expert-panel """ import json import uuid import argparse import os import re import sys import textwrap from datetime import datetime, timezone from pathlib import Path # ── Configuration ── SCRIPT_DIR = Path(__file__).resolve().parent PROJECT_DIR = SCRIPT_DIR.parent DATA_DIR = Path(os.environ.get("CONTENT_OPS_DATA_DIR", PROJECT_DIR / "data")) SKILL_DIR = PROJECT_DIR ATOMS_FILE = DATA_DIR / "content-atoms-latest.json" # Voice configuration files (optional, for LLM mode) VOICE_CONFIG_FILE = os.environ.get("VOICE_CONFIG_FILE", str(PROJECT_DIR / "config" / "voice.md")) STYLE_GUIDE_FILE = os.environ.get("STYLE_GUIDE_FILE", str(PROJECT_DIR / "config" / "style-guide.md")) PLATFORM_MAP = { "x": ["x_thread", "x_post"], "linkedin": ["linkedin_post"], "short_form": ["youtube_short_script"], "newsletter": ["newsletter_section"], "youtube_short": ["youtube_short_script"], } MISSING_TO_FORMAT = { "x": "x_thread", "linkedin": "linkedin_post", "short_form": "youtube_short_script", "newsletter": "newsletter_section", "youtube_short": "youtube_short_script", } MISSING_TO_PLATFORM = { "x": "x", "linkedin": "linkedin", "short_form": "youtube_short", "newsletter": "newsletter", "youtube_short": "youtube_short", } PLATFORM_TO_EXPERT = { "x": "x-articles.md", "linkedin": "linkedin.md", "youtube_short": "youtube-shorts.md", "newsletter": "newsletter.md", } EXPERT_PANEL_THRESHOLD = 95 EXPERT_PANEL_MAX_ITERATIONS = 3 def load_atoms(path=None): p = Path(path) if path else ATOMS_FILE with open(p) as f: data = json.load(f) return data.get("atoms", data) if isinstance(data, dict) else data def rank_atoms(atoms, top_n=10): """Sort by repurpose_score * len(platforms_missing), take top N.""" for a in atoms: a["_rank"] = a.get("repurpose_score", 0) * max(len(a.get("platforms_missing", [])), 1) ranked = sorted(atoms, key=lambda x: x["_rank"], reverse=True) return ranked[:top_n] def clean_content(content): content = re.sub(r'^[\w]+\s*·\s*@[\w]+\s*·.*$', '', content, flags=re.MULTILINE) content = re.sub(r'\n{3,}', '\n\n', content) return content.strip() def extract_hook(content, max_chars=200): content = clean_content(content) for sep in [". ", ".\n", "\n"]: idx = content.find(sep) if 0 < idx < max_chars: return content[:idx + 1].strip() return content[:max_chars].strip() def extract_key_points(content, max_points=6): lines = content.split("\n") points = [] for line in lines: line = line.strip() if not line: continue if line.startswith(("•", "-", "→", "*")) or re.match(r"^\d+[\.\)]", line): cleaned = re.sub(r"^[•\-→\*\d+\.\)]+\s*", "", line).strip() if len(cleaned) > 15: points.append(cleaned) elif len(line) > 20 and len(line) < 280: points.append(line) return points[:max_points] if points else [content[:200]] def extract_numbers(content): patterns = [ r'\$[\d,]+[KkMmBb]?(?:\+)?', r'\d+%', r'\d+x', r'\d+[\.,]?\d*\s*(?:hours?|minutes?|days?|weeks?|months?|years?)', r'\d+\s*(?:pages?|pieces?|tools?|agents?|companies|founders?|members)', ] numbers = [] for p in patterns: numbers.extend(re.findall(p, content, re.IGNORECASE)) return numbers[:5] def shorten_sentence(s, max_words=15): words = s.split() if len(words) <= max_words: return s return " ".join(words[:max_words]) + "." def make_punchy(text, max_words=15): sentences = re.split(r'(?<=[.!?])\s+', text) result = [] for s in sentences: s = s.strip() if not s: continue if len(s.split()) > max_words: parts = re.split(r'[,;—]', s) for p in parts: p = p.strip() if p: result.append(shorten_sentence(p) if not p.endswith(('.', '!', '?')) else p) else: result.append(s) return result # ── TEMPLATE GENERATORS (used with --template-only) ── def generate_x_thread(atom): content = clean_content(atom["content"]) hook_text = extract_hook(content, 200) points = extract_key_points(content) numbers = extract_numbers(content) tags = atom.get("tags", []) atom_type = atom.get("atom_type", "") if "data" in atom_type or numbers: tweet1 = f"{hook_text}\n\nThe numbers tell a different story. 🧵" elif "story" in atom_type or "anecdote" in atom_type: tweet1 = f"{hook_text}\n\nHere's what happened next. 🧵" else: tweet1 = f"Most people get this wrong about {tags[0] if tags else 'this'}.\n\n{hook_text}" if len(tweet1) > 280: tweet1 = tweet1[:277] + "..." tweets = [tweet1] for i, point in enumerate(points[:5]): point_short = shorten_sentence(point, 15) if numbers and i < len(numbers): tweet = f"{point_short}\n\n{numbers[i]} — that's the real number." else: tweet = point_short if len(tweet) > 280: tweet = tweet[:277] + "..." tweets.append(tweet) ctas = [ "What's your take? Reply with what you'd add.", "What did I miss? Drop your thoughts below.", "Agree or disagree? I want to hear your take.", ] tweets.append(ctas[hash(atom["id"]) % len(ctas)]) while len(tweets) < 5: tweets.insert(-1, "The gap is only getting wider. Those who move now win.") thread = "\n\n---\n\n".join([f"🧵 {i+1}/{len(tweets)}\n{t}" for i, t in enumerate(tweets)]) return thread, tweets[0] def generate_x_post(atom): content = clean_content(atom["content"]) hook = extract_hook(content, 180) numbers = extract_numbers(content) num_str = f"\n\n{numbers[0]}." if numbers else "" post = f"{hook}{num_str}\n\nWhat's your take?" if len(post) > 280: post = post[:277] + "..." return post, hook def generate_linkedin_post(atom): content = clean_content(atom["content"]) hook = extract_hook(content, 150) points = extract_key_points(content) numbers = extract_numbers(content) hook_section = f"{hook}\n\nHere's what I learned." punchy = make_punchy(content) story = "\n\n".join(punchy[:6]) point_section = "\n".join([f"→ {p}" for p in points[:4]]) if len(points) > 2 else "" data_section = f"\nThe data: {', '.join(numbers[:3])}." if numbers else "" ctas = [ "What would you do differently?", "What's your experience with this?", "Curious — what's your take?", ] cta = ctas[hash(atom["id"]) % len(ctas)] parts = [hook_section, story] if point_section: parts.append(point_section) if data_section: parts.append(data_section) parts.append(cta) post = "\n\n".join(parts) if len(post) > 1500: post = post[:1497] + "..." return post, hook def generate_youtube_short(atom): content = clean_content(atom["content"]) hook = extract_hook(content, 100) points = extract_key_points(content) numbers = extract_numbers(content) tags = atom.get("tags", []) topic = tags[0] if tags else "this" hook_line = f"[HOOK] (0:00-0:03)\n[Look directly at camera, energy up]\n\"{hook}\"" setup_points = points[:2] setup_text = " ".join([shorten_sentence(p, 12) for p in setup_points]) setup_line = f"[SETUP] (0:03-0:13)\n[Cut to B-roll or screen share]\n\"{setup_text}\"" payoff_points = points[2:5] if len(points) > 2 else points payoff_items = "\n".join([f" → {shorten_sentence(p, 12)}" for p in payoff_points]) num_callout = f"\n[TEXT OVERLAY: {numbers[0]}]" if numbers else "" payoff_line = f"[PAYOFF] (0:13-0:40)\n[Quick cuts between points]{num_callout}\n{payoff_items}" cta_line = f"[CTA] (0:40-0:45)\n[Point at camera]\n\"Comment '{topic.upper()}' and I'll show you exactly how.\"\n[TEXT: Follow for more]" script = f"{hook_line}\n\n{setup_line}\n\n{payoff_line}\n\n{cta_line}" return script, hook def generate_newsletter_section(atom): content = clean_content(atom["content"]) hook = extract_hook(content, 150) points = extract_key_points(content) numbers = extract_numbers(content) headline = f"**{hook}**" punchy = make_punchy(content) para1 = " ".join(punchy[:4]) para2 = " ".join(punchy[4:8]) if len(punchy) > 4 else "" data = f"The numbers: {', '.join(numbers[:3])}." if numbers else "" why = f"> **Why this matters:** {shorten_sentence(points[-1] if points else content[:100], 15)}" parts = [headline, para1] if para2: parts.append(para2) if data: parts.append(data) parts.append(why) return "\n\n".join([p for p in parts if p.strip()]), hook FORMAT_GENERATORS = { "x_thread": generate_x_thread, "x_post": generate_x_post, "linkedin_post": generate_linkedin_post, "youtube_short_script": generate_youtube_short, "newsletter_section": generate_newsletter_section, } def estimate_engagement(atom, platform): score = atom.get("repurpose_score", 5) if score >= 8: return "high" elif score >= 5: return "medium" return "low" def generate_drafts_for_atom(atom): drafts = [] missing = atom.get("platforms_missing", []) for platform_key in missing: fmt = MISSING_TO_FORMAT.get(platform_key) platform = MISSING_TO_PLATFORM.get(platform_key) if not fmt or fmt not in FORMAT_GENERATORS: continue generator = FORMAT_GENERATORS[fmt] draft_text, hook = generator(atom) draft = { "id": str(uuid.uuid4()), "atom_id": atom["id"], "atom_content": atom["content"][:500], "atom_source": atom.get("source", "unknown"), "platform": platform, "format": fmt, "draft": draft_text, "hook": hook[:200], "char_count": len(draft_text), "estimated_engagement": estimate_engagement(atom, platform), "created_at": datetime.now(timezone.utc).isoformat(), "status": "draft", "expert_score": None, "iterations": 0, "key_improvements": [], } drafts.append(draft) return drafts # ── ANTHROPIC API ── def get_anthropic_key(): """Get Anthropic API key from environment.""" key = os.environ.get("ANTHROPIC_API_KEY") if key: return key print("ERROR: Set ANTHROPIC_API_KEY environment variable") return None def load_file_safe(path): """Load a text file, return empty string if missing.""" try: return Path(path).read_text() except Exception: return "" def load_expert_panel(platform): """Load expert panel for a platform.""" filename = PLATFORM_TO_EXPERT.get(platform, "x-articles.md") return load_file_safe(SKILL_DIR / "experts" / filename) def load_scoring_rubric(): """Load content quality scoring rubric.""" return load_file_safe(SKILL_DIR / "scoring-rubrics" / "content-quality.md") def load_voice_references(): """Load voice/style references for content generation.""" voice_config = load_file_safe(VOICE_CONFIG_FILE) style_guide = load_file_safe(STYLE_GUIDE_FILE) return voice_config, style_guide def call_anthropic(client, messages, system=None, model="claude-sonnet-4-20250514", max_tokens=2000): """Call Anthropic API.""" kwargs = {"model": model, "max_tokens": max_tokens, "messages": messages} if system: kwargs["system"] = system response = client.messages.create(**kwargs) return response.content[0].text.strip() def llm_generate_draft(client, atom, platform, fmt, voice_config, style_guide): """Generate a draft using Claude API.""" platform_instructions = { "x": "Write an X article (long-form X post). Include at least one ASCII diagram in a code block. Keep paragraphs to 1-3 sentences. End with a natural CTA.", "linkedin": "Write a LinkedIn post. Hook must work before the 'see more' fold (first 2-3 lines). Use line breaks for readability. Professional but personal. 800-1500 chars.", "youtube_short": "Write a YouTube Short script. Format: [HOOK] (0:00-0:03), [SETUP] (0:03-0:13), [PAYOFF] (0:13-0:40), [CTA] (0:40-0:45). Include visual directions. 30-60 seconds total.", "newsletter": "Write a newsletter section. Subject line + scannable body. Headers, bullets, bold for skimmers. End with 'why this matters'.", } system_parts = ["You are a content writer creating platform-native content. Follow the configured voice and style EXACTLY."] if voice_config: system_parts.append(f"\nVOICE CONFIGURATION:\n{voice_config}") if style_guide: system_parts.append(f"\nSTYLE GUIDE:\n{style_guide[:2000]}") system_parts.append(""" RULES: - Short punchy sentences. Max 15 words. - Specific numbers always. Never vague. - Contrarian angles backed by data. - No corporate speak. No "I'm excited to share." - Personal stories and specific examples. - Every sentence earns its place.""") system = "\n".join(system_parts) topic_tags = atom.get('tags', []) prompt = f"""Create a {platform} draft from this content atom. PLATFORM INSTRUCTIONS: {platform_instructions.get(platform, platform_instructions['x'])} SOURCE CONTENT: {clean_content(atom['content'])} SOURCE: {atom.get('source_title', 'unknown')} TAGS: {', '.join(topic_tags)} Write ONLY the draft content. No preamble, no explanation.""" return call_anthropic(client, [{"role": "user", "content": prompt}], system=system) def expert_panel_score(client, draft_text, platform, expert_panel, rubric, voice_config): """Run expert panel scoring. Returns (score, feedback_dict).""" system = f"""You are simulating 10 domain experts reviewing content for quality. EXPERT PANEL: {expert_panel} SCORING RUBRIC: {rubric} VOICE REFERENCE: {voice_config[:1000] if voice_config else 'No specific voice config provided.'}""" prompt = f"""Score this {platform} draft. Each of 11 experts scores 0-100 on the rubric criteria. Expert #11 is the AI Writing Detector (Humanizer) — scores how AI-generated the draft sounds. BANNED AI VOCABULARY (flag any occurrence): delve, tapestry, landscape (abstract), leverage, multifaceted, nuanced, pivotal, realm, robust, seamless, testament, transformative, underscore (verb), utilize, whilst, keen, embark, comprehensive, intricate, commendable, meticulous, paramount, groundbreaking, innovative, cutting-edge, synergy, holistic, paradigm, ecosystem, Additionally, crucial, enduring, enhance, fostering, garner, highlight (verb), interplay, intricacies, showcase, vibrant, valuable, profound, renowned, breathtaking, nestled, stunning AI PATTERNS TO CHECK: - Significance inflation ("pivotal moment", "is a testament", "stands as") - Superficial -ing phrases ("highlighting", "showcasing", "underscoring") - Promotional language ("boasts", "vibrant", "commitment to") - Vague attributions ("Experts believe", "Industry reports") - Formulaic "despite challenges... continues to" structures - Copula avoidance ("serves as" instead of "is") - Negative parallelisms ("It's not just X, it's Y") - Rule-of-three forcing (triple adjectives/clauses) - Em dash overuse (max 1 per 200 words) - Filler phrases ("In order to", "It is important to note") - Excessive hedging ("could potentially") - Generic positive conclusions ("The future looks bright") If the Humanizer expert scores below 70, the draft MUST be flagged for revision. DRAFT: {draft_text} Respond in this EXACT JSON format (no other text): {{ "average_score": , "expert_scores": [<11 numbers>], "weaknesses": ["", "", ...], "line_feedback": ["", "", ...], "strengths": ["", ""], "ai_patterns_detected": ["", "", ...], "humanizer_score": }} Be harsh. Score honestly.""" response = call_anthropic(client, [{"role": "user", "content": prompt}], system=system, max_tokens=1500) try: json_match = re.search(r'\{[\s\S]*\}', response) if json_match: result = json.loads(json_match.group()) return result.get("average_score", 0), result else: return 0, {"error": "No JSON in response"} except json.JSONDecodeError: return 0, {"error": "Invalid JSON", "raw": response[:500]} def expert_panel_revise(client, draft_text, platform, feedback, voice_config, style_guide): """Revise draft based on expert feedback.""" system_parts = ["You are revising content based on expert feedback."] if voice_config: system_parts.append(f"\nVOICE CONFIGURATION:\n{voice_config}") system_parts.append(""" RULES: - Fix every weakness identified - Keep all strengths - Maintain configured voice exactly - Short punchy sentences, specific numbers, contrarian angles""") system = "\n".join(system_parts) weaknesses = feedback.get("weaknesses", []) line_fixes = feedback.get("line_feedback", []) ai_patterns = feedback.get("ai_patterns_detected", []) ai_section = "" if ai_patterns: ai_section = f""" AI PATTERNS DETECTED (MUST FIX ALL): {chr(10).join(f'- {p}' for p in ai_patterns)} BANNED VOCABULARY (replace every occurrence): delve, tapestry, landscape (abstract), leverage, multifaceted, nuanced, pivotal, realm, robust, seamless, testament, transformative, underscore (verb), utilize, whilst, keen, embark, comprehensive, intricate, commendable, meticulous, paramount, groundbreaking, innovative, cutting-edge, synergy, holistic, paradigm, ecosystem, Additionally, crucial, enduring, enhance, fostering, garner, highlight (verb), interplay, intricacies, showcase, vibrant, valuable, profound, renowned, breathtaking, nestled, stunning """ prompt = f"""Revise this {platform} draft based on expert feedback. CURRENT DRAFT: {draft_text} WEAKNESSES TO FIX: {chr(10).join(f'- {w}' for w in weaknesses)} SPECIFIC LINE FIXES: {chr(10).join(f'- {f}' for f in line_fixes)} {ai_section} CURRENT SCORE: {feedback.get('average_score', 'unknown')} TARGET SCORE: {EXPERT_PANEL_THRESHOLD}+ Write ONLY the revised draft. No preamble.""" return call_anthropic(client, [{"role": "user", "content": prompt}], system=system) def process_draft_with_expert_panel(client, atom, platform, fmt, voice_config, style_guide): """Full expert panel pipeline: generate → score → revise loop.""" expert_panel = load_expert_panel(platform) rubric = load_scoring_rubric() print(f" Generating {platform} draft via Claude...") draft_text = llm_generate_draft(client, atom, platform, fmt, voice_config, style_guide) iterations = [] best_draft = draft_text best_score = 0 for iteration in range(1, EXPERT_PANEL_MAX_ITERATIONS + 1): print(f" Expert panel scoring (iteration {iteration})...") score, feedback = expert_panel_score(client, draft_text, platform, expert_panel, rubric, voice_config) print(f" Score: {score}/100") iteration_log = { "iteration": iteration, "score": score, "weaknesses": feedback.get("weaknesses", []), "line_feedback": feedback.get("line_feedback", []), "strengths": feedback.get("strengths", []), } iterations.append(iteration_log) if score > best_score: best_score = score best_draft = draft_text if score >= EXPERT_PANEL_THRESHOLD: print(f" ✓ Passed threshold ({score} >= {EXPERT_PANEL_THRESHOLD})") break if iteration < EXPERT_PANEL_MAX_ITERATIONS: print(f" Revising based on feedback...") draft_text = expert_panel_revise(client, draft_text, platform, feedback, voice_config, style_guide) key_improvements = [] for it in iterations: for w in it.get("weaknesses", []): key_improvements.append(f"Iter {it['iteration']}: Fixed — {w}") return best_draft, best_score, len(iterations), key_improvements, iterations def rewrite_with_llm(drafts, use_expert_panel=False, expert_panel_top_n=10): """Rewrite drafts using Claude API, optionally with expert panel.""" try: import anthropic except ImportError: print("ERROR: anthropic package not installed. Run: pip install anthropic") return drafts api_key = get_anthropic_key() if not api_key: return drafts client = anthropic.Anthropic(api_key=api_key) voice_config, style_guide = load_voice_references() rewritten = [] for i, draft in enumerate(drafts): atom = {"content": draft["atom_content"], "source_title": draft.get("atom_source", ""), "tags": [], "atom_type": ""} if use_expert_panel and i < expert_panel_top_n: print(f"\n [{i+1}/{len(drafts)}] Expert panel: {draft['format']} (atom {draft['atom_id'][:8]})") try: import time as _time _start = _time.time() new_text, score, iters, improvements, iter_log = process_draft_with_expert_panel( client, atom, draft["platform"], draft["format"], voice_config, style_guide ) _elapsed = _time.time() - _start draft["draft"] = new_text draft["hook"] = extract_hook(new_text, 200) draft["char_count"] = len(new_text) draft["expert_score"] = score draft["iterations"] = iters draft["key_improvements"] = improvements draft["iteration_log"] = iter_log status = "✓" if score >= EXPERT_PANEL_THRESHOLD else f"⚠ ({score})" print(f" {status} Final: {score}/100 after {iters} iteration(s) [{_elapsed:.1f}s]") except Exception as e: print(f" ✗ Expert panel failed ({type(e).__name__}): {e}") try: new_text = llm_generate_draft(client, atom, draft["platform"], draft["format"], voice_config, style_guide) draft["draft"] = new_text draft["hook"] = extract_hook(new_text, 200) draft["char_count"] = len(new_text) print(f" ↳ Fell back to simple LLM rewrite") except Exception as e2: print(f" ✗ LLM rewrite also failed: {e2}") else: print(f"\n [{i+1}/{len(drafts)}] LLM rewrite: {draft['format']} (atom {draft['atom_id'][:8]})") try: new_text = llm_generate_draft(client, atom, draft["platform"], draft["format"], voice_config, style_guide) draft["draft"] = new_text draft["hook"] = extract_hook(new_text, 200) draft["char_count"] = len(new_text) print(f" ✓ Rewrote") except Exception as e: print(f" ✗ LLM rewrite failed: {e}") rewritten.append(draft) return rewritten def main(): parser = argparse.ArgumentParser(description="Transform content atoms into platform-native drafts") parser.add_argument("--atoms", type=str, help="Path to atoms JSON file") parser.add_argument("--top-n", type=int, default=10, help="Number of top atoms to process") parser.add_argument("--template-only", action="store_true", help="Use template-based generation (no LLM)") parser.add_argument("--no-expert-panel", action="store_true", help="Disable expert panel quality gate") parser.add_argument("--expert-panel-top-n", type=int, default=10, help="Apply expert panel to top N drafts") parser.add_argument("--output", type=str, help="Output file path") args = parser.parse_args() use_llm = not args.template_only use_expert_panel = use_llm and not args.no_expert_panel atoms = load_atoms(args.atoms) print(f"Loaded {len(atoms)} atoms") top_atoms = rank_atoms(atoms, args.top_n) print(f"Selected top {len(top_atoms)} atoms by repurpose_score × missing platforms") all_drafts = [] for atom in top_atoms: drafts = generate_drafts_for_atom(atom) all_drafts.extend(drafts) missing = atom.get("platforms_missing", []) print(f" Atom {atom['id'][:8]}: {len(drafts)} drafts ({', '.join(missing)})") print(f"\nGenerated {len(all_drafts)} total drafts") if use_llm: mode = "LLM + Expert Panel" if use_expert_panel else "LLM only" print(f"\n{'='*60}") print(f"Rewriting with {mode}...") print(f"{'='*60}") all_drafts = rewrite_with_llm(all_drafts, use_expert_panel=use_expert_panel, expert_panel_top_n=args.expert_panel_top_n) by_platform = {} for d in all_drafts: by_platform[d["platform"]] = by_platform.get(d["platform"], 0) + 1 print(f"\n{'='*60}") print("Drafts by platform:") for p, c in sorted(by_platform.items()): print(f" {p}: {c}") scored = [d for d in all_drafts if d.get("expert_score")] if scored: avg = sum(d["expert_score"] for d in scored) / len(scored) passed = sum(1 for d in scored if d["expert_score"] >= EXPERT_PANEL_THRESHOLD) print(f"\nExpert panel: {len(scored)} scored, {passed} passed (≥{EXPERT_PANEL_THRESHOLD}), avg {avg:.1f}") today = datetime.now().strftime("%Y-%m-%d") output_path = Path(args.output) if args.output else DATA_DIR / f"content-drafts-{today}.json" latest_path = DATA_DIR / "content-drafts-latest.json" output = { "generated_at": datetime.now(timezone.utc).isoformat(), "atom_count": len(top_atoms), "draft_count": len(all_drafts), "used_llm": use_llm, "used_expert_panel": use_expert_panel, "expert_panel_threshold": EXPERT_PANEL_THRESHOLD if use_expert_panel else None, "drafts": all_drafts, } output_path.parent.mkdir(parents=True, exist_ok=True) with open(output_path, "w") as f: json.dump(output, f, indent=2) with open(latest_path, "w") as f: json.dump(output, f, indent=2) print(f"\nSaved to {output_path}") print(f"Saved to {latest_path}") if scored: print(f"\n{'='*60}") print("TOP DRAFTS BY SCORE:") print(f"{'='*60}") for d in sorted(scored, key=lambda x: x["expert_score"], reverse=True)[:5]: print(f"\n[{d['platform'].upper()}] Score: {d['expert_score']}/100 | Iterations: {d['iterations']}") print(f"Hook: {d['hook'][:100]}...") if d.get("key_improvements"): print(f"Key improvements: {d['key_improvements'][0]}") print(f"---") print(d["draft"][:300]) print("...\n") if __name__ == "__main__": main()