Anomalous_TTS no longer takes a folder or file path on this computer from a request (ComfyUI-Manager review of the node): the storage place, other character folders and pretrained sources are set only in its settings file, and imported files are always uploaded. - GPT-SoVITS settings shows the storage place, other places and package sources without change, move or remove buttons, and names the node's settings file (status.settings_file) with a copy button and how to edit it. - The import window drops "from this computer": no server path picker (ui_tts_path_picker.js is gone), no local folder scan or local clip preview; a very large upload is confirmed instead. - tts_setup_api.js loses the removed routes; formatSize moves there. - The storage-change activity entry, unused locale keys and picker CSS go. Verified: tools/check_structure.mjs; tts_setup_api in Node with fetch stubbed; the settings card, batch hero, add menu, file sources and player rendered with jsdom and stubbed ComfyUI scripts. Not run in ComfyUI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018P2T2R7TFbJsc3C9maCD8B
93 lines
4.3 KiB
JavaScript
93 lines
4.3 KiB
JavaScript
import { t } from './interface_settings.js';
|
|
import { anomalousConfirm } from './ui_dialog.js';
|
|
import { formatSize, importKind } from './tts_setup_api.js';
|
|
import { DROP_FILE_LIMIT, sortOutFolders } from './tts_import_groups.js';
|
|
|
|
/**
|
|
* Where the import window's files come from: the browser's file and folder dialogs
|
|
* and drops, all uploaded to the node (it never reads a path on the computer).
|
|
* Folders of Python environments, base models and a GPT-SoVITS package's program
|
|
* and training runs are left out (tts_import_groups.js skipFolder), and the user is
|
|
* told which; a very large upload is confirmed first. Every source gives each file's
|
|
* folder starting with the folder that was chosen, so files are sorted the same
|
|
* whichever way they came in.
|
|
*/
|
|
|
|
const ACCEPT = { gpt: '.ckpt', sovits: '.pth', audio: '.wav,.flac,.ogg,.mp3', text: '.txt,.lab,.list' };
|
|
const ACCEPT_ALL = Object.values(ACCEPT).join(',');
|
|
export const UPLOAD_WARN_BYTES = 2 * 1024 ** 3;
|
|
export const UPLOAD_WARN_FILES = 1500;
|
|
|
|
/** `a、b、c…`: the first few names of a list, in name order. */
|
|
function few(names) {
|
|
const sorted = [...names].sort((a, b) => a.localeCompare(b));
|
|
return sorted.slice(0, 3).join('、') + (sorted.length > 3 ? '…' : '');
|
|
}
|
|
|
|
/**
|
|
* `add(items, into)` puts `{ name, dir, file }` items into the window;
|
|
* `note(text)` adds a line under the toolbar. Returns the hidden inputs to mount and
|
|
* the ways to bring files in.
|
|
*/
|
|
export function createFileSources({ signal, add, note }) {
|
|
let pickFor = null; // the draft the browser file dialog adds to, or null (sort by name)
|
|
|
|
/**
|
|
* Browser files, `{ file, dir }`, with `{ skipped: folder }` and `{ truncated: true }`
|
|
* markers from a drop (ui_tts_file_drop.js). Says what was left out, and asks before
|
|
* a very large upload.
|
|
*/
|
|
async function takeFiles(list, into = null) {
|
|
const skipped = list.filter(item => item.skipped).map(item => item.skipped);
|
|
const truncated = list.some(item => item.truncated);
|
|
const items = list.filter(item => item.file).map(item => ({ name: item.file.name, dir: item.dir || '', file: item.file }));
|
|
const usable = items.filter(item => importKind(item.name));
|
|
const bytes = usable.reduce((sum, item) => sum + item.file.size, 0);
|
|
if (bytes > UPLOAD_WARN_BYTES || usable.length > UPLOAD_WARN_FILES) {
|
|
const ok = await anomalousConfirm(t('ttsUploadLarge', { count: usable.length, size: formatSize(bytes) }), t('ttsUploadLargeTitle'));
|
|
if (signal.aborted || !ok) return;
|
|
}
|
|
if (items.length) add(items, into);
|
|
if (skipped.length) note(t('ttsSkippedFolders', { folders: few(skipped) }));
|
|
if (truncated) note(t('ttsBatchScanTruncated', { count: items.length }));
|
|
}
|
|
|
|
/** The browser's folder dialog lists every file: leave out the same folders a drop does. */
|
|
function takeFolder(files) {
|
|
const all = files.map(file => ({ file, name: file.name, dir: file.webkitRelativePath.split('/').slice(0, -1).join('/') }));
|
|
const { kept, skipped } = sortOutFolders(all);
|
|
const usable = kept.map(i => all[i]).filter(item => importKind(item.name));
|
|
const list = usable.slice(0, DROP_FILE_LIMIT).map(item => ({ file: item.file, dir: item.dir }));
|
|
takeFiles([...list, ...skipped.map(folder => ({ skipped: folder })), ...(usable.length > DROP_FILE_LIMIT ? [{ truncated: true }] : [])]);
|
|
}
|
|
|
|
const fileInput = document.createElement('input');
|
|
fileInput.type = 'file';
|
|
fileInput.multiple = true;
|
|
fileInput.hidden = true;
|
|
fileInput.onchange = () => {
|
|
takeFiles([...fileInput.files].map(file => ({ file, dir: '' })), pickFor);
|
|
fileInput.value = '';
|
|
};
|
|
const folderInput = document.createElement('input');
|
|
folderInput.type = 'file';
|
|
folderInput.webkitdirectory = true;
|
|
folderInput.hidden = true;
|
|
folderInput.onchange = () => {
|
|
takeFolder([...folderInput.files]);
|
|
folderInput.value = '';
|
|
};
|
|
const chooseFiles = (kinds = null, into = null) => {
|
|
pickFor = into;
|
|
fileInput.accept = kinds ? kinds.map(kind => ACCEPT[kind]).join(',') : ACCEPT_ALL;
|
|
fileInput.click();
|
|
};
|
|
|
|
return {
|
|
inputs: [fileInput, folderInput],
|
|
chooseFiles,
|
|
chooseFolder: () => folderInput.click(),
|
|
takeFiles,
|
|
};
|
|
}
|