jcnok commited on
Commit
cc3baf4
·
verified ·
1 Parent(s): 0da041d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +31 -27
app.py CHANGED
@@ -5,7 +5,7 @@ FFmpeg API Service for n8n Video Automation.
5
  This FastAPI application provides a highly optimized, single endpoint
6
  to perform all FFmpeg/FFprobe operations for video creation.
7
 
8
- Version: 6.2.0 (Final - Corrected Crossfade Syntax)
9
  """
10
  # --- Core Imports ---
11
  import asyncio
@@ -14,12 +14,12 @@ import os
14
  import shutil
15
  import shlex
16
  import uuid
17
- from typing import List, Dict
18
 
19
  # --- Library Imports ---
20
  import aiohttp
21
- from fastapi import FastAPI, UploadFile, File, Form, HTTPException, Body
22
- from fastapi.responses import FileResponse, JSONResponse
23
  from starlette.background import BackgroundTask
24
 
25
  # ==============================================================================
@@ -28,7 +28,7 @@ from starlette.background import BackgroundTask
28
  app = FastAPI(
29
  title="FFmpeg as a Service for n8n",
30
  description="A robust API to create full videos with crossfade transitions.",
31
- version="6.2.0",
32
  )
33
 
34
  TEMP_DIR = "/tmp/ffmpeg_processing"
@@ -47,7 +47,6 @@ async def startup_event():
47
  def cleanup_directory(directory_path: str):
48
  """
49
  Safely removes a directory and its contents after processing.
50
- This is critical for preventing disk space exhaustion.
51
  """
52
  if os.path.exists(directory_path):
53
  shutil.rmtree(directory_path, ignore_errors=True)
@@ -56,7 +55,7 @@ def cleanup_directory(directory_path: str):
56
  async def run_subprocess(command: str) -> (str, str):
57
  """
58
  Executes a shell command asynchronously, capturing its output.
