compact_sets/webapp/generate.html

782 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Path Generator</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: #000000;
color: #cccccc;
}
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 20px;
font-weight: 300;
letter-spacing: 2px;
color: #ffffff;
}
.form-section {
background: #0a0a0a;
padding: 15px;
margin-bottom: 15px;
border-radius: 4px;
border: 1px solid #1a1a1a;
}
.form-section h3 {
margin-top: 0;
color: #666666;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 500;
margin-bottom: 15px;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.form-group {
display: flex;
flex-direction: column;
min-width: 150px;
}
.form-group label {
font-size: 11px;
color: #666666;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.form-group input[type="number"],
.form-group input[type="text"] {
padding: 8px;
background: #0a0a0a;
color: #888888;
border: 1px solid #222222;
border-radius: 4px;
font-size: 14px;
}
/* Hide default number input spinners */
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
.form-group input:focus {
outline: none;
border-color: #444444;
color: #ffffff;
}
.form-group input[type="checkbox"] {
width: 18px;
height: 18px;
margin-top: 2px;
}
.checkbox-group {
flex-direction: row !important;
align-items: center;
}
/* Number input with inline +/- buttons */
.number-input-group {
display: inline-flex;
align-items: center;
}
.number-input-group input {
width: 50px;
text-align: center;
margin: 0 2px;
}
.number-btn {
width: 24px;
height: 24px;
padding: 0;
font-size: 14px;
cursor: pointer;
background: #0a0a0a;
color: #666666;
border: 1px solid #222222;
border-radius: 3px;
}
.number-btn:hover {
background: #151515;
color: #999999;
border-color: #444444;
}
.checkbox-group label {
margin-bottom: 0;
text-transform: none;
font-size: 12px;
color: #888888;
}
.buttons {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 20px;
}
.buttons button {
padding: 12px 24px;
font-size: 14px;
cursor: pointer;
background: #0a0a0a;
color: #888888;
border: 1px solid #222222;
border-radius: 4px;
transition: all 0.2s ease;
}
.buttons button:hover {
background: #151515;
color: #ffffff;
border-color: #444444;
}
.buttons button.primary {
background: #1a3a1a;
border-color: #2a5a2a;
color: #66cc66;
}
.buttons button.primary:hover {
background: #2a4a2a;
}
#message {
text-align: center;
padding: 15px;
margin-bottom: 15px;
border-radius: 4px;
display: none;
}
#message.success {
display: block;
background: #1a3a1a;
border: 1px solid #2a5a2a;
color: #66cc66;
}
#message.error {
display: block;
background: #3a1a1a;
border: 1px solid #5a2a2a;
color: #cc6666;
}
.nav-link {
text-align: center;
margin-top: 10px;
}
.nav-link a {
color: #666666;
text-decoration: none;
font-size: 12px;
}
.nav-link a:hover {
color: #888888;
}
.buttons-row {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 15px;
}
.buttons-row button {
padding: 8px 16px;
font-size: 12px;
cursor: pointer;
background: #0a0a0a;
color: #666666;
border: 1px solid #222222;
border-radius: 4px;
transition: all 0.2s ease;
}
.buttons-row button:hover {
background: #151515;
color: #888888;
border-color: #444444;
}
</style>
</head>
<body>
<div class="container">
<h1>Path Generator</h1>
<div class="buttons-row">
<button type="button" id="exportSettingsBtn">Export Settings</button>
<button type="button" id="importSettingsBtn">Import Settings</button>
<button type="button" id="loadDefaultsBtn">Load Defaults</button>
<input type="file" id="importFileInput" accept=".json" style="display: none;">
</div>
<div id="message"></div>
<form id="generatorForm">
<div class="form-section">
<h3>Basic Settings</h3>
<div class="form-row">
<div class="form-group">
<label>Dims</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('dims', -1)"></button>
<input type="number" id="dims" value="7" min="1" max="10">
<button type="button" class="number-btn" onclick="adjustValue('dims', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Chord Size</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('chordSize', -1)"></button>
<input type="number" id="chordSize" value="3" min="1" max="10">
<button type="button" class="number-btn" onclick="adjustValue('chordSize', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Max Path</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('maxPath', -1)"></button>
<input type="number" id="maxPath" value="50" min="1">
<button type="button" class="number-btn" onclick="adjustValue('maxPath', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Seed (optional)</label>
<input type="number" id="seed" placeholder="random">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Output Directory</label>
<input type="text" id="outputDir" value="output">
</div>
<div class="form-group">
<label>Fundamental (Hz)</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('fundamental', -1)"></button>
<input type="number" id="fundamental" value="55" step="any">
<button type="button" class="number-btn" onclick="adjustValue('fundamental', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Transcribe Name</label>
<input type="text" id="transcribeName" value="compact_sets_transcription">
</div>
</div>
</div>
<div class="form-section">
<h3>Symdiff / Melodic</h3>
<div class="form-row">
<div class="form-group">
<label>Symdiff Min</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('symdiffMin', -1)"></button>
<input type="number" id="symdiffMin" value="2" min="0">
<button type="button" class="number-btn" onclick="adjustValue('symdiffMin', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Symdiff Max</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('symdiffMax', -1)"></button>
<input type="number" id="symdiffMax" value="2" min="0">
<button type="button" class="number-btn" onclick="adjustValue('symdiffMax', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Melodic Min</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('melodicMin', -1)"></button>
<input type="number" id="melodicMin" value="0" min="0">
<button type="button" class="number-btn" onclick="adjustValue('melodicMin', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Melodic Max</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('melodicMax', -1)"></button>
<input type="number" id="melodicMax" value="500" min="0">
<button type="button" class="number-btn" onclick="adjustValue('melodicMax', 1)">+</button>
</div>
</div>
</div>
</div>
<div class="form-section">
<h3>Target Register</h3>
<div class="form-row">
<div class="form-group">
<label>Target Register (octaves)</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('targetRegister', -1)"></button>
<input type="number" id="targetRegister" value="0" step="any">
<button type="button" class="number-btn" onclick="adjustValue('targetRegister', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Target Register Power</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterPower', -1)"></button>
<input type="number" id="targetRegisterPower" value="1.0" step="any">
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterPower', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Oscillations</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterOscillations', -1)"></button>
<input type="number" id="targetRegisterOscillations" value="0" step="any">
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterOscillations', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Amplitude</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterAmplitude', -1)"></button>
<input type="number" id="targetRegisterAmplitude" value="0.25" step="any">
<button type="button" class="number-btn" onclick="adjustValue('targetRegisterAmplitude', 1)">+</button>
</div>
</div>
</div>
</div>
<div class="form-section">
<h3>Voice Leading</h3>
<div class="form-row">
<div class="form-group checkbox-group">
<input type="checkbox" id="allowVoiceCrossing">
<label>Allow Voice Crossing</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="disableDirectTuning">
<label>Disable Direct Tuning</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="uniformSymdiff">
<label>Uniform Symdiff</label>
</div>
</div>
</div>
<div class="form-section">
<h3>Weights</h3>
<div class="form-row">
<div class="form-group">
<label>Weight Melodic</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('weightMelodic', -1)"></button>
<input type="number" id="weightMelodic" value="1" step="any">
<button type="button" class="number-btn" onclick="adjustValue('weightMelodic', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Weight Contrary Motion</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('weightContraryMotion', -1)"></button>
<input type="number" id="weightContraryMotion" value="0" step="any">
<button type="button" class="number-btn" onclick="adjustValue('weightContraryMotion', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Weight DCA Hamiltonian</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('weightDcaHamiltonian', -1)"></button>
<input type="number" id="weightDcaHamiltonian" value="1" step="any">
<button type="button" class="number-btn" onclick="adjustValue('weightDcaHamiltonian', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Weight DCA Voice Movement</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('weightDcaVoiceMovement', -1)"></button>
<input type="number" id="weightDcaVoiceMovement" value="1" step="any">
<button type="button" class="number-btn" onclick="adjustValue('weightDcaVoiceMovement', 1)">+</button>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Weight RGR Voice Movement</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('weightRgrVoiceMovement', -1)"></button>
<input type="number" id="weightRgrVoiceMovement" value="0" step="any">
<button type="button" class="number-btn" onclick="adjustValue('weightRgrVoiceMovement', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>RGR Threshold</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('rgrVoiceMovementThreshold', -1)"></button>
<input type="number" id="rgrVoiceMovementThreshold" value="5" min="1">
<button type="button" class="number-btn" onclick="adjustValue('rgrVoiceMovementThreshold', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Weight Harmonic Compactness</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('weightHarmonicCompactness', -1)"></button>
<input type="number" id="weightHarmonicCompactness" value="0" step="any">
<button type="button" class="number-btn" onclick="adjustValue('weightHarmonicCompactness', 1)">+</button>
</div>
</div>
<div class="form-group">
<label>Weight Target Register</label>
<div class="number-input-group">
<button type="button" class="number-btn" onclick="adjustValue('weightTargetRegister', -1)"></button>
<input type="number" id="weightTargetRegister" value="1" step="any">
<button type="button" class="number-btn" onclick="adjustValue('weightTargetRegister', 1)">+</button>
</div>
</div>
</div>
</div>
<div class="form-section">
<h3>Caching</h3>
<div class="form-row">
<div class="form-group">
<label>Cache Directory</label>
<input type="text" id="cacheDir" value="cache">
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="rebuildCache">
<label>Rebuild Cache</label>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="noCache">
<label>No Cache</label>
</div>
</div>
</div>
<div class="buttons">
<button type="button" id="reseedGenerateBtn">Reseed & Generate</button>
<button type="button" id="generateBtn">Generate</button>
<button type="button" id="viewBtn">View</button>
<button type="button" id="transcribeBtn" class="primary">Transcribe</button>
</div>
</form>
<div class="nav-link">
<a href="/">View Path Navigator</a>
</div>
</div>
<script>
function adjustValue(id, delta) {
const input = document.getElementById(id);
const min = parseFloat(input.min) || -Infinity;
const max = parseFloat(input.max) || Infinity;
const step = parseFloat(input.step) || 1;
let val = parseFloat(input.value) || 0;
val = Math.max(min, Math.min(max, val + (delta * step)));
input.value = step % 1 !== 0 ? parseFloat(val.toFixed(2)) : Math.round(val);
}
function getFormData() {
return {
dims: parseInt(document.getElementById('dims').value) || 7,
chordSize: parseInt(document.getElementById('chordSize').value) || 3,
maxPath: parseInt(document.getElementById('maxPath').value) || 50,
seed: document.getElementById('seed').value ? parseInt(document.getElementById('seed').value) : null,
outputDir: document.getElementById('outputDir').value || 'output',
fundamental: parseFloat(document.getElementById('fundamental').value) || 55,
symdiffMin: parseInt(document.getElementById('symdiffMin').value) || 2,
symdiffMax: parseInt(document.getElementById('symdiffMax').value) || 2,
melodicMin: parseInt(document.getElementById('melodicMin').value) || 0,
melodicMax: parseInt(document.getElementById('melodicMax').value) || 500,
targetRegister: parseFloat(document.getElementById('targetRegister').value) || 0,
targetRegisterPower: parseFloat(document.getElementById('targetRegisterPower').value) || 1.0,
targetRegisterOscillations: parseFloat(document.getElementById('targetRegisterOscillations').value) || 0,
targetRegisterAmplitude: parseFloat(document.getElementById('targetRegisterAmplitude').value) || 0.25,
allowVoiceCrossing: document.getElementById('allowVoiceCrossing').checked,
disableDirectTuning: document.getElementById('disableDirectTuning').checked,
uniformSymdiff: document.getElementById('uniformSymdiff').checked,
weightMelodic: (() => { const v = parseFloat(document.getElementById('weightMelodic').value); return isNaN(v) ? 1 : v; })(),
weightContraryMotion: (() => { const v = parseFloat(document.getElementById('weightContraryMotion').value); return isNaN(v) ? 0 : v; })(),
weightDcaHamiltonian: (() => { const v = parseFloat(document.getElementById('weightDcaHamiltonian').value); return isNaN(v) ? 1 : v; })(),
weightDcaVoiceMovement: (() => { const v = parseFloat(document.getElementById('weightDcaVoiceMovement').value); return isNaN(v) ? 1 : v; })(),
weightRgrVoiceMovement: (() => { const v = parseFloat(document.getElementById('weightRgrVoiceMovement').value); return isNaN(v) ? 0 : v; })(),
rgrVoiceMovementThreshold: parseInt(document.getElementById('rgrVoiceMovementThreshold').value) || 5,
weightHarmonicCompactness: (() => { const v = parseFloat(document.getElementById('weightHarmonicCompactness').value); return isNaN(v) ? 0 : v; })(),
weightTargetRegister: (() => { const v = parseFloat(document.getElementById('weightTargetRegister').value); return isNaN(v) ? 1 : v; })(),
cacheDir: document.getElementById('cacheDir').value || 'cache',
rebuildCache: document.getElementById('rebuildCache').checked,
noCache: document.getElementById('noCache').checked,
};
}
function showMessage(text, isError = false) {
const msg = document.getElementById('message');
msg.textContent = text;
msg.className = isError ? 'error' : 'success';
}
function hideMessage() {
const msg = document.getElementById('message');
msg.style.display = 'none';
msg.className = '';
}
async function generate() {
hideMessage();
const data = getFormData();
try {
const response = await fetch('/api/generate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.error) {
showMessage('Error: ' + result.error, true);
return;
}
// Update seed field with the seed used
document.getElementById('seed').value = result.seed;
showMessage('Generated ' + result.totalChords + ' chords (seed: ' + result.seed + ')');
} catch (err) {
showMessage('Error: ' + err.message, true);
}
}
// View - open Path Navigator in new tab
function viewChords() {
window.open('/', '_blank');
}
// Transcribe - generate if needed, then transcribe
async function transcribe() {
hideMessage();
const transcribeName = document.getElementById('transcribeName').value || 'compact_sets_transcription';
const data = getFormData();
try {
// First generate if chords don't exist
const response = await fetch('/api/generate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.error) {
showMessage('Error: ' + result.error, true);
return;
}
// Update seed field
document.getElementById('seed').value = result.seed;
// Now transcribe
const transcribeResponse = await fetch('/api/transcribe', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ name: transcribeName })
});
const transcribeResult = await transcribeResponse.json();
if (transcribeResult.error) {
showMessage('Transcription error: ' + transcribeResult.error, true);
return;
}
showMessage('Transcribed ' + transcribeResult.chordCount + ' chords to ' + transcribeResult.outputFile);
} catch (err) {
showMessage('Error: ' + err.message, true);
}
}
function reseedAndGenerate() {
const newSeed = Math.floor(Math.random() * 999999) + 1;
document.getElementById('seed').value = newSeed;
generate();
}
document.getElementById('reseedGenerateBtn').addEventListener('click', reseedAndGenerate);
document.getElementById('generateBtn').addEventListener('click', generate);
document.getElementById('viewBtn').addEventListener('click', viewChords);
document.getElementById('transcribeBtn').addEventListener('click', transcribe);
// Export settings to JSON file
function exportSettings() {
const data = getFormData();
data.exportedAt = new Date().toISOString();
const json = JSON.stringify(data, null, 2);
const blob = new Blob([json], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'generator_settings.json';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
showMessage('Settings exported to generator_settings.json');
}
// Import settings from JSON file
function importSettings() {
document.getElementById('importFileInput').click();
}
function applySettings(data) {
// Map of field IDs to data keys
const fieldMap = {
dims: 'dims',
chordSize: 'chordSize',
maxPath: 'maxPath',
seed: 'seed',
outputDir: 'outputDir',
fundamental: 'fundamental',
symdiffMin: 'symdiffMin',
symdiffMax: 'symdiffMax',
melodicMin: 'melodicMin',
melodicMax: 'melodicMax',
targetRegister: 'targetRegister',
targetRegisterPower: 'targetRegisterPower',
targetRegisterOscillations: 'targetRegisterOscillations',
targetRegisterAmplitude: 'targetRegisterAmplitude',
allowVoiceCrossing: 'allowVoiceCrossing',
disableDirectTuning: 'disableDirectTuning',
uniformSymdiff: 'uniformSymdiff',
weightMelodic: 'weightMelodic',
weightContraryMotion: 'weightContraryMotion',
weightDcaHamiltonian: 'weightDcaHamiltonian',
weightDcaVoiceMovement: 'weightDcaVoiceMovement',
weightRgrVoiceMovement: 'weightRgrVoiceMovement',
rgrVoiceMovementThreshold: 'rgrVoiceMovementThreshold',
weightHarmonicCompactness: 'weightHarmonicCompactness',
weightTargetRegister: 'weightTargetRegister',
cacheDir: 'cacheDir',
rebuildCache: 'rebuildCache',
noCache: 'noCache',
};
for (const [fieldId, dataKey] of Object.entries(fieldMap)) {
const input = document.getElementById(fieldId);
if (input && data[dataKey] !== undefined) {
if (input.type === 'checkbox') {
input.checked = data[dataKey];
} else {
input.value = data[dataKey];
}
}
}
}
document.getElementById('exportSettingsBtn').addEventListener('click', exportSettings);
document.getElementById('importSettingsBtn').addEventListener('click', importSettings);
// Load default settings from server
async function loadDefaults() {
try {
const response = await fetch('/generator_settings.json');
if (!response.ok) {
// No defaults file, that's fine
return;
}
const data = await response.json();
applySettings(data);
showMessage('Default settings loaded');
} catch (err) {
// File not found or error, that's fine - use hardcoded defaults
}
}
// Try to load defaults on page load
loadDefaults();
document.getElementById('loadDefaultsBtn').addEventListener('click', function() {
// Reset form to hardcoded defaults
applySettings({
dims: 7,
chordSize: 3,
maxPath: 50,
seed: null,
outputDir: 'output',
fundamental: 55,
symdiffMin: 2,
symdiffMax: 2,
melodicMin: 0,
melodicMax: 500,
targetRegister: 0,
targetRegisterPower: 1.0,
targetRegisterOscillations: 0,
targetRegisterAmplitude: 0.25,
allowVoiceCrossing: false,
disableDirectTuning: false,
uniformSymdiff: false,
weightMelodic: 1,
weightContraryMotion: 0,
weightDcaHamiltonian: 1,
weightDcaVoiceMovement: 1,
weightRgrVoiceMovement: 0,
rgrVoiceMovementThreshold: 5,
weightHarmonicCompactness: 0,
weightTargetRegister: 1,
cacheDir: 'cache',
rebuildCache: false,
noCache: false,
});
showMessage('Form reset to defaults');
});
document.getElementById('importFileInput').addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const data = JSON.parse(e.target.result);
applySettings(data);
showMessage('Settings imported from ' + file.name);
} catch (err) {
showMessage('Error reading file: ' + err.message, true);
}
};
reader.readAsText(file);
this.value = ''; // Reset for re-import
});
</script>
</body>
</html>