Integra KISUM (audio→Strudel) + pestaña Visor + accesos en la cabecera
- Pestaña KISUM: sube audio/vídeo y lo convierte en código Strudel vía un mini-servidor Python local (kisum/, puerto 3010) que arranca el lanzador. - Pestaña Visor: piano-roll (pentagrama) + osciloscopio + espectro en vivo de lo que suena, sin escribir .pianoroll()/.scope(). - Botones en la cabecera: projects, KISUM, visor, ajustes. - KISUM empaquetado en kisum/ (código, sin venv) + install.sh de un comando y lanzador portable (sin rutas absolutas). - .gitignore excluye venv/cachés/logs de KISUM. Docs en README. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
970b89b08c
commit
7ace286d8a
13 changed files with 1052 additions and 13 deletions
120
kisum/kisum_server.py
Normal file
120
kisum/kisum_server.py
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Mini-servidor local de KISUM para integrarlo en Strudel.
|
||||
|
||||
Expone un endpoint HTTP que recibe un archivo de audio/video, lo analiza con
|
||||
kisum.analyze() y devuelve el codigo Strudel en JSON. Pensado para correr en
|
||||
local (127.0.0.1) junto a Strudel; lo arranca strudel-launcher.sh.
|
||||
|
||||
python3 kisum_server.py # escucha en 127.0.0.1:3010
|
||||
KISUM_PORT=7331 python3 kisum_server.py
|
||||
|
||||
Endpoints:
|
||||
GET /health -> {"ok": true}
|
||||
POST /analyze?bars=4&bass=1 (cuerpo = bytes del archivo,
|
||||
cabecera X-Filename: nombre.mp4)
|
||||
-> {"code": "<codigo strudel>", "filename": ...}
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
if HERE not in sys.path:
|
||||
sys.path.insert(0, HERE)
|
||||
|
||||
# Import pesado (librosa) al arrancar: asi la primera peticion ya es rapida.
|
||||
import kisum # noqa: E402
|
||||
|
||||
PORT = int(os.environ.get("KISUM_PORT", "3010"))
|
||||
|
||||
|
||||
def _cors(handler):
|
||||
handler.send_header("Access-Control-Allow-Origin", "*")
|
||||
handler.send_header("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
||||
handler.send_header("Access-Control-Allow-Headers", "Content-Type, X-Filename")
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def log_message(self, *a):
|
||||
pass # silencio: el log lo lleva el launcher
|
||||
|
||||
def _json(self, code, obj):
|
||||
body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
_cors(self)
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(204)
|
||||
_cors(self)
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
path = urlparse(self.path).path
|
||||
if path in ("/health", "/"):
|
||||
self._json(200, {"ok": True, "service": "kisum"})
|
||||
else:
|
||||
self._json(404, {"error": "not found"})
|
||||
|
||||
def do_POST(self):
|
||||
parsed = urlparse(self.path)
|
||||
if parsed.path != "/analyze":
|
||||
self._json(404, {"error": "not found"})
|
||||
return
|
||||
|
||||
qs = parse_qs(parsed.query)
|
||||
try:
|
||||
bars = max(1, int(qs.get("bars", ["4"])[0]))
|
||||
except ValueError:
|
||||
bars = 4
|
||||
want_bass = qs.get("bass", ["0"])[0] in ("1", "true", "yes", "on")
|
||||
|
||||
filename = self.headers.get("X-Filename", "audio")
|
||||
ext = os.path.splitext(filename)[1] or ".bin"
|
||||
|
||||
length = int(self.headers.get("Content-Length", "0") or 0)
|
||||
if length <= 0:
|
||||
self._json(400, {"error": "archivo vacio"})
|
||||
return
|
||||
data = self.rfile.read(length)
|
||||
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=ext, delete=False)
|
||||
tmp.write(data)
|
||||
tmp.close()
|
||||
try:
|
||||
code = kisum.analyze(tmp.name, bars, want_bass)
|
||||
# el comentario de cabecera lleva la ruta temporal: la cambiamos
|
||||
# por el nombre real del archivo que subio el usuario
|
||||
code = code.replace(tmp.name, filename)
|
||||
self._json(200, {"code": code, "filename": filename})
|
||||
except SystemExit as e:
|
||||
# kisum.analyze() hace sys.exit(msg) cuando no encuentra pulso, etc.
|
||||
msg = str(e.code) if e.code else "no se pudo analizar el audio"
|
||||
self._json(422, {"error": msg})
|
||||
except Exception as e: # noqa: BLE001
|
||||
self._json(500, {"error": f"{type(e).__name__}: {e}"})
|
||||
finally:
|
||||
try:
|
||||
os.remove(tmp.name)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
srv = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
|
||||
print(f"KISUM server escuchando en http://127.0.0.1:{PORT}", flush=True)
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue