优化Gemini图片生成功能
This commit is contained in:
parent
08a1bd8007
commit
57a439f063
@ -123,65 +123,101 @@ async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/jp
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载远程URL图片并上传到 S3,返回缩略图和原图的 URL
|
* 下载远程URL图片并上传到 S3,返回缩略图和原图的 URL
|
||||||
|
* 带重试机制,确保成功下载并上传到国内中转
|
||||||
*/
|
*/
|
||||||
async function downloadAndUploadToS3(imageUrl: string): Promise<{ thumbnail: string; original: string }> {
|
async function downloadAndUploadToS3(imageUrl: string, retries = 3): Promise<{ thumbnail: string; original: string }> {
|
||||||
try {
|
for (let attempt = 0; attempt < retries; attempt++) {
|
||||||
console.log(`[Gemini Image] Downloading image from URL: ${imageUrl.substring(0, 100)}...`);
|
try {
|
||||||
|
console.log(`[Gemini Image] Downloading image from URL (attempt ${attempt + 1}/${retries}): ${imageUrl.substring(0, 100)}...`);
|
||||||
// 下载图片
|
|
||||||
const response = await fetch(imageUrl);
|
// 下载图片(增加超时时间到 30 秒)
|
||||||
if (!response.ok) {
|
const controller = new AbortController();
|
||||||
throw new Error(`Failed to download image: ${response.statusText}`);
|
const timeoutId = setTimeout(() => controller.abort(), 30000);
|
||||||
|
|
||||||
|
const response = await fetch(imageUrl, {
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片数据
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
|
|
||||||
|
if (buffer.length === 0) {
|
||||||
|
throw new Error("下载的图片数据为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从响应头或URL获取MIME类型
|
||||||
|
const contentType = response.headers.get("content-type") || "image/jpeg";
|
||||||
|
const ext = contentType.split("/")[1] || "jpg";
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const randomStr = Math.random().toString(36).slice(2, 8);
|
||||||
|
const thumbnailFilename = `gemini-thumb-${timestamp}-${randomStr}.${ext}`;
|
||||||
|
const originalFilename = `gemini-orig-${timestamp}-${randomStr}.${ext}`;
|
||||||
|
|
||||||
|
console.log(`[Gemini Image] Downloaded size: ${buffer.length} bytes`);
|
||||||
|
|
||||||
|
// 生成缩略图
|
||||||
|
const thumbnailBuffer = await createThumbnail(buffer, contentType);
|
||||||
|
|
||||||
|
// 转换为 ArrayBuffer
|
||||||
|
const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice(
|
||||||
|
thumbnailBuffer.byteOffset,
|
||||||
|
thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength
|
||||||
|
) as ArrayBuffer;
|
||||||
|
const originalArrayBuffer = buffer.buffer.slice(
|
||||||
|
buffer.byteOffset,
|
||||||
|
buffer.byteOffset + buffer.byteLength
|
||||||
|
) as ArrayBuffer;
|
||||||
|
|
||||||
|
// 并行上传缩略图和原图到 S3
|
||||||
|
console.log(`[Gemini Image] Uploading downloaded image to S3...`);
|
||||||
|
const [thumbnailResult, originalResult] = await Promise.all([
|
||||||
|
uploadFile(thumbnailFilename, thumbnailArrayBuffer),
|
||||||
|
uploadFile(originalFilename, originalArrayBuffer),
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log(`[Gemini Image] Uploaded to S3:`);
|
||||||
|
console.log(` - Thumbnail: ${thumbnailResult.url}`);
|
||||||
|
console.log(` - Original: ${originalResult.url}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
thumbnail: thumbnailResult.url,
|
||||||
|
original: originalResult.url,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const isLastAttempt = attempt === retries - 1;
|
||||||
|
console.error(
|
||||||
|
`[Gemini Image] Download attempt ${attempt + 1}/${retries} failed:`,
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLastAttempt) {
|
||||||
|
// 最后一次尝试失败,抛出错误(不要 fallback 到原始 URL)
|
||||||
|
throw new Error(
|
||||||
|
`图片下载并上传失败(已重试 ${retries} 次): ${error instanceof Error ? error.message : "未知错误"}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等待后重试(指数退避)
|
||||||
|
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s...
|
||||||
|
console.log(`[Gemini Image] Retrying in ${delay}ms...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取图片数据
|
|
||||||
const arrayBuffer = await response.arrayBuffer();
|
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
|
||||||
|
|
||||||
// 从响应头或URL获取MIME类型
|
|
||||||
const contentType = response.headers.get("content-type") || "image/jpeg";
|
|
||||||
const ext = contentType.split("/")[1] || "jpg";
|
|
||||||
|
|
||||||
// 生成文件名
|
|
||||||
const timestamp = Date.now();
|
|
||||||
const randomStr = Math.random().toString(36).slice(2, 8);
|
|
||||||
const thumbnailFilename = `openai-thumb-${timestamp}-${randomStr}.${ext}`;
|
|
||||||
const originalFilename = `openai-orig-${timestamp}-${randomStr}.${ext}`;
|
|
||||||
|
|
||||||
console.log(`[Gemini Image] Downloaded size: ${buffer.length} bytes`);
|
|
||||||
|
|
||||||
// 生成缩略图
|
|
||||||
const thumbnailBuffer = await createThumbnail(buffer, contentType);
|
|
||||||
|
|
||||||
// 转换为 ArrayBuffer
|
|
||||||
const thumbnailArrayBuffer = thumbnailBuffer.buffer.slice(
|
|
||||||
thumbnailBuffer.byteOffset,
|
|
||||||
thumbnailBuffer.byteOffset + thumbnailBuffer.byteLength
|
|
||||||
) as ArrayBuffer;
|
|
||||||
const originalArrayBuffer = buffer.buffer.slice(
|
|
||||||
buffer.byteOffset,
|
|
||||||
buffer.byteOffset + buffer.byteLength
|
|
||||||
) as ArrayBuffer;
|
|
||||||
|
|
||||||
// 并行上传缩略图和原图到 S3
|
|
||||||
console.log(`[Gemini Image] Uploading downloaded image to S3...`);
|
|
||||||
const [thumbnailResult, originalResult] = await Promise.all([
|
|
||||||
uploadFile(thumbnailFilename, thumbnailArrayBuffer),
|
|
||||||
uploadFile(originalFilename, originalArrayBuffer),
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log(`[Gemini Image] Uploaded to S3:`);
|
|
||||||
console.log(` - Thumbnail: ${thumbnailResult.url}`);
|
|
||||||
console.log(` - Original: ${originalResult.url}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
thumbnail: thumbnailResult.url,
|
|
||||||
original: originalResult.url,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Gemini Image] Failed to download and upload image:", error);
|
|
||||||
throw new Error(`图片下载并上传失败: ${error instanceof Error ? error.message : "未知错误"}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 理论上不会到达这里,但 TypeScript 需要
|
||||||
|
throw new Error("下载失败:未知错误");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gemini API 响应格式(参考灵魂助手3.0的格式)
|
// Gemini API 响应格式(参考灵魂助手3.0的格式)
|
||||||
@ -253,14 +289,17 @@ async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail:
|
|||||||
.then(urls => { images.push(urls); });
|
.then(urls => { images.push(urls); });
|
||||||
uploadPromises.push(uploadPromise);
|
uploadPromises.push(uploadPromise);
|
||||||
} else {
|
} else {
|
||||||
// 非 base64 图片(OpenAI URL 等),下载并上传到 S3
|
// 非 base64 图片(OpenAI URL 等),必须下载并上传到 S3(国内中转)
|
||||||
console.log("[Gemini Image] Detected external URL, will download and upload to S3:", img.url.substring(0, 100) + "...");
|
console.log("[Gemini Image] Detected external URL, will download and upload to S3:", img.url.substring(0, 100) + "...");
|
||||||
const uploadPromise = downloadAndUploadToS3(img.url)
|
const uploadPromise = downloadAndUploadToS3(img.url)
|
||||||
.then(urls => { images.push(urls); })
|
.then(urls => {
|
||||||
|
console.log("[Gemini Image] Successfully downloaded and uploaded external image to S3");
|
||||||
|
images.push(urls);
|
||||||
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("[Gemini Image] Failed to download/upload URL, falling back to original:", error);
|
console.error("[Gemini Image] Failed to download/upload URL after retries:", error);
|
||||||
// 如果下载失败,使用原始URL(可能无法访问,但至少不会崩溃)
|
// 不再 fallback 到原始 URL,而是抛出错误
|
||||||
images.push({ thumbnail: img.url, original: img.url });
|
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
|
||||||
});
|
});
|
||||||
uploadPromises.push(uploadPromise);
|
uploadPromises.push(uploadPromise);
|
||||||
}
|
}
|
||||||
@ -274,13 +313,17 @@ async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail:
|
|||||||
for (const result of response.results) {
|
for (const result of response.results) {
|
||||||
if (result.url && typeof result.url === "string") {
|
if (result.url && typeof result.url === "string") {
|
||||||
console.log("[Gemini Image] Found Nano Banana image URL:", result.url.substring(0, 100) + "...");
|
console.log("[Gemini Image] Found Nano Banana image URL:", result.url.substring(0, 100) + "...");
|
||||||
// Nano Banana 返回的是外部URL,需要下载并上传到 S3
|
// Nano Banana 返回的是外部URL,必须下载并上传到 S3(国内中转)
|
||||||
|
// 如果下载失败,抛出错误,不要使用原始URL(官方地址可能无法访问)
|
||||||
const uploadPromise = downloadAndUploadToS3(result.url)
|
const uploadPromise = downloadAndUploadToS3(result.url)
|
||||||
.then(urls => { images.push(urls); })
|
.then(urls => {
|
||||||
|
console.log("[Gemini Image] Successfully downloaded and uploaded Nano Banana image to S3");
|
||||||
|
images.push(urls);
|
||||||
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("[Gemini Image] Failed to download/upload Nano Banana URL:", error);
|
console.error("[Gemini Image] Failed to download/upload Nano Banana URL after retries:", error);
|
||||||
// 如果下载失败,使用原始URL
|
// 不再 fallback 到原始 URL,而是抛出错误
|
||||||
images.push({ thumbnail: result.url, original: result.url });
|
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
|
||||||
});
|
});
|
||||||
uploadPromises.push(uploadPromise);
|
uploadPromises.push(uploadPromise);
|
||||||
}
|
}
|
||||||
@ -300,13 +343,17 @@ async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail:
|
|||||||
.then(urls => { images.push(urls); });
|
.then(urls => { images.push(urls); });
|
||||||
uploadPromises.push(uploadPromise);
|
uploadPromises.push(uploadPromise);
|
||||||
} else {
|
} else {
|
||||||
// 非 base64 图片(OpenAI URL 等),下载并上传到 S3
|
// 非 base64 图片(OpenAI URL 等),必须下载并上传到 S3(国内中转)
|
||||||
console.log("[Gemini Image] Detected external URL in Gemini response, will download and upload to S3:", img.url.substring(0, 100) + "...");
|
console.log("[Gemini Image] Detected external URL in Gemini response, will download and upload to S3:", img.url.substring(0, 100) + "...");
|
||||||
const uploadPromise = downloadAndUploadToS3(img.url)
|
const uploadPromise = downloadAndUploadToS3(img.url)
|
||||||
.then(urls => { images.push(urls); })
|
.then(urls => {
|
||||||
|
console.log("[Gemini Image] Successfully downloaded and uploaded Gemini external image to S3");
|
||||||
|
images.push(urls);
|
||||||
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("[Gemini Image] Failed to download/upload URL in Gemini response, falling back to original:", error);
|
console.error("[Gemini Image] Failed to download/upload URL in Gemini response after retries:", error);
|
||||||
images.push({ thumbnail: img.url, original: img.url });
|
// 不再 fallback 到原始 URL,而是抛出错误
|
||||||
|
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
|
||||||
});
|
});
|
||||||
uploadPromises.push(uploadPromise);
|
uploadPromises.push(uploadPromise);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -526,3 +526,4 @@ curl http://localhost:8002/health
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user