strudel/kisum/kisum_server.py
hacklab 7ace286d8a
Some checks are pending
Build and Deploy to warm (strudel.cc) / build (push) Waiting to run
Strudel tests / build (20) (push) Waiting to run
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>
2026-07-26 18:31:37 +02:00

120 lines
4 KiB
Python

#!/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()