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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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