This commit is contained in:
fengchuanhn@gmail.com
2026-05-18 08:52:36 +08:00
parent caaa898d90
commit c559cb1581
5 changed files with 124 additions and 28 deletions

View File

@@ -157,6 +157,16 @@ function signOss(secret, str) {
return crypto.createHmac("sha1", secret).update(str).digest("base64");
}
/** 私有桶ASR 需可公网 GET 的签名 URL对齐 Electron generateSignedUrl */
function ossSignedGetUrl(cfg, objectKey, expiresSec = 3600) {
const expireTime = Math.floor(Date.now() / 1000) + expiresSec;
const resource = `/${cfg.bucket}/${objectKey}`;
const stringToSign = `GET\n\n\n${expireTime}\n${resource}`;
const signature = encodeURIComponent(signOss(cfg.accessKeySecret, stringToSign));
const host = `${cfg.bucket}.${cfg.region}.aliyuncs.com`;
return `https://${host}/${objectKey}?OSSAccessKeyId=${encodeURIComponent(cfg.accessKeyId)}&Expires=${expireTime}&Signature=${signature}`;
}
async function aliyunOssUpload(localPath, cfg) {
const ak = cfg.accessKeyId;
const sk = cfg.accessKeySecret;
@@ -196,7 +206,12 @@ async function aliyunOssUpload(localPath, cfg) {
res.on("data", (c) => chunks.push(c));
res.on("end", () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve({ success: true, url });
const signedUrl = ossSignedGetUrl(
{ accessKeyId: ak, accessKeySecret: sk, bucket, region },
objectKey,
3600,
);
resolve({ success: true, url: signedUrl });
} else {
resolve({
success: false,
@@ -211,6 +226,39 @@ async function aliyunOssUpload(localPath, cfg) {
});
}
/** filetrans: output.result批量模型: output.results[0] */
function extractTranscriptionUrl(output) {
if (!output || typeof output !== "object") return null;
if (output.result?.transcription_url) return output.result.transcription_url;
const results = output.results;
if (Array.isArray(results) && results[0]?.transcription_url) {
return results[0].transcription_url;
}
return null;
}
function parseTranscriptionText(trans) {
let fullText = "";
if (trans?.transcripts?.length) {
for (const t of trans.transcripts) {
if (t.sentences?.length) {
for (const s of t.sentences) {
if (s.text) {
if (fullText) fullText += "\n";
fullText += s.text;
}
}
} else if (t.text) {
if (fullText) fullText += "\n";
fullText += t.text;
}
}
} else if (trans?.text) {
fullText = trans.text;
}
return fullText;
}
async function fetchJson(url, options = {}) {
const res = await fetch(url, options);
const text = await res.text();
@@ -235,6 +283,14 @@ async function aliyunAsrFiletrans(audioUrl, opts) {
if (!apiKey) {
return { success: false, error: "缺少 DashScope apiKey" };
}
const fileUrl = String(audioUrl || "").trim();
if (!/^https?:\/\//i.test(fileUrl)) {
return { success: false, error: "ASR 需要可访问的 http(s) 音频 URL请检查 OSS 上传与签名)" };
}
const input =
model === "qwen3-asr-flash-filetrans"
? { file_url: fileUrl }
: { file_urls: [fileUrl] };
const submit = await fetchJson(
"https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription",
{
@@ -246,13 +302,11 @@ async function aliyunAsrFiletrans(audioUrl, opts) {
},
body: JSON.stringify({
model,
input: { file_urls: [audioUrl] },
input,
parameters: {
channel_id: [0],
// 音频通道ID
enable_itn: false
// 是否启用逆文本归一化
}
enable_itn: false,
},
}),
}
);
@@ -270,24 +324,27 @@ async function aliyunAsrFiletrans(audioUrl, opts) {
const status = poll?.output?.task_status;
log("info", "voice recognition status",status);
if (status === "SUCCEEDED") {
const transUrl = poll?.output?.results?.[0]?.transcription_url;
const transUrl = extractTranscriptionUrl(poll?.output);
if (!transUrl) {
log("error", "voice recognition transUrl",transUrl);
return { success: false, error: "ASR 成功但无 transcription_url" };
log("error", "voice recognition output", poll?.output);
return {
success: false,
error: "ASR 成功但无 transcription_url期望 output.result 或 output.results[0]",
};
}
const trans = await fetchJson(transUrl);
let fullText = "";
if (trans.transcripts && trans.transcripts.length) {
fullText = trans.transcripts.map((t) => t.text || "").filter(Boolean).join("\n");
} else if (trans.text) {
fullText = trans.text;
}
const fullText = parseTranscriptionText(trans);
return { success: true, text: fullText };
}
if (status === "FAILED" || status === "UNKNOWN") {
const detail =
poll?.output?.message ||
poll?.output?.code ||
poll?.message ||
(poll?.output ? JSON.stringify(poll.output).slice(0, 300) : null);
return {
success: false,
error: poll?.output?.message || "ASR 任务失败",
error: detail || "ASR 任务失败",
};
}
}