rairo commited on
Commit
1ceffca
Β·
verified Β·
1 Parent(s): d15492d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +127 -10
app.py CHANGED
@@ -859,7 +859,7 @@ def animate_image_fade(img: np.ndarray, dur: float, out: Path, fps: int = 24) ->
859
 
860
  def concat_media(file_paths: List[str], output_path: Path, media_type: str):
861
  """
862
- Concatenate multiple media files using FFmpeg.
863
 
864
  Args:
865
  file_paths (List[str]): List of input file paths
@@ -875,35 +875,152 @@ def concat_media(file_paths: List[str], output_path: Path, media_type: str):
875
 
876
  with open(list_file, 'w') as f:
877
  for path in file_paths:
878
- # Escape path for FFmpeg
879
- escaped_path = str(path).replace('\\', '\\\\').replace("'", "\\'")
 
 
880
  f.write(f"file '{escaped_path}'\n")
881
 
882
- # Build FFmpeg command
883
  cmd = [
884
  "ffmpeg", "-y", "-f", "concat", "-safe", "0",
885
  "-i", str(list_file)
886
  ]
887
 
888
  if media_type == "video":
889
- cmd.extend(["-c", "copy"])
 
890
  else: # audio
891
- cmd.extend(["-c:a", "aac"])
 
 
 
 
 
 
892
 
893
  cmd.append(str(output_path))
894
 
895
  # Execute FFmpeg command
896
- subprocess.run(cmd, check=True, capture_output=True)
897
 
898
  # Clean up temporary file
899
  list_file.unlink(missing_ok=True)
900
 
 
 
 
 
 
 
 
901
  except Exception as e:
902
  print(f"Media concatenation failed: {e}")
903
  # Create a fallback if concatenation fails
904
- if file_paths:
905
- # Just copy the first file as a fallback
906
- subprocess.run(["cp", file_paths[0], str(output_path)], check=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
 
908
  # ─── ENHANCED MAIN FUNCTIONS (DROP-IN REPLACEMENTS) ────────────────────────────
909
 
 
859
 
860
  def concat_media(file_paths: List[str], output_path: Path, media_type: str):
861
  """
862
+ Concatenate multiple media files using FFmpeg with proper sync handling.
863
 
864
  Args:
865
  file_paths (List[str]): List of input file paths
 
875
 
876
  with open(list_file, 'w') as f:
877
  for path in file_paths:
878
+ # Escape path for FFmpeg and ensure it exists
879
+ if not Path(path).exists():
880
+ continue
881
+ escaped_path = str(path).replace('\\', '/').replace("'", "\\'")
882
  f.write(f"file '{escaped_path}'\n")
883
 
884
+ # Build FFmpeg command with proper codec settings
885
  cmd = [
886
  "ffmpeg", "-y", "-f", "concat", "-safe", "0",
887
  "-i", str(list_file)
888
  ]
889
 
890
  if media_type == "video":
891
+ # For video: copy streams without re-encoding to preserve timing
892
+ cmd.extend(["-c:v", "copy", "-avoid_negative_ts", "make_zero"])
893
  else: # audio
894
+ # For audio: ensure consistent sample rate and format
895
+ cmd.extend([
896
+ "-c:a", "aac",
897
+ "-ar", "44100", # Consistent sample rate
898
+ "-ac", "2", # Stereo
899
+ "-b:a", "128k" # Consistent bitrate
900
+ ])
901
 
902
  cmd.append(str(output_path))
903
 
904
  # Execute FFmpeg command
905
+ result = subprocess.run(cmd, check=True, capture_output=True, text=True)
906
 
907
  # Clean up temporary file
908
  list_file.unlink(missing_ok=True)
909
 
910
+ except subprocess.CalledProcessError as e:
911
+ print(f"FFmpeg concatenation failed: {e.stderr}")
912
+ # Create a fallback if concatenation fails
913
+ if file_paths and Path(file_paths[0]).exists():
914
+ # Just copy the first file as a fallback
915
+ import shutil
916
+ shutil.copy2(file_paths[0], str(output_path))
917
  except Exception as e:
918
  print(f"Media concatenation failed: {e}")
919
  # Create a fallback if concatenation fails
