gnosticdev commited on
Commit
c7d476a
·
verified ·
1 Parent(s): ced4e6e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -108
app.py CHANGED
@@ -5,10 +5,7 @@ import os
5
  import asyncio
6
  import time
7
  from threading import Timer
8
- from moviepy.editor import (
9
- VideoFileClip, AudioFileClip, ImageClip,
10
- concatenate_videoclips, CompositeVideoClip, CompositeAudioClip
11
- )
12
  import edge_tts
13
  import gradio as gr
14
  from pydub import AudioSegment
@@ -22,190 +19,161 @@ OUTRO_VIDEO = "outrovideo.mp4"
22
  MUSIC_BG = "musicafondo.mp3"
23
  FX_SOUND = "fxsound.mp3"
24
  WATERMARK = "watermark.png"
25
- EJEMPLO_VIDEO = "ejemplo.mp4" # Video de ejemplo en el root
26
 
27
- # Validar existencia de archivos obligatorios
28
  for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, FX_SOUND, WATERMARK, EJEMPLO_VIDEO]:
29
  if not os.path.exists(file):
30
  logging.error(f"Falta archivo necesario: {file}")
31
- raise FileNotFoundError(f"Falta archivo necesario: {file}")
32
 
33
  def eliminar_archivo_tiempo(ruta, delay=1800):
34
- """Elimina un archivo después de 'delay' segundos (30 minutos por defecto)."""
35
  def eliminar():
36
  try:
37
  if os.path.exists(ruta):
38
  os.remove(ruta)
39
- logging.info(f"Archivo eliminado por timer: {ruta}")
40
  except Exception as e:
41
  logging.error(f"Error al eliminar {ruta}: {e}")
42
  Timer(delay, eliminar).start()
43
 
44
- def cortar_video(video_path, metodo="inteligente", duracion=10):
45
- """Corta el video en clips según el método especificado."""
46
- try:
47
- logging.info("Iniciando corte de video...")
48
- video = VideoFileClip(video_path)
49
-
50
- if metodo == "manual":
51
- clips = [video.subclip(i * duracion, (i + 1) * duracion)
52
- for i in range(math.ceil(video.duration / duracion))]
53
- logging.info(f"Video cortado en {len(clips)} clips manuales.")
54
- return clips
55
-
56
- # Método "inteligente" simplificado (cortes cada 5 segundos)
57
- clips = []
58
- ultimo_corte = 0
59
- for i in range(1, math.ceil(video.duration)):
60
- if i % 5 == 0:
61
- clips.append(video.subclip(ultimo_corte, i))
62
- ultimo_corte = i
63
- if ultimo_corte < video.duration:
64
- clips.append(video.subclip(ultimo_corte, video.duration))
65
- logging.info(f"Video cortado en {len(clips)} clips automáticos.")
66
- return clips
67
- except Exception as e:
68
- logging.error(f"Error al cortar video: {e}")
69
- raise
70
-
71
- async def procesar_audio(texto, voz, duracion_total, duracion_intro, duracion_video_editado):
72
- """Genera el TTS y mezcla con la música de fondo."""
73
  temp_files = []
74
  try:
75
- logging.info("Generando TTS y mezclando audio...")
76
  communicate = edge_tts.Communicate(texto, voz)
77
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
78
  await communicate.save(tmp.name)
79
  tts_audio = AudioFileClip(tmp.name)
80
  temp_files.append(tmp.name)
81
 
82
- # Ajustar duración del TTS
83
- if tts_audio.duration > duracion_video_editado:
84
- tts_audio = tts_audio.subclip(0, duracion_video_editado)
85
-
86
- # Preparar música de fondo
87
  bg_music = AudioSegment.from_mp3(MUSIC_BG)
88
- if len(bg_music) < duracion_total * 1000:
89
- bg_music = bg_music * math.ceil(duracion_total * 1000 / len(bg_music))
90
- bg_music = bg_music[:duracion_total * 1000].fade_out(3000)
91
 
92
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
93
  bg_music.export(tmp.name, format="mp3")
94
- bg_audio = AudioFileClip(tmp.name).volumex(0.10)
95
  temp_files.append(tmp.name)
96
 
97
  # Combinar audios
