amagastya commited on
Commit
8a656d1
·
verified ·
1 Parent(s): b6582c4

Add rest of files

Browse files
cache_utils.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from typing import Dict, Optional, Tuple
3
+
4
+
5
+ def get_cached_content(app_name: str) -> Optional[Tuple[str, Dict]]:
6
+ """
7
+ Get cached video path and script for predefined apps
8
+ """
9
+ # Use relative paths for outputs
10
+ video_path = os.path.join('outputs', 'sizzle_reel', 'final')
11
+ script_path = os.path.join('outputs', 'sizzle_reel', 'scripts')
12
+
13
+ # Create directories if they don't exist
14
+ os.makedirs(video_path, exist_ok=True)
15
+ os.makedirs(script_path, exist_ok=True)
16
+
17
+ cached_videos = {
18
+ "BloomBuddy": "BloomBuddy_sizzle_reel_HQ.mp4",
19
+ "FitFlow AI": "FitFlowAI_sizzle_reel_HQ.mp4",
20
+ "LingoPal": "LingoPal_sizzle_reel_HQ.mp4",
21
+ "MindMate": "MindMate_sizzle_reel_HQ.mp4",
22
+ "EcoCart": "EcoCart_sizzle_reel_HQ.mp4",
23
+ "ChefSync": "ChefSync_sizzle_reel_HQ.mp4",
24
+ "WanderWise": "WanderWise_sizzle_reel_HQ.mp4",
25
+ "SkillShare+": "SkillShare__sizzle_reel_HQ.mp4"
26
+ }
27
+
28
+ if app_name not in cached_videos:
29
+ return None
30
+
31
+ # Remove spaces from app name for file paths
32
+ sanitized_name = app_name.strip().replace(" ", "")
33
+ video_file = os.path.join(video_path, cached_videos[app_name.strip()])
34
+ script_file = os.path.join(script_path, f"{sanitized_name.lower()}.json")
35
+
36
+ # Add debug print to check paths
37
+ print(f"Looking for video at: {video_file}")
38
+ print(f"Looking for script at: {script_file}")
39
+
40
+ if not os.path.exists(video_file) or not os.path.exists(script_file):
41
+ print("Script or video not found... Try again.")
42
+ return None
43
+
44
+ with open(script_file, 'r') as f:
45
+ script = f.read()
46
+
47
+ return video_file, script
config/agents.yaml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ hero_research_specialist:
2
+ role: "Hero Research Specialist"
3
+ goal: "Identify the hero user, use case, and journey"
4
+ backstory: >
5
+ Context: You are part of a generative crew of assistants integrated into a 'Sizzle Reel Script Generation System'
6
+ The objective of this crew is to generate an engaging script for sizzle reel generation.
7
+
8
+ Your persona: An experienced user experience researcher specializing in understanding user personas and their journeys.
9
+
10
+ content_strategist:
11
+ role: "Content Strategy Finalizer"
12
+ goal: "Refine and finalize the content strategy for the sizzle reel"
13
+ backstory: >
14
+ An expert in aligning content with brand messaging and audience expectations
15
+
16
+ narrative_scriptwriter:
17
+ role: "Narrative Scriptwriter"
18
+ goal: "Generate the content plan and script"
19
+ backstory: >
20
+ Context: You are part of a generative crew of assistants integrated into a 'Sizzle Reel Script Generation System'.
21
+ The objective of this crew is to generate an engaging script for sizzle reel generation.
22
+
23
+ Your persona: A creative scriptwriter skilled in crafting engaging narratives for multimedia presentations.
config/tasks.yaml ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ hero_research_task:
2
+ description: >
3
+ Identify the hero user and their primary use case. Understand why they would use the app and what problem it solves for them.
4
+ Identify the hero-user, hero-usecase, and hero user journey from the app name '{app_name}',
5
+ customer idea '{customer_idea}'. Constraint - Maximum 5-7 steps in the user journey
6
+ expected_output: "Outline the steps the hero user takes in their journey within the app, from start to finish."
7
+ agent: hero_research_specialist
8
+
9
+ content_plan_task:
10
+ description: >
11
+ Generate a detailed content plan for the sizzle reel for App '{app_name}', idea: '{app_idea}'.
12
+ expected_output: "A detailed content plan for the sizzle reel."
13
+ agent: content_strategist
14
+
15
+ script_generation_task:
16
+ description: >
17
+ Develop the final script for the sizzle reel for app: '{app_name}', including engaging narrations and screen actions for each step in the hero's journey.
18
+ expected_output: "A detailed script with narrations and screen actions."
19
+ agent: narrative_scriptwriter
crew.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ from crewai import LLM, Agent, Crew, Process, Task
4
+ from crewai.project import CrewBase, agent, crew, task
5
+ from dotenv import load_dotenv
6
+ from models import *
7
+
8
+ load_dotenv()
9
+
10
+ @CrewBase
11
+ class SizzleReelCrew:
12
+ """Sizzle Reel Script Generation Crew"""
13
+
14
+ def __init__(self, inputs, *args, **kwargs):
15
+ super().__init__(*args, **kwargs)
16
+ # Get OpenAI API key from environment
17
+ openai_api_key = os.getenv('OPENAI_API_KEY')
18
+ if not openai_api_key:
19
+ raise ValueError("OpenAI API Key is not set. Please set the OPENAI_API_KEY environment variable.")
20
+
21
+ # Configure LLM with explicit provider
22
+ self.llm = LLM(
23
+ model="openai/gpt-4o-mini",
24
+ temperature=0.7,
25
+ api_key=openai_api_key
26
+ )
27
+ self.inputs = inputs
28
+
29
+ # self.llm = LLM(
30
+ # model="gemini/gemini-2.0-flash-lite",
31
+ # temperature=0.7,
32
+ # api_key=os.getenv('GEMINI_API_KEY')
33
+ # )
34
+
35
+ @agent
36
+ def hero_research_specialist(self) -> Agent:
37
+ return Agent(
38
+ config=self.agents_config['hero_research_specialist'],
39
+ verbose=True,
40
+ llm=self.llm # Use the configured LLM
41
+ )
42
+
43
+ @agent
44
+ def content_strategist(self) -> Agent:
45
+ return Agent(
46
+ config=self.agents_config['content_strategist'],
47
+ verbose=True,
48
+ llm=self.llm # Use the configured LLM
49
+ )
50
+
51
+ @agent
52
+ def narrative_scriptwriter(self) -> Agent:
53
+ return Agent(
54
+ config=self.agents_config['narrative_scriptwriter'],
55
+ verbose=True,
56
+ llm=self.llm # Use the configured LLM
57
+ )
58
+
59
+ @task
60
+ def hero_research_task(self) -> Task:
61
+ return Task(
62
+ description=f"""Identify the hero user and their primary use case. Understand why they would use the app and what problem it solves for them.
63
+ Identify the hero-user, hero-usecase, and hero user journey from app name {self.inputs['app_name']}.
64
+ App Description: {self.inputs['customer_idea']}""",
65
+ expected_output="""Outline the steps the hero user takes in their journey within the app, from start to finish.""",
66
+ agent=self.hero_research_specialist(),
67
+ output_pydantic=HeroResearchOutput
68
+ )
69
+
70
+ @task
71
+ def content_plan_task(self) -> Task:
72
+ return Task(
73
+ description=f"""Generate a detailed content plan for the sizzle reel for App {self.inputs['app_name']}""",
74
+ expected_output="""A detailed content plan for the sizzle reel""",
75
+ agent=self.content_strategist(),
76
+ context=[self.hero_research_task()]
77
+ )
78
+
79
+ @task
80
+ def script_generation_task(self) -> Task:
81
+ return Task(
82
+ description=f"""Develop the final script for the sizzle reel for app: '{self.inputs['app_name']}', including engaging narrations and screen actions for each step in the hero's journey.""",
83
+ expected_output="""A detailed script with narrations and screen actions.""",
84
+ agent=self.narrative_scriptwriter(),
85
+ output_pydantic=ScriptOutput,
86
+ context=[self.hero_research_task(), self.content_plan_task()]
87
+ )
88
+
89
+ @crew
90
+ def crew(self) -> Crew:
91
+ return Crew(
92
+ agents=self.agents,
93
+ tasks=self.tasks,
94
+ process=Process.sequential,
95
+ verbose=True,
96
+ llm=self.llm # Use the configured LLM for the entire crew
97
+ )
pyproject.toml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "script_gen"
3
+ version = "0.1.0"
4
+ description = "script-gen using crewAI"
5
+ authors = [
6
+ { name = "Your Name", email = "you@example.com" },
7
+ ]
8
+ requires-python = ">=3.10,<3.13"
9
+ dependencies = [
10
+ "crewai[tools]>=0.98.0,<1.0.0",
11
+ "deepgram-sdk>=3.9.0",
12
+ "moviepy>=2.1.2",
13
+ ]
14
+
15
+ [project.scripts]
16
+ script_gen = "script_gen.main:run"
17
+ run_crew = "script_gen.main:run"
18
+ train = "script_gen.main:train"
19
+ replay = "script_gen.main:replay"
20
+ test = "script_gen.main:test"
21
+
22
+ [build-system]
23
+ requires = [
24
+ "hatchling",
25
+ ]
26
+ build-backend = "hatchling.build"
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ crewai
2
+ deepgram-sdk
3
+ crewai_tools
4
+ crewai['tools']
5
+ moviepy
6
+ python-dotenv
7
+ pydantic
tools/__init__.py ADDED
File without changes
tools/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (182 Bytes). View file
 
