Files
DemonGatanjieu-Anomalous_Mo…/web/modules/browser.js
T
DemonGatanjieuandClaude Opus 5.5 89fa1a93bf Voice-over: several characters in one script
Pick lines on Voice-over (click; Ctrl / Cmd adds, Shift a run; Esc lets
go) and click a character in the list on the left to have it read them;
with none picked, the click changes who reads by default. On Voice-over
the list is the cast: narrower, characters with their colours, and a
click never leaves the page. Cards show who reads each line in its
colour, with that character's emotions; the bundle carries [name] where
the speaker changes, so generating, the cards' listen button, pushing,
dragging and copying follow it, and a file voiced again from the audio
gallery keeps its speakers. The character cards' Voice-over button is
gone. The workflow actions move to ui_script_workflow.js and the cast
helpers to ui_script_cast.js, keeping the director under 600 lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 22:53:03 +08:00

238 lines
12 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, fillNotebookGalleries } 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).
*/
show({ followSelection = false } = {}) {
followComfyLanguage();
if (this._idleReleaseTimer) {
clearTimeout(this._idleReleaseTimer);
this._idleReleaseTimer = null;
}
const page = followSelection && followsSelection() ? pageForNode(selectedCanvasNode(), NODE_KINDS) : null;
this.setTriggerVisible(false);
this.modal.classList.add('visible');
if (page === 'prompts') {
this.goTo('prompts');
return;
}
if (page === 'assistant') {
this.openCurrentNode();
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 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 character reads the picked lines, or by default.
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.fillNotebookGalleries = fillNotebookGalleries;
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;