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' }, ]);