jcnok commited on
Commit
f8ef0a4
·
verified ·
1 Parent(s): fddafcc

Update app.py

Browse files

Nova tentativa de unificar tudo

Files changed (1) hide show
  1. app.py +59 -46
app.py CHANGED
@@ -1,11 +1,13 @@
1
  # -*- coding: utf-8 -*-
 
2
  """
3
- FFmpeg API Service for n8n Video Automation.
4
 
5
- This FastAPI application provides a single, highly-optimized endpoint
6
- to orchestrate a full video production pipeline from raw assets.
 
7
 
8
- Version: 9.0.0 (Ultimate - All-In-One Endpoint)
9
  """
10
  # --- Core Imports ---
11
  import asyncio
@@ -27,9 +29,9 @@ from starlette.background import BackgroundTask
27
  # 1. APPLICATION CONFIGURATION & SETUP
28
  # ==============================================================================
29
  app = FastAPI(
30
- title="FFmpeg as a Service - All-in-One Video Generator",
31
  description="A single endpoint to create full videos with music, transitions, and subtitles.",
32
- version="8.0.0",
33
  )
34
 
35
  TEMP_DIR = "/tmp/ffmpeg_processing"
@@ -49,28 +51,30 @@ def cleanup_directory(directory_path: str):
49
  shutil.rmtree(directory_path, ignore_errors=True)
50
  print(f"--- CLEANUP: Successfully removed {directory_path} ---")
51
 
52
- async def run_subprocess(command: str) -> (str, str):
53
  print(f"--- EXECUTING COMMAND ---\n{command}\n-------------------------")
54
  process = await asyncio.create_subprocess_shell(
55
  command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
56
  )
57
  stdout, stderr = await process.communicate()
 
58
  if process.returncode != 0:
59
  error_message = stderr.decode(errors='ignore').strip()
60
  print(f"ERROR: Command failed.\nStderr: {error_message}")
61
  raise RuntimeError(f"FFmpeg command failed: {error_message}")
 
62
  print("SUCCESS: Command executed.")
63
- return stdout.decode(errors='ignore'), stderr.decode(errors='ignore')
64
 
65
  async def download_asset(session: aiohttp.ClientSession, url: str, path: str, asset_type: str):
66
  print(f"Downloading {asset_type} from {url}...")
67
  async with session.get(url) as resp:
68
- resp.raise_for_status()
69
  with open(path, "wb") as f:
70
  f.write(await resp.read())
71
 
72
  # ==============================================================================
73
- # 3. THE ALL-IN-ONE ENDPOINT
74
  # ==============================================================================
75
 
76
  @app.post("/generate-complete-video/")
