/**
* 🏷️ Widget Inline Tags Section
* Builds an engine-aware inline tag panel for Step Audio EditX, Higgs Audio v3,
* CosyVoice3, and OmniVoice.
*/
import { api } from "/scripts/api.js";
import { INDEX_TTS_EMOTION_VISUALS } from "./emotion_radar_canvas_widget.js";
const INDEX_TTS_EMOTIONS = ["happy", "angry", "sad", "afraid", "disgusted", "melancholic", "surprised", "calm"];
const INDEX_TTS_RADIAL_EMOTIONS = ["happy", "surprised", "angry", "disgusted", "sad", "afraid", "calm", "melancholic"];
const INDEX_TTS_EMOTION_COLORS = Object.fromEntries(
INDEX_TTS_EMOTION_VISUALS.map(({ name, color }) => [name.toLowerCase(), color])
);
const HIGGS_TAGS = {
emotion: [
"elation", "amusement", "enthusiasm", "determination", "pride", "contentment", "affection",
"relief", "contemplation", "confusion", "surprise", "awe", "longing", "arousal", "anger",
"fear", "disgust", "bitterness", "sadness", "shame", "helplessness"
],
style: ["singing", "shouting", "whispering"],
prosody: [
"speed_very_slow", "speed_slow", "speed_fast", "speed_very_fast",
"pitch_low", "pitch_high", "pause", "long_pause", "expressive_high", "expressive_low"
],
sfx: ["cough", "laughter", "crying", "screaming", "burping", "humming", "sigh", "sniff", "sneeze"],
};
const COSY_SINGLE_TAGS = [
"breath", "quick_breath", "laughter", "cough", "sigh", "gasp", "noise",
"hissing", "vocalized-noise", "lipsmack", "mn", "clucking", "accent"
];
const COSY_WRAPPER_TAGS = ["laughing", "strong"];
const OMNIVOICE_NON_VERBAL_TAGS = [
"laughter", "sigh", "confirmation-en", "question-en", "question-ah",
"question-oh", "question-ei", "question-yi", "surprise-ah", "surprise-oh",
"surprise-wa", "surprise-yo", "dissatisfaction-hnn"
];
function stylePanelContainer(element, { separated = true } = {}) {
element.style.marginBottom = "8px";
if (separated) {
element.style.paddingBottom = "8px";
element.style.borderBottom = "1px solid #444";
}
}
function createPanelLabel(text, color, title = "") {
const label = document.createElement("div");
label.textContent = text;
label.style.fontWeight = "bold";
label.style.marginBottom = "5px";
label.style.fontSize = "11px";
label.style.color = color;
if (title) {
label.title = title;
}
return label;
}
function createSelect(options, placeholder, value = "") {
const select = document.createElement("select");
select.style.width = "100%";
select.style.marginBottom = "4px";
select.style.padding = "3px";
select.style.fontSize = "10px";
select.style.background = "#2a2a2a";
select.style.color = "#eee";
select.style.border = "1px solid #444";
select.innerHTML = `${options.map(({ value: optionValue, label }) => ``).join("")}`;
if (value) {
select.value = value;
}
return select;
}
function createInfoText(text, title = "") {
const label = document.createElement("div");
label.textContent = text;
label.style.fontSize = "9px";
label.style.marginBottom = "4px";
label.style.color = "#999";
if (title) {
label.title = title;
}
return label;
}
function createRangeControl(state, storageKey, {
stateValueKey,
labelPrefix,
min = "1",
max = "5",
title = "",
}) {
const value = state[stateValueKey] || "1";
const label = createInfoText(`${labelPrefix}: ${value}`, title);
const slider = document.createElement("input");
slider.type = "range";
slider.min = min;
slider.max = max;
slider.value = value;
slider.style.width = "100%";
slider.style.marginBottom = "4px";
if (title) {
slider.title = title;
}
slider.addEventListener("input", () => {
label.textContent = `${labelPrefix}: ${slider.value}`;
state[stateValueKey] = slider.value;
state.saveToLocalStorage(storageKey);
});
return { label, slider };
}
function createButton(text, title) {
const button = document.createElement("button");
button.textContent = text;
button.title = title;
button.style.width = "100%";
button.style.padding = "4px";
button.style.cursor = "pointer";
button.style.fontSize = "10px";
button.style.background = "#3a3a3a";
button.style.color = "#eee";
button.style.border = "1px solid #555";
button.style.borderRadius = "2px";
return button;
}
function buildStepSection(state, storageKey) {
const section = document.createElement("div");
section.style.display = "flex";
section.style.flexDirection = "column";
section.style.gap = "8px";
const paraSection = document.createElement("div");
stylePanelContainer(paraSection);
const paraSelect = createSelect([
{ value: "Laughter", label: "Laughter" },
{ value: "Breathing", label: "Breathing" },
{ value: "Sigh", label: "Sigh" },
{ value: "Uhm", label: "Uhm" },
{ value: "Surprise-oh", label: "Surprise (oh)" },
{ value: "Surprise-ah", label: "Surprise (ah)" },
{ value: "Surprise-wa", label: "Surprise (wa)" },
{ value: "Confirmation-en", label: "Confirmation (en)" },
{ value: "Question-ei", label: "Question (ei)" },
{ value: "Dissatisfaction-hnn", label: "Dissatisfaction (hnn)" },
], "Select sound...", state.lastParalinguisticType || "");
paraSelect.addEventListener("change", () => {
state.lastParalinguisticType = paraSelect.value;
state.saveToLocalStorage(storageKey);
});
const { label: paraIterLabel, slider: paraIterSlider } = createRangeControl(state, storageKey, {
stateValueKey: "lastParalinguisticIter",
labelPrefix: "Iterations",
});
const addParaBtn = createButton("Add Paralinguistic", "Insert paralinguistic tag at cursor");
paraSection.append(
createPanelLabel("Paralinguistic", "#00ffff"),
paraSelect,
paraIterLabel,
paraIterSlider,
addParaBtn
);
const emotionSection = document.createElement("div");
stylePanelContainer(emotionSection);
const emotionSelect = createSelect([
"happy", "sad", "angry", "excited", "calm", "fearful", "surprised",
"disgusted", "confusion", "empathy", "embarrass", "depressed", "coldness", "admiration"
].map((value) => ({ value, label: value.charAt(0).toUpperCase() + value.slice(1) })), "Select emotion...", state.lastEmotionType || "");
emotionSelect.addEventListener("change", () => {
state.lastEmotionType = emotionSelect.value;
state.saveToLocalStorage(storageKey);
});
const { label: emotionIterLabel, slider: emotionIterSlider } = createRangeControl(state, storageKey, {
stateValueKey: "lastEmotionIter",
labelPrefix: "Iterations",
});
const addEmotionBtn = createButton("Add Emotion", "Insert emotion tag at cursor");
emotionSection.append(
createPanelLabel("Emotion", "#ffaa00"),
emotionSelect,
emotionIterLabel,
emotionIterSlider,
addEmotionBtn
);
const styleSection = document.createElement("div");
stylePanelContainer(styleSection);
const styleSelect = createSelect([
"whisper", "serious", "child", "older", "girl", "pure", "sister", "sweet",
"exaggerated", "ethereal", "generous", "recite", "act_coy", "warm", "shy",
"comfort", "authority", "chat", "radio", "soulful", "gentle", "story", "vivid",
"program", "news", "advertising", "roar", "murmur", "shout", "deeply", "loudly",
"arrogant", "friendly"
].map((value) => ({ value, label: value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase()) })), "Select style...", state.lastStyleType || "");
styleSelect.addEventListener("change", () => {
state.lastStyleType = styleSelect.value;
state.saveToLocalStorage(storageKey);
});
const { label: styleIterLabel, slider: styleIterSlider } = createRangeControl(state, storageKey, {
stateValueKey: "lastStyleIter",
labelPrefix: "Iterations",
});
const addStyleBtn = createButton("Add Style", "Insert style tag at cursor");
styleSection.append(
createPanelLabel("Style", "#ff5555"),
styleSelect,
styleIterLabel,
styleIterSlider,
addStyleBtn
);
const speedSection = document.createElement("div");
stylePanelContainer(speedSection);
const speedSelect = createSelect([
{ value: "faster", label: "Faster" },
{ value: "slower", label: "Slower" },
{ value: "more_faster", label: "More Faster" },
{ value: "more_slower", label: "More Slower" },
], "Select speed...", state.lastSpeedType || "");
speedSelect.addEventListener("change", () => {
state.lastSpeedType = speedSelect.value;
state.saveToLocalStorage(storageKey);
});
const { label: speedIterLabel, slider: speedIterSlider } = createRangeControl(state, storageKey, {
stateValueKey: "lastSpeedIter",
labelPrefix: "Iterations",
});
const addSpeedBtn = createButton("Add Speed", "Insert speed tag at cursor");
speedSection.append(
createPanelLabel("Speed", "#66ff66"),
speedSelect,
speedIterLabel,
speedIterSlider,
addSpeedBtn
);
const restoreSection = document.createElement("div");
stylePanelContainer(restoreSection, { separated: false });
const { label: restorePassLabel, slider: restorePassSlider } = createRangeControl(state, storageKey, {
stateValueKey: "lastRestorePasses",
labelPrefix: "VC Passes",
title: "How many restoration passes to run. More passes push harder toward the chosen reference voice.",
});
const restoreRefLabel = createInfoText(
"Reference Iteration (optional)",
"Optional edit-step reference. In , the @2 points to edit step 2 from earlier inline edits."
);
const restoreRefInput = document.createElement("input");
restoreRefInput.type = "number";
restoreRefInput.placeholder = "Leave empty for original";
restoreRefInput.min = "1";
restoreRefInput.max = "10";
restoreRefInput.value = state.lastRestoreRefIter || "";
restoreRefInput.style.width = "100%";
restoreRefInput.style.padding = "3px";
restoreRefInput.style.fontSize = "10px";
restoreRefInput.style.background = "#2a2a2a";
restoreRefInput.style.color = "#eee";
restoreRefInput.style.border = "1px solid #444";
restoreRefInput.style.marginBottom = "4px";
restoreRefInput.title = "Leave empty to use the original clean pre-edit audio. Enter an edit step number to use that earlier edited snapshot as the reference voice.";
restoreRefInput.addEventListener("change", () => {
state.lastRestoreRefIter = restoreRefInput.value;
state.saveToLocalStorage(storageKey);
});
const addRestoreBtn = createButton("Add Restore", "Insert , , or .");
restoreSection.append(
createPanelLabel("Voice Restoration", "#ffcc33", "Restore runs after all other inline edits."),
restorePassLabel,
restorePassSlider,
restoreRefLabel,
restoreRefInput,
addRestoreBtn
);
section.append(paraSection, emotionSection, styleSection, speedSection, restoreSection);
return {
panel: section,
controls: {
paraSelect, paraIterSlider, addParaBtn,
emotionSelect, emotionIterSlider, addEmotionBtn,
styleSelect, styleIterSlider, addStyleBtn,
speedSelect, speedIterSlider, addSpeedBtn,
restorePassSlider, restoreRefInput, addRestoreBtn,
},
};
}
function buildHiggsSection(state, storageKey) {
const section = document.createElement("div");
section.style.display = "flex";
section.style.flexDirection = "column";
section.style.gap = "8px";
state.lastHiggsInlineValues = state.lastHiggsInlineValues || {};
const buildHiggsTagSubsection = ({ category, label, color, separated = true }) => {
const subsection = document.createElement("div");
stylePanelContainer(subsection, { separated });
const savedValue = state.lastHiggsInlineValues[category] || HIGGS_TAGS[category][0] || "";
const select = createSelect(
HIGGS_TAGS[category].map((value) => ({ value, label: value })),
`Select ${label.toLowerCase()}...`,
savedValue
);
select.addEventListener("change", () => {
state.lastHiggsInlineValues[category] = select.value;
state.saveToLocalStorage(storageKey);
});
const addBtn = createButton(`Add ${label}`, `Insert canonical Higgs ${label.toLowerCase()} tag at cursor`);
subsection.append(
createPanelLabel(label, color),
select,
addBtn
);
return { subsection, select, addBtn };
};
const emotionSection = buildHiggsTagSubsection({
category: "emotion",
label: "Emotion",
color: "#64d7ff",
});
const styleSection = buildHiggsTagSubsection({
category: "style",
label: "Style",
color: "#8fcbff",
});
const prosodySection = buildHiggsTagSubsection({
category: "prosody",
label: "Prosody",
color: "#b5a3ff",
});
const sfxSection = buildHiggsTagSubsection({
category: "sfx",
label: "SFX",
color: "#ff9e7a",
separated: false,
});
section.append(
emotionSection.subsection,
styleSection.subsection,
prosodySection.subsection,
sfxSection.subsection
);
return {
panel: section,
controls: {
emotionSelect: emotionSection.select,
addEmotionBtn: emotionSection.addBtn,
styleSelect: styleSection.select,
addStyleBtn: styleSection.addBtn,
prosodySelect: prosodySection.select,
addProsodyBtn: prosodySection.addBtn,
sfxSelect: sfxSection.select,
addSfxBtn: sfxSection.addBtn,
},
};
}
function buildCosySection(state, storageKey) {
const section = document.createElement("div");
section.style.display = "flex";
section.style.flexDirection = "column";
section.style.gap = "8px";
const singleSection = document.createElement("div");
stylePanelContainer(singleSection);
const singleTagSelect = createSelect(
COSY_SINGLE_TAGS.map((value) => ({ value, label: value })),
"Select single tag...",
state.lastCosyInlineSingleTag || "breath"
);
singleTagSelect.addEventListener("change", () => {
state.lastCosyInlineSingleTag = singleTagSelect.value;
state.saveToLocalStorage(storageKey);
});
const addSingleTagBtn = createButton("Add Single Tag", "Insert native CosyVoice3 single tag like ");
singleSection.append(
createPanelLabel("Single Tags", "#8cff9d"),
singleTagSelect,
addSingleTagBtn
);
const wrapperSection = document.createElement("div");
stylePanelContainer(wrapperSection, { separated: false });
const wrapperTagSelect = createSelect(
COSY_WRAPPER_TAGS.map((value) => ({ value, label: value })),
"Select wrapper tag...",
state.lastCosyInlineWrapperTag || "laughing"
);
wrapperTagSelect.addEventListener("change", () => {
state.lastCosyInlineWrapperTag = wrapperTagSelect.value;
state.saveToLocalStorage(storageKey);
});
const wrapperHint = createInfoText("If text is selected, the wrapper will surround it. Otherwise it inserts an example pair.");
const addWrapperTagBtn = createButton("Add Wrapper Tag", "Insert wrapper tag pair like text");
wrapperSection.append(
createPanelLabel("Wrapper Tags", "#f6c95b"),
wrapperTagSelect,
wrapperHint,
addWrapperTagBtn
);
section.append(singleSection, wrapperSection);
return {
panel: section,
controls: {
singleTagSelect,
addSingleTagBtn,
wrapperTagSelect,
addWrapperTagBtn,
},
};
}
function buildOmniVoiceSection(state, storageKey) {
const section = document.createElement("div");
section.style.display = "flex";
section.style.flexDirection = "column";
section.style.gap = "8px";
const tagSection = document.createElement("div");
stylePanelContainer(tagSection, { separated: false });
const tagSelect = createSelect(
OMNIVOICE_NON_VERBAL_TAGS.map((value) => ({ value, label: value })),
"Select OmniVoice tag...",
state.lastOmniVoiceInlineTag || "laughter"
);
tagSelect.addEventListener("change", () => {
state.lastOmniVoiceInlineTag = tagSelect.value;
state.saveToLocalStorage(storageKey);
});
const tagHint = createInfoText(
"Editor inserts suite-default aliases like . The OmniVoice processor converts them to official [laughter] tags at generation time.",
"OmniVoice also supports bracketed CMU pronunciation overrides like [B EY1 S], but those are typed manually."
);
const addTagBtn = createButton("Add OmniVoice Tag", "Insert suite-default OmniVoice tag like ");
tagSection.append(
createPanelLabel("Native Non-Verbal Tags", "#82d4ff"),
tagSelect,
tagHint,
addTagBtn
);
section.append(tagSection);
return {
panel: section,
controls: {
tagSelect,
addTagBtn,
},
};
}
function createIndexTTSRadialPicker(onPick) {
const button = createButton("Press + drag to pick emotion", "Drag toward an emotion; distance sets its magnitude");
button.style.background = `linear-gradient(90deg, ${INDEX_TTS_RADIAL_EMOTIONS.map(emotion => INDEX_TTS_EMOTION_COLORS[emotion]).join(", ")})`;
button.addEventListener("pointerdown", event => {
if (event.button !== 0) return;
event.preventDefault();
event.stopPropagation();
const size = 220;
const center = size / 2;
const maxRadius = 82;
const originX = event.clientX;
const originY = event.clientY;
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
Object.assign(canvas.style, {
position: "fixed", left: `${originX - center}px`, top: `${originY - center}px`,
width: `${size}px`, height: `${size}px`, zIndex: "100002", pointerEvents: "none",
borderRadius: "50%", filter: "drop-shadow(0 8px 20px rgba(0,0,0,.85))"
});
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
let selectedIndex = 0;
let magnitude = 0;
const draw = () => {
ctx.clearRect(0, 0, size, size);
const selectedEmotion = INDEX_TTS_RADIAL_EMOTIONS[selectedIndex];
const selectedColor = INDEX_TTS_EMOTION_COLORS[selectedEmotion];
ctx.fillStyle = "rgba(25,25,27,.96)";
ctx.beginPath();
ctx.arc(center, center, 104, 0, Math.PI * 2);
ctx.fill();
ctx.strokeStyle = selectedColor;
ctx.lineWidth = 2;
ctx.stroke();
const selectedAngle = selectedIndex * Math.PI / 4 - Math.PI / 2;
ctx.fillStyle = `${selectedColor}38`;
ctx.beginPath();
ctx.moveTo(center, center);
ctx.arc(center, center, 100, selectedAngle - Math.PI / 8, selectedAngle + Math.PI / 8);
ctx.closePath();
ctx.fill();
INDEX_TTS_RADIAL_EMOTIONS.forEach((emotion, index) => {
const angle = index * Math.PI / 4 - Math.PI / 2;
const x = center + Math.cos(angle) * maxRadius;
const y = center + Math.sin(angle) * maxRadius;
ctx.fillStyle = index === selectedIndex ? INDEX_TTS_EMOTION_COLORS[emotion] : "#aaa";
ctx.font = index === selectedIndex ? "bold 11px Arial" : "10px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(emotion, x, y);
});
ctx.fillStyle = selectedColor;
ctx.beginPath();
ctx.arc(center, center, 28, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = selectedEmotion === "happy" ? "#201b00" : "#f5f5f5";
ctx.font = "bold 12px Arial";
ctx.textAlign = "center";
ctx.fillText(magnitude.toFixed(2), center, center);
};
const update = pointerEvent => {
const dx = pointerEvent.clientX - originX;
const dy = pointerEvent.clientY - originY;
const distance = Math.sqrt(dx * dx + dy * dy);
let angle = Math.atan2(dy, dx) + Math.PI / 2;
if (angle < 0) angle += Math.PI * 2;
selectedIndex = Math.round(angle / (Math.PI / 4)) % 8;
magnitude = Math.min(1.2, Math.max(0, distance / maxRadius * 1.2));
onPick(INDEX_TTS_RADIAL_EMOTIONS[selectedIndex], magnitude, false);
draw();
};
const finish = pointerEvent => {
update(pointerEvent);
window.removeEventListener("pointermove", update, true);
window.removeEventListener("pointerup", finish, true);
window.removeEventListener("pointercancel", cancel, true);
canvas.remove();
onPick(INDEX_TTS_RADIAL_EMOTIONS[selectedIndex], magnitude, true);
};
const cancel = () => {
window.removeEventListener("pointermove", update, true);
window.removeEventListener("pointerup", finish, true);
window.removeEventListener("pointercancel", cancel, true);
canvas.remove();
};
window.addEventListener("pointermove", update, true);
window.addEventListener("pointerup", finish, true);
window.addEventListener("pointercancel", cancel, true);
draw();
});
return button;
}
function buildIndexTTSSection(state, storageKey) {
const section = document.createElement("div");
Object.assign(section.style, { display: "flex", flexDirection: "column", gap: "8px" });
const vectorSection = document.createElement("div");
stylePanelContainer(vectorSection);
const vectorModeSelect = createSelect([
{ value: "absolute", label: "Absolute vector" },
{ value: "delta", label: "Delta vector" },
], "Vector mode...", state.lastIndexTTSVectorMode || "absolute");
vectorModeSelect.addEventListener("change", () => {
state.lastIndexTTSVectorMode = vectorModeSelect.value;
state.saveToLocalStorage(storageKey);
});
const addVectorBtn = createButton("Add Vector Tag", "Insert a full vector tag; click it in the editor to open the radar");
vectorSection.append(
createPanelLabel("Full Vector Radar", "#45b7d1"),
vectorModeSelect,
createInfoText("Insert the tag, then click it in the editor for live radar editing."),
addVectorBtn
);
const namedSection = document.createElement("div");
stylePanelContainer(namedSection);
const namedEmotionSelect = createSelect(
INDEX_TTS_EMOTIONS.map(value => ({ value, label: value.charAt(0).toUpperCase() + value.slice(1) })),
"Select emotion...",
state.lastIndexTTSNamedEmotion || "happy"
);
const namedOperationSelect = createSelect([
{ value: "absolute", label: "Absolute value" },
{ value: "positive", label: "Positive delta (+)" },
{ value: "negative", label: "Negative delta (−)" },
], "Operation...", state.lastIndexTTSNamedOperation || "absolute");
const namedValueInput = document.createElement("input");
namedValueInput.type = "range";
namedValueInput.min = "0";
namedValueInput.max = "1.2";
namedValueInput.step = "0.05";
namedValueInput.value = state.lastIndexTTSNamedValue ?? "0.5";
Object.assign(namedValueInput.style, { width: "100%", boxSizing: "border-box", marginBottom: "4px" });
const namedValueLabel = createInfoText(`Magnitude: ${Number(namedValueInput.value).toFixed(2)}`);
const updateNamedSliderFill = () => {
const ratio = Math.max(0, Math.min(100, Number(namedValueInput.value) / 1.2 * 100));
const color = INDEX_TTS_EMOTION_COLORS[namedEmotionSelect.value] || "#20B2AA";
namedValueInput.dataset.fillColor = color;
namedValueInput.style.accentColor = color;
namedValueInput.style.background = `linear-gradient(90deg, ${color} 0%, ${color} ${ratio}%, rgba(53,53,52,.9) ${ratio}%, rgba(53,53,52,.9) 100%)`;
};
const saveNamedState = (persist = true) => {
state.lastIndexTTSNamedEmotion = namedEmotionSelect.value;
state.lastIndexTTSNamedOperation = namedOperationSelect.value;
state.lastIndexTTSNamedValue = namedValueInput.value;
if (persist) state.saveToLocalStorage(storageKey);
};
namedEmotionSelect.addEventListener("change", saveNamedState);
namedOperationSelect.addEventListener("change", saveNamedState);
namedValueInput.addEventListener("input", () => {
namedValueLabel.textContent = `Magnitude: ${Number(namedValueInput.value).toFixed(2)}`;
updateNamedSliderFill();
saveNamedState();
});
const radialPickerBtn = createIndexTTSRadialPicker((emotion, magnitude, isFinal) => {
namedEmotionSelect.value = emotion;
namedValueInput.value = magnitude.toFixed(2);
namedValueLabel.textContent = `Magnitude: ${magnitude.toFixed(2)} · ${emotion}`;
updateNamedSliderFill();
saveNamedState(isFinal);
});
namedEmotionSelect.addEventListener("change", updateNamedSliderFill);
updateNamedSliderFill();
const addNamedEmotionBtn = createButton("Add Named Emotion", "Insert [sad:0.5], [sad:+0.5], or [sad:-0.5]");
namedSection.append(
createPanelLabel("Named Emotion Value", "#ffcc66"),
radialPickerBtn,
namedEmotionSelect, namedOperationSelect, namedValueLabel, namedValueInput, addNamedEmotionBtn
);
const textSection = document.createElement("div");
stylePanelContainer(textSection, { separated: false });
const presetSelect = createSelect([], "Select saved preset...");
const populateTextPresets = (presets = {}) => {
const previous = presetSelect.value;
while (presetSelect.options.length > 1) presetSelect.remove(1);
Object.entries(presets)
.filter(([, preset]) => preset?.type === "text" || preset?.type === "vector")
.sort(([a], [b]) => a.localeCompare(b))
.forEach(([name, preset]) => {
const option = document.createElement("option");
option.value = name;
option.textContent = `${name} (${preset.type})`;
option.dataset.presetType = preset.type;
if (preset.type === "vector") option.dataset.vectorValues = JSON.stringify(preset.values || []);
presetSelect.appendChild(option);
});
if ([...presetSelect.options].some(option => option.value === previous)) presetSelect.value = previous;
};
const refreshTextPresets = () => api.fetchApi("/api/tts-audio-suite/index-tts-emotion-presets")
.then(response => response.json())
.then(({ presets = {} }) => populateTextPresets(presets))
.catch(error => console.warn("Could not load IndexTTS emotion presets", error));
refreshTextPresets();
presetSelect.addEventListener("focus", refreshTextPresets);
window.addEventListener("tts-audio-suite:index-tts-presets-changed", event => {
populateTextPresets(event.detail?.presets || {});
});
const addTextPresetBtn = createButton("Add Preset Tag", "Insert a saved text or vector emotion preset");
const managePresetsBtn = createButton("Manage Emotion Presets", "Open the IndexTTS emotion preset editor");
managePresetsBtn.style.marginBottom = "5px";
const emotionTextInput = document.createElement("input");
emotionTextInput.type = "text";
emotionTextInput.placeholder = "Emotion description; {seg} makes it dynamic";
emotionTextInput.value = state.lastIndexTTSEmotionText || "";
Object.assign(emotionTextInput.style, { width: "100%", boxSizing: "border-box", padding: "3px", margin: "4px 0", fontSize: "10px", color: "#eee", background: "#2a2a2a", border: "1px solid #444" });
emotionTextInput.addEventListener("change", () => {
state.lastIndexTTSEmotionText = emotionTextInput.value;
state.saveToLocalStorage(storageKey);
});
const addEmotionTextBtn = createButton("Add Quoted Text", "Insert a quoted text emotion; include {seg} for dynamic analysis");
textSection.append(
createPanelLabel("Text Emotion", "#d78cff"),
managePresetsBtn,
presetSelect, addTextPresetBtn,
emotionTextInput, addEmotionTextBtn
);
section.append(vectorSection, namedSection, textSection);
return {
panel: section,
controls: {
vectorModeSelect, addVectorBtn,
namedEmotionSelect, namedOperationSelect, namedValueInput, addNamedEmotionBtn,
presetSelect, addTextPresetBtn, managePresetsBtn, emotionTextInput, addEmotionTextBtn,
},
};
}
export function buildInlineEditSection(state, storageKey) {
const container = document.createElement("div");
container.style.display = "flex";
container.style.flexDirection = "column";
container.style.gap = "8px";
container.style.overflowY = "visible";
container.style.flex = "0 0 auto";
container.style.paddingRight = "0";
const engineSection = document.createElement("div");
stylePanelContainer(engineSection);
const inlineEngineSelect = createSelect([
{ value: "step_audio_editx", label: "Step Audio EditX" },
{ value: "higgs_audio_v3", label: "Higgs Audio v3" },
{ value: "cosyvoice3", label: "CosyVoice3" },
{ value: "omnivoice", label: "OmniVoice" },
{ value: "index_tts", label: "IndexTTS-2" },
], "Select inline tag engine...", state.activeInlineTagEngine || "step_audio_editx");
inlineEngineSelect.addEventListener("change", () => {
state.activeInlineTagEngine = inlineEngineSelect.value;
updateVisiblePanel();
state.saveToLocalStorage(storageKey);
});
engineSection.append(
createPanelLabel("Tag Engine", "#d0d0d0"),
inlineEngineSelect
);
const stepSection = buildStepSection(state, storageKey);
const higgsSection = buildHiggsSection(state, storageKey);
const cosySection = buildCosySection(state, storageKey);
const omnivoiceSection = buildOmniVoiceSection(state, storageKey);
const indexTTSSection = buildIndexTTSSection(state, storageKey);
const panels = {
step_audio_editx: stepSection.panel,
higgs_audio_v3: higgsSection.panel,
cosyvoice3: cosySection.panel,
omnivoice: omnivoiceSection.panel,
index_tts: indexTTSSection.panel,
};
const updateVisiblePanel = () => {
const activeEngine = inlineEngineSelect.value || "step_audio_editx";
Object.entries(panels).forEach(([engineKey, panel]) => {
panel.style.display = engineKey === activeEngine ? "flex" : "none";
});
};
container.append(engineSection, stepSection.panel, higgsSection.panel, cosySection.panel, omnivoiceSection.panel, indexTTSSection.panel);
updateVisiblePanel();
return {
inlineEditSection: container,
inlineTagControls: {
inlineEngineSelect,
step: stepSection.controls,
higgs: higgsSection.controls,
cosy: cosySection.controls,
omnivoice: omnivoiceSection.controls,
indexTTS: indexTTSSection.controls,
},
updateInlineEnginePanelVisibility: updateVisiblePanel,
};
}