habulaj commited on
Commit
7016ccf
·
verified ·
1 Parent(s): c8c4355

Delete routers/video.py

Browse files
Files changed (1) hide show
  1. routers/video.py +0 -396
routers/video.py DELETED
@@ -1,396 +0,0 @@
1
- from fastapi import APIRouter, Query, HTTPException
2
- from fastapi.responses import StreamingResponse
3
- from moviepy.editor import VideoFileClip, CompositeVideoClip, ColorClip, ImageClip, TextClip
4
- from moviepy.video.VideoClip import VideoClip
5
- from moviepy.video.fx.all import resize
6
- from io import BytesIO
7
- import tempfile
8
- import requests
9
- import os
10
- import numpy as np
11
- from PIL import Image, ImageDraw, ImageFont
12
- import gc
13
- import re
14
- from typing import List, Tuple, Optional
15
-
16
- router = APIRouter()
17
-
18
- def download_file(url: str, suffix: str = ".mp4") -> str:
19
- """Download genérico para vídeos e arquivos SRT"""
20
- print(f"Tentando baixar arquivo de: {url}")
21
- headers = {
22
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
23
- 'Accept': '*/*',
24
- 'Accept-Language': 'en-US,en;q=0.5',
25
- 'Accept-Encoding': 'gzip, deflate',
26
- 'Connection': 'keep-alive',
27
- 'Upgrade-Insecure-Requests': '1',
28
- }
29
-
30
- try:
31
- response = requests.get(url, headers=headers, stream=True, timeout=30)
32
- print(f"Status da resposta: {response.status_code}")
33
- response.raise_for_status()
34
- except requests.exceptions.RequestException as e:
35
- print(f"Erro na requisição: {e}")
36
- raise HTTPException(status_code=400, detail=f"Não foi possível baixar o arquivo: {str(e)}")
37
-
38
- if response.status_code != 200:
39
- raise HTTPException(status_code=400, detail=f"Erro ao baixar arquivo. Status: {response.status_code}")
40
-
41
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
42
- try:
43
- total_size = 0
44
- for chunk in response.iter_content(chunk_size=8192):
45
- if chunk:
46
- tmp.write(chunk)
47
- total_size += len(chunk)
48
- tmp.close()
49
- print(f"Arquivo baixado com sucesso. Tamanho: {total_size} bytes")
50
- return tmp.name
51
- except Exception as e:
52
- tmp.close()
53
- if os.path.exists(tmp.name):
54
- os.unlink(tmp.name)
55
- print(f"Erro ao salvar arquivo: {e}")
56
- raise HTTPException(status_code=400, detail=f"Erro ao salvar arquivo: {str(e)}")
57
-
58
- def download_video(video_url: str) -> str:
59
- return download_file(video_url, ".mp4")
60
-
61
- def download_srt(srt_url: str) -> str:
62
- return download_file(srt_url, ".srt")
63
-
64
- def parse_srt(srt_path: str) -> List[Tuple[float, float, str]]:
65
- """Parse arquivo SRT e retorna lista de tuplas (start_time, end_time, text)"""
66
- subtitles = []
67
-
68
- with open(srt_path, 'r', encoding='utf-8') as f:
69
- content = f.read()
70
-
71
- # Regex para extrair informações do SRT
72
- pattern = r'(\d+)\s*\n(\d{2}:\d{2}:\d{2},\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2},\d{3})\s*\n(.*?)(?=\n\d+\s*\n|\n*$)'
73
- matches = re.findall(pattern, content, re.DOTALL)
74
-
75
- for match in matches:
76
- start_time_str = match[1]
77
- end_time_str = match[2]
78
- text = match[3].strip()
79
-
80
- # Converter timestamp para segundos
81
- start_time = time_to_seconds(start_time_str)
82
- end_time = time_to_seconds(end_time_str)
83
-
84
- subtitles.append((start_time, end_time, text))
85
-
86
- print(f"Parsed {len(subtitles)} subtítulos do arquivo SRT")
87
- return subtitles
88
-
89
- def time_to_seconds(time_str: str) -> float:
90
- """Converte timestamp SRT (HH:MM:SS,mmm) para segundos"""
91
- time_str = time_str.replace(',', '.')
92
- parts = time_str.split(':')
93
- hours = int(parts[0])
94
- minutes = int(parts[1])
95
- seconds = float(parts[2])
96
- return hours * 3600 + minutes * 60 + seconds
97
-
98
- def create_rounded_mask(w: int, h: int, radius: int) -> np.ndarray:
99
- """Cria uma máscara numpy com cantos arredondados otimizada"""
100
- img = Image.new("L", (w, h), 0)
101
- draw = ImageDraw.Draw(img)
102
- draw.rounded_rectangle((0, 0, w, h), radius=radius, fill=255)
103
- mask = np.array(img, dtype=np.float32) / 255.0
104
- return mask
105
-
106
- def create_text_image(text: str, font_path: str, font_size: int, color: str = "white", width: int = 900, background_color: str = None) -> np.ndarray:
107
- """Cria uma imagem com texto usando PIL e retorna array numpy diretamente com quebra de linha"""
108
- try:
109
- font = ImageFont.truetype(font_path, font_size)
110
- except:
111
- font = ImageFont.load_default()
112
-
113
- # Função para quebrar texto em múltiplas linhas
114
- def wrap_text(text, font, max_width):
115
- # Primeiro, dividir por quebras de linha existentes (importantes para SRT)
116
- existing_lines = text.split('\n')
117
- final_lines = []
118
-
119
- for line in existing_lines:
120
- if not line.strip(): # Pular linhas vazias
121
- continue
122
-
123
- words = line.split(' ')
124
- current_line = []
125
-
126
- for word in words:
127
- test_line = ' '.join(current_line + [word])
128
- bbox = font.getbbox(test_line)
129
- test_width = bbox[2] - bbox[0]
130
-
131
- if test_width <= max_width - 40: # 40px de margem total
132
- current_line.append(word)
133
- else:
134
- if current_line:
135
- final_lines.append(' '.join(current_line))
136
- current_line = [word]
137
- else:
138
- final_lines.append(word)
139
-
140
- if current_line:
141
- final_lines.append(' '.join(current_line))
142
-
143
- return final_lines
144
-
145
- # Quebrar o texto em linhas
146
- lines = wrap_text(text, font, width)
147
-
148
- # Calcular dimensões totais baseadas na altura real da fonte
149
- font_metrics = font.getmetrics()
150
- ascent, descent = font_metrics
151
- line_height = ascent + descent
152
- line_spacing = int(line_height * 0.2)
153
- total_height = len(lines) * line_height + (len(lines) - 1) * line_spacing
154
-
155
- # Definir padding para o fundo
156
- padding_vertical = 16 if background_color else 10
157
- padding_horizontal = 24 if background_color else 10
158
-
159
- # Criar imagem com altura ajustada para múltiplas linhas
160
- img = Image.new("RGBA", (width, total_height + padding_vertical * 2), (0, 0, 0, 0))
161
- draw = ImageDraw.Draw(img)
162
-
163
- # Desenhar fundo se especificado
164
- if background_color:
165
- # Calcular largura máxima do texto para um fundo mais ajustado
166
- max_text_width = 0
167
- for line in lines:
168
- bbox = font.getbbox(line)
169
- line_width = bbox[2] - bbox[0]
170
- max_text_width = max(max_text_width, line_width)
171
-
172
- # Calcular dimensões do fundo
173
- bg_width = max_text_width + padding_horizontal * 2
174
- bg_height = total_height + padding_vertical * 2
175
- bg_x = (width - bg_width) // 2
176
- bg_y = 0
177
-
178
- # Desenhar fundo com cantos arredondados
179
- draw.rounded_rectangle(
180
- (bg_x, bg_y, bg_x + bg_width, bg_y + bg_height),
181
- radius=6,
182
- fill=background_color
183
- )
184
-
185
- # Desenhar cada linha centralizada usando baseline correto
186
- current_y = padding_vertical
187
- for line in lines:
188
- bbox = font.getbbox(line)
189
- line_width = bbox[2] - bbox[0]
190
- line_x = (width - line_width) // 2 # Centralizar cada linha
191
- draw.text((line_x, current_y), line, font=font, fill=color)
192
- current_y += line_height + line_spacing
193
-
194
- return np.array(img, dtype=np.uint8)
195
-
196
- def create_subtitle_clips(subtitles: List[Tuple[float, float, str]], video_duration: float) -> List[ImageClip]:
197
- """Cria clips de legenda otimizados usando ImageClip"""
198
- subtitle_clips = []
199
-
200
- for start_time, end_time, text in subtitles:
201
- # Ignorar legendas que ultrapassam a duração do vídeo
202
- if start_time >= video_duration:
203
- continue
204
-
205
- # Ajustar end_time se necessário
206
- if end_time > video_duration:
207
- end_time = video_duration
208
-
209
- # Criar imagem da legenda com fonte Medium e fundo escuro
210
- subtitle_array = create_text_image(
211
- text,
212
- "fonts/Montserrat-Medium.ttf", # Fonte Medium para legendas
213
- 32, # Tamanho para legendas
214
- "white",
215
- 900,
216
- "#1A1A1A" # Fundo escuro para legendas
217
- )
218
-
219
- # Criar clip de imagem
220
- subtitle_clip = ImageClip(subtitle_array, duration=end_time - start_time)
221
- subtitle_clip = subtitle_clip.set_start(start_time)
222
-
223
- subtitle_clips.append(subtitle_clip)
224
-
225
- print(f"Criados {len(subtitle_clips)} clips de legenda")
226
- return subtitle_clips
227
-
228
- def create_centered_video_on_black_background(
229
- video_path: str,
230
- text: str = "Season 1, episode 1",
231
- srt_path: Optional[str] = None,
232
- output_resolution=(1080, 1920),
233
- max_height=500,
234
- max_width=900
235
- ) -> BytesIO:
236
- print(f"Iniciando processamento do vídeo: {video_path}")
237
-
238
- clip = None
239
- background = None
240
- text_clip = None
241
- centered_clip = None
242
- final = None
243
- subtitle_clips = []
244
-
245
- try:
246
- # Carregar vídeo
247
- clip = VideoFileClip(video_path, audio=True, verbose=False)
248
- print(f"Vídeo carregado - Dimensões: {clip.w}x{clip.h}, Duração: {clip.duration}s, FPS: {clip.fps}")
249
-
250
- # Redimensionar vídeo para 500px de altura máxima
251
- if clip.w != max_width or clip.h > max_height:
252
- scale_w = max_width / clip.w
253
- scale_h = max_height / clip.h
254
- scale = min(scale_w, scale_h)
255
- new_width = int(clip.w * scale)
256
- new_height = int(clip.h * scale)
257
- print(f"Redimensionando para: {new_width}x{new_height} (max_height={max_height})")
258
- clip = clip.resize(newsize=(new_width, new_height))
259
-
260
- # Criar fundo preto
261
- background = ColorClip(size=output_resolution, color=(0, 0, 0), duration=clip.duration)
262
-
263
- # Criar máscara arredondada baseada no tamanho atual do vídeo
264
- print(f"Criando máscara para vídeo: {clip.w}x{clip.h}")
265
- mask_array = create_rounded_mask(clip.w, clip.h, radius=80)
266
-
267
- def make_mask_frame(t):
268
- return mask_array
269
-
270
- mask_clip = VideoClip(make_mask_frame, ismask=True, duration=clip.duration)
271
- clip = clip.set_mask(mask_clip)
272
-
273
- # Criar texto principal
274
- text_array = create_text_image(text, "fonts/Montserrat-SemiBold.ttf", 38, "white", 900)
275
- text_clip = ImageClip(text_array, duration=clip.duration)
276
-
277
- # Centralizar o vídeo
278
- centered_clip = clip.set_position(("center", "center"))
279
-
280
- # Posicionar texto principal (45px de distância do vídeo)
281
- video_top = (output_resolution[1] - clip.h) // 2
282
- text_y = video_top - 45 - text_clip.h
283
- text_clip = text_clip.set_position(("center", text_y))
284
-
285
- # Processar legendas se fornecidas
286
- if srt_path:
287
- print("Processando legendas SRT...")
288
- subtitles = parse_srt(srt_path)
289
- subtitle_clips = create_subtitle_clips(subtitles, clip.duration)
290
-
291
- # Posicionar legendas abaixo do vídeo (45px de distância)
292
- video_bottom = (output_resolution[1] + clip.h) // 2
293
- subtitle_y = video_bottom + 45 # 45px de espaçamento
294
-
295
- # Aplicar posicionamento a cada clip individual
296
- for i, subtitle_clip in enumerate(subtitle_clips):
297
- subtitle_clips[i] = subtitle_clip.set_position(("center", subtitle_y))
298
-
299
- # Compor todos os elementos
300
- all_clips = [background, text_clip, centered_clip] + subtitle_clips
301
- final = CompositeVideoClip(all_clips)
302
-
303
- print("Composição finalizada, iniciando renderização...")
304
-
305
- buffer = BytesIO()
306
- tmp_output_path = None
307
-
308
- try:
309
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp_output:
310
- tmp_output_path = tmp_output.name
311
-
312
- print(f"Renderizando para arquivo temporário: {tmp_output_path}")
313
-
314
- final.write_videofile(
315
- tmp_output_path,
316
- codec="libx264",
317
- audio_codec="aac",
318
- fps=clip.fps,
319
- preset="ultrafast",
320
- threads=os.cpu_count(),
321
- temp_audiofile="temp-audio.m4a",
322
- remove_temp=True,
323
- audio=True,
324
- logger=None,
325
- verbose=False,
326
- ffmpeg_params=[
327
- "-crf", "23",
328
- "-movflags", "+faststart",
329
- "-tune", "fastdecode",
330
- "-x264opts", "no-scenecut"
331
- ]
332
- )
333
-
334
- print("Renderização concluída, lendo arquivo...")
335
- with open(tmp_output_path, "rb") as f:
336
- buffer.write(f.read())
337
- buffer.seek(0)
338
-
339
- print(f"Vídeo processado com sucesso. Tamanho final: {buffer.getbuffer().nbytes} bytes")
340
-
341
- finally:
342
- if tmp_output_path and os.path.exists(tmp_output_path):
343
- os.unlink(tmp_output_path)
344
-
345
- except Exception as e:
346
- print(f"Erro durante processamento: {e}")
347
- raise
348
-
349
- finally:
350
- # Limpeza de memória
351
- clips_to_close = [clip, background, text_clip, centered_clip, final] + subtitle_clips
352
- for c in clips_to_close:
353
- if c is not None:
354
- try:
355
- c.close()
356
- except:
357
- pass
358
-
359
- gc.collect()
360
-
361
- return buffer
362
-
363
- @router.get("/cover/video")
364
- def get_video_with_black_background(
365
- video_url: str = Query(..., description="URL do vídeo em .mp4 para centralizar em fundo preto com cantos arredondados"),
366
- text: str = Query("Season 1, episode 1", description="Texto a ser exibido acima do vídeo"),
367
- srt_url: Optional[str] = Query(None, description="URL do arquivo SRT de legendas (opcional)")
368
- ):
369
- local_video = None
370
- local_srt = None
371
-
372
- try:
373
- # Baixar vídeo
374
- local_video = download_video(video_url)
375
-
376
- # Baixar SRT se fornecido
377
- if srt_url:
378
- local_srt = download_srt(srt_url)
379
-
380
- # Processar vídeo com altura máxima de 500px
381
- video_buffer = create_centered_video_on_black_background(
382
- local_video,
383
- text,
384
- local_srt
385
- )
386
-
387
- return StreamingResponse(video_buffer, media_type="video/mp4")
388
-
389
- except Exception as e:
390
- raise HTTPException(status_code=500, detail=f"Erro ao processar vídeo: {e}")
391
-
392
- finally:
393
- # Limpeza de arquivos temporários
394
- for temp_file in [local_video, local_srt]:
395
- if temp_file and os.path.exists(temp_file):
396
- os.unlink(temp_file)