BelloSilva3 / app.py
jfforero's picture
Update app.py
dd63068 verified
import gradio as gr
import numpy as np
import librosa
import requests
from io import BytesIO
from PIL import Image
import os
import secrets
import tempfile
import base64
import math
import struct
import cv2
import shutil
from tensorflow.keras.models import load_model
from faster_whisper import WhisperModel
from textblob import TextBlob
import torch
import scipy.io.wavfile
from transformers import AutoProcessor, MusicgenForConditionalGeneration
from pydub import AudioSegment
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse, HTMLResponse
from gradio.routes import mount_gradio_app
# Intentar importar PyGithub, pero si no está, mostrar mensaje amigable
try:
from github import Github, GithubException
GITHUB_AVAILABLE = True
except ImportError:
GITHUB_AVAILABLE = False
print("⚠️ PyGithub no instalado. La publicación en GitHub no estará disponible.")
# ============================================================
# 1. Carga de modelos de IA
# ============================================================
def load_emotion_model(model_path):
try:
m = load_model(model_path)
print("Emotion model loaded successfully")
return m
except Exception as e:
print("Error loading emotion model:", e)
return None
model_path = "mymodel_SER_LSTM_RAVDESS.h5"
model = load_emotion_model(model_path)
# Whisper
model_size = "small"
whisper_model = WhisperModel(model_size, device="cpu", compute_type="int8")
# MusicGen
def load_musicgen_model():
try:
device = "cuda" if torch.cuda.is_available() else "cpu"
processor = AutoProcessor.from_pretrained("facebook/musicgen-small")
music_model = MusicgenForConditionalGeneration.from_pretrained("facebook/musicgen-small")
music_model.to(device)
print("MusicGen model loaded successfully")
return processor, music_model, device
except Exception as e:
print("Error loading MusicGen:", e)
return None, None, None
processor, music_model, device = load_musicgen_model()
# DeepAI API key (opcional, para imágenes)
DEEPAI_API_KEY = os.getenv("DeepAI_api_key")
# ============================================================
# 2. Utilidades de audio y emociones
# ============================================================
def chunk_audio(audio_path, chunk_duration=10):
try:
audio = AudioSegment.from_file(audio_path)
duration_ms = len(audio)
chunk_ms = chunk_duration * 1000
if chunk_duration <= 0:
raise ValueError("Chunk duration must be positive")
if chunk_duration > duration_ms / 1000:
return [audio_path], 1
chunk_files = []
num_chunks = math.ceil(duration_ms / chunk_ms)
for i in range(num_chunks):
start_ms = i * chunk_ms
end_ms = min((i + 1) * chunk_ms, duration_ms)
chunk = audio[start_ms:end_ms]
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
chunk.export(tmp.name, format="wav")
chunk_files.append(tmp.name)
return chunk_files, num_chunks
except Exception as e:
print("Error chunking audio:", e)
return [audio_path], 1
def transcribe(wav_filepath):
try:
segments, _ = whisper_model.transcribe(wav_filepath, beam_size=5)
return "".join([seg.text for seg in segments])
except Exception as e:
print("Error transcribing:", e)
return "Transcription failed"
def extract_mfcc(wav_file_name):
try:
y, sr = librosa.load(wav_file_name)
mfccs = np.mean(librosa.feature.mfcc(y=y, sr=sr, n_mfcc=40).T, axis=0)
return mfccs
except Exception as e:
print("Error extracting MFCC:", e)
return None
emotions = {
0: "neutral", 1: "calm", 2: "happy", 3: "sad",
4: "angry", 5: "fearful", 6: "disgust", 7: "surprised",
}
def predict_emotion_from_audio(wav_filepath):
try:
if model is None:
return "Model not loaded"
feats = extract_mfcc(wav_filepath)
if feats is None:
return "Feature extraction error"
feats = np.reshape(feats, (1, 40, 1))
pred = model.predict(feats, verbose=0)
label = np.argmax(pred[0])
return emotions.get(label, "unknown")
except Exception as e:
print("Emotion prediction error:", e)
return "Prediction error"
def analyze_sentiment(text):
if not text or not text.strip():
return "neutral", 0.0
analysis = TextBlob(text)
polarity = analysis.sentiment.polarity
sentiment = "positive" if polarity > 0.1 else "negative" if polarity < -0.1 else "neutral"
return sentiment, polarity
# ============================================================
# 3. Prompts para imagen y música
# ============================================================
def get_image_prompt(sentiment, text, chunk_idx, total_chunks):
base = f"Generate an equirectangular 360° panoramic graphite sketch drawing, detailed pencil texture with faint neon glows, cinematic lighting of: {text[:200]}."
if sentiment == "positive":
return base + " Use bright, high contrast, rich colors, joyful atmosphere."
elif sentiment == "negative":
return base + " Use dark, low contrast, somber tones, melancholic atmosphere."
else:
return base + " Use balanced, neutral colors, calm atmosphere."
def get_music_prompt(emotion, text, chunk_idx, total_chunks):
prompts = {
"neutral": f"Neutral ambient orchestral music, steady tempo, no strong emotions, inspired by: {text[:100]}",
"calm": f"Calm, peaceful orchestral music, slow strings, soft dynamics, inspired by: {text[:100]}",
"happy": f"Happy, uplifting orchestral music, major key, lively rhythm, inspired by: {text[:100]}",
"sad": f"Sad, melancholic orchestral music, minor key, slow tempo, inspired by: {text[:100]}",
"angry": f"Angry, aggressive orchestral music, dissonant, strong percussion, inspired by: {text[:100]}",
"fearful": f"Fearful, tense orchestral music, unstable harmonies, suspenseful, inspired by: {text[:100]}",
"disgust": f"Disgusted, harsh orchestral music, irregular rhythm, rough textures, inspired by: {text[:100]}",
"surprised": f"Surprised, sudden changes, playful orchestral music, inspired by: {text[:100]}",
}
return prompts.get(emotion.lower(), f"Background music with {emotion} mood for: {text[:100]}")
# ============================================================
# 4. Generación de imagen (DeepAI) y música (MusicGen)
# ============================================================
def upscale_image(image, target_width=4096, target_height=2048):
try:
if not DEEPAI_API_KEY:
img = image.resize((target_width, target_height), Image.Resampling.LANCZOS)
return img
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
image.save(tmp.name, "JPEG", quality=95)
response = requests.post(
"https://api.deepai.org/api/torch-srgan",
files={"image": open(tmp.name, "rb")},
headers={"api-key": DEEPAI_API_KEY},
)
data = response.json()
if "output_url" in data:
img_resp = requests.get(data["output_url"])
up_img = Image.open(BytesIO(img_resp.content))
up_img = up_img.resize((target_width, target_height), Image.Resampling.LANCZOS)
return up_img
# fallback
return image.resize((target_width, target_height), Image.Resampling.LANCZOS)
except Exception as e:
print("Upscale error:", e)
return image.resize((target_width, target_height), Image.Resampling.LANCZOS)
def generate_image(sentiment, text, chunk_idx, total_chunks):
try:
prompt = get_image_prompt(sentiment, text, chunk_idx, total_chunks)
if DEEPAI_API_KEY:
response = requests.post(
"https://api.deepai.org/api/text2img",
data={"text": prompt, "width": 1024, "height": 512, "image_generator_version": "hd"},
headers={"api-key": DEEPAI_API_KEY},
)
data = response.json()
if "output_url" in data:
img_resp = requests.get(data["output_url"])
img = Image.open(BytesIO(img_resp.content))
else:
img = Image.new("RGB", (1024, 512), color="white")
else:
img = Image.new("RGB", (1024, 512), color="white")
up_img = upscale_image(img)
return up_img
except Exception as e:
print("Image generation error:", e)
return Image.new("RGB", (4096, 2048), color="white")
def generate_music(text, emotion, chunk_idx, total_chunks):
try:
if processor is None or music_model is None:
return None
prompt = get_music_prompt(emotion, text, chunk_idx, total_chunks)
if len(prompt) > 200:
prompt = prompt[:200] + "..."
inputs = processor(text=[prompt], padding=True, return_tensors="pt").to(device)
audio_values = music_model.generate(**inputs, max_new_tokens=512)
sampling_rate = music_model.config.audio_encoder.sampling_rate
audio_data = audio_values[0, 0].cpu().numpy()
audio_data = audio_data / max(1e-9, np.max(np.abs(audio_data)))
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
scipy.io.wavfile.write(tmp.name, rate=sampling_rate, data=audio_data)
return tmp.name
except Exception as e:
print("Music generation error:", e)
return None
# ============================================================
# 5. Metadatos 360° (XMP)
# ============================================================
def create_xmp_block(width, height):
return f'''<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="ExifTool">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about=""
xmlns:GPano="http://ns.google.com/photos/1.0/panorama/"
GPano:ProjectionType="equirectangular"
GPano:UsePanoramaViewer="True"
GPano:FullPanoWidthPixels="{width}"
GPano:FullPanoHeightPixels="{height}"
GPano:CroppedAreaImageWidthPixels="{width}"
GPano:CroppedAreaImageHeightPixels="{height}"
GPano:CroppedAreaLeftPixels="0"
GPano:CroppedAreaTopPixels="0"/>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end="w"?>'''
def write_xmp_to_jpg(input_path, output_path, width, height):
with open(input_path, "rb") as f:
data = f.read()
if data[0:2] != b"\xFF\xD8":
raise ValueError("Not a valid JPEG")
xmp_data = create_xmp_block(width, height)
app1_marker = b"\xFF\xE1"
xmp_header = b"http://ns.adobe.com/xap/1.0/\x00"
xmp_bytes = xmp_data.encode("utf-8")
length = len(xmp_header) + len(xmp_bytes) + 2
length_bytes = struct.pack(">H", length)
output = bytearray()
output.extend(data[0:2])
output.extend(app1_marker)
output.extend(length_bytes)
output.extend(xmp_header)
output.extend(xmp_bytes)
output.extend(data[2:])
with open(output_path, "wb") as f:
f.write(output)
def add_360_metadata(img):
try:
target_width, target_height = 4096, 2048
if img.size != (target_width, target_height):
img = img.resize((target_width, target_height), Image.Resampling.LANCZOS)
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
img.save(tmp.name, "JPEG", quality=90)
write_xmp_to_jpg(tmp.name, tmp.name, target_width, target_height)
return tmp.name
except Exception as e:
print("Error adding 360 metadata:", e)
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
img.save(tmp.name, "JPEG", quality=90)
return tmp.name
# ============================================================
# 6. Procesamiento de segmentos
# ============================================================
def process_chunk(chunk_path, idx, total, gen_audio):
try:
emotion = predict_emotion_from_audio(chunk_path)
text = transcribe(chunk_path)
sentiment, _ = analyze_sentiment(text)
img = generate_image(sentiment, text, idx, total)
img_360_path = add_360_metadata(img)
music_path = None
if gen_audio:
music_path = generate_music(text, emotion, idx, total)
return {
"emotion": emotion,
"transcription": text,
"sentiment": sentiment,
"image": img,
"image_360": img_360_path,
"music": music_path,
}
except Exception as e:
print(f"Chunk {idx+1} error:", e)
return {
"emotion": "Error",
"transcription": "Transcription failed",
"sentiment": "error",
"image": Image.new("RGB", (4096, 2048), color="white"),
"image_360": None,
"music": None,
}
def get_predictions(audio_input, gen_audio, chunk_duration):
chunk_files, total = chunk_audio(audio_input, chunk_duration)
results = []
for i, cf in enumerate(chunk_files):
print(f"Processing chunk {i+1}/{total}")
res = process_chunk(cf, i, total, gen_audio)
results.append(res)
for cf in chunk_files:
if cf != audio_input:
try: os.unlink(cf)
except: pass
return results
# ============================================================
# 7. Generación del visor HTML 360°
# ============================================================
def create_360_viewer_html(image_paths, audio_paths, output_path):
image_data_list = []
for img_path in image_paths:
img = Image.open(img_path)
img = img.resize((2048, 1024), Image.Resampling.LANCZOS)
buf = BytesIO()
img.save(buf, format="JPEG", quality=75, optimize=True)
b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
image_data_list.append(f"data:image/jpeg;base64,{b64}")
audio_base64_list = []
for ap in audio_paths:
if ap and os.path.exists(ap):
with open(ap, "rb") as f:
audio_base64_list.append(base64.b64encode(f.read()).decode("utf-8"))
else:
audio_base64_list.append(None)
html = f'''<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>EVA 360 - Visualizador Inmersivo</title>
<script src="https://aframe.io/releases/1.7.1/aframe.min.js"></script>
<style>
body {{ margin:0; overflow:hidden; font-family:sans-serif; }}
.controls {{ position:fixed; bottom:20px; left:20px; right:20px; background:rgba(0,0,0,0.7); border-radius:40px; padding:10px 20px; display:flex; gap:15px; justify-content:center; z-index:100; backdrop-filter:blur(5px); }}
button {{ background:#4a6fa5; border:none; color:white; padding:8px 20px; border-radius:30px; cursor:pointer; font-weight:bold; }}
button.active {{ background:#2ecc71; }}
#status {{ color:white; margin-left:auto; background:#333; padding:5px 15px; border-radius:20px; }}
</style>
</head>
<body>
<a-scene vr-mode-ui="enabled: true">
<a-sky id="sky" src="{image_data_list[0]}" rotation="0 -90 0"></a-sky>
<a-light type="ambient" intensity="0.3"></a-light>
</a-scene>
<div class="controls">
<button id="playPause">▶ Play</button>
<div><label style="color:white;">Continuous</label> <button id="continuous" class="active">ON</button></div>
<div><label style="color:white;">Random</label> <button id="random">OFF</button></div>
<div id="status">Scene 1 / {len(image_data_list)}</div>
</div>
<script>
const images = {image_data_list};
const audios = {audio_base64_list};
let state = {{ idx:0, playing:false, continuous:true, random:false, audio:null, timeout:null }};
const sky = document.getElementById("sky");
const playBtn = document.getElementById("playPause");
const continuousBtn = document.getElementById("continuous");
const randomBtn = document.getElementById("random");
const statusDiv = document.getElementById("status");
function b64toBlob(b64) {{
if(!b64) return null;
const bin = atob(b64);
const arr = new Uint8Array(bin.length);
for(let i=0;i<bin.length;i++) arr[i]=bin.charCodeAt(i);
return new Blob([arr], {{type:"audio/wav"}});
}}
function loadScene(i) {{
if(i<0||i>=images.length) return;
state.idx = i;
sky.setAttribute("src", images[i]);
statusDiv.innerText = `Scene ${{i+1}} / ${{images.length}}`;
}}
function stopAudio() {{
if(state.audio) {{ state.audio.pause(); state.audio.src = ""; state.audio.onended = null; }}
if(state.timeout) clearTimeout(state.timeout);
}}
function playAudio() {{
return new Promise((resolve) => {{
stopAudio();
const b64 = audios[state.idx];
if(!b64) {{
state.timeout = setTimeout(resolve, 2000);
return;
}}
const blob = b64toBlob(b64);
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
state.audio = audio;
audio.onended = () => {{ URL.revokeObjectURL(url); resolve(); }};
audio.play().catch(e => {{ console.warn(e); resolve(); }});
}});
}}
async function runLoop() {{
if(!state.playing) return;
loadScene(state.idx);
await playAudio();
if(state.playing && state.continuous) {{
let next = state.random ? Math.floor(Math.random()*images.length) : (state.idx+1)%images.length;
if(state.random && next===state.idx && images.length>1) next = (next+1)%images.length;
state.idx = next;
runLoop();
}} else if(!state.continuous) {{
state.playing = false;
playBtn.innerText = "▶ Play";
}}
}}
playBtn.onclick = () => {{
if(state.playing) {{
state.playing = false;
stopAudio();
playBtn.innerText = "▶ Play";
}} else {{
state.playing = true;
playBtn.innerText = "⏸ Pause";
runLoop();
}}
}};
continuousBtn.onclick = () => {{
state.continuous = !state.continuous;
continuousBtn.classList.toggle("active", state.continuous);
continuousBtn.innerText = state.continuous ? "ON" : "OFF";
}};
randomBtn.onclick = () => {{
state.random = !state.random;
randomBtn.classList.toggle("active", state.random);
randomBtn.innerText = state.random ? "ON" : "OFF";
}};
loadScene(0);
</script>
</body>
</html>'''
with open(output_path, "w", encoding="utf-8") as f:
f.write(html)
return output_path
# ============================================================
# 8. Publicación a GitHub (Gist) mediante OAuth
# ============================================================
_pending_gists = {} # {token: html_file_path}
def get_space_base_url():
# Hugging Face Spaces inyecta SPACE_HOST automáticamente
host = os.getenv("SPACE_HOST")
if host:
return f"https://{host}"
# Modo local
return "http://localhost:7860"
def trigger_github_oauth(html_file_path):
if not GITHUB_AVAILABLE:
return "❌ PyGithub no está instalado. Contacta al administrador."
token = secrets.token_urlsafe(16)
_pending_gists[token] = html_file_path
client_id = os.getenv("GITHUB_CLIENT_ID")
if not client_id:
return "⚠️ GitHub OAuth no configurado (falta GITHUB_CLIENT_ID)."
base_url = get_space_base_url()
redirect_uri = f"{base_url}/github_callback"
auth_url = f"https://github.com/login/oauth/authorize?client_id={client_id}&scope=gist&state={token}&redirect_uri={redirect_uri}"
return auth_url
# ============================================================
# 9. Aplicación FastAPI para el callback OAuth
# ============================================================
fastapi_app = FastAPI()
@fastapi_app.get("/oauth_redirect.html")
async def serve_redirect_page():
return HTMLResponse('''
<!DOCTYPE html>
<html>
<head><title>Autorizando...</title></head>
<body>
<h1>Autorización completada</h1>
<p>Esta ventana se cerrará automáticamente. Puedes volver a la aplicación.</p>
<script>window.close();</script>
</body>
</html>
''')
@fastapi_app.get("/github_callback")
async def github_callback(request: Request):
code = request.query_params.get("code")
state = request.query_params.get("state")
if not code or not state:
return RedirectResponse(url="/")
if not GITHUB_AVAILABLE:
return HTMLResponse("PyGithub no instalado.", status_code=500)
client_id = os.getenv("GITHUB_CLIENT_ID")
client_secret = os.getenv("GITHUB_CLIENT_SECRET")
if not client_id or not client_secret:
return HTMLResponse("Error: GitHub OAuth no configurado correctamente.", status_code=500)
token_url = "https://github.com/login/oauth/access_token"
payload = {
"client_id": client_id,
"client_secret": client_secret,
"code": code,
"state": state,
}
headers = {"Accept": "application/json"}
resp = requests.post(token_url, data=payload, headers=headers)
if resp.status_code != 200:
return HTMLResponse(f"Error al obtener token: {resp.text}", status_code=500)
token_data = resp.json()
access_token = token_data.get("access_token")
if not access_token:
return HTMLResponse("No se recibió access_token", status_code=500)
html_path = _pending_gists.pop(state, None)
if not html_path or not os.path.exists(html_path):
return HTMLResponse("El archivo HTML ya no está disponible. Vuelve a generar el entorno.", status_code=404)
try:
g = Github(access_token)
user = g.get_user()
with open(html_path, "r", encoding="utf-8") as f:
content = f.read()
gist = user.create_gist(
public=True,
description="Entorno Virtual Afectivo - EVA 360",
files={f"eva_360_{secrets.token_hex(4)}.html": {"content": content}}
)
gist_url = gist.html_url
return HTMLResponse(f'''
<!DOCTYPE html>
<html>
<head><title>Publicado en GitHub</title></head>
<body>
<h2>✅ Entorno publicado correctamente</h2>
<p>Tu EVA 360 está disponible en: <a href="{gist_url}" target="_blank">{gist_url}</a></p>
<p>Puedes cerrar esta ventana y volver a la aplicación.</p>
<script>setTimeout(() => window.close(), 5000);</script>
</body>
</html>
''')
except Exception as e:
return HTMLResponse(f"Error al crear Gist: {str(e)}", status_code=500)
# ============================================================
# 10. Interfaz Gradio
# ============================================================
output_containers = []
group_components = []
def process_and_display(audio_input, generate_audio, chunk_duration):
if chunk_duration is None or chunk_duration <= 0:
chunk_duration = 10
yield (
[gr.HTML(f'''
<div style="text-align:center; padding:20px;">
<p>Procesando audio en segmentos de {chunk_duration} segundos...</p>
<div class="loader"></div>
<style>.loader {{ border:4px solid #f3f3f3; border-top:4px solid #3498db; border-radius:50%; width:30px; height:30px; animation:spin 2s linear infinite; margin:auto; }} @keyframes spin {{ 0% {{ transform:rotate(0deg); }} 100% {{ transform:rotate(360deg); }} }}</style>
</div>''')]
+ [gr.update(visible=False)] * len(group_components)
+ [None] * (len(output_containers) * 7)
+ [None, None, ""]
)
results = get_predictions(audio_input, generate_audio, chunk_duration)
outputs = []
group_vis = []
all_360_images = []
all_music_paths = []
for i, res in enumerate(results):
if i < len(output_containers):
group_vis.append(gr.update(visible=True))
outputs.extend([
res["emotion"], res["transcription"], res["sentiment"],
res["image"], res["image_360"], res["music"], res["music"]
])
if res["image_360"]:
all_360_images.append(res["image_360"])
if res["music"]:
all_music_paths.append(res["music"])
else:
group_vis.append(gr.update(visible=False))
outputs.extend([None]*7)
for _ in range(len(results), len(output_containers)):
group_vis.append(gr.update(visible=False))
outputs.extend([None]*7)
viewer_html_path = None
if all_360_images:
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as tmp:
viewer_html_path = create_360_viewer_html(all_360_images, all_music_paths, tmp.name)
info_msg = "✅ Entorno generado. Puedes descargar el HTML y, si lo deseas, publicarlo en tu GitHub con el botón especial."
yield [gr.HTML("")] + group_vis + outputs + [viewer_html_path, info_msg, ""]
def clear_all():
return [None] + [gr.update(visible=False)] * len(group_components) + [None] * (len(output_containers)*7) + [gr.HTML(""), 10, None, None, ""]
def publish_to_github(html_file_path):
if not html_file_path or not os.path.exists(html_file_path):
return "❌ No hay ningún entorno generado. Primero genera tu EVA."
if not GITHUB_AVAILABLE:
return "❌ PyGithub no está instalado. No se puede publicar."
auth_url = trigger_github_oauth(html_file_path)
if auth_url.startswith("http"):
return f"🔐 **Para publicar en tu GitHub, [haz clic aquí para autorizar]({auth_url}).** Se abrirá una ventana emergente. Luego vuelve aquí."
else:
return auth_url
# Construcción de la UI
custom_css = """
.download-section { background: #f9f9f9; padding: 20px; border-radius: 15px; border: 1px solid #ddd; margin: 20px 0; }
.download-button { background: #4CAF50 !important; color: white !important; }
"""
with gr.Blocks(css=custom_css, title="EVA 360 - Entornos Virtuales Afectivos") as interface:
gr.Markdown("# Bello: Entornos Virtuales Afectivos")
gr.Markdown("""
Sube o graba un audio y el sistema generará un entorno 360° con imágenes y música basada en la emoción y el contenido.
""")
with gr.Row():
with gr.Column(scale=2):
audio_input = gr.Audio(label="Audio de entrada", type="filepath", sources=["microphone", "upload"])
with gr.Column(scale=1):
chunk_duration = gr.Number(label="Duración del segmento (segundos)", value=10, minimum=1, maximum=60, step=1)
gen_music = gr.Checkbox(label="Generar música (más lento)", value=False)
with gr.Row():
process_btn = gr.Button("Generar EVA", variant="primary")
clear_btn = gr.Button("Limpiar", variant="secondary")
loading_indicator = gr.HTML("")
for i in range(20):
with gr.Group(visible=False) as grp:
gr.Markdown(f"### Segmento {i+1}")
with gr.Row():
emo = gr.Label(label="Emoción")
trans = gr.Label(label="Transcripción")
sent = gr.Label(label="Sentimiento")
with gr.Row():
img_out = gr.Image(label="Imagen 360° generada")
img_file = gr.File(label="Descargar imagen 360°", type="filepath")
with gr.Row():
audio_out = gr.Audio(label="Música generada")
audio_file = gr.File(label="Descargar música", type="filepath")
gr.HTML("<hr>")
group_components.append(grp)
output_containers.append({
"emotion": emo, "transcription": trans, "sentiment": sent,
"image": img_out, "image_360": img_file,
"music": audio_out, "music_file": audio_file
})
with gr.Group(elem_classes="download-section"):
html_download = gr.File(label="Descargar tu EVA (HTML)", type="filepath", interactive=False)
publish_btn = gr.Button("📤 Publicar en mi GitHub (Gist)", variant="primary")
publish_status = gr.Markdown("*Al hacer clic, se te redirigirá a GitHub para autorizar la publicación.*")
js_output = gr.HTML(visible=False)
process_btn.click(
fn=process_and_display,
inputs=[audio_input, gen_music, chunk_duration],
outputs=[loading_indicator] + group_components +
[comp for cont in output_containers for comp in [cont["emotion"], cont["transcription"], cont["sentiment"],
cont["image"], cont["image_360"], cont["music"], cont["music_file"]]] +
[html_download, publish_status, js_output]
)
clear_btn.click(
fn=clear_all,
inputs=[],
outputs=[audio_input] + group_components +
[comp for cont in output_containers for comp in [cont["emotion"], cont["transcription"], cont["sentiment"],
cont["image"], cont["image_360"], cont["music"], cont["music_file"]]] +
[loading_indicator, chunk_duration, html_download, publish_status, js_output]
)
publish_btn.click(
fn=publish_to_github,
inputs=[html_download],
outputs=[publish_status]
)
# ============================================================
# 11. Montar la app FastAPI + Gradio
# ============================================================
app = mount_gradio_app(fastapi_app, interface, path="/")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)