Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -26,14 +26,14 @@ from typing import List, Optional, Literal, Dict, Any
|
|
26 |
|
27 |
# Video and audio processing
|
28 |
from moviepy.editor import ImageClip, AudioFileClip, concatenate_videoclips
|
29 |
-
# from moviepy.config import change_settings # Potential
|
30 |
|
31 |
# Type hints
|
32 |
import typing_extensions as typing
|
33 |
|
34 |
# Async support for Streamlit/Google API
|
35 |
import nest_asyncio
|
36 |
-
nest_asyncio.apply()
|
37 |
|
38 |
# --- Logging Setup ---
|
39 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
@@ -48,19 +48,14 @@ Generate multiple, branching story timelines from a single theme using AI, compl
|
|
48 |
""")
|
49 |
|
50 |
# --- Constants ---
|
51 |
-
|
52 |
-
|
53 |
-
# Audio Model Config
|
54 |
-
AUDIO_MODEL_ID = "models/gemini-1.5-flash" # Model used for audio tasks
|
55 |
AUDIO_SAMPLING_RATE = 24000
|
56 |
-
#
|
57 |
-
IMAGE_MODEL_ID = "imagen-3" # <<< NOTE: Likely needs Vertex AI SDK access
|
58 |
DEFAULT_ASPECT_RATIO = "1:1"
|
59 |
-
# Video Config
|
60 |
VIDEO_FPS = 24
|
61 |
VIDEO_CODEC = "libx264"
|
62 |
AUDIO_CODEC = "aac"
|
63 |
-
# File Management
|
64 |
TEMP_DIR_BASE = ".chrono_temp"
|
65 |
|
66 |
# --- API Key Handling ---
|
@@ -70,10 +65,8 @@ try:
|
|
70 |
logger.info("Google API Key loaded from Streamlit secrets.")
|
71 |
except KeyError:
|
72 |
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
|
73 |
-
if GOOGLE_API_KEY:
|
74 |
-
|
75 |
-
else:
|
76 |
-
st.error("π¨ **Google API Key Not Found!** Please configure it.", icon="π¨"); st.stop()
|
77 |
|
78 |
# --- Initialize Google Clients ---
|
79 |
try:
|
@@ -83,14 +76,11 @@ try:
|
|
83 |
logger.info(f"Initialized text/JSON model handle: {TEXT_MODEL_ID}.")
|
84 |
live_model = genai.GenerativeModel(AUDIO_MODEL_ID)
|
85 |
logger.info(f"Initialized audio model handle: {AUDIO_MODEL_ID}.")
|
86 |
-
image_model_genai = genai.GenerativeModel(IMAGE_MODEL_ID)
|
87 |
logger.info(f"Initialized google-generativeai handle for image model: {IMAGE_MODEL_ID} (May require Vertex AI SDK).")
|
88 |
# ---> TODO: Initialize Vertex AI client here if switching SDK <---
|
89 |
-
|
90 |
-
except
|
91 |
-
logger.exception("AttributeError during Client Init."); st.error(f"π¨ Init Error: {ae}. Update library?", icon="π¨"); st.stop()
|
92 |
-
except Exception as e:
|
93 |
-
logger.exception("Failed to initialize Google Clients/Models."); st.error(f"π¨ Failed Init: {e}", icon="π¨"); st.stop()
|
94 |
|
95 |
# --- Define Pydantic Schemas (Using V2 Syntax) ---
|
96 |
class StorySegment(BaseModel):
|
@@ -116,21 +106,33 @@ class ChronoWeaveResponse(BaseModel):
|
|
116 |
def check_timeline_segment_count(self) -> 'ChronoWeaveResponse':
|
117 |
expected = self.total_scenes_per_timeline
|
118 |
for i, t in enumerate(self.timelines):
|
119 |
-
if len(t.segments) != expected: raise ValueError(f"Timeline {i} ID {t.timeline_id}: Expected {expected}
|
120 |
return self
|
121 |
|
122 |
# --- Helper Functions ---
|
|
|
|
|
123 |
@contextlib.contextmanager
|
124 |
def wave_file_writer(filename: str, channels: int = 1, rate: int = AUDIO_SAMPLING_RATE, sample_width: int = 2):
|
125 |
"""Context manager to safely write WAV files."""
|
126 |
wf = None
|
127 |
try:
|
128 |
-
|
|
|
|
|
|
|
|
|
129 |
yield wf
|
130 |
-
except Exception as e:
|
|
|
|
|
131 |
finally:
|
132 |
-
if wf:
|
133 |
-
|
|
|
|
|
|
|
|
|
134 |
|
135 |
|
136 |
async def generate_audio_live_async(api_text: str, output_filename: str, voice: Optional[str] = None) -> Optional[str]:
|
@@ -138,16 +140,10 @@ async def generate_audio_live_async(api_text: str, output_filename: str, voice:
|
|
138 |
collected_audio = bytearray(); task_id = os.path.basename(output_filename).split('.')[0]
|
139 |
logger.info(f"ποΈ [{task_id}] Requesting audio: '{api_text[:60]}...'")
|
140 |
try:
|
141 |
-
#
|
142 |
-
config = {
|
143 |
-
"response_modalities": ["AUDIO"],
|
144 |
-
# Removed 'audio_config' nesting
|
145 |
-
"audio_encoding": "LINEAR16",
|
146 |
-
"sample_rate_hertz": AUDIO_SAMPLING_RATE,
|
147 |
-
# Add other parameters like "voice" here directly if needed
|
148 |
-
}
|
149 |
directive_prompt = f"Narrate directly: \"{api_text}\""
|
150 |
-
async with live_model.connect(config=config) as session:
|
151 |
await session.send_request([directive_prompt])
|
152 |
async for response in session.stream_content():
|
153 |
if response.audio_chunk and response.audio_chunk.data: collected_audio.extend(response.audio_chunk.data)
|
@@ -157,11 +153,7 @@ async def generate_audio_live_async(api_text: str, output_filename: str, voice:
|
|
157 |
logger.info(f" β
[{task_id}] Audio saved: {os.path.basename(output_filename)} ({len(collected_audio)} bytes)")
|
158 |
return output_filename
|
159 |
except genai.types.generation_types.BlockedPromptException as bpe: logger.error(f" β [{task_id}] Audio blocked: {bpe}"); st.error(f"Audio blocked {task_id}.", icon="π"); return None
|
160 |
-
|
161 |
-
except TypeError as te:
|
162 |
-
logger.exception(f" β [{task_id}] Audio config TypeError: {te}")
|
163 |
-
st.error(f"Audio configuration error for {task_id} (TypeError): {te}. Check library version/config structure.", icon="βοΈ")
|
164 |
-
return None
|
165 |
except Exception as e: logger.exception(f" β [{task_id}] Audio failed: {e}"); st.error(f"Audio failed {task_id}: {e}", icon="π"); return None
|
166 |
|
167 |
|
@@ -253,7 +245,7 @@ if generate_button:
|
|
253 |
|
254 |
# --- 2b. Audio Generation ---
|
255 |
generated_audio_path: Optional[str] = None
|
256 |
-
if not scene_has_error: # Should not be reached currently
|
257 |
with st.spinner(f"[{task_id}] Generating audio... π"):
|
258 |
audio_path_temp = os.path.join(temp_dir, f"{task_id}_audio.wav")
|
259 |
try: generated_audio_path = asyncio.run(generate_audio_live_async(segment.audio_text, audio_path_temp, audio_voice))
|
@@ -319,7 +311,7 @@ if generate_button:
|
|
319 |
scene_errors = [err for err in generation_errors[timeline_id] if not err.startswith(f"T{timeline_id}:")]
|
320 |
if scene_errors:
|
321 |
with st.expander(f"β οΈ View {len(scene_errors)} Scene Issues"):
|
322 |
-
for err in scene_errors: st.warning(f"- {err}")
|
323 |
except FileNotFoundError: logger.error(f"Video missing: {video_path}"); st.error(f"Error: Video missing T{timeline_id}.", icon="π¨")
|
324 |
except Exception as e: logger.exception(f"Display error {video_path}: {e}"); st.error(f"Display error T{timeline_id}: {e}", icon="π¨")
|
325 |
else: # No videos generated
|
|
|
26 |
|
27 |
# Video and audio processing
|
28 |
from moviepy.editor import ImageClip, AudioFileClip, concatenate_videoclips
|
29 |
+
# from moviepy.config import change_settings # Potential
|
30 |
|
31 |
# Type hints
|
32 |
import typing_extensions as typing
|
33 |
|
34 |
# Async support for Streamlit/Google API
|
35 |
import nest_asyncio
|
36 |
+
nest_asyncio.apply()
|
37 |
|
38 |
# --- Logging Setup ---
|
39 |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
|
48 |
""")
|
49 |
|
50 |
# --- Constants ---
|
51 |
+
TEXT_MODEL_ID = "models/gemini-1.5-flash"
|
52 |
+
AUDIO_MODEL_ID = "models/gemini-1.5-flash"
|
|
|
|
|
53 |
AUDIO_SAMPLING_RATE = 24000
|
54 |
+
IMAGE_MODEL_ID = "imagen-3" # <<< NOTE: Requires Vertex AI SDK access
|
|
|
55 |
DEFAULT_ASPECT_RATIO = "1:1"
|
|
|
56 |
VIDEO_FPS = 24
|
57 |
VIDEO_CODEC = "libx264"
|
58 |
AUDIO_CODEC = "aac"
|
|
|
59 |
TEMP_DIR_BASE = ".chrono_temp"
|
60 |
|
61 |
# --- API Key Handling ---
|
|
|
65 |
logger.info("Google API Key loaded from Streamlit secrets.")
|
66 |
except KeyError:
|
67 |
GOOGLE_API_KEY = os.environ.get('GOOGLE_API_KEY')
|
68 |
+
if GOOGLE_API_KEY: logger.info("Google API Key loaded from environment variable.")
|
69 |
+
else: st.error("π¨ **Google API Key Not Found!** Please configure it.", icon="π¨"); st.stop()
|
|
|
|
|
70 |
|
71 |
# --- Initialize Google Clients ---
|
72 |
try:
|
|
|
76 |
logger.info(f"Initialized text/JSON model handle: {TEXT_MODEL_ID}.")
|
77 |
live_model = genai.GenerativeModel(AUDIO_MODEL_ID)
|
78 |
logger.info(f"Initialized audio model handle: {AUDIO_MODEL_ID}.")
|
79 |
+
image_model_genai = genai.GenerativeModel(IMAGE_MODEL_ID)
|
80 |
logger.info(f"Initialized google-generativeai handle for image model: {IMAGE_MODEL_ID} (May require Vertex AI SDK).")
|
81 |
# ---> TODO: Initialize Vertex AI client here if switching SDK <---
|
82 |
+
except AttributeError as ae: logger.exception("AttributeError during Client Init."); st.error(f"π¨ Init Error: {ae}. Update library?", icon="π¨"); st.stop()
|
83 |
+
except Exception as e: logger.exception("Failed to initialize Google Clients/Models."); st.error(f"π¨ Failed Init: {e}", icon="π¨"); st.stop()
|
|
|
|
|
|
|
84 |
|
85 |
# --- Define Pydantic Schemas (Using V2 Syntax) ---
|
86 |
class StorySegment(BaseModel):
|
|
|
106 |
def check_timeline_segment_count(self) -> 'ChronoWeaveResponse':
|
107 |
expected = self.total_scenes_per_timeline
|
108 |
for i, t in enumerate(self.timelines):
|
109 |
+
if len(t.segments) != expected: raise ValueError(f"Timeline {i} ID {t.timeline_id}: Expected {expected}, found {len(t.segments)}.")
|
110 |
return self
|
111 |
|
112 |
# --- Helper Functions ---
|
113 |
+
|
114 |
+
# CORRECTED wave_file_writer function with proper indentation
|
115 |
@contextlib.contextmanager
|
116 |
def wave_file_writer(filename: str, channels: int = 1, rate: int = AUDIO_SAMPLING_RATE, sample_width: int = 2):
|
117 |
"""Context manager to safely write WAV files."""
|
118 |
wf = None
|
119 |
try:
|
120 |
+
# Indented correctly
|
121 |
+
wf = wave.open(filename, "wb")
|
122 |
+
wf.setnchannels(channels)
|
123 |
+
wf.setsampwidth(sample_width)
|
124 |
+
wf.setframerate(rate)
|
125 |
yield wf
|
126 |
+
except Exception as e:
|
127 |
+
logger.error(f"Error wave file {filename}: {e}")
|
128 |
+
raise
|
129 |
finally:
|
130 |
+
if wf:
|
131 |
+
# Indented correctly
|
132 |
+
try:
|
133 |
+
wf.close()
|
134 |
+
except Exception as e_close:
|
135 |
+
logger.error(f"Error closing wave file {filename}: {e_close}")
|
136 |
|
137 |
|
138 |
async def generate_audio_live_async(api_text: str, output_filename: str, voice: Optional[str] = None) -> Optional[str]:
|
|
|
140 |
collected_audio = bytearray(); task_id = os.path.basename(output_filename).split('.')[0]
|
141 |
logger.info(f"ποΈ [{task_id}] Requesting audio: '{api_text[:60]}...'")
|
142 |
try:
|
143 |
+
# Corrected config structure
|
144 |
+
config = {"response_modalities": ["AUDIO"], "audio_encoding": "LINEAR16", "sample_rate_hertz": AUDIO_SAMPLING_RATE}
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
directive_prompt = f"Narrate directly: \"{api_text}\""
|
146 |
+
async with live_model.connect(config=config) as session:
|
147 |
await session.send_request([directive_prompt])
|
148 |
async for response in session.stream_content():
|
149 |
if response.audio_chunk and response.audio_chunk.data: collected_audio.extend(response.audio_chunk.data)
|
|
|
153 |
logger.info(f" β
[{task_id}] Audio saved: {os.path.basename(output_filename)} ({len(collected_audio)} bytes)")
|
154 |
return output_filename
|
155 |
except genai.types.generation_types.BlockedPromptException as bpe: logger.error(f" β [{task_id}] Audio blocked: {bpe}"); st.error(f"Audio blocked {task_id}.", icon="π"); return None
|
156 |
+
except TypeError as te: logger.exception(f" β [{task_id}] Audio config TypeError: {te}"); st.error(f"Audio config error {task_id} (TypeError): {te}. Check library/config.", icon="βοΈ"); return None
|
|
|
|
|
|
|
|
|
157 |
except Exception as e: logger.exception(f" β [{task_id}] Audio failed: {e}"); st.error(f"Audio failed {task_id}: {e}", icon="π"); return None
|
158 |
|
159 |
|
|
|
245 |
|
246 |
# --- 2b. Audio Generation ---
|
247 |
generated_audio_path: Optional[str] = None
|
248 |
+
if not scene_has_error: # Should not be reached currently
|
249 |
with st.spinner(f"[{task_id}] Generating audio... π"):
|
250 |
audio_path_temp = os.path.join(temp_dir, f"{task_id}_audio.wav")
|
251 |
try: generated_audio_path = asyncio.run(generate_audio_live_async(segment.audio_text, audio_path_temp, audio_voice))
|
|
|
311 |
scene_errors = [err for err in generation_errors[timeline_id] if not err.startswith(f"T{timeline_id}:")]
|
312 |
if scene_errors:
|
313 |
with st.expander(f"β οΈ View {len(scene_errors)} Scene Issues"):
|
314 |
+
for err in scene_errors: st.warning(f"- {err}") # Use standard loop
|
315 |
except FileNotFoundError: logger.error(f"Video missing: {video_path}"); st.error(f"Error: Video missing T{timeline_id}.", icon="π¨")
|
316 |
except Exception as e: logger.exception(f"Display error {video_path}: {e}"); st.error(f"Display error T{timeline_id}: {e}", icon="π¨")
|
317 |
else: # No videos generated
|