98
  audio_final = CompositeAudioClip([
99
  bg_audio,
100
- tts_audio.volumex(0.9).set_start(duracion_intro)
101
  ])
102
- logging.info("Audio procesado correctamente.")
103
  return audio_final
104
- except Exception as e:
105
- logging.error(f"Error al procesar audio: {e}")
106
- raise
107
  finally:
108
- # Eliminar archivos temporales de audio inmediatamente
109
  for file in temp_files:
110
  try:
111
- if os.path.exists(file):
112
- os.remove(file)
113
- logging.info(f"Archivo temporal de audio eliminado: {file}")
114
- except Exception as e:
115
- logging.warning(f"No se pudo eliminar el archivo temporal {file}: {e}")
116
 
117
  def agregar_transiciones(clips):
118
- """Agrega transiciones con watermark y sonido FX entre clips."""
119
  try:
120
- logging.info("Agregando transiciones...")
121
- fx_audio = AudioFileClip(FX_SOUND).set_duration(2.5)
122
- transicion = ImageClip(WATERMARK).set_duration(2.5).resize(height=clips[0].h).set_position(("center", 0.1))
 
 
 
123
 
124
- clips_con_fx = []
125
  for i, clip in enumerate(clips):
126
- clip_watermarked = CompositeVideoClip([clip, transicion])
127
- clips_con_fx.append(clip_watermarked)
128
- if i < len(clips) - 1:
129
- clips_con_fx.append(
130
- CompositeVideoClip([transicion.set_position("center")]).set_audio(fx_audio)
131
- )
132
- logging.info("Transiciones agregadas correctamente.")
133
- return concatenate_videoclips(clips_con_fx)
 
 
 
134
  except Exception as e:
135
- logging.error(f"Error al agregar transiciones: {e}")
136
- raise
137
 
138
  async def procesar_video(video_input, texto_tts, voz_seleccionada, metodo_corte, duracion_corte):
139
- """Procesa el video completo con intro, cortes, TTS y outro."""
140
  try:
141
- logging.info("Iniciando procesamiento de video...")
142
- # Cortar y editar video
143
- clips = cortar_video(video_input, metodo_corte, duracion_corte)
 
 
 
 
 
 
 
 
 
 
 
144
  video_editado = agregar_transiciones(clips)
145
 
146
- # Cargar intro y outro
147
  intro = VideoFileClip(INTRO_VIDEO)
148
  outro = VideoFileClip(OUTRO_VIDEO)
149
-
150
- # Calcular duraciones
151
- duracion_intro = intro.duration
152
- duracion_video_editado = video_editado.duration
153
- duracion_total = duracion_intro + duracion_video_editado + outro.duration
154
-
155
- # Concatenar clips
156
  video_final = concatenate_videoclips([intro, video_editado, outro])
157
 
158
- # Procesar audio
159
- audio_final = await procesar_audio(texto_tts, voz_seleccionada, duracion_total, duracion_intro, duracion_video_editado)
 
160
 
161
- # Renderizar video final como archivo temporal accesible para Gradio
 
 
 
 
 
 
162
  with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
163
  video_final.set_audio(audio_final).write_videofile(
164
  tmp.name,
165
  codec="libx264",
166
  audio_codec="aac",
167
- temp_audiofile="temp-audio.m4a",
168
- remove_temp=True,
169
- fps=24
170
  )
171
- output_path = tmp.name
172
-
173
- # Programar eliminación del video en 30 minutos
174
- eliminar_archivo_tiempo(output_path, delay=1800)
175
- logging.info(f"Video temporal listo para Gradio: {output_path}")
176
- return output_path
177
  except Exception as e:
178
- logging.error(f"Error durante el procesamiento: {e}")
179
  raise
180
 
181
- # Interfaz Gradio con ejemplo de uso
182
  with gr.Blocks() as demo:
183
- gr.Markdown("# Video Editor IA")
184
 
185
  with gr.Tab("Principal"):
186
- video_input = gr.Video(label="Subir video", value=EJEMPLO_VIDEO) # Video de ejemplo
187
  texto_tts = gr.Textbox(
188
  label="Texto para TTS",
189
  lines=3,
190
- value="hola esto es una prueba suscribete al canal ya empieza el video" # Texto de ejemplo
191
  )
