Hitokoto / tests /test_reader.cjs
vomebook's picture
fix(tts): read list numbers as enumerations, not separate sentences
8c72c22
Raw History Blame Contribute Delete
4.49 kB
"use strict";
const assert = require("node:assert/strict");
require("../static/reader.js");
const { split, clipPlan, locations, readBookmark, bookmarkKey } = globalThis.ReaderPlan;
for (const text of ["这是一篇长文。".repeat(200), "甲".repeat(598) + "2026年3.1415版本CPUModel。".repeat(60),
"𠮷长文与😀测试。".repeat(200), "甲".repeat(20000)]) {
const plan = split(text);
assert.equal(plan.map((part) => part.text).join(""), text);
let offset = 0;
for (const part of plan) {
assert.equal(part.start, offset);
assert.equal(text.slice(part.start, part.end), part.text);
assert.ok(Array.from(part.text).length <= 600);
assert.ok(Array.from(part.context_before).length <= 32);
assert.ok(Array.from(part.context_after).length <= 32);
assert.ok(text.slice(0, part.start).endsWith(part.context_before));
assert.ok(text.slice(part.end).startsWith(part.context_after));
offset = part.end;
}
assert.equal(offset, text.length);
}
assert.equal(split("甲".repeat(598) + "2026年。", 600, 600)[0].text, "甲".repeat(598));
assert.throws(() => split("a".repeat(601)));
assert.equal(split("a".repeat(200))[0].text, "a".repeat(200));
assert.throws(() => split("甲".repeat(20001)));
assert.throws(() => split(" "));
const good = { version: 1, text: "长文😀", voice: "zf_001", speed: 1, offset: 2, seconds: 1.3 };
const storage = { getItem: (key) => key === bookmarkKey ? JSON.stringify(good) : null };
assert.deepEqual(readBookmark(storage), good);
for (const changes of [{ seconds: -1 }, { offset: 100 }, { speed: 9 }, { voice: "unknown" }, { playbackRate: 8 },
{ pitchSemitones: 4 }, { pitchSemitones: 1.5 }, { pitchSemitones: "2" }, { pitchSemitones: true }, { text: "甲".repeat(20001) }]) {
assert.equal(readBookmark({ getItem: () => JSON.stringify({ ...good, ...changes }) }), null);
}
assert.equal(readBookmark({ getItem: () => "malformed" }), null);
assert.equal(readBookmark({ getItem: () => JSON.stringify({ ...good, pitchSemitones: -3 }) }).pitchSemitones, -3);
const sources = ["第一章\n\n正文第一句。正文第二句。\n1、第一项\n2、第二项\n", "甲\n".repeat(400),
"甲".repeat(58) + "2026年3.1415版本CPUModel。", "𠀀😀正文。".repeat(50), "正文。" + "\n".repeat(100),
"\n".repeat(100) + "正文。", "甲".repeat(800), "校长文章写得很好。".repeat(40),
"前言。\n1.马克思主义的基本原理是什么。\n2.为什么要学习。\n(3).第三条。", "目录:1.总论,2.分论。" + "甲".repeat(70)];
for (const token of ["2026-10-18", "版本10.02.3", "编号00123", "电话138-0013-8000", "2026年10月18日", "1,234.50元", "3/4"]) {
sources.push("甲".repeat(58) + token + "。后面的长文。");
const text = "甲".repeat(178) + token + "。";
const plan = split(text);
for (const atom of ReaderPlan.numberSpans(text)) assert.ok(plan.some((part) => part.start <= atom.start && part.end >= atom.end));
}
const { spawnSync } = require("node:child_process");
const python = spawnSync("python3", ["-B", "-c", "import json,sys;from tts.text import playback_segments,utf16_offset;print(json.dumps([[{'start':utf16_offset(t,a),'end':utf16_offset(t,b)} for a,b,_ in playback_segments(t)] for t in json.load(sys.stdin)]))"],
{ cwd: require("node:path").resolve(__dirname, ".."), input: JSON.stringify(sources), encoding: "utf8" });
assert.equal(python.status, 0, python.stderr);
assert.deepEqual(sources.map(clipPlan), JSON.parse(python.stdout));
for (const text of sources) {
const list = locations(split(text));
assert.equal(list.map((part) => text.slice(part.start, part.end)).join(""), text);
assert.ok(list.every((part, index) => part.start === (index ? list[index - 1].end : 0)));
}
console.log("Reader: lossless Unicode batches, source navigation/Python parity, context and opt-in bookmarks PASS");
let now = 0;
const sleep = new ReaderPlan.SleepTimer(() => now);
sleep.arm("5");
assert.equal(sleep.remaining(), 300);
now = 299999; assert.equal(sleep.due(), false);
now = 300000; assert.equal(sleep.due(), true);
sleep.arm("clip", "0:1");
assert.equal(sleep.ended("0:0"), false);
assert.equal(sleep.ended("0:1"), true);
sleep.arm("clip"); sleep.bind("1:2"); sleep.bind("1:3");
assert.equal(sleep.ended("1:2"), true);
sleep.clear(); assert.equal(sleep.due(), false); assert.equal(sleep.ended("1:2"), false);
assert.throws(() => sleep.arm("0.01"));
console.log("Sleep timer: wall-clock deadlines, current-clip binding and clearing PASS");