feat: release Find-Word v0.1
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// @vitest-environment node
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { INTRO, PATTERNS, WORDS } from "../src/content";
|
||||
|
||||
// 생성 바이너리를 눈으로 검토할 수 없으므로 문장·해시·PCM 포맷·실제 신호를
|
||||
// 함께 확인한다. manifest는 생성기가 남기며 100개 조합과 안내를 빠짐없이 묶는다.
|
||||
describe("저장한 한국어 음성 자산", () => {
|
||||
it("실제 생성 도구 버전을 기록하여 재생성의 출처를 추적한다", () => {
|
||||
const manifest = JSON.parse(
|
||||
readFileSync("public/audio/manifest.json", "utf8"),
|
||||
) as { toolVersions?: { espeak: string; ffmpeg: string } };
|
||||
expect(manifest.toolVersions?.espeak).toMatch(/eSpeak NG.*\d+\.\d+/);
|
||||
expect(manifest.toolVersions?.ffmpeg).toMatch(/ffmpeg version \d+\.\d+/);
|
||||
});
|
||||
it("모든 문장과 안내에 대응하는 로컬 WAV·출처·해시를 제공한다", () => {
|
||||
const manifest = JSON.parse(
|
||||
readFileSync("public/audio/manifest.json", "utf8"),
|
||||
) as {
|
||||
generator: string;
|
||||
files: { file: string; text: string; sha256: string }[];
|
||||
};
|
||||
expect(manifest.generator).toContain("eSpeak NG");
|
||||
const expected = [
|
||||
{ file: "intro.wav", text: INTRO },
|
||||
...PATTERNS.flatMap((p) =>
|
||||
WORDS.map((w) => ({
|
||||
file: `${p.id}-${w.id}.wav`,
|
||||
text: p.sentence
|
||||
.replaceAll("{word}", w.answer)
|
||||
.replaceAll("{distractor1}", w.distractors[0]!.word)
|
||||
.replaceAll("{distractor2}", w.distractors[1]!.word),
|
||||
})),
|
||||
),
|
||||
];
|
||||
expect(manifest.files.map(({ file, text }) => ({ file, text }))).toEqual(
|
||||
expected,
|
||||
);
|
||||
expect(
|
||||
readdirSync("public/audio").filter((f) => f.endsWith(".wav")),
|
||||
).toHaveLength(101);
|
||||
let size = 0;
|
||||
for (const entry of manifest.files) {
|
||||
const file = readFileSync(`public/audio/${entry.file}`);
|
||||
size += file.length;
|
||||
expect(createHash("sha256").update(file).digest("hex")).toBe(
|
||||
entry.sha256,
|
||||
);
|
||||
expect(file.toString("ascii", 0, 4)).toBe("RIFF");
|
||||
expect(file.toString("ascii", 8, 12)).toBe("WAVE");
|
||||
expect(file.readUInt16LE(22)).toBe(1);
|
||||
expect(file.readUInt32LE(24)).toBe(16000);
|
||||
expect(file.readUInt16LE(34)).toBe(16);
|
||||
expect(file.length).toBeGreaterThan(16000);
|
||||
const values = Array.from({ length: (file.length - 44) / 2 }, (_, i) =>
|
||||
file.readInt16LE(44 + i * 2),
|
||||
);
|
||||
const rms = Math.sqrt(
|
||||
values.reduce((sum, x) => sum + x * x, 0) / values.length,
|
||||
);
|
||||
expect(rms).toBeGreaterThan(100);
|
||||
// RMS 정규화 후 최고 난이도 아닌 가장 큰 +6dB에서도 클리핑 여유를 확인한다.
|
||||
expect(
|
||||
(values.reduce((peak, x) => Math.max(peak, Math.abs(x)), 0) / rms) *
|
||||
0.03 *
|
||||
2,
|
||||
).toBeLessThan(0.8);
|
||||
}
|
||||
expect(size).toBeLessThan(20 * 1024 * 1024);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user