RafaG commited on
Commit
4cc3314
·
verified ·
1 Parent(s): 9d080a1

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -849
app.py DELETED
@@ -1,849 +0,0 @@
1
- import gradio as gr
2
- import subprocess
3
- import os
4
- import sys
5
- import json
6
- import psutil
7
- import shutil
8
- import datetime
9
- import time
10
- import urllib.parse
11
- from fastapi import FastAPI
12
- from fastapi.staticfiles import StaticFiles
13
- import uvicorn
14
-
15
-
16
- import re
17
- import library # Module for Library Logic
18
- import subtitle_handler as subs # Module for Subtitles
19
- import subtitle_editor as editor # Module for Editor Logic
20
-
21
- # Path to the main script
22
- MAIN_SCRIPT_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "main_improved.py")
23
- WORKING_DIR = os.path.dirname(MAIN_SCRIPT_PATH)
24
- sys.path.append(WORKING_DIR)
25
-
26
- from i18n.i18n import I18nAuto
27
- i18n = I18nAuto()
28
-
29
- # --- PRESETS DEFINITIONS ---
30
- FACE_PRESETS = {
31
- "Default (Balanced)": {"thresh": 0.35, "two_face": 0.60, "conf": 0.40, "dead_zone": 150},
32
- "Stable (Focus Main)": {"thresh": 0.60, "two_face": 0.80, "conf": 0.60, "dead_zone": 200},
33
- "Sensitive (Catch All)": {"thresh": 0.10, "two_face": 0.40, "conf": 0.30, "dead_zone": 100},
34
- "High Precision": {"thresh": 0.40, "two_face": 0.65, "conf": 0.75, "dead_zone": 150},
35
- }
36
-
37
- EXPERIMENTAL_PRESETS = {
38
- "Default (Off)": {"focus": False, "mar": 0.03, "score": 1.5, "motion": False, "motion_th": 3.0, "motion_sens": 0.05, "decay": 2.0},
39
- "Active Speaker (Balanced)": {"focus": True, "mar": 0.03, "score": 1.5, "motion": True, "motion_th": 3.0, "motion_sens": 0.05, "decay": 2.0},
40
- "Active Speaker (Sensitive)": {"focus": True, "mar": 0.02, "score": 1.0, "motion": True, "motion_th": 2.0, "motion_sens": 0.10, "decay": 1.0},
41
- "Active Speaker (Stable)": {"focus": True, "mar": 0.05, "score": 2.5, "motion": False, "motion_th": 5.0, "motion_sens": 0.02, "decay": 3.0},
42
- }
43
- # ---------------------------
44
-
45
- VIRALS_DIR = os.path.join(WORKING_DIR, "VIRALS")
46
- MODELS_DIR = os.path.join(WORKING_DIR, "models")
47
-
48
- # Ensure directories exist
49
- if not os.path.exists(VIRALS_DIR):
50
- os.makedirs(VIRALS_DIR, exist_ok=True)
51
- if not os.path.exists(MODELS_DIR):
52
- os.makedirs(MODELS_DIR, exist_ok=True)
53
-
54
- # Global variables
55
- current_process = None
56
-
57
- # Helpers
58
- def convert_color_to_ass(hex_color, alpha="00"):
59
- if not hex_color or not hex_color.startswith("#"):
60
- return f"&H{alpha}FFFFFF&"
61
- hex_clean = hex_color.lstrip('#')
62
- if len(hex_clean) == 6:
63
- r = hex_clean[0:2]
64
- g = hex_clean[2:4]
65
- b = hex_clean[4:6]
66
- return f"&H{alpha}{b}{g}{r}&"
67
- return f"&H{alpha}FFFFFF&"
68
-
69
- def kill_process():
70
- global current_process
71
- if current_process:
72
- try:
73
- parent = psutil.Process(current_process.pid)
74
- for child in parent.children(recursive=True):
75
- child.kill()
76
- parent.kill()
77
- current_process = None
78
- return i18n("Process terminated.")
79
- except Exception as e:
80
- return i18n("Error terminating process: {}").format(e)
81
- return i18n("No process running.")
82
-
83
- GEMINI_MODELS = [
84
- 'gemini-3-pro-preview',
85
- 'gemini-2.5-flash',
86
- 'gemini-2.5-flash-preview-09-2025',
87
- 'gemini-2.5-flash-lite',
88
- 'gemini-2.5-flash-lite-preview-09-2025',
89
- 'gemini-2.5-pro',
90
- 'gemini-2.0-flash',
91
- 'gemini-2.0-flash-lite'
92
- ]
93
-
94
- G4F_MODELS = [
95
- 'gpt-4o',
96
- 'gpt-4o-mini',
97
- 'gpt-4',
98
- 'o1-mini',
99
- 'o1',
100
- 'deepseek-r1',
101
- 'deepseek-v3',
102
- 'llama-3.3-70b',
103
- 'llama-3.1-405b',
104
- 'claude-3.5-sonnet',
105
- 'claude-3.7-sonnet',
106
- 'gemini-2.0-flash',
107
- 'qwen-2.5-72b'
108
- ]
109
-
110
- def get_local_models():
111
- if not os.path.exists(MODELS_DIR): return []
112
- return [f for f in os.listdir(MODELS_DIR) if f.endswith(".gguf")]
113
-
114
-
115
-
116
- def apply_face_preset(preset_name):
117
- if preset_name not in FACE_PRESETS:
118
- return [gr.update() for _ in range(4)] # No change
119
-
120
- p = FACE_PRESETS[preset_name]
121
- return p["thresh"], p["two_face"], p["conf"], p["dead_zone"]
122
-
123
- def apply_experimental_preset(preset_name):
124
- if preset_name not in EXPERIMENTAL_PRESETS:
125
- return [gr.update() for _ in range(7)] # No change
126
-
127
- p = EXPERIMENTAL_PRESETS[preset_name]
128
- return p["focus"], p["mar"], p["score"], p["motion"], p["motion_th"], p["motion_sens"], p["decay"]
129
-
130
- # Subtitle logic moved to subtitle_handler.py
131
-
132
-
133
- def run_viral_cutter(input_source, project_name, url, video_file, segments, viral, themes, min_duration, max_duration, model, ai_backend, api_key, ai_model_name, chunk_size, workflow, face_model, face_mode, face_detect_interval,
134
- face_filter_thresh, face_two_thresh, face_conf_thresh, face_dead_zone, focus_active_speaker, active_speaker_mar, active_speaker_score_diff, include_motion, active_speaker_motion_threshold, active_speaker_motion_sensitivity, active_speaker_decay,
135
- use_custom_subs, font_name, font_size, font_color, highlight_color, outline_color, outline_thickness, shadow_color, shadow_size, is_bold, is_italic, is_uppercase, vertical_pos, alignment,
136
- h_size, w_block, gap, mode, under, strike, border_s, remove_punc, video_quality, use_youtube_subs, translate_target):
137
-
138
- global current_process
139
- yield "", gr.update(value=i18n("Running..."), interactive=False), gr.update(visible=True), None
140
-
141
- cmd = [sys.executable, MAIN_SCRIPT_PATH]
142
-
143
- # Input Source Logic
144
- if input_source == "Existing Project":
145
- if not project_name:
146
- yield i18n("Error: No project selected."), gr.update(value=i18n("Start Processing"), interactive=True), gr.update(visible=False), None
147
- return
148
- full_project_path = os.path.join(VIRALS_DIR, project_name)
149
- cmd.extend(["--project-path", full_project_path])
150
- elif input_source == "Upload Video":
151
- if not video_file:
152
- yield i18n("Error: No video file uploaded."), gr.update(value=i18n("Start Processing"), interactive=True), gr.update(visible=False), None
153
- return
154
-
155
- # Determine project name from filename
156
- original_filename = os.path.basename(video_file)
157
- name_no_ext = os.path.splitext(original_filename)[0]
158
- # Sanitize: Allow alphanumeric, space, dash, underscore
159
- safe_name = "".join([c for c in name_no_ext if c.isalnum() or c in " _-"]).strip()
160
- if not safe_name: safe_name = "Untitled_Upload"
161
-
162
- # Always append timestamp as requested
163
- timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
164
- project_name_upload = f"{safe_name}_{timestamp}"
165
- project_path = os.path.join(VIRALS_DIR, project_name_upload)
166
-
167
- os.makedirs(project_path, exist_ok=True)
168
-
169
- target_path = os.path.join(project_path, "input.mp4")
170
- shutil.copy(video_file, target_path)
171
-
172
- cmd.extend(["--project-path", project_path])
173
- # Skip YouTube subs as it is a local upload
174
- cmd.append("--skip-youtube-subs")
175
-
176
- else:
177
- if url: cmd.extend(["--url", url])
178
- # Pass Video Quality
179
- if video_quality: cmd.extend(["--video-quality", video_quality])
180
- # Pass Subtitle Option (if False, we skip)
181
- if not use_youtube_subs: cmd.append("--skip-youtube-subs")
182
-
183
- # Translation
184
- if translate_target and translate_target != "None":
185
- cmd.extend(["--translate-target", translate_target])
186
-
187
-
188
- cmd.extend(["--segments", str(int(segments))])
189
- if viral: cmd.append("--viral")
190
- if themes: cmd.extend(["--themes", themes])
191
- cmd.extend(["--min-duration", str(int(min_duration))])
192
- cmd.extend(["--max-duration", str(int(max_duration))])
193
- cmd.extend(["--model", model])
194
- cmd.extend(["--ai-backend", ai_backend])
195
- if api_key: cmd.extend(["--api-key", api_key])
196
-
197
- # New AI Params
198
- if ai_model_name: cmd.extend(["--ai-model-name", str(ai_model_name)])
199
- if chunk_size: cmd.extend(["--chunk-size", str(int(chunk_size))])
200
-
201
- workflow_map = {"Full": "1", "Cut Only": "2", "Subtitles Only": "3"}
202
- cmd.extend(["--workflow", workflow_map.get(workflow, "1")])
203
- cmd.extend(["--face-model", face_model])
204
- cmd.extend(["--face-mode", face_mode])
205
- if face_detect_interval: cmd.extend(["--face-detect-interval", str(face_detect_interval)])
206
-
207
- # New Face Params
208
- if face_filter_thresh is not None: cmd.extend(["--face-filter-threshold", str(face_filter_thresh)])
209
- if face_two_thresh is not None: cmd.extend(["--face-two-threshold", str(face_two_thresh)])
210
- if face_conf_thresh is not None: cmd.extend(["--face-confidence-threshold", str(face_conf_thresh)])
211
- if face_dead_zone is not None: cmd.extend(["--face-dead-zone", str(face_dead_zone)])
212
-
213
-
214
-
215
- cmd.append("--skip-prompts")
216
-
217
- if focus_active_speaker:
218
- cmd.append("--focus-active-speaker")
219
- if active_speaker_mar is not None: cmd.extend(["--active-speaker-mar", str(active_speaker_mar)])
220
- if active_speaker_score_diff is not None: cmd.extend(["--active-speaker-score-diff", str(active_speaker_score_diff)])
221
- if include_motion: cmd.append("--include-motion")
222
- if active_speaker_motion_threshold is not None: cmd.extend(["--active-speaker-motion-threshold", str(active_speaker_motion_threshold)])
223
- if active_speaker_motion_sensitivity is not None: cmd.extend(["--active-speaker-motion-sensitivity", str(active_speaker_motion_sensitivity)])
224
- if active_speaker_decay is not None: cmd.extend(["--active-speaker-decay", str(active_speaker_decay)])
225
-
226
- cmd.append("--skip-prompts") # Always skip prompts in WebUI to prevent freezing
227
-
228
- if use_custom_subs:
229
- subtitle_config = {
230
- "font": font_name, "base_size": int(font_size), "base_color": convert_color_to_ass(font_color), "highlight_color": convert_color_to_ass(highlight_color),
231
- "outline_color": convert_color_to_ass(outline_color), "outline_thickness": outline_thickness, "shadow_color": convert_color_to_ass(shadow_color),
232
- "shadow_size": shadow_size, "vertical_position": vertical_pos, "alignment": alignment, "bold": 1 if is_bold else 0, "italic": 1 if is_italic else 0,
233
- "underline": 1 if under else 0, "strikeout": 1 if strike else 0, "border_style": border_s, "words_per_block": int(w_block), "gap_limit": gap,
234
- "mode": mode, "highlight_size": int(h_size), "remove_punctuation": remove_punc
235
- }
236
- # Uppercase is handled in main script or logic?
237
- # Actually subtitle_config doesn't seem to natively support "uppercase" in get_subtitle_config default, but app.py was using it.
238
- # I should probably add it back if I want to support it, but user said "PROHIBITED to remove existing ones".
239
- # I'll re-add 'uppercase': 1 if is_uppercase else 0 to the dict if the backend supports it, otherwise it's just ignored.
240
- # But wait, main_improved.py doesn't have 'uppercase' in get_subtitle_config.
241
- # I'll keep it in the dict just in case logic uses it elsewhere or if I missed it.
242
- # Actually, standard ASS doesn't support uppercase flag directly in Style, it needs to be text transform.
243
- # But I'll leave it in the dict.
244
- subtitle_config["uppercase"] = 1 if is_uppercase else 0
245
-
246
- subtitle_config_path = os.path.join(WORKING_DIR, "temp_subtitle_config.json")
247
- try:
248
- with open(subtitle_config_path, "w", encoding="utf-8") as f:
249
- json.dump(subtitle_config, f, indent=4)
250
- cmd.extend(["--subtitle-config", subtitle_config_path])
251
- except Exception: pass
252
-
253
- env = os.environ.copy()
254
- env["PYTHONUNBUFFERED"] = "1"
255
- try:
256
- current_process = subprocess.Popen(cmd, cwd=WORKING_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True, env=env)
257
- logs = ""
258
- project_folder_path = None
259
- if input_source == "Existing Project" and project_name:
260
- # If using existing project, we already know the path, but let's see if logs confirm it
261
- project_folder_path = os.path.join(VIRALS_DIR, project_name)
262
-
263
- while True:
264
- line = current_process.stdout.readline()
265
- if not line and current_process.poll() is not None:
266
- break
267
-
268
- if line:
269
- logs += line
270
- if "Project Folder:" in line:
271
- parts = line.split("Project Folder:")
272
- if len(parts) > 1: project_folder_path = parts[1].strip()
273
- yield logs, gr.update(visible=True, interactive=False), gr.update(visible=True), None
274
- except Exception as e:
275
- logs += f"\nError running process: {str(e)}\n"
276
- yield logs, gr.update(visible=True, interactive=False), gr.update(visible=True), None
277
- finally:
278
- if current_process:
279
- if current_process.stdout:
280
- try:
281
- current_process.stdout.close()
282
- except Exception: pass
283
- if current_process.poll() is None:
284
- # If we are here, it means we finished reading or errored out, but process is still running.
285
- # If it was a normal break from loop, process should be done or close to done.
286
- # If we are stopping, current_process.terminate() might be needed outside?
287
- # But here we just wait.
288
- try:
289
- current_process.wait()
290
- except Exception: pass
291
- current_process = None
292
-
293
- # Wait to ensure filesystem flush
294
- time.sleep(1.0)
295
-
296
- html_output = ""
297
- if project_folder_path and os.path.exists(project_folder_path):
298
- html_output = library.generate_project_gallery(project_folder_path, is_full_path=True)
299
- else:
300
- html_output = f"<h3>{i18n('Error: Project folder could not be determined from logs.')}</h3>"
301
- yield logs, gr.update(value=i18n("Start Processing"), interactive=True), gr.update(visible=False), html_output
302
-
303
- css = """
304
- /* Global Dark Theme Overrides */
305
- body, .gradio-container {
306
- background-color: #0b0b0b !important;
307
- color: #ffffff !important;
308
- }
309
-
310
- /* Force dark background for specific inputs that might be white */
311
- input[type="password"], textarea, select {
312
- background-color: #1f1f1f !important;
313
- color: #ffffff !important;
314
- border: 1px solid #333 !important;
315
- }
316
-
317
- /* Hide Footer */
318
- footer {visibility: hidden}
319
-
320
- /* Container Width */
321
- .gradio-container {
322
- max-width: 98% !important;
323
- width: 98% !important;
324
- margin: 0 auto !important;
325
- }
326
- """
327
-
328
- import header
329
-
330
- with gr.Blocks(title=i18n("ViralCutter WebUI"), theme=gr.themes.Default(primary_hue="orange", neutral_hue="slate"), css=css) as demo:
331
- gr.Markdown(header.badges)
332
- gr.Markdown(header.description)
333
- with gr.Tabs():
334
- with gr.Tab(i18n("Create New")):
335
- with gr.Row():
336
- with gr.Column(scale=1):
337
- input_source = gr.Radio([(i18n("YouTube URL"), "YouTube URL"), (i18n("Existing Project"), "Existing Project"), (i18n("Upload Video"), "Upload Video")], label=i18n("Input Source"), value="Upload Video")
338
-
339
- url_input = gr.Textbox(label=i18n("YouTube URL"), placeholder="https://www.youtube.com/watch?v=...", visible=True)
340
- video_upload = gr.File(label=i18n("Upload Video"), file_count="single", file_types=["video"], visible=False)
341
-
342
- with gr.Row():
343
- video_quality_input = gr.Dropdown(choices=["best", "1080p", "720p", "480p"], label=i18n("Video Quality"), value="best")
344
- translate_input = gr.Dropdown(choices=["None", "pt", "en", "es", "fr", "de", "it", "ru", "ja", "ko", "zh-CN"], label=i18n("Translate Subtitles To"), value="None")
345
- use_youtube_subs_input = gr.Checkbox(label=i18n("Use YouTube Subs"), value=True, info=i18n("Download and use official subtitles if available. (Recommended, it speeds up the process)"))
346
-
347
- project_selector = gr.Dropdown(choices=[], label=i18n("Select Project"), visible=False)
348
-
349
- def on_source_change(source):
350
- if source == "YouTube URL":
351
- return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(value="Full")
352
- elif source == "Upload Video":
353
- return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(value="Full")
354
- else:
355
- # Load projects
356
- projs = library.get_existing_projects()
357
- return gr.update(visible=False), gr.update(choices=projs, visible=True), gr.update(visible=False), gr.update(value="Subtitles Only")
358
-
359
-
360
- with gr.Row():
361
- segments_input = gr.Number(label=i18n("Segments"), value=3, precision=0)
362
- viral_input = gr.Checkbox(label=i18n("Viral Mode"), value=True)
363
- themes_input = gr.Textbox(label=i18n("Themes"), placeholder=i18n("funny, sad..."), visible=False)
364
- viral_input.change(lambda x: gr.update(visible=not x), viral_input, themes_input)
365
- with gr.Row():
366
- min_dur_input = gr.Number(label=i18n("Min Duration (s)"), value=15)
367
- max_dur_input = gr.Number(label=i18n("Max Duration (s)"), value=90)
368
- with gr.Column(scale=1):
369
- with gr.Row():
370
- ai_backend_input = gr.Dropdown(choices=[(i18n("Gemini"), "gemini"), (i18n("G4F"), "g4f"), (i18n("Local (GGUF)"), "local"), (i18n("Manual"), "manual")], label=i18n("AI Backend"), value="gemini", scale=2)
371
- api_key_input = gr.Textbox(label=i18n("Gemini API Key"), type="password", scale=3)
372
-
373
- # New Dynamic Inputs
374
- with gr.Row():
375
- ai_model_input = gr.Dropdown(choices=GEMINI_MODELS, label=i18n("AI Model"), value=GEMINI_MODELS[1], allow_custom_value=True, visible=True, scale=5)
376
- refresh_models_btn = gr.Button("🔄", size="sm", visible=False, scale=0, min_width=50) # Only local
377
- chunk_size_input = gr.Number(label=i18n("Chunk Size"), value=20000, precision=0, scale=2)
378
-
379
- # Update listeners with logic to hide/show API key
380
- def update_ai_ui(backend):
381
- show_api = (backend == "gemini")
382
- show_refresh = (backend == "local")
383
-
384
- # Definições padrão para evitar que fiquem vazios
385
- new_choices = []
386
- new_val = ""
387
- new_chunk = 20000
388
-
389
- if backend == "gemini":
390
- new_choices = GEMINI_MODELS
391
- new_val = GEMINI_MODELS[1]
392
- new_chunk = 20000
393
- elif backend == "g4f":
394
- new_choices = G4F_MODELS
395
- new_val = G4F_MODELS[0]
396
- new_chunk = 3000
397
- elif backend == "local":
398
- models = get_local_models()
399
- new_choices = models if models else [i18n("No models found")]
400
- new_val = new_choices[0]
401
- new_chunk = 15000
402
- else: # Manual
403
- pass
404
-
405
- return (
406
- gr.update(visible=show_api), # API Key Visibility (Fixes hole 1)
407
- gr.update(choices=new_choices, value=new_val, visible=(backend != "manual")), # Model Dropdown
408
- gr.update(visible=show_refresh), # Refresh Button
409
- gr.update(value=new_chunk) # Chunk Size
410
- )
411
-
412
- def refresh_local_models():
413
- models = get_local_models()
414
- val = models[0] if models else i18n("No models found")
415
- return gr.update(choices=models, value=val)
416
-
417
- refresh_models_btn.click(refresh_local_models, outputs=ai_model_input)
418
- ai_backend_input.change(update_ai_ui, inputs=ai_backend_input, outputs=[api_key_input, ai_model_input, refresh_models_btn, chunk_size_input])
419
-
420
- model_input = gr.Dropdown(["tiny", "small", "medium", "large", "large-v1", "large-v2", "large-v3", "turbo", "large-v3-turbo", "distil-large-v2", "distil-medium.en", "distil-small.en", "distil-large-v3"], label=i18n("Whisper Model"), value="large-v3-turbo")
421
- with gr.Row():
422
- workflow_input = gr.Dropdown(choices=[(i18n("Full"), "Full"), (i18n("Cut Only"), "Cut Only"), (i18n("Subtitles Only"), "Subtitles Only")], label=i18n("Workflow"), value="Full")
423
- face_model_input = gr.Dropdown(["insightface", "mediapipe"], label=i18n("Face Model"), value="insightface")
424
- with gr.Row():
425
- face_mode_input = gr.Dropdown(choices=[(i18n("Auto"), "auto"), ("1", "1"), ("2", "2")], label=i18n("Face Mode"), value="auto")
426
- face_detect_interval_input = gr.Textbox(label=i18n("Face Det. Interval"), value="0.17,1.0")
427
-
428
-
429
- # Update listeners now that all components are defined
430
- input_source.change(on_source_change, inputs=input_source, outputs=[url_input, project_selector, video_upload, workflow_input])
431
-
432
- with gr.Accordion(i18n("Advanced Face Settings"), open=False):
433
- face_preset_input = gr.Dropdown(choices=[(i18n(k), k) for k in FACE_PRESETS.keys()], label=i18n("Configuration Presets"), value="Default (Balanced)", interactive=True)
434
- with gr.Row():
435
- face_filter_thresh_input = gr.Slider(label=i18n("Ignore Small Faces (0.0 - 1.0)"), minimum=0.0, maximum=1.0, value=0.35, step=0.05, info=i18n("Relative size to ignore background."))
436
- face_two_thresh_input = gr.Slider(label=i18n("Threshold for 2 Faces (0.0 - 1.0)"), minimum=0.0, maximum=1.0, value=0.60, step=0.05, info=i18n("Size of 2nd face to activate split mode."))
437
- face_conf_thresh_input = gr.Slider(label=i18n("Minimum Confidence (0.0 - 1.0)"), minimum=0.0, maximum=1.0, value=0.40, step=0.05, info=i18n("Ignore detections with low confidence."))
438
- face_dead_zone_input = gr.Slider(label=i18n("Dead Zone (Stabilization)"), minimum=0, maximum=200, value=150, step=5, info=i18n("Movement pixels to ignore."))
439
-
440
- face_preset_input.change(apply_face_preset, inputs=face_preset_input, outputs=[face_filter_thresh_input, face_two_thresh_input, face_conf_thresh_input, face_dead_zone_input])
441
-
442
- with gr.Accordion(i18n("Experimental: Active Speaker & Motion"), open=False):
443
- experimental_preset_input = gr.Dropdown(choices=[(i18n(k), k) for k in EXPERIMENTAL_PRESETS.keys()], label=i18n("Configuration Presets"), value="Default (Off)", interactive=True)
444
- focus_active_speaker_input = gr.Checkbox(label=i18n("Experimental: Focus on Speaker"), value=False, info=i18n("Tries to focus only on the speaking person instead of split screen."))
445
- with gr.Row():
446
- active_speaker_mar_input = gr.Slider(label=i18n("MAR Threshold (Mouth Open)"), minimum=0.01, maximum=0.20, value=0.03, step=0.005, info=i18n("Mouth open sensitivity."))
447
- active_speaker_score_diff_input = gr.Slider(label=i18n("Score Difference"), minimum=0.5, maximum=10.0, value=1.5, step=0.5, info=i18n("Minimum difference to focus on 1 face."))
448
-
449
- with gr.Row():
450
- include_motion_input = gr.Checkbox(label=i18n("Consider Motion"), value=False, info=i18n("Increases score with motion (gestures)."))
451
-
452
- with gr.Row():
453
- active_speaker_motion_threshold_input = gr.Slider(label=i18n("Motion Dead Zone"), minimum=0.0, maximum=20.0, value=3.0, step=0.5, info=i18n("Pixels ignored."))
454
- active_speaker_motion_sensitivity_input = gr.Slider(label=i18n("Motion Sensitivity"), minimum=0.01, maximum=0.5, value=0.05, step=0.01, info=i18n("Points per pixel."))
455
- active_speaker_decay_input = gr.Slider(label=i18n("Switch Speed"), minimum=0.5, maximum=5.0, value=2.0, step=0.5, info=i18n("Speed to lose focus."))
456
-
457
- experimental_preset_input.change(apply_experimental_preset, inputs=experimental_preset_input, outputs=[focus_active_speaker_input, active_speaker_mar_input, active_speaker_score_diff_input, include_motion_input, active_speaker_motion_threshold_input, active_speaker_motion_sensitivity_input, active_speaker_decay_input])
458
- with gr.Accordion(i18n("Subtitle Settings (alpha)"), open=False):
459
- preset_input = gr.Dropdown(choices=[(i18n("Manual"), "Manual")] + [(i18n(k), k) for k in subs.SUBTITLE_PRESETS.keys()], label=i18n("Quick Presets"), value="Hormozi (Classic)")
460
- use_custom_subs = gr.Checkbox(label=i18n("Enable Subtitle Customization (Includes Preset)"), value=True)
461
-
462
- # Previews (Always Visible)
463
- preview_html = gr.HTML(value=f"<div style='text-align:center; padding:10px; color:#666;'>{i18n('Select options or preset to preview')}</div>")
464
-
465
- with gr.Row():
466
- preview_vid_btn = gr.Button(i18n("🎬 Render Animated Preview (Slow)"), size="sm")
467
- preview_vid = gr.Video(label=i18n("Animated Preview"), height=300, autoplay=True, interactive=False)
468
-
469
- with gr.Accordion(i18n("Advanced Settings"), open=False):
470
- gr.Markdown(f"### {i18n('Appearance')}")
471
- with gr.Row():
472
- font_name_input = gr.Textbox(label=i18n("Font Name"), value="Montserrat-Regular")
473
- font_size_input = gr.Slider(label=i18n("Font Size (Base)"), minimum=8, maximum=80, value=12)
474
- highlight_size_input = gr.Slider(label=i18n("Highlight Size"), minimum=8, maximum=80, value=14)
475
-
476
- with gr.Row():
477
- font_color_input = gr.ColorPicker(label=i18n("Base Color"), value="#FFFFFF")
478
- highlight_color_input = gr.ColorPicker(label=i18n("Highlight Color"), value="#00FF00")
479
- outline_color_input = gr.ColorPicker(label=i18n("Outline Color"), value="#000000")
480
- shadow_color_input = gr.ColorPicker(label=i18n("Shadow Color"), value="#000000")
481
-
482
- gr.Markdown(f"### {i18n('Styling & Effects')}")
483
- with gr.Row():
484
- outline_thickness_input = gr.Slider(label=i18n("Outline Thickness"), minimum=0, maximum=10, value=1.5)
485
- shadow_size_input = gr.Slider(label=i18n("Shadow Size"), minimum=0, maximum=10, value=2)
486
- border_style_input = gr.Dropdown(choices=[(i18n("Outline"), 1), (i18n("Opaque Box"), 3)], label=i18n("Border Style"), value=1)
487
-
488
- with gr.Row():
489
- bold_input = gr.Checkbox(label=i18n("Bold"))
490
- italic_input = gr.Checkbox(label=i18n("Italic"))
491
- uppercase_input = gr.Checkbox(label=i18n("Uppercase"))
492
- remove_punc_input = gr.Checkbox(label=i18n("Remove Punctuation"), value=True)
493
- underline_input = gr.Checkbox(label=i18n("Underline"))
494
- strikeout_input = gr.Checkbox(label=i18n("Strikeout"))
495
-
496
- gr.Markdown(f"### {i18n('Positioning & Layout')}")
497
- with gr.Row():
498
- vertical_pos_input = gr.Slider(label=i18n("V-Pos (Margin V)"), minimum=0, maximum=500, value=210)
499
- alignment_input = gr.Dropdown(choices=[(i18n("Left"), 1), (i18n("Center"), 2), (i18n("Right"), 3)], label=i18n("Alignment"), value=2)
500
- gap_limit_input = gr.Slider(label=i18n("Gap Limit"), minimum=0.0, maximum=5.0, value=0.5, step=0.1)
501
- mode_input = gr.Dropdown(choices=[(i18n("Highlight"), "highlight"), (i18n("Word by Word"), "word_by_word"), (i18n("No Highlight"), "no_highlight")], label=i18n("Mode"), value="highlight")
502
- words_per_block_input = gr.Slider(label=i18n("Words per Block"), minimum=1, maximum=20, value=3, step=1)
503
-
504
- manual_inputs = [
505
- font_name_input, font_size_input, font_color_input, highlight_color_input,
506
- outline_color_input, outline_thickness_input, shadow_color_input, shadow_size_input,
507
- bold_input, italic_input, uppercase_input,
508
- highlight_size_input, words_per_block_input, gap_limit_input, mode_input,
509
- underline_input, strikeout_input, border_style_input,
510
- vertical_pos_input, alignment_input,
511
- remove_punc_input
512
- ]
513
-
514
- # Update manual inputs when preset changes
515
- preset_input.change(subs.apply_preset, inputs=[preset_input], outputs=manual_inputs)
516
-
517
- # Auto-update PREVIEW HTML on any change
518
- for inp in manual_inputs:
519
- inp.change(subs.generate_preview_html, inputs=manual_inputs, outputs=preview_html)
520
-
521
- # Render video button
522
- preview_vid_btn.click(
523
- subs.render_preview_video,
524
- inputs=manual_inputs,
525
- outputs=preview_vid
526
- )
527
-
528
- # Initial load
529
- demo.load(subs.generate_preview_html, inputs=manual_inputs, outputs=preview_html)
530
- demo.load(subs.apply_preset, inputs=[preset_input], outputs=manual_inputs) # Apply default preset on load
531
-
532
- with gr.Row():
533
- start_btn = gr.Button(i18n("Start Processing"), variant="primary")
534
- stop_btn = gr.Button(i18n("Stop"), variant="stop", visible=False)
535
- stop_btn.click(kill_process, outputs=[])
536
- logs_output = gr.Textbox(label=i18n("Logs"), lines=10, autoscroll=True, elem_id="logs_output")
537
-
538
- # Force scroll to bottom via JS
539
- logs_output.change(fn=None, inputs=[], outputs=[], js="""
540
- function() {
541
- var ta = document.querySelector('#logs_output textarea');
542
- if(ta) {
543
- ta.scrollTop = ta.scrollHeight;
544
- }
545
- }
546
- """)
547
- results_html = gr.HTML(label=i18n("Results"))
548
-
549
- # MUST pass all all new inputs to the run function
550
- start_btn.click(run_viral_cutter, inputs=[
551
- input_source, project_selector, url_input, video_upload, segments_input, viral_input, themes_input, min_dur_input, max_dur_input,
552
- model_input, ai_backend_input, api_key_input, ai_model_input, chunk_size_input,
553
- workflow_input, face_model_input, face_mode_input, face_detect_interval_input,
554
- face_filter_thresh_input, face_two_thresh_input, face_conf_thresh_input, face_dead_zone_input, focus_active_speaker_input,
555
- active_speaker_mar_input, active_speaker_score_diff_input, include_motion_input, active_speaker_motion_threshold_input, active_speaker_motion_sensitivity_input, active_speaker_decay_input,
556
- use_custom_subs,
557
- # Expanded Manual Inputs mapping
558
- font_name_input, font_size_input, font_color_input, highlight_color_input,
559
- outline_color_input, outline_thickness_input, shadow_color_input, shadow_size_input,
560
- bold_input, italic_input, uppercase_input, vertical_pos_input, alignment_input,
561
- # New Inputs
562
- highlight_size_input, words_per_block_input, gap_limit_input, mode_input,
563
- underline_input, strikeout_input, border_style_input, remove_punc_input,
564
- video_quality_input, use_youtube_subs_input, translate_input
565
- ], outputs=[logs_output, start_btn, stop_btn, results_html])
566
-
567
-
568
- with gr.Tab(i18n("Subtitle Editor")):
569
- gr.Markdown(f"### {i18n('Edit Subtitles (Smart Mode)')}")
570
-
571
- with gr.Group():
572
- editor_project_dropdown = gr.Dropdown(choices=library.get_existing_projects(), label=i18n("Select Project"), value=None)
573
- editor_refresh_btn = gr.Button(i18n("Refresh"), size="sm")
574
-
575
- with gr.Group():
576
- editor_file_dropdown = gr.Dropdown(choices=[], label=i18n("Select Subtitle File"), interactive=True)
577
- editor_load_btn = gr.Button(i18n("Load Subtitles"), variant="secondary")
578
-
579
- # Hidden state to store full path of currently loaded JSON
580
- current_json_path = gr.State()
581
-
582
- # The Dataframe Editor
583
- # Headers: Start, End, Text
584
- subtitle_dataframe = gr.Dataframe(
585
- headers=["Start", "End", "Text"],
586
- datatype=["str", "str", "str"],
587
- col_count=(3, "fixed"),
588
- interactive=True,
589
- label=i18n("Subtitle Segments"),
590
- wrap=True
591
- )
592
-
593
- with gr.Row():
594
- editor_save_btn = gr.Button(i18n("💾 Save Changes"), variant="primary")
595
- editor_render_single_btn = gr.Button(i18n("⚡ Render This Segment (Very-Fast)"), variant="secondary")
596
- editor_render_all_btn = gr.Button(i18n("🎬 Render All (Fast)"), variant="stop")
597
-
598
- editor_status = gr.Textbox(label=i18n("Status"), interactive=False)
599
-
600
- # --- Callbacks for Editor ---
601
- editor_refresh_btn.click(library.refresh_projects, outputs=editor_project_dropdown)
602
-
603
- def update_file_list(proj_name):
604
- if not proj_name: return gr.update(choices=[])
605
- proj_path = os.path.join(VIRALS_DIR, proj_name)
606
- files = editor.list_editable_files(proj_path)
607
- return gr.update(choices=files, value=files[0] if files else None)
608
-
609
- editor_project_dropdown.change(update_file_list, inputs=editor_project_dropdown, outputs=editor_file_dropdown)
610
-
611
- def load_subs(proj_name, file_name):
612
- if not proj_name or not file_name:
613
- return [], None, i18n("Please select project and file.")
614
-
615
- full_path = os.path.join(VIRALS_DIR, proj_name, 'subs', file_name)
616
- data = editor.load_transcription_for_editor(full_path)
617
- return data, full_path, i18n("Loaded {} segments.").format(len(data))
618
-
619
- editor_load_btn.click(load_subs, inputs=[editor_project_dropdown, editor_file_dropdown], outputs=[subtitle_dataframe, current_json_path, editor_status])
620
-
621
- def save_subs(json_path, df):
622
- if not json_path: return i18n("No file loaded.")
623
- data_list = df.values.tolist() if hasattr(df, 'values') else df
624
- msg = editor.save_editor_changes(json_path, data_list)
625
- return msg
626
-
627
- editor_save_btn.click(save_subs, inputs=[current_json_path, subtitle_dataframe], outputs=editor_status)
628
-
629
- def render_single(json_path, use_custom, font_name, font_size, font_color, highlight_color,
630
- outline_color, outline_thickness, shadow_color, shadow_size,
631
- is_bold, is_italic, is_uppercase,
632
- h_size, w_block, gap, mode, under, strike, border_s,
633
- vertical_pos, alignment, remove_punc):
634
-
635
- if not json_path: return i18n("No file loaded.")
636
-
637
- subtitle_config_path = os.path.join(WORKING_DIR, "temp_subtitle_config.json")
638
-
639
- # Save config if custom subs enabled
640
- if use_custom:
641
- subtitle_config = {
642
- "font": font_name, "base_size": int(font_size),
643
- "base_color": convert_color_to_ass(font_color),
644
- "highlight_color": convert_color_to_ass(highlight_color),
645
- "outline_color": convert_color_to_ass(outline_color),
646
- "outline_thickness": outline_thickness,
647
- "shadow_color": convert_color_to_ass(shadow_color),
648
- "shadow_size": shadow_size, "vertical_position": vertical_pos,
649
- "alignment": alignment, "bold": 1 if is_bold else 0,
650
- "italic": 1 if is_italic else 0,
651
- "underline": 1 if under else 0, "strikeout": 1 if strike else 0,
652
- "border_style": border_s, "words_per_block": int(w_block),
653
- "gap_limit": gap, "mode": mode, "highlight_size": int(h_size),
654
- "uppercase": 1 if is_uppercase else 0,
655
- "remove_punctuation": remove_punc
656
- }
657
- try:
658
- with open(subtitle_config_path, "w", encoding="utf-8") as f:
659
- json.dump(subtitle_config, f, indent=4)
660
- except Exception: pass
661
- else:
662
- # Remove temp config if it exists to ensure defaults are used
663
- try:
664
- if os.path.exists(subtitle_config_path):
665
- os.remove(subtitle_config_path)
666
- except Exception: pass
667
-
668
- # We expect user to SAVE first, but we could auto-save.
669
- # For now assume saved.
670
- msg = editor.render_specific_video(json_path)
671
- return msg
672
-
673
- editor_render_single_btn.click(
674
- render_single,
675
- inputs=[current_json_path, use_custom_subs] + manual_inputs,
676
- outputs=editor_status
677
- )
678
-
679
- def render_all(proj_name, use_custom, font_name, font_size, font_color, highlight_color,
680
- outline_color, outline_thickness, shadow_color, shadow_size,
681
- is_bold, is_italic, is_uppercase,
682
- h_size, w_block, gap, mode, under, strike, border_s,
683
- vertical_pos, alignment, remove_punc):
684
- if not proj_name: return i18n("No project selected.")
685
-
686
- # Save config
687
- if use_custom:
688
- subtitle_config = {
689
- "font": font_name, "base_size": int(font_size),
690
- "base_color": convert_color_to_ass(font_color),
691
- "highlight_color": convert_color_to_ass(highlight_color),
692
- "outline_color": convert_color_to_ass(outline_color),
693
- "outline_thickness": outline_thickness,
694
- "shadow_color": convert_color_to_ass(shadow_color),
695
- "shadow_size": shadow_size, "vertical_position": vertical_pos,
696
- "alignment": alignment, "bold": 1 if is_bold else 0,
697
- "italic": 1 if is_italic else 0,
698
- "underline": 1 if under else 0, "strikeout": 1 if strike else 0,
699
- "border_style": border_s, "words_per_block": int(w_block),
700
- "gap_limit": gap, "mode": mode, "highlight_size": int(h_size),
701
- "uppercase": 1 if is_uppercase else 0,
702
- "remove_punctuation": remove_punc
703
- }
704
- subtitle_config_path = os.path.join(WORKING_DIR, "temp_subtitle_config.json")
705
- try:
706
- with open(subtitle_config_path, "w", encoding="utf-8") as f:
707
- json.dump(subtitle_config, f, indent=4)
708
- except Exception: pass
709
-
710
- proj_path = os.path.join(VIRALS_DIR, proj_name)
711
-
712
- # IMPORTANT: Pass the config file path to the command
713
- subtitle_config_path = os.path.join(WORKING_DIR, "temp_subtitle_config.json")
714
- cmd = [sys.executable, MAIN_SCRIPT_PATH, "--project-path", proj_path, "--workflow", "3", "--skip-prompts"]
715
-
716
- if use_custom and os.path.exists(subtitle_config_path):
717
- cmd.extend(["--subtitle-config", subtitle_config_path])
718
-
719
- try:
720
- subprocess.Popen(cmd, cwd=WORKING_DIR)
721
- return i18n("Render All started in background... Check terminal/logs.")
722
- except Exception as e:
723
- return i18n("Error starting render: {}").format(e)
724
-
725
- editor_render_all_btn.click(
726
- render_all,
727
- inputs=[editor_project_dropdown, use_custom_subs] + manual_inputs,
728
- outputs=editor_status
729
- )
730
-
731
-
732
- with gr.Tab(i18n("Library")):
733
- gr.Markdown(f"### {i18n('Existing Projects')}")
734
- with gr.Row():
735
- project_dropdown = gr.Dropdown(choices=library.get_existing_projects(), label=i18n("Select Project"), value=None)
736
- refresh_btn = gr.Button(i18n("Refresh List"))
737
- project_gallery_html = gr.HTML()
738
- refresh_btn.click(library.refresh_projects, outputs=project_dropdown)
739
- def on_select_project(proj_name): return library.generate_project_gallery(proj_name)
740
- project_dropdown.change(on_select_project, project_dropdown, project_gallery_html)
741
-
742
- gr.Markdown(f"""
743
- <hr>
744
- <div style='text-align: center; font-size: 0.9em; color: #777;'>
745
- <p>
746
- <strong>{i18n('Desenvolvido por Rafael Godoy')}</strong>
747
- <br>
748
- {i18n('Apoie o projeto, qualquer valor é bem-vindo:')}
749
- <a href='https://nubank.com.br/pagar/1ls6a4/0QpSSbWBSq' target='_blank'><strong>{i18n('Apoiar via PIX')}</strong></a>
750
- <br>
751
- {i18n('100% local • open source • sem assinatura')}
752
- </p>
753
- </div>
754
- """)
755
- if __name__ == "__main__":
756
- import webbrowser
757
- import threading
758
- import time
759
- import argparse
760
-
761
- parser = argparse.ArgumentParser()
762
- parser.add_argument("--colab", action="store_true", help="Run in Google Colab mode")
763
- args = parser.parse_args()
764
-
765
- if args.colab:
766
- print("Running in Colab mode. Generating public link with Static Mounts...")
767
- library.set_url_mode("fastapi")
768
-
769
- # Broaden allowed paths for Colab
770
- allowed_dirs = [VIRALS_DIR, WORKING_DIR, os.getcwd(), "."]
771
-
772
- # Explicitly set static paths
773
- try:
774
- gr.set_static_paths(paths=allowed_dirs)
775
- print(f"DEBUG: Registered static paths: {allowed_dirs}")
776
- except AttributeError:
777
- print("DEBUG: gr.set_static_paths not available")
778
-
779
- print(f"DEBUG: Allowed paths for Gradio: {allowed_dirs}")
780
-
781
- # Launch with prevent_thread_lock to allow mounting
782
- app, local_url, share_url = demo.queue().launch(
783
- share=True,
784
- allowed_paths=allowed_dirs,
785
- prevent_thread_lock=True
786
- )
787
-
788
- # Mount the VIRALS directory explicitly
789
- app.mount("/virals", StaticFiles(directory=VIRALS_DIR), name="virals")
790
- print(f"Mounted /virals to {VIRALS_DIR}")
791
-
792
- demo.block_thread()
793
- else:
794
- # Check environment
795
- is_windows = (os.name == 'nt')
796
-
797
- library.set_url_mode("fastapi")
798
- allowed_dirs = [VIRALS_DIR, WORKING_DIR, os.getcwd(), "."]
799
- try:
800
- gr.set_static_paths(paths=allowed_dirs)
801
- except AttributeError: pass
802
-
803
- from fastapi.responses import FileResponse
804
- from fastapi import BackgroundTasks
805
-
806
- # Helper to attach routes to any FastAPI app (whether created by Gradio or us)
807
- def attach_extra_routes(fastapi_app):
808
- fastapi_app.mount("/virals", StaticFiles(directory=VIRALS_DIR), name="virals")
809
-
810
- @fastapi_app.get("/export_xml_api")
811
- def export_xml_api(project: str, segment: int, background_tasks: BackgroundTasks, format: str = "premiere"):
812
- try:
813
- project_path = os.path.join(VIRALS_DIR, project)
814
- script_path = os.path.join(WORKING_DIR, "scripts", "export_xml.py")
815
- cmd = [sys.executable, script_path, "--project", project_path, "--segment", str(segment), "--format", format]
816
- subprocess.run(cmd, check=True)
817
- proj_name = os.path.basename(project_path)
818
- zip_filename = f"export_{proj_name}_seg{segment}.zip"
819
- file_path = os.path.join(project_path, zip_filename)
820
- if os.path.exists(file_path):
821
- return FileResponse(file_path, filename=zip_filename, media_type='application/zip')
822
- else:
823
- return {"error": f"File generation failed. Expected: {file_path}"}
824
- except Exception as e:
825
- return {"error": str(e)}
826
-
827
- print(f"Mounted /virals to {VIRALS_DIR}")
828
-
829
- if is_windows:
830
- print("Running in Windows environment (using Gradio launch for convenience).")
831
- # Windows: Use demo.launch() for convenience (auto-browser, etc)
832
- app, local_url, share_url = demo.queue().launch(
833
- share=False,
834
- allowed_paths=allowed_dirs,
835
- inbrowser=True,
836
- server_name="0.0.0.0",
837
- server_port=7860,
838
- prevent_thread_lock=True
839
- )
840
- attach_extra_routes(app)
841
- demo.block_thread()
842
- else:
843
- print("Running in Linux/Container environment (using Uvicorn for stability).")
844
- # Linux/HF: Use Uvicorn for explicit loop control
845
- app = FastAPI()
846
- attach_extra_routes(app)
847
- # Disable SSR to prevent Node proxying issues on HF Spaces
848
- app = gr.mount_gradio_app(app, demo.queue(), path="/", allowed_paths=allowed_dirs, ssr_mode=False)
849
- uvicorn.run(app, host="0.0.0.0", port=7860)