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