Spaces:
Running
on
Zero
Running
on
Zero
#app.py | |
import gradio as gr | |
import os | |
import shutil | |
from typing import List, Optional | |
import spaces | |
from scripts.generate_scripts import generate_script, generate_title, generate_description | |
from scripts.generate_voice import generate_voice | |
from scripts.get_footage import get_video_montage_from_folder | |
from scripts.edit_video import edit_video | |
from scripts.generate_subtitles import ( | |
transcribe_audio_to_subs, | |
chunk_text_by_words, | |
add_subtitles_to_video, | |
) | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# Constants & helper utils | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
WORDS_PER_SECOND = 2.3 # β 140 wpm | |
def safe_copy(src: str, dst: str) -> str: | |
if os.path.abspath(src) == os.path.abspath(dst): | |
return src | |
shutil.copy(src, dst) | |
return dst | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# Core processing pipeline | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
def process_video( | |
context: str, | |
instruction: str, | |
target_duration: int, | |
script_mode: str, | |
custom_script: Optional[str], | |
lum: float, | |
contrast: float, | |
gamma: float, | |
add_subs: bool, | |
accumulated_videos: List[str] | None = None, | |
user_music: Optional[str] = None, | |
show_progress_bar: bool = True, | |
): | |
"""Build the final video with a single encoding pass.""" | |
if not accumulated_videos: | |
raise ValueError("β Please upload at least one background video (.mp4) before generating.") | |
approx_words = int(target_duration * WORDS_PER_SECOND) | |
# ββ 1. Script (AI ou perso) ββββββββββββββββββββββββββββββ | |
if script_mode == "Use my script": | |
if not custom_script or not custom_script.strip(): | |
raise ValueError("β You selected 'Use my script' but the script field is empty!") | |
script = custom_script.strip() | |
else: | |
prompt = ( | |
f"You are a video creation expert.\n\nContext:\n{context.strip()}\n\n" | |
f"Instruction:\n{instruction.strip()}\n\n" | |
f"π΄ Strict target duration: {target_duration}s β β {approx_words} words." | |
) | |
script = generate_script(prompt) | |
title = generate_title(script) | |
description = generate_description(script) | |
# ββ 2. PrΓ©paration rΓ©pertoires βββββββββββββββββββββββββββ | |
for folder in ("./assets/audio", "./assets/backgrounds", "./assets/output", "./assets/video_music"): | |
os.makedirs(folder, exist_ok=True) | |
voice_path = "./assets/audio/voice.mp3" | |
final_no_subs = "./assets/output/final_video.mp4" | |
# ββ 3. Copie unique des vidΓ©os de fond βββββββββββββββββββ | |
for idx, v in enumerate(accumulated_videos): | |
if not os.path.isfile(v) or not v.lower().endswith(".mp4"): | |
raise ValueError(f"β Invalid file: {v}") | |
safe_copy(v, os.path.join("./assets/backgrounds", f"video_{idx:03d}.mp4")) | |
# ββ 4. Voix IA (cache disque) ββββββββββββββββββββββββββββ | |
if not os.path.isfile(voice_path): | |
generate_voice(script, voice_path) | |
# ββ 5. Montage silencieux (pas dβaudio) ββββββββββββββββββ | |
_, out_no_audio = get_video_montage_from_folder( | |
folder_path="./assets/backgrounds", | |
audio_path=voice_path, # juste pour la durΓ©e, pas dβinjection | |
output_dir="./assets/video_music", | |
lum=lum, contrast=contrast, gamma=gamma, | |
show_progress_bar=show_progress_bar, | |
) | |
# ββ 6. Sous-titres (optionnel) βββββββββββββββββββββββββββ | |
subs = None | |
if add_subs: | |
segments = transcribe_audio_to_subs(voice_path) | |
subs = chunk_text_by_words(segments, max_words=3) | |
# ββ 7. Mux final en une passe ββββββββββββββββββββββββββββ | |
music_path = user_music if user_music and os.path.isfile(user_music) else None | |
edit_video( | |
video_path = out_no_audio, | |
audio_path = voice_path, | |
music_path = music_path, | |
output_path = final_no_subs, | |
music_volume = 0.10, | |
subtitles = subs, # β injectΓ©s ici | |
) | |
return script, title, description, final_no_subs | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# Upload helper | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
def accumulate_files(new: List[str], state: List[str] | None): | |
state = state or [] | |
for f in new or []: | |
if isinstance(f, str) and os.path.isfile(f) and f.lower().endswith(".mp4") and f not in state: | |
state.append(f) | |
return state | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
# Gradio UI | |
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
with gr.Blocks(theme="gradio/soft") as demo: | |
gr.Markdown("# π¬ AI Video Generator β Advanced Controls") | |
# ------------------- Parameters ------------------- | |
with gr.Tab("π οΈ Settings"): | |
with gr.Row(): | |
context_input = gr.Textbox(label="π§ Context", lines=4) | |
instruction_input = gr.Textbox(label="π― Instruction", lines=4) | |
duration_slider = gr.Slider(5, 120, 1, 60, label="β±οΈ Target duration (s)") | |
script_mode = gr.Radio([ | |
"Generate script with AI", | |
"Use my script", | |
], value="Generate script with AI", label="Script mode") | |
custom_script_input = gr.Textbox(label="βοΈ My script", lines=8, interactive=False) | |
def toggle_script_input(mode): | |
return gr.update(interactive=(mode == "Use my script")) | |
script_mode.change(toggle_script_input, inputs=script_mode, outputs=custom_script_input) | |
with gr.Accordion("π¨ Video Settings (brightness/contrast/gamma)", open=False): | |
lum_slider = gr.Slider(0, 20, 6, step=0.5, label="Brightness (0β20)") | |
contrast_slider = gr.Slider(0.5, 2.0, 1.0, step=0.05, label="Contrast (0.5β2.0)") | |
gamma_slider = gr.Slider(0.5, 2.0, 1.0, step=0.05, label="Gamma (0.5β2.0)") | |
with gr.Row(): | |
add_subs_checkbox = gr.Checkbox(label="Add dynamic subtitles", value=True) | |
with gr.Row(): | |
show_bar = gr.Checkbox(label="Show progress bar", value=True) | |
# Upload videos | |
videos_dropzone = gr.Files(label="ποΈ Background videos (MP4)", file_types=[".mp4"], type="filepath") | |
videos_state = gr.State([]) | |
video_list_display = gr.Textbox(label="β Selected videos", interactive=False, lines=4) | |
videos_dropzone.upload(accumulate_files, [videos_dropzone, videos_state], videos_state, queue=False) | |
videos_state.change(lambda s: "\n".join(os.path.basename(f) for f in s), videos_state, video_list_display, queue=False) | |
user_music = gr.File(label="π΅ Background music (MP3, optional)", file_types=[".mp3"], type="filepath") | |
generate_btn = gr.Button("π Generate the video", variant="primary") | |
with gr.Tab("π€ Results"): | |
video_output = gr.Video(label="π¬ Generated Video") | |
# Script + copy button | |
script_output = gr.Textbox(label="π Script", lines=6, interactive=False) | |
copy_script_btn = gr.Button("π Copy") | |
copy_script_btn.click( | |
None, | |
inputs=[script_output], | |
outputs=None, | |
js="(text) => navigator.clipboard.writeText(text)" | |
) | |
# Title + copy button | |
title_output = gr.Textbox(label="π¬ Title", lines=1, interactive=False) | |
copy_title_btn = gr.Button("π Copy") | |
copy_title_btn.click(None, inputs=title_output, outputs=None, js="(text) => {navigator.clipboard.writeText(text);}") | |
# Description + copy button | |
desc_output = gr.Textbox(label="π Description", lines=3, interactive=False) | |
copy_desc_btn = gr.Button("π Copy") | |
copy_desc_btn.click(None, inputs=desc_output, outputs=None, js="(text) => {navigator.clipboard.writeText(text);}") | |
# ------------------- Generation Callback ------------------- | |
generate_btn.click( | |
fn=process_video, | |
inputs=[ | |
context_input, | |
instruction_input, | |
duration_slider, | |
script_mode, | |
custom_script_input, | |
lum_slider, | |
contrast_slider, | |
gamma_slider, | |
add_subs_checkbox, | |
videos_state, | |
user_music, | |
show_bar, | |
], | |
outputs=[script_output, title_output, desc_output, video_output], | |
) | |
demo.launch() | |