Spaces:
Running
on
Zero
Running
on
Zero
import gradio as gr | |
import os | |
import shutil | |
from typing import List, Optional | |
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 using userβdefined visual parameters (brightness, contrast, gamma).""" | |
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 or custom) --- | |
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() | |
title = generate_title(script) | |
description = generate_description(script) | |
else: | |
prompt = ( | |
f"You are a video creation expert. Here is the context: {context.strip()}\n" | |
f"Instruction: {instruction.strip()}\n" | |
f"π΄ Strict target duration: {target_duration}s β β {approx_words} words (must be respected)." | |
) | |
script = generate_script(prompt) | |
title = generate_title(script) | |
description = generate_description(script) | |
# --- 2. Prepare folders --- | |
for folder in ("./assets/audio", "./assets/backgrounds", "./assets/output"): | |
os.makedirs(folder, exist_ok=True) | |
voice_path = "./assets/audio/voice.mp3" | |
final_no_subs = "./assets/output/final_video.mp4" | |
final_with_subs = "./assets/output/final_video_subtitles.mp4" | |
# --- 3. Copy videos --- | |
for f in os.listdir("./assets/backgrounds"): | |
if f.lower().endswith(".mp4"): | |
os.remove(os.path.join("./assets/backgrounds", f)) | |
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. AI voice --- | |
generate_voice(script, voice_path) | |
# --- 5. Video montage --- | |
music_path = user_music if user_music and os.path.isfile(user_music) else None | |
_, out_no_audio = get_video_montage_from_folder( | |
folder_path="./assets/backgrounds", | |
audio_path=voice_path, | |
output_dir="./assets/video_music", | |
lum=lum, | |
contrast=contrast, | |
gamma=gamma, | |
show_progress_bar=show_progress_bar, | |
) | |
# --- 6. Mixing & subtitles --- | |
edit_video(out_no_audio, voice_path, music_path, final_no_subs) | |
if add_subs: | |
segments = transcribe_audio_to_subs(voice_path) | |
subs = chunk_text_by_words(segments, max_words=3) | |
add_subtitles_to_video(final_no_subs, subs, final_with_subs) | |
return script, title, description, final_with_subs | |
else: | |
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(block=True) | |