import os import uuid import time import json from google import genai from google.genai import types from PIL import Image def analyze_video_for_titles(api_key, video_path, transcript=None): """ Transcribes a video and uses Gemini to suggest viral YouTube titles. If transcript is provided, skips Whisper transcription. Returns: { "titles": [...], "transcript_summary": "...", "language": "...", "segments": [...], "video_duration": ... } """ if transcript is None: from main import transcribe_video print("🎬 [Thumbnail] Transcribing video...") transcript = transcribe_video(video_path) else: print("🎬 [Thumbnail] Using pre-computed transcript (Whisper already done)...") print("📤 [Thumbnail] Uploading video to Gemini...") client = genai.Client(api_key=api_key) file_upload = client.files.upload(file=video_path) while True: file_info = client.files.get(name=file_upload.name) if file_info.state == "ACTIVE": break elif file_info.state == "FAILED": raise Exception("Video processing failed by Gemini.") time.sleep(2) prompt = f"""You are a YouTube title expert who creates viral, click-worthy titles. Analyze this video and its transcript, then suggest 10 YouTube titles that would maximize CTR (click-through rate). TRANSCRIPT: {transcript['text']} RULES: - Titles must be under 70 characters - Use power words, curiosity gaps, and emotional triggers - Mix styles: how-to, listicle, story-driven, controversial, question-based - Make them specific to the actual content, not generic - Include numbers where appropriate - Consider the language of the video (detected: {transcript['language']}) - Titles should be in the SAME LANGUAGE as the video transcript Also provide a brief summary of the video content (2-3 sentences). After generating all 10 titles, pick the TOP 2 you most recommend and explain concisely WHY (CTR potential, emotional hook, uniqueness, etc.). Reference them by their 0-based index in the titles array. OUTPUT JSON: {{ "titles": ["title1", "title2", ...], "transcript_summary": "Brief summary of the video content...", "language": "{transcript['language']}", "recommended": [ {{"index": 0, "reason": "Why this title is best..."}}, {{"index": 3, "reason": "Why this title is second best..."}} ] }}""" print("🤖 [Thumbnail] Asking Gemini for title suggestions...") response = client.models.generate_content( model="gemini-2.5-flash", contents=[file_upload, prompt], config=types.GenerateContentConfig( response_mime_type="application/json" ) ) # Extract segments and duration from transcript for later use segments = transcript.get("segments", []) video_duration = segments[-1]["end"] if segments else 0 try: text = response.text.strip() if text.startswith("```json"): text = text[7:] if text.startswith("```"): text = text[3:] if text.endswith("```"): text = text[:-3] text = text.strip() start_idx = text.find('{') end_idx = text.rfind('}') if start_idx != -1 and end_idx != -1: text = text[start_idx:end_idx + 1] result = json.loads(text) result["transcript_summary"] = result.get("transcript_summary", "") result["language"] = result.get("language", transcript["language"]) result["segments"] = segments result["video_duration"] = video_duration return result except json.JSONDecodeError: print(f"❌ [Thumbnail] Failed to parse titles JSON: {response.text}") return { "titles": ["Could not generate titles - please try again"], "transcript_summary": transcript["text"][:500], "language": transcript["language"], "segments": segments, "video_duration": video_duration } def refine_titles(api_key, context, user_message, conversation_history=None): """ Takes video context + user feedback and returns refined title suggestions. """ client = genai.Client(api_key=api_key) history_text = "" if conversation_history: for msg in conversation_history: role = msg.get("role", "user") history_text += f"\n{role.upper()}: {msg['content']}" prompt = f"""You are a YouTube title expert. Based on the video context and the user's feedback, suggest 8 new refined YouTube titles. VIDEO CONTEXT: {context} CONVERSATION HISTORY:{history_text} USER'S NEW REQUEST: {user_message} RULES: - Titles must be under 70 characters - Incorporate the user's feedback/direction - Keep titles viral and click-worthy - If the user asks for a specific style, follow it - Titles should be in the same language as the original content OUTPUT JSON: {{ "titles": ["title1", "title2", ...] }}""" response = client.models.generate_content( model="gemini-2.5-flash", contents=[prompt], config=types.GenerateContentConfig( response_mime_type="application/json" ) ) try: text = response.text.strip() if text.startswith("```json"): text = text[7:] if text.startswith("```"): text = text[3:] if text.endswith("```"): text = text[:-3] text = text.strip() start_idx = text.find('{') end_idx = text.rfind('}') if start_idx != -1 and end_idx != -1: text = text[start_idx:end_idx + 1] return json.loads(text) except json.JSONDecodeError: print(f"❌ [Thumbnail] Failed to parse refined titles: {response.text}") return {"titles": ["Could not refine titles - please try again"]} def generate_thumbnail(api_key, title, session_id, face_image_path=None, bg_image_path=None, extra_prompt="", count=3, video_context=""): """ Generates YouTube thumbnails using Gemini image generation. Returns list of saved image paths (relative URLs). """ client = genai.Client(api_key=api_key) output_dir = os.path.join("output", "thumbnails", session_id) os.makedirs(output_dir, exist_ok=True) prompt_parts = [] # Add face image if provided if face_image_path and os.path.exists(face_image_path): face_img = Image.open(face_image_path) prompt_parts.append(face_img) # Add background image if provided if bg_image_path and os.path.exists(bg_image_path): bg_img = Image.open(bg_image_path) prompt_parts.append(bg_img) # Build video context block context_block = "" if video_context: context_block = f""" VIDEO CONTEXT (use this to understand the video and design a relevant thumbnail): {video_context} """ # Build extra instructions block (high priority) extra_block = "" if extra_prompt: extra_block = f""" ⚠️ MANDATORY USER INSTRUCTIONS (MUST follow these exactly — they override any default behavior): {extra_prompt} """ text_prompt = f"""Generate a professional, eye-catching YouTube thumbnail image. VIDEO TITLE (for reference — do NOT put the full title on the thumbnail): "{title}" {context_block} TEXT ON THE THUMBNAIL: - Based on the title AND the video context, create a SHORT visual hook: 1 to 5 words maximum - It should capture the core emotion, surprise, or promise of the video - The thumbnail text should COMPLEMENT the YouTube title (which appears below), not repeat it - Examples: "$10K EN 30 DÍAS", "ESTO FUNCIONA", "NO LO SABÍAS", "GRATIS 🔥" - Use ALL CAPS for maximum impact, split into 2-3 lines {extra_block} DESIGN REQUIREMENTS: - The text MUST be large, bold, and high-contrast (readable at small sizes) - Use vibrant, eye-catching colors that match the video's mood - Professional YouTube thumbnail aesthetic - Clean composition — text and face/subject as clear focal points - NO clutter, NO small text, NO watermarks""" if face_image_path and os.path.exists(face_image_path): text_prompt += "\n- Include the provided face/person prominently with an exaggerated expression (surprise, excitement, shock)" if bg_image_path and os.path.exists(bg_image_path): text_prompt += "\n- Use the provided background image as the base/backdrop" prompt_parts.append(text_prompt) thumbnails = [] last_error = None for i in range(count): print(f"🎨 [Thumbnail] Generating thumbnail {i + 1}/{count}...") try: response = client.models.generate_content( model="gemini-3.1-flash-image-preview", contents=prompt_parts, config=types.GenerateContentConfig( response_modalities=["TEXT", "IMAGE"], image_config=types.ImageConfig( aspect_ratio="16:9", image_size="2K" ) ) ) for part in response.parts: if part.text is not None: print(f"📝 [Thumbnail] Gemini text: {part.text}") elif image := part.as_image(): filename = f"thumb_{i + 1}.jpg" filepath = os.path.join(output_dir, filename) image.save(filepath) thumbnails.append(f"/thumbnails/{session_id}/{filename}") print(f"✅ [Thumbnail] Saved: {filepath}") break except Exception as e: last_error = str(e) print(f"❌ [Thumbnail] Generation {i + 1} failed: {e}") if not thumbnails and last_error: raise RuntimeError(f"All thumbnail generations failed. Last error: {last_error}") return thumbnails def generate_youtube_description(api_key, title, transcript_segments, language, video_duration): """ Uses Gemini to generate a YouTube description with chapter markers from transcript segments. Returns: { "description": "full description text with chapters" } """ client = genai.Client(api_key=api_key) # Format segments for the prompt formatted_segments = [] for seg in transcript_segments: start = seg.get("start", 0) mins = int(start // 60) secs = int(start % 60) timestamp = f"{mins}:{secs:02d}" formatted_segments.append(f"[{timestamp}] {seg.get('text', '').strip()}") segments_text = "\n".join(formatted_segments) # Format total duration dur_mins = int(video_duration // 60) dur_secs = int(video_duration % 60) duration_str = f"{dur_mins}:{dur_secs:02d}" prompt = f"""You are a YouTube SEO expert. Generate a complete YouTube video description for the following video. VIDEO TITLE: "{title}" VIDEO LANGUAGE: {language} VIDEO DURATION: {duration_str} TRANSCRIPT WITH TIMESTAMPS: {segments_text} REQUIREMENTS: 1. Write the description in the SAME LANGUAGE as the video ({language}) 2. Start with a compelling 2-3 sentence summary/hook 3. Add relevant CTAs (subscribe, like, comment) 4. Generate YouTube CHAPTERS based on the transcript timestamps: - First chapter MUST start at 0:00 - Minimum 3 chapters, each at least 10 seconds apart - Chapter titles should be concise and descriptive - Format: 0:00 Chapter Title - Place chapters in their own section with a blank line before and after 5. Add 5-10 relevant hashtags at the end 6. Keep the total description under 5000 characters OUTPUT: Return ONLY the description text (no JSON wrapper, no markdown code blocks). The description should be ready to paste directly into YouTube.""" print("🤖 [Thumbnail] Generating YouTube description with chapters...") response = client.models.generate_content( model="gemini-2.5-flash", contents=[prompt], ) description = response.text.strip() # Clean up any accidental markdown wrappers if description.startswith("```"): lines = description.split("\n") description = "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:]) return {"description": description}