- 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>
120 lines
4 KiB
Python
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()
|