""" Ollama Cloud API client for script generation. Uses OpenAI-compatible API at https://ollama.com/v1 Ollama Cloud tiers (as of 2026): Free — smaller models, 1 concurrent, light usage Pro ($20/mo) — all models, 3 concurrent, 50x usage Max ($100/mo) — all models, 10 concurrent, 250x usage """ import json import re from openai import OpenAI # Models tagged by estimated tier access. # Top = likely Free tier (smaller models). Bottom = may need Pro/Max. # If a model returns 403, the user needs to upgrade or pick a Free-tier model. OLLAMA_CLOUD_MODELS = [ # --- 🟢 Likely Free tier (smaller models) --- "gemma3:4b", "gemma3:12b", "gemma3:27b", "ministral-3:3b", "ministral-3:8b", "ministral-3:14b", "gpt-oss:20b", "rnj-1:8b", "devstral-small-2:24b", "nemotron-3-nano:30b", # --- 🔵 May need Pro/Max subscription --- "gemma4:31b", "gemini-3-flash-preview", "deepseek-v4-flash", "deepseek-v3.2", "deepseek-v3.1:671b", "gpt-oss:120b", "qwen3.5:397b", "qwen3-coder:480b", "qwen3-coder-next", "qwen3-next:80b", "qwen3-vl:235b", "qwen3-vl:235b-instruct", "mistral-large-3:675b", "devstral-2:123b", "minimax-m2.7", "minimax-m2.5", "minimax-m2.1", "minimax-m2", "kimi-k2.6", "kimi-k2.5", "kimi-k2:1t", "kimi-k2-thinking", "nemotron-3-super", "glm-5.1", "glm-5", "glm-4.7", "glm-4.6", "cogito-2.1:671b", ] # Default model — small, likely on free tier DEFAULT_MODEL = "gemma3:12b" def get_ollama_client(api_key: str, base_url: str = "https://ollama.com/v1") -> OpenAI: """Create an OpenAI-compatible client for Ollama cloud.""" return OpenAI(base_url=base_url, api_key=api_key) def get_model_name(dropdown_value: str, custom_model: str) -> str: """Resolve model name: use custom input if provided, else dropdown.""" if custom_model and custom_model.strip(): return custom_model.strip() return dropdown_value or DEFAULT_MODEL def extract_json_from_text(text: str) -> str: """Extract JSON array or object from LLM response text.""" patterns = [r'\[[\s\S]*\]', r'\{[\s\S]*\}'] for pattern in patterns: matches = re.findall(pattern, text) for match in matches: try: parsed = json.loads(match) if isinstance(parsed, list) and len(parsed) > 0: return match elif isinstance(parsed, dict): return match except json.JSONDecodeError: continue return text def _friendly_api_error(e: Exception, model: str) -> str: """Turn raw API errors into user-friendly messages.""" err_str = str(e) if "403" in err_str or "subscription" in err_str.lower() or "upgrade" in err_str.lower(): return ( f"Model '{model}' requires a paid Ollama subscription.\n\n" f"Options:\n" f" - Pick a Free-tier model (top of dropdown): gemma3:4b, gemma3:12b, gpt-oss:20b, etc.\n" f" - Upgrade your Ollama plan at https://ollama.com/upgrade\n" f" - Use a custom endpoint with your own model in the 'Custom Model Name' field." ) if "401" in err_str or "unauthorized" in err_str.lower() or "invalid" in err_str.lower(): return ( f"Invalid Ollama API key. Please check your key at https://ollama.com/settings" ) if "404" in err_str or "not found" in err_str.lower(): return ( f"Model '{model}' not found. Check the model name or try one from the dropdown." ) if "rate" in err_str.lower() or "429" in err_str: return ( f"Rate limit reached. Ollama free tier has limited usage. " f"Wait a few minutes or upgrade at https://ollama.com/upgrade" ) return f"Script generation failed: {err_str}" def generate_viral_script( topic: str, research_text: str, key_facts: list[str], model: str, api_key: str, additional_notes: str = "", theme: str = "white", num_scenes: int = 6, target_duration: int = 60, base_url: str = "https://ollama.com/v1", ) -> dict: """Generate a viral video script using the Ollama cloud model.""" client = get_ollama_client(api_key, base_url) facts_text = "\n".join([f"- {fact}" for fact in key_facts[:15]]) system_prompt = """You are an elite viral video scriptwriter who creates content that gets millions of views on Instagram Reels, TikTok, and YouTube Shorts. Your scripts follow these VIRAL FORMULAS: 1. HOOK (first 3 seconds): Start with a pattern interrupt - a shocking fact, bold claim, or curiosity gap 2. TENSION: Build curiosity with "but here's what most people don't know..." 3. VALUE BOMBS: Deliver surprising, specific facts that make people say "I didn't know that!" 4. ENGAGEMENT: Use conversational tone, direct address ("you"), rhetorical questions 5. CTA: End with a call-to-action that drives engagement Style rules: - Short punchy sentences (max 15 words each) - No filler words or boring intros - Each scene must have a VISUAL that matches a single clear image search query - Write narration that sounds natural when spoken aloud (TTS-friendly) - Use power words: "secret", "shocking", "actually", "here's why", "nobody talks about" """ user_prompt = f"""Create a {target_duration}-second viral video script about: **{topic}** {f'Additional context: {additional_notes}' if additional_notes else ''} Theme: {theme} (design the visual mood accordingly) Here's the research I gathered: {research_text[:4000]} Key facts found: {facts_text} Generate EXACTLY {num_scenes} scenes as a JSON array. Each scene MUST have: - "scene_number": integer (1 to {num_scenes}) - "duration": integer (seconds for this scene, total must equal {target_duration}) - "hook": string (bold caption text shown on screen, MAX 10 words, attention-grabbing) - "narration": string (what the TTS voice will say, conversational and engaging, 2-3 sentences) - "visual": string (specific image search query for finding a matching stock photo, e.g. "close up of DNA double helix blue glow" not just "science") RULES: - Scene 1 MUST be a powerful hook that stops scrolling - Last scene MUST have a call-to-action - Each narration should flow naturally into the next - Visual descriptions must be SPECIFIC enough to find good stock photos - Total duration of all scenes must equal {target_duration} seconds Return ONLY the JSON array, no other text. Example format: [ {{"scene_number": 1, "duration": 8, "hook": "Your brain does THIS while sleeping", "narration": "What if I told you your brain is more active while you sleep than when you're awake? Sounds crazy right? But science says it's true.", "visual": "human brain neural activity glowing blue dark background"}} ]""" try: response = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], temperature=0.8, max_tokens=4000, ) content = response.choices[0].message.content if "" in content: content = re.sub(r'[\s\S]*?', '', content).strip() json_str = extract_json_from_text(content) scenes = json.loads(json_str) validated_scenes = [] for scene in scenes: validated = { "scene_number": scene.get("scene_number", len(validated_scenes) + 1), "duration": max(scene.get("duration", target_duration // num_scenes), 3), "hook": str(scene.get("hook", ""))[:80], "narration": str(scene.get("narration", "")), "visual": str(scene.get("visual", topic)), "imageUrl": "", } validated_scenes.append(validated) total_dur = sum(s["duration"] for s in validated_scenes) if total_dur != target_duration and validated_scenes: diff = target_duration - total_dur validated_scenes[-1]["duration"] = max(validated_scenes[-1]["duration"] + diff, 3) full_narration = " ".join([s["narration"] for s in validated_scenes]) return { "scenes": validated_scenes, "full_narration": full_narration, "raw_response": content, } except Exception as e: raise RuntimeError(_friendly_api_error(e, model)) def generate_title(topic: str, model: str, api_key: str, base_url: str = "https://ollama.com/v1") -> str: """Generate an engaging video title.""" client = get_ollama_client(api_key, base_url) try: response = client.chat.completions.create( model=model, messages=[ {"role": "system", "content": "Generate a short, catchy, viral video title. Return ONLY the title text, nothing else. Max 8 words."}, {"role": "user", "content": f"Topic: {topic}"}, ], temperature=0.9, max_tokens=50, ) title = response.choices[0].message.content.strip().strip('"').strip("'") return title[:80] except: return topic[:80]