RafaG commited on
Commit
b7f3430
·
verified ·
1 Parent(s): f476e63

Upload 4 files

Browse files
Files changed (4) hide show
  1. app.py +243 -551
  2. edgeTTS.py +130 -0
  3. tiktokTTS.py +133 -0
  4. utils.py +155 -0
app.py CHANGED
@@ -1,551 +1,243 @@
1
- import subprocess
2
- import os
3
- import json
4
- import gradio as gr
5
- from pydub import AudioSegment
6
- from pydub.playback import play
7
- from header import badges, description
8
- from pydub.silence import split_on_silence
9
- from get_voices import get_voices
10
- import asyncio
11
- from pathlib import Path
12
- import pysrt
13
- from tqdm import tqdm
14
- import shutil
15
- from pathlib import Path
16
-
17
- srt_temp_deleta = True
18
- # Constantes no início do script
19
- OUTPUT_DIR = Path("output")
20
- SRT_OUTPUT_DIR = OUTPUT_DIR / "srt_output"
21
- SRT_TEMP_DIR = OUTPUT_DIR / "srt_temp"
22
- VOICES_JSON_FILE = Path("voices.json")
23
-
24
- # Exemplo de uso
25
- SRT_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
26
-
27
- def initialize_voices():
28
- if not VOICES_JSON_FILE.exists():
29
- print("Arquivo voices.json não encontrado. Baixando a lista de vozes...")
30
- try:
31
- get_voices()
32
- print("Lista de vozes baixada com sucesso.")
33
- except Exception as e:
34
- print(f"Não foi possível baixar a lista de vozes: {e}")
35
- # Cria um arquivo vazio para evitar que o app quebre
36
- with open(VOICES_JSON_FILE, 'w', encoding='utf-8') as f:
37
- json.dump({}, f)
38
-
39
-
40
- def load_voices():
41
- with open('voices.json', 'r', encoding='utf-8') as f:
42
- return json.load(f)
43
-
44
- def get_voice_options(language, voices_data):
45
- if language in voices_data:
46
- return [f"{voice['name']} | {voice['gender']}" for voice in voices_data[language]]
47
- return []
48
-
49
- def extract_voice_name(formatted_voice):
50
- return formatted_voice.split(" | ")[0]
51
-
52
- def update_voice_options(language):
53
- voices_data = load_voices()
54
- voice_options = get_voice_options(language, voices_data)
55
- if voice_options:
56
- # Usa gr.update() para atualizar as opções e o valor do componente existente
57
- return gr.update(choices=voice_options, value=voice_options[0], interactive=True)
58
- # Desabilita o dropdown se não houver vozes
59
- return gr.update(choices=[], value=None, interactive=False)
60
-
61
- def update_voices_and_refresh():
62
- get_voices()
63
- voices_data = load_voices()
64
- available_languages = list(voices_data.keys())
65
- initial_voices = get_voice_options(available_languages[0], voices_data) if available_languages else []
66
- return (
67
- gr.Dropdown(choices=available_languages, value=available_languages[0] if available_languages else None),
68
- gr.Dropdown(choices=initial_voices, value=initial_voices[0] if initial_voices else None)
69
- )
70
-
71
- def remove_silence(input_file, output_file):
72
- audio = AudioSegment.from_wav(input_file)
73
- segments = split_on_silence(audio, min_silence_len=500, silence_thresh=-40)
74
- non_silent_audio = AudioSegment.silent(duration=0)
75
- for segment in segments:
76
- non_silent_audio += segment
77
- non_silent_audio.export(output_file, format="wav")
78
-
79
- def generate_audio(voice, text_or_file, rate, pitch, volume):
80
- """Gera áudio a partir de um texto ou de um arquivo."""
81
-
82
- # Constrói os argumentos comuns
83
- rate_str = f"+{rate}%" if rate >= 0 else f"{rate}%"
84
- pitch_str = f"+{pitch}Hz" if pitch >= 0 else f"{pitch}Hz"
85
- volume_str = f"+{volume}%" if volume >= 0 else f"{volume}%"
86
-
87
- output_dir = Path("output")
88
- output_dir.mkdir(exist_ok=True)
89
- output_file = output_dir / "new_audio.mp3"
90
-
91
- cmd = [
92
- "edge-tts",
93
- "--rate=" + rate_str,
94
- "--pitch=" + pitch_str,
95
- "--volume=" + volume_str,
96
- "-v", extract_voice_name(voice),
97
- "--write-media", str(output_file)
98
- ]
99
-
100
- # Adiciona o argumento de texto ou arquivo
101
- if Path(text_or_file).is_file():
102
- cmd.extend(["-f", text_or_file])
103
- else:
104
- cmd.extend(["-t", text_or_file])
105
-
106
- print("Gerando áudio...")
107
- try:
108
- subprocess.run(cmd, check=True, capture_output=True, text=True)
109
- print("Áudio gerado com sucesso!")
110
- return str(output_file)
111
- except subprocess.CalledProcessError as e:
112
- print(f"Erro ao gerar áudio: {e.stderr}")
113
- return None
114
-
115
- def generate_audio(texto, modelo_de_voz, velocidade, tom, volume):
116
- actual_voice = extract_voice_name(modelo_de_voz)
117
- rate_str = f"+{velocidade}%" if velocidade >= 0 else f"{velocidade}%"
118
- pitch_str = f"+{tom}Hz" if tom >= 0 else f"{tom}Hz"
119
- volume_str = f"+{volume}%" if volume >= 0 else f"{volume}%"
120
-
121
- output_dir = "output"
122
- os.makedirs(output_dir, exist_ok=True)
123
- output_file = os.path.join(output_dir, "new_audio.mp3")
124
-
125
- cmd = [
126
- "edge-tts",
127
- "--rate=" + rate_str,
128
- "--pitch=" + pitch_str,
129
- "--volume=" + volume_str,
130
- "-v", actual_voice,
131
- "-t", texto,
132
- "--write-media", output_file
133
- ]
134
-
135
- print("Gerando áudio...")
136
- try:
137
- subprocess.run(cmd, check=True)
138
- except subprocess.CalledProcessError as e:
139
- print("Erro ao gerar áudio:", e)
140
- return None
141
-
142
- print("Áudio gerado com sucesso!")
143
- return output_file
144
-
145
- def generate_audio_from_file(file_path, modelo_de_voz, velocidade, tom, volume):
146
- actual_voice = extract_voice_name(modelo_de_voz)
147
- rate_str = f"+{velocidade}%" if velocidade >= 0 else f"{velocidade}%"
148
- pitch_str = f"+{tom}Hz" if tom >= 0 else f"{tom}Hz"
149
- volume_str = f"+{volume}%" if volume >= 0 else f"{volume}%"
150
-
151
- output_dir = "output"
152
- os.makedirs(output_dir, exist_ok=True)
153
- output_file = os.path.join(output_dir, "new_audio.mp3")
154
-
155
- cmd = [
156
- "edge-tts",
157
- "-f", file_path,
158
- "--rate=" + rate_str,
159
- "--pitch=" + pitch_str,
160
- "--volume=" + volume_str,
161
- "-v", actual_voice,
162
- "--write-media", output_file
163
- ]
164
-
165
- print("Gerando áudio do arquivo...")
166
- try:
167
- subprocess.run(cmd, check=True)
168
- except subprocess.CalledProcessError as e:
169
- print("Erro ao gerar áudio:", e)
170
- return None
171
-
172
- print("Áudio gerado com sucesso!")
173
- return output_file
174
-
175
- def controlador_generate_audio_from_file(file, voice_model_input, speed_input, pitch_input, volume_input, checkbox_cortar_silencio):
176
- if file is None:
177
- return None
178
-
179
- temp_file_path = file
180
- audio_file = generate_audio_from_file(temp_file_path, voice_model_input, speed_input, pitch_input, volume_input)
181
-
182
- if audio_file:
183
- print("Áudio gerado com sucesso:", audio_file)
184
- if checkbox_cortar_silencio:
185
- print("Cortando silêncio...")
186
- remove_silence(audio_file, audio_file)
187
- print("Silêncio removido com sucesso!")
188
- else:
189
- print("Erro ao gerar áudio.")
190
-
191
- return audio_file
192
-
193
- def timetoms(time_obj):
194
- return time_obj.hours * 3600000 + time_obj.minutes * 60000 + time_obj.seconds * 1000 + time_obj.milliseconds
195
-
196
- async def merge_audio_files(output_folder, srt_file):
197
- subs = pysrt.open(str(srt_file))
198
- final_audio = AudioSegment.silent(duration=0)
199
- base_name = Path(srt_file).stem
200
- audio_dir = Path(output_folder)
201
- total_files = len(subs)
202
- additional_silence_duration = 1000
203
-
204
- with tqdm(total=total_files, desc=f"Mesclando áudios para {base_name}", unit="segmento") as pbar:
205
- current_time = 0
206
- for i, sub in enumerate(subs, start=1):
207
- start_time = timetoms(sub.start)
208
- end_time = timetoms(sub.end)
209
- audio_file = audio_dir / f"{sub.index:02d}.mp3"
210
-
211
- if audio_file.exists():
212
- audio = AudioSegment.from_mp3(str(audio_file))
213
- audio_segment = audio
214
- else:
215
- print(f"\nArquivo de áudio não encontrado: {audio_file}")
216
- audio_segment = AudioSegment.silent(duration=end_time - start_time)
217
- pbar.update(1)
218
-
219
- if i == 1 and start_time > 0:
220
- silence = AudioSegment.silent(duration=start_time)
221
- final_audio += silence
222
- current_time = start_time
223
-
224
- if start_time > current_time:
225
- silence_duration = start_time - current_time
226
- silence = AudioSegment.silent(duration=silence_duration)
227
- final_audio += silence
228
-
229
- final_audio += audio_segment
230
- current_time = end_time
231
-
232
- final_audio += AudioSegment.silent(duration=additional_silence_duration)
233
-
234
- srt_output_dir = Path("output/srt_output")
235
- srt_output_dir.mkdir(parents=True, exist_ok=True)
236
- output_file = srt_output_dir / f"{base_name}_final.mp3"
237
- final_audio.export(str(output_file), format="mp3")
238
- print(f"\nÁudio final salvo em: {output_file}\n")
239
- return str(output_file)
240
-
241
- async def adjust_audio_speed(input_file, output_file, target_duration_ms):
242
- audio = AudioSegment.from_mp3(input_file)
243
- original_duration_ms = len(audio)
244
-
245
- if original_duration_ms == 0:
246
- print(f"Erro: Áudio em {input_file} tem duração zero.")
247
- return audio
248
-
249
- speed_factor = original_duration_ms / target_duration_ms
250
- adjusted_audio = audio.speedup(playback_speed=speed_factor) if speed_factor > 1 else audio._spawn(audio.raw_data, overrides={"frame_rate": int(audio.frame_rate * speed_factor)})
251
-
252
- if len(adjusted_audio) > target_duration_ms:
253
- adjusted_audio = adjusted_audio[:target_duration_ms]
254
- elif len(adjusted_audio) < target_duration_ms:
255
- adjusted_audio += AudioSegment.silent(duration=target_duration_ms - len(adjusted_audio))
256
-
257
- adjusted_audio.export(output_file, format="mp3")
258
- return adjusted_audio
259
-
260
- async def process_srt_file(srt_file, voice, output_dir, pitch, volume, progress=None):
261
- from edge_tts import Communicate as EdgeTTS
262
- subs = pysrt.open(srt_file)
263
- output_dir = Path(output_dir)
264
- output_dir.mkdir(parents=True, exist_ok=True)
265
-
266
- total_indices = len(subs)
267
- batches = [list(range(i, min(i + 2, total_indices))) for i in range(0, total_indices, 2)]
268
-
269
- pitch_str = f"+{pitch}Hz" if pitch >= 0 else f"{pitch}Hz"
270
- volume_str = f"+{volume}%" if volume >= 0 else f"{volume}%"
271
-
272
- with tqdm(total=total_indices, desc="Gerando e ajustando áudios com EdgeTTS", unit="segmento") as pbar:
273
- for batch in batches:
274
- tasks = []
275
- for i in batch:
276
- sub = subs[i]
277
- output_file = output_dir / f"{sub.index:02d}.mp3"
278
- temp_file = output_dir / f"{sub.index:02d}_temp.mp3"
279
- target_duration_ms = timetoms(sub.end) - timetoms(sub.start)
280
-
281
- if not output_file.exists() or output_file.stat().st_size == 0:
282
- tts = EdgeTTS(text=sub.text, voice=voice, pitch=pitch_str, volume=volume_str)
283
- tasks.append(tts.save(str(temp_file)))
284
-
285
- if tasks:
286
- await asyncio.gather(*tasks)
287
-
288
- for i in batch:
289
- sub = subs[i]
290
- temp_file = output_dir / f"{sub.index:02d}_temp.mp3"
291
- output_file = output_dir / f"{sub.index:02d}.mp3"
292
- target_duration_ms = timetoms(sub.end) - timetoms(sub.start)
293
-
294
- if temp_file.exists():
295
- await adjust_audio_speed(temp_file, output_file, target_duration_ms)
296
- os.remove(temp_file)
297
- pbar.update(1)
298
-
299
- final_audio = await merge_audio_files(output_dir, srt_file)
300
-
301
- if srt_temp_deleta:
302
- shutil.rmtree(output_dir, ignore_errors=True)
303
- print(f"Pasta temporária {output_dir} apagada.")
304
- else:
305
- print(f"Pasta temporária {output_dir} mantida.")
306
-
307
- return final_audio
308
-
309
- async def controlador_process_srt_file_async(srt_file, voice_model_input, pitch_input, volume_input):
310
- if srt_file is None:
311
- return None, gr.update() # Retorna uma tupla com o tamanho correto de saídas
312
-
313
- actual_voice = extract_voice_name(voice_model_input)
314
- output_dir = "output/srt_temp"
315
-
316
- # Agora use 'await' em vez de 'asyncio.run()'
317
- audio_file = await process_srt_file(srt_file, actual_voice, output_dir, pitch_input, volume_input)
318
-
319
- # Atualiza a lista de áudios após a geração
320
- updated_list = listar_audios()
321
-
322
- return audio_file, gr.update(choices=updated_list)
323
-
324
- def listar_audios():
325
- try:
326
- srt_output_dir = "output/srt_output"
327
- if not os.path.exists(srt_output_dir):
328
- os.makedirs(srt_output_dir, exist_ok=True)
329
- return ["Nenhum áudio gerado ainda"]
330
- arquivos = [f for f in os.listdir(srt_output_dir) if f.endswith(('.mp3', '.wav'))]
331
- return arquivos if arquivos else ["Nenhum áudio gerado ainda"]
332
- except Exception as e:
333
- print(f"Erro ao listar áudios: {e}")
334
- return ["Erro ao listar arquivos"]
335
-
336
- def tocar_audio(arquivo):
337
- if arquivo and arquivo != "Nenhum áudio gerado ainda":
338
- return f"output/srt_output/{arquivo}"
339
- return None
340
-
341
- initialize_voices()
342
-
343
- with gr.Blocks(theme=gr.themes.Default(primary_hue="green", secondary_hue="blue"), title="QuickTTS") as iface:
344
- gr.Markdown(badges)
345
- gr.Markdown(description)
346
-
347
- voices_data = load_voices()
348
- available_languages = list(voices_data.keys())
349
-
350
- with gr.Tabs():
351
- with gr.TabItem("Edge-TTS"):
352
- gr.Markdown("É ilimitado, podendo até mesmo colocar um livro inteiro, mas claro, tem a questão de tempo, quanto maior o texto, mais demorado é.")
353
-
354
- with gr.Row():
355
- language_input = gr.Dropdown(
356
- choices=available_languages,
357
- label="Idioma",
358
- value=available_languages[52] if available_languages else None
359
- )
360
- initial_voices = get_voice_options(available_languages[52], voices_data) if available_languages else []
361
- voice_model_input = gr.Dropdown(
362
- choices=initial_voices,
363
- label="Modelo de Voz",
364
- value=initial_voices[0] if initial_voices else None
365
- )
366
-
367
- language_input.change(
368
- fn=update_voice_options,
369
- inputs=[language_input],
370
- outputs=[voice_model_input]
371
- )
372
-
373
- audio_input = gr.Textbox(label="Texto", value='Texto de exemplo!', interactive=True)
374
-
375
- with gr.Row():
376
- with gr.Column():
377
- speed_input = gr.Slider(minimum=-200, maximum=200, label="Velocidade (%)", value=0, interactive=True)
378
- with gr.Column():
379
- pitch_input = gr.Slider(minimum=-100, maximum=100, label="Tom (Hz)", value=0, interactive=True)
380
- with gr.Column():
381
- volume_input = gr.Slider(minimum=-99, maximum=100, label="Volume (%)", value=0, interactive=True)
382
-
383
- checkbox_cortar_silencio = gr.Checkbox(label="Cortar Silencio", interactive=True)
384
- audio_output = gr.Audio(label="Resultado", type="filepath", interactive=False)
385
-
386
- with gr.Row():
387
- edgetts_button = gr.Button(value="Falar")
388
- edgetts_button.click(
389
- controlador_generate_audio_from_file,
390
- inputs=[audio_input, voice_model_input, speed_input, pitch_input, volume_input, checkbox_cortar_silencio],
391
- outputs=[audio_output]
392
- )
393
- clear_button = gr.ClearButton(audio_input, value='Limpar')
394
-
395
- update_voices_btn = gr.Button(value="Atualizar Lista de Vozes")
396
- update_voices_btn.click(
397
- fn=update_voices_and_refresh,
398
- inputs=[],
399
- outputs=[language_input, voice_model_input]
400
- )
401
- gr.Markdown("Agradecimentos a rany2 pelo Edge-TTS")
402
-
403
- with gr.TabItem("Lote (Arquivo txt)"):
404
- gr.Markdown("Carregar texto de um arquivo")
405
- with gr.Row():
406
- language_input_file = gr.Dropdown(
407
- choices=available_languages,
408
- label="Idioma",
409
- value=available_languages[52] if available_languages else None
410
- )
411
- initial_voices = get_voice_options(available_languages[52], voices_data) if available_languages else []
412
- voice_model_input_file = gr.Dropdown(
413
- choices=initial_voices,
414
- label="Modelo de Voz",
415
- value=initial_voices[0] if initial_voices else None
416
- )
417
-
418
- language_input_file.change(
419
- fn=update_voice_options,
420
- inputs=[language_input_file],
421
- outputs=[voice_model_input_file]
422
- )
423
- gr.Markdown("O programa vai ler linha por linha e entregar em um único áudio")
424
- file_input = gr.File(label="Arquivo de Texto", file_types=[".txt"], type="filepath")
425
-
426
- with gr.Row():
427
- with gr.Column():
428
- speed_input_file = gr.Slider(minimum=-200, maximum=200, label="Velocidade (%)", value=0, interactive=True)
429
- with gr.Column():
430
- pitch_input_file = gr.Slider(minimum=-100, maximum=100, label="Tom (Hz)", value=0, interactive=True)
431
- with gr.Column():
432
- volume_input_file = gr.Slider(minimum=-99, maximum=100, label="Volume (%)", value=0, interactive=True)
433
-
434
- checkbox_cortar_silencio_file = gr.Checkbox(label="Cortar Silencio", interactive=True)
435
- audio_output_file = gr.Audio(label="Resultado", type="filepath", interactive=False)
436
- with gr.Row():
437
- edgetts_button_file = gr.Button(value="Falar")
438
- edgetts_button_file.click(
439
- controlador_generate_audio_from_file,
440
- inputs=[file_input, voice_model_input_file, speed_input_file, pitch_input_file, volume_input_file, checkbox_cortar_silencio_file],
441
- outputs=[audio_output_file]
442
- )
443
- clear_button_file = gr.ClearButton(file_input, value='Limpar')
444
-
445
- gr.Markdown("Agradecimentos a rany2 pelo Edge-TTS")
446
-
447
- with gr.TabItem("Ler .SRT"):
448
- gr.Markdown("Carregar um arquivo SRT e gerenciar áudios sincronizados com os tempos das legendas.<br><br>Se você precisa de dublagem por IA para seus vídeos do YouTube, cursos e outros projetos, entre em contato comigo:<br>https://www.instagram.com/rafael.godoy.ebert/<br>Este é apenas um teste para brincar e explorar a funcionalidade básica. Tenho uma versão mais completa e personalizada que pode atender às suas necessidades específicas, incluindo clone de voz, entonação na fala e outras funcionalidades.")
449
-
450
- with gr.Tabs():
451
- with gr.TabItem("Gerar áudio"):
452
- gr.Markdown("A velocidade é ajustada automaticamente para cada legenda.")
453
- with gr.Row():
454
- language_input_srt = gr.Dropdown(
455
- choices=available_languages,
456
- label="Idioma",
457
- value=available_languages[52] if available_languages else None
458
- )
459
- initial_voices = get_voice_options(available_languages[52], voices_data) if available_languages else []
460
- voice_model_input_srt = gr.Dropdown(
461
- choices=initial_voices,
462
- label="Modelo de Voz",
463
- value=initial_voices[0] if initial_voices else None
464
- )
465
-
466
- language_input_srt.change(
467
- fn=update_voice_options,
468
- inputs=[language_input_srt],
469
- outputs=[voice_model_input_srt]
470
- )
471
-
472
- srt_input = gr.File(label="Arquivo SRT", file_types=[".srt"], type="filepath")
473
-
474
- with gr.Row():
475
- with gr.Column():
476
- pitch_input_srt = gr.Slider(minimum=-100, maximum=100, label="Tom (Hz)", value=0, interactive=True)
477
- with gr.Column():
478
- volume_input_srt = gr.Slider(minimum=-99, maximum=200, label="Volume (%)", value=0, interactive=True)
479
-
480
- audio_output_srt = gr.Audio(label="Resultado", type="filepath", interactive=False)
481
- with gr.Row():
482
- srt_button = gr.Button(value="Gerar Áudio")
483
- clear_button_srt = gr.ClearButton(srt_input, value='Limpar')
484
-
485
- # Adicione um componente de status na sua UI
486
- status_srt = gr.Markdown(visible=False)
487
-
488
- # Modifique sua função de clique
489
- async def generate_and_update_list(srt_file, voice, pitch, volume, progress=gr.Progress(track_tqdm=True)):
490
- if not srt_file:
491
- return None, gr.update(), gr.update(value="Por favor, carregue um arquivo SRT.", visible=True)
492
-
493
- progress(0, desc="Iniciando processamento...")
494
- status_srt.update(visible=False) # Esconde a mensagem de erro antiga
495
-
496
- try:
497
- # Passe o objeto 'progress' para a função que usa tqdm
498
- audio_file = await process_srt_file(srt_file, extract_voice_name(voice), "output/srt_temp", pitch, volume, progress)
499
- updated_list = listar_audios()
500
- return audio_file, gr.update(choices=updated_list)
501
- except Exception as e:
502
- error_message = f"Ocorreu um erro: {e}"
503
- print(error_message)
504
- return None, gr.update(), gr.update(value=error_message, visible=True)
505
-
506
- srt_button.click(
507
- fn=controlador_process_srt_file_async, # Use a nova função async
508
- inputs=[srt_input, voice_model_input_srt, pitch_input_srt, volume_input_srt],
509
- outputs=[audio_output_srt, audio_input], # Atualize o dropdown diretamente
510
- queue=True
511
- )
512
-
513
- gr.Markdown("Agradecimentos a rany2 pelo Edge-TTS")
514
-
515
- with gr.TabItem("Arquivos gerados"):
516
- gr.Markdown("Lista de arquivos de áudio gerados na pasta 'output/srt_output'.")
517
- audio_list = gr.Dropdown(
518
- label="Arquivos de áudio",
519
- choices=listar_audios(),
520
- value=None,
521
- interactive=True,
522
- allow_custom_value=True
523
- )
524
- play_button = gr.Button(value="Tocar")
525
- audio_player = gr.Audio(label="Reproduzir", type="filepath", interactive=False)
526
- status_message = gr.Textbox(label="Status", interactive=False, visible=True)
527
-
528
- def update_audio_list():
529
- arquivos = listar_audios()
530
- return gr.update(choices=arquivos, value=None), "Lista atualizada com sucesso" if "Erro" not in arquivos[0] else "Erro ao atualizar lista"
531
-
532
- refresh_button = gr.Button(value="Atualizar Lista")
533
- refresh_button.click(
534
- fn=update_audio_list,
535
- inputs=[],
536
- outputs=[audio_list, status_message],
537
- queue=True
538
- )
539
-
540
- play_button.click(
541
- fn=tocar_audio,
542
- inputs=[audio_list],
543
- outputs=[audio_player],
544
- queue=True
545
- )
546
-
547
- gr.Markdown("""
548
- Desenvolvido por Rafael Godoy <br>
549
- Apoie o projeto pelo https://nubank.com.br/pagar/1ls6a4/0QpSSbWBSq, qualquer valor é bem vindo.
550
- """)
551
- iface.launch(share=True)
 