192
  voz_seleccionada = gr.Dropdown(
193
- label="Seleccionar voz",
194
  choices=["es-ES-AlvaroNeural", "es-MX-BeatrizNeural"],
195
  value="es-ES-AlvaroNeural"
196
  )
197
  procesar_btn = gr.Button("Generar Video")
198
- video_output = gr.Video(label="Resultado")
199
 
200
  with gr.Tab("Ajustes"):
201
  metodo_corte = gr.Radio(
202
  ["inteligente", "manual"],
203
- label="Método de cortes",
204
  value="inteligente"
205
  )
206
  duracion_corte = gr.Slider(
207
  1, 60, 10,
208
- label="Segundos por corte (solo manual)"
209
  )
210
 
211
  procesar_btn.click(
@@ -215,5 +183,4 @@ with gr.Blocks() as demo:
215
  )
216
 
217
  if __name__ == "__main__":
218
- logging.info("Iniciando aplicación Gradio...")
219
  demo.queue().launch()
 
5
  import asyncio
6
  import time
7
  from threading import Timer
8
+ from moviepy.editor import *
 
 
 
9
  import edge_tts
10
  import gradio as gr
11
  from pydub import AudioSegment
 
19
  MUSIC_BG = "musicafondo.mp3"
20
  FX_SOUND = "fxsound.mp3"
21
  WATERMARK = "watermark.png"
22
+ EJEMPLO_VIDEO = "ejemplo.mp4"
23
 
24
+ # Validar existencia de archivos
25
  for file in [INTRO_VIDEO, OUTRO_VIDEO, MUSIC_BG, FX_SOUND, WATERMARK, EJEMPLO_VIDEO]:
26
  if not os.path.exists(file):
27
  logging.error(f"Falta archivo necesario: {file}")
28
+ raise FileNotFoundError(f"Falta: {file}")
29
 
30
  def eliminar_archivo_tiempo(ruta, delay=1800):
31
+ """Elimina archivos después de 30 minutos"""
32
  def eliminar():
33
  try:
34
  if os.path.exists(ruta):
35
  os.remove(ruta)
36
+ logging.info(f"Archivo eliminado: {ruta}")
37
  except Exception as e:
38
  logging.error(f"Error al eliminar {ruta}: {e}")
39
  Timer(delay, eliminar).start()
40
 
