Files

221 lines
6.9 KiB
Python

import asyncio
import os
import subprocess
import imageio_ffmpeg
import config
FFMPEG = imageio_ffmpeg.get_ffmpeg_exe()
W, H, FPS = 1280, 720, 20
FUENTE = None
for p in (r"C:\Windows\Fonts\arialbd.ttf",
r"C:\Windows\Fonts\arial.ttf",
r"C:\Windows\Fonts\segoeui.ttf"):
if os.path.exists(p):
FUENTE = p
break
def _duracion_audio(ruta):
try:
r = subprocess.run(
[FFMPEG, "-i", ruta, "-f", "null", "-"],
capture_output=True, text=True, timeout=30,
)
for linea in (r.stderr or "").splitlines():
if "Duration:" in linea:
hh, mm, ss = linea.split("Duration:")[1].split(",")[0].strip().split(":")
return int(hh) * 3600 + int(mm) * 60 + float(ss)
except Exception:
pass
return None
def sintetizar_con_oraciones(texto, nombre="anuncio.mp3"):
"""Genera el audio y devuelve (ruta_audio, oraciones_con_tiempos)."""
from PIL import Image # noqa: F401 (import de dependencia temprano)
import edge_tts
ruta = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"voz_media", nombre)
os.makedirs(os.path.dirname(ruta), exist_ok=True)
audio = []
oraciones = []
async def _gen():
comunicador = edge_tts.Communicate(
texto, config.VOZ_EDGE,
rate=config.VOZ_RATE, pitch=config.VOZ_PITCH,
)
async for ch in comunicador.stream():
if ch["type"] == "audio":
audio.append(ch["data"])
elif ch["type"] == "SentenceBoundary":
inicio = ch["offset"] / 1e7
fin = inicio + ch["duration"] / 1e7
oraciones.append((ch["text"], inicio, fin))
asyncio.run(_gen())
if not audio:
return None, []
with open(ruta, "wb") as f:
f.write(b"".join(audio))
return ruta, oraciones
_TITULO_COLORES = {
"red": (255, 30, 30),
"rojo": (255, 30, 30),
"rosa": (255, 105, 180),
"pink": (255, 105, 180),
}
def _dibujar_titulo(d, t, font_title):
"""Titulo parpadeante en mayusculas (rojo/rosa) en la parte superior."""
titulo = config.TV_TITULO.upper()
if not titulo:
return
if config.TV_TITULO_PARPADEO and int(t * 2) % 2 == 1:
return
color = _TITULO_COLORES.get(config.TV_TITULO_COLOR, (255, 30, 30))
ancho = d.textlength(titulo, font=font_title)
x = (W - ancho) // 2
d.text((x, 30), titulo, fill=color, font=font_title)
def _palabras(oraciones):
"""Distribuye las palabras dentro de cada frase por longitud."""
palabras = []
for texto, s, e in oraciones:
tokens = texto.split()
total = sum(len(t) for t in tokens) or 1
t = s
for tok in tokens:
dur = (e - s) * (len(tok) / total)
palabras.append((tok, t, t + dur))
t += dur
return palabras
def generar_video(texto, ruta_audio, oraciones, nombre="anuncio.mp4"):
from PIL import Image, ImageDraw, ImageFont
ruta_video = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"voz_media", nombre)
palabras = _palabras(oraciones)
dur = _duracion_audio(ruta_audio) or ((palabras[-1][2] if palabras else 0) + 2)
total = dur + 0.5
n_frames = int(total * FPS)
font = ImageFont.truetype(FUENTE, 52) if FUENTE else ImageFont.load_default()
font_title = ImageFont.truetype(FUENTE, 96) if FUENTE else ImageFont.load_default()
gap = 18
# agrupar en lineas
lineas = []
linea = []
ancho_linea = 0
for tok, s, e in palabras:
ancho = font.getlength(tok)
if ancho_linea + ancho + gap > W - 80 and linea:
lineas.append(linea)
linea = [(tok, s, e)]
ancho_linea = ancho
else:
linea.append((tok, s, e))
ancho_linea += ancho + gap
if linea:
lineas.append(linea)
cmd = [FFMPEG, "-y", "-f", "rawvideo", "-vcodec", "rawvideo",
"-s", f"{W}x{H}", "-pix_fmt", "rgb24", "-r", str(FPS), "-i", "-",
"-i", ruta_audio,
"-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p",
"-c:a", "aac", "-b:a", "128k", "-shortest", ruta_video]
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL)
for i in range(n_frames):
t = i / FPS
img = Image.new("RGB", (W, H), (8, 8, 8))
d = ImageDraw.Draw(img)
_dibujar_titulo(d, t, font_title)
y = H // 2 - (len(lineas) // 2) * 80 + 30
for linea_words in lineas:
x = 40
for tok, s, e in linea_words:
if s <= t < e:
color = (255, 220, 0)
elif t >= e:
color = (255, 255, 255)
else:
color = (90, 90, 90)
d.text((x, y), tok, fill=color, font=font)
x += font.getlength(tok) + gap
y += 80
try:
proc.stdin.write(img.tobytes())
except BrokenPipeError:
break
proc.stdin.close()
proc.wait()
return ruta_video
def generar_video_texto_final(texto, segundos, nombre="texto_final.mp4"):
"""Video estatico con el texto completo en pantalla durante `segundos` s."""
from PIL import Image, ImageDraw, ImageFont
ruta_video = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"voz_media", nombre)
font = ImageFont.truetype(FUENTE, 52) if FUENTE else ImageFont.load_default()
font_title = ImageFont.truetype(FUENTE, 96) if FUENTE else ImageFont.load_default()
gap = 18
lineas = []
linea = []
ancho = 0
for w in texto.split():
a = font.getlength(w)
if ancho + a + gap > W - 80 and linea:
lineas.append(linea)
linea = [w]
ancho = a
else:
linea.append(w)
ancho += a + gap
if linea:
lineas.append(linea)
n_frames = int(segundos * FPS)
cmd = [FFMPEG, "-y", "-f", "rawvideo", "-vcodec", "rawvideo",
"-s", f"{W}x{H}", "-pix_fmt", "rgb24", "-r", str(FPS), "-i", "-",
"-c:v", "libx264", "-preset", "veryfast", "-pix_fmt", "yuv420p",
"-an", ruta_video]
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE,
stderr=subprocess.DEVNULL)
for i in range(n_frames):
img = Image.new("RGB", (W, H), (8, 8, 8))
d = ImageDraw.Draw(img)
_dibujar_titulo(d, i / FPS, font_title)
y = H // 2 - (len(lineas) // 2) * 80 + 30
for linea_words in lineas:
x = 40
for w in linea_words:
d.text((x, y), w, fill=(255, 255, 255), font=font)
x += font.getlength(w) + gap
y += 80
try:
proc.stdin.write(img.tobytes())
except BrokenPipeError:
break
proc.stdin.close()
proc.wait()
return ruta_video