habulaj commited on
Commit
ca0460f
·
verified ·
1 Parent(s): a1b34e4

Delete routers/subtitle.py

Browse files
Files changed (1) hide show
  1. routers/subtitle.py +0 -497
routers/subtitle.py DELETED
@@ -1,497 +0,0 @@
1
- from fastapi import APIRouter, Query, HTTPException
2
- from moviepy.editor import VideoFileClip
3
- import tempfile
4
- import requests
5
- import os
6
- import shutil
7
- from groq import Groq
8
- from audio_separator.separator import Separator
9
- from google import genai
10
- from google.genai import types
11
-
12
- router = APIRouter()
13
-
14
- def download_file(url: str, suffix: str) -> str:
15
- """Download genérico para arquivos de áudio e vídeo"""
16
- print(f"Tentando baixar arquivo de: {url}")
17
- headers = {
18
- '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',
19
- 'Accept': '*/*',
20
- 'Accept-Language': 'en-US,en;q=0.5',
21
- 'Accept-Encoding': 'gzip, deflate',
22
- 'Connection': 'keep-alive',
23
- 'Upgrade-Insecure-Requests': '1',
24
- }
25
-
26
- try:
27
- response = requests.get(url, headers=headers, stream=True, timeout=30)
28
- print(f"Status da resposta: {response.status_code}")
29
- response.raise_for_status()
30
- except requests.exceptions.RequestException as e:
31
- print(f"Erro na requisição: {e}")
32
- raise HTTPException(status_code=400, detail=f"Não foi possível baixar o arquivo: {str(e)}")
33
-
34
- if response.status_code != 200:
35
- raise HTTPException(status_code=400, detail=f"Erro ao baixar arquivo. Status: {response.status_code}")
36
-
37
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
38
- try:
39
- total_size = 0
40
- for chunk in response.iter_content(chunk_size=8192):
41
- if chunk:
42
- tmp.write(chunk)
43
- total_size += len(chunk)
44
- tmp.close()
45
- print(f"Arquivo baixado com sucesso. Tamanho: {total_size} bytes")
46
- return tmp.name
47
- except Exception as e:
48
- tmp.close()
49
- if os.path.exists(tmp.name):
50
- os.unlink(tmp.name)
51
- print(f"Erro ao salvar arquivo: {e}")
52
- raise HTTPException(status_code=400, detail=f"Erro ao salvar arquivo: {str(e)}")
53
-
54
- def extract_audio_from_video(video_path: str) -> str:
55
- """Extrai áudio de um arquivo de vídeo e salva como WAV"""
56
- print(f"Extraindo áudio do vídeo: {video_path}")
57
-
58
- audio_tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
59
- audio_path = audio_tmp.name
60
- audio_tmp.close()
61
-
62
- try:
63
- video = VideoFileClip(video_path)
64
- audio = video.audio
65
- audio.write_audiofile(audio_path, verbose=False, logger=None)
66
- audio.close()
67
- video.close()
68
- print(f"Áudio extraído com sucesso: {audio_path}")
69
- return audio_path
70
- except Exception as e:
71
- if os.path.exists(audio_path):
72
- os.unlink(audio_path)
73
- print(f"Erro ao extrair áudio: {e}")
74
- raise HTTPException(status_code=500, detail=f"Erro ao extrair áudio do vídeo: {str(e)}")
75
-
76
- def separate_vocals(audio_path: str) -> str:
77
- """Separa vocais do áudio usando audio-separator com modelo UVR_MDXNET_KARA_2.onnx"""
78
- print(f"Iniciando separação de vocais do arquivo: {audio_path}")
79
-
80
- # Criar diretório temporário para saída
81
- temp_output_dir = tempfile.mkdtemp(prefix="vocal_separation_")
82
-
83
- try:
84
- # Inicializar o separador
85
- separator = Separator(output_dir=temp_output_dir)
86
-
87
- # Carregar modelo específico para vocais (UVR_MDXNET_KARA_2.onnx é melhor para vocais)
88
- print("Carregando modelo UVR_MDXNET_KARA_2.onnx...")
89
- separator.load_model('UVR_MDXNET_KARA_2.onnx')
90
-
91
- # Processar arquivo
92
- print("Processando separação de vocais...")
93
- separator.separate(audio_path)
94
-
95
- # Encontrar o arquivo de vocais gerado
96
- # O audio-separator geralmente gera arquivos com sufixos específicos
97
- base_name = os.path.splitext(os.path.basename(audio_path))[0]
98
-
99
- # Procurar pelo arquivo de vocais (pode ter diferentes sufixos dependendo do modelo)
100
- possible_vocal_files = [
101
- f"{base_name}_(Vocals).wav",
102
- f"{base_name}_vocals.wav",
103
- f"{base_name}_Vocals.wav",
104
- f"{base_name}_(Vocal).wav"
105
- ]
106
-
107
- vocal_file_path = None
108
- for possible_file in possible_vocal_files:
109
- full_path = os.path.join(temp_output_dir, possible_file)
110
- if os.path.exists(full_path):
111
- vocal_file_path = full_path
112
- break
113
-
114
- # Se não encontrou pelos nomes padrão, procurar qualquer arquivo wav no diretório
115
- if not vocal_file_path:
116
- wav_files = [f for f in os.listdir(temp_output_dir) if f.endswith('.wav')]
117
- if wav_files:
118
- # Pegar o primeiro arquivo wav encontrado (assumindo que seja o vocal)
119
- vocal_file_path = os.path.join(temp_output_dir, wav_files[0])
120
-
121
- if not vocal_file_path or not os.path.exists(vocal_file_path):
122
- raise HTTPException(status_code=500, detail="Arquivo de vocais não foi gerado corretamente")
123
-
124
- # Mover arquivo de vocais para um local temporário permanente
125
- vocal_temp = tempfile.NamedTemporaryFile(delete=False, suffix="_vocals.wav")
126
- vocal_final_path = vocal_temp.name
127
- vocal_temp.close()
128
-
129
- shutil.copy2(vocal_file_path, vocal_final_path)
130
- print(f"Vocais separados com sucesso: {vocal_final_path}")
131
-
132
- return vocal_final_path
133
-
134
- except Exception as e:
135
- print(f"Erro na separação de vocais: {e}")
136
- raise HTTPException(status_code=500, detail=f"Erro ao separar vocais: {str(e)}")
137
-
138
- finally:
139
- # Limpar diretório temporário de separação
140
- if os.path.exists(temp_output_dir):
141
- try:
142
- shutil.rmtree(temp_output_dir)
143
- print(f"Diretório temporário removido: {temp_output_dir}")
144
- except Exception as cleanup_error:
145
- print(f"Erro ao remover diretório temporário: {cleanup_error}")
146
-
147
- def format_time(seconds_float: float) -> str:
148
- """Converte segundos para formato de tempo SRT (HH:MM:SS,mmm) - versão melhorada"""
149
- # Calcula segundos totais e milissegundos
150
- total_seconds = int(seconds_float)
151
- milliseconds = int((seconds_float - total_seconds) * 1000)
152
-
153
- # Calcula horas, minutos e segundos restantes
154
- hours = total_seconds // 3600
155
- minutes = (total_seconds % 3600) // 60
156
- seconds = total_seconds % 60
157
-
158
- return f"{hours:02}:{minutes:02}:{seconds:02},{milliseconds:03}"
159
-
160
- def json_to_srt(segments_data) -> str:
161
- """
162
- Converte dados de segmentos para formato SRT
163
- """
164
- if not segments_data:
165
- return ""
166
-
167
- srt_lines = []
168
-
169
- for segment in segments_data:
170
- segment_id = segment.get('id', 0) + 1
171
- start_time = format_time(segment.get('start', 0.0))
172
- end_time = format_time(segment.get('end', 0.0))
173
- text = segment.get('text', '').strip()
174
-
175
- if text: # Só adiciona se há texto
176
- srt_lines.append(f"{segment_id}")
177
- srt_lines.append(f"{start_time} --> {end_time}")
178
- srt_lines.append(text)
179
- srt_lines.append("") # Linha em branco
180
-
181
- return '\n'.join(srt_lines)
182
-
183
- def convert_to_srt(transcription_data) -> str:
184
- """
185
- Função para conversão usando apenas segments
186
- """
187
- if hasattr(transcription_data, 'segments') and transcription_data.segments:
188
- return json_to_srt(transcription_data.segments)
189
- else:
190
- return ""
191
-
192
- def translate_subtitle_internal(content: str) -> str:
193
- """
194
- Função interna para traduzir legendas usando Gemini
195
- Baseada na lógica do inference_sub.py
196
- """
197
- try:
198
- print("Iniciando tradução da legenda...")
199
-
200
- api_key = os.environ.get("GEMINI_API_KEY")
201
- if not api_key:
202
- raise HTTPException(status_code=500, detail="GEMINI_API_KEY não configurada")
203
-
204
- client = genai.Client(api_key=api_key)
205
- model = "gemini-2.5-pro"
206
-
207
- # Instruções do sistema aprimoradas
208
- SYSTEM_INSTRUCTIONS = """
209
- Você é um tradutor profissional de legendas especializado em tradução do inglês para o português brasileiro.
210
- Sua função é traduzir legendas mantendo a formatação SRT original intacta e seguindo os padrões da Netflix.
211
- REGRAS FUNDAMENTAIS:
212
- 1. NUNCA altere os timestamps (00:00:00,000 --> 00:00:00,000)
213
- 2. NUNCA altere os números das legendas (1, 2, 3, etc.)
214
- 3. Mantenha a formatação SRT exata: número, timestamp, texto traduzido, linha em branco
215
- 4. Traduza APENAS o texto das falas
216
- PADRÕES DE TRADUÇÃO:
217
- - Tradução natural para português brasileiro
218
- - Mantenha o tom e registro da fala original (formal/informal, gírias, etc.)
219
- - Preserve nomes próprios, lugares e marcas
220
- - Adapte expressões idiomáticas para equivalentes em português quando necessário
221
- - Use contrações naturais do português brasileiro (você → cê, para → pra, quando apropriado)
222
- FORMATAÇÃO NETFLIX:
223
- - Máximo de 2 linhas por legenda
224
- - Máximo de 42 caracteres por linha (incluindo espaços)
225
- - Use quebra de linha quando o texto for muito longo
226
- - Prefira quebras em pontos naturais da fala (após vírgulas, conjunções, etc.)
227
- - Centralize o texto quando possível
228
- PONTUAÇÃO E ESTILO:
229
- - Use pontuação adequada em português
230
- - Mantenha reticências (...) para hesitações ou falas interrompidas
231
- - Use travessão (–) para diálogos quando necessário
232
- - Evite pontos finais desnecessários em falas curtas
233
- Sempre retorne APENAS o conteúdo das legendas traduzidas, mantendo a formatação SRT original.
234
- """
235
-
236
- # Primeiro exemplo
237
- EXAMPLE_INPUT_1 = """1
238
- 00:00:00,000 --> 00:00:03,500
239
- You could argue he'd done it to curry favor with the guards.
240
- 2
241
- 00:00:04,379 --> 00:00:07,299
242
- Or maybe make a few friends among us Khans.
243
- 3
244
- 00:00:08,720 --> 00:00:12,199
245
- Me, I think he did it just to feel normal again.
246
- 4
247
- 00:00:13,179 --> 00:00:14,740
248
- If only for a short while."""
249
-
250
- EXAMPLE_OUTPUT_1 = """1
251
- 00:00:00,000 --> 00:00:03,500
252
- Você pode dizer que ele fez isso
253
- para agradar os guardas.
254
- 2
255
- 00:00:04,379 --> 00:00:07,299
256
- Ou talvez para fazer alguns amigos
257
- entre nós, os Khans.
258
- 3
259
- 00:00:08,720 --> 00:00:12,199
260
- Eu acho que ele fez isso só para se sentir
261
- normal de novo.
262
- 4
263
- 00:00:13,179 --> 00:00:14,740
264
- Mesmo que só por um tempo."""
265
-
266
- # Segundo exemplo
267
- EXAMPLE_INPUT_2 = """1
268
- 00:00:15,420 --> 00:00:18,890
269
- I'm not saying you're wrong, but have you considered the alternatives?
270
- 2
271
- 00:00:19,234 --> 00:00:21,567
272
- What if we just... I don't know... talked to him?
273
- 3
274
- 00:00:22,890 --> 00:00:26,234
275
- Listen, Jack, this isn't some Hollywood movie where everything works out.
276
- 4
277
- 00:00:27,123 --> 00:00:29,456
278
- Sometimes you gotta make the hard choices."""
279
-
280
- EXAMPLE_OUTPUT_2 = """1
281
- 00:00:15,420 --> 00:00:18,890
282
- Não tô dizendo que você tá errado, mas
283
- já pensou nas alternativas?
284
- 2
285
- 00:00:19,234 --> 00:00:21,567
286
- E se a gente só... sei lá...
287
- conversasse com ele?
288
- 3
289
- 00:00:22,890 --> 00:00:26,234
290
- Escuta, Jack, isso não é um filme de
291
- Hollywood onde tudo dá certo.
292
- 4
293
- 00:00:27,123 --> 00:00:29,456
294
- Às vezes você tem que fazer
295
- as escolhas difíceis."""
296
-
297
- # Terceiro exemplo com diálogos
298
- EXAMPLE_INPUT_3 = """1
299
- 00:00:30,789 --> 00:00:32,456
300
- - Hey, what's up?
301
- - Not much, just chilling.
302
- 2
303
- 00:00:33,567 --> 00:00:36,123
304
- Did you see that new Netflix show everyone's talking about?
305
- 3
306
- 00:00:37,234 --> 00:00:40,789
307
- Yeah, it's incredible! The cinematography is absolutely stunning.
308
- 4
309
- 00:00:41,890 --> 00:00:44,567
310
- I can't believe they canceled it after just one season though."""
311
-
312
- EXAMPLE_OUTPUT_3 = """1
313
- 00:00:30,789 --> 00:00:32,456
314
- – E aí, tudo bem?
315
- – De boa, só relaxando.
316
- 2
317
- 00:00:33,567 --> 00:00:36,123
318
- Você viu aquela série nova da Netflix
319
- que todo mundo tá falando?
320
- 3
321
- 00:00:37,234 --> 00:00:40,789
322
- Vi, é incrível! A cinematografia
323
- é absolutamente deslumbrante.
324
- 4
325
- 00:00:41,890 --> 00:00:44,567
326
- Não acredito que cancelaram depois
327
- de só uma temporada."""
328
-
329
- # Estrutura de conversação correta com múltiplos exemplos
330
- contents = [
331
- # Primeiro exemplo: usuário envia legenda
332
- types.Content(
333
- role="user",
334
- parts=[
335
- types.Part.from_text(text=EXAMPLE_INPUT_1)
336
- ]
337
- ),
338
- # Primeiro exemplo: modelo responde com tradução
339
- types.Content(
340
- role="model",
341
- parts=[
342
- types.Part.from_text(text=EXAMPLE_OUTPUT_1)
343
- ]
344
- ),
345
- # Segundo exemplo: usuário envia outra legenda
346
- types.Content(
347
- role="user",
348
- parts=[
349
- types.Part.from_text(text=EXAMPLE_INPUT_2)
350
- ]
351
- ),
352
- # Segundo exemplo: modelo responde com tradução
353
- types.Content(
354
- role="model",
355
- parts=[
356
- types.Part.from_text(text=EXAMPLE_OUTPUT_2)
357
- ]
358
- ),
359
- # Terceiro exemplo: usuário envia legenda com diálogos
360
- types.Content(
361
- role="user",
362
- parts=[
363
- types.Part.from_text(text=EXAMPLE_INPUT_3)
364
- ]
365
- ),
366
- # Terceiro exemplo: modelo responde com tradução
367
- types.Content(
368
- role="model",
369
- parts=[
370
- types.Part.from_text(text=EXAMPLE_OUTPUT_3)
371
- ]
372
- ),
373
- # Agora o usuário envia a legenda real para ser traduzida
374
- types.Content(
375
- role="user",
376
- parts=[
377
- types.Part.from_text(text=content)
378
- ]
379
- )
380
- ]
381
-
382
- config = types.GenerateContentConfig(
383
- system_instruction=SYSTEM_INSTRUCTIONS,
384
- response_mime_type="text/plain",
385
- max_output_tokens=4096,
386
- temperature=0.3, # Menos criatividade, mais precisão na tradução
387
- )
388
-
389
- response_text = ""
390
- for chunk in client.models.generate_content_stream(
391
- model=model,
392
- contents=contents,
393
- config=config
394
- ):
395
- if chunk.text:
396
- response_text += chunk.text
397
-
398
- translated_content = response_text.strip()
399
- print("Tradução concluída com sucesso")
400
- return translated_content
401
-
402
- except Exception as e:
403
- print(f"Erro na tradução interna: {e}")
404
- # Retorna o conteúdo original se a tradução falhar
405
- return content
406
-
407
- @router.get("/subtitle/generate-srt")
408
- def generate_srt_subtitle(
409
- url: str = Query(..., description="URL do arquivo de áudio (.wav) ou vídeo")
410
- ):
411
- """
412
- Gera legenda em formato SRT a partir de arquivo de áudio ou vídeo
413
- - Se for .wav: separa vocais e transcreve
414
- - Se for vídeo: extrai áudio, separa vocais e transcreve
415
- - Usa modelo UVR_MDXNET_KARA_2.onnx para separação de vocais
416
- - Usa segmentação natural do Whisper (segments)
417
- - Detecção automática de idioma
418
- - Tradução automática sempre ativada
419
- """
420
- local_file = None
421
- audio_file = None
422
- vocal_file = None
423
-
424
- try:
425
- # Determinar tipo de arquivo pela URL
426
- url_lower = url.lower()
427
- is_audio = url_lower.endswith('.wav')
428
- is_video = any(url_lower.endswith(ext) for ext in ['.mp4', '.avi', '.mov', '.mkv', '.webm'])
429
-
430
- if not (is_audio or is_video):
431
- raise HTTPException(
432
- status_code=400,
433
- detail="URL deve ser de um arquivo de áudio (.wav) ou vídeo"
434
- )
435
-
436
- if is_audio:
437
- local_file = download_file(url, ".wav")
438
- audio_file = local_file
439
- else:
440
- local_file = download_file(url, ".mp4")
441
- audio_file = extract_audio_from_video(local_file)
442
-
443
- # Separar vocais do áudio
444
- vocal_file = separate_vocals(audio_file)
445
-
446
- # Transcrição com configurações fixas otimizadas
447
- api_key = os.getenv("GROQ_API")
448
- if not api_key:
449
- raise HTTPException(status_code=500, detail="GROQ_API key não configurada")
450
-
451
- client = Groq(api_key=api_key)
452
-
453
- print(f"Iniciando transcrição com modelo: whisper-large-v3")
454
- with open(vocal_file, "rb") as file:
455
- transcription_params = {
456
- "file": (os.path.basename(vocal_file), file.read()),
457
- "model": "whisper-large-v3",
458
- "response_format": "verbose_json",
459
- "timestamp_granularities": ["segment"],
460
- "temperature": 0.0,
461
- # language é automaticamente detectado (não enviado)
462
- }
463
-
464
- transcription = client.audio.transcriptions.create(**transcription_params)
465
-
466
- # Converter para SRT usando segments
467
- srt_content_original = convert_to_srt(transcription)
468
-
469
- # Traduzir sempre
470
- srt_content = translate_subtitle_internal(srt_content_original) if srt_content_original else None
471
-
472
- return {
473
- "srt": srt_content,
474
- "duration": getattr(transcription, 'duration', 0),
475
- "language": getattr(transcription, 'language', 'unknown'),
476
- "model_used": "whisper-large-v3",
477
- "processing_method": "segments",
478
- "vocal_separation": "UVR_MDXNET_KARA_2.onnx",
479
- "translation_applied": True,
480
- "segment_count": len(transcription.segments) if hasattr(transcription, 'segments') and transcription.segments else 0,
481
- "subtitle_count": len([line for line in srt_content.split('\n') if line.strip().isdigit()]) if srt_content else 0
482
- }
483
-
484
- except HTTPException:
485
- raise
486
- except Exception as e:
487
- raise HTTPException(status_code=500, detail=f"Erro inesperado: {str(e)}")
488
-
489
- finally:
490
- # Limpeza de arquivos temporários
491
- for temp_file in [local_file, audio_file, vocal_file]:
492
- if temp_file and os.path.exists(temp_file):
493
- try:
494
- os.unlink(temp_file)
495
- print(f"Arquivo temporário removido: {temp_file}")
496
- except Exception as cleanup_error:
497
- print(f"Erro ao remover arquivo temporário {temp_file}: {cleanup_error}")