| """ |
| Script Generator using Groq and Gemini APIs |
| Generates story scripts from topics for TTS narration |
| Tries Groq first (works in all regions), falls back to Gemini |
| """ |
| import logging |
| import json |
| import os |
| from typing import Optional |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class ScriptGenerator: |
| """ |
| Generates story scripts using Groq API (primary) or Gemini API (fallback). |
| |
| Features: |
| - Topic → Full narration script (<=1000 chars) |
| - Character-aware script generation |
| - AI image prompt generation |
| - Optimized for TTS output |
| """ |
| |
| GROQ_MODEL = "openai/gpt-oss-120b" |
| GEMINI_MODEL = "gemini-2.0-flash" |
| |
| |
| SYSTEM_PROMPT = """You are a professional script writer for short-form video content (TikTok, Reels, Shorts). |
| |
| RULES: |
| 1. Write a narration script for the given topic |
| 2. Maximum 1000 characters (STRICT LIMIT) |
| 3. Write in a natural, engaging voice |
| 4. Focus on storytelling - beginning, middle, end |
| 5. Use simple, clear sentences for TTS |
| 6. NO emojis, NO hashtags, NO special formatting |
| 7. Output ONLY the script text, nothing else |
| |
| If a character is provided, write the story from their perspective or about them.""" |
|
|
| def __init__(self, gemini_api_key: str = None, groq_api_key: str = None): |
| self.gemini_api_key = gemini_api_key |
| self.groq_api_key = groq_api_key |
| |
| |
| self.groq_client = None |
| self.gemini_client = None |
| |
| if groq_api_key: |
| try: |
| from groq import Groq |
| self.groq_client = Groq(api_key=groq_api_key) |
| logger.info("Groq client initialized (primary)") |
| except ImportError: |
| logger.warning("Groq package not installed, using Gemini only") |
| |
| if gemini_api_key: |
| try: |
| from google import genai |
| self.gemini_client = genai.Client(api_key=gemini_api_key) |
| logger.info("Gemini client initialized (fallback)") |
| except ImportError: |
| logger.warning("google-genai package not installed") |
| |
| if not self.groq_client and not self.gemini_client: |
| raise ValueError("At least one API key (GROQ_API or GEMINI_API_KEY) is required") |
| |
| def generate_script( |
| self, |
| topic: str, |
| character_name: Optional[str] = None, |
| max_chars: int = 1000 |
| ) -> str: |
| """ |
| Generate a story script from topic. |
| Tries Groq first, falls back to Gemini if Groq fails. |
| """ |
| |
| user_prompt = f"Topic: {topic}" |
| |
| if character_name: |
| user_prompt += f"\nMain Character: {character_name}" |
| |
| user_prompt += f"\n\nWrite a short narration script (max {max_chars} characters)." |
| |
| logger.info(f"Generating script for topic: {topic[:50]}...") |
| |
| |
| if self.groq_client: |
| try: |
| script = self._generate_with_groq(user_prompt) |
| if len(script) > max_chars: |
| script = script[:max_chars].rsplit(' ', 1)[0] + "." |
| logger.info(f"Generated script with Groq: {len(script)} chars") |
| return script.strip() |
| except Exception as e: |
| logger.warning(f"Groq failed: {e}, trying Gemini...") |
| |
| |
| if self.gemini_client: |
| try: |
| script = self._generate_with_gemini(user_prompt) |
| if len(script) > max_chars: |
| script = script[:max_chars].rsplit(' ', 1)[0] + "." |
| logger.info(f"Generated script with Gemini: {len(script)} chars") |
| return script.strip() |
| except Exception as e: |
| logger.error(f"Gemini also failed: {e}") |
| raise Exception(f"Script generation failed: {e}") |
| |
| raise Exception("No AI backend available for script generation") |
| |
| def _generate_with_groq(self, user_prompt: str) -> str: |
| """Generate using Groq API""" |
| completion = self.groq_client.chat.completions.create( |
| model=self.GROQ_MODEL, |
| messages=[ |
| {"role": "system", "content": self.SYSTEM_PROMPT}, |
| {"role": "user", "content": user_prompt} |
| ], |
| temperature=0.7, |
| max_tokens=500, |
| top_p=0.9 |
| ) |
| return completion.choices[0].message.content |
| |
| def _generate_with_gemini(self, user_prompt: str) -> str: |
| """Generate using Gemini API""" |
| response = self.gemini_client.models.generate_content( |
| model=self.GEMINI_MODEL, |
| contents=self.SYSTEM_PROMPT + "\n\n" + user_prompt |
| ) |
| return response.text |
| |
| @staticmethod |
| def test_connection(gemini_api_key: str = None, groq_api_key: str = None) -> bool: |
| """Test API connection""" |
| try: |
| gen = ScriptGenerator(gemini_api_key=gemini_api_key, groq_api_key=groq_api_key) |
| gen.generate_script("test", max_chars=50) |
| return True |
| except: |
| return False |
| |
| |
| IMAGE_PROMPT_SYSTEM = """You are an expert at creating detailed image prompts for AI image generation. |
| |
| Your task: Generate detailed image prompts for each 2-second scene of a story video. |
| |
| CONTEXT: |
| - Full story script is provided so you understand the narrative |
| - Each 2-second chunk needs a visual prompt |
| - Images will play in SEQUENCE to tell a story |
| - All images MUST look like they belong to the SAME VIDEO |
| |
| CRITICAL RULES FOR CONSISTENCY: |
| 1. SAME STYLE: Every prompt MUST start with the exact style name (e.g., "semi-realistic style", "anime style", "sticky animation style") |
| 2. SAME CHARACTER: If a character is described, use IDENTICAL description in EVERY prompt (same clothes, hair, face features) |
| 3. SCENE CONTINUITY: Each scene should logically follow the previous one |
| - Example: Scene 1 "boy picking up bag" → Scene 2 "boy walking with bag on shoulder" → Scene 3 "boy approaching school gate" |
| 4. CONSISTENT LIGHTING: Use same lighting style across all scenes |
| 5. CONSISTENT COLOR PALETTE: Maintain similar color tones |
| |
| PROMPT STRUCTURE: |
| 1. [STYLE] - Always start with style (e.g., "semi-realistic style artwork") |
| 2. [CHARACTER] - Describe the character with exact same details every time |
| 3. [ACTION] - What's happening in THIS specific 2-second moment |
| 4. [ENVIRONMENT] - Where is this taking place |
| 5. [CAMERA] - Camera angle (close-up, medium shot, wide shot) |
| 6. [LIGHTING & MOOD] - Lighting and emotional atmosphere |
| 7. [QUALITY TAGS] - high quality, detailed, cinematic, 8k |
| |
| CONTINUITY TIPS: |
| - If character was sitting, show transition to standing (not jumping to running) |
| - Keep background elements consistent (same room, same street) |
| - Props should persist (if bag appeared, keep showing it) |
| - Time progression should be logical |
| |
| OUTPUT FORMAT: |
| Return ONLY valid JSON array, no markdown, no explanation: |
| [ |
| {"chunk_id": 1, "prompt": "detailed prompt here..."}, |
| {"chunk_id": 2, "prompt": "detailed prompt here..."} |
| ]""" |
|
|
| def generate_image_prompts( |
| self, |
| full_script: str, |
| chunks: list, |
| image_style: str = "semi-realistic", |
| max_batch: int = 30 |
| ) -> list: |
| """ |
| Generate detailed image prompts for all 2-second chunks. |
| """ |
| all_prompts = [] |
| total_chunks = len(chunks) |
| |
| for batch_start in range(0, total_chunks, max_batch): |
| batch_end = min(batch_start + max_batch, total_chunks) |
| batch_chunks = chunks[batch_start:batch_end] |
| |
| logger.info(f"Generating prompts for chunks {batch_start+1}-{batch_end} of {total_chunks}") |
| |
| |
| user_prompt = f"""FULL STORY SCRIPT: |
| {full_script} |
| |
| IMAGE STYLE: {image_style} |
| (Apply this style consistently to ALL images: {image_style}, high quality, detailed, cinematic lighting) |
| |
| """ |
| |
| user_prompt += "2-SECOND CHUNKS TO GENERATE PROMPTS FOR:\n" |
| for chunk in batch_chunks: |
| user_prompt += f"- Chunk {chunk['chunk_id']}: \"{chunk['text']}\"\n" |
| |
| user_prompt += "\nGenerate detailed image prompts for each chunk. Return ONLY JSON array." |
| |
| try: |
| |
| if self.groq_client: |
| text = self._generate_image_prompts_groq(user_prompt) |
| elif self.gemini_client: |
| text = self._generate_image_prompts_gemini(user_prompt) |
| else: |
| raise Exception("No AI backend available") |
| |
| |
| text = text.strip() |
| if text.startswith("```"): |
| text = text.split("```")[1] |
| if text.startswith("json"): |
| text = text[4:] |
| text = text.strip() |
| |
| |
| batch_prompts = json.loads(text) |
| all_prompts.extend(batch_prompts) |
| |
| logger.info(f"Generated {len(batch_prompts)} prompts in batch") |
| |
| except json.JSONDecodeError as e: |
| logger.error(f"Failed to parse JSON response: {e}") |
| for chunk in batch_chunks: |
| all_prompts.append({ |
| "chunk_id": chunk["chunk_id"], |
| "prompt": f"{chunk['text']}, semi-realistic style, high quality, detailed" |
| }) |
| except Exception as e: |
| logger.error(f"AI API error: {e}") |
| for chunk in batch_chunks: |
| all_prompts.append({ |
| "chunk_id": chunk["chunk_id"], |
| "prompt": f"{chunk['text']}, semi-realistic style, high quality" |
| }) |
| |
| logger.info(f"Generated {len(all_prompts)} total image prompts") |
| return all_prompts |
| |
| def _generate_image_prompts_groq(self, user_prompt: str) -> str: |
| """Generate image prompts using Groq""" |
| completion = self.groq_client.chat.completions.create( |
| model=self.GROQ_MODEL, |
| messages=[ |
| {"role": "system", "content": self.IMAGE_PROMPT_SYSTEM}, |
| {"role": "user", "content": user_prompt} |
| ], |
| temperature=0.7, |
| max_tokens=4000, |
| top_p=0.9 |
| ) |
| return completion.choices[0].message.content |
| |
| def _generate_image_prompts_gemini(self, user_prompt: str) -> str: |
| """Generate image prompts using Gemini""" |
| response = self.gemini_client.models.generate_content( |
| model=self.GEMINI_MODEL, |
| contents=self.IMAGE_PROMPT_SYSTEM + "\n\n" + user_prompt |
| ) |
| return response.text |
|
|