Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 890 |
else: # audio
|
| 891 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 906 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|