Spaces:
Sleeping
Sleeping
File size: 37,027 Bytes
80b326d f7598da 80b326d f7598da 80b326d 1934649 80b326d f7598da 80b326d f7598da 80b326d f7598da 80b326d f7598da 80b326d f7598da 80b326d f7598da 80b326d 1934649 80b326d 1934649 80b326d f7598da 80b326d |
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 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 |
import os
import sys
# Suppress unnecessary logs before importing heavy libs
os.environ["ORT_LOGGING_LEVEL"] = "3"
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3"
import warnings
warnings.filterwarnings("ignore")
import json
import shutil
import subprocess
import argparse
import time
from scripts import (
download_video,
transcribe_video,
create_viral_segments,
cut_segments,
edit_video,
transcribe_cuts,
adjust_subtitles,
burn_subtitles,
save_json,
organize_output,
translate_json,
)
from i18n.i18n import I18nAuto
# Inicializa sistema de tradução
i18n = I18nAuto()
#
# Configurações de Legenda (ASS Style)
# Cores no formato BGR (Blue-Green-Red) para o ASS
COLORS = {
"red": "0000FF", # Red
"yellow": "00FFFF", # Yellow
"green": "00FF00", # Green
"white": "FFFFFF", # White
"black": "000000", # Black
"grey": "808080", # Grey
}
def get_subtitle_config(config_path=None):
"""
Returns the subtitle configuration dictionary.
Can be expanded to load from a JSON/YAML file in the future.
"""
# Default Config
base_color_transparency = "00"
outline_transparency = "FF"
highlight_color_transparency = "00"
shadow_color_transparency = "00"
config = {
"font": "Montserrat-Regular",
"base_size": 30,
"base_color": f"&H{base_color_transparency}{COLORS['white']}&",
"highlight_size": 35,
"words_per_block": 3,
"gap_limit": 0.5,
"mode": 'highlight', # Options: 'no_highlight', 'word_by_word', 'highlight'
"highlight_color": f"&H{highlight_color_transparency}{COLORS['green']}&",
"vertical_position": 210, # 1=170(top), ... 4=60(default)
"alignment": 2, # 2=Center
"bold": 0,
"italic": 0,
"underline": 0,
"strikeout": 0,
"border_style": 2, # 1=outline, 3=box
"outline_thickness": 1.5,
"outline_color": f"&H{outline_transparency}{COLORS['grey']}&",
"shadow_size": 2,
"shadow_color": f"&H{shadow_color_transparency}{COLORS['black']}&",
"remove_punctuation": True,
}
if config_path and os.path.exists(config_path):
try:
with open(config_path, 'r', encoding='utf-8') as f:
loaded_config = json.load(f)
config.update(loaded_config)
print(i18n("Loaded subtitle config from {}").format(config_path))
except Exception as e:
print(i18n("Error loading subtitle config: {}. Using defaults.").format(e))
return config
def interactive_input_int(prompt_text):
"""Solicita um inteiro ao usuário via terminal."""
while True:
try:
value = int(input(i18n(prompt_text)))
if value > 0:
return value
print(i18n("\nError: Number must be greater than 0."))
except ValueError:
print(i18n("\nError: The value you entered is not an integer. Please try again."))
def main():
# Configuração de Argumentos via Linha de Comando (CLI)
parser = argparse.ArgumentParser(description="ViralCutter CLI")
parser.add_argument("--url", help="YouTube Video URL")
parser.add_argument("--segments", type=int, help="Number of segments to create")
parser.add_argument("--viral", action="store_true", help="Enable viral mode")
parser.add_argument("--themes", help="Comma-separated themes (if not viral mode)")
parser.add_argument("--burn-only", action="store_true", help="Skip processing and only burn subtitles")
parser.add_argument("--min-duration", type=int, default=15, help="Minimum segment duration (seconds)")
parser.add_argument("--max-duration", type=int, default=90, help="Maximum segment duration (seconds)")
parser.add_argument("--model", default="large-v3-turbo", help="Whisper model to use")
parser.add_argument("--ai-backend", choices=["manual", "gemini", "g4f", "local"], help="AI backend for viral analysis")
parser.add_argument("--api-key", help="Gemini API Key (required if ai-backend is gemini)")
parser.add_argument("--chunk-size", help="Override Chunk Size")
parser.add_argument("--ai-model-name", help="Override AI Model Name")
parser.add_argument("--project-path", help="Path to existing project folder (overrides URL/Latest)")
parser.add_argument("--workflow", choices=["1", "2", "3"], default="1", help="Workflow choice: 1=Full, 2=Cut Only, 3=Subtitles Only")
parser.add_argument("--face-model", choices=["insightface", "mediapipe"], default="insightface", help="Face detection model")
parser.add_argument("--face-mode", choices=["auto", "1", "2"], default="auto", help="Face tracking mode: auto, 1, 2")
parser.add_argument("--subtitle-config", help="Path to subtitle configuration JSON file")
parser.add_argument("--no-face-mode", choices=["padding", "zoom"], default="padding", help="Method to handle segments with no face detected: 'padding' (9:16 frame with black bars) or 'zoom' (Center Crop Zoom)")
parser.add_argument("--face-detect-interval", type=str, default="0.17,1.0", help="Face detection interval in seconds. Single value or 'interval_1face,interval_2face'")
parser.add_argument("--face-filter-threshold", type=float, default=0.35, help="Relative area threshold to ignore background faces (default: 0.35)")
parser.add_argument("--face-two-threshold", type=float, default=0.60, help="Relative area threshold to trigger 2-face mode (default: 0.60)")
parser.add_argument("--face-confidence-threshold", type=float, default=0.30, help="Face detection confidence threshold (0.0 - 1.0) (default: 0.30)")
parser.add_argument("--face-dead-zone", type=str, default="40", help="Camera movement dead zone in pixels (default: 40)") # str to support future "auto"
parser.add_argument("--focus-active-speaker", action="store_true", help="Enable experimental active speaker focus (InsightFace only)")
parser.add_argument("--active-speaker-mar", type=float, default=0.03, help="Mouth Aspect Ratio threshold for active speaker (0.0 - 1.0) (default: 0.03)")
parser.add_argument("--active-speaker-score-diff", type=float, default=1.5, help="Score difference to focus on active speaker (default: 1.5)")
parser.add_argument("--include-motion", action="store_true", help="Include motion (body/head movement) in activity score")
parser.add_argument("--active-speaker-motion-threshold", type=float, default=3.0, help="Motion deadzone in pixels (default: 3.0)")
parser.add_argument("--active-speaker-motion-sensitivity", type=float, default=0.05, help="Motion sensitivity multiplier (default: 0.05)")
parser.add_argument("--active-speaker-decay", type=float, default=2.0, help="Activity score decay rate (default: 2.0)")
parser.add_argument("--skip-prompts", action="store_true", help="Skip interactive prompts and use defaults/existing files")
parser.add_argument("--video-quality", choices=["best", "1080p", "720p", "480p"], default="best", help="Video download quality")
parser.add_argument("--skip-youtube-subs", action="store_true", help="Skip downloading YouTube subtitles")
parser.add_argument("--translate-target", help="Target language code for subtitle translation (e.g. 'pt', 'en').")
args = parser.parse_args()
# Workflow Logic
workflow_choice = args.workflow
# If Subtitles Only, checking project path
if workflow_choice == "3" and not args.project_path and not args.url and not args.skip_prompts:
# Prompt for project path or use latest if not provided?
pass # Will handle in main flow
# Modo Apenas Queimar Legenda (Legacy support, mapped to Workflow 3 internally if burn-only is set)
# Verifica o argumento CLI ou uma variável local hardcoded (para compatibilidade)
burn_only_mode = args.burn_only
if burn_only_mode:
print(i18n("Burn only mode activated. Switching to Workflow 3..."))
workflow_choice = "3"
# Obtenção de Inputs (CLI ou Interativo)
url = args.url
project_path_arg = args.project_path
input_video = None
# Se project_path for fornecido, ignoramos URL
if project_path_arg:
if os.path.exists(project_path_arg):
print(i18n("Using provided project path: {}").format(project_path_arg))
# Tentar achar o input.mp4 pra manter compatibilidade de variaveis, embora Workflow 3 não precise de download
possible_input = os.path.join(project_path_arg, "input.mp4")
if os.path.exists(possible_input):
input_video = possible_input
else:
# Se não tiver input.mp4, tudo bem para workflow 3, mas definimos um dummy para não quebrar logica
input_video = os.path.join(project_path_arg, "dummy_input.mp4")
# Se for workflow 3, não precisamos de URL
else:
print(i18n("Error: Provided project path does not exist."))
sys.exit(1)
# Se não temos URL via CLI nem Project Path, pedimos agora
if not url and not project_path_arg:
if args.skip_prompts:
print(i18n("No URL provided and skipping prompts. Trying to load latest project..."))
# Fallthrough to project loading logic
else:
user_input = input(i18n("Enter the YouTube video URL (or press Enter to use latest project): ")).strip()
if user_input:
url = user_input
if not url and not input_video:
# Usuário apertou Enter (Vazio) -> Tentar pegar último projeto
base_virals = "VIRALS"
if os.path.exists(base_virals):
subdirs = [os.path.join(base_virals, d) for d in os.listdir(base_virals) if os.path.isdir(os.path.join(base_virals, d))]
if subdirs:
latest_project = max(subdirs, key=os.path.getmtime)
detected_video = os.path.join(latest_project, "input.mp4")
if os.path.exists(detected_video):
input_video = detected_video
print(i18n("Using latest project: {}").format(latest_project))
else:
print(i18n("Latest project found but 'input.mp4' is missing."))
sys.exit(1)
else:
print(i18n("No existing projects found in VIRALS folder."))
sys.exit(1)
else:
print(i18n("VIRALS folder not found. Cannot load latest project."))
sys.exit(1)
# -------------------------------------------------------------------------
# Checagem Antecipada de Segmentos Virais (Para pular configurações se já existirem)
# -------------------------------------------------------------------------
viral_segments = None
project_folder_anticipated = None
if input_video:
# Se já temos o vídeo, podemos deduzir a pasta
project_folder_anticipated = os.path.dirname(input_video)
viral_segments_file = os.path.join(project_folder_anticipated, "viral_segments.txt")
if os.path.exists(viral_segments_file):
print(i18n("\nExisting viral segments found: {}").format(viral_segments_file))
if args.skip_prompts:
use_existing_json = 'yes'
else:
use_existing_json = input(i18n("Use existing viral segments? (yes/no) [default: yes]: ")).strip().lower()
if use_existing_json in ['', 'y', 'yes']:
try:
with open(viral_segments_file, 'r', encoding='utf-8') as f:
viral_segments = json.load(f)
print(i18n("Loaded existing viral segments. Skipping configuration prompts."))
if viral_segments and "segments" in viral_segments:
print(f"DEBUG: Loaded {len(viral_segments['segments'])} segments from file.")
else:
print("DEBUG: Loaded JSON but 'segments' key is missing or empty.")
except Exception as e:
print(i18n("Error loading JSON: {}.").format(e))
# Variaveis de config de IA (só necessárias se não tivermos os segmentos)
num_segments = None
viral_mode = False
themes = ""
ai_backend = "manual" # default
api_key = None
if not viral_segments:
num_segments = args.segments
if not num_segments:
if args.skip_prompts:
print(i18n("No segments count provided and skip-prompts is ON. Using default 3."))
num_segments = 3
else:
num_segments = interactive_input_int("Enter the number of viral segments to create: ")
viral_mode = args.viral
if not args.viral and not args.themes:
if args.skip_prompts:
print(i18n("Viral mode not set, defaulting to True."))
viral_mode = True
else:
response = input(i18n("Do you want viral mode? (yes/no): ")).lower()
viral_mode = response in ['yes', 'y']
themes = args.themes if args.themes else ""
if not viral_mode and not themes:
if not args.skip_prompts:
themes = input(i18n("Enter themes (comma-separated, leave blank if viral mode is True): "))
# Duration Config
print(i18n("\nCurrent duration settings: {}s - {}s").format(args.min_duration, args.max_duration))
if not args.skip_prompts:
change_dur = input(i18n("Change duration? (y/n) [default: n]: ")).strip().lower()
if change_dur in ['y', 'yes']:
try:
min_d = input(i18n("Minimum duration [{}]: ").format(args.min_duration)).strip()
if min_d: args.min_duration = int(min_d)
max_d = input(i18n("Maximum duration [{}]: ").format(args.max_duration)).strip()
if max_d: args.max_duration = int(max_d)
except ValueError:
print(i18n("Invalid number. Using previous values."))
# Load API Config
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'api_config.json')
api_config = {}
if os.path.exists(config_path):
try:
with open(config_path, 'r', encoding='utf-8') as f:
api_config = json.load(f)
except:
pass
# Seleção do Backend de IA
ai_backend = args.ai_backend
# Try to load backend from config if not in args
if not ai_backend and api_config.get("selected_api"):
ai_backend = api_config.get("selected_api")
print(i18n("Using AI Backend from config: {}").format(ai_backend))
if not ai_backend:
if args.skip_prompts:
print(i18n("No AI backend selected, defaulting to Manual."))
ai_backend = "manual"
else:
print("\n" + i18n("Select AI Backend for Viral Analysis:"))
print(i18n("1. Gemini API (Best / Recommended)"))
print(i18n("2. G4F (Free / Experimental)"))
print(i18n("3. Local (GGUF via llama.cpp)"))
print(i18n("4. Manual (Copy/Paste Prompt)"))
choice = input(i18n("Choose (1-4): ")).strip()
if choice == "1":
ai_backend = "gemini"
elif choice == "2":
ai_backend = "g4f"
elif choice == "3":
ai_backend = "local"
# Interactive model selection for local
# List models
models_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "models")
if not os.path.exists(models_dir): os.makedirs(models_dir)
models = [f for f in os.listdir(models_dir) if f.endswith(".gguf")]
if not models:
print(i18n("\nNo .gguf models found in 'models' directory."))
print(i18n("Please place a module file in: {}").format(models_dir))
print(i18n("Falling back to Manual..."))
ai_backend = "manual"
else:
print(i18n("\nAvailable Models:"))
for idx, m in enumerate(models):
print(f"{idx+1}. {m}")
try:
m_idx = int(input(i18n("Select Model (Number): "))) - 1
if 0 <= m_idx < len(models):
args.ai_model_name = models[m_idx] # Set global arg
else:
print(i18n("Invalid selection. Using first model."))
args.ai_model_name = models[0]
except:
print(i18n("Invalid input. Using first model."))
args.ai_model_name = models[0]
else:
ai_backend = "manual"
api_key = args.api_key
# Check config for API Key if using Gemini
if ai_backend == "gemini" and not api_key:
cfg_key = api_config.get("gemini", {}).get("api_key", "")
if cfg_key and cfg_key != "SUA_KEY_AQUI":
api_key = cfg_key
if ai_backend == "gemini" and not api_key:
if args.skip_prompts:
print(i18n("Gemini API key missing, but skip-prompts is ON. Might fail."))
else:
print(i18n("Gemini API Key not found in api_config.json or arguments."))
api_key = input(i18n("Enter your Gemini API Key: ")).strip()
# Workflow & Face Config Inputs
workflow_choice = args.workflow
face_model = args.face_model
face_mode = args.face_mode
# If args weren't provided and we are not skipping prompts, ask user
# Note: argparse defaults are set, so they "are provided" effectively.
# To truly detect "not provided", request default=None in argparse.
# But for "Simplified Mode", defaults are good.
# Advanced users use params.
# We will assume CLI defaults are what we want if skip_prompts is on.
# Logic for detection intervals (Moved out of interactive block to support CLI/WebUI)
detection_intervals = None
if args.face_detect_interval:
try:
parts = args.face_detect_interval.split(',')
if len(parts) == 1:
val = float(parts[0])
detection_intervals = {'1': val, '2': val}
elif len(parts) >= 2:
val1 = float(parts[0])
val2 = float(parts[1])
detection_intervals = {'1': val1, '2': val2}
except ValueError:
pass
if not args.burn_only and not args.skip_prompts:
# Interactive Face Config
print(i18n("\n--- Face Detection Settings ---"))
print(i18n("Current Face Model: {} | Mode: {}").format(face_model, face_mode))
if detection_intervals:
print(i18n("Custom detection intervals: {}").format(detection_intervals))
else:
print(i18n("Using dynamic intervals: 1s for 2-face, ~0.16s for 1-face."))
# Pipeline Execution
try:
# 1. Download & Project Setup
print(f"DEBUG: Checking input_video state. input_video={input_video}")
if not input_video:
if not url:
print(i18n("Error: No URL provided and no existing video selected."))
sys.exit(1)
print(i18n("Starting download..."))
download_subs = not args.skip_youtube_subs
download_result = download_video.download(url, download_subs=download_subs, quality=args.video_quality)
if isinstance(download_result, tuple):
input_video, project_folder = download_result
else:
input_video = download_result
project_folder = os.path.dirname(input_video)
print(f"DEBUG: Download finished. input_video={input_video}, project_folder={project_folder}")
else:
# Reuso de video existente
print("DEBUG: Using existing video logic.")
project_folder = os.path.dirname(input_video)
print(f"Project Folder: {project_folder}")
# 2. Transcribe
if workflow_choice == "3":
print(i18n("Workflow 3: Skipping Transcribe."))
# We assume transcription exists (SRT/JSON) or we won't need it for 'adjust_subtitles' if it uses 'subs/*.json' which are created by 'cut_segments'
# Actually 'adjust_subtitles' reads from 'project_folder/subs'.
# viral_segments = True # Removed to avoid overwritting dict loaded earlier
else:
print(i18n("Transcribing with model {}...").format(args.model))
# Se skip config, args.model é default
srt_file, tsv_file = transcribe_video.transcribe(input_video, args.model, project_folder=project_folder)
# 3. Create Viral Segments
if workflow_choice != "3":
# Se não carregamos 'viral_segments' lá em cima (ou se era download novo), checamos agora ou criamos
if not viral_segments:
# Checagem tardia para downloads novos que por acaso ja tenham json (Ex: URL repetida)
viral_segments_file_late = os.path.join(project_folder, "viral_segments.txt")
if os.path.exists(viral_segments_file_late):
print(i18n("Found existing viral segments file at {}").format(viral_segments_file_late))
if args.skip_prompts:
print(i18n("Skipping prompts enabled. Loading existing segments."))
try:
with open(viral_segments_file_late, 'r', encoding='utf-8') as f:
viral_segments = json.load(f)
except Exception as e:
print(i18n("Error loading existing JSON: {}. Proceeding to create new segments.").format(e))
else:
print(i18n("Loading existing viral segments found at {}").format(viral_segments_file_late))
try:
with open(viral_segments_file_late, 'r', encoding='utf-8') as f:
viral_segments = json.load(f)
except Exception as e:
print(i18n("Error loading existing JSON: {}.").format(e))
if not viral_segments:
print(i18n("Creating viral segments using {}...").format(ai_backend.upper()))
viral_segments = create_viral_segments.create(
num_segments,
viral_mode,
themes,
args.min_duration,
args.max_duration,
ai_mode=ai_backend,
api_key=api_key,
project_folder=project_folder,
chunk_size_arg=args.chunk_size,
model_name_arg=args.ai_model_name
)
if not viral_segments or not viral_segments.get("segments"):
print(i18n("Error: No viral segments were generated."))
print(i18n("Possible reasons: API error, Model not found, or empty response."))
print(i18n("Stopping execution."))
sys.exit(1)
save_json.save_viral_segments(viral_segments, project_folder=project_folder)
# 3.5. Fix Raw Segments (missing timestamps)
if workflow_choice != "3" and viral_segments and "segments" in viral_segments:
segs = viral_segments.get("segments", [])
if segs and len(segs) > 0:
# Check first segment for duration 0 but having start_time_ref or just check duration
first = segs[0]
# If duration is effectively 0 and we have a ref tag (or even if we dont, we cant cut 0s video)
# We assume if duration is 0, it is raw.
if first.get("duration", 0) == 0:
print(i18n("Detected raw AI segments without timestamps (Duration 0). Running alignment..."))
try:
# Load transcript
transcript = create_viral_segments.load_transcript(project_folder)
# Process (Align)
# Use None for output_count to keep all found segments
viral_segments = create_viral_segments.process_segments(
segs,
transcript,
args.min_duration,
args.max_duration,
output_count=None
)
save_json.save_viral_segments(viral_segments, project_folder=project_folder)
print(i18n("Segments aligned and saved."))
except Exception as e:
print(i18n("Failed to align raw segments: {}").format(e))
# If alignment fails, it might crash later, but we tried.
# 4. Cut Segments
# Se workflow for 3, pulamos corte
if workflow_choice == "3":
print(i18n("Workflow 3 (Subtitles Only): Skipping Cut and Edit."))
# Deduzir cuts folder apenas para log
cuts_folder = os.path.join(project_folder, "cuts")
else:
cuts_folder = os.path.join(project_folder, "cuts")
skip_cutting = False
if os.path.exists(cuts_folder) and os.listdir(cuts_folder):
print(i18n("\nExisting cuts found in: {}").format(cuts_folder))
if args.skip_prompts:
cut_again_resp = 'no'
else:
cut_again_resp = input(i18n("Cuts already exist. Cut again? (yes/no) [default: no]: ")).strip().lower()
# Default is no (skip) if they just press enter or say no
if cut_again_resp not in ['y', 'yes']:
skip_cutting = True
if skip_cutting:
print(i18n("Skipping Video Rendering (using existing cuts), but updating Subtitle JSONs..."))
else:
print(i18n("Cutting segments..."))
cut_segments.cut(viral_segments, project_folder=project_folder, skip_video=skip_cutting)
# 5. Workflow Check
if workflow_choice == "2":
print(i18n("Cut Only selected. Skipping Face Crop and Subtitles."))
print(i18n(f"Process completed! Check your results in: {project_folder}"))
sys.exit(0)
# 5. Edit Video (Face Crop)
if workflow_choice != "3":
print(i18n("Editing video with {} (Mode: {})...").format(face_model, face_mode))
# Parse dead zone safely
try:
dead_zone_val = float(args.face_dead_zone)
except:
dead_zone_val = 40.0
edit_video.edit(
project_folder=project_folder,
face_model=face_model,
face_mode=face_mode,
detection_period=detection_intervals,
filter_threshold=args.face_filter_threshold,
two_face_threshold=args.face_two_threshold,
confidence_threshold=args.face_confidence_threshold,
dead_zone=dead_zone_val,
focus_active_speaker=args.focus_active_speaker,
active_speaker_mar=args.active_speaker_mar,
active_speaker_score_diff=args.active_speaker_score_diff,
include_motion=args.include_motion,
active_speaker_motion_deadzone=args.active_speaker_motion_threshold,
active_speaker_motion_sensitivity=args.active_speaker_motion_sensitivity,
active_speaker_decay=args.active_speaker_decay,
segments_data=viral_segments.get("segments", []) if viral_segments else None,
no_face_mode=args.no_face_mode
)
else:
print(i18n("Workflow 3: Skipping Face Crop."))
# Rename existing files if viral_segments available (since edit_video didn't run)
if viral_segments and "segments" in viral_segments:
segments_data = viral_segments.get("segments", [])
final_folder = os.path.join(project_folder, "final")
subs_folder = os.path.join(project_folder, "subs")
print(i18n("Renaming existing files with titles..."))
for idx, segment in enumerate(segments_data):
title = segment.get("title", f"Segment_{idx}")
safe_title = "".join([c for c in title if c.isalnum() or c in " _-"]).strip()
safe_title = safe_title.replace(" ", "_")[:60]
new_base_name = f"{idx:03d}_{safe_title}"
# 1. MP4
old_mp4_name = f"final-output{idx:03d}_processed.mp4"
old_mp4_path = os.path.join(final_folder, old_mp4_name)
new_mp4_path = os.path.join(final_folder, f"{new_base_name}.mp4")
if os.path.exists(old_mp4_path) and not os.path.exists(new_mp4_path):
os.rename(old_mp4_path, new_mp4_path)
print(f"Renamed (Workflow 3): {old_mp4_name} -> {new_base_name}.mp4")
# 2. JSON Sub
old_json_name = f"final-output{idx:03d}_processed.json"
old_json_path = os.path.join(subs_folder, old_json_name)
new_json_path = os.path.join(subs_folder, f"{new_base_name}_processed.json")
if os.path.exists(old_json_path) and not os.path.exists(new_json_path):
os.rename(old_json_path, new_json_path)
print(f"Renamed (Workflow 3): {old_json_name} -> {new_base_name}_processed.json")
# 3. Timeline
old_tl_name = f"temp_video_no_audio_{idx}_timeline.json"
old_tl_path = os.path.join(final_folder, old_tl_name)
new_tl_path = os.path.join(final_folder, f"{new_base_name}_timeline.json")
if os.path.exists(old_tl_path) and not os.path.exists(new_tl_path):
os.rename(old_tl_path, new_tl_path)
print(f"Renamed (Workflow 3): {old_tl_name} -> {new_base_name}_timeline.json")
# 6. Subtitles
burn_subtitles_option = True
if burn_subtitles_option:
print(i18n("Processing subtitles..."))
# transcribe_cuts removido: JSON de legenda já é gerado no corte
# transcribe_cuts.transcribe(project_folder=project_folder)
# --- Translation Integration ---
if args.translate_target and args.translate_target.lower() != "none":
print(i18n("Translating subtitles to: {}").format(args.translate_target))
import asyncio
try:
asyncio.run(translate_json.translate_project_subs(project_folder, args.translate_target))
except Exception as e:
print(i18n("Translation failed: {}").format(e))
# -------------------------------
sub_config = get_subtitle_config(args.subtitle_config)
# Passa o dicionário desempacotado como argumentos, mais o project_folder
try:
adjust_subtitles.adjust(project_folder=project_folder, **sub_config)
burn_subtitles.burn(project_folder=project_folder)
except FileNotFoundError as fnf_error:
print(i18n("\n[ERROR] Subtitle processing failed: {}").format(str(fnf_error)))
print(i18n("Tip: If you are using Workflow 3 (Subtitles Only), ensure the 'subs' folder exists and contains valid JSON files."))
sys.exit(1)
except Exception as e:
print(i18n("\n[ERROR] Unexpected error during subtitle processing: {}").format(str(e)))
raise e
else:
print(i18n("Subtitle burning skipped."))
# Organização Final (Opcional, pois agora já está tudo em project_folder)
# organize_output.organize(project_folder=project_folder)
# --- Save Processing Configuration ---
try:
# Determine AI Model used
used_ai_model = args.ai_model_name
if not used_ai_model and ai_backend != "manual":
if ai_backend == "gemini":
used_ai_model = api_config.get("gemini", {}).get("model", "default")
elif ai_backend == "g4f":
used_ai_model = api_config.get("g4f", {}).get("model", "default")
# Ensure sub_config exists
current_sub_config = sub_config if 'sub_config' in locals() else get_subtitle_config(args.subtitle_config)
final_config = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"workflow": workflow_choice,
"ai_config": {
"backend": ai_backend,
"model_name": used_ai_model,
"viral_mode": viral_mode,
"themes": themes,
"num_segments": num_segments,
"chunk_size": args.chunk_size
},
"face_config": {
"model": face_model,
"mode": face_mode,
"detect_interval": args.face_detect_interval,
"filter_threshold": args.face_filter_threshold,
"two_face_threshold": args.face_two_threshold,
"confidence_threshold": args.face_confidence_threshold,
"dead_zone": args.face_dead_zone,
"focus_active_speaker": args.focus_active_speaker,
"active_speaker_mar": args.active_speaker_mar,
"active_speaker_score_diff": args.active_speaker_score_diff,
"include_motion": args.include_motion
},
"video_config": {
"min_duration": args.min_duration,
"max_duration": args.max_duration,
"whisper_model": args.model
},
"subtitle_config": current_sub_config
}
config_save_path = os.path.join(project_folder, "process_config.json")
with open(config_save_path, "w", encoding="utf-8") as f:
json.dump(final_config, f, indent=4, ensure_ascii=False)
print(i18n("Configuration saved to: {}").format(config_save_path))
except Exception as e:
print(i18n("Error saving configuration JSON: {}").format(e))
# -------------------------------------
print(i18n("Process completed! Check your results in: {}").format(project_folder))
except Exception as e:
print(i18n("\nAn error occurred: {}").format(str(e)))
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()
|