IAweb / cliente_template.py
Tu Nombre
filtrar voces por pais
a98fdd9
Raw
History Blame Contribute Delete
17.5 kB
import subprocess
try:
subprocess.run(["sed", "-i", 's/rights="none"/rights="read|write"/g', "/etc/ImageMagick-6/policy.xml"], capture_output=True)
subprocess.run(["sed", "-i", 's/rights="none"/rights="read|write"/g', "/etc/ImageMagick-7/policy.xml"], capture_output=True)
except: pass
import os, asyncio, time, random, requests, threading
import PIL.Image
if not hasattr(PIL.Image, "ANTIALIAS"): PIL.Image.ANTIALIAS = PIL.Image.LANCZOS
import gradio as gr
from groq import Groq
import edge_tts
from moviepy.editor import VideoFileClip, AudioFileClip, TextClip, CompositeVideoClip
NICHO = "NICHO_PLACEHOLDER"
import json as _json
try:
CONFIG = _json.loads("""CONFIG_PLACEHOLDER""")
except:
CONFIG = {}
# Mapeo de voces edge-tts
VOCES = {
"es": {
"masculina": ["es-ES-AlvaroNeural", "es-MX-JorgeNeural", "es-AR-TomasNeural", "es-CO-GonzaloNeural"],
"femenina": ["es-ES-ElviraNeural", "es-MX-DaliaNeural", "es-AR-ElenaNeural", "es-CO-SalomeNeural"]
},
"en": {
"masculina": ["en-US-GuyNeural", "en-US-DavisNeural", "en-GB-RyanNeural", "en-AU-WilliamNeural"],
"femenina": ["en-US-JennyNeural", "en-US-AriaNeural", "en-GB-SoniaNeural", "en-AU-NatashaNeural"]
},
"pt": {
"masculina": ["pt-BR-AntonioNeural", "pt-BR-FabioNeural"],
"femenina": ["pt-BR-FranciscaNeural", "pt-BR-BrendaNeural"]
},
"fr": {
"masculina": ["fr-FR-HenriNeural", "fr-FR-JeromeNeural"],
"femenina": ["fr-FR-DeniseNeural", "fr-FR-EloiseNeural"]
}
}
CLIENT_EMAIL = "EMAIL_PLACEHOLDER"
HF_REPO = "REPO_PLACEHOLDER"
# Descargar token.json del repo si existe
try:
import urllib.request
urllib.request.urlretrieve(f"https://huggingface.co/spaces/{HF_REPO}/resolve/main/token.json", "token.json")
print("Token descargado", flush=True)
except Exception as e:
print(f"Sin token aún: {e}", flush=True)
GROQ_KEYS = ["GROQ_PLACEHOLDER", "GROQ_PLACEHOLDER_2", "GROQ_PLACEHOLDER_3"]
GROQ_KEYS = [k for k in GROQ_KEYS if k and not k.startswith("GROQ_PLACEHOLDER")]
if not GROQ_KEYS:
GROQ_KEYS = ["GROQ_PLACEHOLDER"]
GROQ_KEY = GROQ_KEYS[0]
TIPO_VIDEO = "TIPO_PLACEHOLDER"
PEXELS_KEY = "PEXELS_PLACEHOLDER"
LOGS = []
STATUS = "Iniciando..."
def log(msg):
t = time.strftime("%H:%M:%S")
LOGS.append(f"[{t}] {msg}")
if len(LOGS) > 40: LOGS.pop(0)
print(msg, flush=True)
def generar_guion():
_tono_actual = CONFIG.get("tono", "educativo")
last_err = None
for _key in GROQ_KEYS:
try:
client = Groq(api_key=_key)
return _generar_guion_con(client, _tono_actual)
except Exception as _e:
last_err = _e
if "rate_limit" in str(_e).lower() or "429" in str(_e):
continue
else:
raise
raise last_err if last_err else Exception("Sin keys Groq")
def _generar_guion_con(client, _tono_actual):
_canal_url = CONFIG.get("canal_promocionar", "").strip()
_contexto_canal = f" El video debe ser un teaser que invite a ver el canal {_canal_url}." if _canal_url else ""
res = client.chat.completions.create(
messages=[{"role":"user","content":f"Escribe SOLO el texto a narrar en un video de 60 segundos sobre {NICHO}. Tono: {_tono_actual}.{_contexto_canal} NO incluyas indicaciones como narrador dice, voz en off, intro, outro, ni nada que no sea el texto a leer. Directo al grano, sin simbolos, sin hashtags, sin emojis."}],
model="llama-3.3-70b-versatile", temperature=0.82
)
guion_base = res.choices[0].message.content.strip()
frase_i = CONFIG.get("frase_inicio", "").strip()
frase_f = CONFIG.get("frase_final", "").strip()
partes = []
if frase_i: partes.append(frase_i)
partes.append(guion_base)
if frase_f: partes.append(frase_f)
return ". ".join(partes)
async def pipeline():
global STATUS
while True:
try:
STATUS = "Generando guion..."
log(f"Nuevo video: {NICHO}")
guion = generar_guion()
log("Guion listo")
STATUS = "Generando voz..."
voz_ia_on = CONFIG.get("voz_ia", True)
if voz_ia_on:
idioma_v = CONFIG.get("idioma", "es")
genero_v = CONFIG.get("voz_genero", "masculina")
import random as _rnd
voces_disponibles = VOCES.get(idioma_v, VOCES["es"]).get(genero_v, ["es-ES-AlvaroNeural"])
if isinstance(voces_disponibles, list):
# Filtrar por pais si esta configurado
pais_voz = CONFIG.get("pais_voz", "").lower()
if pais_voz:
cods = {"españa":"ES","mexico":"MX","argentina":"AR","colombia":"CO","usa":"US","uk":"GB","britanico":"GB","australia":"AU","brasil":"BR","francia":"FR"}
cod = cods.get(pais_voz, "")
if cod:
filtradas = [v for v in voces_disponibles if f"-{cod}-" in v]
if filtradas:
voces_disponibles = filtradas
if isinstance(voces_disponibles, str):
voz_seleccionada = voces_disponibles
else:
voz_seleccionada = _rnd.choice(voces_disponibles)
log(f"Voz elegida: {voz_seleccionada}")
vel = CONFIG.get("velocidad_voz", 1.0)
rate_str = f"+{int((vel-1)*100)}%" if vel >= 1 else f"-{int((1-vel)*100)}%"
await edge_tts.Communicate(guion, voz_seleccionada, rate=rate_str).save("voz.mp3")
else:
# Sin voz IA: crear audio en silencio
import subprocess
subprocess.run(["ffmpeg","-y","-f","lavfi","-i","anullsrc=r=44100:cl=mono","-t","60","voz.mp3"], capture_output=True)
log("Voz lista")
STATUS = "Preparando fondos..."
fondos = []
# Revisar si hay media propia del cliente
archivos_propios = []
import glob as _g
log(f"DEBUG cwd: {os.getcwd()}")
log(f"DEBUG ls: {os.listdir('.')}")
for ext in ["jpg","jpeg","png","webp","mp4","mov"]:
archivos_propios.extend(sorted(_g.glob(f"media_*.{ext}")))
log(f"DEBUG archivos encontrados: {archivos_propios}")
if archivos_propios:
random.shuffle(archivos_propios)
fondos = archivos_propios[:6]
log(f"Usando {len(fondos)} archivos del cliente")
else:
STATUS = "Descargando fondo..."
h = {"Authorization": PEXELS_KEY}
r = requests.get(f"https://api.pexels.com/videos/search?query={NICHO}&per_page=15", headers=h)
vids = r.json().get("videos", [])
if vids:
random.shuffle(vids)
for i, v in enumerate(vids[:6]):
try:
u = v["video_files"][0]["link"]
with open(f"fondo{i}.mp4","wb") as f: f.write(requests.get(u, timeout=30).content)
fondos.append(f"fondo{i}.mp4")
except: pass
log(f"Fondos listos: {len(fondos)}")
STATUS = "Renderizando video..."
from moviepy.editor import AudioFileClip as AC, VideoFileClip as VC, ImageClip as IC, concatenate_videoclips
voz = AC("voz.mp3")
if TIPO_VIDEO == "short":
clips_lista = []
dur_total_real = min(voz.duration, 58)
# Cada plano max 4s, calcular cuantos planos necesitamos
num_planos = max(int(dur_total_real / 4) + 1, len(fondos))
# Repetir fondos si hace falta
fondos_loop = (fondos * ((num_planos // len(fondos)) + 1))[:num_planos]
dur_clip = dur_total_real / num_planos
log(f"Planos: {num_planos}, dur_clip: {dur_clip:.1f}s")
for fp in fondos_loop:
try:
es_img = fp.lower().endswith((".jpg",".jpeg",".png",".webp"))
if es_img:
from PIL import Image as PILImage
img = PILImage.open(fp).convert("RGB")
w, h = img.size
bg = img.resize((1080, 1920), PILImage.LANCZOS)
from PIL import ImageFilter as _PIF
bg = bg.filter(_PIF.GaussianBlur(40))
ratio = min(1080/w, 1920/h)
new_w = int(w * ratio)
new_h = int(h * ratio)
fg = img.resize((new_w, new_h), PILImage.LANCZOS)
offset_x = (1080 - new_w) // 2
offset_y = (1920 - new_h) // 2
bg.paste(fg, (offset_x, offset_y))
img = bg
img.save(fp + "_r.jpg", "JPEG", quality=90)
cc = IC(fp + "_r.jpg").set_duration(dur_clip)
else:
cc = VC(fp).without_audio().loop(duration=dur_clip)
if cc.w/cc.h > 1080/1920:
cc = cc.resize(height=1920)
else:
cc = cc.resize(width=1080)
cc = cc.crop(x_center=cc.w/2, y_center=cc.h/2, width=1080, height=1920)
clips_lista.append(cc)
except Exception as e_c:
log(f"Error clip {fp}: {e_c}")
log(f"Clips OK: {len(clips_lista)}/{len(fondos)}")
clip = concatenate_videoclips(clips_lista, method="compose")
dur_final = min(voz.duration, 58)
clip = clip.subclip(0, dur_final)
voz = voz.subclip(0, dur_final)
else:
clips_lista = []
for fp in fondos:
try:
cc = IC(fp, duration=5).resize(width=1280) if fp.lower().endswith((".jpg",".jpeg",".png",".webp")) else VC(fp).without_audio().resize(width=1280)
cc = cc.loop(duration=voz.duration/max(len(fondos),1))
clips_lista.append(cc)
except: pass
clip = concatenate_videoclips(clips_lista, method="compose")
palabras = guion.split()
frases = [" ".join(palabras[i:i+6]) for i in range(0,len(palabras),6)]
t_f = voz.duration / max(len(frases),1)
if not CONFIG.get("subtitulos", True):
txts = []
else:
txts = [TextClip(t.upper(), fontsize={"tiktok":72,"clasico":50,"minimal":40}.get(CONFIG.get("estilo_subtitulos","tiktok"),72), color=CONFIG.get("color_subtitulos","#D4AF37"), font="DejaVu-Sans-Bold",
stroke_color="black", stroke_width=2, method="caption",
size=(clip.w*0.8,None)).set_start(i*t_f).set_duration(t_f).set_pos(("center", {"arriba":0.15,"centro":0.5,"abajo":0.75}.get(CONFIG.get("posicion_subtitulos","abajo"),0.75)), relative=True)
for i,t in enumerate(frases)]
final = CompositeVideoClip([clip]+txts).set_audio(voz)
final.write_videofile("out.mp4", fps=30, codec="libx264", bitrate="1500k", preset="medium", logger=None, threads=2, audio_codec="aac", audio_bitrate="128k")
log("Video renderizado")
STATUS = "Esperando conexión YouTube..."
while not os.path.exists("token.json"):
await asyncio.sleep(30)
STATUS = "Subiendo a YouTube..."
try:
import json, google.oauth2.credentials, googleapiclient.discovery, googleapiclient.http
with open("token.json") as f:
tok = json.load(f)
creds = google.oauth2.credentials.Credentials(
token=tok["access_token"],
refresh_token=tok.get("refresh_token"),
token_uri="https://oauth2.googleapis.com/token",
client_id=tok.get("client_id",""),
client_secret=tok.get("client_secret","")
)
yt = googleapiclient.discovery.build("youtube","v3",credentials=creds)
titulo = guion[:80].split(".")[0]
req_yt = yt.videos().insert(
part="snippet,status",
body={
"snippet":{"title":titulo,"description":guion + ("\n\n" + CONFIG.get("link_descripcion","") if CONFIG.get("link_descripcion") else ""),"categoryId":"22"},
"status":{"privacyStatus":"public"}
},
media_body=googleapiclient.http.MediaFileUpload("out.mp4",mimetype="video/mp4",resumable=True)
)
resp = None
while resp is None:
_, resp = req_yt.next_chunk()
log(f"Subido: {titulo[:40]}")
# Reportar uso al IAweb
try:
proyecto_idx = CONFIG.get("proyecto_idx", -1)
if proyecto_idx >= 0:
requests.post("https://cristobal299-iaweb.hf.space/reportar-uso",
json={"proyecto_idx": proyecto_idx, "email": CLIENT_EMAIL, "titulo": titulo}, timeout=10)
except Exception as eu:
log(f"No se pudo reportar uso: {eu}")
except FileNotFoundError:
log("Sin token.json, saltando subida")
except Exception as ey:
log(f"Error YouTube: {ey}")
STATUS = "Esperando siguiente ciclo..."
await asyncio.sleep(14400)
except Exception as e:
log(f"Error: {e}")
STATUS = f"Error: {str(e)[:40]}"
await asyncio.sleep(600)
def start():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
time.sleep(30); loop.run_until_complete(pipeline())
threading.Thread(target=start, daemon=True).start()
CONTADOR_VIDEOS = 0
ULTIMO_VIDEO = "Ninguno aún"
def youtube_conectado():
return os.path.exists("token.json")
def get_yt_status():
if youtube_conectado():
return "✅ YouTube conectado — publicando automáticamente"
return "❌ YouTube NO conectado — conecta tu canal para publicar"
def get_stats():
return f"📊 Videos publicados: {CONTADOR_VIDEOS} | 🎬 Último: {ULTIMO_VIDEO} | ⚡ Estado: {STATUS}"
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
import uvicorn
app = FastAPI()
from fastapi.responses import FileResponse as _FR
@app.get("/descargar")
async def descargar_video():
if os.path.exists("out.mp4"):
return _FR("out.mp4", media_type="video/mp4", filename="tubebot_video.mp4")
return {"error": "Aun no hay video generado"}
@app.get("/", response_class=HTMLResponse)
async def panel():
yt_status = "✅ YouTube conectado" if os.path.exists("token.json") else "❌ YouTube NO conectado"
logs_html = "<br>".join(LOGS[-20:]) if LOGS else "Iniciando..."
return f"""<!DOCTYPE html><html><head><meta charset="utf-8"><title>TubeBot</title>
<meta http-equiv="refresh" content="5">
<style>
body{{background:#0a0a0a;color:#fff;font-family:system-ui;margin:0;padding:20px;max-width:900px;margin:0 auto}}
h1{{color:#ff2d2d;font-size:3rem;text-align:center;font-weight:900;letter-spacing:2px}}
.box{{background:#111;border:1px solid #333;border-radius:12px;padding:20px;margin:20px 0}}
.stat{{font-size:1.1rem;color:#D4AF37}}
.logs{{background:#000;color:#D4AF37;padding:15px;border-radius:8px;font-family:monospace;height:400px;overflow-y:auto;font-size:0.9rem;line-height:1.6}}
.btn{{display:block;background:#ff0000;color:#fff;padding:18px;border-radius:10px;text-decoration:none;font-weight:800;text-align:center;font-size:1.2rem;margin:20px 0}}
.btn:hover{{background:#cc0000}}
hr{{border:none;border-top:1px solid #333;margin:25px 0}}
</style></head>
<body>
<h1>🤖 TUBEBOT — {NICHO.upper()}</h1>
<p style="text-align:center;color:#888">Tu canal publica solo cada 4 horas</p>
<div class="box">
<div class="stat">📊 <b>Videos publicados:</b> {CONTADOR_VIDEOS}</div>
<div class="stat">🎬 <b>Último video:</b> {ULTIMO_VIDEO}</div>
<div class="stat">⚡ <b>Estado:</b> {STATUS}</div>
<div class="stat">📺 <b>YouTube:</b> {yt_status}</div>
<div class="stat">⏰ <b>Próximo video en:</b> <span id="countdown">--:--:--</span></div>
</div>
<script>
let nextVideo = new Date().getTime() + 4*60*60*1000;
setInterval(()=>{{
let now = new Date().getTime();
let diff = nextVideo - now;
if(diff<0){{nextVideo = now + 4*60*60*1000; diff = 4*60*60*1000;}}
let h = Math.floor(diff/(1000*60*60));
let m = Math.floor((diff%(1000*60*60))/(1000*60));
let s = Math.floor((diff%(1000*60))/1000);
document.getElementById("countdown").innerText = h+"h "+m+"m "+s+"s";
}}, 1000);
</script>
<hr>
<h3 style="color:#ff2d2d">📜 Logs en tiempo real</h3>
<div class="logs">{logs_html}</div>
<hr>
<p style="text-align:center;color:#888">💡 ¿Problemas? WhatsApp: +34656691085</p>
</body></html>"""
uvicorn.run(app, host="0.0.0.0", port=7860)