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