优化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
|
||||
* 带重试机制,确保成功下载并上传到国内中转
|
||||
*/
|
||||
async function downloadAndUploadToS3(imageUrl: string): Promise<{ thumbnail: string; original: string }> {
|
||||
try {
|
||||
console.log(`[Gemini Image] Downloading image from URL: ${imageUrl.substring(0, 100)}...`);
|
||||
|
||||
// 下载图片
|
||||
const response = await fetch(imageUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download image: ${response.statusText}`);
|
||||
async function downloadAndUploadToS3(imageUrl: string, retries = 3): Promise<{ thumbnail: string; original: string }> {
|
||||
for (let attempt = 0; attempt < retries; attempt++) {
|
||||
try {
|
||||
console.log(`[Gemini Image] Downloading image from URL (attempt ${attempt + 1}/${retries}): ${imageUrl.substring(0, 100)}...`);
|
||||
|
||||
// 下载图片(增加超时时间到 30 秒)
|
||||
const controller = new AbortController();
|
||||
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的格式)
|
||||
@ -253,14 +289,17 @@ async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail:
|
||||
.then(urls => { images.push(urls); });
|
||||
uploadPromises.push(uploadPromise);
|
||||
} 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) + "...");
|
||||
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 => {
|
||||
console.error("[Gemini Image] Failed to download/upload URL, falling back to original:", error);
|
||||
// 如果下载失败,使用原始URL(可能无法访问,但至少不会崩溃)
|
||||
images.push({ thumbnail: img.url, original: img.url });
|
||||
console.error("[Gemini Image] Failed to download/upload URL after retries:", error);
|
||||
// 不再 fallback 到原始 URL,而是抛出错误
|
||||
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
|
||||
});
|
||||
uploadPromises.push(uploadPromise);
|
||||
}
|
||||
@ -274,13 +313,17 @@ async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail:
|
||||
for (const result of response.results) {
|
||||
if (result.url && typeof result.url === "string") {
|
||||
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)
|
||||
.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 => {
|
||||
console.error("[Gemini Image] Failed to download/upload Nano Banana URL:", error);
|
||||
// 如果下载失败,使用原始URL
|
||||
images.push({ thumbnail: result.url, original: result.url });
|
||||
console.error("[Gemini Image] Failed to download/upload Nano Banana URL after retries:", error);
|
||||
// 不再 fallback 到原始 URL,而是抛出错误
|
||||
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
|
||||
});
|
||||
uploadPromises.push(uploadPromise);
|
||||
}
|
||||
@ -300,13 +343,17 @@ async function extractAndUploadImages(response: any): Promise<Array<{ thumbnail:
|
||||
.then(urls => { images.push(urls); });
|
||||
uploadPromises.push(uploadPromise);
|
||||
} 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) + "...");
|
||||
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 => {
|
||||
console.error("[Gemini Image] Failed to download/upload URL in Gemini response, falling back to original:", error);
|
||||
images.push({ thumbnail: img.url, original: img.url });
|
||||
console.error("[Gemini Image] Failed to download/upload URL in Gemini response after retries:", error);
|
||||
// 不再 fallback 到原始 URL,而是抛出错误
|
||||
throw new Error(`无法下载并上传图片到国内中转: ${error instanceof Error ? error.message : "未知错误"}`);
|
||||
});
|
||||
uploadPromises.push(uploadPromise);
|
||||
}
|
||||
|
||||
@ -526,3 +526,4 @@ curl http://localhost:8002/health
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user