<!-- SS · part 45 -->
Bright pop': { robot: 0.85, vib: 25, vibRate: 6, slide: 50, breath: 0.1, bright: 4, formant: 0.5, cons: 0.25 }, 'Deep baritone': { robot: 0.75, vib: 35, vibRate: 5, slide: 80, breath: 0.12, bright: -3, formant: -1.5, cons: 0.3 } };
  function load(k, d) { try { return JSON.parse(localStorage.getItem(k) || '') || d; } catch (e) { return d; } }
  function store(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} }
  var G = Object.assign({}, DEF, load(GK, {})), PRE = load(PK, {}), MEL = load(MK, {}); // MEL[lineText|beats] = {notes}
  var CTL = [['robot', 'Correction (natural ↔ robot)', 0, 1, 0.05, ''], ['vib', 'Vibrato depth', 0, 150, 5, ' ¢'], ['vibRate', 'Vibrato rate', 2, 9, 0.1, ' Hz'], ['slide', 'Slide time', 0, 400, 10, ' ms'],
    ['formant', 'Formant', -6, 6, 0.5, ' st'], ['bright', 'Brightness', -12, 12, 0.5, ' dB'], ['breath', 'Breathiness', 0, 1, 0.05, ''], ['cons', 'Consonant share', 0.05, 0.6, 0.05, '']];
  host.innerHTML = '<h2 class="es-h"><span class="es-ic">♪</span> SING <small>ADMIN · KOKORO + WORLD · ON THIS SERVER</small></h2>' +
    '<div class="es-sg-row"><select id="es-sg-pre" aria-label="Voice preset"></select><button type="button" class="es-btn es-btn-sm" id="es-sg-psave">Save preset</button><button type="button" class="es-btn es-btn-sm" id="es-sg-pdel">Delete</button></div>' +
    '<div class="es-sg-ctl">' + CTL.map(function (c) { return '<label class="es-slider"><span>' + c[1] + ' <b data-gv="' + c[0] + '"></b></span><input type="range" data-g="' + c[0] + '" min="' + c[2] + '" max="' + c[3] + '" step="' + c[4] + '"></label>'; }).join('') + '</div>' +
    '<div class="es-sg-row"><label>Range <input type="number" id="es-sg-lo" min="36" max="84"> – <input type="number" id="es-sg-hi" min="40" max="96"></label><label>Engine <select id="es-sg-eng"><option value="world">WORLD (best)</option><option value="psola">PSOLA</option></select></label></div>' +
    '<div class="es-sg-row"><button type="button" class="es-btn es-btn-red" id="es-sg-write">♪ Write melody</button><button type="button" class="es-btn es-btn-red" id="es-sg-all">Sing all lines</button></div>' +
    '<div class="es-sg-row"><select id="es-sg-line" aria-label="Line"></select><button type="button" class="es-btn es-btn-sm" id="es-sg-one">Re-sing line</button><button type="button" class="es-btn es-btn-sm" id="es-sg-play">▶ Line</button></div>' +
    '<canvas id="es-sg-cv" class="es-sg-cv"></canvas>' +
    '<div class="es-sg-note" id="es-sg-note"><span class="es-hint">Tap a note. Drag up/down for pitch, drag its right edge for length.</span></div>' +
    '<div class="es-status" id="es-sg-status">1) Write melody (Gemini, or rules if it fails) 2) tweak notes 3) Sing. Sung lines replace the spoken ones on the timeline. Takes ~3 s per word on this server.</div>';
  function say(m) { $('#es-sg-status').textContent = m; }
  function syncG() {
    CTL.forEach(function (c) { var r = host.querySelector('[data-g="' + c[0] + '"]'); r.value = G[c[0]]; host.querySelector('[data-gv="' + c[0] + '"]').textContent = (+G[c[0]]).toFixed(c[4] < 1 ? 2 : 0).replace(/\.00$/, '') + c[5]; });
    $('#es-sg-lo').value = G.lo; $('#es-sg-hi').value = G.hi; $('#es-sg-eng').value = G.engine;
    $('#es-sg-pre').innerHTML = '<option value="">— voice presets —</option>' + Object.keys(BUILTIN).map(function (k) { return '<option value="b:' + esc(k) + '">' + esc(k) + '</option>'; }).join('') + Object.keys(PRE).map(function (k) { return '<option value="c:' + esc(k) + '">★ ' + esc(k) + '</option>'; }).join('');
  }
  host.addEventListener('input', function (e) { var k = e.target.getAttribute('data-g'); if (!k) return; G[k] = +e.target.value; store(GK, G); syncG(); });
  $('#es-sg-lo').addEventListener('change', function () { G.lo = Math.max(36, Math.min(84, +this.value || 48)); store(GK, G); });
  $('#es-sg-hi').addEventListener('change', function () { G.hi = Math.max(G.lo + 7, Math.min(96, +this.value || 64)); store(GK, G); syncG(); });
  $('#es-sg-eng').addEventListener('change', function () { G.engine = this.value; store(GK, G); });
  $('#es-sg-pre').addEventListener('change', function () { var v = this.value; if (!v) return; var p = v[0] === 'b' ? BUILTIN[v.slice(2)] : PRE[v.slice(2)]; if (p) { Object.assign(G, p); store(GK, G); syncG(); say('Preset loaded: ' + v.slice(2) + '. Re-sing to hear it.'); } });
  $('#es-sg-psave').addEventListener('click', function () { var n = prompt('Name this voice preset'); if (!n || !n.trim()) return; var p = {}; CTL.forEach(function (c) { p[c[0]] = G[c[0]]; }); p.voice = ES.S.voice.name; p.speed = ES.S.voice.speed; PRE[n.trim().slice(0, 40)] = p; store(PK, PRE); syncG(); say('Saved preset ' + n.trim() + '.'); });
  $('#es-sg-pdel').addEventListener('click', function () { var v = $('#es-sg-pre').value; if (v.indexOf('c:') !== 0) return say('Pick one of your ★ presets to delete.'); delete PRE[v.slice(2)]; store(PK, PRE); syncG(); });
  // ---- lines from the arrangement grid
  function