tools/__pycache__/custom_tool.cpython-310.pyc ADDED
Binary file (1.23 kB). View file
 
tools/__pycache__/video_generation_tool.cpython-310.pyc ADDED
Binary file (2.11 kB). View file
 
tools/__pycache__/video_tool.cpython-310.pyc ADDED
Binary file (1.39 kB). View file
 
tools/custom_tool.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from crewai.tools import BaseTool
2
+ from typing import Type
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class MyCustomToolInput(BaseModel):
7
+ """Input schema for MyCustomTool."""
8
+ argument: str = Field(..., description="Description of the argument.")
9
+
10
+ class MyCustomTool(BaseTool):
11
+ name: str = "Name of my tool"
12
+ description: str = (
13
+ "Clear description for what this tool is useful for, your agent will need this information to use it."
14
+ )
15
+ args_schema: Type[BaseModel] = MyCustomToolInput
16
+
17
+ def _run(self, argument: str) -> str:
18
+ # Implementation goes here
19
+ return "this is an example of a tool output, ignore it and move along."
tools/video_generation_tool.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /Users/amogh/Documents/amogh/projects/crew-ai/script_gen/src/script_gen/tools/video_tool.py
2
+ from crewai.tools import BaseTool
3
+ from typing import Dict, Any
4
+ import logging
5
+
6
+ # Import the video generator
7
+ from script_gen.video_gen_new import SizzleReelVideoGenerator
8
+
9
+ class VideoGenerationTool(BaseTool):
10
+ """
11
+ Custom tool for generating sizzle reel videos
12
+ """
13
+ name: str = "Video Generation Tool"
14
+ description: str = (
15
+ "Generates a sizzle reel video from a prepared script. "
16
+ "Takes a JSON script and an app name, and produces a video path."
17
+ )
18
+
19
+ def __init__(self):
20
+ super().__init__()
21
+
22
+
23
+ def _run(
24
+ self,
25
+ script_json: Dict[str, Any],
26
+ app_name: str = "CleverApp"
27
+ ) -> str:
28
+ """
29
+ Generate a sizzle reel video from the provided script
30
+
31
+ Args:
32
+ script_json (Dict): The script for the sizzle reel
33
+ app_name (str, optional): Name of the app. Defaults to "CleverApp".
34
+
35
+ Returns:
36
+ str: Path to the generated video
37
+
38
+ Raises:
39
+ ValueError: If video generation fails
40
+ """
41
+ try:
42
+ # Log the start of video generation
43
+ logging.info(f"Starting video generation for {app_name}")
44
+
45
+ self.video_generator = SizzleReelVideoGenerator()
46
+
47
+ # Generate the video
48
+ video_path = self.video_generator.generate_sizzle_reel(
49
+ script_json=script_json,
50
+ app_name=app_name
51
+ )
52
+
53
+ # Validate video generation
54
+ if not video_path:
55
+ raise ValueError("Video generation failed: No video path returned")
56
+
57
+ # Log successful generation
58
+ logging.info(f"Video generated successfully: {video_path}")
59
+
60
+ return video_path
61
+
62
+ except Exception as e:
63
+ # Log any errors during video generation
64
+ logging.error(f"Error in video generation: {e}")
65
+ raise ValueError(f"Video generation failed: {str(e)}")
66
+
67
+ # Create a single instance of the tool
68
+ video_tool = VideoGenerationTool()
video_gen_new.py ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import os
4
+ import re
5
+
6
+ import numpy as np
7
+ import requests
8
+ from deepgram import DeepgramClient, SpeakOptions
9
+ from dotenv import load_dotenv
10
+ from moviepy import (AudioFileClip, CompositeVideoClip, TextClip,
11
+ VideoFileClip, concatenate_videoclips)
12
+
13
+ # Create logs directory if it doesn't exist
14
+ os.makedirs('logs', exist_ok=True)
15
+
16
+ # Configure logging
17
+ logging.basicConfig(level=logging.INFO,
18
+ format='%(asctime)s - %(levelname)s - %(message)s',
19
+ handlers=[
20
+ logging.FileHandler('logs/video_generation.log'),
21
+ logging.StreamHandler()
22
+ ])
23
+
24
+ class SizzleReelVideoGenerator:
25
+ def __init__(self, min_clip_duration=3, max_clip_duration=10):
26
+ load_dotenv()
27
+
28
+ # Collect Pexels API keys
29
+ self.pexels_api_keys = [
30
+ os.getenv('PEXELS_API_KEY'),
31
+ os.getenv('PEXELS_API_KEY_2'),
32
+ os.getenv('PEXELS_API_KEY_3')
33
+ ]
34
+ self.pexels_api_keys = [key for key in self.pexels_api_keys if key]
35
+
36
+ # Initialize ElevenLabs client with the single key
37
+ self.deepgram_client = DeepgramClient(os.getenv('DEEPGRAM_API_KEY'))
38
+
39
+ # Standard video settings
40
+ self.target_width = 800
41
+ self.target_height = 600
42
+ self.target_aspect_ratio = 16/9
43
+
44
+ # Clip duration constraints
45
+ self.min_clip_duration = min_clip_duration
46
+ self.max_clip_duration = max_clip_duration
47
+
48
+ # Use relative paths for outputs
49
+ self.base_output_dir = os.path.join('outputs', 'sizzle_reel')
50
+ self.audio_dir = os.path.join(self.base_output_dir, 'audio')
51
+ self.video_dir = os.path.join(self.base_output_dir, 'videos')
52
+ self.final_dir = os.path.join(self.base_output_dir, 'final')
53
+
54
+ # Create all necessary directories
55
+ os.makedirs('logs', exist_ok=True)
56
+ for dir_path in [self.audio_dir, self.video_dir, self.final_dir]:
57
+ os.makedirs(dir_path, exist_ok=True)
58
+
59
+ def _rotate_pexels_key(self):
60
+ """Rotate Pexels API keys if one fails"""
61
+ if len(self.pexels_api_keys) > 1:
62
+ # Move the first key to the end
63
+ self.pexels_api_keys.append(self.pexels_api_keys.pop(0))
64
+ return self.pexels_api_keys[0]
65
+
66
+ def fetch_pexels_video(self, query, max_retries=3):
67
+ # Create a copy of the API keys to avoid modifying the original list
68
+ current_keys = self.pexels_api_keys.copy()
69
+
70
+ for attempt in range(max_retries):
71
+ # If no keys left, break the loop
72
+ if not current_keys:
73
+ logging.error(f"No more Pexels API keys available for query: {query}")
74
+ return None
75
+
76
+ try:
77
+ # Use the first available key
78
+ current_key = current_keys[0]
79
+
80
+ url = "https://api.pexels.com/videos/search"
81
+ headers = {"Authorization": current_key}
82
+ params = {
83
+ "query": query,
84
+ "per_page": 5, # Increase to have more fallback options
85
+ "page": 1
86
+ }
87
+
88
+ response = requests.get(url, headers=headers, params=params)
89
+
90
+ # Check for rate limit or authentication error
91
+ if response.status_code in [401, 403]:
92
+ # Remove the current key and continue with the next
93
+ current_keys.pop(0)
94
+ logging.warning(f"Pexels API key failed. Trying next key. Remaining keys: {len(current_keys)}")
95
+ continue
96
+
97
+ # Raise an exception for other HTTP errors
98
+ response.raise_for_status()
99
+
100
+ data = response.json()
101
+
102
+ if not data.get('videos'):
103
+ logging.warning(f"No videos found for query: {query}")
104
+ return None
105
+
106
+ # Try to find an SD video from the list of results
107
+ for video in data['videos']:
108
+ video_files = video['video_files']
109
+ sd_videos = [v for v in video_files if v['quality'] == 'sd']
110
+
111
+ if sd_videos:
112
+ sd_videos.sort(key=lambda x: x['size'])
113
+ return sd_videos[0]['link']
114
+
115
+ # If no SD videos found
116
+ logging.warning(f"No SD quality videos found for query: {query}")
117
+ return None
118
+
119
+ except Exception as e:
120
+ logging.error(f"Error fetching Pexels video (Attempt {attempt + 1}): {e}")
121
+
122
+ # Remove the current key and continue with the next
123
+ current_keys.pop(0)
124
+ logging.warning(f"Pexels API key failed. Trying next key. Remaining keys: {len(current_keys)}")
125
+
126
+ # If all attempts fail
127
+ logging.error(f"Failed to fetch video for query: {query} after multiple attempts")
128
+ return None
129
+
130
+ def _sanitize_filename(self, filename):
131
+ sanitized = re.sub(r'[^\w\-_\. ]', '_', filename)
132
+ return sanitized[:50]
133
+
134
+ # def generate_elevenlabs_voiceover(self, text, step_description, voice_id="JBFqnCBsd6RMkjVDRZzb"):
135
+ # audio_stream = self.elevenlabs_client.text_to_speech.convert(
136
+ # text=text,
137
+ # voice_id=voice_id,
138
+ # model_id="eleven_flash_v2",
139
+ # output_format="mp3_44100_128",
140
+ # voice_settings={
141
+ # "stability": 0.0,
142
+ # "similarity_boost": 1.0,
143
+ # "style": 0.0,
144
+ # "use_speaker_boost": True
145
+ # }
146
+ # )
147
+
148
+ # sanitized_step = self._sanitize_filename(step_description)
149
+ # audio_filename = f"{sanitized_step}_voiceover.mp3"
150
+ # audio_path = os.path.join(self.audio_dir, audio_filename)
151
+
152
+ # with open(audio_path, 'wb') as f:
153
+ # for chunk in audio_stream:
154
+ # if chunk:
155
+ # f.write(chunk)
156
+
157
+ # return audio_path
158
+
159
+
160
+ def generate_voiceover(self, text, step_description, voice_id="aura-athena-en"):
161
+ try:
162
+ # Prepare text and filename
163
+ text_payload = {"text": text}
164
+ sanitized_step = self._sanitize_filename(step_description)
165
+ audio_filename = f"{sanitized_step}_voiceover.mp3"
166
+ audio_path = os.path.join(self.audio_dir, audio_filename)
167
+
168
+ # Configure speak options
169
+ options = SpeakOptions(
170
+ model=voice_id, # Use the specified voice, with a default
171
+ )
172
+
173
+ # Generate and save the audio
174
+ response = self.deepgram_client.speak.v("1").save(audio_path, text_payload, options)
175
+
176
+ logging.info(f"Generated voiceover for step: {step_description}")
177
+ return audio_path
178
+
179
+ except Exception as e:
180
+ logging.error(f"Error generating voiceover for step: {step_description}")
181
+ logging.error(f"Error details: {e}")
182
+ return None
183
+
184
+
185
+ def download_video(self, video_url, step_description):
186
+ response = requests.get(video_url, stream=True)
187
+ sanitized_step = self._sanitize_filename(step_description)
188
+ video_filename = f"{sanitized_step}_video.mp4"
189
+ video_path = os.path.join(self.video_dir, video_filename)
190
+
191
+ with open(video_path, 'wb') as f:
192
+ for chunk in response.iter_content(chunk_size=8192):
193
+ f.write(chunk)
194
+
195
+ return video_path
196
+
197
+ def generate_step_video(self, step, query_type='narrator'):
198
+ # Alternate between action and narrator for video query
199
+ video_query = step['action'] if query_type == 'action' else step['narrator']
200
+
201
+ try:
202
+ # Generate voiceover
203
+ audio_path = self.generate_voiceover(
204
+ step['narrator'],
205
+ step['step_description']
206
+ )
207
+ logging.info(f"Generated voiceover for step: {step['step_description']}")
208
+
209
+ # Fetch and download video
210
+ video_url = self.fetch_pexels_video(video_query)
211
+ video_path = self.download_video(
212
+ video_url,
213
+ step['step_description']
214
+ )
215
+ logging.info(f"Downloaded video for query: {video_query}")
216
+
217
+ # Get audio duration
218
+ with AudioFileClip(audio_path) as audio_clip:
219
+ audio_duration = audio_clip.duration
220
+
221
+ return {
222
+ 'audio_path': audio_path,
223
+ 'video_path': video_path,
224
+ 'step_description': step['step_description'],
225
+ 'narrator': step['narrator'],
226
+ 'duration': audio_duration
227
+ }
228
+ except Exception as e:
229
+ logging.error(f"Error generating video for step: {step['step_description']}")
230
+ logging.error(f"Error details: {e}")
231
+ return None
232
+
233
+ def add_captions_to_video(self, video_clip, narrator_text, audio_duration):
234
+ # Create a TextClip for the captions
235
+ caption = TextClip(
236
+ text=narrator_text,
237
+ font='Arial',
238
+ color='white',
239
+ stroke_color='black',
240
+ stroke_width=1,
241
+ method='caption',
242
+ size=(video_clip.w * 0.8, 50), # Narrower width, shorter height
243
+ bg_color='rgba(0,0,0,0.5)', # Semi-transparent background
244
+ text_align='center',
245
+ horizontal_align='center',
246
+ vertical_align='bottom'
247
+ )
248
+
249
+ # Position the caption at the bottom of the video
250
+ caption = caption.with_position(('center', 'bottom'))
251
+
252
+ # Set the duration to match audio
253
+ caption = caption.with_duration(audio_duration)
254
+
255
+ # Composite the video with the caption
256
+ return CompositeVideoClip([video_clip, caption])
257
+
258
+ def create_smooth_transition(self, clip1, clip2, transition_duration=1):
259
+ """
260
+ Create a smooth crossfade transition between two video clips
261
+ """
262
+ # Extract the underlying video clip if it's a CompositeVideoClip
263
+ if isinstance(clip1, CompositeVideoClip):
264
+ clip1 = clip1.clips[0] # Assume the first clip is the base video
265
+ if isinstance(clip2, CompositeVideoClip):
266
+ clip2 = clip2.clips[0] # Assume the first clip is the base video
267
+
268
+ # Both clips should already be standardized, but verify sizes
269
+ assert clip1.size == (self.target_width, self.target_height), "Clip1 size mismatch"
270
+ assert clip2.size == (self.target_width, self.target_height), "Clip2 size mismatch"
271
+
272
+ # Ensure both clips have the same size and fps
273
+ if clip1.size != clip2.size:
274
+ clip2 = clip2.resized(clip1.size)
275
+
276
+ # Create a transition clip
277
+ def transition_func(t):
278
+ if t < transition_duration:
279
+ # Linear crossfade
280
+ alpha1 = 1 - (t / transition_duration)
281
+ alpha2 = t / transition_duration
282
+
283
+ # Get frames from both clips
284
+ frame1 = clip1.get_frame(clip1.duration - transition_duration + t)
285
+ frame2 = clip2.get_frame(t)
286
+
287
+ # Blend frames
288
+ blended_frame = (alpha1 * frame1 + alpha2 * frame2).astype(np.uint8)
289
+ return blended_frame
290
+ else:
291
+ # After transition, return the second clip's frame
292
+ return clip2.get_frame(t - transition_duration)
293
+
294
+ # Create a transition clip
295
+ transition_clip = VideoFileClip(
296
+ filename,
297
+ audio=False
298
+ ).with_duration(transition_duration)
299
+ transition_clip.get_frame = transition_func
300
+
301
+ return transition_clip
302
+
303
+
304
+ def process_video_clip(self, video_path, audio_path):
305
+ try:
306
+ # Load video and audio
307
+ video_clip = VideoFileClip(video_path)
308
+ audio_clip = AudioFileClip(audio_path)
309
+
310
+ # Standardize video size first
311
+ video_clip = self._standardize_video_size(video_clip)
312
+
313
+ # Synchronize duration with constraints
314
+ audio_duration = audio_clip.duration
315
+ video_duration = video_clip.duration
316
+
317
+ # Adjust clip duration to be within min and max constraints
318
+ clip_duration = max(
319
+ self.min_clip_duration,
320
+ min(audio_duration, self.max_clip_duration, video_duration)
321
+ )
322
+
323
+ # Safely trim video and audio to synchronized duration
324
+ try:
325
+ video_clip = video_clip.subclipped(0, min(clip_duration, video_duration))
326
+ except Exception as ve:
327
+ logging.warning(f"Error trimming video clip: {ve}. Using full video duration.")
328
+ video_clip = video_clip.subclipped(0, video_duration)
329
+
330
+ try:
331
+ audio_clip = audio_clip.subclipped(0, min(clip_duration, audio_duration))
332
+ except Exception as ae:
333
+ logging.warning(f"Error trimming audio clip: {ae}. Using full audio duration.")
334
+ audio_clip = audio_clip.subclipped(0, audio_duration)
335
+
336
+ # Ensure audio and video have the same duration
337
+ min_duration = min(video_clip.duration, audio_clip.duration)
338
+ video_clip = video_clip.subclipped(0, min_duration)
339
+ audio_clip = audio_clip.subclipped(0, min_duration)
340
+
341
+ # Attach audio to video
342
+ video_clip = video_clip.with_audio(audio_clip)
343
+
344
+ return video_clip
345
+ except Exception as e:
346
+ logging.error(f"Comprehensive error processing video clip: {e}")
347
+
348
+ # Additional diagnostic logging
349
+ try:
350
+ logging.error(f"Video path: {video_path}")
351
+ logging.error(f"Audio path: {audio_path}")
352
+
353
+ # Log file details if possible
354
+ import os
355
+ video_exists = os.path.exists(video_path)
356
+ audio_exists = os.path.exists(audio_path)
357
+
358
+ logging.error(f"Video file exists: {video_exists}")
359
+ logging.error(f"Audio file exists: {audio_exists}")
360
+
361
+ if video_exists:
362
+ video_clip = VideoFileClip(video_path)
363
+ logging.error(f"Video duration: {video_clip.duration}")
364
+
365
+ if audio_exists:
366
+ audio_clip = AudioFileClip(audio_path)
367
+ logging.error(f"Audio duration: {audio_clip.duration}")
368
+ except Exception as diag_error:
369
+ logging.error(f"Additional diagnostic error: {diag_error}")
370
+
371
+ return None
372
+
373
+ def add_captions_to_video(self, video_clip, narrator_text, audio_duration):
374
+ # Create a TextClip for the captions
375
+ caption = TextClip(
376
+ text=narrator_text,
377
+ font='Arial',
378
+ color='white',
379
+ stroke_color='black',
380
+ stroke_width=2,
381
+ method='caption',
382
+ size=(video_clip.w, 100),
383
+ bg_color=None,
384
+ text_align='center',
385
+ horizontal_align='center',
386
+ vertical_align='center'
387
+ )
388
+
389
+ # Position the caption at the bottom of the video
390
+ caption = caption.with_position(('center', 'bottom'))
391
+
392
+ # Set the duration to match audio
393
+ caption = caption.with_duration(audio_duration)
394
+
395
+ # Composite the video with the caption
396
+ return CompositeVideoClip([video_clip, caption])
397
+
398
+ def create_smooth_transition(self, clip1, clip2, transition_duration=1):
399
+ """
400
+ Create a smooth crossfade transition between two video clips
401
+ """
402
+ # Extract the underlying video clip if it's a CompositeVideoClip
403
+ if isinstance(clip1, CompositeVideoClip):
404
+ clip1 = clip1.clips[0] # Assume the first clip is the base video
405
+ if isinstance(clip2, CompositeVideoClip):
406
+ clip2 = clip2.clips[0] # Assume the first clip is the base video
407
+
408
+ # Ensure both clips have the same size and fps
409
+ if clip1.size != clip2.size:
410
+ clip2 = clip2.resized(clip1.size)
411
+
412
+ # Create a transition clip
413
+ def transition_func(t):
414
+ if t < transition_duration:
415
+ # Linear crossfade
416
+ alpha1 = 1 - (t / transition_duration)
417
+ alpha2 = t / transition_duration
418
+
419
+ # Get frames from both clips
420
+ frame1 = clip1.get_frame(clip1.duration - transition_duration + t)
421
+ frame2 = clip2.get_frame(t)
422
+
423
+ # Blend frames
424
+ blended_frame = (alpha1 * frame1 + alpha2 * frame2).astype(np.uint8)
425
+ return blended_frame
426
+ else:
427
+ # After transition, return the second clip's frame
428
+ return clip2.get_frame(t - transition_duration)
429
+
430
+ # Create a transition clip
431
+ transition_clip = VideoFileClip(
432
+ clip1.filename if hasattr(clip1, 'filename') else '/tmp/transition.mp4',
433
+ audio=False
434
+ ).with_duration(transition_duration)
435
+ transition_clip.get_frame = transition_func
436
+
437
+ return transition_clip
438
+
439
+ def _standardize_video_size(self, clip):
440
+ """
441
+ Standardize video size to target resolution while maintaining aspect ratio
442
+ """
443
+ # Get current clip size
444
+ w, h = clip.size
445
+ current_aspect_ratio = w/h
446
+
447
+ if current_aspect_ratio > self.target_aspect_ratio:
448
+ # Video is wider than target ratio
449
+ new_width = self.target_width
450
+ new_height = int(new_width / current_aspect_ratio)
451
+ else:
452
+ # Video is taller than target ratio
453
+ new_height = self.target_height
454
+ new_width = int(new_height * current_aspect_ratio)
455
+
456
+ # Resize video
457
+ resized_clip = clip.resized(width=new_width, height=new_height)
458
+
459
+ # Create black background of target size
460
+ from moviepy.video.VideoClip import ColorClip
461
+ bg = ColorClip(size=(self.target_width, self.target_height),
462
+ color=(0,0,0))
463
+ bg = bg.with_duration(clip.duration)
464
+
465
+ # Center the video on the background
466
+ x_offset = (self.target_width - new_width) // 2
467
+ y_offset = (self.target_height - new_height) // 2
468
+
469
+ final_clip = CompositeVideoClip([
470
+ bg,
471
+ resized_clip.with_position((x_offset, y_offset))
472
+ ])
473
+
474
+ return final_clip
475
+
476
+ def generate_sizzle_reel(self, script_json, app_name="CleverApp"):
477
+ # Parse script based on input type
478
+ print("Debug", script_json)
479
+
480
+ if isinstance(script_json, str):
481
+ script = json.loads(script_json)
482
+ script_steps = script.get('sizzle_reel_script', [])
483
+ elif isinstance(script_json, dict) and 'sizzle_reel_script' in script_json:
484
+ # Handle the case where sizzle_reel_script is a CrewOutput object
485
+ crew_output = script_json['sizzle_reel_script']
486
+ if hasattr(crew_output, 'raw'):
487
+ # Parse the raw JSON string from CrewOutput
488
+ try:
489
+ parsed_data = json.loads(crew_output.raw)
490
+ script_steps = parsed_data.get('sizzle_reel_script', [])
491
+ except json.JSONDecodeError:
492
+ logging.error("Failed to parse raw CrewOutput JSON")
493
+ script_steps = []
494
+ else:
495
+ script_steps = crew_output
496
+ else:
497
+ script_steps = []
498
+
499
+ # Process video steps
500
+ processed_clips = []
501
+
502
+ # Convert to list if it's not already
503
+ if not isinstance(script_steps, list):
504
+ script_steps = [script_steps]
505
+
506
+ for i, step in enumerate(script_steps):
507
+ # Convert Pydantic model to dict if necessary
508
+ if hasattr(step, 'dict'):
509
+ step = step.dict()
510
+ # Alternate query type
511
+ query_type = 'narrator' if i % 2 == 0 else 'action'
512
+
513
+ # Generate step video
514
+ step_video = self.generate_step_video(step, query_type)
515
+ if not step_video:
516
+ logging.warning(f"Skipping step {i} due to video generation failure")
517
+ continue
518
+
519
+ # Process video clip
520
+ processed_clip = self.process_video_clip(
521
+ step_video['video_path'],
522
+ step_video['audio_path']
523
+ )
524
+
525
+ if processed_clip:
526
+ # Add captions
527
+ try:
528
+ captioned_clip = self.add_captions_to_video(
529
+ processed_clip,
530
+ step_video['narrator'],
531
+ processed_clip.duration
532
+ )
533
+ processed_clips.append(captioned_clip)
534
+ except Exception as caption_error:
535
+ logging.error(f"Error adding captions to clip {i}: {caption_error}")
536
+ # Fallback: use processed clip without captions
537
+ processed_clips.append(processed_clip)
538
+
539
+ # Check if we have any processed clips
540
+ if not processed_clips:
541
+ logging.error("No video clips could be generated")
542
+ return None
543
+
544
+ # Concatenate processed clips with smooth transitions
545
+ final_clips = []
546
+ for i in range(len(processed_clips) - 1):
547
+ final_clips.append(processed_clips[i])
548
+ # Add transition between clips
549
+ try:
550
+ transition = self.create_smooth_transition(
551
+ processed_clips[i],
552
+ processed_clips[i+1],
553
+ transition_duration=1
554
+ )
555
+ final_clips.append(transition)
556
+ except Exception as transition_error:
557
+ logging.warning(f"Could not create transition between clips {i} and {i+1}: {transition_error}")
558
+
559
+ # Add the last clip
560
+ final_clips.append(processed_clips[-1])
561
+
562
+ # Concatenate video clips
563
+ try:
564
+ final_video = concatenate_videoclips(final_clips, method="compose")
565
+ except Exception as e:
566
+ logging.error(f"Error concatenating video clips: {e}")
567
+ return None
568
+
569
+ # Sanitize app name for filename
570
+ sanitized_app_name = re.sub(r'[^\w\-_\. ]', '_', app_name)
571
+
572
+ # Output final video with app name
573
+ output_filename = f"{sanitized_app_name}_sizzle_reel.mp4"
574
+ output_path = os.path.join(self.final_dir, output_filename)
575
+
576
+ try:
577
+ final_video.write_videofile(
578
+ output_path,
579
+ codec='libx264',
580
+ audio_codec='aac',
581
+ preset='ultrafast', # Changed from 'medium' to 'ultrafast'
582
+ threads=12,
583
+ # bitrate='600k',
584
+ fps=24,
585
+ )
586
+ logging.info(f"Successfully generated final video: {output_path}")
587
+ except Exception as e:
588
+ logging.error(f"Error writing final video: {e}")
589
+ return None
590
+
591
+ # Clean up resources
592
+ for clip in processed_clips + final_clips + [final_video]:
593
+ try:
594
+ clip.close()
595
+ except Exception as close_error:
596
+ logging.warning(f"Error closing clip: {close_error}")
597
+
598
+ return output_path
599
+
600
+
601
+ # Main execution
602
+ if __name__ == "__main__":
603
+ # Your sample script here
604
+ sample_script = {
605
+ "sizzle_reel_script": [
606
+ {
607
+ "step_description": "Establishing the Problem",
608
+ "narrator": "In a world filled with stress and uncertainty, finding peace can feel like an uphill battle.",
609
+ "action": "A split-screen shows a person overwhelmed with stress on the left and another individual feeling calm and focused on the right.",
610
+ "features": []
611
+ },
612
+ {
613
+ "step_description": "The Solution",
614
+ "narrator": "Meet MindMate, your personal companion for mental well-being. Track your moods, find balance, and embrace tranquility.",
615
+ "action": "Transition to the MindMate app interface on a smartphone, showcasing its sleek design.",
616
+ "features": []
617
+ },
618
+ {
619
+ "step_description": "The Onboarding Experience",
620
+ "narrator": "Start your journey by downloading the app and sharing your story through a simple assessment.",
621
+ "action": "Quick clips of a user downloading the app, creating an account, and completing the initial assessment.",
622
+ "features": []
623
+ },
624
+ {
625
+ "step_description": "Daily Engagement",
626
+ "narrator": "Log your emotions daily and receive gentle prompts to help you stay connected with your feelings.",
627
+ "action": "User logs their feelings in the mood tracker, with reminders popping up on their phone.",
628
+ "features": [
629
+ {"feature_name": "Mood Tracker", "description": "Log daily emotions."},
630
+ {"feature_name": "Reminders", "description": "Gentle prompts for mood check-ins."}
631
+ ]
632
+ },
633
+ {
634
+ "step_description": "Tailored Support",
635
+ "narrator": "Experience personalized meditation sessions designed just for you, based on your unique mood patterns.",
636
+ "action": "User engages in a serene meditation environment, surrounded by calming visuals and soundscapes.",
637
+ "features": [
638
+ {"feature_name": "Personalized Meditations", "description": "Meditations based on mood patterns."}
639
+ ]
640
+ },
641
+ {
642
+ "step_description": "Empowering Analytics",
643
+ "narrator": "Track your progress and discover how meditation can transform your mental health journey.",
644
+ "action": "User reviews analytics on their progress, smiling as they see improvements.",
645
+ "features": [
646
+ {"feature_name": "Progress Tracking", "description": "View mood trends and meditation effectiveness."}
647
+ ]
648
+ },
649
+ {
650
+ "step_description": "Connection",
651
+ "narrator": "Join a community of individuals just like you, sharing stories, and celebrating progress together.",
652
+ "action": "Users share experiences and feedback within the app community, fostering connection and support.",
653
+ "features": [
654
+ {"feature_name": "Community Support", "description": "Share experiences and feedback."}
655
+ ]
656
+ },
657
+ {
658
+ "step_description": "Download Now",
659
+ "narrator": "Ready to take the first step towards a healthier mind? Download MindMate today and start your journey to tranquility.",
660
+ "action": "The MindMate logo appears with app store icons for iOS and Android.",
661
+ "features": []
662
+ },
663
+ {
664
+ "step_description": "Inspirational Tone",
665
+ "narrator": "Your mental well-being matters. Let MindMate guide you.",
666
+ "action": "A serene landscape with the tagline overlaid: 'MindMate - Your Path to Peace.'",
667
+ "features": []
668
+ }
669
+ ]
670
+ }
671
+
672
+ generator = SizzleReelVideoGenerator()
673
+ final_video = generator.generate_sizzle_reel(sample_script, app_name="MindMate")
674
+ print(f"Sizzle reel generated: {final_video}")