41
+ async def procesar_audio(texto, voz, duracion_total, duracion_intro):
42
+ """Genera TTS y mezcla con música"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  temp_files = []
44
  try:
45
+ # Generar TTS
46
  communicate = edge_tts.Communicate(texto, voz)
47
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
48
  await communicate.save(tmp.name)
49
  tts_audio = AudioFileClip(tmp.name)
50
  temp_files.append(tmp.name)
51
 
52
+ # Preparar música de fondo en loop
 
 
 
 
53
  bg_music = AudioSegment.from_mp3(MUSIC_BG)
54
+ bg_music = bg_music * (duracion_total // len(bg_music) + 1)
55
+ bg_music = bg_music[:duracion_total * 1000].fade_out(5000)
 
56
 
57
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp:
58
  bg_music.export(tmp.name, format="mp3")
59
+ bg_audio = AudioFileClip(tmp.name).volumex(0.15)
60
  temp_files.append(tmp.name)
61
 
62
  # Combinar audios
63
  audio_final = CompositeAudioClip([
64
  bg_audio,
65
+ tts_audio.volumex(0.85).set_start(duracion_intro)
66
  ])
 
67
  return audio_final
 
 
 
68
  finally:
 
69
  for file in temp_files:
70
  try:
71
+ os.remove(file)
72
+ except: pass
 
 
 
73
 
74
  def agregar_transiciones(clips):
75
+ """Transiciones suaves cada 40 segundos"""
76
  try:
77
+ transicion_fx = AudioFileClip(FX_SOUND).subclip(0, 0.5)
78
+ watermark = (ImageClip(WATERMARK)
79
+ .set_duration(0.5)
80
+ .resize(height=50)
81
+ .margin(right=10, bottom=10, opacity=0)
82
+ .set_pos(("right", "bottom")))
83
 
84
+ clips_finales = []
85
  for i, clip in enumerate(clips):
86
+ # Añadir watermark al clip principal
87
+ clip_watermarked = CompositeVideoClip([clip, watermark])
88
+
89
+ # Agregar transición cada 40 segundos
90
+ if i > 0 and i % 40 == 0:
91
+ transicion = CompositeVideoClip([watermark.set_duration(0.5)]).set_audio(transicion_fx)
92
+ clips_finales.append(transicion)
93
+
94
+ clips_finales.append(clip_watermarked)
95
+
96
+ return concatenate_videoclips(clips_finales, method="compose")
97
  except Exception as e:
98
+ logging.error(f"Error en transiciones: {e}")
99
+ return concatenate_videoclips(clips)
100
 
101
  async def procesar_video(video_input, texto_tts, voz_seleccionada, metodo_corte, duracion_corte):
 
102
  try:
103
+ # Cargar video original con su audio
104
+ video_original = VideoFileClip(video_input)
105
+ audio_original = video_original.audio.volumex(0.7) # Conservar audio original al 70%
106
+
107
+ # Cortar video según método
108
+ clips = []
109
+ if metodo_corte == "manual":
110
+ for i in range(math.ceil(video_original.duration / duracion_corte)):
111
+ clips.append(video_original.subclip(i*duracion_corte, (i+1)*duracion_corte))
112
+ else:
113
+ # Cortes automáticos cada 40 segundos para transiciones
114
+ clips = [video_original.subclip(i, i+40) for i in range(0, math.ceil(video_original.duration), 40)]
115
+
116
+ # Procesar transiciones
117
  video_editado = agregar_transiciones(clips)
118
 
119
+ # Combinar con intro/outro
120
  intro = VideoFileClip(INTRO_VIDEO)
121
  outro = VideoFileClip(OUTRO_VIDEO)
 
 
 
 
 
 
 
122
  video_final = concatenate_videoclips([intro, video_editado, outro])
123
 
124
+ # Procesar audio completo
125
+ duracion_total = video_final.duration
126
+ audio_tts_bg = await procesar_audio(texto_tts, voz_seleccionada, duracion_total, intro.duration)
127
 
128
+ # Combinar TODOS los audios
129
+ audio_final = CompositeAudioClip([
130
+ audio_original.set_duration(video_final.duration),
131
+ audio_tts_bg
132
+ ])
133
+
134
+ # Renderizar video final
135
  with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
136
  video_final.set_audio(audio_final).write_videofile(
137
  tmp.name,
138
  codec="libx264",
139
  audio_codec="aac",
140
+ fps=24,
141
+ threads=4
 
142
  )
143
+ eliminar_archivo_tiempo(tmp.name)
144
+ return tmp.name
 
 
 
 
145
  except Exception as e:
146
+ logging.error(f"Error procesando video: {e}")
147
  raise
148
 
149
+ # Interfaz Gradio
150
  with gr.Blocks() as demo:
151
+ gr.Markdown("# Editor de Video con IA")
152
 
153
  with gr.Tab("Principal"):
154
+ video_input = gr.Video(label="Subir video", value=EJEMPLO_VIDEO)
155
  texto_tts = gr.Textbox(
156
  label="Texto para TTS",
157
  lines=3,
158
+ value="¡Hola! Esto es una prueba. Suscríbete al canal y activa la campanita."
159
  )
160
  voz_seleccionada = gr.Dropdown(
161
+ label="Voz",
162
  choices=["es-ES-AlvaroNeural", "es-MX-BeatrizNeural"],
163
  value="es-ES-AlvaroNeural"
164
  )
165
  procesar_btn = gr.Button("Generar Video")
166
+ video_output = gr.Video(label="Video Procesado")
167
 
168
  with gr.Tab("Ajustes"):
169
  metodo_corte = gr.Radio(
170
  ["inteligente", "manual"],
171
+ label="Método de corte",
172
  value="inteligente"
173
  )
174
  duracion_corte = gr.Slider(
175
  1, 60, 10,
176
+ label="Segundos por corte (manual)"
177
  )
178
 
179
  procesar_btn.click(
 
183
  )
184
 
185
  if __name__ == "__main__":
 
186
  demo.queue().launch()