rairo commited on
Commit
0d73b2a
Β·
verified Β·
1 Parent(s): af74a16

Create Demo1.py

Browse files
Files changed (1) hide show
  1. Demo1.py +364 -0
Demo1.py ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ###############################################################################
2
+ # Sozo Business Studio Β· AI transforms business data into compelling narratives
3
+ ###############################################################################
4
+ import os, re, json, hashlib, uuid, base64, io, tempfile, wave, requests, subprocess
5
+ from pathlib import Path
6
+
7
+ import streamlit as st
8
+ import pandas as pd
9
+ import numpy as np
10
+ import matplotlib
11
+ matplotlib.use("Agg")
12
+ import matplotlib.pyplot as plt
13
+ from fpdf import FPDF, HTMLMixin
14
+ from markdown_it import MarkdownIt
15
+ from PIL import Image
16
+
17
+ from langchain_experimental.agents import create_pandas_dataframe_agent
18
+ from langchain_google_genai import ChatGoogleGenerativeAI
19
+ from google import genai
20
+ import cv2 # Added for video processing
21
+
22
+ # ─────────────────────────────────────────────────────────────────────────────
23
+ # CONFIG & CONSTANTS
24
+ # ─────────────────────────────────────────────────────────────────────────────
25
+ st.set_page_config(page_title="Sozo Business Studio", layout="wide")
26
+ st.title("πŸ“Š Sozo Business Studio")
27
+ st.caption("AI transforms business data into compelling narratives.")
28
+
29
+ # --- Feature Caps ---
30
+ MAX_CHARTS = 5
31
+ VIDEO_SCENES = 5 # Number of scenes for the video
32
+
33
+ # --- API Keys & Clients (Correct Initialization) ---
34
+ API_KEY = os.getenv("GEMINI_API_KEY")
35
+ if not API_KEY:
36
+ st.error("⚠️ GEMINI_API_KEY is not set."); st.stop()
37
+ # Use the Client pattern from the original script
38
+ GEM = genai.Client(api_key=API_KEY)
39
+
40
+ DG_KEY = os.getenv("DEEPGRAM_API_KEY") # Optional but needed for narration
41
+
42
+ # --- Session State ---
43
+ # Simplified state to hold the most recent generated output
44
+ st.session_state.setdefault("bundle", None)
45
+
46
+ # ─────────────────────────────────────────────────────────────────────────────
47
+ # HELPERS
48
+ # ─────────────────────────────────────────────────────────────────────────────
49
+ sha1_bytes = lambda b: hashlib.sha1(b).hexdigest()
50
+
51
+ def validate_file_upload(f):
52
+ errs=[]
53
+ if f is None: errs.append("No file uploaded")
54
+ elif f.size==0: errs.append("File is empty")
55
+ elif f.size>50*1024*1024: errs.append("File >50 MB")
56
+ if f and Path(f.name).suffix.lower() not in (".csv",".xlsx",".xls"):
57
+ errs.append("Unsupported file type")
58
+ return errs
59
+
60
+ def load_dataframe_safely(buf:bytes, name:str):
61
+ try:
62
+ ext = Path(name).suffix.lower()
63
+ df = pd.read_excel(io.BytesIO(buf)) if ext in (".xlsx", ".xls") else pd.read_csv(io.BytesIO(buf))
64
+ if df.empty or len(df.columns)==0: raise ValueError("File contains no data")
65
+ df.columns=df.columns.astype(str).str.strip()
66
+ df=df.dropna(how="all")
67
+ if df.empty: raise ValueError("Rows all empty")
68
+ return df,None
69
+ except Exception as e: return None,str(e)
70
+
71
+ def fix_bullet(t:str)->str:
72
+ return re.sub(r"[\x80-\x9f]", "", t) if isinstance(t, str) else ""
73
+
74
+ # β€”β€”β€” Arrow helpers β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
75
+ def arrow_df(df:pd.DataFrame)->pd.DataFrame:
76
+ safe=df.copy()
77
+ for c in safe.columns:
78
+ if safe[c].dtype.name in ("Int64","Float64","Boolean"):
79
+ safe[c]=safe[c].astype(safe[c].dtype.name.lower())
80
+ return safe
81
+
82
+ # β€”β€”β€” Text-to-Speech (Used by Both Features) β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
83
+ @st.cache_data(show_spinner=False)
84
+ def deepgram_tts(text:str):
85
+ if not DG_KEY or not text: return None, None
86
+ text = re.sub(r"[^\w\s.,!?;:-]", "", text)[:1000]
87
+ try:
88
+ r = requests.post("https://api.deepgram.com/v1/speak",
89
+ params={"model":"aura-asteria-en"},
90
+ headers={"Authorization":f"Token {DG_KEY}", "Content-Type":"application/json"},
91
+ json={"text":text}, timeout=30)
92
+ r.raise_for_status()
93
+ return r.content, r.headers.get("Content-Type", "audio/mpeg")
94
+ except Exception:
95
+ return None, None
96
+
97
+ def pcm_to_wav(pcm,sr=24000,ch=1,w=2):
98
+ buf=io.BytesIO()
99
+ with wave.open(buf,'wb') as wf:
100
+ wf.setnchannels(ch); wf.setsampwidth(w); wf.setframerate(sr); wf.writeframes(pcm)
101
+ buf.seek(0); return buf.getvalue()
102
+
103
+ # β€”β€”β€” Chart & Tag Helpers β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”
104
+ TAG_RE = re.compile(r'[<\[]\s*generate_?chart\s*[:=]?\s*["\']?(?P<d>[^>\]\'"”’]+?)["\']?\s*[>\]]', re.I)
105
+ extract_chart_tags = lambda t: list(dict.fromkeys(m.group("d").strip() for m in TAG_RE.finditer(t or "")))
106
+ def repl_tags(txt:str,mp:dict,str_fn):
107
+ return TAG_RE.sub(lambda m: str_fn(mp[m.group("d").strip()]) if m.group("d").strip() in mp else m.group(0), txt)
108
+
109
+ # ─────────────────────────────────────────────────────────────────────────────
110
+ # FEATURE 1: REPORT GENERATION
111
+ # ─────────────────────────────────────────────────────────────────────────────
112
+ class PDF(FPDF,HTMLMixin): pass
113
+
114
+ def build_pdf(md, charts):
115
+ md = fix_bullet(md).replace("β€’", "*")
116
+ md = repl_tags(md, charts, lambda p: f'<img src="{p}">')
117
+ html = MarkdownIt("commonmark", {"breaks":True}).enable("table").render(md)
118
+ pdf = PDF(); pdf.set_auto_page_break(True, margin=15)
119
+ pdf.add_page()
120
+ pdf.set_font("Arial", "B", 18)
121
+ pdf.cell(0, 12, "AI-Generated Business Report", ln=True); pdf.ln(3)
122
+ pdf.set_font("Arial", "", 11)
123
+ pdf.write_html(html)
124
+ return bytes(pdf.output(dest="S"))
125
+
126
+ def generate_report_assets(key, buf, name, ctx):
127
+ df, err = load_dataframe_safely(buf, name)
128
+ if err: st.error(err); return None
129
+ llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", google_api_key=API_KEY, temperature=0.1)
130
+ ctx_dict = {"shape": df.shape, "columns": list(df.columns), "user_ctx": ctx or "General business analysis"}
131
+
132
+ report_md = llm.invoke(f"""You are a senior business analyst. Write an executive-level Markdown report
133
+ with insights & recommendations. Use chart tags like <generate_chart: "description"> where helpful.
134
+ Data Context: {json.dumps(ctx_dict, indent=2)}""").content
135
+
136
+ chart_descs = extract_chart_tags(report_md)[:MAX_CHARTS]
137
+ chart_paths = {}
138
+ if chart_descs:
139
+ ag = create_pandas_dataframe_agent(llm=llm, df=df, verbose=False, allow_dangerous_code=True)
140
+ for d in chart_descs:
141
+ with st.spinner(f"Generating chart: {d}"):
142
+ with plt.ioff():
143
+ try:
144
+ ag.run(f"Create a {d} with Matplotlib and save.")
145
+ fig = plt.gcf()
146
+ if fig.axes:
147
+ p = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
148
+ fig.savefig(p, dpi=300, bbox_inches="tight", facecolor="white")
149
+ chart_paths[d] = str(p)
150
+ plt.close("all")
151
+ except: plt.close("all")
152
+
153
+ md = fix_bullet(report_md)
154
+ pdf = build_pdf(md, chart_paths)
155
+ preview = repl_tags(md, chart_paths, lambda p: f'<img src="data:image/png;base64,{base64.b64encode(Path(p).read_bytes()).decode()}" style="max-width:100%;">')
156
+
157
+ return {"type": "report", "preview": preview, "pdf": pdf, "report_md": md, "key": key}
158
+
159
+ # ─────────────────────────────────────────────────────────────────────────────
160
+ # FEATURE 2: VIDEO GENERATION
161
+ # ─────────────────────────────────────────────────────────────────────────────
162
+ def generate_image_from_prompt(prompt, style):
163
+ """Generates an illustrative image using the Gemini Client."""
164
+ try:
165
+ full_prompt = f"A professional, clean, illustrative image for a business presentation: {prompt}, in the style of {style}."
166
+ # Use the globally defined GEM client, as per the original script's pattern
167
+ response = GEM.generate_content(
168
+ contents=full_prompt,
169
+ model="models/gemini-1.5-flash-latest",
170
+ generation_config={"response_mime_type": "image/png"}
171
+ )
172
+ img_bytes = response.parts[0].blob.data
173
+ return Image.open(io.BytesIO(img_bytes)).convert("RGB")
174
+ except Exception as e:
175
+ st.warning(f"Illustrative image generation failed: {e}. Using placeholder.")
176
+ return Image.new('RGB', (1024, 768), color = (230, 230, 230))
177
+
178
+ def create_silent_video(images, durations, output_path):
179
+ width, height = 1280, 720
180
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
181
+ video = cv2.VideoWriter(output_path, fourcc, 24, (width, height))
182
+
183
+ for img, duration in zip(images, durations):
184
+ # Resize image and convert to BGR for OpenCV
185
+ frame = np.array(img.resize((width, height)))
186
+ frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
187
+ for _ in range(int(duration * 24)): # 24 fps
188
+ video.write(frame_bgr)
189
+ video.release()
190
+ return output_path
191
+
192
+ def combine_video_audio(video_path, audio_paths, output_path):
193
+ concat_list_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.txt"
194
+ with open(concat_list_path, 'w') as f:
195
+ for af in audio_paths:
196
+ f.write(f"file '{Path(af).resolve()}'\n")
197
+
198
+ concat_audio_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp3"
199
+ subprocess.run(['ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', str(concat_list_path), '-c', 'copy', str(concat_audio_path)], check=True, capture_output=True)
200
+
201
+ subprocess.run(['ffmpeg', '-y', '-i', video_path, '-i', str(concat_audio_path), '-c:v', 'copy', '-c:a', 'aac', '-shortest', output_path], check=True, capture_output=True)
202
+
203
+ concat_list_path.unlink(missing_ok=True)
204
+ concat_audio_path.unlink(missing_ok=True)
205
+ return output_path
206
+
207
+ def get_audio_duration(audio_file):
208
+ try:
209
+ result = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', audio_file],
210
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True)
211
+ return float(result.stdout.strip())
212
+ except Exception:
213
+ return 5.0 # Default duration
214
+
215
+ def generate_video_assets(key, buf, name, ctx, style):
216
+ try:
217
+ subprocess.run(['ffmpeg', '-version'], check=True, capture_output=True)
218
+ except (FileNotFoundError, subprocess.CalledProcessError):
219
+ st.error("πŸ”΄ FFmpeg is not installed or not in your system's PATH. Video generation is not possible.")
220
+ return None
221
+
222
+ df, err = load_dataframe_safely(buf, name)
223
+ if err: st.error(err); return None
224
+ llm = ChatGoogleGenerativeAI(model="gemini-1.5-flash", google_api_key=API_KEY, temperature=0.2)
225
+ ctx_dict = {"shape": df.shape, "columns": list(df.columns), "user_ctx": ctx or "General business analysis"}
226
+
227
+ story_prompt = f"""Create a script for a short business video with exactly {VIDEO_SCENES} scenes.
228
+ For each scene:
229
+ 1. Write a concise narration (1-2 sentences).
230
+ 2. If the data can be visualized for this scene, add a chart tag like <generate_chart: "bar chart of sales by region">.
231
+ 3. Separate each scene with the marker `[SCENE_BREAK]`.
232
+ Data Context: {json.dumps(ctx_dict, indent=2)}"""
233
+
234
+ with st.spinner("Generating video script..."):
235
+ full_script = llm.invoke(story_prompt).content
236
+ scenes = [s.strip() for s in full_script.split("[SCENE_BREAK]")]
237
+
238
+ visuals, audio_paths, temp_files = [], [], []
239
+ try:
240
+ ag = create_pandas_dataframe_agent(llm=llm, df=df, verbose=False, allow_dangerous_code=True)
241
+ for i, scene_text in enumerate(scenes[:VIDEO_SCENES]):
242
+ progress = (i + 1) / VIDEO_SCENES
243
+ st.progress(progress, text=f"Processing Scene {i+1}/{VIDEO_SCENES}...")
244
+
245
+ chart_descs = extract_chart_tags(scene_text)
246
+ narrative = repl_tags(scene_text, {}, lambda _: "").strip()
247
+
248
+ if narrative: # Only process scenes with text
249
+ # 1. Generate Visual
250
+ if chart_descs:
251
+ with plt.ioff():
252
+ try:
253
+ ag.run(f"Create a {chart_descs[0]} with Matplotlib and save.")
254
+ fig = plt.gcf()
255
+ if fig.axes:
256
+ p = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
257
+ fig.savefig(p, dpi=200, bbox_inches="tight", facecolor="white")
258
+ visuals.append(Image.open(p).convert("RGB"))
259
+ temp_files.append(p)
260
+ else: raise ValueError("No chart produced")
261
+ except Exception:
262
+ visuals.append(generate_image_from_prompt(narrative, style))
263
+ finally: plt.close("all")
264
+ else:
265
+ visuals.append(generate_image_from_prompt(narrative, style))
266
+
267
+ # 2. Generate Audio
268
+ audio_content, _ = deepgram_tts(narrative)
269
+ if audio_content:
270
+ audio_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp3"
271
+ audio_path.write_bytes(audio_content)
272
+ audio_paths.append(str(audio_path))
273
+ temp_files.append(audio_path)
274
+
275
+ if not visuals or not audio_paths:
276
+ st.error("Could not generate any scenes for the video. Please try a different context or file.")
277
+ return None
278
+
279
+ st.progress(1.0, text="Assembling video...")
280
+ durations = [get_audio_duration(ap) for ap in audio_paths]
281
+ silent_video_path = str(Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4")
282
+ final_video_path = str(Path(tempfile.gettempdir()) / f"{key}.mp4")
283
+
284
+ create_silent_video(visuals, durations, silent_video_path)
285
+ temp_files.append(Path(silent_video_path))
286
+ combine_video_audio(silent_video_path, audio_paths, final_video_path)
287
+
288
+ return {"type": "video", "video_path": final_video_path, "key": key}
289
+ finally:
290
+ for f in temp_files: f.unlink(missing_ok=True) # Cleanup all temp files
291
+
292
+ # ─────────────────────────────────────────────────────────────────────────────
293
+ # UI & MAIN WORKFLOW
294
+ # ─────────────────────────────────────────────────────────────────────────────
295
+ mode = st.radio("Select Output Format:", ["Report (PDF)", "Video Narrative"], horizontal=True)
296
+
297
+ # --- Conditional UI ---
298
+ video_style = "professional illustration"
299
+ if mode == "Video Narrative":
300
+ with st.sidebar:
301
+ st.subheader("🎬 Video Options")
302
+ video_style = st.selectbox("Visual Style",
303
+ ["professional illustration", "minimalist infographic", "photorealistic", "cinematic", "data visualization aesthetic"])
304
+ st.info("The AI will generate charts from your data where possible, and illustrative images for other scenes.")
305
+
306
+ # --- Common UI ---
307
+ upl = st.file_uploader("Upload CSV or Excel", type=["csv", "xlsx", "xls"])
308
+ if upl:
309
+ df_prev, _ = load_dataframe_safely(upl.getvalue(), upl.name)
310
+ with st.expander("πŸ“Š Data Preview"):
311
+ st.dataframe(arrow_df(df_prev.head()))
312
+
313
+ ctx = st.text_area("Business context or specific instructions (optional)")
314
+
315
+ if st.button("πŸš€ Generate", type="primary"):
316
+ if not upl:
317
+ st.warning("Please upload a file first.")
318
+ st.stop()
319
+
320
+ bkey = sha1_bytes(b"".join([upl.getvalue(), mode.encode(), ctx.encode(), video_style.encode()]))
321
+
322
+ if mode == "Report (PDF)":
323
+ with st.spinner("Generating report and charts..."):
324
+ bundle = generate_report_assets(bkey, upl.getvalue(), upl.name, ctx)
325
+ else: # Video Narrative
326
+ bundle = generate_video_assets(bkey, upl.getvalue(), upl.name, ctx, video_style)
327
+
328
+ st.session_state.bundle = bundle
329
+ st.rerun()
330
+
331
+ # --- Display Area (handles state correctly after rerun) ---
332
+ if "bundle" in st.session_state and st.session_state.bundle:
333
+ bundle = st.session_state.bundle
334
+
335
+ if bundle.get("type") == "report":
336
+ st.subheader("πŸ“„ Generated Report")
337
+ with st.expander("View Report", expanded=True):
338
+ if bundle["preview"]:
339
+ st.markdown(bundle["preview"], unsafe_allow_html=True)
340
+
341
+ c1, c2 = st.columns(2)
342
+ with c1:
343
+ st.download_button("Download PDF", bundle["pdf"], "business_report.pdf", "application/pdf", use_container_width=True)
344
+ with c2:
345
+ if DG_KEY and st.button("πŸ”Š Narrate Summary", use_container_width=True):
346
+ report_text = re.sub(r'<[^>]+>', '', bundle["report_md"]) # Basic HTML strip
347
+ audio, mime = deepgram_tts(report_text)
348
+ if audio:
349
+ st.audio(audio, format=mime)
350
+ else:
351
+ st.error("Narration failed.")
352
+ else:
353
+ st.warning("No report content was generated.")
354
+
355
+ elif bundle.get("type") == "video":
356
+ st.subheader("🎬 Generated Video Narrative")
357
+ video_path = bundle.get("video_path")
358
+ if video_path and Path(video_path).exists():
359
+ with open(video_path, "rb") as f:
360
+ st.video(f.read())
361
+ with open(video_path, "rb") as f:
362
+ st.download_button("Download Video", f, f"sozo_narrative_{bundle['key'][:8]}.mp4", "video/mp4")
363
+ else:
364
+ st.error("Video file could not be found or generation failed.")