920
+ if file_paths and Path(file_paths[0]).exists():
921
+ import shutil
922
+ shutil.copy2(file_paths[0], str(output_path))
923
+
924
+
925
+ def generate_video(buf: bytes, name: str, ctx: str, key: str):
926
+ """ENHANCED: Better video generation with reliable charts and FIXED AUDIO SYNC"""
927
+ try:
928
+ subprocess.run(["ffmpeg", "-version"], check=True, capture_output=True)
929
+ except Exception:
930
+ st.error("πŸ”΄ FFmpeg not available β€” cannot render video.")
931
+ return None
932
+
933
+ df, err = load_dataframe_safely(buf, name)
934
+ if err:
935
+ st.error(err)
936
+ return None
937
+
938
+ llm = ChatGoogleGenerativeAI(model="gemini-2.0-flash", google_api_key=API_KEY, temperature=0.2)
939
+
940
+ # ENHANCED: Better context for video generation
941
+ ctx_dict = {
942
+ "shape": df.shape,
943
+ "columns": list(df.columns),
944
+ "user_ctx": ctx or "General business analysis",
945
+ "full_dataframe": df.to_dict("records"),
946
+ "data_types": {col: str(dtype) for col, dtype in df.dtypes.to_dict().items()},
947
+ "numeric_summary": {col: {stat: float(val) for stat, val in stats.items()} for col, stats in df.describe().to_dict().items()} if len(df.select_dtypes(include=["number"]).columns) > 0 else {},
948
+ }
949
+
950
+ script = llm.invoke(build_story_prompt(ctx_dict)).content
951
+ scenes = [s.strip() for s in script.split("[SCENE_BREAK]") if s.strip()]
952
+
953
+ # ENHANCED: Better chart generation for video
954
+ chart_generator = create_chart_generator(llm, df)
955
+
956
+ video_parts, audio_parts, temps = [], [], []
957
+
958
+ for idx, sc in enumerate(scenes[:VIDEO_SCENES]):
959
+ st.progress((idx + 1) / VIDEO_SCENES, text=f"Rendering Scene {idx + 1}/{VIDEO_SCENES}")
960
+ descs, narrative = extract_chart_tags(sc), clean_narration(sc)
961
+
962
+ # FIXED: Generate audio first to get exact duration
963
+ audio_bytes, _ = deepgram_tts(narrative)
964
+ mp3 = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp3"
965
+
966
+ if audio_bytes:
967
+ mp3.write_bytes(audio_bytes)
968
+ # Get the EXACT duration of the generated audio
969
+ dur = audio_duration(str(mp3))
970
+ if dur <= 0: # Fallback if duration detection fails
971
+ dur = 5.0
972
+ else:
973
+ dur = 5.0
974
+ generate_silence_mp3(dur, mp3)
975
+
976
+ audio_parts.append(str(mp3))
977
+ temps.append(mp3)
978
+
979
+ # FIXED: Create video with EXACT same duration as audio
980
+ mp4 = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
981
+
982
+ if descs:
983
+ safe_chart(descs[0], df, dur, mp4)
984
+ else:
985
+ img = generate_image_from_prompt(narrative)
986
+ img_cv = cv2.cvtColor(np.array(img.resize((WIDTH, HEIGHT))), cv2.COLOR_RGB2BGR)
987
+ animate_image_fade(img_cv, dur, mp4)
988
+
989
+ video_parts.append(str(mp4))
990
+ temps.append(mp4)
991
+
992
+ # FIXED: Create concatenated files with proper sync
993
+ silent_vid = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp4"
994
+ audio_mix = Path(tempfile.gettempdir()) / f"{uuid.uuid4()}.mp3"
995
+
996
+ # Concatenate video and audio separately first
997
+ concat_media(video_parts, silent_vid, "video")
998
+ concat_media(audio_parts, audio_mix, "audio")
999
+
1000
+ # FIXED: Final merge with proper sync settings
1001
+ final_vid = Path(tempfile.gettempdir()) / f"{key}.mp4"
1002
+
1003
+ # Enhanced FFmpeg command for perfect sync
1004
+ subprocess.run([
1005
+ "ffmpeg", "-y",
1006
+ "-i", str(silent_vid), # Video input
1007
+ "-i", str(audio_mix), # Audio input
1008
+ "-c:v", "libx264", # Video codec (re-encode for compatibility)
1009
+ "-c:a", "aac", # Audio codec
1010
+ "-map", "0:v:0", # Map first video stream
1011
+ "-map", "1:a:0", # Map first audio stream
1012
+ "-shortest", # End when shortest stream ends
1013
+ "-avoid_negative_ts", "make_zero", # Fix timestamp issues
1014
+ "-fflags", "+genpts", # Generate presentation timestamps
1015
+ "-r", str(FPS), # Ensure consistent framerate
1016
+ str(final_vid)
1017
+ ], check=True, capture_output=True)
1018
+
1019
+ # Clean up temporary files
1020
+ for p in temps + [silent_vid, audio_mix]:
1021
+ p.unlink(missing_ok=True)
1022
+
1023
+ return str(final_vid)
1024
 
1025
  # ─── ENHANCED MAIN FUNCTIONS (DROP-IN REPLACEMENTS) ────────────────────────────
1026