Files
douyin-archive/tests/stt.test.ts
T

168 lines
5.3 KiB
TypeScript

import assert from "node:assert/strict";
import { once } from "node:events";
import { createServer, type Server } from "node:http";
import { after, before, test } from "node:test";
import {
AsrError,
downloadTranscript,
extractTranscript,
getAsrConfig,
MAX_BYTES,
MAX_DURATION_MS,
queryTranscription,
submitTranscription,
validateMedia,
} from "../lib/stt/dashscope";
import { transcriptionState } from "../lib/stt/types";
let server: Server;
let base: string;
let response: unknown = { output: { task_id: "task-1" } };
let status = 200;
const calls: {
url?: string;
headers: Record<string, unknown>;
body: string;
}[] = [];
const savedEnv = { ...process.env };
before(async () => {
server = createServer(async (req, res) => {
let body = "";
for await (const chunk of req) body += chunk;
calls.push({ url: req.url, headers: req.headers, body });
res.writeHead(status, { "Content-Type": "application/json" });
res.end(JSON.stringify(response));
}).listen(0, "127.0.0.1");
await once(server, "listening");
base = `http://127.0.0.1:${(server.address() as { port: number }).port}`;
process.env.DASHSCOPE_API_KEY = "test-key";
process.env.DASHSCOPE_BASE_URL = `${base}/api/v1`;
delete process.env.DASHSCOPE_ASR_MODEL;
});
after(async () => {
process.env = savedEnv;
server.closeAllConnections();
await new Promise<void>((resolve) => server.close(() => resolve()));
});
test("native ASR submission sends only audio URL and recognition options", async () => {
assert.equal(
await submitTranscription("https://storage.test/audio.mp3"),
"task-1",
);
const call = calls.at(-1);
assert.ok(call);
assert.equal(call.url, "/api/v1/services/audio/asr/transcription");
assert.equal(call.headers.authorization, "Bearer test-key");
assert.equal(call.headers["x-dashscope-async"], "enable");
assert.deepEqual(JSON.parse(call.body), {
model: "qwen3-asr-flash-filetrans",
input: { file_url: "https://storage.test/audio.mp3" },
parameters: { channel_id: [0], enable_itn: false, enable_words: false },
});
});
test("query pending, running, success, failure and unknown", async () => {
for (const state of ["PENDING", "RUNNING"] as const) {
response = { output: { task_status: state } };
assert.deepEqual(await queryTranscription("task-1"), { status: state });
}
response = {
output: {
task_status: "SUCCEEDED",
result: { transcription_url: `${base}/result` },
},
};
assert.deepEqual(await queryTranscription("task-1"), {
status: "SUCCEEDED",
resultURL: `${base}/result`,
});
response = {
output: {
task_status: "FAILED",
code: "FILE_403_FORBIDDEN",
message: "secret-url",
},
};
await assert.rejects(queryTranscription("task-1"), /FILE_403_FORBIDDEN/);
response = { output: { task_status: "UNKNOWN" } };
await assert.rejects(queryTranscription("task-1"), /UNKNOWN/);
});
test("extract only text, retain order, fall back to full text", async () => {
response = {
transcripts: [
{
sentences: [
{ text: " 第一段 ", emotion: "happy", language: "zh" },
{ text: "第二段" },
],
text: "全文",
},
{ sentences: [], text: "备用全文" },
],
};
assert.deepEqual(await downloadTranscript(`${base}/result`), [
"第一段",
"第二段",
"备用全文",
]);
assert.equal(calls.at(-1)?.headers.authorization, undefined);
assert.deepEqual(extractTranscript({ transcripts: [] }), []);
assert.deepEqual(
extractTranscript({ transcripts: [{ text: " ", sentences: [] }] }),
[],
);
assert.throws(() => extractTranscript({}), /transcripts/);
assert.throws(
() =>
extractTranscript({
transcripts: [{ sentences: [{ emotion: "happy" }] }],
}),
/文字/,
);
});
test("limits are inclusive and oversized media fails without truncation", () => {
validateMedia(MAX_DURATION_MS, MAX_BYTES);
assert.throws(() => validateMedia(MAX_DURATION_MS + 1, 1), /12 小时/);
assert.throws(() => validateMedia(1, MAX_BYTES + 1), /2 GB/);
assert.throws(() => validateMedia(1, 0), /为空/);
});
test("HTTP throttling can retry; authentication cannot; no provider error leakage", async () => {
response = { code: "InvalidApiKey", message: "sensitive" };
status = 401;
await assert.rejects(
queryTranscription("task-1"),
(e) =>
e instanceof AsrError && !e.retryable && !e.message.includes("sensitive"),
);
status = 429;
await assert.rejects(
queryTranscription("task-1"),
(e) => e instanceof AsrError && e.retryable,
);
status = 200;
});
test("missing key and incompatible model produce explicit errors", () => {
delete process.env.DASHSCOPE_API_KEY;
assert.throws(getAsrConfig, /DASHSCOPE_API_KEY/);
process.env.DASHSCOPE_API_KEY = "test-key";
process.env.DASHSCOPE_ASR_MODEL = "gemini-2.5-flash";
assert.throws(getAsrConfig, /qwen3-asr-flash-filetrans/);
delete process.env.DASHSCOPE_ASR_MODEL;
});
test("historical empty transcript is complete; failed retry retains old text", () => {
assert.equal(
transcriptionState({ transcript: [] }, null).status,
"SUCCEEDED",
);
assert.deepEqual(
transcriptionState(
{ transcript: ["旧文字"] },
{ status: "FAILED", lastError: "失败" },
),
{
status: "FAILED",
error: "失败",
transcript: ["旧文字"],
},
);
});