61 lines
3.1 KiB
TypeScript
61 lines
3.1 KiB
TypeScript
import { promises as fs } from "node:fs";
|
|
import { createHash } from "node:crypto";
|
|
import path from "node:path";
|
|
import { parseScoreExport, type ModelId, type ScoreResponse } from "@/lib/model-scores";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
const root = path.resolve(process.cwd(), "..", "..");
|
|
const sources: { model: ModelId; directory: string; name: string }[] = [
|
|
{ model: "maai", directory: "maai-ch-kyoto", name: "MaAI" },
|
|
{ model: "smart_turn", directory: "smart-turn-v3.2", name: "Smart Turn" },
|
|
{ model: "silero_vad", directory: "silero-vad", name: "Silero VAD" },
|
|
];
|
|
const json = (data: unknown, status = 200) => Response.json(data, { status, headers: { "Cache-Control": "no-store" } });
|
|
|
|
export async function GET(request: Request) {
|
|
const params = new URL(request.url).searchParams;
|
|
const callId = params.get("callId") || "";
|
|
const recordingPath = params.get("recordingPath") || "";
|
|
if (!callId || callId.length > 200 || !recordingPath || recordingPath.length > 1000) {
|
|
return json({ error: "请选择已关联项目录音的对话。" }, 400);
|
|
}
|
|
const payload: ScoreResponse = { callId, recordingPath, series: [], messages: [] };
|
|
try {
|
|
for (const source of sources) {
|
|
try {
|
|
// Fixed paths only: no request value is used to locate an export.
|
|
const file = path.join(root, "experiments", source.directory, "results", "score-timeline.json");
|
|
const data = parseScoreExport(JSON.parse(await fs.readFile(file, "utf8")), source.model);
|
|
const call = data.calls.find(item => item.call_id === callId && item.audio_path === recordingPath);
|
|
if (call) payload.series.push({ ...call, model: data.model, name: data.name,
|
|
score: data.score, sample_interval_ms: data.sample_interval_ms });
|
|
else payload.messages.push(`${source.name} 暂无这段录音的评分。`);
|
|
} catch (error) {
|
|
if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) {
|
|
console.error(`Cannot read ${source.name} scores`, error);
|
|
}
|
|
payload.messages.push(`${source.name} 评分暂时不可用。`);
|
|
}
|
|
}
|
|
if (payload.series.length) {
|
|
const dataRoot = await fs.realpath(path.join(root, "data"));
|
|
const audio = await fs.realpath(path.resolve(dataRoot, payload.series[0].audio_path));
|
|
const relative = path.relative(dataRoot, audio);
|
|
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) {
|
|
return json({ error: "评分对应的录音不在项目目录内。" }, 422);
|
|
}
|
|
const hash = createHash("sha256").update(await fs.readFile(audio)).digest("hex");
|
|
payload.series = payload.series.filter(series => {
|
|
if (series.audio_sha256 === hash) return true;
|
|
payload.messages.push(`${series.name} 的评分对应旧录音,请重新生成评分。`);
|
|
return false;
|
|
});
|
|
}
|
|
return json(payload);
|
|
} catch (error) {
|
|
console.error("Cannot verify score recording", error);
|
|
return json({ error: "无法核对评分对应的录音,请检查项目文件。" }, 500);
|
|
}
|
|
}
|