// @vitest-environment node import { createHash } from "node:crypto"; import { readFileSync, readdirSync } from "node:fs"; import { describe, expect, it } from "vitest"; import { INTRO, PATTERNS, WORDS, renderSentence } from "../src/content"; const manifest = JSON.parse( readFileSync("public/audio/manifest.json", "utf8"), ) as { version: string; provider: string; voice: string; rate: string; pitch: string; sampleRate: number; toolVersions: { "edge-tts"?: string; ffmpeg?: string }; files: { file: string; text: string; sha256: string; bytes: number; duration: number; }[]; }; const expected = [ { file: "intro.wav", text: INTRO }, ...PATTERNS.flatMap((p) => WORDS.map((w) => ({ file: `${p.id}-${w.id}.wav`, text: renderSentence(p, w), })), ), ]; // 생성기의 검증 함수를 재사용하지 않고 실제 WAV 바이트를 직접 읽는다. // 문장·해시·헤더·신호를 묶어 manifest만 갱신하는 거짓 업그레이드를 막는다. describe("저장한 한국어 neural 음성 자산", () => { it("0.2.3 neural provider·voice·prosody와 실측 도구 버전을 기록한다", () => { expect.soft(manifest.version).toBe("0.2.3"); expect.soft(manifest.provider).toBe("Microsoft Edge online TTS"); expect.soft(manifest.voice).toBe("ko-KR-HyunsuMultilingualNeural"); expect.soft(manifest.rate).toBe("-6%"); expect.soft(manifest.pitch).toBe("-4Hz"); expect.soft(manifest.sampleRate).toBe(24000); expect.soft(manifest.toolVersions["edge-tts"]).toBe("7.3.1"); expect .soft(manifest.toolVersions.ffmpeg) .toMatch(/^ffmpeg version \d+\.\d+/); expect(JSON.stringify(manifest)).not.toMatch( /espeak|cookie|authorization|trustedclienttoken/i, ); }); it("101개 전체 원문·SHA-256·bytes·duration과 24kHz mono PCM16 신호를 검증한다", () => { expect(manifest.files.map(({ file, text }) => ({ file, text }))).toEqual( expected, ); expect(readdirSync("public/audio").sort()).toEqual( [...expected.map((e) => e.file), "manifest.json"].sort(), ); 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.length).toBe(entry.bytes); expect(file.toString("ascii", 0, 4)).toBe("RIFF"); expect(file.toString("ascii", 8, 12)).toBe("WAVE"); expect(file.toString("ascii", 12, 16)).toBe("fmt "); expect(file.readUInt32LE(16)).toBe(16); expect(file.readUInt16LE(20)).toBe(1); expect(file.readUInt16LE(22)).toBe(1); expect.soft(file.readUInt32LE(24), entry.file).toBe(24000); expect(file.readUInt16LE(34)).toBe(16); expect(file.toString("ascii", 36, 40)).toBe("data"); expect(file.readUInt32LE(40)).toBe(file.length - 44); const duration = (file.length - 44) / 2 / file.readUInt32LE(24); expect.soft(entry.duration, entry.file).toBe(duration); expect(duration).toBeGreaterThanOrEqual(1); expect(duration).toBeLessThanOrEqual(20); let sum = 0, peak = 0; for (let offset = 44; offset < file.length; offset += 2) { const value = file.readInt16LE(offset); sum += value * value; peak = Math.max(peak, Math.abs(value)); } const rms = Math.sqrt(sum / ((file.length - 44) / 2)); expect(rms).toBeGreaterThan(100); expect(peak).toBeLessThan(32767); // 런타임 최대 +6dB와 RMS 정규화에서도 기존 클리핑 여유를 보존한다. expect((peak / rms) * 0.03 * 2).toBeLessThan(0.8); } expect(size).toBeLessThan(48 * 1024 * 1024); }); });