@@ -91,7 +95,6 @@ async def generate_complete_video(
91
  narration_path = os.path.join(temp_dir, "narration.mp3")
92
  subtitle_path = os.path.join(temp_dir, "subtitle.ass")
93
 
94
- # Salva legenda e músicas
95
  with open(subtitle_path, "wb") as f: shutil.copyfileobj(subtitle_file.file, f)
96
 
97
  local_music_paths = []
@@ -100,7 +103,6 @@ async def generate_complete_video(
100
  with open(path, "wb") as f: shutil.copyfileobj(music_file.file, f)
101
  local_music_paths.append(path)
102
 
103
- # Baixa narração e imagens
104
  image_urls = json.loads(image_urls_json)
105
  local_image_paths = []
106
  async with aiohttp.ClientSession(headers=BROWSER_HEADERS) as session:
@@ -113,17 +115,16 @@ async def generate_complete_video(
113
 
114
  # --- ETAPA 2: PROCESSAR MÚSICA DE FUNDO ---
115
  print("--- STEP 2/8: PROCESSING BACKGROUND MUSIC ---")
116
- combined_music_path = os.path.join(temp_dir, "background_music.mp3")
117
  if len(local_music_paths) > 1:
118
  inputs = "".join([f"-i {shlex.quote(p)} " for p in local_music_paths])
119
- filter_complex = "".join([f"[{i}:a]" for i in range(len(local_music_paths))]) + f"concat=n={len(local_music_paths)}:v=0:a=1[a]"
120
  cmd_concat_music = f"ffmpeg {inputs} -filter_complex \"{filter_complex}\" -map \"[a]\" -y {shlex.quote(combined_music_path)}"
121
  await run_subprocess(cmd_concat_music)
122
  else:
123
  shutil.copy(local_music_paths[0], combined_music_path)
124
 
125
- # Ajusta volume
126
- low_volume_music_path = os.path.join(temp_dir, "background_music_low.mp3")
127
  cmd_volume = f"ffmpeg -i {shlex.quote(combined_music_path)} -filter:a \"volume={music_volume}\" -y {shlex.quote(low_volume_music_path)}"
128
  await run_subprocess(cmd_volume)
129
  print("SUCCESS: Background music processed.")
@@ -131,63 +132,75 @@ async def generate_complete_video(
131
  # --- ETAPA 3: ANALISAR NARRAÇÃO ---
132
  print("--- STEP 3/8: GETTING NARRATION DURATION ---")
133
  cmd_ffprobe = f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {shlex.quote(narration_path)}"
134
- stdout, _ = await run_subprocess(cmd_ffprobe)
135
- total_duration = float(stdout.strip())
136
  print(f"SUCCESS: Narration duration is {total_duration} seconds.")
137
 
138
  # --- ETAPA 4: CRIAR VÍDEO COM TRANSIÇÕES ---
139
  print("--- STEP 4/8: CREATING VIDEO WITH TRANSITIONS ---")
 
 
 
 
 
 
 
140
  transition_duration = 1.0
141
  num_images = len(local_image_paths)
142
- display_duration = (total_duration - (num_images - 1) * transition_duration) / num_images if num_images > 1 else total_duration
143
- if display_duration <= 0: raise ValueError("Audio too short for transitions.")
 
 
 
144
 
 
 
145
  input_args = ""
146
  for path in local_image_paths:
147
- input_args += f"-loop 1 -t {display_duration + transition_duration} -i {shlex.quote(path)} "
148
-
149
- filter_complex = ""
 
150
  last_stream = "0:v"
151
  for i in range(num_images - 1):
152
- offset = (i + 1) * display_duration + i * transition_duration
153
- filter_complex += f"[{last_stream}][{i+1}:v]xfade=transition=fade:duration={transition_duration}:offset={offset}[v{i+1}];"
154
  last_stream = f"v{i+1}"
155
-
156
- final_filter = f"{filter_complex}[{last_stream}]format=yuv420p,fps=25[final_v]"
157
- silent_video_path = os.path.join(temp_dir, "silent_video.mp4")
158
- cmd_transitions = f"ffmpeg {input_args} -filter_complex \"{final_filter}\" -map \"[final_v]\" -an -y {shlex.quote(silent_video_path)}"
159
  await run_subprocess(cmd_transitions)
160
- print("SUCCESS: Video with transitions created.")
161
-
162
- # --- ETAPA 5: MIXAR TODOS OS ÁUDIOS ---
163
- print("--- STEP 5/8: MIXING ALL AUDIO TRACKS ---")
164
- mixed_audio_path = os.path.join(temp_dir, "mixed_audio.mp3")
165
  cmd_mix_audio = (f"ffmpeg -i {shlex.quote(narration_path)} -i {shlex.quote(low_volume_music_path)} "
166
  f"-filter_complex \"[0:a][1:a]amix=inputs=2:duration=first:dropout_transition=3\" -y {shlex.quote(mixed_audio_path)}")
167
  await run_subprocess(cmd_mix_audio)
168
  print("SUCCESS: Audio tracks mixed.")
169
 
170
- # --- ETAPA 6: COMBINAR VÍDEO COM ÁUDIO MIXADO ---
171
- print("--- STEP 6/8: COMBINING VIDEO WITH FINAL AUDIO ---")
172
- video_with_audio_path = os.path.join(temp_dir, "video_with_audio.mp4")
173
- cmd_combine = (f"ffmpeg -i {shlex.quote(silent_video_path)} -i {shlex.quote(mixed_audio_path)} "
174
- f"-c:v copy -c:a aac -shortest -y {shlex.quote(video_with_audio_path)}")
175
  await run_subprocess(cmd_combine)
 
176
 
177
- # --- ETAPA 7: ADICIONAR LEGENDAS ---
178
  print("--- STEP 7/8: BURNING SUBTITLES ---")
179
- final_video_path = os.path.join(temp_dir, "final_video.mp4")
180
  escaped_subtitle_path = subtitle_path.replace("'", "'\\''")
181
- cmd_subtitles = (f"ffmpeg -i {shlex.quote(video_with_audio_path)} -vf \"ass='{escaped_subtitle_path}'\" "
182
  f"-c:v libx264 -preset ultrafast -c:a copy -y {shlex.quote(final_video_path)}")
183
  await run_subprocess(cmd_subtitles)
184
-
 
185
  # --- ETAPA 8: RETORNAR VÍDEO E LIMPAR ---
186
  print("--- STEP 8/8: FINAL VIDEO CREATED. PREPARING RESPONSE. ---")
187
  cleanup_task = BackgroundTask(cleanup_directory, directory_path=temp_dir)
188
- return FileResponse(
189
- path=final_video_path, filename="video_completo.mp4", media_type="video/mp4", background=cleanup_task
190
- )
191
 
192
  except Exception as e:
193
  cleanup_directory(directory_path=temp_dir)
 
1
  # -*- coding: utf-8 -*-
2
+ # -*- coding: utf-8 -*-
3
  """
4
+ FFmpeg API Service for n8n Video Automation
5
 
6
+ This FastAPI application provides a single, all-in-one endpoint to generate a
7
+ complete video with narration, background music, transitions, and subtitles,
8
+ by orchestrating a sequence of FFmpeg operations.
9
 
10
+ Version: 9.0.0 (Unified & Production-Ready)
11
  """
12
  # --- Core Imports ---
13
  import asyncio
 
29
  # 1. APPLICATION CONFIGURATION & SETUP
30
  # ==============================================================================
31
  app = FastAPI(
32
+ title="Vid Automator API",
33
  description="A single endpoint to create full videos with music, transitions, and subtitles.",
34
+ version="9.0.0",
35
  )
36
 
37
  TEMP_DIR = "/tmp/ffmpeg_processing"
 
51
  shutil.rmtree(directory_path, ignore_errors=True)
52
  print(f"--- CLEANUP: Successfully removed {directory_path} ---")
53
 
54
+ async def run_subprocess(command: str) -> str:
55
  print(f"--- EXECUTING COMMAND ---\n{command}\n-------------------------")
56
  process = await asyncio.create_subprocess_shell(
57
  command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
58
  )
59
  stdout, stderr = await process.communicate()
60
+
61
  if process.returncode != 0:
62
  error_message = stderr.decode(errors='ignore').strip()
63
  print(f"ERROR: Command failed.\nStderr: {error_message}")
64
  raise RuntimeError(f"FFmpeg command failed: {error_message}")
65
+
66
  print("SUCCESS: Command executed.")
67
+ return stdout.decode(errors='ignore').strip()
68
 
69
  async def download_asset(session: aiohttp.ClientSession, url: str, path: str, asset_type: str):
70
  print(f"Downloading {asset_type} from {url}...")
71
  async with session.get(url) as resp:
72
+ resp.raise_for_status(f"Failed to download {asset_type} from {url}")
73
  with open(path, "wb") as f:
74
  f.write(await resp.read())
75
 
76
  # ==============================================================================
77
+ # 3. THE ALL-IN-ONE ENDPOINT (UNIFIED LOGIC)
78
  # ==============================================================================
79
 
80
  @app.post("/generate-complete-video/")
 
95
  narration_path = os.path.join(temp_dir, "narration.mp3")
96
  subtitle_path = os.path.join(temp_dir, "subtitle.ass")
97
 
 
98
  with open(subtitle_path, "wb") as f: shutil.copyfileobj(subtitle_file.file, f)
99
 
100
  local_music_paths = []
 
103
  with open(path, "wb") as f: shutil.copyfileobj(music_file.file, f)
104
  local_music_paths.append(path)
105
 
 
106
  image_urls = json.loads(image_urls_json)
107
  local_image_paths = []
108
  async with aiohttp.ClientSession(headers=BROWSER_HEADERS) as session:
 
115
 
116
  # --- ETAPA 2: PROCESSAR MÚSICA DE FUNDO ---
117
  print("--- STEP 2/8: PROCESSING BACKGROUND MUSIC ---")
118
+ combined_music_path = os.path.join(temp_dir, "background_music_raw.mp3")
119
  if len(local_music_paths) > 1:
120
  inputs = "".join([f"-i {shlex.quote(p)} " for p in local_music_paths])
121
+ filter_complex = "".join([f"[{i}:a:0]" for i in range(len(local_music_paths))]) + f"concat=n={len(local_music_paths)}:v=0:a=1[a]"
122
  cmd_concat_music = f"ffmpeg {inputs} -filter_complex \"{filter_complex}\" -map \"[a]\" -y {shlex.quote(combined_music_path)}"
123
  await run_subprocess(cmd_concat_music)
124
  else:
125
  shutil.copy(local_music_paths[0], combined_music_path)
126
 
127
+ low_volume_music_path = os.path.join(temp_dir, "background_music.mp3")
 
128
  cmd_volume = f"ffmpeg -i {shlex.quote(combined_music_path)} -filter:a \"volume={music_volume}\" -y {shlex.quote(low_volume_music_path)}"
129
  await run_subprocess(cmd_volume)
130
  print("SUCCESS: Background music processed.")
 
132
  # --- ETAPA 3: ANALISAR NARRAÇÃO ---
133
  print("--- STEP 3/8: GETTING NARRATION DURATION ---")
134
  cmd_ffprobe = f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {shlex.quote(narration_path)}"
135
+ stdout = await run_subprocess(cmd_ffprobe)
136
+ total_duration = float(stdout)
137
  print(f"SUCCESS: Narration duration is {total_duration} seconds.")
138
 
139
  # --- ETAPA 4: CRIAR VÍDEO COM TRANSIÇÕES ---
140
  print("--- STEP 4/8: CREATING VIDEO WITH TRANSITIONS ---")
141
+ # Esta lógica está correta, mas a matemática do tempo precisa ser perfeita.
142
+ video_with_transitions_path = os.path.join(temp_dir, "video_with_transitions.mp4")
143
+ # (Lógica de transições, combinação e legendas será unificada abaixo)
144
+
145
+ # --- REFORMULAÇÃO DAS ETAPAS 4, 5, 6, 7 PARA MÁXIMA ROBUSTEZ ---
146
+
147
+ # ETAPA 4 (Revisada): Criar slideshow silencioso com transições
148
  transition_duration = 1.0
149
  num_images = len(local_image_paths)
150
+ if num_images <= 1: raise ValueError("Need at least 2 images for transitions.")
151
+
152
+ # Fórmula correta para a duração de cada "cena" (imagem estática + transição)
153
+ total_display_time = total_duration - transition_duration
154
+ display_duration_per_image = total_display_time / num_images
155
 
156
+ if display_duration_per_image <= 0: raise ValueError("Audio too short for this many images/transitions.")
157
+
158
  input_args = ""
159
  for path in local_image_paths:
160
+ # Duração total de cada input de imagem
161
+ input_args += f"-loop 1 -t {display_duration_per_image + transition_duration} -i {shlex.quote(path)} "
162
+
163
+ filter_chain = ""
164
  last_stream = "0:v"
165
  for i in range(num_images - 1):
166
+ offset = (i + 1) * display_duration_per_image
167
+ filter_chain += f"[{last_stream}][{i+1}:v]xfade=transition=fade:duration={transition_duration}:offset={offset}[v{i+1}];"
168
  last_stream = f"v{i+1}"
169
+
170
+ final_video_filter = f"{filter_chain}[{last_stream}]format=yuv420p,fps=25[final_v]"
171
+ cmd_transitions = f"ffmpeg {input_args} -filter_complex \"{final_video_filter}\" -map \"[final_v]\" -an -y {shlex.quote(video_with_transitions_path)}"
 
172
  await run_subprocess(cmd_transitions)
173
+ print("SUCCESS: Silent video with transitions created.")
174
+
175
+ # ETAPA 5 (Revisada): Mixar narração e música de fundo
176
+ print("--- STEP 5/8: MIXING AUDIO TRACKS ---")
177
+ mixed_audio_path = os.path.join(temp_dir, "final_audio_mix.mp3")
178
  cmd_mix_audio = (f"ffmpeg -i {shlex.quote(narration_path)} -i {shlex.quote(low_volume_music_path)} "
179
  f"-filter_complex \"[0:a][1:a]amix=inputs=2:duration=first:dropout_transition=3\" -y {shlex.quote(mixed_audio_path)}")
180
  await run_subprocess(cmd_mix_audio)
181
  print("SUCCESS: Audio tracks mixed.")
182
 
183
+ # ETAPA 6 (Revisada): Combinar o vídeo (com transições) e o áudio final (mixado)
184
+ print("--- STEP 6/8: COMBINING VIDEO AND FINAL AUDIO ---")
185
+ video_with_final_audio_path = os.path.join(temp_dir, "video_with_audio.mp4")
186
+ cmd_combine = (f"ffmpeg -i {shlex.quote(video_with_transitions_path)} -i {shlex.quote(mixed_audio_path)} "
187
+ f"-c:v copy -c:a aac -shortest -y {shlex.quote(video_with_final_audio_path)}")
188
  await run_subprocess(cmd_combine)
189
+ print("SUCCESS: Video and final audio combined.")
190
 
191
+ # ETAPA 7 (Revisada): Adicionar Legendas ao vídeo final
192
  print("--- STEP 7/8: BURNING SUBTITLES ---")
193
+ final_video_path = os.path.join(temp_dir, "final_video_com_legenda.mp4")
194
  escaped_subtitle_path = subtitle_path.replace("'", "'\\''")
195
+ cmd_subtitles = (f"ffmpeg -i {shlex.quote(video_with_final_audio_path)} -vf \"ass='{escaped_subtitle_path}'\" "
196
  f"-c:v libx264 -preset ultrafast -c:a copy -y {shlex.quote(final_video_path)}")
197
  await run_subprocess(cmd_subtitles)
198
+ print("SUCCESS: Subtitles burned into final video.")
199
+
200
  # --- ETAPA 8: RETORNAR VÍDEO E LIMPAR ---
201
  print("--- STEP 8/8: FINAL VIDEO CREATED. PREPARING RESPONSE. ---")
202
  cleanup_task = BackgroundTask(cleanup_directory, directory_path=temp_dir)
203
+ return FileResponse(path=final_video_path, filename="video_completo.mp4", media_type="video/mp4", background=cleanup_task)
 
 
204
 
205
  except Exception as e:
206
  cleanup_directory(directory_path=temp_dir)