rairo commited on
Commit
7dbd51b
·
verified ·
1 Parent(s): adf7b4d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +138 -602
app.py CHANGED
@@ -16,17 +16,14 @@ import logging
16
  import uuid
17
  import subprocess
18
  from pathlib import Path
19
- import wikipedia # using the PyPI wikipedia package
20
  import urllib.parse
21
  import pandas as pd
22
- from PyPDF2 import PdfReader
23
  import plotly.graph_objects as go
24
  import matplotlib.pyplot as plt
25
  from langchain_google_genai import ChatGoogleGenerativeAI
26
  # For PandasAI using a single dataframe
27
  from pandasai import SmartDataframe
28
  from pandasai.responses.response_parser import ResponseParser
29
- #from langchain_community.chat_models.sambanova import ChatSambaNovaCloud
30
  from pandasai.exceptions import InvalidOutputValueMismatch
31
  import base64
32
  import os
@@ -45,7 +42,7 @@ class StreamLitResponse(ResponseParser):
45
  def __init__(self, context):
46
  super().__init__(context)
47
  # Ensure the export directory exists
48
- os.makedirs("/home/user/app/exports/charts", exist_ok=True)
49
 
50
  def format_dataframe(self, result):
51
  """
@@ -56,7 +53,7 @@ class StreamLitResponse(ResponseParser):
56
  df = result['value']
57
  # Apply styling if desired
58
  styled_df = df.style
59
- img_path = f"/home/user/app/exports/charts/{uuid.uuid4().hex}.png"
60
  dfi.export(styled_df, img_path)
61
  except Exception as e:
62
  print("Error in format_dataframe:", e)
@@ -71,7 +68,7 @@ class StreamLitResponse(ResponseParser):
71
  # Case 1: If it's a matplotlib figure
72
  if hasattr(img_value, "savefig"):
73
  try:
74
- img_path = f"/home/user/app/exports/charts/{uuid.uuid4().hex}.png"
75
  img_value.savefig(img_path, format="png")
76
  return {'type': 'plot', 'value': img_path}
77
  except Exception as e:
@@ -85,7 +82,7 @@ class StreamLitResponse(ResponseParser):
85
  # Case 3: If it's a BytesIO object
86
  if isinstance(img_value, io.BytesIO):
87
  try:
88
- img_path = f"/home/user/app/exports/charts/{uuid.uuid4().hex}.png"
89
  with open(img_path, "wb") as f:
90
  f.write(img_value.getvalue())
91
  return {'type': 'plot', 'value': img_path}
@@ -100,7 +97,7 @@ class StreamLitResponse(ResponseParser):
100
  if "base64," in img_value:
101
  img_value = img_value.split("base64,")[1]
102
  # Decode and save to file
103
- img_path = f"/home/user/app/exports/charts/{uuid.uuid4().hex}.png"
104
  with open(img_path, "wb") as f:
105
  f.write(base64.b64decode(img_value))
106
  return {'type': 'plot', 'value': img_path}
@@ -118,7 +115,7 @@ class StreamLitResponse(ResponseParser):
118
 
119
  guid = uuid.uuid4()
120
  new_filename = f"{guid}"
121
- user_defined_path = os.path.join("/exports/charts/", new_filename)
122
 
123
  img_ID = "344744a88ad1098"
124
  img_secret = "3c542a40c215327045d7155bddfd8b8bc84aebbf"
@@ -144,25 +141,22 @@ headers = {"Authorization": f"Bearer {token}"}
144
 
145
  # Pandasai gemini
146
  llm1 = ChatGoogleGenerativeAI(
147
- model="gemini-2.0-flash-thinking-exp",
148
  temperature=0,
149
  max_tokens=None,
150
  timeout=1000,
151
  max_retries=2
152
  )
153
 
154
- # Initialize the supdata client
155
- SUPADATA = os.getenv('SUPADATA')
156
- supadata = Supadata(api_key=f"{SUPADATA}")
157
  # -----------------------
158
  # Utility Constants
159
  # -----------------------
160
- MAX_CHARACTERS = 200000 # Approximate token limit: 50,000 tokens ~ 200,000 characters
161
 
162
  def configure_gemini(api_key):
163
  try:
164
  genai.configure(api_key=api_key)
165
- return genai.GenerativeModel('gemini-2.0-flash-thinking-exp')
166
  except Exception as e:
167
  logger.error(f"Error configuring Gemini: {str(e)}")
168
  raise
@@ -172,60 +166,10 @@ model = configure_gemini(GOOGLE_API_KEY)
172
  os.environ["GEMINI_API_KEY"] = GOOGLE_API_KEY
173
 
174
  # -----------------------
175
- # File Upload Helpers
176
- # -----------------------
177
- def get_pdf_text(pdf_file):
178
- """Extract text from a PDF file and enforce token limit."""
179
- text = ""
180
- pdf_reader = PdfReader(pdf_file)
181
- for page in pdf_reader.pages:
182
- page_text = page.extract_text()
183
- if page_text:
184
- text += page_text + "\n"
185
- if len(text) > MAX_CHARACTERS:
186
- text = text[:MAX_CHARACTERS]
187
- return text
188
-
189
-
190
- # -----------------------
191
- # Audio Transcription
192
- # -----------------------
193
-
194
- def transcribe_audio(audio_file):
195
- """
196
- Transcribe audio using DeepGram's API (model: nova-3).
197
- Expects a WAV audio file.
198
- """
199
- deepgram_api_key = os.getenv("DeepGram")
200
- if not deepgram_api_key:
201
- st.error("DeepGram API Key is missing. Please set DEEPGRAM_API_KEY in environment variables.")
202
- return None
203
- headers_transcribe = {
204
- "Authorization": f"Token {deepgram_api_key}",
205
- "Content-Type": "audio/wav"
206
- }
207
- url = "https://api.deepgram.com/v1/listen?model=nova-3"
208
- try:
209
- audio_bytes = audio_file.read()
210
- response = requests.post(url, headers=headers_transcribe, data=audio_bytes)
211
- if response.status_code == 200:
212
- data = response.json()
213
- transcription = data.get("text", "")
214
- return transcription
215
- else:
216
- st.error(f"Deepgram transcription error: {response.status_code}")
217
- return None
218
- except Exception as e:
219
- st.error(f"Error during transcription: {e}")
220
- return None
221
-
222
- # -----------------------
223
- # PandasAI Response for DataFrame (using SmartDataframe and ChatSambaNovaCloud)
224
  # -----------------------
225
  def generateResponse(prompt, df):
226
-
227
- """Generate response using PandasAI with SmartDataframe and the ChatSambaNovaCloud LLM."""
228
-
229
  pandas_agent = SmartDataframe(df, config={"llm": llm1, "custom_whitelisted_dependencies": [
230
  "os",
231
  "io",
@@ -247,9 +191,6 @@ def generateResponse(prompt, df):
247
  def generate_story_from_dataframe(df, story_type):
248
  """
249
  Generate a data-based story from a CSV/Excel file.
