503 lines
18 KiB
Plaintext
503 lines
18 KiB
Plaintext
// @vitest-environment node
|
||
import { createHash } from "node:crypto";
|
||
import { readFileSync, readdirSync } from "node:fs";
|
||
import { expect, it } from "vitest";
|
||
import { ENVIRONMENTS } from "../src/content";
|
||
|
||
// 과거 ASR/보존 검사는 해당 버전의 정확한 바이트를 읽는다. 새 음원의 인수로 재사용하지 않는다.
|
||
const readHistoricalBytes = (path: string) =>
|
||
readFileSync(
|
||
path.startsWith("public/ambience/")
|
||
? path.replace(
|
||
"public/ambience/",
|
||
".agent/evidence-v0.2.5/baseline/ambience/",
|
||
)
|
||
: path,
|
||
);
|
||
|
||
// 생성기 자기 보고 대신 배포 바이트의 형식·파형·권리 추적 정보를 독립 측정한다.
|
||
it("여덟 현장 녹음은 고유 PCM16 WAV·짧은 fade·완전한 provenance/notice를 가진다", () => {
|
||
const manifest = JSON.parse(
|
||
readFileSync("public/ambience/manifest.json", "utf8"),
|
||
);
|
||
expect(manifest.version).toBe("0.2.5");
|
||
expect(
|
||
manifest.files.map((f: { environmentId: string }) => f.environmentId),
|
||
).toEqual(ENVIRONMENTS.map((e) => e.id));
|
||
expect(readdirSync("public/ambience").sort()).toEqual(
|
||
[
|
||
...ENVIRONMENTS.map((e) => `${e.id}.wav`),
|
||
"manifest.json",
|
||
"AMBIENCE_NOTICES.md",
|
||
].sort(),
|
||
);
|
||
const notice = readFileSync("public/ambience/AMBIENCE_NOTICES.md", "utf8");
|
||
const hashes = new Set();
|
||
for (const entry of manifest.files) {
|
||
for (const key of [
|
||
"displayName",
|
||
"pageUrl",
|
||
"directUrl",
|
||
"creator",
|
||
"license",
|
||
"licenseUrl",
|
||
"attribution",
|
||
"conversion",
|
||
"sourceSha256",
|
||
"sha256",
|
||
"bytes",
|
||
"duration",
|
||
"rms",
|
||
"peak",
|
||
])
|
||
expect(entry[key], key).toBeTruthy();
|
||
expect(entry.sourceSha256).toMatch(/^[a-f0-9]{64}$/);
|
||
expect(entry.license).toMatch(/CC0|Public domain|CC BY(?:-SA)? 4\.0/);
|
||
const section = notice
|
||
.split("## ")
|
||
.find((s: string) =>
|
||
s.startsWith(`${entry.displayName} — ${entry.environmentId}\n`),
|
||
)!;
|
||
expect(section).toContain(entry.attribution);
|
||
for (const line of [
|
||
`Creator: ${entry.creator}`,
|
||
`Source: ${entry.pageUrl}`,
|
||
`Download: ${entry.directUrl}`,
|
||
`License: ${entry.license} — ${entry.licenseUrl}`,
|
||
`Output license: ${entry.outputLicense}`,
|
||
`Changes: ${entry.conversion.operations}`,
|
||
])
|
||
expect(section).toContain(line);
|
||
expect(notice).toContain(entry.licenseUrl);
|
||
expect(notice).toContain(entry.pageUrl);
|
||
const bytes = readFileSync(`public/ambience/${entry.file}`);
|
||
const hash = createHash("sha256").update(bytes).digest("hex");
|
||
hashes.add(hash);
|
||
expect(hash).toBe(entry.sha256);
|
||
expect(bytes.length).toBe(entry.bytes);
|
||
expect(bytes.toString("ascii", 0, 4)).toBe("RIFF");
|
||
expect(bytes.toString("ascii", 8, 12)).toBe("WAVE");
|
||
expect(bytes.readUInt16LE(20)).toBe(1);
|
||
expect(bytes.readUInt16LE(22)).toBe(1);
|
||
expect(bytes.readUInt32LE(24)).toBe(24000);
|
||
expect(bytes.readUInt16LE(34)).toBe(16);
|
||
expect(bytes.readUInt32LE(40)).toBe(bytes.length - 44);
|
||
const samples = Array.from(
|
||
{ length: (bytes.length - 44) / 2 },
|
||
(_, i) => bytes.readInt16LE(44 + i * 2) / 32768,
|
||
);
|
||
const rms = Math.sqrt(
|
||
samples.reduce((sum, n) => sum + n * n, 0) / samples.length,
|
||
);
|
||
const peak = samples.reduce((max, n) => Math.max(max, Math.abs(n)), 0);
|
||
expect(rms).toBeCloseTo(entry.rms, 10);
|
||
expect(peak).toBeCloseTo(entry.peak, 10);
|
||
expect(rms).toBeGreaterThan(0.003);
|
||
expect(peak).toBeLessThanOrEqual(0.85);
|
||
expect(samples[0]).toBe(0);
|
||
expect(samples.at(-1)).toBe(0);
|
||
expect(entry.duration).toBe(samples.length / 24000);
|
||
expect(entry.duration).toBeGreaterThanOrEqual(8);
|
||
expect(entry.duration).toBeLessThanOrEqual(20);
|
||
expect(entry.conversion.fadeSeconds).toBeGreaterThanOrEqual(0.025);
|
||
expect(entry.conversion.startSeconds).toBeGreaterThanOrEqual(0);
|
||
}
|
||
expect(hashes.size).toBe(8);
|
||
});
|
||
|
||
it("원본 계획은 임시 경로 대신 검증된 해시·바이트·직접 미디어 길이를 고정한다", () => {
|
||
const plan = JSON.parse(
|
||
readFileSync(".agent/AMBIENCE-SOURCES-v0.2.3.json", "utf8"),
|
||
);
|
||
const manifest = JSON.parse(
|
||
readFileSync("public/ambience/manifest.json", "utf8"),
|
||
);
|
||
const evidence = JSON.parse(
|
||
readFileSync(".agent/evidence-v0.2.3/source-integrity.json", "utf8"),
|
||
);
|
||
for (const [index, asset] of plan.assets.entries()) {
|
||
const original = evidence.find(
|
||
(e: { file: string }) => e.file.split(".")[0] === asset.environmentId,
|
||
);
|
||
expect.soft(asset).not.toHaveProperty("verifiedLocalSource");
|
||
expect.soft(asset).not.toHaveProperty("sourceDurationSeconds");
|
||
expect.soft(asset.expectedSourceSha256).toBe(original.sha256);
|
||
expect.soft(asset.sourceBytes).toBe(original.bytes);
|
||
expect
|
||
.soft(asset.directMediaDurationSeconds)
|
||
.toBe(Number(original.probe.format.duration));
|
||
expect.soft(asset.sourceVerification).toEqual({
|
||
status: "verified",
|
||
dateUtc: "2026-10-14",
|
||
method:
|
||
"SHA-256, byte count, ffprobe format.duration, full FFmpeg decode",
|
||
evidence: ".agent/evidence-v0.2.3/source-integrity.json",
|
||
});
|
||
expect.soft(manifest.files[index]).toMatchObject(asset);
|
||
expect
|
||
.soft(manifest.files[index].sourceSha256)
|
||
.toBe(asset.expectedSourceSha256);
|
||
}
|
||
});
|
||
|
||
it("선택 구간은 승인 계획의 불변 provenance이며 manifest와 고지에 동일하게 남는다", () => {
|
||
const plan = JSON.parse(
|
||
readFileSync(".agent/AMBIENCE-SOURCES-v0.2.3.json", "utf8"),
|
||
);
|
||
const manifest = JSON.parse(
|
||
readFileSync("public/ambience/manifest.json", "utf8"),
|
||
);
|
||
const notice = readFileSync("public/ambience/AMBIENCE_NOTICES.md", "utf8");
|
||
const starts = {
|
||
street: 35,
|
||
mall: 36,
|
||
theater: 45,
|
||
department: 90,
|
||
market: 24,
|
||
subway: 65,
|
||
cafe: 0,
|
||
rain: 180,
|
||
};
|
||
expect(
|
||
Object.fromEntries(
|
||
plan.assets.map(
|
||
(asset: {
|
||
environmentId: string;
|
||
selectedSegmentStartSeconds: number;
|
||
}) => [asset.environmentId, asset.selectedSegmentStartSeconds],
|
||
),
|
||
),
|
||
).toEqual(starts);
|
||
for (const asset of plan.assets) {
|
||
const start = asset.selectedSegmentStartSeconds;
|
||
expect(Number.isFinite(start)).toBe(true);
|
||
expect(start).toBeGreaterThanOrEqual(0);
|
||
expect(start + 12).toBeLessThanOrEqual(asset.directMediaDurationSeconds);
|
||
const entry = manifest.files.find(
|
||
(e: { environmentId: string }) => e.environmentId === asset.environmentId,
|
||
);
|
||
expect(entry.selectedSegmentStartSeconds).toBe(start);
|
||
expect(entry.conversion.startSeconds).toBe(start);
|
||
expect(entry.conversion.durationSeconds).toBe(12);
|
||
const section = notice
|
||
.split("## ")
|
||
.find((s: string) =>
|
||
s.startsWith(`${asset.displayName} — ${asset.environmentId}\n`),
|
||
);
|
||
expect(section).toContain(`source seconds ${start}–${start + 12}.`);
|
||
}
|
||
});
|
||
|
||
it("v0.2.3 최종 ASR 증거는 정확한 WAV와 연결되며 청취·법적 인수로 주장하지 않는다", () => {
|
||
const directory = ".agent/evidence-v0.2.3/privacy-screening";
|
||
const screening = JSON.parse(
|
||
readFileSync(`${directory}/final-asr.json`, "utf8"),
|
||
);
|
||
const manifest = JSON.parse(
|
||
readFileSync(
|
||
".agent/evidence-v0.2.5/baseline/ambience/manifest.json",
|
||
"utf8",
|
||
),
|
||
);
|
||
expect(screening.tool).toMatchObject({
|
||
name: "faster-whisper",
|
||
version: "1.2.1",
|
||
});
|
||
expect(screening.model).toMatchObject({ name: "tiny", multilingual: true });
|
||
expect(screening.model.revision).toMatch(/^[a-f0-9]{40}$/);
|
||
expect(screening.requiredEmptyPassed).toBe(true);
|
||
expect(screening.requiredEmptyEnvironmentIds).toEqual([
|
||
"mall",
|
||
"market",
|
||
"cafe",
|
||
]);
|
||
expect(screening.method).toMatchObject({
|
||
device: "cpu",
|
||
computeType: "int8",
|
||
});
|
||
expect(screening.method.parameters).toMatchObject({
|
||
task: "transcribe",
|
||
language: null,
|
||
beam_size: 5,
|
||
best_of: 5,
|
||
vad_filter: true,
|
||
vad_parameters: null,
|
||
condition_on_previous_text: true,
|
||
});
|
||
expect(screening.method.manifestSha256).toBe(
|
||
createHash("sha256")
|
||
.update(
|
||
readFileSync(".agent/evidence-v0.2.5/baseline/ambience/manifest.json"),
|
||
)
|
||
.digest("hex"),
|
||
);
|
||
expect(screening.limitations).toMatchObject({
|
||
screeningOnly: true,
|
||
humanListeningAcceptance: false,
|
||
legalPrivacyClearance: false,
|
||
textIsVerifiedTranscript: false,
|
||
});
|
||
expect(
|
||
screening.files.map((e: { environmentId: string }) => e.environmentId),
|
||
).toEqual(ENVIRONMENTS.map((e) => e.id));
|
||
for (const entry of manifest.files) {
|
||
const result = screening.files.find(
|
||
(e: { environmentId: string }) => e.environmentId === entry.environmentId,
|
||
);
|
||
expect(result).toMatchObject({
|
||
file: `public/ambience/${entry.file}`,
|
||
sha256: entry.sha256,
|
||
bytes: entry.bytes,
|
||
durationSeconds: entry.duration,
|
||
selectedSegmentStartSeconds: entry.selectedSegmentStartSeconds,
|
||
sourceSha256: entry.sourceSha256,
|
||
});
|
||
expect(result.language).toEqual(expect.any(String));
|
||
expect(result.languageProbability).toEqual(expect.any(Number));
|
||
expect(Array.isArray(result.segments)).toBe(true);
|
||
if (["mall", "market", "cafe"].includes(entry.environmentId)) {
|
||
const starts: Record<string, number> = { mall: 36, market: 24, cafe: 0 };
|
||
expect(result.selectedSegmentStartSeconds).toBe(
|
||
starts[entry.environmentId],
|
||
);
|
||
expect(result.segments).toEqual([]);
|
||
expect(result.text).toBe("");
|
||
expect(result.info.duration_after_vad).toBe(0);
|
||
}
|
||
}
|
||
const provenance = JSON.parse(
|
||
readFileSync(`${directory}/earlier-asr-provenance.json`, "utf8"),
|
||
);
|
||
expect(provenance.textIsVerifiedTranscript).toBe(false);
|
||
for (const record of provenance.records)
|
||
expect(
|
||
createHash("sha256")
|
||
.update(readFileSync(`${directory}/${record.savedFile}`))
|
||
.digest("hex"),
|
||
).toBe(record.sha256);
|
||
});
|
||
|
||
it("v0.2.3 역사 snapshot은 당시 101개 음성·다섯 현장음 보존을 증명한다", () => {
|
||
const baseline = JSON.parse(
|
||
readFileSync(
|
||
".agent/evidence-v0.2.3/privacy-screening/baseline.json",
|
||
"utf8",
|
||
),
|
||
);
|
||
let voices = 0,
|
||
unchangedAmbience = 0,
|
||
changedAmbience = 0;
|
||
for (const [path, before] of Object.entries(baseline.wavSha256)) {
|
||
const actual = createHash("sha256")
|
||
.update(readHistoricalBytes(path))
|
||
.digest("hex");
|
||
if (/public\/ambience\/(mall|market|cafe)\.wav$/.test(path)) {
|
||
expect(actual).not.toBe(before);
|
||
changedAmbience++;
|
||
} else {
|
||
expect(actual).toBe(before);
|
||
if (path.startsWith("public/audio/")) voices++;
|
||
else unchangedAmbience++;
|
||
}
|
||
}
|
||
expect([voices, unchangedAmbience, changedAmbience]).toEqual([101, 5, 3]);
|
||
for (const [path, before] of Object.entries(baseline.priorAgentFiles)) {
|
||
if (baseline.mutableAgentFiles.includes(path)) continue;
|
||
expect(
|
||
createHash("sha256").update(readHistoricalBytes(path)).digest("hex"),
|
||
path,
|
||
).toBe(before);
|
||
}
|
||
});
|
||
|
||
it("v0.2.3 재시도는 market/cafe만 바꾸고 mall 및 실패·no-VAD 증거를 바이트 그대로 보존한다", () => {
|
||
const directory = ".agent/evidence-v0.2.3/privacy-screening";
|
||
const baseline = JSON.parse(
|
||
readFileSync(`${directory}/retry-market24-cafe0/baseline.json`, "utf8"),
|
||
);
|
||
const hash = (path: string) =>
|
||
createHash("sha256").update(readHistoricalBytes(path)).digest("hex");
|
||
const prior = `${directory}/prior-attempt-market60-cafe180`;
|
||
expect(hash(`${prior}/final-asr.json`)).toBe(
|
||
"01c8a92fab9f8df7a5f778c5f7e478ce96bcedc33e11a1055bd29154a08b706b",
|
||
);
|
||
const failed = JSON.parse(readFileSync(`${prior}/final-asr.json`, "utf8"));
|
||
expect(failed.requiredEmptyPassed).toBe(false);
|
||
expect(failed.limitations).toMatchObject({
|
||
humanListeningAcceptance: false,
|
||
legalPrivacyClearance: false,
|
||
});
|
||
for (const [environmentId, start, count] of [
|
||
["mall", 36, 0],
|
||
["market", 60, 3],
|
||
["cafe", 180, 1],
|
||
]) {
|
||
const result = failed.files.find(
|
||
(f: { environmentId: string }) => f.environmentId === environmentId,
|
||
);
|
||
expect(result.selectedSegmentStartSeconds).toBe(start);
|
||
expect(result.segments).toHaveLength(count as number);
|
||
}
|
||
expect(hash(`${directory}/no-vad-asr.json`)).toBe(
|
||
"14b42ee0b80c194caa3c0de2644f647d6d7720c60c2208374778619f47c2e969",
|
||
);
|
||
for (const [path, before] of Object.entries(
|
||
baseline.immutablePriorAttemptFiles,
|
||
))
|
||
expect(hash(path), path).toBe(before);
|
||
for (const [path, before] of Object.entries(baseline.priorAgentFiles)) {
|
||
if (!baseline.mutableAgentFiles.includes(path))
|
||
expect(hash(path), path).toBe(before);
|
||
}
|
||
const changed: string[] = [];
|
||
let voices = 0,
|
||
preservedAmbience = 0;
|
||
for (const [path, before] of Object.entries(baseline.wavSha256)) {
|
||
if (/public\/ambience\/(market|cafe)\.wav$/.test(path)) {
|
||
expect(hash(path), path).not.toBe(before);
|
||
changed.push(path);
|
||
} else {
|
||
expect(hash(path), path).toBe(before);
|
||
if (path.startsWith("public/audio/")) voices++;
|
||
else preservedAmbience++;
|
||
}
|
||
}
|
||
expect(changed.sort()).toEqual([
|
||
"public/ambience/cafe.wav",
|
||
"public/ambience/market.wav",
|
||
]);
|
||
expect([voices, preservedAmbience]).toEqual([101, 6]);
|
||
});
|
||
|
||
// 이번 릴리스는 8 배경을 모두 바꾸되 원본·선택 구간·권리와 101 voice/660 역사 파일을 보존한다.
|
||
it("v0.2.5는 이전 증거·음성 바이트와 모든 출처 필드를 그대로 보존한다", () => {
|
||
const baseline = JSON.parse(
|
||
readFileSync(".agent/evidence-v0.2.5/baseline.json", "utf8"),
|
||
);
|
||
for (const [path, expected] of Object.entries({
|
||
...baseline.immutable,
|
||
...baseline.voiceWavs,
|
||
}))
|
||
expect(
|
||
createHash("sha256").update(readFileSync(path)).digest("hex"),
|
||
path,
|
||
).toBe(expected);
|
||
const prior = JSON.parse(
|
||
readFileSync(
|
||
".agent/evidence-v0.2.5/baseline/ambience/manifest.json",
|
||
"utf8",
|
||
),
|
||
);
|
||
const current = JSON.parse(
|
||
readFileSync("public/ambience/manifest.json", "utf8"),
|
||
);
|
||
for (const [index, before] of prior.files.entries()) {
|
||
const after = current.files[index];
|
||
for (const key of [
|
||
"environmentId",
|
||
"pageUrl",
|
||
"directUrl",
|
||
"creator",
|
||
"license",
|
||
"licenseUrl",
|
||
"attribution",
|
||
"expectedSourceSha256",
|
||
"sourceBytes",
|
||
"sourceSha256",
|
||
"directMediaDurationSeconds",
|
||
"sourceVerification",
|
||
"selectedSegmentStartSeconds",
|
||
"outputLicense",
|
||
])
|
||
expect(after[key], key).toEqual(before[key]);
|
||
expect(after.sha256).not.toBe(before.sha256);
|
||
}
|
||
});
|
||
|
||
// 새 바이트의 ASR은 과거 승인으로 대체하지 않는다. 성공을 강제하려고 결과를 지우면 실패한다.
|
||
it("거부된 loudnorm WAV의 ASR 실패와 privacy 인수 경계를 보존한다", () => {
|
||
const screening = JSON.parse(
|
||
readFileSync(".agent/evidence-v0.2.5/final-asr.json", "utf8"),
|
||
);
|
||
const manifestBytes = readFileSync(
|
||
".agent/evidence-v0.2.5/review-fixes/rejected-loudnorm-bundle/manifest.json",
|
||
);
|
||
const manifest = JSON.parse(manifestBytes.toString());
|
||
expect(screening.method.manifestSha256).toBe(
|
||
createHash("sha256").update(manifestBytes).digest("hex"),
|
||
);
|
||
expect(screening.requiredEmptyPassed).toBe(false);
|
||
expect(screening.limitations).toMatchObject({
|
||
humanListeningAcceptance: false,
|
||
legalPrivacyClearance: false,
|
||
textIsVerifiedTranscript: false,
|
||
});
|
||
for (const entry of manifest.files) {
|
||
const record = screening.files.find(
|
||
(r: { environmentId: string }) => r.environmentId === entry.environmentId,
|
||
);
|
||
expect(record).toMatchObject({
|
||
sha256: entry.sha256,
|
||
sourceSha256: entry.sourceSha256,
|
||
selectedSegmentStartSeconds: entry.selectedSegmentStartSeconds,
|
||
});
|
||
expect(record.segments).toHaveLength(
|
||
entry.environmentId === "market" ? 1 : 0,
|
||
);
|
||
}
|
||
});
|
||
|
||
// 수정본은 정확한 새 WAV로 같은 VAD gate를 통과해야 하며 과거 결과를 승계하지 않는다.
|
||
it("v0.2.5 최종 WAV의 동일 ASR VAD gate는 requiredEmptyPassed=true여야 한다", () => {
|
||
const screening = JSON.parse(
|
||
readFileSync(".agent/evidence-v0.2.5/review-fixes/final-asr.json", "utf8"),
|
||
);
|
||
const prior = JSON.parse(
|
||
readFileSync(".agent/evidence-v0.2.5/final-asr.json", "utf8"),
|
||
);
|
||
const bytes = readFileSync("public/ambience/manifest.json");
|
||
const manifest = JSON.parse(bytes.toString());
|
||
expect(screening.tool).toEqual(prior.tool);
|
||
expect(screening.model).toEqual(prior.model);
|
||
for (const key of [
|
||
"device",
|
||
"computeType",
|
||
"cpuThreads",
|
||
"numWorkers",
|
||
"randomSeed",
|
||
"parameters",
|
||
"vadModelSha256",
|
||
])
|
||
expect(screening.method[key]).toEqual(prior.method[key]);
|
||
expect(screening.method.manifestSha256).toBe(
|
||
createHash("sha256").update(bytes).digest("hex"),
|
||
);
|
||
expect(screening.requiredEmptyEnvironmentIds).toEqual([
|
||
"mall",
|
||
"market",
|
||
"cafe",
|
||
]);
|
||
expect(screening.requiredEmptyPassed).toBe(true);
|
||
expect(screening.limitations).toMatchObject({
|
||
humanListeningAcceptance: false,
|
||
legalPrivacyClearance: false,
|
||
textIsVerifiedTranscript: false,
|
||
});
|
||
for (const entry of manifest.files) {
|
||
const row = screening.files.find(
|
||
(r: { environmentId: string }) => r.environmentId === entry.environmentId,
|
||
);
|
||
expect(row).toMatchObject({
|
||
sha256: entry.sha256,
|
||
bytes: entry.bytes,
|
||
sourceSha256: entry.sourceSha256,
|
||
selectedSegmentStartSeconds: entry.selectedSegmentStartSeconds,
|
||
});
|
||
if (["mall", "market", "cafe"].includes(entry.environmentId)) {
|
||
expect(row.segments).toEqual([]);
|
||
expect(row.text).toBe("");
|
||
expect(row.info.duration_after_vad).toBe(0);
|
||
}
|
||
}
|
||
});
|