Add double-click resets to the voice settings
This commit is contained in:
+10
@@ -40,6 +40,16 @@ mobile draft at 390px.
|
|||||||
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
- [x] C11 preview caching: Discord-hosted image/GIF previews (WebP 320px, hash-addressed, 5 MiB
|
||||||
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
cap, one attempt) run after every walk in both modes. Live run `...-20260916090722`:
|
||||||
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
`previewed: 2`; thumbs served over `/api/media/<scrape>/<item>/thumb` (200).
|
||||||
|
- [x] C82 Double-click resets in the voice settings. Every slider in the TTS panel resets on
|
||||||
|
double-click: the voice-character profile sliders (to the engine default from the voice list),
|
||||||
|
expression (default, and the override is truly cleared so the label reads `default`), speed
|
||||||
|
(1), pitch (0), volume (100), highpass (40), echo (off), and word gap (120 ms). Hints say so:
|
||||||
|
"within 2 snaps to default · double-click a slider to reset it" in Voice character and
|
||||||
|
"VOX / HEV · double-click resets" in Radio kit, plus a title on the profile sliders. Also
|
||||||
|
fixed: the speech status and preferences now load at startup (`refreshAudio()`), so the panel
|
||||||
|
and settings show real values before the first open — the profile defaults were empty until
|
||||||
|
then, which would have made the profile reset do nothing. Verified in a browser; the UI smoke
|
||||||
|
test passes.
|
||||||
- [x] C81 remchild discovery and heartbeat-miss notices. The server answers UDP probes on 8798
|
- [x] C81 remchild discovery and heartbeat-miss notices. The server answers UDP probes on 8798
|
||||||
and beacons `{type,name,port,id}` to every interface broadcast; `remchild` with no `--server`
|
and beacons `{type,name,port,id}` to every interface broadcast; `remchild` with no `--server`
|
||||||
probes for 5 s, auto-connects on one answer, shows a pick list on several, and falls back to
|
probes for 5 s, auto-connects on one answer, shows a pick list on several, and falls back to
|
||||||
|
|||||||
+51
-10
@@ -2044,15 +2044,19 @@ async function saveSpeech(extra = {}) {
|
|||||||
const speed = Number($("#tts-speed").value);
|
const speed = Number($("#tts-speed").value);
|
||||||
const pitch = Number($("#tts-pitch").value);
|
const pitch = Number($("#tts-pitch").value);
|
||||||
const expressionEl = $("#tts-expression");
|
const expressionEl = $("#tts-expression");
|
||||||
|
// A double-click reset clears the override: it must not be resent as a number.
|
||||||
const expression =
|
const expression =
|
||||||
expressionEl.dataset.dirty === "1" || typeof state.speech.expression === "number"
|
expressionEl.dataset.reset === "1"
|
||||||
? Number(expressionEl.value)
|
? null
|
||||||
: null;
|
: expressionEl.dataset.dirty === "1" || typeof state.speech.expression === "number"
|
||||||
|
? Number(expressionEl.value)
|
||||||
|
: null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const prefs = await VK.api.post("/api/preferences", {
|
const prefs = await VK.api.post("/api/preferences", {
|
||||||
tts: { provider, language, voice, speed, pitch, expression, ...effectOptions(), ...extra }
|
tts: { provider, language, voice, speed, pitch, expression, ...effectOptions(), ...extra }
|
||||||
});
|
});
|
||||||
|
delete expressionEl.dataset.reset;
|
||||||
state.speech = prefs.tts ?? {
|
state.speech = prefs.tts ?? {
|
||||||
provider: null,
|
provider: null,
|
||||||
language: null,
|
language: null,
|
||||||
@@ -2141,6 +2145,7 @@ function buildProfileSliders() {
|
|||||||
const slider = el("input", { type: "range", min: String(min), max: String(max), step: "1" });
|
const slider = el("input", { type: "range", min: String(min), max: String(max), step: "1" });
|
||||||
slider.id = `tts-profile-${key}`;
|
slider.id = `tts-profile-${key}`;
|
||||||
slider.dataset.key = key;
|
slider.dataset.key = key;
|
||||||
|
slider.title = "Double-click to reset to the engine default";
|
||||||
const value = el("span", { class: "vk-mono vk-dim", text: "default" });
|
const value = el("span", { class: "vk-mono vk-dim", text: "default" });
|
||||||
|
|
||||||
root.append(
|
root.append(
|
||||||
@@ -2167,6 +2172,15 @@ function buildProfileSliders() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
slider.addEventListener("change", () => void saveSpeech());
|
slider.addEventListener("change", () => void saveSpeech());
|
||||||
|
slider.addEventListener("dblclick", () => {
|
||||||
|
const engineDefault = state.profileDefaults?.[key];
|
||||||
|
if (typeof engineDefault !== "number") return;
|
||||||
|
|
||||||
|
slider.value = engineDefault;
|
||||||
|
slider.dataset.dirty = "";
|
||||||
|
value.textContent = "default";
|
||||||
|
void saveSpeech();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2980,6 +2994,11 @@ function init() {
|
|||||||
for (const id of ["tts-speed", "tts-pitch"]) {
|
for (const id of ["tts-speed", "tts-pitch"]) {
|
||||||
$(`#${id}`).addEventListener("input", syncSpeechSliderLabels);
|
$(`#${id}`).addEventListener("input", syncSpeechSliderLabels);
|
||||||
$(`#${id}`).addEventListener("change", () => void saveSpeech());
|
$(`#${id}`).addEventListener("change", () => void saveSpeech());
|
||||||
|
$(`#${id}`).addEventListener("dblclick", () => {
|
||||||
|
$(`#${id}`).value = id === "tts-speed" ? "1" : "0";
|
||||||
|
syncSpeechSliderLabels();
|
||||||
|
void saveSpeech();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
$("#tts-expression").addEventListener("input", () => {
|
$("#tts-expression").addEventListener("input", () => {
|
||||||
const slider = $("#tts-expression");
|
const slider = $("#tts-expression");
|
||||||
@@ -2996,15 +3015,34 @@ function init() {
|
|||||||
syncSpeechSliderLabels();
|
syncSpeechSliderLabels();
|
||||||
});
|
});
|
||||||
$("#tts-expression").addEventListener("change", () => void saveSpeech());
|
$("#tts-expression").addEventListener("change", () => void saveSpeech());
|
||||||
|
$("#tts-expression").addEventListener("dblclick", () => {
|
||||||
|
const slider = $("#tts-expression");
|
||||||
|
const engineDefault = state.expressionDefault;
|
||||||
|
if (typeof engineDefault !== "number") return;
|
||||||
|
|
||||||
|
slider.value = engineDefault;
|
||||||
|
slider.dataset.dirty = "";
|
||||||
|
slider.dataset.stored = "0";
|
||||||
|
slider.dataset.reset = "1";
|
||||||
|
syncSpeechSliderLabels();
|
||||||
|
void saveSpeech();
|
||||||
|
});
|
||||||
buildProfileSliders();
|
buildProfileSliders();
|
||||||
for (const id of ["tts-highpass", "tts-echo", "tts-word-gap", "tts-volume"]) {
|
const radioDefaults = { "tts-highpass": 40, "tts-echo": 0, "tts-word-gap": 120, "tts-volume": 100 };
|
||||||
$(`#${id}`).addEventListener("input", () => {
|
const syncRadioLabels = () => {
|
||||||
$("#tts-highpass-value").textContent = `${$("#tts-highpass").value}Hz`;
|
$("#tts-highpass-value").textContent = `${$("#tts-highpass").value}Hz`;
|
||||||
$("#tts-echo-value").textContent = Number($("#tts-echo").value) > 0 ? $("#tts-echo").value : "off";
|
$("#tts-echo-value").textContent = Number($("#tts-echo").value) > 0 ? $("#tts-echo").value : "off";
|
||||||
$("#tts-word-gap-value").textContent = `${$("#tts-word-gap").value}ms`;
|
$("#tts-word-gap-value").textContent = `${$("#tts-word-gap").value}ms`;
|
||||||
$("#tts-volume-value").textContent = `${Number($("#tts-volume").value)}%`;
|
$("#tts-volume-value").textContent = `${Number($("#tts-volume").value)}%`;
|
||||||
});
|
};
|
||||||
|
for (const id of Object.keys(radioDefaults)) {
|
||||||
|
$(`#${id}`).addEventListener("input", syncRadioLabels);
|
||||||
$(`#${id}`).addEventListener("change", () => void saveSpeech());
|
$(`#${id}`).addEventListener("change", () => void saveSpeech());
|
||||||
|
$(`#${id}`).addEventListener("dblclick", () => {
|
||||||
|
$(`#${id}`).value = String(radioDefaults[id]);
|
||||||
|
syncRadioLabels();
|
||||||
|
void saveSpeech();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
$("#tts-words").addEventListener("change", () => void saveSpeech());
|
$("#tts-words").addEventListener("change", () => void saveSpeech());
|
||||||
$("#tts-vox-clean").addEventListener("change", () => void saveSpeech());
|
$("#tts-vox-clean").addEventListener("change", () => void saveSpeech());
|
||||||
@@ -3246,6 +3284,9 @@ function init() {
|
|||||||
renderRightFavourites();
|
renderRightFavourites();
|
||||||
|
|
||||||
void refreshAll();
|
void refreshAll();
|
||||||
|
// The speech status and preferences feed the TTS panel and the settings
|
||||||
|
// overlay, so load them at startup instead of on the first panel open.
|
||||||
|
void refreshAudio();
|
||||||
void refreshHealth();
|
void refreshHealth();
|
||||||
syncTtsControls();
|
syncTtsControls();
|
||||||
|
|
||||||
|
|||||||
@@ -568,7 +568,7 @@
|
|||||||
<section class="vk-profile-section">
|
<section class="vk-profile-section">
|
||||||
<div class="vk-section-head">
|
<div class="vk-section-head">
|
||||||
<span class="vk-rail-label vk-rail-label-flat">Voice character</span>
|
<span class="vk-rail-label vk-rail-label-flat">Voice character</span>
|
||||||
<span class="vk-mono vk-dim">within 2 of the engine default snaps to default</span>
|
<span class="vk-mono vk-dim">within 2 snaps to default · double-click a slider to reset it</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="tts-profile-sliders" class="vk-profile-grid"></div>
|
<div id="tts-profile-sliders" class="vk-profile-grid"></div>
|
||||||
<div class="vk-profile-row">
|
<div class="vk-profile-row">
|
||||||
@@ -593,7 +593,7 @@
|
|||||||
<section class="vk-profile-section">
|
<section class="vk-profile-section">
|
||||||
<div class="vk-section-head">
|
<div class="vk-section-head">
|
||||||
<span class="vk-rail-label vk-rail-label-flat">Radio kit</span>
|
<span class="vk-rail-label vk-rail-label-flat">Radio kit</span>
|
||||||
<span class="vk-mono vk-dim">VOX / HEV</span>
|
<span class="vk-mono vk-dim">VOX / HEV · double-click resets</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="vk-profile-row">
|
<div class="vk-profile-row">
|
||||||
<span class="vk-micro-label">highpass</span>
|
<span class="vk-micro-label">highpass</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user