Files
DemonGatanjieu-Anomalous_Mo…/tests/script_drafts.mjs
T
DemonGatanjieuandClaude Opus 5.5 0a315bf1f8 Voice-over: a drafts box
Every script is a draft, saved on its own in ComfyUI's user folder
(anomalous_model_browser/voice_over_drafts.json through /userdata), read
again and joined before each write so two tabs keep each other's drafts.
A bar over the script names it and leads to New, Drafts and Close; the box
shows a card per draft (open, rename, copy, delete) and Recently deleted
(30 days). A reload opens the draft that was open; a file voiced again
from the gallery becomes a draft of its own; the one localStorage draft
of the earlier build moves in once.

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

93 lines
5.4 KiB
JavaScript

// The voice-over drafts box without storage (web/modules/script_drafts.js): what a kept script
// may hold, the box's moves, two tabs' boxes joined, and the titles and times on its cards.
import assert from 'node:assert/strict';
import {
cleanBox, cleanScript, copyDraft, deleteDraft, deletedDrafts, draftSpeakers, draftTitle, dropOldDeleted, emptyBox,
hasLines, keepScript, liveDrafts, makeDraft, mergeBoxes, putDraft, renameDraft, restoreDraft, whenKey,
} from '../web/modules/script_drafts.js';
const DAY = 24 * 60 * 60 * 1000;
const script = (...texts) => ({ format: 1, lines: texts.map(text => ({ text, emotion: 'main', speaker: 'gpt_sovits:Aris' })), groupKey: 'gpt_sovits:Aris', script: {}, cast: {} });
// A kept script: blank lines and bad settings go, the right ones stay in a fixed order.
const cleaned = cleanScript({
format: 1,
groupKey: 'gpt_sovits:Aris',
lines: [{ text: ' ' }, { text: 'a', tune: { speed: 1.2, language: 'ja', odd: 1 } }, { text: 'b', speaker: 3 }, null],
script: { speed: 'fast', preset: 'stable' },
cast: { 'gpt_sovits:Aris': { language: 'ja' }, 'gpt_sovits:Momoi': { speed: '1' } },
});
assert.deepEqual(cleaned, {
format: 1,
lines: [{ text: 'a', emotion: 'main', speaker: null, tune: { language: 'ja', speed: 1.2 } }, { text: 'b', emotion: 'main', speaker: null }],
groupKey: 'gpt_sovits:Aris',
script: { preset: 'stable' },
cast: { 'gpt_sovits:Aris': { language: 'ja' } },
});
assert.equal(cleanScript({ format: 2, lines: [] }), null);
assert.equal(hasLines(cleanScript({ format: 1, lines: [{ text: ' ' }] })), false);
assert.deepEqual(cleanBox(null), emptyBox());
assert.deepEqual(cleanBox({ format: 1, current: 'x', drafts: [{ id: 'x', script: { format: 9 } }, { id: '', script: script('a') }] }), emptyBox());
// The box: new, renamed, copied, deleted, brought back; the last changed first.
const box = emptyBox();
const first = putDraft(box, makeDraft(script('おはよう、先生!今日もいい天気ですね'), 1000, { id: 'a' }));
const second = putDraft(box, makeDraft(script('b'), 2000, { id: 'b' }));
box.current = 'a';
assert.deepEqual(liveDrafts(box).map(draft => draft.id), ['b', 'a']);
keepScript(box, 'a', script('c'), 3000);
assert.deepEqual(liveDrafts(box).map(draft => draft.id), ['a', 'b']);
assert.equal(draftTitle(first), 'c');
renameDraft(box, 'a', ' 第 3 集开场 ', 3500);
assert.equal(draftTitle(first), '第 3 集开场');
assert.equal(draftTitle(makeDraft(script('おはよう、先生!今日もいい天気ですね。'))), 'おはよう、先生!今日もいい天気ですね…');
assert.equal(draftTitle(makeDraft(script())), '');
const copy = copyDraft(box, 'a', '第 3 集开场(副本)', 4000);
assert.equal(copy.name, '第 3 集开场(副本)');
copy.script.lines[0].text = 'changed';
assert.equal(first.script.lines[0].text, 'c', 'a copy is its own');
deleteDraft(box, 'a', 5000);
assert.equal(box.current, null, 'the open draft is let go when deleted');
assert.deepEqual(deletedDrafts(box).map(draft => draft.id), ['a']);
assert.equal(liveDrafts(box).some(draft => draft.id === 'a'), false);
restoreDraft(box, 'a', 6000);
assert.equal(first.deleted, null);
deleteDraft(box, 'b', 7000);
assert.equal(dropOldDeleted(box, 7000 + 29 * DAY), false);
assert.equal(dropOldDeleted(box, 7000 + 30 * DAY), true);
assert.equal(box.drafts.some(draft => draft.id === 'b'), false);
assert.equal(second.deleted, 7000);
// Who reads, in the order they first speak (a line without a speaker is the cast's pick).
assert.deepEqual(draftSpeakers({ groupKey: 'k:A', lines: [{ text: 'x', speaker: 'k:B' }, { text: 'y', speaker: null }, { text: 'z', speaker: 'k:B' }] }), ['k:B', 'k:A']);
// Two tabs: each draft as it was changed last; the draft open here stays this tab's.
const mine = { format: 1, current: 'm', drafts: [
makeDraft(script('mine'), 100, { id: 'm' }),
makeDraft(script('old here'), 100, { id: 's' }),
] };
const theirs = { format: 1, current: 'o', drafts: [
makeDraft(script('newer there'), 200, { id: 'm' }),
makeDraft(script('newer there'), 200, { id: 's' }),
makeDraft(script('only there'), 150, { id: 'o' }),
] };
const joined = mergeBoxes(mine, theirs);
const text = id => joined.drafts.find(draft => draft.id === id).script.lines[0].text;
assert.equal(joined.current, 'm');
assert.equal(text('m'), 'mine');
assert.equal(text('s'), 'newer there');
assert.equal(text('o'), 'only there');
const gone = { format: 1, current: null, drafts: [{ ...makeDraft(script('x'), 100, { id: 's' }), deleted: 300, updated: 300 }] };
assert.equal(mergeBoxes(gone, theirs).drafts.find(draft => draft.id === 's').deleted, 300, 'a deletion is a change');
// When: just now, minutes, today, yesterday, a date (with the year when it is another one).
const now = new Date(2026, 9, 20, 15, 30).getTime();
assert.deepEqual(whenKey(now - 20 * 1000, now), { key: 'draftsJustNow' });
assert.deepEqual(whenKey(now - 5 * 60000, now), { key: 'draftsMinutesAgo', values: { count: 5 } });
assert.deepEqual(whenKey(new Date(2026, 9, 20, 9, 5).getTime(), now), { key: 'draftsTodayAt', values: { time: '09:05' } });
assert.deepEqual(whenKey(new Date(2026, 9, 19, 23, 0).getTime(), now), { key: 'draftsYesterdayAt', values: { time: '23:00' } });
assert.deepEqual(whenKey(new Date(2026, 9, 18, 8, 0).getTime(), now), { key: 'draftsDate', values: { month: 10, day: 18, year: 2026 } });
assert.equal(whenKey(new Date(2025, 11, 31).getTime(), now).key, 'draftsDateYear');
console.log('script drafts: ok');