malvin noel
changes
c6771b3
raw
history blame
10.5 kB
#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()