59
- Raises RuntimeError on failure, providing clear error feedback.
60
  """
61
  print(f"--- EXECUTING COMMAND ---\n{command}\n-------------------------")
62
  process = await asyncio.create_subprocess_shell(
@@ -68,19 +67,21 @@ async def run_subprocess(command: str) -> (str, str):
68
 
69
  if process.returncode != 0:
70
  error_message = stderr.decode(errors='ignore').strip()
71
- print(f"ERROR: Command failed with exit code {process.returncode}.\nStderr: {error_message}")
72
  raise RuntimeError(f"FFmpeg command failed: {error_message}")
73
 
74
  print("SUCCESS: Command executed.")
75
  return stdout.decode(errors='ignore'), stderr.decode(errors='ignore')
76
 
77
- # Função auxiliar para download de imagens
78
- async def download_asset(session: aiohttp.ClientSession, url: str, path: str):
79
- """Downloads a single asset (image or audio) from a URL to a local path."""
 
 
 
80
  async with session.get(url) as resp:
81
  if not resp.ok:
82
- # Levanta um erro que será capturado pelo try/except principal
83
- raise IOError(f"Failed to download asset from {url}, status: {resp.status}")
84
  with open(path, "wb") as f:
85
  f.write(await resp.read())
86
 
@@ -95,9 +96,6 @@ async def create_full_video_with_subtitles_v2(
95
  subtitle_file: UploadFile = File(...),
96
  output_filename: str = Form("final_video.mp4")
97
  ):
98
- """
99
- Orchestrates the entire video creation process with professional crossfade transitions.
100
- """
101
  unique_id = str(uuid.uuid4())
102
  temp_processing_dir = os.path.join(TEMP_DIR, unique_id)
103
  os.makedirs(temp_processing_dir)
@@ -110,21 +108,18 @@ async def create_full_video_with_subtitles_v2(
110
  print("--- STEP 1/6: ACQUIRING ASSETS ---")
111
  with open(subtitle_path, "wb") as buffer:
112
  shutil.copyfileobj(subtitle_file.file, buffer)
 
113
 
114
  image_urls = json.loads(image_urls_json)
115
  local_image_paths = []
116
 
117
  async with aiohttp.ClientSession(headers=BROWSER_HEADERS) as session:
118
- # Baixa áudio
119
- await download_asset(session, audio_url, audio_path)
120
- print("SUCCESS: Audio downloaded.")
121
-
122
- # Baixa imagens
123
  for i, url in enumerate(image_urls):
124
  local_path = os.path.join(temp_processing_dir, f"image_{i:02d}.jpg")
125
  await download_asset(session, url, local_path)
126
  local_image_paths.append(local_path)
127
- print(f"SUCCESS: {len(local_image_paths)} images downloaded.")
128
 
129
  # ETAPA 2: Obter Duração do Áudio
130
  print("--- STEP 2/6: GETTING AUDIO DURATION ---")
@@ -145,33 +140,43 @@ async def create_full_video_with_subtitles_v2(
145
 
146
  input_args = ""
147
  for path in local_image_paths:
 
148
  input_args += f"-loop 1 -t {image_duration} -i {shlex.quote(path)} "
149
 
150
  filter_complex_chain = ""
151
  last_stream = "0:v"
152
- offset = image_duration - transition_duration
153
-
 
154
  for i in range(num_images - 1):
155
  next_stream_idx = i + 1
156
  output_stream_name = f"v{next_stream_idx}"
 
 
 
 
 
 
 
157
  filter_part = f"[{last_stream}][{next_stream_idx}:v]xfade=transition=fade:duration={transition_duration}:offset={offset}[{output_stream_name}];"
158
  filter_complex_chain += filter_part
159
  last_stream = output_stream_name
160
 
161
  silent_video_path = os.path.join(temp_processing_dir, "silent_video_with_transitions.mp4")
162
 
163
- # Correção final e crucial da sintaxe do filter_complex
164
  final_filter_chain = f"{filter_complex_chain}[{last_stream}]format=yuv420p,fps=25[final_v]"
165
 
166
  cmd_video_transitions = (
167
  f"ffmpeg {input_args} "
168
  f"-filter_complex \"{final_filter_chain}\" "
169
- f"-map \"[final_v]\" -y {shlex.quote(silent_video_path)}"
170
  )
171
 
172
  await run_subprocess(cmd_video_transitions)
173
  print("SUCCESS: Video with transitions created.")
174
 
 
175
  # ETAPA 4: Combinar com áudio
176
  print("--- STEP 4/6: COMBINING WITH AUDIO ---")
177
  video_with_audio_path = os.path.join(temp_processing_dir, "video_with_audio.mp4")
@@ -193,7 +198,6 @@ async def create_full_video_with_subtitles_v2(
193
  await run_subprocess(cmd_subtitles)
194
  print("--- STEP 6/6: FINAL VIDEO CREATED SUCCESSFULLY ---")
195
 
196
- # ETAPA FINAL: Retorno e Limpeza
197
  cleanup_task = BackgroundTask(cleanup_directory, directory_path=temp_processing_dir)
198
  return FileResponse(
199
  path=final_output_path,
 
5
  This FastAPI application provides a highly optimized, single endpoint
6
  to perform all FFmpeg/FFprobe operations for video creation.
7
 
8
+ Version: 6.2.1 (Final - Including all helper functions)
9
  """
10
  # --- Core Imports ---
11
  import asyncio
 
14
  import shutil
15
  import shlex
16
  import uuid
17
+ from typing import Dict
18
 
19
  # --- Library Imports ---
20
  import aiohttp
21
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
22
+ from fastapi.responses import FileResponse
23
  from starlette.background import BackgroundTask
24
 
25
  # ==============================================================================
 
28
  app = FastAPI(
29
  title="FFmpeg as a Service for n8n",
30
  description="A robust API to create full videos with crossfade transitions.",
31
+ version="6.2.1",
32
  )
33
 
34
  TEMP_DIR = "/tmp/ffmpeg_processing"
 
47
  def cleanup_directory(directory_path: str):
48
  """
49
  Safely removes a directory and its contents after processing.
 
50
  """
51
  if os.path.exists(directory_path):
52
  shutil.rmtree(directory_path, ignore_errors=True)
 
55
  async def run_subprocess(command: str) -> (str, str):
56
  """
57
  Executes a shell command asynchronously, capturing its output.
58
+ Raises RuntimeError on failure.
59
  """
60
  print(f"--- EXECUTING COMMAND ---\n{command}\n-------------------------")
