Commit ·
9682ff0
1
Parent(s): d881baf
Simplify the clone dialog: name first, no Use as pills
Browse filesReorder to name -> record/upload (+ read-along script) -> preview ->
checkboxes -> one contextual save button. The Use as speaker pills are
gone: the save button itself says where the voice lands — 'Save & use
as Speaker N' when the dialog was opened from that slot, plain 'Save
voice' (library only, assign later via Change) from the sidebar button.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- static/app.js +11 -23
- static/index.html +7 -9
static/app.js
CHANGED
|
@@ -80,7 +80,7 @@ const el = {};
|
|
| 80 |
"logToggleBtn", "logBox",
|
| 81 |
"voiceLibraryDialog", "closeLibraryBtn", "librarySearch", "libraryFilters", "libraryGrid", "libraryTitle",
|
| 82 |
"cloneVoiceBtn", "cloneDialog", "closeCloneBtn", "recordBtn", "cloneFileInput", "recordTimer",
|
| 83 |
-
"clonePreview", "cloneAudio", "cloneMeta", "
|
| 84 |
"cloneNameInput", "cloneReplaceRow", "cloneReplacePills", "optimizeCheckbox",
|
| 85 |
"readScriptToggle", "readScriptCard", "readScriptText", "readScriptMeta", "readScriptShuffle",
|
| 86 |
"cancelCloneBtn", "useCloneBtn",
|
|
@@ -652,7 +652,7 @@ function renderLibraryGrid() {
|
|
| 652 |
cloneBtn.addEventListener("click", () => {
|
| 653 |
const target = state.libraryTargetSlot;
|
| 654 |
el.voiceLibraryDialog.close();
|
| 655 |
-
openCloneDialog(target
|
| 656 |
});
|
| 657 |
cloneBody.append(cloneHead, cloneMeta, cloneBtn);
|
| 658 |
clone.append(cloneAvatar, cloneBody);
|
|
@@ -807,22 +807,6 @@ function renderCloneReplacePills() {
|
|
| 807 |
});
|
| 808 |
}
|
| 809 |
|
| 810 |
-
function renderCloneSlots() {
|
| 811 |
-
el.cloneSlotRow.innerHTML = "";
|
| 812 |
-
for (let i = 0; i < state.numSpeakers; i += 1) {
|
| 813 |
-
const pill = document.createElement("button");
|
| 814 |
-
pill.type = "button";
|
| 815 |
-
pill.className = "clone-slot-pill";
|
| 816 |
-
pill.textContent = `Speaker ${i + 1} · ${slotVoiceLabel(i)}`;
|
| 817 |
-
pill.classList.toggle("active", i === state.cloneTargetSlot);
|
| 818 |
-
pill.addEventListener("click", () => {
|
| 819 |
-
state.cloneTargetSlot = i;
|
| 820 |
-
renderCloneSlots();
|
| 821 |
-
});
|
| 822 |
-
el.cloneSlotRow.append(pill);
|
| 823 |
-
}
|
| 824 |
-
}
|
| 825 |
-
|
| 826 |
function stopRecording() {
|
| 827 |
if (state.mediaRecorder && state.mediaRecorder.state !== "inactive") state.mediaRecorder.stop();
|
| 828 |
}
|
|
@@ -945,8 +929,8 @@ el.optimizeCheckbox.addEventListener("change", async () => {
|
|
| 945 |
} catch { /* keep the current clip */ }
|
| 946 |
});
|
| 947 |
|
| 948 |
-
function openCloneDialog(targetSlot) {
|
| 949 |
-
state.cloneTargetSlot = Math.min(targetSlot, state.numSpeakers - 1);
|
| 950 |
state.cloneBlob = null;
|
| 951 |
state.cloneName = "";
|
| 952 |
state.cloneDuration = 0;
|
|
@@ -957,7 +941,9 @@ function openCloneDialog(targetSlot) {
|
|
| 957 |
el.cloneMeta.textContent = "";
|
| 958 |
el.cloneNameInput.value = "";
|
| 959 |
el.cloneConsentCheckbox.checked = el.voiceConsentCheckbox.checked;
|
| 960 |
-
|
|
|
|
|
|
|
| 961 |
renderCloneReplacePills();
|
| 962 |
updateCloneConfirm();
|
| 963 |
el.cloneDialog.showModal();
|
|
@@ -969,7 +955,7 @@ function closeCloneDialog() {
|
|
| 969 |
el.cloneDialog.close();
|
| 970 |
}
|
| 971 |
|
| 972 |
-
el.cloneVoiceBtn.addEventListener("click", () => openCloneDialog(
|
| 973 |
el.closeCloneBtn.addEventListener("click", closeCloneDialog);
|
| 974 |
el.cancelCloneBtn.addEventListener("click", closeCloneDialog);
|
| 975 |
el.cloneDialog.addEventListener("click", (e) => { if (e.target === el.cloneDialog) closeCloneDialog(); });
|
|
@@ -999,7 +985,9 @@ el.useCloneBtn.addEventListener("click", () => {
|
|
| 999 |
state.customVoices.push(voice);
|
| 1000 |
}
|
| 1001 |
persistCustomVoice(voice);
|
| 1002 |
-
state.
|
|
|
|
|
|
|
| 1003 |
el.voiceConsentCheckbox.checked = el.cloneConsentCheckbox.checked;
|
| 1004 |
closeCloneDialog();
|
| 1005 |
renderCast();
|
|
|
|
| 80 |
"logToggleBtn", "logBox",
|
| 81 |
"voiceLibraryDialog", "closeLibraryBtn", "librarySearch", "libraryFilters", "libraryGrid", "libraryTitle",
|
| 82 |
"cloneVoiceBtn", "cloneDialog", "closeCloneBtn", "recordBtn", "cloneFileInput", "recordTimer",
|
| 83 |
+
"clonePreview", "cloneAudio", "cloneMeta", "cloneConsentCheckbox",
|
| 84 |
"cloneNameInput", "cloneReplaceRow", "cloneReplacePills", "optimizeCheckbox",
|
| 85 |
"readScriptToggle", "readScriptCard", "readScriptText", "readScriptMeta", "readScriptShuffle",
|
| 86 |
"cancelCloneBtn", "useCloneBtn",
|
|
|
|
| 652 |
cloneBtn.addEventListener("click", () => {
|
| 653 |
const target = state.libraryTargetSlot;
|
| 654 |
el.voiceLibraryDialog.close();
|
| 655 |
+
openCloneDialog(target);
|
| 656 |
});
|
| 657 |
cloneBody.append(cloneHead, cloneMeta, cloneBtn);
|
| 658 |
clone.append(cloneAvatar, cloneBody);
|
|
|
|
| 807 |
});
|
| 808 |
}
|
| 809 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 810 |
function stopRecording() {
|
| 811 |
if (state.mediaRecorder && state.mediaRecorder.state !== "inactive") state.mediaRecorder.stop();
|
| 812 |
}
|
|
|
|
| 929 |
} catch { /* keep the current clip */ }
|
| 930 |
});
|
| 931 |
|
| 932 |
+
function openCloneDialog(targetSlot = null) {
|
| 933 |
+
state.cloneTargetSlot = targetSlot === null ? null : Math.min(targetSlot, state.numSpeakers - 1);
|
| 934 |
state.cloneBlob = null;
|
| 935 |
state.cloneName = "";
|
| 936 |
state.cloneDuration = 0;
|
|
|
|
| 941 |
el.cloneMeta.textContent = "";
|
| 942 |
el.cloneNameInput.value = "";
|
| 943 |
el.cloneConsentCheckbox.checked = el.voiceConsentCheckbox.checked;
|
| 944 |
+
el.useCloneBtn.textContent = state.cloneTargetSlot === null
|
| 945 |
+
? "Save voice"
|
| 946 |
+
: `Save & use as Speaker ${state.cloneTargetSlot + 1}`;
|
| 947 |
renderCloneReplacePills();
|
| 948 |
updateCloneConfirm();
|
| 949 |
el.cloneDialog.showModal();
|
|
|
|
| 955 |
el.cloneDialog.close();
|
| 956 |
}
|
| 957 |
|
| 958 |
+
el.cloneVoiceBtn.addEventListener("click", () => openCloneDialog(null));
|
| 959 |
el.closeCloneBtn.addEventListener("click", closeCloneDialog);
|
| 960 |
el.cancelCloneBtn.addEventListener("click", closeCloneDialog);
|
| 961 |
el.cloneDialog.addEventListener("click", (e) => { if (e.target === el.cloneDialog) closeCloneDialog(); });
|
|
|
|
| 985 |
state.customVoices.push(voice);
|
| 986 |
}
|
| 987 |
persistCustomVoice(voice);
|
| 988 |
+
if (state.cloneTargetSlot !== null) {
|
| 989 |
+
state.voiceSelections[state.cloneTargetSlot] = CUSTOM_PREFIX + voice.id;
|
| 990 |
+
}
|
| 991 |
el.voiceConsentCheckbox.checked = el.cloneConsentCheckbox.checked;
|
| 992 |
closeCloneDialog();
|
| 993 |
renderCast();
|
static/index.html
CHANGED
|
@@ -214,8 +214,10 @@
|
|
| 214 |
</div>
|
| 215 |
<p>
|
| 216 |
Record or upload <strong>15–30 seconds</strong> of one person speaking clearly, with no
|
| 217 |
-
background noise or music.
|
| 218 |
</p>
|
|
|
|
|
|
|
| 219 |
<div class="clone-capture">
|
| 220 |
<button id="recordBtn" class="btn btn-accent" type="button">● Record</button>
|
| 221 |
<label class="btn clone-upload-label">Upload file<input id="cloneFileInput" type="file" accept="audio/*" hidden /></label>
|
|
@@ -229,18 +231,14 @@
|
|
| 229 |
<button id="readScriptShuffle" type="button">↻ Try another</button>
|
| 230 |
</div>
|
| 231 |
</div>
|
| 232 |
-
<label class="clone-optimize">
|
| 233 |
-
<input type="checkbox" id="optimizeCheckbox" checked />
|
| 234 |
-
Optimize audio — suppress mic noise, trim silence, remove rumble, level the volume
|
| 235 |
-
</label>
|
| 236 |
<div id="clonePreview" class="clone-preview" hidden>
|
| 237 |
<audio id="cloneAudio" controls></audio>
|
| 238 |
<div id="cloneMeta" class="clone-meta"></div>
|
| 239 |
</div>
|
| 240 |
-
<
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
<
|
| 244 |
<div id="cloneReplaceRow" hidden>
|
| 245 |
<div class="clone-section-label">Voice library full — choose one to replace</div>
|
| 246 |
<div id="cloneReplacePills" class="clone-slots"></div>
|
|
|
|
| 214 |
</div>
|
| 215 |
<p>
|
| 216 |
Record or upload <strong>15–30 seconds</strong> of one person speaking clearly, with no
|
| 217 |
+
background noise or music.
|
| 218 |
</p>
|
| 219 |
+
<div class="clone-section-label">Voice name</div>
|
| 220 |
+
<input id="cloneNameInput" class="clone-name-input" maxlength="40" placeholder="e.g. Josh, Grandma Ruth, Movie Trailer Guy" />
|
| 221 |
<div class="clone-capture">
|
| 222 |
<button id="recordBtn" class="btn btn-accent" type="button">● Record</button>
|
| 223 |
<label class="btn clone-upload-label">Upload file<input id="cloneFileInput" type="file" accept="audio/*" hidden /></label>
|
|
|
|
| 231 |
<button id="readScriptShuffle" type="button">↻ Try another</button>
|
| 232 |
</div>
|
| 233 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
<div id="clonePreview" class="clone-preview" hidden>
|
| 235 |
<audio id="cloneAudio" controls></audio>
|
| 236 |
<div id="cloneMeta" class="clone-meta"></div>
|
| 237 |
</div>
|
| 238 |
+
<label class="clone-optimize">
|
| 239 |
+
<input type="checkbox" id="optimizeCheckbox" checked />
|
| 240 |
+
Optimize audio — suppress mic noise, trim silence, remove rumble, level the volume
|
| 241 |
+
</label>
|
| 242 |
<div id="cloneReplaceRow" hidden>
|
| 243 |
<div class="clone-section-label">Voice library full — choose one to replace</div>
|
| 244 |
<div id="cloneReplacePills" class="clone-slots"></div>
|