mgbam commited on
Commit
48eb710
Β·
verified Β·
1 Parent(s): 18604c2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +33 -41
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 for setting imagemagick path if needed
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() # Apply patch for asyncio in environments like Streamlit/Jupyter
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
- # Text/JSON Model
52
- TEXT_MODEL_ID = "models/gemini-1.5-flash" # Or "gemini-1.5-pro"
53
- # Audio Model Config
54
- AUDIO_MODEL_ID = "models/gemini-1.5-flash" # Model used for audio tasks
55
  AUDIO_SAMPLING_RATE = 24000
56
- # Image Model Config
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
- logger.info("Google API Key loaded from environment variable.")
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) # Retained but likely needs Vertex SDK
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 AttributeError as ae:
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} segments, found {len(t.segments)}.")
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
- wf = wave.open(filename, "wb"); wf.setnchannels(channels); wf.setsampwidth(sample_width); wf.setframerate(rate)
 
 
 
 
129
  yield wf
130
- except Exception as e: logger.error(f"Error wave file {filename}: {e}"); raise
 
 
131
  finally:
132
- if wf: try: wf.close()
133
- except Exception as e_close: logger.error(f"Error closing wave file {filename}: {e_close}")
 
 
 
 
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
- # CORRECTED config structure for audio generation <<<<<<-------
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: # Pass corrected config
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
- # Catch TypeError specifically for config issues
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 due to image fail
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