61
  process = await asyncio.create_subprocess_shell(
 
67
 
68
  if process.returncode != 0:
69
  error_message = stderr.decode(errors='ignore').strip()
70
+ print(f"ERROR: Command failed.\nStderr: {error_message}")
71
  raise RuntimeError(f"FFmpeg command failed: {error_message}")
72
 
73
  print("SUCCESS: Command executed.")
74
  return stdout.decode(errors='ignore'), stderr.decode(errors='ignore')
75
 
76
+ # >>>>> FUNÇÃO AUXILIAR QUE ESTAVA FALTANDO <<<<<
77
+ async def download_asset(session: aiohttp.ClientSession, url: str, path: str, asset_type: str = "image"):
78
+ """
79
+ Downloads a single asset from a URL to a local path.
80
+ """
81
+ print(f"Downloading {asset_type} from {url}...")
82
  async with session.get(url) as resp:
83
  if not resp.ok:
84
+ raise IOError(f"Failed to download {asset_type} from {url}, status: {resp.status}")
 
85
  with open(path, "wb") as f:
86
  f.write(await resp.read())
87
 
 
96
  subtitle_file: UploadFile = File(...),
97
  output_filename: str = Form("final_video.mp4")
98
  ):
 
 
 
99
  unique_id = str(uuid.uuid4())
100
  temp_processing_dir = os.path.join(TEMP_DIR, unique_id)
101
  os.makedirs(temp_processing_dir)
 
108
  print("--- STEP 1/6: ACQUIRING ASSETS ---")
109
  with open(subtitle_path, "wb") as buffer:
110
  shutil.copyfileobj(subtitle_file.file, buffer)
111
+ print("SUCCESS: Subtitle file saved.")
112
 
113
  image_urls = json.loads(image_urls_json)
114
  local_image_paths = []
115
 
116
  async with aiohttp.ClientSession(headers=BROWSER_HEADERS) as session:
117
+ await download_asset(session, audio_url, audio_path, "audio")
 
 
 
 
118
  for i, url in enumerate(image_urls):
119
  local_path = os.path.join(temp_processing_dir, f"image_{i:02d}.jpg")
120
  await download_asset(session, url, local_path)
121
  local_image_paths.append(local_path)
122
+ print(f"SUCCESS: All assets acquired.")
123
 
124
  # ETAPA 2: Obter Duração do Áudio
125
  print("--- STEP 2/6: GETTING AUDIO DURATION ---")
 
140
 
141
  input_args = ""
142
  for path in local_image_paths:
143
+ # -t agora define a duração de cada input de imagem, que é a duração por imagem
144
  input_args += f"-loop 1 -t {image_duration} -i {shlex.quote(path)} "
145
 
146
  filter_complex_chain = ""
147
  last_stream = "0:v"
148
+
149
+ # >>>>> CORREÇÃO PRINCIPAL AQUI <<<<<
150
+ # O offset agora é CUMULATIVO.
151
  for i in range(num_images - 1):
152
  next_stream_idx = i + 1
153
  output_stream_name = f"v{next_stream_idx}"
154
+
155
+ # O offset aumenta a cada iteração
156
+ # Offset da transição 1: (1 * image_duration) - transition_duration
157
+ # Offset da transição 2: (2 * image_duration) - (2 * transition_duration) ... etc
158
+ # Uma forma mais simples é calcular o offset com base no tempo de início da imagem
159
+ offset = i * (image_duration - transition_duration) + (image_duration - transition_duration)
160
+
161
  filter_part = f"[{last_stream}][{next_stream_idx}:v]xfade=transition=fade:duration={transition_duration}:offset={offset}[{output_stream_name}];"
162
  filter_complex_chain += filter_part
163
  last_stream = output_stream_name
164
 
165
  silent_video_path = os.path.join(temp_processing_dir, "silent_video_with_transitions.mp4")
166
 
167
+ # A sintaxe do filtro final permanece a mesma, agora alimentada pela cadeia correta
168
  final_filter_chain = f"{filter_complex_chain}[{last_stream}]format=yuv420p,fps=25[final_v]"
169
 
170
  cmd_video_transitions = (
171
  f"ffmpeg {input_args} "
172
  f"-filter_complex \"{final_filter_chain}\" "
173
+ f"-map \"[final_v]\" -an -y {shlex.quote(silent_video_path)}" # -an para garantir que não há áudio
174
  )
175
 
176
  await run_subprocess(cmd_video_transitions)
177
  print("SUCCESS: Video with transitions created.")
178
 
179
+ # O resto do fluxo continua como antes...
180
  # ETAPA 4: Combinar com áudio
181
  print("--- STEP 4/6: COMBINING WITH AUDIO ---")
182
  video_with_audio_path = os.path.join(temp_processing_dir, "video_with_audio.mp4")
 
198
  await run_subprocess(cmd_subtitles)
199
  print("--- STEP 6/6: FINAL VIDEO CREATED SUCCESSFULLY ---")
200
 
 
201
  cleanup_task = BackgroundTask(cleanup_directory, directory_path=temp_processing_dir)
202
  return FileResponse(
203
  path=final_output_path,