import gradio as gr import tempfile import os import re import json import subprocess import urllib.parse import urllib.request import time import hashlib STUDIO_NAME = "Aether Studio" MODEL_DISPLAY_NAME = "Nemotron 3 Ultra" OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" OPENROUTER_KEY = os.environ.get( "OPENROUTER_API_KEY", "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", ) MODEL_ID = os.environ.get("OPENROUTER_MODEL", "nvidia/nemotron-3-ultra-550b-a55b:free") CACHE_DIR = "/tmp/aether_cache" os.makedirs(CACHE_DIR, exist_ok=True) SOUNDFONT_CANDIDATES = [ "/usr/share/sounds/sf2/FluidR3_GM.sf2", "/usr/share/sounds/sf2/default-GM.sf2", "/usr/share/sounds/sf2/TimGM6mb.sf2", "/usr/share/soundfonts/FluidR3_GM.sf2", "/usr/share/sounds/sf2/default.sf2", ] NOTE_TO_MIDI = { "C": 0, "C#": 1, "Db": 1, "D": 2, "D#": 3, "Eb": 3, "E": 4, "F": 5, "F#": 6, "Gb": 6, "G": 7, "G#": 8, "Ab": 8, "A": 9, "A#": 10, "Bb": 10, "B": 11, } DUR = {"w": 4.0, "h": 2.0, "q": 1.0, "e": 0.5, "s": 0.25} GM_PROGRAM = { "piano": 0, "bright_piano": 1, "grand_piano": 0, "electric_piano": 4, "harpsichord": 6, "celesta": 8, "glockenspiel": 9, "music_box": 10, "vibraphone": 11, "marimba": 12, "xylophone": 13, "bells": 14, "dulcimer": 15, "organ": 19, "church_organ": 19, "accordion": 21, "harmonica": 22, "guitar": 24, "nylon_guitar": 24, "steel_guitar": 25, "clean_guitar": 27, "lute": 24, "oud": 24, "acoustic_bass": 32, "finger_bass": 33, "pick_bass": 34, "fretless_bass": 35, "synth_bass": 38, "violin": 40, "viola": 41, "cello": 42, "contrabass": 43, "pizzicato_strings": 45, "harp": 46, "timpani": 47, "strings": 48, "slow_strings": 49, "synth_strings": 50, "choir": 52, "synth_voice": 54, "orchestra_hit": 55, "trumpet": 56, "trombone": 57, "tuba": 58, "muted_trumpet": 59, "french_horn": 60, "brass": 61, "synth_brass": 62, "soprano_sax": 64, "alto_sax": 65, "tenor_sax": 66, "oboe": 68, "english_horn": 69, "bassoon": 70, "clarinet": 71, "piccolo": 72, "flute": 73, "recorder": 74, "pan_flute": 75, "whistle": 78, "ocarina": 79, "square_lead": 80, "saw_lead": 81, "calliope_lead": 82, "bass_lead": 87, "pad": 88, "warm_pad": 89, "polysynth_pad": 90, "choir_pad": 91, "bowed_pad": 92, "sitar": 104, "banjo": 105, "koto": 107, "kalimba": 108, "bagpipe": 109, "fiddle": 110, "steel_drums": 114, } SOLO_INSTRUMENTS = {"recorder", "flute", "whistle", "ocarina", "pan_flute"} SUSTAIN_INSTRUMENTS = { "piano", "grand_piano", "bright_piano", "electric_piano", "harp", "strings", "slow_strings", "synth_strings", "guitar", "lute", } CHORD_QUALITIES = { "": [0, 4, 7], "maj": [0, 4, 7], "m": [0, 3, 7], "min": [0, 3, 7], "7": [0, 4, 7, 10], "m7": [0, 3, 7, 10], "min7": [0, 3, 7, 10], "maj7": [0, 4, 7, 11], "dim": [0, 3, 6], "dim7": [0, 3, 6, 9], "aug": [0, 4, 8], "sus2": [0, 2, 7], "sus4": [0, 5, 7], "5": [0, 7], } _QUALITY_RE = r"(maj7|min7|sus2|sus4|dim7|maj|min|m7|dim|aug|7|5|m)?" DYNAMIC_LEVELS = {"pp": 42, "p": 58, "mp": 74, "mf": 90, "f": 106, "ff": 120} PERCUSSION_STYLES = {"epica", "marcha", "ninguno"} NOTE_TOKEN_RE = re.compile(r"^([A-G][#b]?)(\d)([qehws]?)([-!]{0,2})$") REST_TOKEN_RE = re.compile(r"^R([qehws])$", re.I) CHORD_TOKEN_RE = re.compile(r"^([A-G][#b]?)" + _QUALITY_RE + r"(\d(?:\.\d)?)?$") SYSTEM = """Eres el compositor de Aether Studio. Tu trabajo es generar partituras instrumentales en formato PMNS. RECHAZO: si el usuario NO pide musica/cancion/composicion, responde exactamente: lo siento no puedo hacer eso HERRAMIENTAS (usalas cuando las necesites, antes de la partitura final): 1) TOOL_SEARCH: Busca en la web datos musicales (modos, tempos tipicos, instrumentacion de un genero, estructura de un estilo). 2) TOOL_VALIDATE: Valida gramatica PMNS. Te devuelve OK o lista de errores con linea. 3) TOOL_INSTRUMENTS: Lista instrumentos GM validos. Flujo recomendado: - Si dudas del estilo/modo/instrumentacion -> TOOL_SEARCH - Escribe borrador PMNS - TOOL_VALIDATE si quieres comprobar - Cuando este listo, responde SOLO la partitura PMNS (sin TOOL_*, sin explicaciones) GRAMATICA PMNS: obra := cabecera seccion+ cabecera := "# " titulo / "## Estilo" / "## Tempo" / "## Tonalidad" seccion := "### " nombre + campos campos posibles: instrumento: tempo: dinamica: pp|p|mp|mf|f|ff repetir: <1-8> percusion: epica|marcha|ninguno notas: acordes: Notas: Nota+Octava+Duracion[+articulacion] Nota: C C# Db D D# Eb E F F# Gb G G# Ab A A# Bb B Octava: 1-7 Duracion: w h q e s (si omites duracion = q) Articulacion: - legato ! acento Silencio: Rq Re Rh Rw Rs Acordes: Root+calidad+pulsos ej: Dm2 Fmaj74 Am (pulsos opcionales, default 2) calidades: m min maj 7 m7 maj7 dim dim7 aug sus2 sus4 5 Reglas de calidad: - 6-8 secciones para ~1 minuto - >=10 notas por seccion de melodia - Usa tu conocimiento musical + resultados de tools - No inventes instrumentos fuera del catalogo - No escribas letra cantada, solo instrumental - No uses escalas mecanicas como unica melodia Ejemplo de forma (no copies el estilo, solo la forma): # Titulo ## Estilo descripcion ## Tempo 100 ## Tonalidad D dorico ## Estructura ### Intro instrumento: lute dinamica: p notas: D4q F4q A4q D5h acordes: Dm2 Am2 ### Tema instrumento: recorder dinamica: mf repetir: 1 notas: A4q C5q D5q F5h E5q D5q C5h acordes: Dm2 F2 ### Outro instrumento: lute dinamica: p notas: D4h A3h D3w acordes: Dm4 """ def parse_note_token(tok): m = NOTE_TOKEN_RE.match(tok.strip()) if not m: return None name, octv, d, art = m.groups() if name not in NOTE_TO_MIDI: return None midi = NOTE_TO_MIDI[name] + (int(octv) + 1) * 12 beats = DUR.get(d or "q", 1.0) return midi, beats, ("-" in art), ("!" in art) def parse_chord_token(tok): m = CHORD_TOKEN_RE.match(tok.strip()) if not m: return None root, quality, beats = m.groups() if root not in NOTE_TO_MIDI: return None return { "root": NOTE_TO_MIDI[root] + 48, "intervals": CHORD_QUALITIES[quality or ""], "beats": float(beats) if beats else 2.0, } def detect_inst(text): t = text.lower().strip() for w in ("flauta dulce", "flauta de pico", "recorder"): if w in t: return "recorder" if "flauta" in t or "flute" in t: return "flute" for k in sorted(GM_PROGRAM.keys(), key=len, reverse=True): if k in t: return k for es, en in ( ("laud", "lute"), ("laúd", "lute"), ("guitarra", "guitar"), ("arpa", "harp"), ("trompeta", "trumpet"), ("cuerdas", "strings"), ("organo", "organ"), ("campanas", "bells"), ): if es in t: return en return "piano" def parse_pmns(md): errors = [] work = {"title": "Sin titulo", "tempo": 100, "style": "", "key": "", "sections": []} tm = re.search(r"##\s*Tempo\s*\n\s*(\d+)", md, re.I) if tm: work["tempo"] = max(40, min(220, int(tm.group(1)))) ttm = re.search(r"^\s*#\s+(.+)$", md, re.M) if ttm: work["title"] = ttm.group(1).strip() sm = re.search(r"##\s*Estilo\s*\n(.+?)(?=\n##|\n###|$)", md, re.I | re.S) if sm: work["style"] = sm.group(1).strip() km = re.search(r"##\s*Tonalidad\s*\n(.+?)(?=\n##|\n###|$)", md, re.I | re.S) if km: work["key"] = km.group(1).strip() default_inst = detect_inst(work["style"]) for raw in re.split(r"###\s+", md)[1:]: lines = raw.split("\n") name = lines[0].strip() line0 = md[: md.find(raw)].count("\n") + 1 if raw in md else 0 sec = { "name": name, "instrument": default_inst, "tempo": work["tempo"], "notes": "", "chords": "", "dynamic": None, "repeat": 1, "bass": "auto", "percusion": "ninguno", "line_no": line0, } for i, line in enumerate(lines[1:], start=line0 + 1): low = line.lower().strip() if low.startswith("notas:"): sec["notes"] = line.split(":", 1)[1].strip() elif low.startswith("acordes:"): sec["chords"] = line.split(":", 1)[1].strip() elif low.startswith("instrumento:"): raw_inst = line.split(":", 1)[1].strip() inst = detect_inst(raw_inst) if raw_inst.lower().split()[0] not in GM_PROGRAM and inst == "piano" and "piano" not in raw_inst.lower(): errors.append(f"linea ~{i}: instrumento desconocido '{raw_inst}'") sec["instrument"] = inst elif low.startswith("tempo:"): m = re.search(r"(\d+)", line) sec["tempo"] = max(40, min(220, int(m.group(1)))) if m else work["tempo"] elif low.startswith("dinamica:") or low.startswith("dinámica:"): dyn = line.split(":", 1)[1].strip().lower() if dyn in DYNAMIC_LEVELS: sec["dynamic"] = dyn else: errors.append(f"linea ~{i}: dinamica invalida '{dyn}'") elif low.startswith("percusion:") or low.startswith("percusión:"): p = line.split(":", 1)[1].strip().lower() sec["percusion"] = p if p in PERCUSSION_STYLES else "ninguno" elif low.startswith("repetir:"): m = re.search(r"(\d+)", line) sec["repeat"] = max(1, min(8, int(m.group(1)))) if m else 1 if not sec["notes"]: errors.append(f"seccion '{name}': falta notas") else: bad = [] for tok in sec["notes"].split(): if REST_TOKEN_RE.match(tok): continue if not parse_note_token(tok): bad.append(tok) if bad and len(errors) < 8: errors.append(f"seccion '{name}': tokens invalidos {bad[:5]}") for tok in sec["chords"].split(): if not parse_chord_token(tok) and len(errors) < 10: errors.append(f"seccion '{name}': acorde invalido '{tok}'") work["sections"].append(sec) if not work["sections"]: errors.append("no hay secciones ###") return work, errors def score_to_midi(md, path, strict=True): import pretty_midi work, errors = parse_pmns(md) if errors and strict: raise ValueError("; ".join(errors[:6])) pm = pretty_midi.PrettyMIDI(initial_tempo=work["tempo"]) instruments = {} def get_inst(name, is_drum=False): key = "drums" if is_drum else name if key not in instruments: prog = 0 if is_drum else GM_PROGRAM.get(name, 0) instruments[key] = pretty_midi.Instrument(program=prog, is_drum=is_drum, name=name) return instruments[key] global_time = 0.0 n_sections = len(work["sections"]) for s_idx, sec in enumerate(work["sections"]): beat_sec = 60.0 / sec["tempo"] base_vel = DYNAMIC_LEVELS[sec["dynamic"]] if sec["dynamic"] else int(58 + (s_idx / max(n_sections - 1, 1)) * 54) inst_name = sec["instrument"] is_solo = inst_name in SOLO_INSTRUMENTS for _rep in range(sec["repeat"]): sec_start = global_time inst = get_inst(inst_name) t = sec_start for tok in sec["notes"].split(): rm = REST_TOKEN_RE.match(tok) if rm: t += DUR[rm.group(1).lower()] * beat_sec continue parsed = parse_note_token(tok) if not parsed: continue midi, beats, legato, accent = parsed vel = min(127, base_vel + (14 if accent else 0)) gate = 1.0 if legato else 0.96 dur = beats * beat_sec inst.notes.append( pretty_midi.Note( velocity=vel, pitch=min(max(midi, 21), 108), start=t, end=t + max(dur * gate, 0.05), ) ) t += dur melodic_end = t if sec["chords"] and not is_solo: ch_name = ( "warm_pad" if inst_name in ("pad", "strings", "choir", "synth_strings", "choir_pad", "brass") else "harp" ) ch_inst = get_inst(ch_name) ct = sec_start for tok in sec["chords"].split(): ch = parse_chord_token(tok) if not ch: continue dur_b = ch["beats"] for j, off in enumerate(ch["intervals"]): ch_inst.notes.append( pretty_midi.Note( velocity=min(127, int(base_vel * 0.34) + (5 if j == 0 else 0)), pitch=min(max(ch["root"] + off, 21), 108), start=ct, end=ct + dur_b * beat_sec, ) ) ct += dur_b * beat_sec if sec["bass"] in ("auto", "si", "sí") and sec["chords"] and not is_solo: bass = get_inst("finger_bass") bt = sec_start for tok in sec["chords"].split(): ch = parse_chord_token(tok) if not ch: continue root_bass = ch["root"] - 24 for k in range(max(1, int(ch["beats"]))): bass.notes.append( pretty_midi.Note( velocity=int(base_vel * 0.72), pitch=min(max(root_bass, 21), 48), start=bt + k * beat_sec, end=bt + k * beat_sec + 0.9 * beat_sec, ) ) bt += ch["beats"] * beat_sec if sec["percusion"] in ("epica", "marcha"): drums = get_inst("timpani") pt = sec_start while pt < melodic_end - 0.01: drums.notes.append( pretty_midi.Note( velocity=int(base_vel * 0.85), pitch=41 if sec["percusion"] == "epica" else 45, start=pt, end=pt + 0.2, ) ) pt += (2.0 if sec["percusion"] == "epica" else 1.0) * beat_sec if inst_name in SUSTAIN_INSTRUMENTS and melodic_end > sec_start: inst.control_changes.append(pretty_midi.ControlChange(64, 127, sec_start)) inst.control_changes.append(pretty_midi.ControlChange(64, 0, melodic_end)) global_time = melodic_end for inst in instruments.values(): if inst.notes: pm.instruments.append(inst) if not pm.instruments: if strict: raise ValueError("No hay notas interpretables") inst = get_inst("piano") for i, p in enumerate([60, 64, 67, 72]): inst.notes.append(pretty_midi.Note(90, p, i * 0.5, i * 0.5 + 0.4)) pm.instruments.append(inst) pm.write(path) return path, work def find_soundfont(): for p in SOUNDFONT_CANDIDATES: if os.path.isfile(p): return p return None def midi_to_mp3(midi_path, mp3_path): from pydub import AudioSegment sf2 = find_soundfont() if not sf2: raise RuntimeError("No SoundFont (instala fluid-soundfont-gm)") wav_path = midi_path.replace(".mid", ".wav") subprocess.run( ["fluidsynth", "-ni", "-g", "1.3", "-R", "1", "-C", "1", sf2, midi_path, "-F", wav_path, "-r", "44100"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) audio = AudioSegment.from_wav(wav_path) peak = audio.max_dBFS if peak < -3: audio = audio.apply_gain(-3 - peak) audio.export(mp3_path, format="mp3", bitrate="192k") if os.path.exists(wav_path): os.unlink(wav_path) return mp3_path def tool_web_search(query, max_results=5): results = [] try: q = urllib.parse.quote(query) url = f"https://api.duckduckgo.com/?q={q}&format=json&no_html=1&skip_disambig=1" req = urllib.request.Request(url, headers={"User-Agent": "AetherStudio/1.0"}) with urllib.request.urlopen(req, timeout=8) as resp: data = json.loads(resp.read().decode("utf-8", errors="ignore")) if data.get("AbstractText"): results.append(data["AbstractText"][:500]) for t in data.get("RelatedTopics", [])[:max_results]: if isinstance(t, dict) and t.get("Text"): results.append(t["Text"][:300]) except Exception: pass try: q = urllib.parse.quote(query.replace(" ", "_")) url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{q}" req = urllib.request.Request(url, headers={"User-Agent": "AetherStudio/1.0"}) with urllib.request.urlopen(req, timeout=8) as resp: data = json.loads(resp.read().decode("utf-8", errors="ignore")) if data.get("extract"): results.append(data["extract"][:700]) except Exception: pass return "\n".join(f"- {r}" for r in results[:6]) if results else "Sin resultados. Usa tu conocimiento." def tool_validate(md): _, errors = parse_pmns(md) if not errors: return "OK: partitura valida" return "ERRORES:\n" + "\n".join(f"- {e}" for e in errors[:12]) def tool_instruments(): return "Instrumentos validos: " + ", ".join(sorted(GM_PROGRAM.keys())) def openrouter_chat(messages, max_tokens=2000): body = json.dumps({ "model": MODEL_ID, "messages": messages, "max_tokens": max_tokens, "temperature": 0.7, }).encode() req = urllib.request.Request( OPENROUTER_URL, data=body, headers={ "Authorization": f"Bearer {OPENROUTER_KEY}", "Content-Type": "application/json", "HTTP-Referer": "https://huggingface.co", "X-Title": "Aether Studio", }, method="POST", ) with urllib.request.urlopen(req, timeout=120) as resp: data = json.loads(resp.read().decode()) return data["choices"][0]["message"]["content"] def clean_score(text): text = text.strip() if text.lower().startswith("lo siento no puedo hacer eso") and "notas:" not in text.lower(): return "lo siento no puedo hacer eso" idx = text.find("#") if idx == -1: return None md = text[idx:] for stop in ("```", "<|im_end|>"): if stop in md: md = md[: md.find(stop)] if "notas:" not in md.lower(): return None return md.strip() def agent_compose(prompt, max_rounds=4): messages = [ {"role": "system", "content": SYSTEM}, {"role": "user", "content": prompt.strip()}, ] last = "" for _ in range(max_rounds): last = openrouter_chat(messages, max_tokens=2200) search = re.search(r"TOOL_SEARCH:\s*(.+)", last, re.I) if search: q = search.group(1).strip().split("\n")[0][:200] result = tool_web_search(q) messages.append({"role": "assistant", "content": last}) messages.append({"role": "user", "content": f"Resultado TOOL_SEARCH ({q}):\n{result}\nContinua. Si ya puedes, escribe SOLO la partitura PMNS."}) continue if re.search(r"TOOL_INSTRUMENTS\s*$", last, re.I | re.M) or "TOOL_INSTRUMENTS" in last and "notas:" not in last.lower(): result = tool_instruments() messages.append({"role": "assistant", "content": last}) messages.append({"role": "user", "content": f"{result}\nContinua con la partitura PMNS."}) continue val = re.search(r"TOOL_VALIDATE:\s*(.+)", last, re.I | re.S) if val and "notas:" in last.lower(): md_try = clean_score(last) or last result = tool_validate(md_try) messages.append({"role": "assistant", "content": last}) messages.append({"role": "user", "content": f"Resultado TOOL_VALIDATE:\n{result}\nSi hay errores, corrige y escribe SOLO la partitura final. Si OK, escribe SOLO la partitura."}) continue md = clean_score(last) if md and md != "lo siento no puedo hacer eso": return md, last if md == "lo siento no puedo hacer eso": return md, last messages.append({"role": "assistant", "content": last}) messages.append({"role": "user", "content": "Responde ahora SOLO con la partitura PMNS completa (empieza con #)."}) md = clean_score(last) return md or last, last LIBRARY = [] def get_duration(mp3_path): try: from pydub import AudioSegment a = AudioSegment.from_mp3(mp3_path) s = len(a) / 1000 return f"{int(s // 60)}:{int(s % 60):02d}" except Exception: return "--:--" def render_library(): if not LIBRARY: return '
🎵
Aún no hay composiciones
Escribe una descripción y pulsa Crear.
' cards = [] for s in LIBRARY: h = abs(hash(s["title"])) % 360 grad = f"linear-gradient(135deg, hsl({h},65%,45%) 0%, hsl({(h+50)%360},65%,25%) 100%)" style_short = s["style"][:70] + ("…" if len(s["style"]) > 70 else "") cards.append( f'
' f'
♪' f'{s["duration"]}
' f'
{s["title"]}
' f'
{style_short}
' f'
{s["tempo"]} BPM·{s["key"]}
' f'
▶
' ) return f'
{"".join(cards)}
' CUSTOM_CSS = """ .gradio-container { background: #0a0a0d !important; color: #e5e7eb !important; font-family: Inter, sans-serif !important; max-width: 100% !important; } footer { display: none !important; } #aether-topbar { display:flex; align-items:center; gap:14px; padding:14px 26px; background:#0f0f14; border-bottom:1px solid #1c1c25; } #aether-logo { width:34px; height:34px; border-radius:9px; background:linear-gradient(135deg,#8b5cf6,#06b6d4); display:flex; align-items:center; justify-content:center; } #aether-brand-name { font-size:15px; font-weight:700; color:#fff; } #aether-brand-model { font-size:11px; font-weight:600; color:#67e8f9; background:rgba(6,182,212,0.10); border:1px solid rgba(6,182,212,0.28); padding:3px 9px; border-radius:999px; margin-left:10px; } #aether-main { display:flex; gap:18px; padding:18px; min-height:70vh; } #aether-create { width:380px; flex-shrink:0; background:#0f0f14; border:1px solid #1c1c25; border-radius:14px; padding:18px; } #compose-btn { background:linear-gradient(135deg,#8b5cf6,#06b6d4) !important; border:none !important; border-radius:11px !important; color:#fff !important; font-weight:700 !important; width:100% !important; } #aether-library { flex:1; background:#0f0f14; border:1px solid #1c1c25; border-radius:14px; padding:18px; } .library { display:flex; flex-direction:column; gap:10px; } .song-card { display:flex; align-items:center; gap:14px; padding:12px; background:#13131a; border:1px solid #1c1c25; border-radius:12px; cursor:pointer; } .song-cover { width:64px; height:64px; border-radius:9px; display:flex; align-items:center; justify-content:center; position:relative; } .cover-dur { position:absolute; bottom:5px; right:6px; font-size:10px; color:#fff; background:rgba(0,0,0,0.65); padding:1px 5px; border-radius:4px; } .song-title { font-size:14.5px; font-weight:600; color:#fff; } .song-meta { font-size:12px; color:#94a3b8; } .song-stats { font-size:11px; color:#64748b; display:flex; gap:6px; } .song-play { width:34px; height:34px; border-radius:50%; background:rgba(139,92,246,0.15); color:#c4b5fd; display:flex; align-items:center; justify-content:center; } .empty-library { text-align:center; padding:60px 20px; color:#64748b; } #aether-player { padding:12px 24px; background:#0c0c11; border-top:1px solid #1c1c25; } #song-selector { display:none !important; } """ HEADER_HTML = f"""
{STUDIO_NAME}
{MODEL_DISPLAY_NAME}
""" SELECT_JS = """""" def process_request(prompt): if not prompt or not prompt.strip(): yield "Escribe qué canción quieres.", "", render_library(), gr.update(), gr.update() return yield "🎼 *Componiendo con Nemotron 3 Ultra (OpenRouter)...*", "", render_library(), gr.update(), gr.update() try: md, raw = agent_compose(prompt) except Exception as e: yield f"❌ Error OpenRouter: {e}", str(e), render_library(), gr.update(), gr.update() return if md == "lo siento no puedo hacer eso": yield "🚫 No es una petición musical.", raw, render_library(), gr.update(), gr.update() return if not md or "notas:" not in md.lower(): yield "🚫 El modelo no devolvió partitura válida.", raw, render_library(), gr.update(), gr.update() return yield "⏳ *Sintetizando audio...*", raw, render_library(), gr.update(), gr.update() mid = tempfile.NamedTemporaryFile(suffix=".mid", delete=False).name mp3 = mid.replace(".mid", ".mp3") try: try: _, work = score_to_midi(md, mid, strict=True) warnings = [] except ValueError as ve: _, work = score_to_midi(md, mid, strict=False) warnings = [str(ve)] midi_to_mp3(mid, mp3) song_id = hashlib.md5(f"{work['title']}{time.time()}".encode()).hexdigest()[:10] cached_mp3 = os.path.join(CACHE_DIR, f"{song_id}.mp3") subprocess.run(["cp", mp3, cached_mp3], check=False) song_meta = { "id": song_id, "title": work.get("title", "Pieza"), "style": work.get("style") or "Instrumental", "tempo": work.get("tempo", 100), "key": work.get("key") or "—", "duration": get_duration(cached_mp3), "path": cached_mp3, } LIBRARY.insert(0, song_meta) status = f"✅ **{song_meta['title']}** lista · {song_meta['duration']} · {song_meta['tempo']} BPM" if warnings: status += f"\n\n*(validación parcial: {warnings[0][:120]})*" yield status, md, render_library(), cached_mp3, gr.update(value=song_id) except Exception as e: yield f"❌ Error sintetizando: {e}", md, render_library(), gr.update(), gr.update() finally: if os.path.exists(mid): os.unlink(mid) if os.path.exists(mp3): try: os.unlink(mp3) except Exception: pass def load_song_from_library(song_id): if not song_id: return None for s in LIBRARY: if s["id"] == song_id: return s["path"] return None with gr.Blocks(css=CUSTOM_CSS, title=STUDIO_NAME) as demo: gr.HTML(HEADER_HTML + SELECT_JS) song_selector = gr.Textbox(elem_id="song-selector", visible=False) with gr.Row(elem_id="aether-main"): with gr.Column(elem_id="aether-create"): gr.HTML('
Describe tu pieza
') prompt_in = gr.Textbox(placeholder="Ej: canción medieval épica con laúd y recorder...", show_label=False, lines=5) compose_btn = gr.Button("✦ Crear composición", elem_id="compose-btn") gr.HTML('
Estado
') status_out = gr.Markdown("*Listo.*") with gr.Accordion("Partitura / debug", open=False): raw_out = gr.Textbox(show_label=False, lines=12, interactive=False) with gr.Column(elem_id="aether-library"): gr.HTML('
Biblioteca
') library_out = gr.HTML(render_library()) with gr.Row(elem_id="aether-player"): audio_out = gr.Audio(show_label=False, type="filepath") compose_btn.click(fn=process_request, inputs=[prompt_in], outputs=[status_out, raw_out, library_out, audio_out, song_selector]) prompt_in.submit(fn=process_request, inputs=[prompt_in], outputs=[status_out, raw_out, library_out, audio_out, song_selector]) song_selector.change(fn=load_song_from_library, inputs=[song_selector], outputs=[audio_out]) if __name__ == "__main__": demo.queue(max_size=4).launch()