Files
DemonGatanjieu-Anomalous_Mo…/web/modules/audio_script.js
T
DemonGatanjieuandClaude Opus 5.5 66979f698a fix(audio): align voices with ComfyUI-F5-TTS multi-voice layout and repair studio UI
ComfyUI-F5-TTS resolves {happy} to <sample stem>.happy.wav beside the selected
sample, so `Arona_happy.wav` + `{Arona_happy}` could never run. Voices now use
Character.wav (main, {main}) and Character.<emotion>.wav ({emotion}); the
catalog groups a main voice with its variants and marks tags that F5-TTS
cannot resolve (missing main, missing .txt, different format).

- script director: one character per script, untagged lines pinned to {main},
  pushes sample (main voice, node's own combo spelling) and speech together,
  confirms before overwriting and re-checks the node after the dialog
- pure rules (splitting, composing, planning, combo spelling) in audio_script.js
- one module instance per file: drop ?v= imports that duplicated
  ui_audio_gallery/ui_audio_studio state, so studio playback stops the gallery
- all user/file-derived text goes through textContent (no innerHTML)
- studio/sidebar/gallery renders ignore stale responses; errors are shown
- gallery: count badge no longer overwrites the title, paging via "load more",
  deletion errors surfaced, timeupdate instead of a 100ms timer
- sidebar: filter selection updates in place instead of refetching; the header
  tabs, sidebar and domain switch share browser.switchAudioTab
- uploader: createViewScope lifecycle (Esc/overlay/close release listeners and
  object URLs, Esc no longer reaches the parent), 409 -> confirm -> overwrite,
  sends the pre-romanization text as original_text, no hard-coded character
- workflow template: confirm before replacing the canvas, clear message when
  the template file is missing, align sample paths to the node's combo values
- static styles moved from inline JS into 11-audio-studio.css; strings localized

Verified: tests/audio_script_rules.mjs, tests/test_audio_catalog.py (15), a
jsdom walkthrough of studio/gallery/director/uploader with a stubbed app
(outside the repo), JS syntax and import/export link check. Not yet checked in
a live ComfyUI session.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W411Z69FC3em9CzpeqEub5
2026-09-23 08:02:33 +00:00

51 lines
2.1 KiB
JavaScript

/**
* Pure rules for ComfyUI-F5-TTS voices (no DOM, no canvas access).
*
* F5-TTS multi-voice layout: the node's `sample` is a main voice such as
* `F5-TTS/Arona.wav`; `{happy}` in the speech loads `F5-TTS/Arona.happy.wav`
* beside it, and `{main}` is the sample itself. Speech is split only at tags.
*/
export const MAIN_TAG = '{main}';
const normalisePath = value => String(value ?? '').replace(/\\/g, '/');
/** Return the widget's own spelling of `relativePath` (Windows combos use "F5-TTS\\x.wav"), or null when not offered. */
export function comboValueForPath(widget, relativePath) {
const wanted = normalisePath(relativePath);
if (!wanted) return null;
const values = Array.isArray(widget?.options?.values) ? widget.options.values : null;
if (!values) return wanted;
return values.find(value => normalisePath(value) === wanted) ?? null;
}
/** Split on sentence punctuation and newlines; '.' only ends a line before whitespace (keeps 3.5). */
export function splitScriptLines(text) {
return String(text || '')
.split(/\n+|(?<=[。!?!?…])|(?<=\.)(?=\s)/)
.map(line => line.trim())
.filter(Boolean);
}
/** Once any line is tagged, untagged lines get {main} instead of inheriting the previous voice. */
export function composeScript(lines) {
const anyTagged = lines.some(line => line.voice?.tag);
return lines
.map(line => {
const tag = line.voice?.tag || (anyTagged ? MAIN_TAG : '');
return tag ? `${tag} ${line.text}` : line.text;
})
.join('\n')
.trim();
}
/** Check the tagged voices can run in one F5TTSAudio node; returns { mainPath } or { error: localeKey }. */
export function planScriptVoices(lines) {
const voices = lines.map(line => line.voice).filter(Boolean);
if (!voices.length) return { mainPath: null };
if (new Set(voices.map(voice => voice.group)).size > 1) return { error: 'scriptDirectorMixedCharacters' };
if (voices.some(voice => !voice.usable)) return { error: 'scriptDirectorVoiceUnusable' };
if (!voices[0].mainPath) return { error: 'scriptDirectorMainMissing' };
return { mainPath: voices[0].mainPath };
}