File size: 10,469 Bytes
c6771b3
8b05224
 
 
 
fe4c8dd
8b05224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c6771b3
8b05224
 
 
 
 
 
c6771b3
8b05224
 
 
 
 
 
c6771b3
 
 
8b05224
 
 
c6771b3
 
 
 
 
8b05224
 
c6771b3
 
8b05224
c6771b3
8b05224
 
 
 
 
c6771b3
 
 
8b05224
c6771b3
8b05224
 
c6771b3
8b05224
c6771b3
8b05224
 
 
c6771b3
 
8b05224
 
c6771b3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8b05224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9326a98
1b48a1a
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#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()