ACloudCenter Claude Fable 5 commited on
Commit
9682ff0
·
1 Parent(s): d881baf

Simplify the clone dialog: name first, no Use as pills

Browse files

Reorder 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>

Files changed (2) hide show
  1. static/app.js +11 -23
  2. 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", "cloneSlotRow", "cloneConsentCheckbox",
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 === null ? 0 : 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
- renderCloneSlots();
 
 
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(0));
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.voiceSelections[state.cloneTargetSlot] = CUSTOM_PREFIX + voice.id;
 
 
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. That voice becomes one of your speakers.
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
- <div class="clone-section-label">Voice name</div>
241
- <input id="cloneNameInput" class="clone-name-input" maxlength="40" placeholder="e.g. Josh, Grandma Ruth, Movie Trailer Guy" />
242
- <div class="clone-section-label">Use as</div>
243
- <div id="cloneSlotRow" class="clone-slots"></div>
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>