From cae7fd2a4a1ba41c0d2dfa0bda5e1cdead262fc9 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <207458273+DemonGatanjieu@users.noreply.github.com> Date: Wed, 28 Oct 2026 20:26:31 +0800 Subject: [PATCH] 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 --- ARCHITECTURE.md | 9 +++++---- CHANGELOG.md | 3 +++ tests/shell_labels.mjs | 5 +++++ tests/shell_open_rules.mjs | 8 ++++---- web/modules/ui_audio_gallery.js | 7 +++++-- web/modules/ui_shell_nav.js | 8 ++++---- web/modules/ui_shell_rail.js | 7 +++---- web/modules/ui_voice_tabs.js | 11 ++++++----- web/modules/update_guide_data.js | 2 +- 9 files changed, 36 insertions(+), 24 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index eba6280..95fcc74 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 83eac5e..0159879 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/tests/shell_labels.mjs b/tests/shell_labels.mjs index 21b6106..7f60be8 100644 --- a/tests/shell_labels.mjs +++ b/tests/shell_labels.mjs @@ -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'); diff --git a/tests/shell_open_rules.mjs b/tests/shell_open_rules.mjs index 74b63df..17a1843 100644 --- a/tests/shell_open_rules.mjs +++ b/tests/shell_open_rules.mjs @@ -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'); diff --git a/web/modules/ui_audio_gallery.js b/web/modules/ui_audio_gallery.js index 23e7706..0ac0f47 100644 --- a/web/modules/ui_audio_gallery.js +++ b/web/modules/ui_audio_gallery.js @@ -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), }); diff --git a/web/modules/ui_shell_nav.js b/web/modules/ui_shell_nav.js index e756536..2cc830e 100644 --- a/web/modules/ui_shell_nav.js +++ b/web/modules/ui_shell_nav.js @@ -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 = () => { diff --git a/web/modules/ui_shell_rail.js b/web/modules/ui_shell_rail.js index c0ef073..7bf783c 100644 --- a/web/modules/ui_shell_rail.js +++ b/web/modules/ui_shell_rail.js @@ -21,12 +21,12 @@ export const PAGE_ICONS = { combos: TOOL_ICONS.MATERIALS, prompts: icon(''), voices: icon(''), - // 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(''), 'audio-gallery': icon(''), }; -/** 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 }); diff --git a/web/modules/ui_voice_tabs.js b/web/modules/ui_voice_tabs.js index f86d0e1..868a123 100644 --- a/web/modules/ui_voice_tabs.js +++ b/web/modules/ui_voice_tabs.js @@ -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'; diff --git a/web/modules/update_guide_data.js b/web/modules/update_guide_data.js index ab72943..5fbacca 100644 --- a/web/modules/update_guide_data.js +++ b/web/modules/update_guide_data.js @@ -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' }, ]);