fix(video): sync placeholder updates and harden volcano input handling
This commit is contained in:
parent
46b3f4b081
commit
b194f1a7e2
@ -3728,26 +3728,32 @@ export async function POST(request: Request) {
|
||||
const lastUserMessage = messagesForVideo.filter(m => m.role === "user").pop();
|
||||
let prompt = "";
|
||||
const imageUrls: string[] = [];
|
||||
|
||||
// 🐛 调试:打印原始消息结构
|
||||
console.log("[Chat API] 🔍 原始 lastUserMessage:", JSON.stringify(lastUserMessage, null, 2));
|
||||
const summarizeImageRef = (rawUrl: string) => {
|
||||
const trimmed = String(rawUrl || "").trim();
|
||||
if (!trimmed) return "";
|
||||
if (trimmed.startsWith("data:image/")) {
|
||||
return `data:image(base64,${trimmed.length} chars)`;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return `${parsed.origin}${parsed.pathname}`;
|
||||
} catch {
|
||||
return trimmed.slice(0, 120);
|
||||
}
|
||||
};
|
||||
|
||||
if (lastUserMessage) {
|
||||
// 🆕 处理 parts 数组(AI SDK v4 格式)
|
||||
if ((lastUserMessage as any).parts && Array.isArray((lastUserMessage as any).parts)) {
|
||||
console.log("[Chat API] 🔍 Found parts array, length:", (lastUserMessage as any).parts.length);
|
||||
console.log("[Chat API] Video input parts:", (lastUserMessage as any).parts.length);
|
||||
|
||||
for (const part of (lastUserMessage as any).parts) {
|
||||
console.log("[Chat API] 🔍 Processing part:", JSON.stringify(part, null, 2));
|
||||
|
||||
if (part.type === "text" && part.text) {
|
||||
// 提取文本
|
||||
prompt += part.text;
|
||||
console.log("[Chat API] 🔍 ✅ Extracted text:", part.text);
|
||||
} else if (part.type === "file" && part.url && part.mediaType?.startsWith("image/")) {
|
||||
// 提取图片
|
||||
imageUrls.push(part.url);
|
||||
console.log("[Chat API] 🔍 ✅ Extracted image URL:", part.url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3775,8 +3781,11 @@ export async function POST(request: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[Chat API] 🔍 最终提取结果 - prompt:", prompt, "imageCount:", imageUrls.length);
|
||||
console.log("[Chat API] 🔍 imageUrls:", imageUrls);
|
||||
console.log("[Chat API] Video input extracted:", {
|
||||
promptLength: prompt.trim().length,
|
||||
imageCount: imageUrls.length,
|
||||
imageRefs: imageUrls.slice(0, 4).map(summarizeImageRef),
|
||||
});
|
||||
|
||||
const videoMessageId = generateUUID();
|
||||
const videoStartAt = Date.now();
|
||||
@ -3793,7 +3802,7 @@ export async function POST(request: Request) {
|
||||
|
||||
try {
|
||||
console.log("[Chat API] Video generation params:", {
|
||||
prompt,
|
||||
promptLength: prompt.trim().length,
|
||||
imageCount: imageUrls.length,
|
||||
videoParams: effectiveVideoParams,
|
||||
});
|
||||
@ -3920,7 +3929,17 @@ export async function POST(request: Request) {
|
||||
videoOptions.referenceImages = imageUrls;
|
||||
}
|
||||
|
||||
console.log("[Chat API] Video options constructed:", videoOptions);
|
||||
console.log("[Chat API] Video options constructed:", {
|
||||
model: videoOptions.model,
|
||||
mode: videoOptions.mode,
|
||||
resolution: videoOptions.resolution,
|
||||
ratio: videoOptions.ratio,
|
||||
duration: videoOptions.duration,
|
||||
imageCount:
|
||||
videoOptions.referenceImages?.length ||
|
||||
Number(Boolean(videoOptions.firstFrameImage)) +
|
||||
Number(Boolean(videoOptions.lastFrameImage)),
|
||||
});
|
||||
|
||||
// 先写入占位消息,避免刷新后消息丢失
|
||||
const pendingText = "🎬 视频任务已创建,正在处理中,请稍候...";
|
||||
|
||||
@ -1529,19 +1529,41 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
|
||||
const messageData = typeof dataPart.data === 'string'
|
||||
? JSON.parse(dataPart.data)
|
||||
: dataPart.data;
|
||||
const normalizedParts = Array.isArray(messageData.parts) ? messageData.parts : [];
|
||||
const normalizedAttachments = Array.isArray(messageData.attachments)
|
||||
? messageData.attachments
|
||||
: [];
|
||||
|
||||
setMessages((prevMessages) => {
|
||||
if (prevMessages.some(m => m.id === messageData.id)) {
|
||||
return prevMessages;
|
||||
const existingIndex = prevMessages.findIndex(
|
||||
(message) => message.id === messageData.id
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
const existingMessage = prevMessages[existingIndex];
|
||||
const updatedMessage: any = {
|
||||
...existingMessage,
|
||||
role: messageData.role || existingMessage.role,
|
||||
content:
|
||||
normalizedParts.find((part: any) => part?.type === "text")?.text ||
|
||||
(existingMessage as any).content ||
|
||||
"",
|
||||
parts: normalizedParts,
|
||||
attachments: normalizedAttachments,
|
||||
};
|
||||
|
||||
const nextMessages = [...prevMessages];
|
||||
nextMessages[existingIndex] = updatedMessage;
|
||||
return nextMessages;
|
||||
}
|
||||
|
||||
const newMessage: any = {
|
||||
id: messageData.id,
|
||||
role: messageData.role,
|
||||
content: messageData.parts?.[0]?.text || "",
|
||||
content: normalizedParts.find((part: any) => part?.type === "text")?.text || "",
|
||||
createdAt: new Date(),
|
||||
parts: messageData.parts || [],
|
||||
attachments: [],
|
||||
parts: normalizedParts,
|
||||
attachments: normalizedAttachments,
|
||||
};
|
||||
|
||||
return [...prevMessages, newMessage];
|
||||
|
||||
@ -28,6 +28,40 @@ function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
function sanitizeUrlForLog(url: string): string {
|
||||
const trimmed = String(url || "").trim();
|
||||
if (!trimmed) return "";
|
||||
if (trimmed.startsWith("data:image/")) {
|
||||
return `data:image(base64,${trimmed.length} chars)`;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(trimmed);
|
||||
return `${parsed.origin}${parsed.pathname}`;
|
||||
} catch {
|
||||
return trimmed.slice(0, 120);
|
||||
}
|
||||
}
|
||||
|
||||
function extractPublicHttpUrl(rawValue: string): string | null {
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed) return null;
|
||||
if (/^https?:\/\//i.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
const markdownMatch = trimmed.match(/\((https?:\/\/[^)\s]+)\)/i);
|
||||
if (markdownMatch?.[1]) {
|
||||
return markdownMatch[1];
|
||||
}
|
||||
|
||||
const inlineMatch = trimmed.match(/https?:\/\/[^\s"'<>]+/i);
|
||||
if (inlineMatch?.[0]) {
|
||||
return inlineMatch[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function isRetryableFetchError(error: any): boolean {
|
||||
const code = error?.code || error?.cause?.code;
|
||||
if (typeof code === "string" && RETRYABLE_FETCH_CODES.has(code)) {
|
||||
@ -110,9 +144,14 @@ async function fetchWithRetry(
|
||||
*/
|
||||
async function normalizeImageUrlForVolcano(imageUrl: string): Promise<string> {
|
||||
try {
|
||||
const rawInput = String(imageUrl || "").trim();
|
||||
if (!rawInput) {
|
||||
throw new Error("Empty image url");
|
||||
}
|
||||
|
||||
// Base64 输入:上传到 S3,返回永久 URL
|
||||
if (imageUrl.startsWith("data:image/")) {
|
||||
const matches = imageUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/);
|
||||
if (rawInput.startsWith("data:image/")) {
|
||||
const matches = rawInput.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/);
|
||||
if (!matches) {
|
||||
throw new Error("Invalid base64 image format");
|
||||
}
|
||||
@ -127,14 +166,19 @@ async function normalizeImageUrlForVolcano(imageUrl: string): Promise<string> {
|
||||
}
|
||||
|
||||
// 已经是公网 URL,直接使用
|
||||
if (imageUrl.startsWith("http://") || imageUrl.startsWith("https://")) {
|
||||
return imageUrl;
|
||||
const publicHttpUrl = extractPublicHttpUrl(rawInput);
|
||||
if (publicHttpUrl) {
|
||||
return publicHttpUrl;
|
||||
}
|
||||
|
||||
// 其他格式:下载后上传到 S3,返回永久 URL
|
||||
console.log(`[Volcano Video] Downloading image: ${imageUrl}`);
|
||||
if (rawInput.startsWith("blob:") || rawInput.startsWith("file:")) {
|
||||
throw new Error("Unsupported local image URL. Please upload image first.");
|
||||
}
|
||||
|
||||
// 其他格式:尝试下载后上传到 S3,返回永久 URL
|
||||
console.log(`[Volcano Video] Downloading image: ${sanitizeUrlForLog(rawInput)}`);
|
||||
const response = await fetchWithRetry(
|
||||
imageUrl,
|
||||
rawInput,
|
||||
{ method: "GET" },
|
||||
{
|
||||
label: "download-image",
|
||||
@ -154,7 +198,7 @@ async function normalizeImageUrlForVolcano(imageUrl: string): Promise<string> {
|
||||
const filename = `volcano-video-input-${Date.now()}.jpg`;
|
||||
const { url: permanentUrl } = await uploadFile(filename, bufferNode as any);
|
||||
|
||||
console.log(`[Volcano Video] Image uploaded to S3: ${permanentUrl}`);
|
||||
console.log(`[Volcano Video] Image uploaded to S3: ${sanitizeUrlForLog(permanentUrl)}`);
|
||||
|
||||
return permanentUrl;
|
||||
} catch (error) {
|
||||
@ -356,15 +400,18 @@ async function createVideoTask(
|
||||
console.log("[Volcano Video] Creating task:", {
|
||||
model: options.model,
|
||||
mode: options.mode,
|
||||
prompt: options.prompt,
|
||||
fullPrompt,
|
||||
promptLength: options.prompt?.length || 0,
|
||||
fullPromptLength: fullPrompt.length,
|
||||
cameraMove: options.cameraMove,
|
||||
contentItems: content.length,
|
||||
imageRefs: Object.values(inputImageUrls)
|
||||
.flat()
|
||||
.map((value: any) => sanitizeUrlForLog(String(value)))
|
||||
.slice(0, 6),
|
||||
});
|
||||
|
||||
// 4. 发送请求
|
||||
console.log("[Volcano Video] 🔍 Request URL:", url);
|
||||
console.log("[Volcano Video] 🔍 Request method: POST");
|
||||
console.log("[Volcano Video] 🔍 Request body:", JSON.stringify(requestBody, null, 2));
|
||||
console.log("[Volcano Video] Request URL:", url);
|
||||
|
||||
const response = await fetchWithRetry(
|
||||
url,
|
||||
@ -383,8 +430,7 @@ async function createVideoTask(
|
||||
}
|
||||
);
|
||||
|
||||
console.log("[Volcano Video] 🔍 Response status:", response.status);
|
||||
console.log("[Volcano Video] 🔍 Response headers:", JSON.stringify(Object.fromEntries(response.headers.entries())));
|
||||
console.log("[Volcano Video] Response status:", response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
@ -613,9 +659,8 @@ export async function* doGenerateStream(
|
||||
const lastFrameUrl = status.content?.last_frame_url || status.output?.last_frame_url || status.last_frame_url;
|
||||
|
||||
console.log("[Volcano Video] ✅ Task succeeded!");
|
||||
console.log("[Volcano Video] Video URL:", videoUrl);
|
||||
console.log("[Volcano Video] Last frame URL:", lastFrameUrl);
|
||||
console.log("[Volcano Video] Full status response:", JSON.stringify(status, null, 2));
|
||||
console.log("[Volcano Video] Video URL:", sanitizeUrlForLog(videoUrl || ""));
|
||||
console.log("[Volcano Video] Last frame URL:", sanitizeUrlForLog(lastFrameUrl || ""));
|
||||
|
||||
const videoResponse = formatVideoResponse({
|
||||
taskId: taskId,
|
||||
@ -627,8 +672,6 @@ export async function* doGenerateStream(
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
|
||||
console.log("[Volcano Video] Formatted response:", videoResponse);
|
||||
|
||||
yield videoResponse;
|
||||
|
||||
// 输出视频元数据(JSON格式,前端可解析)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user