From c217e04243fda55e55c228c3ec4417a7c1f7b74b Mon Sep 17 00:00:00 2001 From: Michael Winter Date: Wed, 8 Apr 2026 13:24:58 +0200 Subject: [PATCH] Add Path Generator UI with import/export settings, save/load defaults, and transcribe --- webapp/generate.html | 647 +++++++++++++++++++++++++++++++++++++++++++ webapp/server.py | 201 ++++++++++++++ 2 files changed, 848 insertions(+) create mode 100644 webapp/generate.html diff --git a/webapp/generate.html b/webapp/generate.html new file mode 100644 index 0000000..bc7a9a1 --- /dev/null +++ b/webapp/generate.html @@ -0,0 +1,647 @@ + + + + + + Path Generator + + + +
+

Path Generator

+ +
+ + + + +
+ +
+ +
+
+

Basic Settings

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Symdiff / Melodic

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Target Register

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Voice Leading

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Weights

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+

Caching

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ + + +
+
+ + +
+ + + + \ No newline at end of file diff --git a/webapp/server.py b/webapp/server.py index c008157..33d6443 100644 --- a/webapp/server.py +++ b/webapp/server.py @@ -320,6 +320,207 @@ def batch_calculate_cents_api(): return jsonify({"error": str(e)}), 400 +@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 + + if __name__ == "__main__": print("Starting Path Navigator server...") filepath = DATA_DIR / DEFAULT_FILE