2026-03-30 22:38:52 +02:00
|
|
|
#!/usr/bin/env python
|
|
|
|
|
"""
|
|
|
|
|
Flask server for Path Navigator with OSC support.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from flask import Flask, jsonify, request, send_from_directory
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
# Add parent directory to path for imports
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
|
|
|
|
|
|
from src.pitch import Pitch
|
2026-04-04 22:11:27 +02:00
|
|
|
from src.osc_sender import OSCSender
|
2026-03-30 22:38:52 +02:00
|
|
|
from fractions import Fraction
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
2026-04-01 17:14:15 +02:00
|
|
|
# Path to output directory
|
2026-03-30 22:38:52 +02:00
|
|
|
DATA_DIR = Path(__file__).parent.parent / "output"
|
2026-04-01 17:14:15 +02:00
|
|
|
DEFAULT_FILE = "output_chords.json" # default file
|
2026-03-30 22:38:52 +02:00
|
|
|
|
|
|
|
|
# State
|
|
|
|
|
current_index = 0
|
|
|
|
|
chords = []
|
|
|
|
|
|
2026-04-04 22:11:27 +02:00
|
|
|
# Cache persistent OSCSenders per IP for the siren
|
|
|
|
|
_sender_cache = {} # ip -> OSCSender
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_siren_sender(ip):
|
|
|
|
|
"""Get or create a persistent OSCSender for the given siren IP."""
|
|
|
|
|
if ip not in _sender_cache:
|
|
|
|
|
_sender_cache[ip] = OSCSender(ip=ip, port=54001, fundamental=fundamental)
|
|
|
|
|
return _sender_cache[ip]
|
|
|
|
|
|
2026-04-04 21:46:15 +02:00
|
|
|
|
2026-04-01 11:23:24 +02:00
|
|
|
# OSC settings
|
|
|
|
|
fundamental = 110.0
|
|
|
|
|
osc_sender = OSCSender(ip="127.0.0.1", port=57120, fundamental=fundamental)
|
|
|
|
|
|
2026-03-30 22:38:52 +02:00
|
|
|
|
2026-04-01 17:14:15 +02:00
|
|
|
def load_chords(filepath=None):
|
2026-03-30 22:38:52 +02:00
|
|
|
global chords, current_index
|
2026-04-01 17:14:15 +02:00
|
|
|
if filepath is None:
|
|
|
|
|
filepath = DATA_DIR / DEFAULT_FILE
|
|
|
|
|
if filepath.exists():
|
|
|
|
|
with open(filepath) as f:
|
2026-03-30 22:38:52 +02:00
|
|
|
data = json.load(f)
|
|
|
|
|
chords = data.get("chords", [])
|
|
|
|
|
current_index = 0
|
|
|
|
|
else:
|
|
|
|
|
chords = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
load_chords()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_fraction(frac_str):
|
|
|
|
|
"""Parse a fraction string to float."""
|
|
|
|
|
if isinstance(frac_str, (int, float)):
|
|
|
|
|
return float(frac_str)
|
|
|
|
|
if "/" in frac_str:
|
|
|
|
|
num, den = frac_str.split("/")
|
|
|
|
|
return int(num) / int(den)
|
|
|
|
|
return float(frac_str)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def calculate_cents(fraction_str):
|
|
|
|
|
"""Calculate cents from fraction string."""
|
2026-04-01 16:47:45 +02:00
|
|
|
import math
|
|
|
|
|
|
2026-03-30 22:38:52 +02:00
|
|
|
fr = parse_fraction(fraction_str)
|
|
|
|
|
if fr <= 0:
|
|
|
|
|
return 0
|
|
|
|
|
return 1200 * math.log2(fr)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/")
|
|
|
|
|
def index():
|
|
|
|
|
return send_from_directory(".", "path_navigator.html")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/<path:filename>")
|
|
|
|
|
def serve_static(filename):
|
|
|
|
|
return send_from_directory(".", filename)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/chords")
|
|
|
|
|
def get_chords():
|
|
|
|
|
return jsonify({"chords": chords, "total": len(chords)})
|
|
|
|
|
|
|
|
|
|
|
2026-04-01 11:23:24 +02:00
|
|
|
@app.route("/api/set-fundamental", methods=["POST"])
|
|
|
|
|
def set_fundamental():
|
|
|
|
|
"""Set the fundamental frequency for OSC playback."""
|
|
|
|
|
global fundamental, osc_sender
|
|
|
|
|
data = request.json
|
|
|
|
|
fundamental = data.get("fundamental", 110.0)
|
|
|
|
|
osc_sender = OSCSender(ip="127.0.0.1", port=57120, fundamental=fundamental)
|
|
|
|
|
return jsonify({"fundamental": fundamental})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/play-freq", methods=["POST"])
|
|
|
|
|
def play_freq():
|
|
|
|
|
"""Play a single frequency for a node."""
|
|
|
|
|
data = request.json
|
|
|
|
|
chord_index = data.get("chordIndex")
|
|
|
|
|
node_index = data.get("nodeIndex")
|
2026-04-01 18:39:13 +02:00
|
|
|
octave = data.get("octave", 0)
|
2026-04-01 11:23:24 +02:00
|
|
|
|
|
|
|
|
if chord_index < 0 or chord_index >= len(chords):
|
|
|
|
|
return jsonify({"error": "Invalid chord index"}), 400
|
|
|
|
|
|
|
|
|
|
chord = chords[chord_index]
|
|
|
|
|
if node_index < 0 or node_index >= len(chord):
|
|
|
|
|
return jsonify({"error": "Invalid node index"}), 400
|
|
|
|
|
|
|
|
|
|
pitch = chord[node_index]
|
|
|
|
|
fraction = Fraction(pitch.get("fraction", "1"))
|
2026-04-01 18:39:13 +02:00
|
|
|
frequency = fundamental * float(fraction) * (2**octave)
|
2026-04-01 11:23:24 +02:00
|
|
|
voice = node_index + 1 # 1-indexed
|
|
|
|
|
|
|
|
|
|
osc_sender.send_single(frequency, voice)
|
|
|
|
|
|
|
|
|
|
return jsonify(
|
|
|
|
|
{
|
|
|
|
|
"frequency": frequency,
|
|
|
|
|
"voice": voice,
|
|
|
|
|
"fundamental": fundamental,
|
|
|
|
|
"fraction": pitch.get("fraction", "1"),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-01 13:01:50 +02:00
|
|
|
@app.route("/api/play-siren", methods=["POST"])
|
|
|
|
|
def play_siren():
|
|
|
|
|
"""Play a single frequency for a node on the siren."""
|
|
|
|
|
data = request.json
|
2026-04-04 19:56:39 +02:00
|
|
|
|
2026-04-01 13:01:50 +02:00
|
|
|
chord_index = data.get("chordIndex")
|
|
|
|
|
node_index = data.get("nodeIndex")
|
2026-04-04 21:46:15 +02:00
|
|
|
siren_ip = data.get("ip", "192.168.4.200") # Default to actual siren IP
|
2026-04-20 22:46:41 +02:00
|
|
|
siren_number = data.get("sirenNumber") # optional 1-4
|
2026-04-21 16:55:40 +02:00
|
|
|
frequency_input = data.get("frequency") # direct frequency for ghost nodes
|
2026-04-04 19:57:54 +02:00
|
|
|
|
2026-04-21 16:55:40 +02:00
|
|
|
# If frequency provided directly (ghost node), use it
|
|
|
|
|
if frequency_input is not None:
|
|
|
|
|
frequency = float(frequency_input)
|
|
|
|
|
voice = siren_number if siren_number else 1
|
|
|
|
|
else:
|
|
|
|
|
# Original node: calculate from chordIndex/nodeIndex
|
|
|
|
|
if chord_index is None or node_index is None:
|
|
|
|
|
return jsonify({"error": "Missing chordIndex/nodeIndex"}), 400
|
2026-04-04 19:57:54 +02:00
|
|
|
|
2026-04-21 16:55:40 +02:00
|
|
|
if chord_index < 0 or chord_index >= len(chords):
|
|
|
|
|
return jsonify({"error": "Invalid chord index"}), 400
|
2026-04-04 19:57:54 +02:00
|
|
|
|
2026-04-21 16:55:40 +02:00
|
|
|
chord = chords[chord_index]
|
|
|
|
|
if node_index < 0 or node_index >= len(chord):
|
|
|
|
|
return jsonify({"error": "Invalid node index"}), 400
|
2026-04-04 19:57:54 +02:00
|
|
|
|
2026-04-21 16:55:40 +02:00
|
|
|
pitch = chord[node_index]
|
|
|
|
|
fraction = Fraction(pitch.get("fraction", "1"))
|
|
|
|
|
frequency = fundamental * float(fraction)
|
|
|
|
|
voice = siren_number if siren_number else node_index + 1 # 1-indexed
|
2026-04-01 13:01:50 +02:00
|
|
|
|
2026-04-04 22:11:27 +02:00
|
|
|
# Send to siren using cached sender
|
|
|
|
|
siren_sender = get_siren_sender(siren_ip)
|
2026-04-01 13:01:50 +02:00
|
|
|
siren_sender.set_chords(chords) # Set chords to ensure proper voice count
|
|
|
|
|
siren_sender.send_single(frequency, voice)
|
|
|
|
|
|
|
|
|
|
return jsonify(
|
|
|
|
|
{
|
|
|
|
|
"frequency": frequency,
|
|
|
|
|
"voice": voice,
|
2026-04-20 17:56:11 +02:00
|
|
|
"siren": siren_number,
|
2026-04-01 13:01:50 +02:00
|
|
|
"destination": "siren",
|
2026-04-04 21:46:15 +02:00
|
|
|
"ip": siren_ip,
|
2026-04-01 13:01:50 +02:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/kill-siren", methods=["POST"])
|
|
|
|
|
def kill_siren():
|
|
|
|
|
"""Send kill message to siren (soft or hard)."""
|
|
|
|
|
data = request.json
|
|
|
|
|
soft = data.get("soft", True) # default to soft kill
|
2026-04-04 21:46:15 +02:00
|
|
|
siren_ip = data.get("ip", "192.168.4.200") # Default to actual siren IP
|
2026-04-01 13:01:50 +02:00
|
|
|
|
|
|
|
|
kill_freq = 20.0 if soft else 0.0
|
|
|
|
|
kill_type = "soft" if soft else "hard"
|
|
|
|
|
|
2026-04-04 22:11:27 +02:00
|
|
|
# Cancel any running ramp (for this IP)
|
|
|
|
|
siren_sender = get_siren_sender(siren_ip)
|
|
|
|
|
for event in siren_sender._ramp_events.values():
|
|
|
|
|
event.set()
|
2026-04-04 21:46:15 +02:00
|
|
|
|
2026-04-04 22:11:27 +02:00
|
|
|
# Send kill to all voices using cached sender
|
2026-04-01 13:01:50 +02:00
|
|
|
siren_sender.set_chords(chords) # Set chords to get correct num_voices
|
|
|
|
|
siren_sender.send_kill(soft=soft)
|
|
|
|
|
|
|
|
|
|
return jsonify(
|
|
|
|
|
{
|
|
|
|
|
"kill_freq": kill_freq,
|
|
|
|
|
"kill_type": kill_type,
|
|
|
|
|
"voices": list(range(1, siren_sender.num_voices + 1)),
|
2026-04-04 21:46:15 +02:00
|
|
|
"ip": siren_ip,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/ramp-to-chord", methods=["POST"])
|
|
|
|
|
def ramp_to_chord():
|
|
|
|
|
"""Ramp to a chord or single voice on the siren."""
|
|
|
|
|
data = request.json
|
|
|
|
|
|
|
|
|
|
chord_index = data.get("chordIndex")
|
|
|
|
|
node_index = data.get("nodeIndex") # Optional - if present, ramp single voice
|
|
|
|
|
duration_ms = data.get("duration", 3000)
|
2026-04-07 08:48:06 +02:00
|
|
|
exponent = data.get("exponent", 1.0)
|
2026-04-04 21:46:15 +02:00
|
|
|
siren_ip = data.get("ip", "192.168.4.200") # Default to actual siren IP
|
2026-04-20 22:46:41 +02:00
|
|
|
siren_number = data.get("sirenNumber") # optional 1-4
|
2026-04-21 16:55:40 +02:00
|
|
|
frequency_input = data.get("frequency") # direct frequency for ghost nodes
|
2026-04-04 21:46:15 +02:00
|
|
|
|
|
|
|
|
if chord_index is None:
|
|
|
|
|
return jsonify({"error": "Missing chordIndex"}), 400
|
|
|
|
|
|
|
|
|
|
if chord_index < 0 or chord_index >= len(chords):
|
|
|
|
|
return jsonify({"error": "Invalid chord index"}), 400
|
|
|
|
|
|
|
|
|
|
chord = chords[chord_index]
|
|
|
|
|
|
2026-04-04 22:11:27 +02:00
|
|
|
# Use cached sender
|
|
|
|
|
siren_sender = get_siren_sender(siren_ip)
|
2026-04-04 21:46:15 +02:00
|
|
|
siren_sender.set_chords(chords)
|
|
|
|
|
|
|
|
|
|
if node_index is not None:
|
|
|
|
|
# Ramp single voice
|
|
|
|
|
if node_index < 0 or node_index >= len(chord):
|
|
|
|
|
return jsonify({"error": "Invalid node index"}), 400
|
2026-04-21 16:55:40 +02:00
|
|
|
|
|
|
|
|
# If frequency provided directly (ghost node), use it
|
|
|
|
|
if frequency_input is not None:
|
|
|
|
|
frequency = float(frequency_input)
|
|
|
|
|
voice = siren_number if siren_number else 1
|
|
|
|
|
else:
|
|
|
|
|
pitch = chord[node_index]
|
|
|
|
|
fraction = Fraction(pitch.get("fraction", "1"))
|
|
|
|
|
frequency = fundamental * float(fraction)
|
|
|
|
|
voice = siren_number if siren_number else (node_index + 1) # 1-indexed
|
2026-04-04 21:46:15 +02:00
|
|
|
|
2026-04-04 22:11:27 +02:00
|
|
|
# Ramp single voice - let sender get start frequency from current position
|
2026-04-07 08:48:06 +02:00
|
|
|
siren_sender.ramp_to_pitch(
|
|
|
|
|
frequency, voice, duration_ms, start_freq=None, exponent=exponent
|
|
|
|
|
)
|
2026-04-04 21:46:15 +02:00
|
|
|
else:
|
2026-04-04 22:11:27 +02:00
|
|
|
# Ramp entire chord - let sender get start frequencies from current positions
|
2026-04-04 21:46:15 +02:00
|
|
|
frequencies = [
|
2026-04-21 16:55:40 +02:00
|
|
|
fundamental * float(Fraction(p.get("fraction", "1"))) for p in chord
|
2026-04-04 21:46:15 +02:00
|
|
|
]
|
|
|
|
|
|
2026-04-07 08:48:06 +02:00
|
|
|
siren_sender.ramp_to_chord(
|
|
|
|
|
frequencies, duration_ms, start_freq=None, exponent=exponent
|
|
|
|
|
)
|
2026-04-04 21:46:15 +02:00
|
|
|
|
|
|
|
|
return jsonify(
|
|
|
|
|
{
|
|
|
|
|
"status": "ramping",
|
|
|
|
|
"chordIndex": chord_index,
|
|
|
|
|
"voiceIndex": node_index,
|
|
|
|
|
"duration": duration_ms,
|
2026-04-01 13:01:50 +02:00
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-04-01 16:33:13 +02:00
|
|
|
@app.route("/api/load-file", methods=["POST"])
|
|
|
|
|
def load_file():
|
|
|
|
|
global current_index, chords
|
|
|
|
|
data = request.json
|
|
|
|
|
filepath = data.get("filepath")
|
|
|
|
|
|
|
|
|
|
if not filepath:
|
|
|
|
|
return jsonify({"error": "No filepath provided"}), 400
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
with open(filepath) as f:
|
|
|
|
|
chords_data = json.load(f)
|
|
|
|
|
chords = chords_data.get("chords", [])
|
|
|
|
|
current_index = 0
|
|
|
|
|
return jsonify({"loaded": len(chords), "filepath": filepath})
|
|
|
|
|
except FileNotFoundError:
|
|
|
|
|
return jsonify({"error": f"File not found: {filepath}"}), 404
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
return jsonify({"error": f"Invalid JSON in: {filepath}"}), 400
|
|
|
|
|
|
|
|
|
|
|
2026-04-01 16:44:20 +02:00
|
|
|
@app.route("/api/parse-fraction", methods=["POST"])
|
|
|
|
|
def parse_fraction_api():
|
|
|
|
|
"""Parse a fraction string to float."""
|
|
|
|
|
data = request.json
|
|
|
|
|
fraction = data.get("fraction", "1")
|
|
|
|
|
try:
|
|
|
|
|
value = parse_fraction(fraction)
|
|
|
|
|
return jsonify({"fraction": fraction, "value": value})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return jsonify({"error": str(e)}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/calculate-cents", methods=["POST"])
|
|
|
|
|
def calculate_cents_api():
|
|
|
|
|
"""Calculate cents from fraction string."""
|
|
|
|
|
data = request.json
|
|
|
|
|
fraction = data.get("fraction", "1")
|
|
|
|
|
try:
|
|
|
|
|
cents = calculate_cents(fraction)
|
|
|
|
|
return jsonify({"fraction": fraction, "cents": cents})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return jsonify({"error": str(e)}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/batch-calculate-cents", methods=["POST"])
|
|
|
|
|
def batch_calculate_cents_api():
|
|
|
|
|
"""Calculate cents for multiple fractions at once."""
|
|
|
|
|
data = request.json
|
|
|
|
|
fractions = data.get("fractions", [])
|
|
|
|
|
try:
|
|
|
|
|
results = []
|
|
|
|
|
for fraction in fractions:
|
|
|
|
|
cents = calculate_cents(fraction)
|
|
|
|
|
results.append({"fraction": fraction, "cents": cents})
|
|
|
|
|
return jsonify({"results": results})
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return jsonify({"error": str(e)}), 400
|
|
|
|
|
|
|
|
|
|
|
2026-04-08 13:24:58 +02:00
|
|
|
@app.route("/generate")
|
|
|
|
|
def generate_page():
|
|
|
|
|
"""Render the generator page."""
|
|
|
|
|
return send_from_directory(app.root_path, "generate.html")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/generator_settings.json")
|
|
|
|
|
def default_settings():
|
|
|
|
|
"""Serve default generator settings from output directory."""
|
|
|
|
|
settings_path = DATA_DIR / "generator_settings.json"
|
|
|
|
|
if settings_path.exists():
|
|
|
|
|
return send_from_directory(DATA_DIR, "generator_settings.json")
|
|
|
|
|
else:
|
|
|
|
|
return jsonify({"error": "No default settings file"}), 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/transcribe", methods=["POST"])
|
|
|
|
|
def run_transcribe():
|
|
|
|
|
"""Run the transcriber to create LilyPond output."""
|
|
|
|
|
data = request.json
|
|
|
|
|
transcribe_name = data.get("name", "compact_sets_transcription")
|
|
|
|
|
fundamental = data.get("fundamental", 55)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import subprocess
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
input_file = DATA_DIR / "output_chords.json"
|
|
|
|
|
|
|
|
|
|
if not input_file.exists():
|
|
|
|
|
return jsonify({"error": "No chords to transcribe. Generate first."}), 400
|
|
|
|
|
|
|
|
|
|
# Load chords to get count
|
|
|
|
|
with open(input_file) as f:
|
|
|
|
|
chords_data = json.load(f)
|
|
|
|
|
chord_count = len(chords_data.get("chords", chords_data))
|
|
|
|
|
|
|
|
|
|
# Create lilypond directory if needed
|
|
|
|
|
lilypond_dir = DATA_DIR.parent / "lilypond"
|
|
|
|
|
os.makedirs(lilypond_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# Run the transcriber
|
|
|
|
|
args = [
|
|
|
|
|
sys.executable,
|
|
|
|
|
"-m",
|
|
|
|
|
"src.io",
|
|
|
|
|
"--transcribe",
|
|
|
|
|
str(input_file),
|
|
|
|
|
transcribe_name,
|
|
|
|
|
"--fundamental",
|
|
|
|
|
str(fundamental),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
args,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
return jsonify({"error": result.stderr}), 400
|
|
|
|
|
|
|
|
|
|
output_file = lilypond_dir / f"{transcribe_name}.pdf"
|
|
|
|
|
|
|
|
|
|
return jsonify(
|
|
|
|
|
{
|
|
|
|
|
"status": "success",
|
|
|
|
|
"chordCount": chord_count,
|
|
|
|
|
"outputFile": str(output_file),
|
|
|
|
|
"output": result.stdout,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
return jsonify({"error": str(e)}), 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/api/generate", methods=["POST"])
|
|
|
|
|
def run_generator():
|
|
|
|
|
"""Run the path generator with provided options."""
|
|
|
|
|
data = request.json
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
import subprocess
|
|
|
|
|
import os
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
# Build args list
|
|
|
|
|
args = [
|
|
|
|
|
sys.executable,
|
|
|
|
|
"-m",
|
|
|
|
|
"src.io",
|
|
|
|
|
"--dims",
|
|
|
|
|
str(data.get("dims", 7)),
|
|
|
|
|
"--chord-size",
|
|
|
|
|
str(data.get("chordSize", 3)),
|
|
|
|
|
"--max-path",
|
|
|
|
|
str(data.get("maxPath", 50)),
|
|
|
|
|
"--symdiff-min",
|
|
|
|
|
str(data.get("symdiffMin", 2)),
|
|
|
|
|
"--symdiff-max",
|
|
|
|
|
str(data.get("symdiffMax", 2)),
|
|
|
|
|
"--melodic-min",
|
|
|
|
|
str(data.get("melodicMin", 0)),
|
|
|
|
|
"--melodic-max",
|
|
|
|
|
str(data.get("melodicMax", 500)),
|
|
|
|
|
"--target-register",
|
|
|
|
|
str(data.get("targetRegister", 0)),
|
|
|
|
|
"--target-register-power",
|
|
|
|
|
str(data.get("targetRegisterPower", 1.0)),
|
|
|
|
|
"--target-register-oscillations",
|
|
|
|
|
str(data.get("targetRegisterOscillations", 0)),
|
|
|
|
|
"--target-register-amplitude",
|
|
|
|
|
str(data.get("targetRegisterAmplitude", 0.25)),
|
|
|
|
|
"--weight-melodic",
|
|
|
|
|
str(data.get("weightMelodic", 1)),
|
|
|
|
|
"--weight-contrary-motion",
|
|
|
|
|
str(data.get("weightContraryMotion", 0)),
|
|
|
|
|
"--weight-dca-hamiltonian",
|
|
|
|
|
str(data.get("weightDcaHamiltonian", 1)),
|
|
|
|
|
"--weight-dca-voice-movement",
|
|
|
|
|
str(data.get("weightDcaVoiceMovement", 1)),
|
|
|
|
|
"--weight-rgr-voice-movement",
|
|
|
|
|
str(data.get("weightRgrVoiceMovement", 0)),
|
|
|
|
|
"--rgr-voice-movement-threshold",
|
|
|
|
|
str(data.get("rgrVoiceMovementThreshold", 5)),
|
|
|
|
|
"--weight-harmonic-compactness",
|
|
|
|
|
str(data.get("weightHarmonicCompactness", 0)),
|
|
|
|
|
"--weight-target-register",
|
|
|
|
|
str(data.get("weightTargetRegister", 1)),
|
|
|
|
|
"--output-dir",
|
|
|
|
|
data.get("outputDir", "output"),
|
|
|
|
|
"--fundamental",
|
|
|
|
|
str(data.get("fundamental", 55)),
|
|
|
|
|
"--cache-dir",
|
|
|
|
|
data.get("cacheDir", "cache"),
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
seed = data.get("seed")
|
|
|
|
|
# If no seed provided, generate a random one
|
|
|
|
|
if seed is None:
|
|
|
|
|
import random
|
|
|
|
|
|
|
|
|
|
seed = random.randint(1, 999999)
|
|
|
|
|
args.extend(["--seed", str(seed)])
|
|
|
|
|
if data.get("allowVoiceCrossing", False):
|
|
|
|
|
args.append("--allow-voice-crossing")
|
|
|
|
|
if data.get("disableDirectTuning", False):
|
|
|
|
|
args.append("--disable-direct-tuning")
|
|
|
|
|
if data.get("uniformSymdiff", False):
|
|
|
|
|
args.append("--uniform-symdiff")
|
|
|
|
|
if data.get("rebuildCache", False):
|
|
|
|
|
args.append("--rebuild-cache")
|
|
|
|
|
if data.get("noCache", False):
|
|
|
|
|
args.append("--no-cache")
|
|
|
|
|
|
|
|
|
|
# Create output directory
|
|
|
|
|
os.makedirs(data.get("outputDir", "output"), exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# Run the generator
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
args,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
cwd=os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
return jsonify({"error": result.stderr}), 400
|
|
|
|
|
|
|
|
|
|
# Try to count generated chords from output
|
|
|
|
|
total_chords = 0
|
|
|
|
|
for line in result.stdout.split("\n"):
|
|
|
|
|
if "Path length:" in line:
|
|
|
|
|
try:
|
|
|
|
|
total_chords = int(line.split(":")[-1].strip())
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
return jsonify(
|
|
|
|
|
{
|
|
|
|
|
"totalChords": total_chords,
|
|
|
|
|
"status": "success",
|
|
|
|
|
"seed": seed,
|
|
|
|
|
"output": result.stdout,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
import traceback
|
|
|
|
|
|
|
|
|
|
traceback.print_exc()
|
|
|
|
|
return jsonify({"error": str(e)}), 400
|
|
|
|
|
|
|
|
|
|
|
2026-03-30 22:38:52 +02:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
print("Starting Path Navigator server...")
|
2026-04-01 17:14:15 +02:00
|
|
|
filepath = DATA_DIR / DEFAULT_FILE
|
|
|
|
|
print(f"Loading chords from: {filepath}")
|
2026-03-30 22:38:52 +02:00
|
|
|
load_chords()
|
|
|
|
|
print(f"Loaded {len(chords)} chords")
|
2026-04-20 17:56:11 +02:00
|
|
|
app.run(host="0.0.0.0", port=8080, debug=False)
|