Files
DemonGatanjieu-Anomalous_Mo…/web/modules/browser.js
T
DemonGatanjieuandClaude Opus 5.5 09ae7c6b33 Voice-over: pick on the left, change it on the right; settings per character and per line
The side panel follows what is picked: the whole script, one character in
it (its language, speed and mode for this script), or the picked lines (who
reads them, and their own values, each following the character until set).
A line set for itself carries a tag on its card. The values reach
Anomalous_TTS interface 17 as [lang:…][speed:…][preset:…] on each line;
an older node reads the whole script as before. Clicking a character in the
cast now shows it instead of giving it the picked lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:28:22 +08:00

261 lines
13 KiB
JavaScript

import { app } from "../../../scripts/app.js";
import { showDetail } from './ui_detail.js';
import { showEditModal } from './ui_model_editor.js';
import { setWidgetValuePath } from './model_widget_path.js';
import { loadModels, applyModelToCanvas, stopMediaInContainer } from './ui_grid.js';
import { createDOM, renderSidebar, loadFolders } from './ui_sidebar.js';
import { closeWorkspace, hideAllPanels } from './ui_browser_navigation.js';
import { triggerDirectModelScan } from './scan_runner.js';
import { openScanPage, renderScanPage, leaveScanPage } from './ui_scan_page.js';
import { loadGalleryImages, refreshGalleryImages, showGeneratedGallery, showGallerySelectMode, showGalleryViewer } from './ui_gallery.js';
import { showNotebooks, openCombo, openComboByFilename, refreshNotebooks, saveCurrentNotebook, deleteCurrentNotebook } from './ui_notebooks.js';
import { renderNotebookEditor } from './ui_notebook_editor.js';
import { sendNotebookToCanvas } from './notebook_canvas.js';
import { showRecipes, refreshRecipes } from './ui_recipe_catalog.js';
import { renderRecipeList, handleSaveRecipe, openRecipeByFilename } from './ui_recipes.js';
import { leavePromptPage, openPromptStudio } from './ui_prompt_composer.js';
import { closeUpdateGuide } from './ui_update_guide.js';
import { showImageWorkbench } from './ui_gallery_detail.js';
import { openDoctorPage } from './ui_doctor.js';
import { showModelSources } from './ui_model_sources.js';
import { initAssistantPanel, renderAssistantModelCard, _loadAssistantHistory, diagnoseNode, openCurrentNode, openLoraInsertionPicker } from './ui_node_assistant.js';
import { _openGalleryReplacer, findModelComboWidget } from './ui_node_model_picker.js';
import { promptBoxes } from './prompt_boxes.js';
import { followsSelection, pageForNode } from './shell_open_rules.js';
import { renderAudioStudio, stopAudioStudioPlayback } from './ui_audio_studio.js';
import { renderAudioGallery, stopGalleryAudio } from './ui_audio_gallery.js';
import { renderScriptPage } from './ui_script_page.js';
import { castScriptCharacter } from './ui_script_director.js';
import { getActiveAudioFilter, setActiveAudioFilter, syncAudioSidebarSelection } from './ui_audio_sidebar.js';
import { voiceGroupKey } from './audio_engines.js';
import { getActiveDomain } from './ui_domain_switcher.js';
import { startPage } from './ui_shell_nav.js';
import { followComfyLanguage } from './interface_settings.js';
// What a selected node holds, for the page opening the browser shows (shell_open_rules.js).
const NODE_KINDS = {
takesPrompt: node => promptBoxes(node).length > 0,
hasModel: node => Boolean(findModelComboWidget(node)),
};
const selectedCanvasNode = () => Object.values(app.canvas?.selected_nodes || {})[0] || null;
export class AnomalousBrowser {
constructor() {
this.modal = null;
this.sidebar = null;
this.grid = null;
this.detailPanel = null;
this.currentType = 'loras';
this.currentPathIdx = 0;
this.currentSubfolder = '/';
this.foldersData = null;
this.collapsedFolders = new Set(); // folders folded in the folder column (type + path), this session
// Video covers play on hover unless you chose "always": fewer videos decoding at once.
this.energySaving = localStorage.getItem('anomalous_energy_saving') !== 'false';
this.cardThumbnailMode = localStorage.getItem('anomalous_card_thumbnail_mode') === 'original'
? 'original'
: 'balanced';
this.entryMode = 'floating';
this.createDOM();
}
/**
* Opens the browser. `followSelection`: opened by the user (button, shortcut, menu), so a
* node selected on the canvas picks the page: a text node opens the Prompts page on it, any
* other node Current node (Settings -> Opening and window can turn this off). `node`: that
* page for this node whatever the setting; `page`: this rail page (or 'assistant').
*/
show({ followSelection = false, node = null, page: wanted = null } = {}) {
followComfyLanguage();
if (this._idleReleaseTimer) {
clearTimeout(this._idleReleaseTimer);
this._idleReleaseTimer = null;
}
const followed = followSelection && followsSelection() ? selectedCanvasNode() : null;
const page = wanted || pageForNode(node || followed, NODE_KINDS);
this.setTriggerVisible(false);
this.modal.classList.add('visible');
if (page === 'assistant') {
this.openCurrentNode();
return;
}
if (page) {
this.goTo(page);
return;
}
if (!this.currentShellPage()) {
this.goTo(startPage());
return;
}
// Opened again: stay on the page, refresh what may have changed meanwhile.
if (getActiveDomain() === 'audio') {
const audioTabs = { 'audio-gallery': 'gallery', script: 'script' };
this.switchAudioTab(audioTabs[this.currentShellPage()] || 'presets');
} else if (!this.foldersData) {
this.loadFolders();
} else {
this.loadModels();
}
if (this.currentShellPage() === 'gallery') void this.refreshGalleryImages();
if (this.currentShellPage() === 'prompts') this.goTo('prompts'); // its studio stopped when the browser closed
if (this.currentShellPage() === 'scan') void renderScanPage(this, this.scanPanel);
}
/**
* The canvas menu's "Open in AMB": selects `node` and opens its page; `section:
* 'parameters'` opens Current node at the node's saved values.
*/
showForNode(node, { section = '' } = {}) {
app.canvas?.selectNode?.(node, false);
const parameters = section === 'parameters';
this.show({ node, page: parameters ? 'assistant' : null });
if (parameters) this.assistantPanel?.querySelector('.anomalous-node-params')?.scrollIntoView({ block: 'start' });
}
/** The details of `model` (GET /anomalous/find_model's: with its type, path_idx and subfolder). */
openModelDetail(model) {
this.currentType = model.type || this.currentType;
this.currentPathIdx = model.path_idx ?? 0;
this.currentSubfolder = model.subfolder || '/';
this.show({ page: 'models' });
this.historyStack = [];
this.showDetail(model);
if (this.foldersData) this.renderSidebar();
}
/** The image / audio switch: the first page of that side. */
handleDomainChange(domain) {
this.goTo(domain === 'audio' ? 'voices' : 'models');
}
/** Single entry for audio-domain navigation: the rail, the audio list's entries and the domain switch. */
switchAudioTab(tabName, filter = null) {
if (!(tabName === 'presets' && filter?.type === 'group' && this.currentShellPage() === 'script')) this.hideAllPanels();
if (tabName === 'gallery') {
setActiveAudioFilter({ type: 'gallery', value: null });
this.markShellPage?.('audio-gallery');
this.audioGalleryPanel.style.display = 'block';
renderAudioGallery(this.audioGalleryPanel, { owner: this });
} else if (tabName === 'script') {
this.markShellPage?.('script');
this.scriptPanel.style.display = 'flex';
const pending = this.pendingScript || {};
this.pendingScript = null;
renderScriptPage(this.scriptPanel, this, pending);
} else if (filter?.type === 'group' && this.currentShellPage() === 'script') {
// On Voice-over the list is the cast: the side panel shows the character in this script.
castScriptCharacter(filter.value);
return;
} else {
if (filter) setActiveAudioFilter(filter);
else if (getActiveAudioFilter().type === 'gallery') setActiveAudioFilter(null);
this.markShellPage?.('voices');
this.audioStudioPanel.style.display = 'block';
renderAudioStudio(this.audioStudioPanel, { owner: this });
}
syncAudioSidebarSelection(this);
}
/** One character's voice card: the Anomalous_TTS node's "import or edit characters". */
openVoice(character) {
const filter = character ? { type: 'group', value: voiceGroupKey(character), character } : null;
if (this.currentShellPage() === 'voices') {
this.switchAudioTab('presets', filter);
return;
}
if (filter) setActiveAudioFilter(filter);
this.goTo('voices'); // keeps the filter just set
}
/**
* The Voices page's Voice-over view. `group`: a voice group key to choose (a card, the list);
* `script`: `{ speech, subfolder }` of a generated file to voice again (the audio gallery).
*/
openScript(group = null, script = null) {
this.pendingScript = { character: group, script };
if (this.currentShellPage() === 'script') this.switchAudioTab('script');
else this.goTo('script');
}
close() {
this.flushCanvasActivity?.();
leavePromptPage();
closeUpdateGuide(this);
leaveScanPage(); // a running scan's progress floats over the canvas
this.modal.classList.remove('visible');
this.setTriggerVisible(true);
stopAudioStudioPlayback();
stopGalleryAudio();
const canvas = document.getElementById('graph-canvas');
if (canvas instanceof HTMLElement) canvas.focus({ preventScroll: true });
if (this._modelLoadController) this._modelLoadController.abort();
if (this._modelMediaObserver) this._modelMediaObserver.disconnect();
this.modal.querySelectorAll('video, audio').forEach(media => media.pause());
this.stopMediaInContainer(this.grid);
if (this._idleReleaseTimer) clearTimeout(this._idleReleaseTimer);
this._idleReleaseTimer = setTimeout(() => {
if (this.modal.classList.contains('visible')) return;
this.stopMediaInContainer(this.grid);
this.grid.replaceChildren();
this.models = [];
// The output gallery reloads its first page when opened again.
this.galleryGrid.querySelectorAll('.anomalous-gallery-card').forEach(card => card.remove());
this.galleryImagesList = [];
}, 90000);
}
setTriggerVisible(visible) {
const trigger = this.triggerButton || document.getElementById('anomalous-trigger-btn');
trigger?.classList.toggle('anomalous-trigger-hidden', !visible || this.entryMode !== 'floating');
}
}
AnomalousBrowser.prototype.openDoctorPage = function () { openDoctorPage(this); };
AnomalousBrowser.prototype.showModelSources = function (scope) { showModelSources(this, scope); };
AnomalousBrowser.prototype.diagnoseNode = diagnoseNode;
AnomalousBrowser.prototype.openCurrentNode = function () { openCurrentNode(this); };
AnomalousBrowser.prototype.initAssistantPanel = initAssistantPanel;
AnomalousBrowser.prototype.renderAssistantModelCard = renderAssistantModelCard;
AnomalousBrowser.prototype._loadAssistantHistory = _loadAssistantHistory;
AnomalousBrowser.prototype._openGalleryReplacer = _openGalleryReplacer;
AnomalousBrowser.prototype.openLoraInsertionPicker = openLoraInsertionPicker;
AnomalousBrowser.prototype.showNotebooks = showNotebooks;
AnomalousBrowser.prototype.openCombo = openCombo;
AnomalousBrowser.prototype.openComboByFilename = openComboByFilename;
AnomalousBrowser.prototype.closeWorkspace = closeWorkspace;
AnomalousBrowser.prototype.refreshNotebooks = refreshNotebooks;
AnomalousBrowser.prototype.saveCurrentNotebook = saveCurrentNotebook;
AnomalousBrowser.prototype.deleteCurrentNotebook = deleteCurrentNotebook;
AnomalousBrowser.prototype.renderNotebookEditor = renderNotebookEditor;
AnomalousBrowser.prototype.sendNotebookToCanvas = sendNotebookToCanvas;
AnomalousBrowser.prototype.showRecipes = showRecipes;
AnomalousBrowser.prototype.openRecipeByFilename = openRecipeByFilename;
AnomalousBrowser.prototype.refreshRecipes = refreshRecipes;
AnomalousBrowser.prototype.renderRecipeList = renderRecipeList;
AnomalousBrowser.prototype.handleSaveRecipe = handleSaveRecipe;
AnomalousBrowser.prototype.openPromptStudio = openPromptStudio;
AnomalousBrowser.prototype.loadGalleryImages = loadGalleryImages;
AnomalousBrowser.prototype.refreshGalleryImages = refreshGalleryImages;
AnomalousBrowser.prototype.showGeneratedGallery = showGeneratedGallery;
AnomalousBrowser.prototype.showGallerySelectMode = showGallerySelectMode;
AnomalousBrowser.prototype.showGalleryViewer = showGalleryViewer;
AnomalousBrowser.prototype.showImageWorkbench = showImageWorkbench;
AnomalousBrowser.prototype.createDOM = createDOM;
AnomalousBrowser.prototype.openScanPage = function (view) { openScanPage(this, view); };
AnomalousBrowser.prototype.scanSingleModel = triggerDirectModelScan;
AnomalousBrowser.prototype.renderSidebar = renderSidebar;
AnomalousBrowser.prototype.loadFolders = loadFolders;
AnomalousBrowser.prototype.hideAllPanels = hideAllPanels;
AnomalousBrowser.prototype.loadModels = loadModels;
AnomalousBrowser.prototype.applyModelToCanvas = applyModelToCanvas;
AnomalousBrowser.prototype.stopMediaInContainer = stopMediaInContainer;
AnomalousBrowser.prototype.showDetail = showDetail;
AnomalousBrowser.prototype.showEditModal = showEditModal;
AnomalousBrowser.prototype.setWidgetValuePath = setWidgetValuePath;