BelloSilva4 / app.py
jfforero's picture
Update app.py
d72dc44 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
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)
model_size = "small"
whisper_model = WhisperModel(model_size, device="cpu", compute_type="int8")
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 = 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
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° (avión rojo + transiciones)
# ============================================================
def create_360_viewer_html(image_paths, audio_paths, output_path):
image_data_list = []
for img_path in image_paths:
try:
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}")
except Exception as e:
blank = Image.new("RGB", (2048, 1024), color="white")
buf = BytesIO()
blank.save(buf, format="JPEG", quality=75)
b64 = base64.b64encode(buf.getvalue()).decode("utf-8")
image_data_list.append(f"data:image/jpeg;base64,{b64}")
audio_data_list = []
for ap in audio_paths:
if ap and os.path.exists(ap):
with open(ap, "rb") as f:
audio_data_list.append(base64.b64encode(f.read()).decode("utf-8"))
else:
audio_data_list.append(None)
html = f'''<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>EVA 360 - Avión Rojo</title>
<script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
<style>
body {{ margin:0; overflow:hidden; }}
#overlay {{
position: fixed; top:0; left:0; width:100%; height:100%;
background: radial-gradient(circle, #1a1a2e 0%, #0f0f1b 100%);
color: white; display: flex; flex-direction: column; justify-content: center;
align-items: center; z-index: 9999; text-align: center;
transition: opacity 0.8s ease;
}}
h1 {{ margin-bottom: 10px; font-size: 32px; background: linear-gradient(45deg, #ff3737, #ff7300); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }}
button {{
padding: 18px 50px; font-size: 16px; background: #2a2a3a; color: #666;
border-radius: 30px; font-weight: bold; cursor: pointer;
}}
button.ready {{ background: linear-gradient(135deg, #f53737 0%, #cd2323 100%); color: white; }}
</style>
<script>
AFRAME.registerShader('noise-crossfade', {{
schema: {{ texA: {{type:'map'}}, texB: {{type:'map'}}, progress: {{type:'number',default:0}} }},
vertexShader: `varying vec2 vUV; void main() {{ vUV = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0); }}`,
fragmentShader: `
varying vec2 vUV;
uniform sampler2D texA; uniform sampler2D texB; uniform float progress;
float hash(vec2 p) {{ return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453123); }}
void main() {{
vec4 colorA = texture2D(texA, vec2(1.0-vUV.x, vUV.y));
vec4 colorB = texture2D(texB, vec2(1.0-vUV.x, vUV.y));
float n = hash(vUV*1000.0)*0.15;
float mixFactor = smoothstep(0.0,0.15,progress-n);
gl_FragColor = mix(colorA, colorB, mixFactor);
}}
`
}});
</script>
</head>
<body>
<div id="overlay"><h1>EVA 360 – VUELO INMERSIVO</h1><p id="status">Cargando {len(image_data_list)} panoramas...</p><button id="start-btn" class="ready">✈️ INICIAR VUELO</button></div>
<a-scene>
<a-entity camera look-controls></a-entity>
<a-sky id="dynamic-sky" material="shader:noise-crossfade; progress:0" animation__fade="property:material.progress; from:0; to:1; dur:2500; startEvents:startFade"></a-sky>
<a-light type="ambient" intensity="0.6"></a-light>
<a-light type="directional" intensity="1.2" position="10 35 15"></a-light>
<a-entity id="avion-contenedor">
<a-entity id="modelo-avion">
<a-triangle color="#f53737" vertex-a="0 0 1.4" vertex-b="1.2 -0.5 -0.85" vertex-c="0 -0.15 -1.0"></a-triangle>
<a-triangle color="#cd2323" vertex-a="0 0 1.4" vertex-b="0 -0.15 -1.0" vertex-c="-1.2 -0.5 -0.85"></a-triangle>
<a-cone id="propulsion-glow" position="0 -0.05 -1.05" rotation="90 0 0" radius-bottom="0.15" radius-top="0" height="0.4" material="color:#ff7300"></a-cone>
</a-entity>
</a-entity>
</a-scene>
<script>
const panoramasBase64 = {json.dumps(image_data_list)};
const audiosBase64 = {json.dumps(audio_data_list)};
let idx=0, audioActual=null, intervalo=null, activo=false;
const sky = document.getElementById('dynamic-sky');
let texA, texB;
function initTextures(){{
texA = document.createElement('img'); texB = document.createElement('img');
texA.src = panoramasBase64[0]; texB.src = panoramasBase64[1%panoramasBase64.length];
texA.onload = ()=> sky.setAttribute('material', 'texA', texA);
texB.onload = ()=> sky.setAttribute('material', 'texB', texB);
}}
function playAudio(i){{
if(audioActual) audioActual.pause();
if(!audiosBase64[i]) return;
const binary = atob(audiosBase64[i]);
const arr = new Uint8Array(binary.length);
for(let j=0;j<binary.length;j++) arr[j]=binary.charCodeAt(j);
const blob = new Blob([arr], {{type:'audio/wav'}});
const url = URL.createObjectURL(blob);
const audio = new Audio(url);
audio.loop = true; audio.volume = 0.7;
audio.play().catch(e=>console.warn(e));
audioActual = audio;
}}
function rotate(){{
if(!activo) return;
let next = (idx+1)%panoramasBase64.length;
let preload = (next+1)%panoramasBase64.length;
sky.emit('startFade');
setTimeout(()=>{{
idx = next;
texA.src = panoramasBase64[idx];
texB.src = panoramasBase64[preload];
sky.setAttribute('material', 'progress', 0);
playAudio(idx);
}},2500);
}}
let t=0;
const vel=0.001, escX=92, escY=30, escZ=44;
const avion = document.getElementById('avion-contenedor');
function movePlane(){{
if(!activo) return;
t+=vel; if(t>Math.PI*2) t-=Math.PI*2;
let x = Math.sin(t)*escX, y = Math.sin(t*2)*escY-3, z = Math.cos(t*3)*escZ;
avion.setAttribute('position', `${{x}} ${{y}} ${{z}}`);
let dx = Math.cos(t)*escX, dy = 2*Math.cos(t*2)*escY, dz = -3*Math.sin(t*3)*escZ;
let headingY = Math.atan2(dx,dz)*180/Math.PI;
let vHor = Math.hypot(dx,dz);
let headingX = Math.atan2(dy, vHor)*180/Math.PI;
let roll = Math.sin(t)*23;
avion.setAttribute('rotation', `${{-headingX}} ${{headingY}} ${{roll}}`);
requestAnimationFrame(movePlane);
}}
document.getElementById('start-btn').onclick = ()=>{{
document.getElementById('overlay').style.opacity='0';
setTimeout(()=>document.getElementById('overlay').style.display='none',800);
initTextures(); activo=true; playAudio(0);
if(intervalo) clearInterval(intervalo);
intervalo = setInterval(rotate, 25000);
movePlane();
}};
let loaded=0;
for(let i=0;i<panoramasBase64.length;i++){{
const img=new Image();
img.onload=()=>{{ loaded++; if(loaded===panoramasBase64.length) document.getElementById('status').innerHTML='✅ Listo.<br>¡Haz clic para volar!'; }};
img.src=panoramasBase64[i];
}}
</script>
</body>
</html>'''
with open(output_path, "w", encoding="utf-8") as f:
f.write(html)
return output_path
# ============================================================
# 8. Publicación a GitHub OAuth (FastAPI)
# ============================================================
_pending_gists = {}
def get_space_base_url():
host = os.getenv("SPACE_HOST")
return f"https://{host}" if host else "http://localhost:7860"
def trigger_github_oauth(html_file_path):
if not GITHUB_AVAILABLE:
return "❌ PyGithub no instalado."
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."
base_url = get_space_base_url()
redirect_uri = f"{base_url}/github_callback"
return f"https://github.com/login/oauth/authorize?client_id={client_id}&scope=gist&state={token}&redirect_uri={redirect_uri}"
fastapi_app = FastAPI()
@fastapi_app.get("/oauth_redirect.html")
async def serve_redirect_page():
return HTMLResponse('<html><body><h1>Autorización completada</h1><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="/")
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: OAuth no configurado.", status_code=500)
resp = requests.post("https://github.com/login/oauth/access_token",
data={"client_id": client_id, "client_secret": client_secret, "code": code, "state": state},
headers={"Accept": "application/json"})
if resp.status_code != 200:
return HTMLResponse("Error al obtener token", status_code=500)
access_token = resp.json().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.", 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="EVA 360 - Avión Rojo",
files={f"eva_360_{secrets.token_hex(4)}.html": {"content": content}})
return HTMLResponse(f'<h2>✅ Publicado</h2><p><a href="{gist.html_url}" target="_blank">{gist.html_url}</a></p><script>setTimeout(()=>window.close(),3000);</script>')
except Exception as e:
return HTMLResponse(f"Error: {str(e)}", status_code=500)
# ============================================================
# 9. Interfaz Gradio (definitiva, sin iconos gigantes)
# ============================================================
theme = gr.themes.Soft(primary_hue="red", secondary_hue="orange", font=gr.themes.GoogleFont("Inter"))
custom_css = """
.download-section { background: #f8f9fa; padding: 15px; border-radius: 12px; margin: 15px 0; }
audio { max-height: 50px !important; width: 100% !important; }
.file-download button, .file-download a { font-size: 12px !important; padding: 4px 8px !important; }
.gr-box { border-radius: 12px; }
.loader { border: 3px solid #f3f3f3; border-top: 3px solid #f53737; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
"""
MAX_SEGMENTS = 10
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.update(visible=True)] + [gr.update(visible=False)] * len(group_components) +
[None] * (MAX_SEGMENTS * 7) + [None, None, ""])
results = get_predictions(audio_input, generate_audio, chunk_duration)
outputs = []
group_vis = []
all_360_images = []
all_music_paths = []
for i in range(MAX_SEGMENTS):
if i < len(results):
res = results[i]
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)
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 = f"✅ Entorno generado con {len(results)} segmentos."
yield ([gr.update(visible=False)] + group_vis + outputs + [viewer_html_path, info_msg, ""])
def clear_all():
return ([None] + [gr.update(visible=False)] * len(group_components) +
[None] * (MAX_SEGMENTS * 7) + [gr.update(visible=False), 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."
if not GITHUB_AVAILABLE:
return "❌ PyGithub no instalado."
auth_url = trigger_github_oauth(html_file_path)
if auth_url.startswith("http"):
return f"🔐 [Autorizar en GitHub]({auth_url}) (se abrirá una ventana)"
return auth_url
with gr.Blocks(theme=theme, css=custom_css, title="EVA 360 - Avión Rojo") as interface:
gr.Markdown("# ✈️ EVA 360: Entornos Virtuales Afectivos con Avión Rojo")
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.Slider(label="⏱️ Segmento (segundos)", value=10, minimum=3, maximum=30, step=1)
gen_music = gr.Checkbox(label="🎵 Generar música", value=False)
with gr.Row():
process_btn = gr.Button("🚀 Generar EVA", variant="primary")
clear_btn = gr.Button("🗑️ Limpiar", variant="secondary")
loading_indicator = gr.HTML('<div class="loader"></div><p style="text-align:center">Procesando...</p>', visible=False)
with gr.Tabs():
for i in range(MAX_SEGMENTS):
with gr.TabItem(f"Segmento {i+1}", visible=False) as tab:
with gr.Group():
with gr.Row():
emo = gr.Label(label="😌 Emoción")
sent = gr.Label(label="💭 Sentimiento")
trans = gr.Textbox(label="📝 Transcripción", lines=2)
with gr.Row():
img_out = gr.Image(label="🖼️ Panorama 360°", height=240)
img_file = gr.File(label="📁 Descargar imagen")
with gr.Row():
audio_out = gr.Audio(label="🎶 Música")
audio_file = gr.File(label="📁 Descargar música")
group_components.append(tab)
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 visor HTML (avión + transiciones)", type="filepath", interactive=False)
publish_btn = gr.Button("📤 Publicar en GitHub (Gist)", variant="primary")
publish_status = gr.Markdown("")
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])
app = mount_gradio_app(fastapi_app, interface, path="/")
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)