Audio gallery becomes a view of Voices; the rail fits the number keys

With the AI workbench the rail had ten entries for nine number keys, so
the audio gallery had none. It is now the third tab of Voices (Characters,
Voice-over, Audio); 9 reopens Voices on the view used last, and a test
keeps the rail within nine entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-10-28 20:26:31 +08:00
co-authored by Claude Opus 5.5
parent 1d444f0669
commit cae7fd2a4a
9 changed files with 36 additions and 24 deletions
+5 -4
View File
@@ -694,10 +694,11 @@ covered by the `styles.css` manifest.
`planVoiceDrop` decides every character drop and names the reason for each refusal;
unlisted nodes are refused, never matched by widget name. Supporting a node
means adding one entry plus a test.
- The rail's Voices entry has two views, switched by `ui_voice_tabs.js` at the top of
each and sharing the character list: Characters (shell page `voices`) and Voice-over
(shell page `script`; `ui_shell_nav.js` keeps the rail on Voices and reopens the view
used last).
- The rail's Voices entry has three views, switched by `ui_voice_tabs.js` at the top of
each and sharing the list column: Characters (shell page `voices`), Voice-over (shell
page `script`) and Audio (shell page `audio-gallery`, `ui_audio_gallery.js`).
`ui_shell_nav.js` keeps the rail on Voices and reopens the view used last, from the
rail and from the 9 key; the rail stays within the nine number keys.
`ui_audio_studio.js` owns the Characters view: the character cards, preview playback,
tag copying, dragging a card header onto a node through the shared `bindMaterialDrag`
with `targetHint`/`rejectHint` telling the user what releasing does, and each card's
+3
View File
@@ -2,6 +2,9 @@
## v1.62.0 — unreleased
- 音频库搬进了“角色语音”页:页面顶上的标签现在是 **角色 | 配音 | 音频库**,左边栏少了一项。加了 AI 工作台以后左边栏有 10 项,数字键只有 1–9,音频库就没键可按了;现在 9 打开角色语音,并回到你上次停的那个标签。
- The audio gallery moved into Voices: the tabs at the top read Characters | Voice-over | Audio, and the rail is one entry shorter. With the AI workbench the rail had ten entries for nine number keys, leaving the gallery without one; 9 now opens Voices on the view used last.
- AI 能拿刚出的图接着改了:搭“在这张图上重画”的工作流时,加载图像节点也认输出文件夹里的图(`xxx.png [output]`,ComfyUI 本来就支持);以前 input 文件夹里没有这张图就被拦下。左侧栏的“模型”“图库”不再显示成英文 models / gallery。
- The AI can redraw a picture it just made: a Load Image it builds now takes an output picture (`name.png [output]`, as ComfyUI itself does) instead of being refused when the input folder lacks it. The rail shows Models / Gallery again instead of the raw keys.
+5
View File
@@ -15,3 +15,8 @@ assert.ok(titleKeys.includes('gallery'));
for (const key of new Set([...railKeys, ...titleKeys])) {
for (const lang of ['zh', 'en']) assert.ok(i18n[lang][key], `${lang} has no '${key}'`);
}
// Every rail page has a number key (1-9, ui_shell_nav.js): the AI workbench pushed the audio
// gallery off the keys until it became a view of Voices.
const railPages = read('ui_shell_rail.js').match(/export const RAIL_PAGES = Object\.freeze\(\[([\s\S]*?)\]\);/)[1];
assert.ok([...railPages.matchAll(/\{ page: '/g)].length <= 9, 'more rail pages than number keys');
+4 -4
View File
@@ -16,14 +16,14 @@ assert.equal(pageForNode({ model: true }, kinds), 'assistant'); // a loader
assert.equal(pageForNode({ prompt: true, model: true }, kinds), 'assistant'); // a loader with prompt boxes: its models first
// Number keys: 1-9 from the top of the rail.
const pages = ['home', 'activity', 'models', 'gallery', 'recipes', 'combos', 'prompts', 'voices', 'audio-gallery'].map(page => ({ page }));
const pages = ['home', 'activity', 'ai', 'models', 'gallery', 'recipes', 'combos', 'prompts', 'voices'].map(page => ({ page }));
const canvas = { closest: () => null };
const field = { closest: selector => (selector.includes('textarea') ? {} : null) };
const key = (code, extra = {}) => ({ code, key: code.slice(-1), target: canvas, ...extra });
assert.equal(railPageForKey(key('Digit1'), pages), 'home');
assert.equal(railPageForKey(key('Digit7'), pages), 'prompts');
assert.equal(railPageForKey(key('Numpad9'), pages), 'audio-gallery');
assert.equal(railPageForKey(key('Digit8'), pages), 'prompts');
assert.equal(railPageForKey(key('Numpad9'), pages), 'voices');
assert.equal(railPageForKey(key('Digit0'), pages), null);
assert.equal(railPageForKey(key('KeyW'), pages), null); // W stays ComfyUI's (the Workflows sidebar)
assert.equal(railPageForKey(key('Digit3', { target: field }), pages), null); // typing a number into a box
@@ -31,7 +31,7 @@ assert.equal(railPageForKey(key('Digit3', { ctrlKey: true }), pages), null);
assert.equal(railPageForKey(key('Digit3', { shiftKey: true }), pages), null);
assert.equal(railPageForKey(key('Digit3', { isComposing: true }), pages), null); // a Chinese input method choosing a word
assert.equal(railPageForKey(key('Digit3', { repeat: true }), pages), null);
assert.equal(railPageForKey({ code: '', key: '4', target: canvas }, pages), 'gallery'); // no code: by the key
assert.equal(railPageForKey({ code: '', key: '5', target: canvas }, pages), 'gallery'); // no code: by the key
assert.equal(railPageForKey(key('Digit9'), pages.slice(0, 3)), null); // fewer pages than keys
console.log('shell_open_rules: ok');
+5 -2
View File
@@ -1,6 +1,7 @@
import { t } from './interface_settings.js';
import { anomalousAlert, anomalousConfirm } from './ui_dialog.js';
import { parseTaggedSpeech } from './audio_script.js';
import { renderVoiceTabs } from './ui_voice_tabs.js';
/**
* Audio Gallery: generated audio in ComfyUI's output folder, shown with the
@@ -288,7 +289,8 @@ function createTrackRow(item, { mode, onDeleted, onSaved, onVoiceAgain }) {
// ---------- sections ----------
function renderGalleryToolbar({ onRefresh, onSearch }) {
/** Its title, or with `owner` (the browser) the Voices page's view switch in its place. */
function renderGalleryToolbar({ owner, onRefresh, onSearch }) {
const bar = el('div', 'anomalous-audio-toolbar');
const left = el('div', 'anomalous-audio-title-group');
@@ -314,7 +316,7 @@ function renderGalleryToolbar({ onRefresh, onSearch }) {
const countBadge = el('span', 'anomalous-track-count');
right.append(search, countBadge, iconButton('', SVG.REFRESH, t('audioRefresh'), onRefresh));
bar.append(left, right);
bar.append(owner ? renderVoiceTabs(owner, 'audio-gallery') : left, right);
return { bar, countBadge };
}
@@ -359,6 +361,7 @@ export async function renderAudioGallery(container, { owner = null } = {}) {
const listSection = el('div', 'anomalous-track-list-section');
let loadList = () => {};
const { bar, countBadge } = renderGalleryToolbar({
owner,
onRefresh: () => renderAudioGallery(container, { owner }),
onSearch: query => loadList(query),
});
+4 -4
View File
@@ -24,8 +24,8 @@ const REMEMBERED = new Set(['home', 'activity', 'ai', 'models', 'gallery', 'prom
const AUDIO_PAGES = new Set(['voices', 'script', 'audio-gallery']);
// The audio tab (browser.switchAudioTab) of each audio page.
const AUDIO_TABS = { voices: 'presets', script: 'script', 'audio-gallery': 'gallery' };
// Voice-over is a view of the Voices page: same rail entry, same list column.
const RAIL_OF = { script: 'voices' };
// Voice-over and Audio are views of the Voices page: same rail entry, same list column.
const RAIL_OF = { script: 'voices', 'audio-gallery': 'voices' };
const railOf = page => RAIL_OF[page] || page;
// Pages with a list column, and where each remembers whether you closed it.
const LIST_KEYS = {
@@ -36,7 +36,7 @@ const LIST_KEYS = {
};
const TITLE_KEYS = {
home: 'shellHome', activity: 'activityTitle', ai: 'shellAi', models: 'shellTitleModels', gallery: 'gallery', prompts: 'shellPrompts', recipes: 'recipeTitle',
combos: 'shellCombos', voices: 'shellVoices', script: 'shellVoices', 'audio-gallery': 'shellAudioGallery',
combos: 'shellCombos', voices: 'shellVoices', script: 'shellVoices', 'audio-gallery': 'shellVoices',
doctor: 'sidebarDoctor', assistant: 'sidebarAssistant', scan: 'scanPageTitle', settings: 'sidebarSettings',
};
// Below this width the list covers the page instead of sitting beside it, and starts closed.
@@ -102,7 +102,7 @@ export function installShellNavigation(owner, { container, rail, listToggle, tit
const page = railPageForKey(e, RAIL_PAGES);
if (!page || dialogOpen()) return;
e.preventDefault();
owner.goTo(page);
owner.goTo(page === 'voices' ? lastVoiceView : page);
});
const toggleList = () => {
+3 -4
View File
@@ -21,12 +21,12 @@ export const PAGE_ICONS = {
combos: TOOL_ICONS.MATERIALS,
prompts: icon('<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>'),
voices: icon('<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/>'),
// Not a rail entry (Voice-over is a view of Voices); Home's voice-over card uses it.
// Not rail entries (Voice-over and Audio are views of Voices); Home's voice-over card uses this one.
script: icon('<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 9h8"/><path d="M8 13h5"/>'),
'audio-gallery': icon('<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>'),
};
/** Pages on the rail, top to bottom. `audio`: the page belongs to the audio domain. */
/** Pages on the rail, top to bottom: no more than the nine number keys. `audio`: the page belongs to the audio domain. */
export const RAIL_PAGES = Object.freeze([
{ page: 'home', id: 'anomalous-home-btn', labelKey: 'shellHome' },
{ page: 'activity', id: 'anomalous-activity-btn', labelKey: 'shellActivity' },
@@ -36,8 +36,7 @@ export const RAIL_PAGES = Object.freeze([
{ page: 'recipes', id: 'anomalous-notebook-btn', labelKey: 'recipeTitle' },
{ page: 'combos', id: 'anomalous-combos-btn', labelKey: 'shellCombos' },
{ page: 'prompts', id: 'anomalous-prompts-btn', labelKey: 'shellPrompts' }, // Prompt Studio as a page
{ page: 'voices', id: 'anomalous-voices-btn', labelKey: 'shellVoices', audio: true },
{ page: 'audio-gallery', id: 'anomalous-audio-gallery-btn', labelKey: 'shellAudioGallery', audio: true },
{ page: 'voices', id: 'anomalous-voices-btn', labelKey: 'shellVoices', audio: true }, // with Voice-over and Audio as its views
]);
const div = className => Object.assign(document.createElement('div'), { className });
+6 -5
View File
@@ -1,14 +1,15 @@
/**
* The Voices page's two views as one switch at the top of each: Characters (the cards,
* ui_audio_studio.js, shell page `voices`) and Voice-over (ui_script_page.js, shell page
* `script`). Both sit under the rail's Voices entry and share the character list.
* The Voices page's three views as one switch at the top of each: Characters (the cards,
* ui_audio_studio.js, shell page `voices`), Voice-over (ui_script_page.js, shell page
* `script`) and Audio (ui_audio_gallery.js, shell page `audio-gallery`). All sit under the
* rail's Voices entry and share the list column.
*/
import { t } from './interface_settings.js';
const TABS = [['voices', 'voiceTabCharacters'], ['script', 'scriptDirectorTitle']];
const TABS = [['voices', 'voiceTabCharacters'], ['script', 'scriptDirectorTitle'], ['audio-gallery', 'shellAudioGallery']];
/** The switch; `active`: 'voices' | 'script'. */
/** The switch; `active`: 'voices' | 'script' | 'audio-gallery'. */
export function renderVoiceTabs(owner, active) {
const bar = document.createElement('div');
bar.className = 'anomalous-voice-tabs';
+1 -1
View File
@@ -19,7 +19,7 @@ export const AUDIO_TOUR_STEPS = Object.freeze([
{ targetSelector: '.anomalous-character-voice-actions', icon: '🎭', titleKey: 'audioTourEditTitle', bodyKey: 'audioTourEditBody', position: 'bottom' },
{ targetSelector: '.anomalous-voice-copy-btn', icon: '🏷️', titleKey: 'audioTourTagTitle', bodyKey: 'audioTourTagBody', position: 'bottom' },
{ targetSelector: '[data-tour="voice-tab-script"]', icon: '📜', titleKey: 'audioTourScriptTitle', bodyKey: 'audioTourScriptBody', position: 'bottom' },
{ targetSelector: '#anomalous-audio-gallery-btn', icon: '🎧', titleKey: 'audioTourGalleryTitle', bodyKey: 'audioTourGalleryBody', position: 'right' },
{ targetSelector: '[data-tour="voice-tab-audio-gallery"]', icon: '🎧', titleKey: 'audioTourGalleryTitle', bodyKey: 'audioTourGalleryBody', position: 'bottom' },
{ targetSelector: '.anomalous-audio-sidebar-settings', icon: '⚙️', titleKey: 'audioTourSettingsTitle', bodyKey: 'audioTourSettingsBody', position: 'top' },
]);