250
- The dataframe is converted to a JSON string and used as input in a prompt that instructs the model to produce
251
- exactly 5 sections. Each section includes a brief analysis and an image description inside <>.
252
- For dataframe stories, the image descriptions should be chart prompts based on the data.
253
  """
254
  df_json = json.dumps(df.to_dict())
255
  prompts = {
@@ -278,14 +219,13 @@ def generate_story_from_dataframe(df, story_type):
278
  if not response or not response.text:
279
  return None
280
 
281
- # Ensure exactly 5 sections
282
  sections = response.text.split("[break]")
283
- sections = [s.strip() for s in sections if s.strip()] # Remove empty sections
284
 
285
  if len(sections) < 5:
286
- sections += ["(Placeholder section)"] * (5 - len(sections)) # Fill missing sections
287
  elif len(sections) > 5:
288
- sections = sections[:5] # Trim excess sections
289
 
290
  return "[break]".join(sections)
291
 
@@ -293,171 +233,6 @@ def generate_story_from_dataframe(df, story_type):
293
  st.error(f"Error generating story from dataframe: {e}")
294
  return None
295
 
296
-
297
- # -----------------------
298
- # Existing Story Generation Functions (Text, Wikipedia, Bible, Youtube(new))
299
- # -----------------------
300
- def generate_story_from_text(prompt_text, story_type):
301
- prompts = {
302
- "free_form": "You are a professional storyteller. Based on the prompt: " + prompt_text + ", create an engaging and concise story. ",
303
- "children": "You are a professional storyteller for children. Based on the prompt: " + prompt_text + ", create a fun and concise story. ",
304
- "education": "You are a professional storyteller. Based on the prompt: " + prompt_text + ", create an educational and engaging story. ",
305
- "business": "You are a professional storyteller. Based on the prompt: " + prompt_text + ", create a professional business story. ",
306
- "entertainment": "You are a professional storyteller. Based on the prompt: " + prompt_text + ", create an entertaining and concise story. "
307
- }
308
- story_prompt = prompts.get(story_type, prompts["free_form"])
309
- response = model.generate_content(
310
- story_prompt +
311
- "Write a short story for a narrator meaning no labels of pages or sections the story should just flow and narrated in 2 minutes or less. Divide your story into exactly 5 sections separated by [break]. For each section, include an image description inside <>."
312
- )
313
- return response.text if response else None
314
-
315
- def generate_story_from_wiki(wiki_url, story_type):
316
- try:
317
- page_title = wiki_url.rstrip("/").split("/")[-1]
318
- wikipedia.set_lang("en")
319
- page = wikipedia.page(page_title)
320
- wiki_text = page.summary
321
- prompts = {
322
- "free_form": "You are a professional storyteller. Using the following Wikipedia info: " + wiki_text +
323
- ", create an engaging and concise story. ",
324
- "children": "You are a professional storyteller for children. Using the following Wikipedia info: " + wiki_text +
325
- ", create a fun and concise story. ",
326
- "education": "You are a professional storyteller. Using the following Wikipedia info: " + wiki_text +
327
- ", create an educational and engaging story. ",
328
- "business": "You are a professional storyteller. Using the following Wikipedia info: " + wiki_text +
329
- ", create a professional business story. ",
330
- "entertainment": "You are a professional storyteller. Using the following Wikipedia info: " + wiki_text +
331
- ", create an entertaining and concise story. "
332
- }
333
- story_prompt = prompts.get(story_type, prompts["free_form"])
334
- response = model.generate_content(
335
- story_prompt +
336
- "Write a short story for a narrator meaning no labels of pages or sections the story should just flow and narrated in 2 minutes or less. Divide your story into exactly 5 sections separated by [break]. For each section, include an image description inside <>."
337
- )
338
- return response.text if response else None
339
- except Exception as e:
340
- st.error(f"Error generating story from Wikipedia: {e}")
341
- return None
342
-
343
- def fetch_bible_text(reference):
344
- m = re.match(r"(?P<book>[1-3]?\s*\w+(?:\s+\w+)*)\s+(?P<chapter>\d+)(?::(?P<verse_start>\d+)(?:-(?P<verse_end>\d+))?)?", reference)
345
- if not m:
346
- st.error("Bible reference format invalid. Use format like 'Genesis 1:1-5' or 'Psalms 23'.")
347
- return None
348
- book = m.group("book").strip().lower().replace(" ", "")
349
- chapter = m.group("chapter")
350
- verse_start = m.group("verse_start")
351
- verse_end = m.group("verse_end")
352
- if verse_start:
353
- if verse_end is None:
354
- verse_range = [verse_start]
355
- else:
356
- verse_range = [str(v) for v in range(int(verse_start), int(verse_end) + 1)]
357
- verses_text = []
358
- for verse in verse_range:
359
- url = f"https://cdn.jsdelivr.net/gh/wldeh/bible-api/bibles/en-asv/books/{book}/chapters/{chapter}/verses/{verse}.json"
360
- try:
361
- response = requests.get(url)
362
- if response.status_code == 200:
363
- data = response.json()
364
- verses_text.append(data.get("text", ""))
365
- else:
366
- verses_text.append(f"[Error fetching verse {verse}]")
367
- except Exception as e:
368
- verses_text.append(f"[Exception fetching verse {verse}: {e}]")
369
- return " ".join(verses_text)
370
- else:
371
- url = f"https://cdn.jsdelivr.net/gh/wldeh/bible-api/bibles/en-asv/books/{book}/chapters/{chapter}.json"
372
- try:
373
- response = requests.get(url)
374
- if response.status_code == 200:
375
- data = response.json()
376
- if isinstance(data, list):
377
- verses = [verse.get("text", "") for verse in data]
378
- return " ".join(verses)
379
- elif isinstance(data, dict) and "verses" in data:
380
- verses = [verse.get("text", "") for verse in data["verses"]]
381
- return " ".join(verses)
382
- else:
383
- return str(data)
384
- else:
385
- st.error("Error fetching chapter text.")
386
- return None
387
- except Exception as e:
388
- st.error(f"Exception fetching chapter: {e}")
389
- return None
390
-
391
- def generate_story_from_bible(reference, story_type):
392
- bible_text = fetch_bible_text(reference)
393
- if bible_text is None:
394
- return None
395
- prompts = {
396
- "free_form": "You are a professional storyteller. Using the following Bible text: " + bible_text +
397
- ", create an engaging and concise story. ",
398
- "children": "You are a professional storyteller for children. Using the following Bible text: " + bible_text +
399
- ", create a fun and concise story. ",
400
- "education": "You are a professional storyteller. Using the following Bible text: " + bible_text +
401
- ", create an educational and engaging story. ",
402
- "business": "You are a professional storyteller. Using the following Bible text: " + bible_text +
403
- ", create a professional business story. ",
404
- "entertainment": "You are a professional storyteller. Using the following Bible text: " + bible_text +
405
- ", create an entertaining and concise story. "
406
- }
407
- story_prompt = prompts.get(story_type, prompts["free_form"])
408
- response = model.generate_content(
409
- story_prompt +
410
- "Write a short story for a narrator meaning no labels of pages or sections the story should just flow and narrated in 2 minutes or less. Divide your story into exactly 5 sections separated by [break]. For each section, include a brief image description inside <>."
411
- )
412
- return response.text if response else None
413
-
414
-
415
- def generate_story_from_youtube(youtube_url, story_type):
416
- try:
417
- # Extract video_id from the URL
418
- if "v=" in youtube_url:
419
- video_id = youtube_url.split("v=")[1].split("&")[0]
420
- elif "youtu.be/" in youtube_url:
421
- video_id = youtube_url.split("youtu.be/")[1].split("?")[0]
422
- else:
423
- raise ValueError("Invalid YouTube URL provided.")
424
-
425
- # Retrieve the transcript as a list of dictionaries
426
- transcript_res = supadata.youtube.transcript(
427
- video_id=video_id,
428
- text=True
429
- )
430
- transcript_text = transcript_res.content
431
- # Define story prompts based on story_type, similar to the Wikipedia function
432
- prompts = {
433
- "free_form": "You are a professional storyteller. Using the following YouTube transcript: " + transcript_text +
434
- ", create an engaging and concise story. ",
435
- "children": "You are a professional storyteller for children. Using the following YouTube transcript: " + transcript_text +
436
- ", create a fun and concise story. ",
437
- "education": "You are a professional storyteller. Using the following YouTube transcript: " + transcript_text +
438
- ", create an educational and engaging story. ",
439
- "business": "You are a professional storyteller. Using the following YouTube transcript: " + transcript_text +
440
- ", create a professional business story. ",
441
- "entertainment": "You are a professional storyteller. Using the following YouTube transcript: " + transcript_text +
442
- ", create an entertaining and concise story. "
443
- }
444
- # Use the provided story_type, defaulting to free_form if not found
445
- story_prompt = prompts.get(story_type, prompts["free_form"])
446
-
447
- # Append additional instructions for story structure
448
- full_prompt = story_prompt + (
449
- "Write a short story for a narrator meaning no labels of pages or sections the story should just flow and narrated in 2 minutes or less. Divide your story into exactly 5 sections separated by [break]. "
450
- "For each section, include an image description inside <>."
451
- )
452
-
453
- # Generate content using your model (assumes model.generate_content is available)
454
- response = model.generate_content(full_prompt)
455
- return response.text if response else None
456
-
457
- except Exception as e:
458
- st.error(f"Error generating story from YouTube transcript: {e}")
459
- return None
460
-
461
  # -----------------------
462
  # Extract Image Prompts and Story Sections
463
  # -----------------------
@@ -479,127 +254,72 @@ def extract_image_prompts_and_story(story_text):
479
  return pages, image_prompts
480
 
481
  def is_valid_png(file_path):
482
- """Check if the PNG file at `file_path` is valid."""
483
  try:
484
  with open(file_path, "rb") as f:
485
- # Read the first 8 bytes to check the PNG signature
486
  header = f.read(8)
487
  if header != b'\x89PNG\r\n\x1a\n':
488
  return False
489
-
490
- # Attempt to open and verify the entire image
491
  with Image.open(file_path) as img:
492
- img.verify() # Verify the file integrity
493
  return True
494
  except Exception as e:
495
  print(f"Invalid PNG file at {file_path}: {e}")
496
  return False
497
 
498
-
499
  def standardize_and_validate_image(file_path):
500
- """Validate, standardize, and overwrite the image at `file_path`."""
501
  try:
502
- # Verify basic integrity
503
  with Image.open(file_path) as img:
504
  img.verify()
505
-
506
- # Reopen and convert to RGB
507
  with Image.open(file_path) as img:
508
- img = img.convert("RGB") # Remove alpha channel if present
509
-
510
- # Save to a temporary BytesIO buffer first
511
  buffer = io.BytesIO()
512
  img.save(buffer, format="PNG")
513
  buffer.seek(0)
514
-
515
- # Write the buffer to the file
516
  with open(file_path, "wb") as f:
517
  f.write(buffer.getvalue())
518
-
519
  return True
520
  except Exception as e:
521
  print(f"Failed to standardize/validate {file_path}: {e}")
522
  return False
523
 
524
  def generate_image(prompt_text, style, model="hf"):
525
- """
526
- Generate an image from a text prompt using either Hugging Face's, Pollinations Turbo's,
527
- or Google's Gemini API.
528
-
529
- Args:
530
- prompt_text (str): The text prompt for image generation.
531
- style (str or None): The style of the image (used for HF and Gemini models).
532
- model (str): Which model to use ("hf" for Hugging Face, "pollinations_turbo" for Pollinations Turbo,
533
- or "gemini" for Google's Gemini).
534
-
535
- Returns:
536
- tuple: A tuple containing the generated PIL.Image and a Base64 string of the image.
537
- """
538
  try:
539
  if model == "pollinations_turbo":
540
- # URL-encode the prompt and add the query parameter to specify the model as "turbo"
541
  prompt_encoded = urllib.parse.quote(prompt_text)
542
  api_url = f"https://image.pollinations.ai/prompt/{prompt_encoded}?model=turbo"
543
  response = requests.get(api_url)
544
  if response.status_code != 200:
545
  logger.error(f"Pollinations API error: {response.status_code}, {response.text}")
546
- st.error(f"Error from image generation API: {response.status_code}")
547
  return None, None
548
  image_bytes = response.content
549
 
550
  elif model == "gemini":
551
- # For Google's Gemini model
552
  try:
553
-
554
- # Get API key from environment variable
555
  g_api_key = os.getenv("GEMINI")
556
  if not g_api_key:
557
- logger.error("GEMINI_API_KEY not found in environment variables")
558
- st.error("Google Gemini API key is missing. Please set the GEMINI_API_KEY environment variable.")
559
  return None, None
560
-
561
- # Initialize Gemini client
562
  client = genai.Client(api_key=g_api_key)
563
-
564
- # Enhance prompt with style
565
  enhanced_prompt = f"image of {prompt_text} in {style} style, high quality, detailed illustration"
566
-
567
- # Generate content
568
  response = client.models.generate_content(
569
- model="models/gemini-2.0-flash-exp",
570
  contents=enhanced_prompt,
571
  config=types.GenerateContentConfig(response_modalities=['Text', 'Image'])
572
  )
573
-
574
- # Extract image from response
575
  for part in response.candidates[0].content.parts:
576
  if part.inline_data is not None:
577
  image = Image.open(BytesIO(part.inline_data.data))
578
-
579
- # Convert to base64 string
580
  buffered = io.BytesIO()
581
  image.save(buffered, format="JPEG")
582
  img_str = base64.b64encode(buffered.getvalue()).decode()
583
-
584
  return image, img_str
585
-
586
- # If no image was found in the response
587
  logger.error("No image was found in the Gemini API response")
588
- st.error("Gemini API didn't return an image")
589
  return None, None
590
-
591
- except ImportError:
592
- logger.error("Google Gemini libraries not installed")
593
- st.error("Google Gemini libraries not installed. Install with 'pip install google-genai'")
594
- return None, None
595
-
596
  except Exception as e:
597
  logger.error(f"Gemini API error: {str(e)}")
598
- st.error(f"Error from Gemini image generation: {str(e)}")
599
  return None, None
600
 
601
- else: # Default to Hugging Face model
602
- # For Hugging Face model, include style details in the prompt
603
  enhanced_prompt = f"{prompt_text} in {style} style, high quality, detailed illustration"
604
  model_id = "black-forest-labs/FLUX.1-dev"
605
  api_url = f"https://api-inference.huggingface.co/models/{model_id}"
@@ -607,11 +327,9 @@ def generate_image(prompt_text, style, model="hf"):
607
  response = requests.post(api_url, headers=headers, json=payload)
608
  if response.status_code != 200:
609
  logger.error(f"Hugging Face API error: {response.status_code}, {response.text}")
610
- st.error(f"Error from image generation API: {response.status_code}")
611
  return None, None
612
  image_bytes = response.content
613
 
614
- # For HF and Pollinations models that return image bytes
615
  if model != "gemini":
616
  image = Image.open(io.BytesIO(image_bytes))
617
  buffered = io.BytesIO()
@@ -620,25 +338,11 @@ def generate_image(prompt_text, style, model="hf"):
620
  return image, img_str
621
 
622
  except Exception as e:
623
- st.error(f"Error generating image: {e}")
624
  logger.error(f"Image generation error: {str(e)}")
625
 
626
- # Return a placeholder image in case of failure
627
  return Image.new('RGB', (1024, 1024), color=(200,200,200)), None
628
 
629
  def generate_image_with_retry(prompt_text, style, model="hf", max_retries=3):
630
- """
631
- Attempt to generate an image using generate_image, retrying up to max_retries if needed.
632
-
633
- Args:
634
- prompt_text (str): The text prompt for image generation.
635
- style (str or None): The style of the image (ignored for Pollinations Turbo).
636
- model (str): Which model to use ("hf" or "pollinations_turbo").
637
- max_retries (int): Maximum number of retries.
638
-
639
- Returns:
640
- tuple: The generated image and its Base64 string.
641
- """
642
  for attempt in range(max_retries):
643
  try:
644
  if attempt > 0:
@@ -664,18 +368,16 @@ def create_silent_video(images, durations, output_path, logo_path="sozo_logo2.pn
664
  st.error("Failed to create video file.")
665
  return None
666
 
667
- # Load font for text overlay
668
  font_size = 45
669
  font = ImageFont.truetype(font_path, font_size)
670
 
671
- # Load logo for fallback and full-screen display at the end
672
  logo = None
673
  if logo_path:
674
  logo = cv2.imread(logo_path)
675
  if logo is not None:
676
- logo = cv2.resize(logo, (width, height)) # Resize logo to full screen
677
  else:
678
- st.warning(f"Failed to load logo from {logo_path}. No fallback image will be used.")
679
 
680
  for img, duration in zip(images, durations):
681
  try:
@@ -684,42 +386,31 @@ def create_silent_video(images, durations, output_path, logo_path="sozo_logo2.pn
684
  frame = np.array(img_resized)
685
  except Exception as e:
686
  print(f"Invalid image detected, replacing with logo: {e}")
687
- if logo is not None:
688
- frame = logo # Use the logo as a fallback
689
- else:
690
- # If no logo is available, create a blank frame
691
- frame = np.zeros((height, width, 3), dtype=np.uint8)
692
 
693
- # Convert to PIL for text drawing
694
  pil_img = Image.fromarray(frame)
695
  draw = ImageDraw.Draw(pil_img)
696
 
697
- # Add "Sozo Dream Lab" text at bottom right
698
  text1 = "Made With"
699
- text2 = "Sozo Dream Lab"
700
 
701
- # Calculate the height of the first text to adjust the second text's position
702
  bbox = draw.textbbox((0, 0), text1, font=font)
703
- text1_width = bbox[2] - bbox[0]
704
  text1_height = bbox[3] - bbox[1]
705
 
706
- text_position1 = (width - 270, height - 120) # position for "Made with"
707
- text_position2 = (width - 330, height - 120 + text1_height + 5) # position for "Sozo dream lab", +5 for a little gap.
708
 
709
- draw.text(text_position1, text1, font=font, fill=(81, 34, 97, 255)) # RGB: Purple
710
- draw.text(text_position2, text2, font=font, fill=(81, 34, 97, 255)) # RGB: Purple
711
 
712
- # Convert back to OpenCV format
713
  frame = np.array(pil_img)
714
  frame_cv = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
715
 
716
- # Write frame multiple times to match duration
717
  for _ in range(int(duration * fps)):
718
  video.write(frame_cv)
719
 
720
- # Add full-screen logo frame at the end
721
  if logo is not None:
722
- for _ in range(int(3 * fps)): # Display for 3 seconds
723
  video.write(logo)
724
 
725
  video.release()
@@ -729,7 +420,6 @@ def create_silent_video(images, durations, output_path, logo_path="sozo_logo2.pn
729
  st.error(f"Error creating silent video: {e}")
730
  return None
731
 
732
-
733
  def combine_video_audio(video_path, audio_files, output_path=None):
734
  try:
735
  if output_path is None:
@@ -765,46 +455,30 @@ def combine_video_audio(video_path, audio_files, output_path=None):
765
 
766
  def create_video(images, audio_files, output_path=None):
767
  try:
768
- try:
769
- subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
770
- except FileNotFoundError:
771
- st.error("ffmpeg not installed.")
772
- return None
773
- if output_path is None:
774
- output_path = f"output_video_{uuid.uuid4()}.mp4"
775
- silent_video_path = f"silent_{uuid.uuid4()}.mp4"
776
- durations = [get_audio_duration(af) if af else 5.0 for af in audio_files]
777
- if len(durations) < len(images):
778
- durations.extend([5.0]*(len(images)-len(durations)))
779
- silent_video = create_silent_video(images, durations, silent_video_path)
780
- if not silent_video:
781
- return None
782
- final_video = combine_video_audio(silent_video, audio_files, output_path)
783
- try:
784
- os.unlink(silent_video_path)
785
- except Exception:
786
- pass
787
- return final_video
788
- except Exception:
789
  return None
 
 
 
 
 
 
790
 
791
  # -----------------------
792
  # Audio Generation Function
793
  # -----------------------
794
  def generate_audio(text, voice_model, audio_model="deepgram"):
795
- """
796
- Generate audio from text using either DeepGram or Pollinations OpenAI-Audio.
797
-
798
- Args:
799
- text (str): The text to convert to speech.
800
- voice_model (str): The voice/model to use.
801
- - For DeepGram, e.g., "aura-asteria-en" or "aura-helios-en".
802
- - For Pollinations, e.g., "sage" (female) or "echo" (male).
803
- audio_model (str): Which audio generation service to use ("deepgram" or "openai-audio").
804
-
805
- Returns:
806
- str or None: The path to the generated audio file, or None if generation failed.
807
- """
808
  if audio_model == "deepgram":
809
  deepgram_api_key = os.getenv("DeepGram")
810
  if not deepgram_api_key:
@@ -825,7 +499,6 @@ def generate_audio(text, voice_model, audio_model="deepgram"):
825
  st.error(f"DeepGram TTS error: {response.status_code}")
826
  return None
827
  elif audio_model == "openai-audio":
828
- # URL encode the text and call Pollinations TTS endpoint for openai-audio
829
  encoded_text = urllib.parse.quote(text)
830
  url = f"https://text.pollinations.ai/{encoded_text}?model=openai-audio&voice={voice_model}"
831
  response = requests.get(url)
@@ -842,14 +515,11 @@ def generate_audio(text, voice_model, audio_model="deepgram"):
842
  return None
843
 
844
  def get_audio_duration(audio_file):
845
- import subprocess
846
  try:
847
  cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
848
  '-of', 'default=noprint_wrappers=1:nokey=1', audio_file]
849
  result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
850
- if result.returncode != 0:
851
- return 5.0
852
- return float(result.stdout.strip())
853
  except Exception:
854
  return 5.0
855
 
@@ -857,93 +527,46 @@ def get_audio_duration(audio_file):
857
  # Unified Process-Story Function
858
  # -----------------------
859
  def process_generated_story(style, voice_model):
860
- """
861
- Process the generated story by creating images and audio for each section.
862
- For dataframe stories, it attempts to generate a chart image using PandasAI;
863
- if that fails, it falls back on the default image generation.
864
- This function now correctly handles images generated as file paths from base64,
865
- matplotlib figures, or BytesIO objects.
866
- """
867
- # Add browser-like headers to avoid rate limiting
868
- browser_headers = {
869
- 'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
870
- 'AppleWebKit/537.36 (KHTML, like Gecko) '
871
- 'Chrome/91.0.4472.124 Safari/537.36')
872
- }
873
-
874
- # Extract story pages and image prompts
875
  pages, image_prompts = extract_image_prompts_and_story(st.session_state.full_story)
876
  st.session_state.story_pages = pages
877
  st.session_state.image_descriptions = image_prompts
878
  st.session_state.generated_images = []
879
  st.session_state.story_audio = []
880
  progress_bar = st.progress(0)
881
- is_dataframe_story = st.session_state.dataframe is not None
 
882
 
883
- # Process each section sequentially: image then audio
884
  for i, (page, img_prompt) in enumerate(zip(pages, image_prompts)):
885
- with st.spinner(f"Generating image {i+1} of {len(pages)}..."):
886
- img = None # Ensure img is always defined
887
-
888
- if is_dataframe_story:
889
- try:
890
- # generateResponse is expected to return a dict with type 'plot'
891
- chart_response = generateResponse("Generate this visualization: " + img_prompt,
892
- st.session_state.dataframe)
893
- if isinstance(chart_response, dict) and chart_response.get("type") == "plot":
894
- img_path = chart_response["value"]
895
-
896
- # Verify that the image file is valid (this will work for images saved from base64, matplotlib, or BytesIO)
897
- if isinstance(img_path, str) and os.path.isfile(img_path):
898
- if is_valid_png(img_path) and standardize_and_validate_image(img_path):
899
- img = Image.open(img_path)
900
- else:
901
- print(f"Invalid image file at {img_path}, generating default image")
902
- img, _ = generate_image_with_retry(img_prompt, style)
903
- else:
904
- print(f"Image file not found at {img_path}, generating default image")
905
- img, _ = generate_image_with_retry(img_prompt, style)
906
  else:
907
- # Fallback if the response is not in expected dict format
908
- print("Not a valid plot response, generating default image")
909
  img, _ = generate_image_with_retry(img_prompt, style)
910
-
911
- except InvalidOutputValueMismatch as e:
912
- # Catch specific dataframe error and use fallback image generation
913
- st.warning(f"Skipping chart for section {i+1} due to invalid output type. Using default image instead.")
914
- img, _ = generate_image_with_retry(img_prompt, style)
915
-
916
- except Exception as e:
917
- # General fallback for any other errors during dataframe processing
918
- st.warning(f"Chart generation failed for section {i+1}: {str(e)}")
919
  img, _ = generate_image_with_retry(img_prompt, style)
920
- else:
921
- # Process non-dataframe story flow
922
  img, _ = generate_image_with_retry(img_prompt, style)
 
 
 
 
 
923
 
924
- # Ensure img is not None before appending; if None, create a blank image
925
- if img is None:
926
- img = Image.new('RGB', (1024, 1024), color=(200, 200, 200))
927
- else:
928
- img = img.convert('RGB')
929
- st.session_state.generated_images.append(img)
930
-
931
- # Update progress
932
- progress_bar.progress((i + 1) / len(pages))
933
-
934
-
935
- # Audio generation (unchanged)
936
  for i, page in enumerate(pages):
937
- with st.spinner(f"Generating audio {i+1} of {len(pages)}..."):
938
  audio = generate_audio(page, voice_model, audio_model=audio_model_param)
939
  st.session_state.story_audio.append(audio)
 
 
940
 
941
- # Update progress bar
942
- progress_bar.progress((i + 1) / len(pages))
943
-
944
- # Create video from the generated images and audio
945
  if st.session_state.generated_images:
946
- with st.spinner("Creating video..."):
947
  audio_paths = [af for af in st.session_state.story_audio if af]
948
  if audio_paths:
949
  st.session_state.final_video_path = create_video(st.session_state.generated_images, audio_paths)
@@ -951,211 +574,124 @@ def process_generated_story(style, voice_model):
951
  silent_path = f"silent_video_{uuid.uuid4()}.mp4"
952
  durations = [5.0] * len(st.session_state.generated_images)
953
  st.session_state.final_video_path = create_silent_video(st.session_state.generated_images, durations, silent_path)
 
954
  # -----------------------
955
  # Display Generated Content
956
  # -----------------------
957
  def display_generated_content():
958
- st.subheader("Generated Story")
959
- tab1, tab2, tab3 = st.tabs(["Story Pages", "Full Story", "Video"])
960
 
961
  with tab1:
962
- for i, (page, img) in enumerate(zip(st.session_state.story_pages, st.session_state.generated_images)):
963
- st.image(img, caption=f"Page {i+1}")
964
- st.markdown(f"**Page {i+1}**: {page[:150]}{'...' if len(page)>150 else ''}")
965
- if i < len(st.session_state.story_audio):
966
- st.audio(st.session_state.story_audio[i])
967
-
968
- with tab2:
969
- st.text_area("Complete Story", st.session_state.full_story, height=400)
970
-
971
- with tab3:
972
  if st.session_state.final_video_path and os.path.exists(st.session_state.final_video_path):
973
  with open(st.session_state.final_video_path, "rb") as f:
974
  video_bytes = f.read()
975
  st.video(video_bytes)
976
- st.download_button("Download Video", data=video_bytes, file_name="story_video.mp4", mime="video/mp4")
977
- share_message = "Check out my AI generated story video! Download it from the attached link."
978
  whatsapp_link = f"https://api.whatsapp.com/send?text={urllib.parse.quote(share_message)}"
979
- st.markdown(f"[Share on WhatsApp with Video Attachment]({whatsapp_link})", unsafe_allow_html=True)
980
  else:
981
  st.error("Video file not found or not readable.")
982
 
 
 
 
 
 
 
 
 
 
 
 
983
  # -----------------------
984
  # Streamlit App Configuration and Sidebar
985
  # -----------------------
986
- st.set_page_config(page_title="Video Story Generator", page_icon="🎬", layout="wide", initial_sidebar_state="expanded")
987
- # Ensure session state keys are set.
988
  for key in ["story_pages", "image_descriptions", "generated_images", "story_audio", "full_story", "final_video_path", "dataframe"]:
989
  if key not in st.session_state:
990
- st.session_state[key] = [] if key in ["story_pages", "image_descriptions", "generated_images", "story_audio"] else None
991
 
992
  with st.sidebar:
993
  st.sidebar.image("sozo_logo1.jpeg", use_container_width=True)
994
- # Story Type Selection
995
  story_types = {
 
 
 
996
  "free_form": "Free Form (AI's choice)",
997
  "children": "Children's Story",
998
- "education": "Educational",
999
- "business": "Business Narrative",
1000
- "entertainment": "Entertaining"
1001
- }
1002
  selected_story_type = st.selectbox(
1003
- "Story Type",
1004
  options=list(story_types.keys()),
1005
  format_func=lambda x: story_types[x],
1006
- index=0,
1007
  key="story_type_select"
1008
  )
1009
 
1010
- # Image Generation Configuration
1011
  model_options = ["HuggingFace Flux", "Pollinations Turbo", "Google Gemini"]
1012
- selected_model_name = st.selectbox(
1013
- "Select Image Generation Model",
1014
- model_options,
1015
- index=0,
1016
- key="image_model_select"
1017
- )
1018
 
1019
- # Shared style options for all models
1020
- style_options = ["whimsical", "photorealistic", "cartoon", "concept art", "oil painting", "fantasy illustration", "cinematic"]
1021
- selected_style = st.selectbox(
1022
- "Image Style",
1023
- style_options,
1024
- index=0,
1025
- key="style_select"
1026
- )
1027
 
1028
- # Map the selected model name to the parameter value
1029
- if selected_model_name == "HuggingFace Flux":
1030
- model_param = "hf"
1031
- elif selected_model_name == "Pollinations Turbo":
1032
- model_param = "pollinations_turbo"
1033
- else:
1034
- model_param = "gemini"
1035
 
1036
- # Audio Generation Configuration
1037
  audio_model_options = ["DeepGram", "Pollinations OpenAI-Audio"]
1038
- selected_audio_model = st.selectbox(
1039
- "Select Audio Generation Model for Audio",
1040
- audio_model_options,
1041
- index=0,
1042
- key="audio_model_select"
1043
- )
1044
 
1045
  if selected_audio_model == "DeepGram":
1046
- deepgram_voice_options = {
1047
- "aura-asteria-en": "Female Voice (aura-asteria-en)",
1048
- "aura-helios-en": "Male Voice (aura-helios-en)"
1049
- }
1050
- selected_voice = st.selectbox(
1051
- "Voice Model for Audio Narration",
1052
- options=list(deepgram_voice_options.keys()),
1053
- format_func=lambda x: deepgram_voice_options[x],
1054
- index=0,
1055
- key="voice_select_deepgram"
1056
- )
1057
  audio_model_param = "deepgram"
1058
  else:
1059
- pollinations_voice_options = {
1060
- "sage": "Female Voice (sage)",
1061
- "echo": "Male Voice (echo)"
1062
- }
1063
- selected_voice = st.selectbox(
1064
- "Voice Model for Audio Narration",
1065
- options=list(pollinations_voice_options.keys()),
1066
- format_func=lambda x: pollinations_voice_options[x],
1067
- index=0,
1068
- key="voice_select_pollinations"
1069
- )
1070
  audio_model_param = "openai-audio"
1071
 
1072
  st.markdown("### Tips for Best Results")
1073
- st.markdown("""
1074
- - Use detailed prompts for best story generation.
1075
- - Try different image styles for varied visuals.
1076
- - Educational stories work well with Wikipedia, Bible, or file inputs.
1077
- - Choose a story type and voice that match your audience.
1078
- """)
1079
  if st.button("Check System Requirements"):
1080
  try:
1081
  result = subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1082
- st.success("✅ ffmpeg is installed and working" if result.returncode==0 else "❌ ffmpeg error")
1083
  except FileNotFoundError:
1084
- st.error("❌ ffmpeg not installed.")
 
 
 
 
 
 
 
 
 
 
 
 
1085
 
1086
- st.subheader("🎬 Sozo. Dream Lab video story generator")
1087
- st.markdown("Generate video stories from text, wikipedia, bible verses and more using AI.")
 
 
 
 
 
 
 
1088
 
1089
- # -----------------------
1090
- # Input Method Selection
1091
- # -----------------------
1092
- input_method = st.radio("Choose input method:",
1093
- ["Text Prompt", "Wikipedia URL","Youtube URL", "Bible Reference", "Voice Input", "File Upload"])
1094
-
1095
- user_prompt = None
1096
- wiki_url = None
1097
- bible_reference = None
1098
- youtube_url = None
1099
-
1100
- if input_method == "Text Prompt":
1101
- user_prompt = st.text_area("Enter a story prompt:", value="", placeholder="A magical adventure in an enchanted forest...")
1102
- elif input_method == "Wikipedia URL":
1103
- wiki_url = st.text_input("Enter Wikipedia URL:", value="", placeholder="https://en.wikipedia.org/wiki/Elephant")
1104
- elif input_method == "Youtube URL":
1105
- youtube_url = st.text_input("Enter Youtube URL:", value="", placeholder="https://www.youtube.com/watch?v=tKKxPtP6XjQ")
1106
- elif input_method == "Bible Reference":
1107
- bible_reference = st.text_input("Enter Bible Reference (e.g. 'Genesis 1:1-5' or 'Psalms 23'):", value="")
1108
- elif input_method == "Voice Input":
1109
- uploaded_audio = st.file_uploader("Record or upload your audio input (WAV format)", type=["wav"])
1110
- if uploaded_audio is not None:
1111
- transcription = transcribe_audio(uploaded_audio)
1112
- if transcription:
1113
- st.success("Transcription successful!")
1114
- user_prompt = st.text_area("Edit transcribed prompt:", value=transcription)
1115
- else:
1116
- st.error("Failed to transcribe audio.")
1117
- elif input_method == "File Upload":
1118
- uploaded_file = st.file_uploader("Upload a PDF or CSV/Excel file", type=['pdf', 'csv', 'xlsx', 'xls'], accept_multiple_files=False)
1119
- if uploaded_file:
1120
- ext = uploaded_file.name.split(".")[-1].lower()
1121
- if ext == "pdf":
1122
- extracted_text = get_pdf_text(uploaded_file)
1123
- if extracted_text:
1124
- user_prompt = extracted_text
1125
- elif ext in ["csv", "xlsx", "xls"]:
1126
- try:
1127
- if ext == "csv":
1128
- df = pd.read_csv(uploaded_file)
1129
- else:
1130
- df = pd.read_excel(uploaded_file)
1131
- st.session_state.dataframe = df
1132
-
1133
- except Exception as e:
1134
- st.error(f"Error processing {uploaded_file.name}: {e}")
1135
-
1136
- if st.button("Generate Story"):
1137
- with st.spinner("Generating story..."):
1138
- if input_method == "Text Prompt" and user_prompt:
1139
- st.session_state.full_story = generate_story_from_text(user_prompt, selected_story_type)
1140
- elif input_method == "Wikipedia URL" and wiki_url:
1141
- st.session_state.full_story = generate_story_from_wiki(wiki_url, selected_story_type)
1142
- elif input_method == "Youtube URL" and youtube_url:
1143
- st.session_state.full_story = generate_story_from_youtube(youtube_url, selected_story_type)
1144
- elif input_method == "Bible Reference" and bible_reference:
1145
- st.session_state.full_story = generate_story_from_bible(bible_reference, selected_story_type)
1146
- elif input_method == "Voice Input" and user_prompt:
1147
- st.session_state.full_story = generate_story_from_text(user_prompt, selected_story_type)
1148
- elif input_method == "File Upload" and not st.session_state.full_story:
1149
- if user_prompt: # PDF fallback
1150
- st.session_state.full_story = generate_story_from_text(user_prompt, selected_story_type)
1151
- elif st.session_state.dataframe is not None:
1152
- st.session_state.full_story = generate_story_from_dataframe(df, selected_story_type)
1153
- else:
1154
- st.error("Please provide valid input for the selected method.")
1155
- if st.session_state.full_story:
1156
- process_generated_story(selected_style, selected_voice)
1157
- else:
1158
- st.error("Failed to generate story. Please try a different prompt.")
1159
 
1160
  if st.session_state.story_pages:
 
1161
  display_generated_content()
 
16
  import uuid
17
  import subprocess
18
  from pathlib import Path
 
19
  import urllib.parse
20
  import pandas as pd
 
21
  import plotly.graph_objects as go
22
  import matplotlib.pyplot as plt
23
  from langchain_google_genai import ChatGoogleGenerativeAI
24
  # For PandasAI using a single dataframe
25
  from pandasai import SmartDataframe
26
  from pandasai.responses.response_parser import ResponseParser
 
27
  from pandasai.exceptions import InvalidOutputValueMismatch
28
  import base64
29
  import os
 
42
  def __init__(self, context):
43
  super().__init__(context)
44
  # Ensure the export directory exists
45
+ os.makedirs("./exports/charts", exist_ok=True)
46
 
47
  def format_dataframe(self, result):
48
  """
 
53
  df = result['value']
54
  # Apply styling if desired
55
  styled_df = df.style
56
+ img_path = f"./exports/charts/{uuid.uuid4().hex}.png"
57
  dfi.export(styled_df, img_path)
58
  except Exception as e:
59
  print("Error in format_dataframe:", e)
 
68
  # Case 1: If it's a matplotlib figure
69
  if hasattr(img_value, "savefig"):
70
  try:
71
+ img_path = f"./exports/charts/{uuid.uuid4().hex}.png"
72
  img_value.savefig(img_path, format="png")
73
  return {'type': 'plot', 'value': img_path}
74
  except Exception as e:
 
82
  # Case 3: If it's a BytesIO object
83
  if isinstance(img_value, io.BytesIO):
84
  try:
85
+ img_path = f"./exports/charts/{uuid.uuid4().hex}.png"
86
  with open(img_path, "wb") as f:
87
  f.write(img_value.getvalue())
88
  return {'type': 'plot', 'value': img_path}
 
97
  if "base64," in img_value:
98
  img_value = img_value.split("base64,")[1]
99
  # Decode and save to file
100
+ img_path = f"./exports/charts/{uuid.uuid4().hex}.png"
101
  with open(img_path, "wb") as f:
102
  f.write(base64.b64decode(img_value))
103
  return {'type': 'plot', 'value': img_path}
 
115
 
116
  guid = uuid.uuid4()
117
  new_filename = f"{guid}"
118
+ user_defined_path = os.path.join("./exports/charts/", new_filename)
119
 
120
  img_ID = "344744a88ad1098"
121
  img_secret = "3c542a40c215327045d7155bddfd8b8bc84aebbf"
 
141
 
142
  # Pandasai gemini
143
  llm1 = ChatGoogleGenerativeAI(
144
+ model="gemini-2.0-flash-thinking-exp", # MODEL REVERTED
145
  temperature=0,
146
  max_tokens=None,
147
  timeout=1000,
148
  max_retries=2
149
  )
150
 
 
 
 
151
  # -----------------------
152
  # Utility Constants
153
  # -----------------------
154
+ MAX_CHARACTERS = 200000
155
 
156
  def configure_gemini(api_key):
157
  try:
158
  genai.configure(api_key=api_key)
159
+ return genai.GenerativeModel('gemini-2.0-flash-thinking-exp') # MODEL REVERTED
160
  except Exception as e:
161
  logger.error(f"Error configuring Gemini: {str(e)}")
162
  raise
 
166
  os.environ["GEMINI_API_KEY"] = GOOGLE_API_KEY
167
 
168
  # -----------------------
169
+ # PandasAI Response for DataFrame
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  # -----------------------
171
  def generateResponse(prompt, df):
172
+ """Generate response using PandasAI with SmartDataframe."""
 
 
173
  pandas_agent = SmartDataframe(df, config={"llm": llm1, "custom_whitelisted_dependencies": [
174
  "os",
175
  "io",
 
191
  def generate_story_from_dataframe(df, story_type):
192
  """
193
  Generate a data-based story from a CSV/Excel file.
 
 
 
194
  """
195
  df_json = json.dumps(df.to_dict())
196
  prompts = {
 
219
  if not response or not response.text:
220
  return None
221
 
 
222
  sections = response.text.split("[break]")
223
+ sections = [s.strip() for s in sections if s.strip()]
224
 
225
  if len(sections) < 5:
226
+ sections += ["(Placeholder section)"] * (5 - len(sections))
227
  elif len(sections) > 5:
228
+ sections = sections[:5]
229
 
230
  return "[break]".join(sections)
231
 
 
233
  st.error(f"Error generating story from dataframe: {e}")
234
  return None
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  # -----------------------
237
  # Extract Image Prompts and Story Sections
238
  # -----------------------
 
254
  return pages, image_prompts
255
 
256
  def is_valid_png(file_path):
 
257
  try:
258
  with open(file_path, "rb") as f:
 
259
  header = f.read(8)
260
  if header != b'\x89PNG\r\n\x1a\n':
261
  return False
 
 
262
  with Image.open(file_path) as img:
263
+ img.verify()
264
  return True
265
  except Exception as e:
266
  print(f"Invalid PNG file at {file_path}: {e}")
267
  return False
268
 
 
269
  def standardize_and_validate_image(file_path):
 
270
  try:
 
271
  with Image.open(file_path) as img:
272
  img.verify()
 
 
273
  with Image.open(file_path) as img:
274
+ img = img.convert("RGB")
 
 
275
  buffer = io.BytesIO()
276
  img.save(buffer, format="PNG")
277
  buffer.seek(0)
 
 
278
  with open(file_path, "wb") as f:
279
  f.write(buffer.getvalue())
 
280
  return True
281
  except Exception as e:
282
  print(f"Failed to standardize/validate {file_path}: {e}")
283
  return False
284
 
285
  def generate_image(prompt_text, style, model="hf"):
 
 
 
 
 
 
 
 
 
 
 
 
 
286
  try:
287
  if model == "pollinations_turbo":
 
288
  prompt_encoded = urllib.parse.quote(prompt_text)
289
  api_url = f"https://image.pollinations.ai/prompt/{prompt_encoded}?model=turbo"
290
  response = requests.get(api_url)
291
  if response.status_code != 200:
292
  logger.error(f"Pollinations API error: {response.status_code}, {response.text}")
 
293
  return None, None
294
  image_bytes = response.content
295
 
296
  elif model == "gemini":
 
297
  try:
 
 
298
  g_api_key = os.getenv("GEMINI")
299
  if not g_api_key:
300
+ st.error("Google Gemini API key is missing.")
 
301
  return None, None
 
 
302
  client = genai.Client(api_key=g_api_key)
 
 
303
  enhanced_prompt = f"image of {prompt_text} in {style} style, high quality, detailed illustration"
 
 
304
  response = client.models.generate_content(
305
+ model="models/gemini-2.0-flash-exp", # MODEL REVERTED
306
  contents=enhanced_prompt,
307
  config=types.GenerateContentConfig(response_modalities=['Text', 'Image'])
308
  )
 
 
309
  for part in response.candidates[0].content.parts:
310
  if part.inline_data is not None:
311
  image = Image.open(BytesIO(part.inline_data.data))
 
 
312
  buffered = io.BytesIO()
313
  image.save(buffered, format="JPEG")
314
  img_str = base64.b64encode(buffered.getvalue()).decode()
 
315
  return image, img_str
 
 
316
  logger.error("No image was found in the Gemini API response")
 
317
  return None, None
 
 
 
 
 
 
318
  except Exception as e:
319
  logger.error(f"Gemini API error: {str(e)}")
 
320
  return None, None
321
 
322
+ else:
 
323
  enhanced_prompt = f"{prompt_text} in {style} style, high quality, detailed illustration"
324
  model_id = "black-forest-labs/FLUX.1-dev"
325
  api_url = f"https://api-inference.huggingface.co/models/{model_id}"
 
327
  response = requests.post(api_url, headers=headers, json=payload)
328
  if response.status_code != 200:
329
  logger.error(f"Hugging Face API error: {response.status_code}, {response.text}")
 
330
  return None, None
331
  image_bytes = response.content
332
 
 
333
  if model != "gemini":
334
  image = Image.open(io.BytesIO(image_bytes))
335
  buffered = io.BytesIO()
 
338
  return image, img_str
339
 
340
  except Exception as e:
 
341
  logger.error(f"Image generation error: {str(e)}")
342
 
 
343
  return Image.new('RGB', (1024, 1024), color=(200,200,200)), None
344
 
345
  def generate_image_with_retry(prompt_text, style, model="hf", max_retries=3):
 
 
 
 
 
 
 
 
 
 
 
 
346
  for attempt in range(max_retries):
347
  try:
348
  if attempt > 0:
 
368
  st.error("Failed to create video file.")
369
  return None
370
 
 
371
  font_size = 45
372
  font = ImageFont.truetype(font_path, font_size)
373
 
 
374
  logo = None
375
  if logo_path:
376
  logo = cv2.imread(logo_path)
377
  if logo is not None:
378
+ logo = cv2.resize(logo, (width, height))
379
  else:
380
+ st.warning(f"Failed to load logo from {logo_path}.")
381
 
382
  for img, duration in zip(images, durations):
383
  try:
 
386
  frame = np.array(img_resized)
387
  except Exception as e:
388
  print(f"Invalid image detected, replacing with logo: {e}")
389
+ frame = logo if logo is not None else np.zeros((height, width, 3), dtype=np.uint8)
 
 
 
 
390
 
 
391
  pil_img = Image.fromarray(frame)
392
  draw = ImageDraw.Draw(pil_img)
393
 
 
394
  text1 = "Made With"
395
+ text2 = "Sozo Business Studio" # TEXT UPDATED
396
 
 
397
  bbox = draw.textbbox((0, 0), text1, font=font)
 
398
  text1_height = bbox[3] - bbox[1]
399
 
400
+ text_position1 = (width - 270, height - 120)
401
+ text_position2 = (width - 430, height - 120 + text1_height + 5) # Position adjusted for longer text
402
 
403
+ draw.text(text_position1, text1, font=font, fill=(81, 34, 97, 255))
404
+ draw.text(text_position2, text2, font=font, fill=(81, 34, 97, 255))
405
 
 
406
  frame = np.array(pil_img)
407
  frame_cv = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
408
 
 
409
  for _ in range(int(duration * fps)):
410
  video.write(frame_cv)
411
 
 
412
  if logo is not None:
413
+ for _ in range(int(3 * fps)):
414
  video.write(logo)
415
 
416
  video.release()
 
420
  st.error(f"Error creating silent video: {e}")
421
  return None
422
 
 
423
  def combine_video_audio(video_path, audio_files, output_path=None):
424
  try:
425
  if output_path is None:
 
455
 
456
  def create_video(images, audio_files, output_path=None):
457
  try:
458
+ subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
459
+ except FileNotFoundError:
460
+ st.error("ffmpeg not installed.")
461
+ return None
462
+ if output_path is None:
463
+ output_path = f"output_video_{uuid.uuid4()}.mp4"
464
+ silent_video_path = f"silent_{uuid.uuid4()}.mp4"
465
+ durations = [get_audio_duration(af) if af else 5.0 for af in audio_files]
466
+ if len(durations) < len(images):
467
+ durations.extend([5.0]*(len(images)-len(durations)))
468
+ silent_video = create_silent_video(images, durations, silent_video_path)
469
+ if not silent_video:
 
 
 
 
 
 
 
 
 
470
  return None
471
+ final_video = combine_video_audio(silent_video, audio_files, output_path)
472
+ try:
473
+ os.unlink(silent_video_path)
474
+ except Exception:
475
+ pass
476
+ return final_video
477
 
478
  # -----------------------
479
  # Audio Generation Function
480
  # -----------------------
481
  def generate_audio(text, voice_model, audio_model="deepgram"):
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  if audio_model == "deepgram":
483
  deepgram_api_key = os.getenv("DeepGram")
484
  if not deepgram_api_key:
 
499
  st.error(f"DeepGram TTS error: {response.status_code}")
500
  return None
501
  elif audio_model == "openai-audio":
 
502
  encoded_text = urllib.parse.quote(text)
503
  url = f"https://text.pollinations.ai/{encoded_text}?model=openai-audio&voice={voice_model}"
504
  response = requests.get(url)
 
515
  return None
516
 
517
  def get_audio_duration(audio_file):
 
518
  try:
519
  cmd = ['ffprobe', '-v', 'error', '-show_entries', 'format=duration',
520
  '-of', 'default=noprint_wrappers=1:nokey=1', audio_file]
521
  result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
522
+ return float(result.stdout.strip()) if result.returncode == 0 else 5.0
 
 
523
  except Exception:
524
  return 5.0
525
 
 
527
  # Unified Process-Story Function
528
  # -----------------------
529
  def process_generated_story(style, voice_model):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
530
  pages, image_prompts = extract_image_prompts_and_story(st.session_state.full_story)
531
  st.session_state.story_pages = pages
532
  st.session_state.image_descriptions = image_prompts
533
  st.session_state.generated_images = []
534
  st.session_state.story_audio = []
535
  progress_bar = st.progress(0)
536
+ total_steps = len(pages) * 2 # 1 for image, 1 for audio
537
+ current_step = 0
538
 
 
539
  for i, (page, img_prompt) in enumerate(zip(pages, image_prompts)):
540
+ with st.spinner(f"Generating image {i+1}/{len(pages)}..."):
541
+ img = None
542
+ try:
543
+ chart_response = generateResponse("Generate this visualization: " + img_prompt, st.session_state.dataframe)
544
+ if isinstance(chart_response, dict) and chart_response.get("type") == "plot":
545
+ img_path = chart_response["value"]
546
+ if isinstance(img_path, str) and os.path.isfile(img_path) and is_valid_png(img_path) and standardize_and_validate_image(img_path):
547
+ img = Image.open(img_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  else:
 
 
549
  img, _ = generate_image_with_retry(img_prompt, style)
550
+ else:
 
 
 
 
 
 
 
 
551
  img, _ = generate_image_with_retry(img_prompt, style)
552
+ except Exception as e:
553
+ st.warning(f"Chart generation failed for section {i+1}: {e}. Using default image.")
554
  img, _ = generate_image_with_retry(img_prompt, style)
555
+
556
+ img = img if img else Image.new('RGB', (1024, 1024), color=(200, 200, 200))
557
+ st.session_state.generated_images.append(img.convert('RGB'))
558
+ current_step += 1
559
+ progress_bar.progress(current_step / total_steps)
560
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  for i, page in enumerate(pages):
562
+ with st.spinner(f"Generating audio {i+1}/{len(pages)}..."):
563
  audio = generate_audio(page, voice_model, audio_model=audio_model_param)
564
  st.session_state.story_audio.append(audio)
565
+ current_step += 1
566
+ progress_bar.progress(current_step / total_steps)
567
 
 
 
 
 
568
  if st.session_state.generated_images:
569
+ with st.spinner("Assembling video..."):
570
  audio_paths = [af for af in st.session_state.story_audio if af]
571
  if audio_paths:
572
  st.session_state.final_video_path = create_video(st.session_state.generated_images, audio_paths)
 
574
  silent_path = f"silent_video_{uuid.uuid4()}.mp4"
575
  durations = [5.0] * len(st.session_state.generated_images)
576
  st.session_state.final_video_path = create_silent_video(st.session_state.generated_images, durations, silent_path)
577
+ progress_bar.empty()
578
  # -----------------------
579
  # Display Generated Content
580
  # -----------------------
581
  def display_generated_content():
582
+ st.subheader("Generated Narrative Video")
583
+ tab1, tab2, tab3 = st.tabs(["Video Output", "Story Pages", "Full Script"])
584
 
585
  with tab1:
 
 
 
 
 
 
 
 
 
 
586
  if st.session_state.final_video_path and os.path.exists(st.session_state.final_video_path):
587
  with open(st.session_state.final_video_path, "rb") as f:
588
  video_bytes = f.read()
589
  st.video(video_bytes)
590
+ st.download_button("Download Video", data=video_bytes, file_name="sozo_business_narrative.mp4", mime="video/mp4")
591
+ share_message = "Check out this AI-generated business narrative video!"
592
  whatsapp_link = f"https://api.whatsapp.com/send?text={urllib.parse.quote(share_message)}"
593
+ st.markdown(f"[Share on WhatsApp]({whatsapp_link})", unsafe_allow_html=True)
594
  else:
595
  st.error("Video file not found or not readable.")
596
 
597
+ with tab2:
598
+ for i, (page, img) in enumerate(zip(st.session_state.story_pages, st.session_state.generated_images)):
599
+ st.image(img, caption=f"Scene {i+1}")
600
+ st.markdown(f"**Narration {i+1}**: {page}")
601
+ if i < len(st.session_state.story_audio) and st.session_state.story_audio[i]:
602
+ st.audio(st.session_state.story_audio[i])
603
+
604
+ with tab3:
605
+ st.text_area("Complete Narrative Script", st.session_state.full_story, height=400)
606
+
607
+
608
  # -----------------------
609
  # Streamlit App Configuration and Sidebar
610
  # -----------------------
611
+ st.set_page_config(page_title="Sozo Business Studio", page_icon="💼", layout="wide", initial_sidebar_state="expanded")
612
+
613
  for key in ["story_pages", "image_descriptions", "generated_images", "story_audio", "full_story", "final_video_path", "dataframe"]:
614
  if key not in st.session_state:
615
+ st.session_state[key] = [] if key.startswith("story") or key.startswith("generated") else None
616
 
617
  with st.sidebar:
618
  st.sidebar.image("sozo_logo1.jpeg", use_container_width=True)
 
619
  story_types = {
620
+ "business": "Business Narrative",
621
+ "education": "Educational",
622
+ "entertainment": "Entertaining",
623
  "free_form": "Free Form (AI's choice)",
624
  "children": "Children's Story",
625
+ }
 
 
 
626
  selected_story_type = st.selectbox(
627
+ "Narrative Style",
628
  options=list(story_types.keys()),
629
  format_func=lambda x: story_types[x],
 
630
  key="story_type_select"
631
  )
632
 
 
633
  model_options = ["HuggingFace Flux", "Pollinations Turbo", "Google Gemini"]
634
+ selected_model_name = st.selectbox("Select Image Generation Model", model_options, index=0, key="image_model_select")
 
 
 
 
 
635
 
636
+ style_options = ["photorealistic", "cinematic", "cartoon", "concept art", "oil painting", "fantasy illustration", "whimsical"]
637
+ selected_style = st.selectbox("Image Style", style_options, key="style_select")
 
 
 
 
 
 
638
 
639
+ model_param = {"HuggingFace Flux": "hf", "Pollinations Turbo": "pollinations_turbo", "Google Gemini": "gemini"}[selected_model_name]
 
 
 
 
 
 
640
 
 
641
  audio_model_options = ["DeepGram", "Pollinations OpenAI-Audio"]
642
+ selected_audio_model = st.selectbox("Select Audio Generation Model", audio_model_options, key="audio_model_select")
 
 
 
 
 
643
 
644
  if selected_audio_model == "DeepGram":
645
+ voice_options = {"aura-asteria-en": "Female", "aura-helios-en": "Male"}
646
+ selected_voice = st.selectbox("Voice Model", options=list(voice_options.keys()), format_func=voice_options.get, key="voice_select_deepgram")
 
 
 
 
 
 
 
 
 
647
  audio_model_param = "deepgram"
648
  else:
649
+ voice_options = {"sage": "Female", "echo": "Male"}
650
+ selected_voice = st.selectbox("Voice Model", options=list(voice_options.keys()), format_func=voice_options.get, key="voice_select_pollinations")
 
 
 
 
 
 
 
 
 
651
  audio_model_param = "openai-audio"
652
 
653
  st.markdown("### Tips for Best Results")
654
+ st.markdown("- Ensure your data has clear column headers.\n- Use the 'Business Narrative' style for professional reports.\n- Try different image styles and voices to match your brand.")
 
 
 
 
 
655
  if st.button("Check System Requirements"):
656
  try:
657
  result = subprocess.run(['ffmpeg', '-version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
658
+ st.success("✅ ffmpeg is installed.")
659
  except FileNotFoundError:
660
+ st.error("❌ ffmpeg not found. It must be installed to create videos.")
661
+
662
+ # --- MAIN PAGE ---
663
+ st.subheader("Sozo Business Studio")
664
+ st.markdown("#### Turn business data into compelling narratives.")
665
+ st.markdown("---")
666
+
667
+ st.markdown("### 1. Upload Your Business Data")
668
+ uploaded_file = st.file_uploader(
669
+ "Upload a CSV or Excel file to begin.",
670
+ type=['csv', 'xlsx', 'xls'],
671
+ label_visibility="collapsed"
672
+ )
673
 
674
+ if uploaded_file:
675
+ try:
676
+ df = pd.read_excel(uploaded_file) if uploaded_file.name.endswith(('xlsx', 'xls')) else pd.read_csv(uploaded_file)
677
+ st.session_state.dataframe = df
678
+ st.success(f"✅ Loaded `{uploaded_file.name}`. Data preview:")
679
+ st.dataframe(df.head())
680
+ except Exception as e:
681
+ st.error(f"Error processing {uploaded_file.name}: {e}")
682
+ st.session_state.dataframe = None
683
 
684
+ st.markdown("### 2. Generate Your Video")
685
+ if st.button("Generate Video Narrative", disabled=st.session_state.dataframe is None):
686
+ with st.spinner("Analyzing data and generating narrative script..."):
687
+ st.session_state.full_story = generate_story_from_dataframe(st.session_state.dataframe, selected_story_type)
688
+
689
+ if st.session_state.full_story:
690
+ st.success("Script generated! Now creating video assets...")
691
+ process_generated_story(selected_style, selected_voice)
692
+ else:
693
+ st.error("Failed to generate narrative script. The data might be formatted incorrectly or the AI model could be temporarily unavailable.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
694
 
695
  if st.session_state.story_pages:
696
+ st.markdown("---")
697
  display_generated_content()