Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -251,83 +251,111 @@ def generate_image_from_prompt(prompt, style):
|
|
| 251 |
st.warning(f"Illustrative image generation failed: {e}. Using placeholder.")
|
| 252 |
return Image.new("RGB", (WIDTH, HEIGHT), color=(230, 230, 230))
|
| 253 |
|
|
|
|
| 254 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 255 |
-
# REPORT GENERATION (
|
| 256 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 257 |
-
|
| 258 |
-
report_prompt = (
|
| 259 |
-
"You are a senior business analyst. Write an executive-level Markdown report "
|
| 260 |
-
"with insights & recommendations.\n"
|
| 261 |
-
"When you need a visual, insert a tag like\n"
|
| 262 |
-
'<generate_chart: "pie | sales by region">\n'
|
| 263 |
-
"(chart_type **first**, pipe, then short description). "
|
| 264 |
-
"Valid chart_type values: line, bar, scatter, pie, hist.\n"
|
| 265 |
-
f"Data Context: {json.dumps(ctx_dict, indent=2)}"
|
| 266 |
-
)
|
| 267 |
-
|
| 268 |
def generate_report_assets(key, buf, name, ctx):
|
| 269 |
df, err = load_dataframe_safely(buf, name)
|
| 270 |
-
if err:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
-
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash",
|
| 273 |
-
google_api_key=API_KEY, temperature=0.1)
|
| 274 |
-
ctx_dict = {"shape": df.shape, "columns": list(df.columns),
|
| 275 |
-
"user_ctx": ctx or "General business analysis"}
|
| 276 |
md = llm.invoke(report_prompt).content
|
| 277 |
|
| 278 |
-
#
|
| 279 |
chart_descs = extract_chart_tags(md)[:MAX_CHARTS]
|
| 280 |
charts = {}
|
| 281 |
if chart_descs:
|
| 282 |
-
|
|
|
|
|
|
|
| 283 |
for d in chart_descs:
|
| 284 |
with st.spinner(f"Generating chart: {d}"):
|
| 285 |
with plt.ioff():
|
| 286 |
try:
|
| 287 |
-
|
|
|
|
| 288 |
if fig.axes:
|
| 289 |
p = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
|
| 290 |
fig.savefig(p, dpi=300, bbox_inches="tight", facecolor="white")
|
| 291 |
charts[d] = str(p)
|
| 292 |
plt.close("all")
|
| 293 |
-
except
|
|
|
|
| 294 |
|
| 295 |
preview = repl_tags(
|
| 296 |
-
md,
|
| 297 |
-
|
| 298 |
-
|
|
|
|
| 299 |
)
|
| 300 |
pdf = build_pdf(md, charts)
|
| 301 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
|
| 303 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 304 |
-
# VIDEO GENERATION (
|
| 305 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 306 |
-
# ββ Video script prompt βββββββββββββββββββββββββββββββββββ
|
| 307 |
-
story_prompt = (
|
| 308 |
-
f"Create a script for a short business video with exactly {VIDEO_SCENES} scenes.\n"
|
| 309 |
-
"For each scene:\n"
|
| 310 |
-
"1. Provide 1β2 sentences of narration.\n"
|
| 311 |
-
'2. If a visual is useful, add <generate_chart: "bar | monthly revenue"> '
|
| 312 |
-
"(chart_type first).\n"
|
| 313 |
-
"3. Separate scenes with [SCENE_BREAK].\n"
|
| 314 |
-
f"Data Context: {json.dumps(ctx_dict, indent=2)}"
|
| 315 |
-
)
|
| 316 |
-
|
| 317 |
def generate_video_assets(key, buf, name, ctx, style, animate_charts=True):
|
| 318 |
-
# FFmpeg presence
|
| 319 |
try:
|
| 320 |
subprocess.run(["ffmpeg", "-version"], check=True, capture_output=True)
|
| 321 |
except Exception:
|
| 322 |
-
st.error("π΄ FFmpeg not available β cannot render video.")
|
|
|
|
| 323 |
|
| 324 |
df, err = load_dataframe_safely(buf, name)
|
| 325 |
-
if err:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
|
| 327 |
-
llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash",
|
| 328 |
-
google_api_key=API_KEY, temperature=0.2)
|
| 329 |
-
ctx_dict = {"shape": df.shape, "columns": list(df.columns),
|
| 330 |
-
"user_ctx": ctx or "General business analysis"}
|
| 331 |
script = llm.invoke(story_prompt).content
|
| 332 |
scenes = [s.strip() for s in script.split("[SCENE_BREAK]") if s.strip()]
|
| 333 |
|
|
@@ -335,45 +363,66 @@ def generate_video_assets(key, buf, name, ctx, style, animate_charts=True):
|
|
| 335 |
|
| 336 |
for idx, scene in enumerate(scenes[:VIDEO_SCENES]):
|
| 337 |
st.progress((idx + 1) / VIDEO_SCENES, text=f"Processing Scene {idx+1}/{VIDEO_SCENES}β¦")
|
|
|
|
| 338 |
chart_tags = extract_chart_tags(scene)
|
| 339 |
-
narrative
|
| 340 |
|
| 341 |
-
#
|
| 342 |
audio_bytes, _ = deepgram_tts(narrative)
|
| 343 |
audio_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp3"
|
| 344 |
-
|
|
|
|
| 345 |
duration = get_audio_duration(str(audio_path)) if audio_bytes else 5.0
|
| 346 |
-
audio_parts.append(str(audio_path))
|
|
|
|
| 347 |
|
| 348 |
-
#
|
| 349 |
if chart_tags and animate_charts:
|
| 350 |
clip_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
|
| 351 |
animate_chart(chart_tags[0], df, duration, clip_path, FPS)
|
| 352 |
-
video_parts.append(str(clip_path))
|
|
|
|
| 353 |
else:
|
| 354 |
-
# illustrative image fade
|
| 355 |
img = generate_image_from_prompt(narrative, style)
|
| 356 |
png_tmp = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
|
| 357 |
-
img.save(png_tmp)
|
|
|
|
| 358 |
clip_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
|
| 359 |
animate_image_fade(
|
| 360 |
cv2.cvtColor(np.array(img.resize((WIDTH, HEIGHT))), cv2.COLOR_RGB2BGR),
|
| 361 |
-
duration,
|
|
|
|
|
|
|
| 362 |
)
|
| 363 |
-
video_parts.append(str(clip_path))
|
|
|
|
| 364 |
|
| 365 |
-
#
|
| 366 |
silent_vid = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
|
| 367 |
concat_media(video_parts, silent_vid, "video")
|
| 368 |
-
audio_mix
|
| 369 |
concat_media(audio_parts, audio_mix, "audio")
|
| 370 |
|
| 371 |
-
final_vid
|
| 372 |
subprocess.run(
|
| 373 |
-
[
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
)
|
|
|
|
| 377 |
return {"type": "video", "video_path": str(final_vid), "key": key}
|
| 378 |
|
| 379 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 251 |
st.warning(f"Illustrative image generation failed: {e}. Using placeholder.")
|
| 252 |
return Image.new("RGB", (WIDTH, HEIGHT), color=(230, 230, 230))
|
| 253 |
|
| 254 |
+
|
| 255 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 256 |
+
# REPORT GENERATION (unchanged models β prompt now local)
|
| 257 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 258 |
def generate_report_assets(key, buf, name, ctx):
|
| 259 |
df, err = load_dataframe_safely(buf, name)
|
| 260 |
+
if err:
|
| 261 |
+
st.error(err)
|
| 262 |
+
return None
|
| 263 |
+
|
| 264 |
+
llm = ChatGoogleGenerativeAI(
|
| 265 |
+
model="gemini-2.0-flash", google_api_key=API_KEY, temperature=0.1
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
# build context dict **after** df exists
|
| 269 |
+
ctx_dict = {
|
| 270 |
+
"shape": df.shape,
|
| 271 |
+
"columns": list(df.columns),
|
| 272 |
+
"user_ctx": ctx or "General business analysis",
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
report_prompt = (
|
| 276 |
+
"You are a senior business analyst. Write an executive-level Markdown report "
|
| 277 |
+
"with insights & recommendations.\n"
|
| 278 |
+
'When you need a visual, insert a tag like <generate_chart: "pie | sales by region"> '
|
| 279 |
+
"(chart_type first, then a description). "
|
| 280 |
+
"Valid chart_type values: line, bar, scatter, pie, hist.\n"
|
| 281 |
+
f"Data Context: {json.dumps(ctx_dict, indent=2)}"
|
| 282 |
+
)
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
md = llm.invoke(report_prompt).content
|
| 285 |
|
| 286 |
+
# ------------------------------------------------------------------ charts
|
| 287 |
chart_descs = extract_chart_tags(md)[:MAX_CHARTS]
|
| 288 |
charts = {}
|
| 289 |
if chart_descs:
|
| 290 |
+
agent = create_pandas_dataframe_agent(
|
| 291 |
+
llm=llm, df=df, verbose=False, allow_dangerous_code=True
|
| 292 |
+
)
|
| 293 |
for d in chart_descs:
|
| 294 |
with st.spinner(f"Generating chart: {d}"):
|
| 295 |
with plt.ioff():
|
| 296 |
try:
|
| 297 |
+
agent.run(f"Create a {d} with Matplotlib and save.")
|
| 298 |
+
fig = plt.gcf()
|
| 299 |
if fig.axes:
|
| 300 |
p = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
|
| 301 |
fig.savefig(p, dpi=300, bbox_inches="tight", facecolor="white")
|
| 302 |
charts[d] = str(p)
|
| 303 |
plt.close("all")
|
| 304 |
+
except Exception:
|
| 305 |
+
plt.close("all")
|
| 306 |
|
| 307 |
preview = repl_tags(
|
| 308 |
+
md,
|
| 309 |
+
charts,
|
| 310 |
+
lambda p: f'<img src="data:image/png;base64,'
|
| 311 |
+
f'{base64.b64encode(Path(p).read_bytes()).decode()}">'
|
| 312 |
)
|
| 313 |
pdf = build_pdf(md, charts)
|
| 314 |
+
|
| 315 |
+
return {
|
| 316 |
+
"type": "report",
|
| 317 |
+
"preview": preview,
|
| 318 |
+
"pdf": pdf,
|
| 319 |
+
"report_md": md,
|
| 320 |
+
"key": key,
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
|
| 324 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 325 |
+
# VIDEO GENERATION (animated charts β prompt now local)
|
| 326 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
def generate_video_assets(key, buf, name, ctx, style, animate_charts=True):
|
|
|
|
| 328 |
try:
|
| 329 |
subprocess.run(["ffmpeg", "-version"], check=True, capture_output=True)
|
| 330 |
except Exception:
|
| 331 |
+
st.error("π΄ FFmpeg not available β cannot render video.")
|
| 332 |
+
return None
|
| 333 |
|
| 334 |
df, err = load_dataframe_safely(buf, name)
|
| 335 |
+
if err:
|
| 336 |
+
st.error(err)
|
| 337 |
+
return None
|
| 338 |
+
|
| 339 |
+
llm = ChatGoogleGenerativeAI(
|
| 340 |
+
model="gemini-2.0-flash", google_api_key=API_KEY, temperature=0.2
|
| 341 |
+
)
|
| 342 |
+
|
| 343 |
+
ctx_dict = {
|
| 344 |
+
"shape": df.shape,
|
| 345 |
+
"columns": list(df.columns),
|
| 346 |
+
"user_ctx": ctx or "General business analysis",
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
story_prompt = (
|
| 350 |
+
f"Create a script for a short business video with exactly {VIDEO_SCENES} scenes.\n"
|
| 351 |
+
"For each scene:\n"
|
| 352 |
+
"1. Provide 1β2 sentences of narration.\n"
|
| 353 |
+
'2. If a visual is helpful, add <generate_chart: "bar | monthly revenue"> '
|
| 354 |
+
"(chart_type first).\n"
|
| 355 |
+
"3. Separate scenes with [SCENE_BREAK].\n"
|
| 356 |
+
f"Data Context: {json.dumps(ctx_dict, indent=2)}"
|
| 357 |
+
)
|
| 358 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
script = llm.invoke(story_prompt).content
|
| 360 |
scenes = [s.strip() for s in script.split("[SCENE_BREAK]") if s.strip()]
|
| 361 |
|
|
|
|
| 363 |
|
| 364 |
for idx, scene in enumerate(scenes[:VIDEO_SCENES]):
|
| 365 |
st.progress((idx + 1) / VIDEO_SCENES, text=f"Processing Scene {idx+1}/{VIDEO_SCENES}β¦")
|
| 366 |
+
|
| 367 |
chart_tags = extract_chart_tags(scene)
|
| 368 |
+
narrative = repl_tags(scene, {}, lambda _: "").strip()
|
| 369 |
|
| 370 |
+
# ---------------- audio -------------------------------------------
|
| 371 |
audio_bytes, _ = deepgram_tts(narrative)
|
| 372 |
audio_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp3"
|
| 373 |
+
if audio_bytes:
|
| 374 |
+
audio_path.write_bytes(audio_bytes)
|
| 375 |
duration = get_audio_duration(str(audio_path)) if audio_bytes else 5.0
|
| 376 |
+
audio_parts.append(str(audio_path))
|
| 377 |
+
temps.append(audio_path)
|
| 378 |
|
| 379 |
+
# ---------------- visual ------------------------------------------
|
| 380 |
if chart_tags and animate_charts:
|
| 381 |
clip_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
|
| 382 |
animate_chart(chart_tags[0], df, duration, clip_path, FPS)
|
| 383 |
+
video_parts.append(str(clip_path))
|
| 384 |
+
temps.append(clip_path)
|
| 385 |
else:
|
|
|
|
| 386 |
img = generate_image_from_prompt(narrative, style)
|
| 387 |
png_tmp = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.png"
|
| 388 |
+
img.save(png_tmp)
|
| 389 |
+
temps.append(png_tmp)
|
| 390 |
clip_path = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
|
| 391 |
animate_image_fade(
|
| 392 |
cv2.cvtColor(np.array(img.resize((WIDTH, HEIGHT))), cv2.COLOR_RGB2BGR),
|
| 393 |
+
duration,
|
| 394 |
+
clip_path,
|
| 395 |
+
FPS,
|
| 396 |
)
|
| 397 |
+
video_parts.append(str(clip_path))
|
| 398 |
+
temps.append(clip_path)
|
| 399 |
|
| 400 |
+
# -------------- concatenate ----------------------------------------------
|
| 401 |
silent_vid = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
|
| 402 |
concat_media(video_parts, silent_vid, "video")
|
| 403 |
+
audio_mix = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp3"
|
| 404 |
concat_media(audio_parts, audio_mix, "audio")
|
| 405 |
|
| 406 |
+
final_vid = Path(tempfile.gettempdir()) / f"{key}.mp4"
|
| 407 |
subprocess.run(
|
| 408 |
+
[
|
| 409 |
+
"ffmpeg",
|
| 410 |
+
"-y",
|
| 411 |
+
"-i",
|
| 412 |
+
str(silent_vid),
|
| 413 |
+
"-i",
|
| 414 |
+
str(audio_mix),
|
| 415 |
+
"-c:v",
|
| 416 |
+
"copy",
|
| 417 |
+
"-c:a",
|
| 418 |
+
"aac",
|
| 419 |
+
"-shortest",
|
| 420 |
+
str(final_vid),
|
| 421 |
+
],
|
| 422 |
+
check=True,
|
| 423 |
+
capture_output=True,
|
| 424 |
)
|
| 425 |
+
|
| 426 |
return {"type": "video", "video_path": str(final_vid), "key": key}
|
| 427 |
|
| 428 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|