File size: 10,010 Bytes
8b05224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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)