111 lines
3.5 KiB
TypeScript
111 lines
3.5 KiB
TypeScript
type AliyunVideoModel = "qwen3.8-max" | "qwen3.8-flash" | "qwen3.8-omni-flash";
|
||
|
||
export function isAliyunVideoModel(modelId: string, providerType: string) {
|
||
return providerType === "alibaba" &&
|
||
["qwen3.8-max", "qwen3.8-flash", "qwen3.8-omni-flash"].includes(modelId);
|
||
}
|
||
|
||
export function supportsVideoSound(modelId: string) {
|
||
return modelId === "qwen3.8-omni-flash";
|
||
}
|
||
|
||
function officialEndpoint(baseUrl: string) {
|
||
const url = new URL(baseUrl);
|
||
if (url.protocol !== "https:" ||
|
||
!(url.hostname === "dashscope.aliyuncs.com" ||
|
||
/^[-a-z0-9]+\.[-a-z0-9]+\.maas\.aliyuncs\.com$/.test(url.hostname))) {
|
||
throw new Error("视频解析仅允许阿里云百炼官方地址");
|
||
}
|
||
return `${url.href.replace(/\/$/, "")}/chat/completions`;
|
||
}
|
||
|
||
function sseText(body: string) {
|
||
let result = "";
|
||
let finishReason: string | undefined;
|
||
for (const line of body.split(/\r?\n/)) {
|
||
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
|
||
try {
|
||
const event = JSON.parse(line.slice(6));
|
||
result += event.choices?.[0]?.delta?.content || "";
|
||
finishReason = event.choices?.[0]?.finish_reason || finishReason;
|
||
} catch {
|
||
// Ignore keep-alive and incomplete events; a missing result is rejected below.
|
||
}
|
||
}
|
||
if (finishReason === "length") throw new Error("阿里云视频分析结果被截断,未提交成片合成");
|
||
return result;
|
||
}
|
||
|
||
export async function analyzeAliyunVideo({
|
||
apiKey,
|
||
baseUrl,
|
||
modelId,
|
||
prompt,
|
||
videoUrl,
|
||
contextImages = [],
|
||
fps = 1,
|
||
jsonOutput = false,
|
||
signal,
|
||
}: {
|
||
apiKey: string;
|
||
baseUrl: string;
|
||
modelId: AliyunVideoModel;
|
||
prompt: string;
|
||
videoUrl: string;
|
||
contextImages?: string[];
|
||
fps?: number;
|
||
jsonOutput?: boolean;
|
||
signal?: AbortSignal;
|
||
}) {
|
||
const video = new URL(videoUrl);
|
||
if (video.protocol !== "https:") {
|
||
throw new Error("阿里云视频解析需要可公网访问的 HTTPS 视频地址");
|
||
}
|
||
if (!Number.isFinite(fps) || fps < 0.1 || fps > 10 || contextImages.length > 2)
|
||
throw new Error("视频分析采样或参考图片参数无效");
|
||
const omni = supportsVideoSound(modelId);
|
||
const response = await fetch(officialEndpoint(baseUrl), {
|
||
method: "POST",
|
||
headers: {
|
||
Authorization: `Bearer ${apiKey}`,
|
||
"Content-Type": "application/json",
|
||
},
|
||
body: JSON.stringify({
|
||
model: modelId,
|
||
messages: [{
|
||
role: "user",
|
||
content: [
|
||
{ type: "video_url", video_url: { url: video.href }, fps },
|
||
...contextImages.map(url => ({ type: "image_url", image_url: { url } })),
|
||
{ type: "text", text: prompt },
|
||
],
|
||
}],
|
||
stream: omni,
|
||
...(jsonOutput ? { response_format: { type: "json_object" } } : {}),
|
||
...(omni ? { modalities: ["text"], reasoning_effort: "none" } : {
|
||
temperature: 0.2,
|
||
max_tokens: 6000,
|
||
}),
|
||
}),
|
||
signal,
|
||
});
|
||
if (!response.ok) {
|
||
let code = "";
|
||
try {
|
||
const body = await response.json();
|
||
code = String(body.error?.code || body.code || "").slice(0, 80);
|
||
} catch {}
|
||
throw new Error(`阿里云视频解析失败(HTTP ${response.status}${code ? `,${code}` : ""})`);
|
||
}
|
||
let text: string;
|
||
if (omni) text = sseText(await response.text());
|
||
else {
|
||
const choice = (await response.json()).choices?.[0];
|
||
if (choice?.finish_reason === "length")
|
||
throw new Error("阿里云视频分析结果被截断,未提交成片合成");
|
||
text = String(choice?.message?.content || "");
|
||
}
|
||
if (!text.trim()) throw new Error("阿里云视频解析返回空结果");
|
||
return text;
|
||
}
|