1
+ # app.py
2
+
3
+ import gradio as gr
4
+ from get_voices import get_voices
5
+ from header import badges, description
6
+
7
+ # --- Imports from our new modules ---
8
+ from utils import listar_audios, tocar_audio
9
+ from edgeTTS import (
10
+ load_voices, get_voice_options, controlador_generate_audio,
11
+ controlador_generate_audio_from_file, controlador_process_srt_file
12
+ )
13
+ from tiktokTTS import (
14
+ TIKTOK_TTS_AVAILABLE, TIKTOK_VOICES_CATEGORIZED, get_tiktok_voice_options,
15
+ controlador_generate_audio_tiktok, controlador_process_srt_file_tiktok
16
+ )
17
+
18
+ # --- Global Settings ---
19
+ srt_temp_deleta = True
20
+
21
+ # --- UI Helper Functions ---
22
+ def update_edge_voice_options(language, voices_data):
23
+ voice_options = get_voice_options(language, voices_data)
24
+ if voice_options:
25
+ return gr.update(choices=voice_options, value=voice_options[0], interactive=True)
26
+ return gr.update(choices=[], value=None, interactive=False)
27
+
28
+ def update_tiktok_voice_options(language):
29
+ voices = get_tiktok_voice_options(language)
30
+ return gr.update(choices=voices, value=voices[0] if voices else None)
31
+
32
+ def update_voices_and_refresh():
33
+ get_voices()
34
+ voices_data = load_voices()
35
+ available_languages = list(voices_data.keys())
36
+ initial_voices = get_voice_options(available_languages[0], voices_data) if available_languages else []
37
+ return (
38
+ gr.update(choices=available_languages, value=available_languages[0] if available_languages else None),
39
+ gr.update(choices=initial_voices, value=initial_voices[0] if initial_voices else None)
40
+ )
41
+
42
+ # --- Gradio Interface ---
43
+ with gr.Blocks(theme=gr.themes.Default(primary_hue="green", secondary_hue="blue"), title="QuickTTS") as iface:
44
+ gr.Markdown(badges)
45
+ gr.Markdown(description)
46
+
47
+ edge_voices_data = load_voices()
48
+ edge_available_languages = list(edge_voices_data.keys())
49
+ tiktok_available_categories = list(TIKTOK_VOICES_CATEGORIZED.keys())
50
+
51
+ with gr.Tabs():
52
+ with gr.TabItem("TTS"):
53
+ gr.Markdown("Gere áudio a partir de texto usando diferentes provedores.")
54
+ provider_choice = gr.Radio(choices=["Edge-TTS", "TikTok"], value="Edge-TTS", label="Escolha o Provedor de TTS", interactive=TIKTOK_TTS_AVAILABLE)
55
+
56
+ with gr.Column(visible=True) as edge_tts_ui:
57
+ with gr.Row():
58
+ lang_val = edge_available_languages[52] if len(edge_available_languages) > 52 else None
59
+ language_input = gr.Dropdown(choices=edge_available_languages, label="Idioma", value=lang_val)
60
+ initial_voices = get_voice_options(lang_val, edge_voices_data) if lang_val else []
61
+ voice_model_input = gr.Dropdown(choices=initial_voices, label="Modelo de Voz", value=initial_voices[0] if initial_voices else None)
62
+ audio_input = gr.Textbox(label="Texto", value='Texto de exemplo!', interactive=True)
63
+ with gr.Row():
64
+ speed_input = gr.Slider(-200, 200, label="Velocidade (%)", value=0, interactive=True)
65
+ pitch_input = gr.Slider(-100, 100, label="Tom (Hz)", value=0, interactive=True)
66
+ volume_input = gr.Slider(-99, 100, label="Volume (%)", value=0, interactive=True)
67
+ checkbox_cortar_silencio = gr.Checkbox(label="Cortar Silencio", interactive=True)
68
+
69
+ with gr.Column(visible=False) as tiktok_tts_ui:
70
+ gr.Markdown("Use as vozes populares do TikTok.")
71
+ with gr.Row():
72
+ tiktok_category_input = gr.Dropdown(choices=tiktok_available_categories, label="Idioma / Categoria", value=tiktok_available_categories[0])
73
+ initial_tiktok_voices = get_tiktok_voice_options(tiktok_available_categories[0])
74
+ tiktok_voice_model_input = gr.Dropdown(choices=initial_tiktok_voices, label="Modelo de Voz", value=initial_tiktok_voices[0] if initial_tiktok_voices else None)
75
+ tiktok_audio_input = gr.Textbox(label="Texto", value='Olá, isso é um teste com a voz do TikTok!', interactive=True)
76
+ # ADICIONADO: Checkbox para o TikTok
77
+ checkbox_cortar_silencio_tiktok = gr.Checkbox(label="Cortar Silencio", interactive=True)
78
+
79
+ audio_output = gr.Audio(label="Resultado", type="filepath", interactive=False)
80
+ with gr.Row():
81
+ gerar_button = gr.Button(value="Falar")
82
+ clear_button = gr.ClearButton(components=[audio_input, tiktok_audio_input], value='Limpar Texto')
83
+
84
+ update_voices_btn = gr.Button(value="Atualizar Lista de Vozes (Edge-TTS)")
85
+ gr.Markdown("Agradecimentos a rany2 pelo Edge-TTS e outros desenvolvedores pelo TikTok-Voice-TTS")
86
+
87
+ # --- Event Handlers for TTS Tab ---
88
+ language_input.change(fn=lambda lang: update_edge_voice_options(lang, edge_voices_data), inputs=language_input, outputs=voice_model_input)
89
+ tiktok_category_input.change(fn=update_tiktok_voice_options, inputs=tiktok_category_input, outputs=tiktok_voice_model_input)
90
+ update_voices_btn.click(fn=update_voices_and_refresh, inputs=[], outputs=[language_input, voice_model_input])
91
+
92
+ def switch_provider_ui(provider):
93
+ return gr.update(visible=provider == "Edge-TTS"), gr.update(visible=provider == "TikTok")
94
+ provider_choice.change(fn=switch_provider_ui, inputs=provider_choice, outputs=[edge_tts_ui, tiktok_tts_ui])
95
+
96
+ # MODIFICADO: Função principal agora aceita o novo checkbox
97
+ def gerar_audio_principal(provider, edge_text, edge_voice, speed, pitch, vol, cut_silence, tiktok_voice, tiktok_text, tiktok_cut_silence):
98
+ if provider == "Edge-TTS":
99
+ return controlador_generate_audio(edge_text, edge_voice, speed, pitch, vol, cut_silence)
100
+ else:
101
+ return controlador_generate_audio_tiktok(tiktok_voice, tiktok_text, None, tiktok_cut_silence)
102
+
103
+ # MODIFICADO: Lista de inputs do botão foi atualizada
104
+ gerar_button.click(
105
+ fn=gerar_audio_principal,
106
+ inputs=[
107
+ provider_choice, audio_input, voice_model_input, speed_input, pitch_input, volume_input, checkbox_cortar_silencio,
108
+ tiktok_voice_model_input, tiktok_audio_input, checkbox_cortar_silencio_tiktok
109
+ ],
110
+ outputs=audio_output
111
+ )
112
+
113
+ with gr.TabItem("Lote (Arquivo txt)"):
114
+ provider_choice_file = gr.Radio(choices=["Edge-TTS", "TikTok"], value="Edge-TTS", label="Escolha o Provedor de TTS", interactive=TIKTOK_TTS_AVAILABLE)
115
+ file_input = gr.File(label="Arquivo de Texto", file_types=[".txt"], type="filepath")
116
+
117
+ with gr.Column(visible=True) as edge_tts_ui_file:
118
+ with gr.Row():
119
+ lang_val_file = edge_available_languages[52] if len(edge_available_languages) > 52 else None
120
+ language_input_file = gr.Dropdown(choices=edge_available_languages, label="Idioma", value=lang_val_file)
121
+ initial_voices_file = get_voice_options(lang_val_file, edge_voices_data) if lang_val_file else []
122
+ voice_model_input_file = gr.Dropdown(choices=initial_voices_file, label="Modelo de Voz", value=initial_voices_file[0] if initial_voices_file else None)
123
+ with gr.Row():
124
+ speed_input_file = gr.Slider(-200, 200, label="Velocidade (%)", value=0, interactive=True)
125
+ pitch_input_file = gr.Slider(-100, 100, label="Tom (Hz)", value=0, interactive=True)
126
+ volume_input_file = gr.Slider(-99, 100, label="Volume (%)", value=0, interactive=True)
127
+ checkbox_cortar_silencio_file = gr.Checkbox(label="Cortar Silencio", interactive=True)
128
+
129
+ with gr.Column(visible=False) as tiktok_tts_ui_file:
130
+ with gr.Row():
131
+ tiktok_category_input_file = gr.Dropdown(choices=tiktok_available_categories, label="Idioma / Categoria", value=tiktok_available_categories[0])
132
+ initial_tiktok_voices_file = get_tiktok_voice_options(tiktok_available_categories[0])
133
+ tiktok_voice_model_input_file = gr.Dropdown(choices=initial_tiktok_voices_file, label="Modelo de Voz", value=initial_tiktok_voices_file[0] if initial_tiktok_voices_file else None)
134
+ # ADICIONADO: Checkbox para o TikTok em lote
135
+ checkbox_cortar_silencio_tiktok_file = gr.Checkbox(label="Cortar Silencio", interactive=True)
136
+
137
+ audio_output_file = gr.Audio(label="Resultado", type="filepath", interactive=False)
138
+ with gr.Row():
139
+ gerar_button_file = gr.Button(value="Falar")
140
+ clear_button_file = gr.ClearButton(file_input, value='Limpar')
141
+
142
+ # --- Event Handlers for Lote Tab ---
143
+ language_input_file.change(fn=lambda lang: update_edge_voice_options(lang, edge_voices_data), inputs=language_input_file, outputs=voice_model_input_file)
144
+ tiktok_category_input_file.change(fn=update_tiktok_voice_options, inputs=tiktok_category_input_file, outputs=tiktok_voice_model_input_file)
145
+ provider_choice_file.change(fn=switch_provider_ui, inputs=provider_choice_file, outputs=[edge_tts_ui_file, tiktok_tts_ui_file])
146
+
147
+ # MODIFICADO: Função principal agora aceita o novo checkbox
148
+ def gerar_audio_lote_principal(provider, file, edge_voice, speed, pitch, vol, cut_silence, tiktok_voice, tiktok_cut_silence):
149
+ if provider == "Edge-TTS":
150
+ return controlador_generate_audio_from_file(file, edge_voice, speed, pitch, vol, cut_silence)
151
+ else:
152
+ return controlador_generate_audio_tiktok(tiktok_voice, None, file, tiktok_cut_silence)
153
+
154
+ # MODIFICADO: Lista de inputs do botão foi atualizada
155
+ gerar_button_file.click(
156
+ fn=gerar_audio_lote_principal,
157
+ inputs=[
158
+ provider_choice_file, file_input, voice_model_input_file, speed_input_file, pitch_input_file, volume_input_file, checkbox_cortar_silencio_file,
159
+ tiktok_voice_model_input_file, checkbox_cortar_silencio_tiktok_file
160
+ ],
161
+ outputs=audio_output_file
162
+ )
163
+
164
+ with gr.TabItem("Ler .SRT"):
165
+ gr.Markdown("Gere áudio sincronizado a partir de um arquivo .SRT usando o provedor de sua escolha.")
166
+
167
+ with gr.Tabs():
168
+ with gr.TabItem("Gerar áudio"):
169
+ # ADICIONADO: Seletor de provedor para SRT
170
+ provider_choice_srt = gr.Radio(choices=["Edge-TTS", "TikTok"], value="Edge-TTS", label="Escolha o Provedor de TTS", interactive=TIKTOK_TTS_AVAILABLE)
171
+
172
+ # --- UI do Edge-TTS para SRT ---
173
+ with gr.Column(visible=True) as edge_tts_ui_srt:
174
+ gr.Markdown("A velocidade é ajustada automaticamente para cada legenda.")
175
+ with gr.Row():
176
+ lang_val_srt = edge_available_languages[52] if len(edge_available_languages) > 52 else None
177
+ language_input_srt = gr.Dropdown(choices=edge_available_languages, label="Idioma", value=lang_val_srt)
178
+ initial_voices_srt = get_voice_options(lang_val_srt, edge_voices_data) if lang_val_srt else []
179
+ voice_model_input_srt = gr.Dropdown(choices=initial_voices_srt, label="Modelo de Voz", value=initial_voices_srt[0] if initial_voices_srt else None)
180
+ with gr.Row():
181
+ pitch_input_srt = gr.Slider(-100, 100, label="Tom (Hz)", value=0, interactive=True)
182
+ volume_input_srt = gr.Slider(-99, 200, label="Volume (%)", value=0, interactive=True)
183
+
184
+ # --- UI do TikTok para SRT ---
185
+ with gr.Column(visible=False) as tiktok_tts_ui_srt:
186
+ gr.Markdown("A velocidade do áudio será ajustada automaticamente para cada legenda. Tom e volume não são aplicáveis.")
187
+ with gr.Row():
188
+ tiktok_category_input_srt = gr.Dropdown(choices=tiktok_available_categories, label="Idioma / Categoria", value=tiktok_available_categories[0])
189
+ initial_tiktok_voices_srt = get_tiktok_voice_options(tiktok_available_categories[0])
190
+ tiktok_voice_model_input_srt = gr.Dropdown(choices=initial_tiktok_voices_srt, label="Modelo de Voz", value=initial_tiktok_voices_srt[0] if initial_tiktok_voices_srt else None)
191
+
192
+ # --- Componentes Comuns ---
193
+ srt_input = gr.File(label="Arquivo SRT", file_types=[".srt"], type="filepath")
194
+ audio_output_srt = gr.Audio(label="Resultado", type="filepath", interactive=False)
195
+ audio_list_target = gr.Dropdown(visible=False)
196
+ with gr.Row():
197
+ srt_button = gr.Button(value="Gerar Áudio")
198
+ clear_button_srt = gr.ClearButton(srt_input, value='Limpar')
199
+
200
+ # --- Lógica e Event Handlers ---
201
+ def switch_provider_ui_srt(provider):
202
+ return gr.update(visible=provider == "Edge-TTS"), gr.update(visible=provider == "TikTok")
203
+
204
+ provider_choice_srt.change(fn=switch_provider_ui_srt, inputs=provider_choice_srt, outputs=[edge_tts_ui_srt, tiktok_tts_ui_srt])
205
+ language_input_srt.change(fn=lambda lang: update_edge_voice_options(lang, edge_voices_data), inputs=language_input_srt, outputs=voice_model_input_srt)
206
+ tiktok_category_input_srt.change(fn=update_tiktok_voice_options, inputs=tiktok_category_input_srt, outputs=tiktok_voice_model_input_srt)
207
+
208
+ def controlador_srt_principal(provider, srt_file, edge_voice, pitch, volume, tiktok_voice):
209
+ if provider == "Edge-TTS":
210
+ audio_file = controlador_process_srt_file(srt_file, edge_voice, pitch, volume, srt_temp_deleta)
211
+ else: # TikTok
212
+ audio_file = controlador_process_srt_file_tiktok(srt_file, tiktok_voice, srt_temp_deleta)
213
+
214
+ return audio_file, gr.update(choices=listar_audios())
215
+
216
+ srt_button.click(
217
+ fn=controlador_srt_principal,
218
+ inputs=[provider_choice_srt, srt_input, voice_model_input_srt, pitch_input_srt, volume_input_srt, tiktok_voice_model_input_srt],
219
+ outputs=[audio_output_srt, audio_list_target],
220
+ queue=True
221
+ )
222
+ gr.Markdown("Agradecimentos a rany2 pelo Edge-TTS")
223
+
224
+ with gr.TabItem("Arquivos gerados"):
225
+ audio_list = gr.Dropdown(label="Arquivos de áudio", choices=listar_audios(), interactive=True)
226
+ audio_list_target.change(lambda x: x, inputs=[audio_list_target], outputs=[audio_list])
227
+ play_button = gr.Button(value="Tocar")
228
+ refresh_button = gr.Button(value="Atualizar Lista")
229
+ audio_player = gr.Audio(label="Reproduzir", type="filepath", interactive=False)
230
+ status_message = gr.Textbox(label="Status", interactive=False, visible=True)
231
+
232
+ def update_audio_list():
233
+ arquivos = listar_audios()
234
+ return gr.update(choices=arquivos, value=None), "Lista atualizada."
235
+
236
+ refresh_button.click(fn=update_audio_list, outputs=[audio_list, status_message], queue=True)
237
+ play_button.click(fn=tocar_audio, inputs=[audio_list], outputs=[audio_player], queue=True)
238
+
239
+ gr.Markdown("""
240
+ Desenvolvido por Rafael Godoy <br>
241
+ Apoie o projeto pelo https://nubank.com.br/pagar/1ls6a4/0QpSSbWBSq, qualquer valor é bem vindo.
242
+ """)
243
+ iface.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
edgeTTS.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # edgeTTS.py
2
+
3
+ import subprocess
4
+ import os
5
+ import json
6
+ import asyncio
7
+ from pathlib import Path
8
+ import pysrt
9
+ from tqdm import tqdm
10
+ import shutil
11
+
12
+ # Importa funções do nosso arquivo de utilidades
13
+ from utils import remove_silence, timetoms, merge_audio_files, adjust_audio_speed
14
+
15
+ # --- Funções de Gerenciamento de Voz ---
16
+ def load_voices():
17
+ with open('voices.json', 'r', encoding='utf-8') as f:
18
+ return json.load(f)
19
+
20
+ def get_voice_options(language, voices_data):
21
+ if language in voices_data:
22
+ return [f"{voice['name']} | {voice['gender']}" for voice in voices_data[language]]
23
+ return []
24
+
25
+ def extract_voice_name(formatted_voice):
26
+ return formatted_voice.split(" | ")[0]
27
+
28
+ # --- Funções de Geração de Áudio (Edge-TTS) ---
29
+ def generate_audio(texto, modelo_de_voz, velocidade, tom, volume):
30
+ actual_voice = extract_voice_name(modelo_de_voz)
31
+ rate_str = f"+{velocidade}%" if velocidade >= 0 else f"{velocidade}%"
32
+ pitch_str = f"+{tom}Hz" if tom >= 0 else f"{tom}Hz"
33
+ volume_str = f"+{volume}%" if volume >= 0 else f"{volume}%"
34
+
35
+ output_dir = "output"
36
+ os.makedirs(output_dir, exist_ok=True)
37
+ output_file = os.path.join(output_dir, "new_audio.mp3")
38
+
39
+ cmd = ["edge-tts", "--rate=" + rate_str, "--pitch=" + pitch_str, "--volume=" + volume_str,
40
+ "-v", actual_voice, "-t", texto, "--write-media", output_file]
41
+
42
+ print("Gerando áudio com Edge-TTS...")
43
+ try:
44
+ subprocess.run(cmd, check=True, capture_output=True, text=True)
45
+ print("Áudio gerado com sucesso!")
46
+ return output_file
47
+ except subprocess.CalledProcessError as e:
48
+ print(f"Erro ao gerar áudio: {e.stderr}")
49
+ return None
50
+
51
+ def generate_audio_from_file(file_path, modelo_de_voz, velocidade, tom, volume):
52
+ actual_voice = extract_voice_name(modelo_de_voz)
53
+ rate_str = f"+{velocidade}%" if velocidade >= 0 else f"{velocidade}%"
54
+ pitch_str = f"+{tom}Hz" if tom >= 0 else f"{tom}Hz"
55
+ volume_str = f"+{volume}%" if volume >= 0 else f"{volume}%"
56
+
57
+ output_dir = "output"
58
+ os.makedirs(output_dir, exist_ok=True)
59
+ output_file = os.path.join(output_dir, "new_audio.mp3")
60
+
61
+ cmd = ["edge-tts", "-f", file_path, "--rate=" + rate_str, "--pitch=" + pitch_str,
62
+ "--volume=" + volume_str, "-v", actual_voice, "--write-media", output_file]
63
+
64
+ print("Gerando áudio do arquivo com Edge-TTS...")
65
+ try:
66
+ subprocess.run(cmd, check=True, capture_output=True, text=True)
67
+ print("Áudio gerado com sucesso!")
68
+ return output_file
69
+ except subprocess.CalledProcessError as e:
70
+ print(f"Erro ao gerar áudio do arquivo: {e.stderr}")
71
+ return None
72
+
73
+ # --- Funções Controladoras (Edge-TTS) ---
74
+ def controlador_generate_audio(audio_input, voice_model_input, speed, pitch, volume, cut_silence):
75
+ audio_file = generate_audio(audio_input, voice_model_input, speed, pitch, volume)
76
+ if audio_file and cut_silence:
77
+ print("Removendo silêncio...")
78
+ remove_silence(audio_file, audio_file)
79
+ print("Silêncio removido.")
80
+ return audio_file
81
+
82
+ def controlador_generate_audio_from_file(file, voice_model_input, speed, pitch, volume, cut_silence):
83
+ if not file: return None
84
+ audio_file = generate_audio_from_file(file.name, voice_model_input, speed, pitch, volume)
85
+ if audio_file and cut_silence:
86
+ print("Cortando silêncio...")
87
+ remove_silence(audio_file, audio_file)
88
+ print("Silêncio removido com sucesso!")
89
+ return audio_file
90
+
91
+ # --- Lógica de Processamento de SRT (Usa Edge-TTS) ---
92
+ async def process_srt_file(srt_file_path, voice, output_dir_str, pitch, volume, srt_temp_deleta):
93
+ from edge_tts import Communicate as EdgeTTS # Importação local para manter dependências contidas
94
+ subs = pysrt.open(srt_file_path)
95
+ output_dir = Path(output_dir_str)
96
+ output_dir.mkdir(parents=True, exist_ok=True)
97
+
98
+ total_indices = len(subs)
99
+ pitch_str = f"+{pitch}Hz" if pitch >= 0 else f"{pitch}Hz"
100
+ volume_str = f"+{volume}%" if volume >= 0 else f"{volume}%"
101
+
102
+ with tqdm(total=total_indices, desc="Gerando e ajustando áudios com EdgeTTS", unit="segmento") as pbar:
103
+ for sub in subs:
104
+ temp_file = output_dir / f"{sub.index:02d}_temp.mp3"
105
+ output_file = output_dir / f"{sub.index:02d}.mp3"
106
+ target_duration_ms = timetoms(sub.end) - timetoms(sub.start)
107
+
108
+ if not output_file.exists() or output_file.stat().st_size == 0:
109
+ tts_edge = EdgeTTS(text=sub.text, voice=voice, pitch=pitch_str, volume=volume_str)
110
+ await tts_edge.save(str(temp_file))
111
+
112
+ if temp_file.exists():
113
+ await adjust_audio_speed(str(temp_file), str(output_file), target_duration_ms)
114
+ os.remove(temp_file)
115
+ pbar.update(1)
116
+
117
+ final_audio = await merge_audio_files(output_dir, srt_file_path)
118
+
119
+ if srt_temp_deleta:
120
+ shutil.rmtree(output_dir, ignore_errors=True)
121
+ print(f"Pasta temporária {output_dir} apagada.")
122
+
123
+ return final_audio
124
+
125
+ def controlador_process_srt_file(srt_file, voice_model_input, pitch, volume, srt_temp_deleta):
126
+ if not srt_file: return None
127
+ actual_voice = extract_voice_name(voice_model_input)
128
+ output_dir = "output/srt_temp"
129
+
130
+ return asyncio.run(process_srt_file(srt_file.name, actual_voice, output_dir, pitch, volume, srt_temp_deleta))
tiktokTTS.py ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # tiktokTTS.py
2
+
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+ import gradio as gr
7
+ import asyncio
8
+ import pysrt
9
+ from tqdm import tqdm
10
+ import shutil
11
+
12
+ # Importa funções utilitárias
13
+ from utils import remove_silence, timetoms, merge_audio_files, adjust_audio_speed
14
+
15
+ # --- Configuração e Imports da Biblioteca TikTok ---
16
+ try:
17
+ sys.path.append(str(Path(__file__).parent / "TikTok_TTS"))
18
+ from TikTok_TTS.tiktok_voice import Voice, tts
19
+ TIKTOK_TTS_AVAILABLE = True
20
+ print("Biblioteca TikTok TTS carregada com sucesso.")
21
+ except ImportError:
22
+ TIKTOK_TTS_AVAILABLE = False
23
+ print("Aviso: Biblioteca TikTok TTS não encontrada. A funcionalidade estará desabilitada.")
24
+ class Voice: pass
25
+ def tts(*args, **kwargs): pass
26
+
27
+ # --- DICIONÁRIO DE VOZES CATEGORIZADAS ---
28
+ TIKTOK_VOICES_CATEGORIZED = {
29
+ 'Português (Brasil)': [
30
+ 'BR_FEMALE_1', 'BR_FEMALE_2', 'BR_FEMALE_3', 'BR_MALE',
31
+ 'BP_FEMALE_IVETE', 'BP_FEMALE_LUDMILLA', 'PT_FEMALE_LHAYS', 'PT_FEMALE_LAIZZA', 'PT_MALE_BUENO'
32
+ ],
33
+ 'Inglês (EUA)': [
34
+ 'US_FEMALE_1', 'US_FEMALE_2', 'US_MALE_1', 'US_MALE_2', 'US_MALE_3', 'US_MALE_4'
35
+ ],
36
+ 'Inglês (Reino Unido)': ['UK_MALE_1', 'UK_MALE_2'],
37
+ 'Inglês (Austrália)': ['AU_FEMALE_1', 'AU_MALE_1'],
38
+ 'Inglês (Personagens Especiais)': [
39
+ 'MALE_JOMBOY', 'MALE_CODY', 'FEMALE_SAMC', 'FEMALE_MAKEUP', 'FEMALE_RICHGIRL',
40
+ 'MALE_ASHMAGIC', 'MALE_OLANTERKKERS', 'MALE_UKNEIGHBOR', 'MALE_UKBUTLER',
41
+ 'FEMALE_SHENNA', 'FEMALE_PANSINO', 'MALE_TREVOR', 'FEMALE_BETTY', 'MALE_CUPID',
42
+ 'FEMALE_GRANDMA', 'MALE_NARRATION', 'MALE_FUNNY', 'FEMALE_EMOTIONAL'
43
+ ],
44
+ 'Inglês Personagens (Filmes e Outros)': [
45
+ 'GHOSTFACE', 'CHEWBACCA', 'C3PO', 'STITCH', 'STORMTROOPER', 'ROCKET',
46
+ 'MADAME_LEOTA', 'GHOST_HOST', 'PIRATE', 'MALE_GRINCH', 'MALE_DEADPOOL', 'MALE_JARVIS'
47
+ ],
48
+ 'Inglês Personagens (Festivos)': [
49
+ 'MALE_XMXS_CHRISTMAS', 'MALE_SANTA_NARRATION', 'MALE_SANTA_EFFECT',
50
+ 'FEMALE_HT_NEYEAR', 'MALE_WIZARD', 'FEMALE_HT_HALLOWEEN'
51
+ ],
52
+ 'Inglês Cantores / Músicas': [
53
+ 'MALE_SING_DEEP_JINGLE', 'SING_FEMALE_ALTO', 'SING_MALE_TENOR', 'SING_FEMALE_WARMY_BREEZE',
54
+ 'SING_MALE_SUNSHINE_SOON', 'SING_FEMALE_GLORIOUS', 'SING_MALE_IT_GOES_UP',
55
+ 'SING_MALE_CHIPMUNK', 'SING_FEMALE_WONDERFUL_WORLD', 'SING_MALE_FUNNY_THANKSGIVING'
56
+ ],
57
+ 'Japonês': [
58
+ 'JP_FEMALE_1', 'JP_FEMALE_2', 'JP_FEMALE_3', 'JP_MALE', 'JP_FEMALE_FUJICOCHAN',
59
+ 'JP_FEMALE_HASEGAWARIONA', 'JP_MALE_KEIICHINAKANO', 'JP_FEMALE_OOMAEAIIKA',
60
+ 'JP_MALE_YUJINCHIGUSA', 'JP_FEMALE_SHIROU', 'JP_MALE_TAMAWAKAZUKI',
61
+ 'JP_FEMALE_KAORISHOJI', 'JP_FEMALE_YAGISHAKI', 'JP_MALE_HIKAKIN', 'JP_FEMALE_REI',
62
+ 'JP_MALE_SHUICHIRO', 'JP_MALE_MATSUDAKE', 'JP_FEMALE_MACHIKORIIITA',
63
+ 'JP_MALE_MATSUO', 'JP_MALE_OSADA'
64
+ ],
65
+ 'Coreano': ['KR_MALE_1', 'KR_FEMALE', 'KR_MALE_2'],
66
+ 'Espanhol': ['ES_MALE', 'ES_MX_MALE'],
67
+ 'Francês': ['FR_MALE_1', 'FR_MALE_2'],
68
+ 'Alemão': ['DE_FEMALE', 'DE_MALE'],
69
+ 'Indonésio': ['ID_FEMALE']
70
+ }
71
+
72
+ def get_tiktok_voice_options(language):
73
+ return TIKTOK_VOICES_CATEGORIZED.get(language, [])
74
+
75
+ # --- Função Controladora de Texto/Arquivo ---
76
+ def controlador_generate_audio_tiktok(voice_str, text, text_file, cut_silence):
77
+ # ... (esta função permanece a mesma)
78
+ if not TIKTOK_TTS_AVAILABLE:
79
+ raise gr.Error("A biblioteca TikTok TTS não está instalada ou configurada corretamente.")
80
+ if not text and text_file is None:
81
+ raise gr.Error("Por favor, forneça um texto ou um arquivo .txt para gerar o áudio.")
82
+ output_dir = "output"; os.makedirs(output_dir, exist_ok=True)
83
+ output_file = os.path.join(output_dir, "tiktok_audio.mp3")
84
+ input_text = text if text else Path(text_file.name).read_text(encoding='utf-8')
85
+ try:
86
+ print(f"Gerando áudio com a voz TikTok: {voice_str}...")
87
+ tts(input_text, Voice[voice_str], output_file)
88
+ print("Áudio TikTok gerado com sucesso!")
89
+ if cut_silence:
90
+ print("Removendo silêncio do áudio TikTok..."); remove_silence(output_file, output_file); print("Silêncio removido.")
91
+ return output_file
92
+ except KeyError:
93
+ raise gr.Error(f"A voz '{voice_str}' não foi encontrada.")
94
+ except Exception as e:
95
+ raise gr.Error(f"Ocorreu um erro: {e}.")
96
+
97
+ # --- NOVA LÓGICA DE PROCESSAMENTO DE SRT PARA TIKTOK ---
98
+
99
+ async def process_srt_file_tiktok(srt_file_path, voice_str, output_dir_str, srt_temp_deleta):
100
+ """Função principal assíncrona para processar SRT com TikTok TTS."""
101
+ subs = pysrt.open(srt_file_path)
102
+ output_dir = Path(output_dir_str)
103
+ output_dir.mkdir(parents=True, exist_ok=True)
104
+
105
+ with tqdm(total=len(subs), desc="Gerando e ajustando áudios com TikTok", unit="segmento") as pbar:
106
+ for sub in subs:
107
+ temp_file = output_dir / f"{sub.index:02d}_temp.mp3"
108
+ output_file = output_dir / f"{sub.index:02d}.mp3"
109
+ target_duration_ms = timetoms(sub.end) - timetoms(sub.start)
110
+
111
+ if not output_file.exists() or output_file.stat().st_size == 0:
112
+ # Roda a função síncrona 'tts' em uma thread separada para não bloquear o asyncio
113
+ await asyncio.to_thread(tts, sub.text, Voice[voice_str], str(temp_file))
114
+
115
+ if temp_file.exists():
116
+ await adjust_audio_speed(str(temp_file), str(output_file), target_duration_ms)
117
+ os.remove(temp_file)
118
+ pbar.update(1)
119
+
120
+ final_audio = await merge_audio_files(output_dir, srt_file_path)
121
+
122
+ if srt_temp_deleta:
123
+ shutil.rmtree(output_dir, ignore_errors=True)
124
+ print(f"Pasta temporária {output_dir} apagada.")
125
+
126
+ return final_audio
127
+
128
+ def controlador_process_srt_file_tiktok(srt_file, voice_str, srt_temp_deleta):
129
+ """Função controladora que inicia o processamento de SRT."""
130
+ if not srt_file: return None
131
+ output_dir = "output/srt_temp"
132
+
133
+ return asyncio.run(process_srt_file_tiktok(srt_file.name, voice_str, output_dir, srt_temp_deleta))
utils.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # utils.py
2
+
3
+ import os
4
+ import subprocess
5
+ from pathlib import Path
6
+ from pydub import AudioSegment
7
+ from pydub.silence import split_on_silence
8
+ import pysrt
9
+ from tqdm import tqdm
10
+ import asyncio
11
+
12
+ def remove_silence(input_file, output_file):
13
+ """Lê um arquivo MP3, remove o silêncio e salva como MP3 com alta qualidade, mantendo pequenas pausas."""
14
+ audio = AudioSegment.from_mp3(input_file)
15
+ segments = split_on_silence(
16
+ audio,
17
+ min_silence_len=500,
18
+ silence_thresh=-40,
19
+ keep_silence=250
20
+ )
21
+ non_silent_audio = AudioSegment.silent(duration=0)
22
+ for segment in segments:
23
+ non_silent_audio += segment
24
+ non_silent_audio.export(output_file, format="mp3", bitrate="192k")
25
+
26
+ def timetoms(time_obj):
27
+ """Converte um objeto de tempo do Pysrt para milissegundos."""
28
+ return time_obj.hours * 3600000 + time_obj.minutes * 60000 + time_obj.seconds * 1000 + time_obj.milliseconds
29
+
30
+ # --- VERSÃO COMPLETAMENTE NOVA E ROBUSTA ---
31
+ async def adjust_audio_speed(input_file, output_file, target_duration_ms):
32
+ """Ajusta a velocidade do áudio usando o filtro 'atempo' do FFmpeg para máxima qualidade."""
33
+
34
+ # Usa ffprobe para obter a duração exata, é mais confiável que pydub
35
+ try:
36
+ probe_cmd = [
37
+ "ffprobe", "-v", "error", "-show_entries", "format=duration",
38
+ "-of", "default=noprint_wrappers=1:nokey=1", input_file
39
+ ]
40
+ result = subprocess.run(probe_cmd, capture_output=True, text=True, check=True)
41
+ original_duration_ms = float(result.stdout.strip()) * 1000
42
+ except (subprocess.CalledProcessError, FileNotFoundError):
43
+ # Fallback para pydub se ffprobe não estiver disponível ou falhar
44
+ original_duration_ms = len(AudioSegment.from_mp3(input_file))
45
+
46
+ if original_duration_ms == 0 or target_duration_ms <= 0:
47
+ silent_audio = AudioSegment.silent(duration=target_duration_ms)
48
+ silent_audio.export(output_file, format="mp3", bitrate="192k")
49
+ return silent_audio
50
+
51
+ speed_factor = original_duration_ms / target_duration_ms
52
+
53
+ # Se a velocidade já for quase perfeita, apenas renomeia para evitar re-compressão
54
+ if 0.99 < speed_factor < 1.01:
55
+ Path(input_file).rename(output_file)
56
+ return AudioSegment.from_mp3(output_file)
57
+
58
+ # Constrói a cadeia de filtros 'atempo'
59
+ atempo_filters = []
60
+ current_factor = speed_factor
61
+
62
+ # Para aceleração > 2.0x
63
+ while current_factor > 2.0:
64
+ atempo_filters.append("atempo=2.0")
65
+ current_factor /= 2.0
66
+
67
+ # Para desaceleração < 0.5x
68
+ while current_factor < 0.5:
69
+ atempo_filters.append("atempo=0.5")
70
+ current_factor /= 0.5
71
+
72
+ # Adiciona o fator final (que agora está entre 0.5 e 2.0)
73
+ if current_factor != 1.0:
74
+ atempo_filters.append(f"atempo={current_factor:.5f}")
75
+
76
+ filter_string = ",".join(atempo_filters)
77
+
78
+ # Executa o comando FFmpeg
79
+ ffmpeg_cmd = [
80
+ "ffmpeg", "-y", "-i", input_file, "-filter:a", filter_string,
81
+ "-b:a", "192k", "-ar", "44100", # Define bitrate e sample rate de alta qualidade
82
+ "-hide_banner", "-loglevel", "error", output_file
83
+ ]
84
+
85
+ try:
86
+ # Roda o subprocesso bloqueante em uma thread separada para não congelar a UI
87
+ proc = await asyncio.create_subprocess_exec(
88
+ *ffmpeg_cmd,
89
+ stdout=asyncio.subprocess.PIPE,
90
+ stderr=asyncio.subprocess.PIPE
91
+ )
92
+ stdout, stderr = await proc.communicate()
93
+ if proc.returncode != 0:
94
+ print(f"Erro no FFmpeg ao ajustar a velocidade: {stderr.decode()}")
95
+ # Em caso de erro, cria silêncio para não quebrar o processo
96
+ silent = AudioSegment.silent(duration=target_duration_ms)
97
+ silent.export(output_file, format="mp3")
98
+ except FileNotFoundError:
99
+ print("ERRO: FFmpeg não encontrado. Verifique se ele está instalado e no PATH do sistema.")
100
+ raise
101
+
102
+ return AudioSegment.from_mp3(output_file)
103
+
104
+
105
+ async def merge_audio_files(output_folder, srt_file_path):
106
+ """Mescla segmentos de áudio baseados nos tempos de um arquivo SRT com sincronização correta."""
107
+ subs = pysrt.open(srt_file_path)
108
+ final_audio = AudioSegment.silent(duration=0)
109
+ base_name = Path(srt_file_path).stem
110
+
111
+ with tqdm(total=len(subs), desc=f"Mesclando áudios para {base_name}", unit="segmento") as pbar:
112
+ for sub in subs:
113
+ start_time_ms = timetoms(sub.start)
114
+ end_time_ms = timetoms(sub.end)
115
+
116
+ audio_file = Path(output_folder) / f"{sub.index:02d}.mp3"
117
+
118
+ silence_duration = start_time_ms - len(final_audio)
119
+ if silence_duration > 5: # Adiciona uma pequena margem para evitar micro-silêncios
120
+ final_audio += AudioSegment.silent(duration=silence_duration)
121
+
122
+ if audio_file.exists() and audio_file.stat().st_size > 0:
123
+ audio_segment = AudioSegment.from_mp3(str(audio_file))
124
+ final_audio += audio_segment
125
+ else:
126
+ segment_duration = end_time_ms - start_time_ms
127
+ final_audio += AudioSegment.silent(duration=max(0, segment_duration))
128
+
129
+ pbar.update(1)
130
+
131
+ srt_output_dir = Path("output/srt_output")
132
+ srt_output_dir.mkdir(parents=True, exist_ok=True)
133
+ output_file_path = srt_output_dir / f"{base_name}_final.mp3"
134
+ final_audio.export(str(output_file_path), format="mp3", bitrate="192k")
135
+ print(f"\nÁudio final salvo em: {output_file_path}\n")
136
+ return str(output_file_path)
137
+
138
+ def listar_audios():
139
+ """Lista os arquivos de áudio na pasta de saída do SRT."""
140
+ try:
141
+ srt_output_dir = "output/srt_output"
142
+ if not os.path.exists(srt_output_dir):
143
+ os.makedirs(srt_output_dir, exist_ok=True)
144
+ return ["Nenhum áudio gerado ainda"]
145
+ arquivos = [f for f in os.listdir(srt_output_dir) if f.endswith(('.mp3', '.wav'))]
146
+ return arquivos if arquivos else ["Nenhum áudio gerado ainda"]
147
+ except Exception as e:
148
+ print(f"Erro ao listar áudios: {e}")
149
+ return ["Erro ao listar arquivos"]
150
+
151
+ def tocar_audio(arquivo):
152
+ """Retorna o caminho completo para um arquivo de áudio selecionado para tocar."""
153
+ if arquivo and arquivo != "Nenhum áudio gerado ainda":
154
+ return f"output/srt_output/{arquivo}"
155
+ return None