You've already forked Nano-Banana-AI-Image-Editor
阶段性提交
This commit is contained in:
@@ -9,6 +9,8 @@ export interface GenerationRequest {
|
||||
referenceImages?: Blob[] // Blob数组
|
||||
temperature?: number
|
||||
seed?: number
|
||||
// 添加abortSignal参数
|
||||
abortSignal?: AbortSignal
|
||||
}
|
||||
|
||||
export interface EditRequest {
|
||||
@@ -18,6 +20,8 @@ export interface EditRequest {
|
||||
maskImage?: Blob // Blob
|
||||
temperature?: number
|
||||
seed?: number
|
||||
// 添加abortSignal参数
|
||||
abortSignal?: AbortSignal
|
||||
}
|
||||
|
||||
export interface UsageMetadata {
|
||||
@@ -29,48 +33,130 @@ export interface UsageMetadata {
|
||||
export interface SegmentationRequest {
|
||||
image: Blob // Blob
|
||||
query: string // "像素(x,y)处的对象" 或 "红色汽车"
|
||||
// 添加abortSignal参数
|
||||
abortSignal?: AbortSignal
|
||||
}
|
||||
|
||||
export class GeminiService {
|
||||
// 缓存base64图像数据,确保它们不会被清除
|
||||
private base64ImagesCache: Map<string, string> = new Map()
|
||||
|
||||
// 将Blob转换为base64的辅助函数
|
||||
private async blobToBase64(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const result = reader.result as string;
|
||||
const base64 = result.split(',')[1]; // Remove data:image/png;base64, prefix
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
const result = reader.result as string
|
||||
const base64 = result.split(',')[1] // Remove data:image/png;base64, prefix
|
||||
resolve(base64)
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(blob)
|
||||
})
|
||||
}
|
||||
|
||||
async generateImage(request: GenerationRequest): Promise<{ images: Blob[]; usageMetadata?: any }> {
|
||||
// 生成Blob的唯一标识符
|
||||
private async generateBlobId(blob: Blob): Promise<string> {
|
||||
// 使用Blob的部分内容生成唯一标识符
|
||||
const arrayBuffer = await blob.slice(0, 1024).arrayBuffer()
|
||||
const uint8Array = new Uint8Array(arrayBuffer)
|
||||
let hash = ''
|
||||
for (let i = 0; i < uint8Array.length; i++) {
|
||||
hash += uint8Array[i].toString(16).padStart(2, '0')
|
||||
}
|
||||
return `${blob.type}-${blob.size}-${hash.substring(0, 32)}`
|
||||
}
|
||||
|
||||
// 清理过期的缓存项(可选)
|
||||
private cleanupExpiredCache(): void {
|
||||
// 在这个实现中,我们不自动清理缓存
|
||||
// 只有在显式调用clearBase64Cache时才清理
|
||||
console.log('缓存大小:', this.base64ImagesCache.size)
|
||||
}
|
||||
|
||||
async generateImage(request: GenerationRequest): Promise<{ images: Blob[]; usageMetadata?: UsageMetadata }> {
|
||||
try {
|
||||
const contents: any[] = [{ text: request.prompt }]
|
||||
const contents: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }> = [{ text: request.prompt }]
|
||||
|
||||
// 如果提供了参考图像则添加
|
||||
if (request.referenceImages && request.referenceImages.length > 0) {
|
||||
// 将Blob转换为base64以发送到API
|
||||
const base64Images = await Promise.all(
|
||||
request.referenceImages.map(blob => this.blobToBase64(blob))
|
||||
);
|
||||
|
||||
const base64Images: string[] = []
|
||||
|
||||
// 为每个参考图像生成或获取base64数据
|
||||
for (const blob of request.referenceImages) {
|
||||
// 生成Blob的唯一标识符
|
||||
const blobId = await this.generateBlobId(blob)
|
||||
|
||||
let base64: string
|
||||
// 检查缓存中是否已有该图像的base64数据
|
||||
if (this.base64ImagesCache.has(blobId)) {
|
||||
// 从缓存中获取base64数据
|
||||
base64 = this.base64ImagesCache.get(blobId)!
|
||||
console.log('从缓存中获取参考图像base64数据')
|
||||
} else {
|
||||
// 转换Blob为base64并缓存结果
|
||||
base64 = await this.blobToBase64(blob)
|
||||
// 将base64数据存储到缓存中
|
||||
this.base64ImagesCache.set(blobId, base64)
|
||||
console.log('生成并缓存参考图像base64数据')
|
||||
}
|
||||
|
||||
// 如果base64数据为空,重新生成
|
||||
if (!base64 || base64.length === 0) {
|
||||
console.warn('参考图像base64数据为空,重新生成')
|
||||
base64 = await this.blobToBase64(blob)
|
||||
// 更新缓存
|
||||
this.base64ImagesCache.set(blobId, base64)
|
||||
}
|
||||
|
||||
base64Images.push(base64)
|
||||
}
|
||||
|
||||
base64Images.forEach(image => {
|
||||
contents.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: image,
|
||||
},
|
||||
})
|
||||
// 确保图像数据不为空
|
||||
if (image && image.length > 0) {
|
||||
contents.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: image,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.warn('跳过空的参考图像数据')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const response = await genAI.models.generateContent({
|
||||
// 检查contents是否包含有效的图像数据或文本提示
|
||||
const hasImageData = contents.some(item => item.inlineData && item.inlineData.data && item.inlineData.data.length > 0)
|
||||
const hasTextPrompt = contents.some(item => item.text && item.text.length > 0)
|
||||
|
||||
// 如果既没有图像数据也没有文本提示,抛出错误
|
||||
if (!hasImageData && !hasTextPrompt) {
|
||||
throw new Error('没有有效的图像数据或文本提示用于生成')
|
||||
}
|
||||
|
||||
// 准备请求配置,包括abortSignal
|
||||
const generateContentParams: {
|
||||
model: string;
|
||||
contents: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }>;
|
||||
config?: { httpOptions: { abortSignal: AbortSignal } };
|
||||
} = {
|
||||
model: 'gemini-2.5-flash-image-preview',
|
||||
contents,
|
||||
})
|
||||
}
|
||||
|
||||
// 如果提供了abortSignal,则添加到请求配置中
|
||||
if (request.abortSignal) {
|
||||
generateContentParams.config = {
|
||||
httpOptions: {
|
||||
abortSignal: request.abortSignal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await genAI.models.generateContent(generateContentParams)
|
||||
|
||||
// 检查是否有被禁止的内容
|
||||
if (response.candidates && response.candidates.length > 0) {
|
||||
@@ -79,24 +165,24 @@ export class GeminiService {
|
||||
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。')
|
||||
}
|
||||
if (candidate.finishReason === 'IMAGE_SAFETY') {
|
||||
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。')
|
||||
throw new Error('图像安全检查失败:请求的图像内容可能不安全。请尝试调整提示词。')
|
||||
}
|
||||
// 检查finishReason为STOP但没有inlineData的情况
|
||||
if (candidate.finishReason === 'STOP') {
|
||||
// 检查是否有inlineData
|
||||
let hasInlineData = false;
|
||||
let hasInlineData = false
|
||||
if (candidate.content && candidate.content.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.inlineData) {
|
||||
hasInlineData = true;
|
||||
break;
|
||||
hasInlineData = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果没有inlineData,则抛出错误
|
||||
if (!hasInlineData && candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
|
||||
throw new Error(candidate.content.parts[0].text || '生成失败:未返回图像数据');
|
||||
throw new Error(candidate.content.parts[0].text || '生成失败:未返回图像数据')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,15 +194,28 @@ export class GeminiService {
|
||||
for (const part of response.candidates[0].content.parts) {
|
||||
if (part.inlineData) {
|
||||
// 将返回的base64数据转换为Blob
|
||||
const byteString = atob(part.inlineData.data);
|
||||
const mimeString = part.inlineData.mimeType || 'image/png';
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
const byteString = atob(part.inlineData.data)
|
||||
const mimeString = part.inlineData.mimeType || 'image/png'
|
||||
const ab = new ArrayBuffer(byteString.length)
|
||||
const ia = new Uint8Array(ab)
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
ia[i] = byteString.charCodeAt(i)
|
||||
}
|
||||
const blob = new Blob([ab], { type: mimeString });
|
||||
images.push(blob);
|
||||
const blob = new Blob([ab], { type: mimeString })
|
||||
images.push(blob)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有图像数据但有文本响应,抛出包含文本的错误
|
||||
if (images.length === 0) {
|
||||
let textResponse = ''
|
||||
for (const part of response.candidates[0].content.parts) {
|
||||
if (part.text) {
|
||||
textResponse += part.text
|
||||
}
|
||||
}
|
||||
if (textResponse) {
|
||||
throw new Error(`生成失败:${textResponse}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,6 +226,10 @@ export class GeminiService {
|
||||
return { images, usageMetadata }
|
||||
} catch (error) {
|
||||
console.error('生成图像时出错:', error)
|
||||
// 检查是否是由于abortSignal导致的取消
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error('生成已取消')
|
||||
}
|
||||
if (error instanceof Error && error.message) {
|
||||
throw error
|
||||
}
|
||||
@@ -134,11 +237,35 @@ export class GeminiService {
|
||||
}
|
||||
}
|
||||
|
||||
async editImage(request: EditRequest): Promise<{ images: Blob[]; usageMetadata?: any }> {
|
||||
async editImage(request: EditRequest): Promise<{ images: Blob[]; usageMetadata?: UsageMetadata }> {
|
||||
try {
|
||||
// 将Blob转换为base64以发送到API
|
||||
const originalImageBase64 = await this.blobToBase64(request.originalImage);
|
||||
|
||||
// 将原始图像Blob转换为base64以发送到API
|
||||
let originalImageBase64: string
|
||||
|
||||
// 生成原始图像Blob的唯一标识符
|
||||
const originalBlobId = await this.generateBlobId(request.originalImage)
|
||||
|
||||
// 检查缓存中是否已有该图像的base64数据
|
||||
if (this.base64ImagesCache.has(originalBlobId)) {
|
||||
// 从缓存中获取base64数据
|
||||
originalImageBase64 = this.base64ImagesCache.get(originalBlobId)!
|
||||
console.log('从缓存中获取原始图像base64数据')
|
||||
} else {
|
||||
// 转换Blob为base64并缓存结果
|
||||
originalImageBase64 = await this.blobToBase64(request.originalImage)
|
||||
// 将base64数据存储到缓存中
|
||||
this.base64ImagesCache.set(originalBlobId, originalImageBase64)
|
||||
console.log('生成并缓存原始图像base64数据')
|
||||
}
|
||||
|
||||
// 如果base64数据为空,重新生成
|
||||
if (!originalImageBase64 || originalImageBase64.length === 0) {
|
||||
console.warn('原始图像base64数据为空,重新生成')
|
||||
originalImageBase64 = await this.blobToBase64(request.originalImage)
|
||||
// 更新缓存
|
||||
this.base64ImagesCache.set(originalBlobId, originalImageBase64)
|
||||
}
|
||||
|
||||
const contents = [
|
||||
{ text: this.buildEditPrompt(request) },
|
||||
{
|
||||
@@ -152,35 +279,123 @@ export class GeminiService {
|
||||
// 如果提供了参考图像则添加
|
||||
if (request.referenceImages && request.referenceImages.length > 0) {
|
||||
// 将Blob转换为base64以发送到API
|
||||
const base64ReferenceImages = await Promise.all(
|
||||
request.referenceImages.map(blob => this.blobToBase64(blob))
|
||||
);
|
||||
|
||||
const base64ReferenceImages: string[] = []
|
||||
|
||||
// 为每个参考图像生成或获取base64数据
|
||||
for (const blob of request.referenceImages) {
|
||||
// 生成Blob的唯一标识符
|
||||
const blobId = await this.generateBlobId(blob)
|
||||
|
||||
let base64: string
|
||||
// 检查缓存中是否已有该图像的base64数据
|
||||
if (this.base64ImagesCache.has(blobId)) {
|
||||
// 从缓存中获取base64数据
|
||||
base64 = this.base64ImagesCache.get(blobId)!
|
||||
console.log('从缓存中获取参考图像base64数据')
|
||||
} else {
|
||||
// 转换Blob为base64并缓存结果
|
||||
base64 = await this.blobToBase64(blob)
|
||||
// 将base64数据存储到缓存中
|
||||
this.base64ImagesCache.set(blobId, base64)
|
||||
console.log('生成并缓存参考图像base64数据')
|
||||
}
|
||||
|
||||
// 如果base64数据为空,重新生成
|
||||
if (!base64 || base64.length === 0) {
|
||||
console.warn('参考图像base64数据为空,重新生成')
|
||||
base64 = await this.blobToBase64(blob)
|
||||
// 更新缓存
|
||||
this.base64ImagesCache.set(blobId, base64)
|
||||
}
|
||||
|
||||
base64ReferenceImages.push(base64)
|
||||
}
|
||||
|
||||
base64ReferenceImages.forEach(image => {
|
||||
contents.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: image,
|
||||
},
|
||||
})
|
||||
// 确保图像数据不为空
|
||||
if (image && image.length > 0) {
|
||||
contents.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: image,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.warn('跳过空的参考图像数据')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (request.maskImage) {
|
||||
// 将Blob转换为base64以发送到API
|
||||
const maskImageBase64 = await this.blobToBase64(request.maskImage);
|
||||
contents.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: maskImageBase64,
|
||||
},
|
||||
})
|
||||
// 将遮罩图像Blob转换为base64以发送到API
|
||||
let maskImageBase64: string
|
||||
|
||||
// 生成遮罩图像Blob的唯一标识符
|
||||
const maskBlobId = await this.generateBlobId(request.maskImage)
|
||||
|
||||
// 检查缓存中是否已有该图像的base64数据
|
||||
if (this.base64ImagesCache.has(maskBlobId)) {
|
||||
// 从缓存中获取base64数据
|
||||
maskImageBase64 = this.base64ImagesCache.get(maskBlobId)!
|
||||
console.log('从缓存中获取遮罩图像base64数据')
|
||||
} else {
|
||||
// 转换Blob为base64并缓存结果
|
||||
maskImageBase64 = await this.blobToBase64(request.maskImage)
|
||||
// 将base64数据存储到缓存中
|
||||
this.base64ImagesCache.set(maskBlobId, maskImageBase64)
|
||||
console.log('生成并缓存遮罩图像base64数据')
|
||||
}
|
||||
|
||||
// 如果base64数据为空,重新生成
|
||||
if (!maskImageBase64 || maskImageBase64.length === 0) {
|
||||
console.warn('遮罩图像base64数据为空,重新生成')
|
||||
maskImageBase64 = await this.blobToBase64(request.maskImage)
|
||||
// 更新缓存
|
||||
this.base64ImagesCache.set(maskBlobId, maskImageBase64)
|
||||
}
|
||||
|
||||
// 确保遮罩图像数据不为空
|
||||
if (maskImageBase64 && maskImageBase64.length > 0) {
|
||||
contents.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: maskImageBase64,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
console.warn('跳过空的遮罩图像数据')
|
||||
}
|
||||
}
|
||||
|
||||
const response = await genAI.models.generateContent({
|
||||
// 检查contents是否包含有效的图像数据或文本提示
|
||||
const hasImageData = contents.some(item => item.inlineData && item.inlineData.data && item.inlineData.data.length > 0)
|
||||
const hasTextPrompt = contents.some(item => item.text && item.text.length > 0)
|
||||
|
||||
// 如果既没有图像数据也没有文本提示,抛出错误
|
||||
if (!hasImageData && !hasTextPrompt) {
|
||||
throw new Error('没有有效的图像数据或文本提示用于编辑')
|
||||
}
|
||||
|
||||
// 准备请求配置,包括abortSignal
|
||||
const generateContentParams: {
|
||||
model: string;
|
||||
contents: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }>;
|
||||
config?: { httpOptions: { abortSignal: AbortSignal } };
|
||||
} = {
|
||||
model: 'gemini-2.5-flash-image-preview',
|
||||
contents,
|
||||
})
|
||||
}
|
||||
|
||||
// 如果提供了abortSignal,则添加到请求配置中
|
||||
if (request.abortSignal) {
|
||||
generateContentParams.config = {
|
||||
httpOptions: {
|
||||
abortSignal: request.abortSignal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await genAI.models.generateContent(generateContentParams)
|
||||
|
||||
// 检查是否有被禁止的内容
|
||||
if (response.candidates && response.candidates.length > 0) {
|
||||
@@ -188,22 +403,25 @@ export class GeminiService {
|
||||
if (candidate.finishReason === 'PROHIBITED_CONTENT') {
|
||||
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。')
|
||||
}
|
||||
if (candidate.finishReason === 'IMAGE_SAFETY') {
|
||||
throw new Error('图像安全检查失败:请求的图像内容可能不安全。请尝试调整提示词。')
|
||||
}
|
||||
// 检查finishReason为STOP但没有inlineData的情况
|
||||
if (candidate.finishReason === 'STOP') {
|
||||
// 检查是否有inlineData
|
||||
let hasInlineData = false;
|
||||
let hasInlineData = false
|
||||
if (candidate.content && candidate.content.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.inlineData) {
|
||||
hasInlineData = true;
|
||||
break;
|
||||
hasInlineData = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果没有inlineData,则抛出错误
|
||||
if (!hasInlineData && candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
|
||||
throw new Error(candidate.content.parts[0].text || '编辑失败:未返回图像数据');
|
||||
throw new Error(candidate.content.parts[0].text || '编辑失败:未返回图像数据')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,15 +433,28 @@ export class GeminiService {
|
||||
for (const part of response.candidates[0].content.parts) {
|
||||
if (part.inlineData) {
|
||||
// 将返回的base64数据转换为Blob
|
||||
const byteString = atob(part.inlineData.data);
|
||||
const mimeString = part.inlineData.mimeType || 'image/png';
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
const byteString = atob(part.inlineData.data)
|
||||
const mimeString = part.inlineData.mimeType || 'image/png'
|
||||
const ab = new ArrayBuffer(byteString.length)
|
||||
const ia = new Uint8Array(ab)
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
ia[i] = byteString.charCodeAt(i)
|
||||
}
|
||||
const blob = new Blob([ab], { type: mimeString });
|
||||
images.push(blob);
|
||||
const blob = new Blob([ab], { type: mimeString })
|
||||
images.push(blob)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有图像数据但有文本响应,抛出包含文本的错误
|
||||
if (images.length === 0) {
|
||||
let textResponse = ''
|
||||
for (const part of response.candidates[0].content.parts) {
|
||||
if (part.text) {
|
||||
textResponse += part.text
|
||||
}
|
||||
}
|
||||
if (textResponse) {
|
||||
throw new Error(`编辑失败:${textResponse}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -234,6 +465,10 @@ export class GeminiService {
|
||||
return { images, usageMetadata }
|
||||
} catch (error) {
|
||||
console.error('编辑图像时出错:', error)
|
||||
// 检查是否是由于abortSignal导致的取消
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error('编辑已取消')
|
||||
}
|
||||
if (error instanceof Error && error.message) {
|
||||
throw error
|
||||
}
|
||||
@@ -241,11 +476,35 @@ export class GeminiService {
|
||||
}
|
||||
}
|
||||
|
||||
async segmentImage(request: SegmentationRequest): Promise<any> {
|
||||
async segmentImage(request: SegmentationRequest): Promise<{ masks: Array<{ label: string; box_2d: [number, number, number, number]; mask: string }> }> {
|
||||
try {
|
||||
// 将Blob转换为base64以发送到API
|
||||
const imageBase64 = await this.blobToBase64(request.image);
|
||||
|
||||
// 将图像Blob转换为base64以发送到API
|
||||
let imageBase64: string
|
||||
|
||||
// 生成图像Blob的唯一标识符
|
||||
const blobId = await this.generateBlobId(request.image)
|
||||
|
||||
// 检查缓存中是否已有该图像的base64数据
|
||||
if (this.base64ImagesCache.has(blobId)) {
|
||||
// 从缓存中获取base64数据
|
||||
imageBase64 = this.base64ImagesCache.get(blobId)!
|
||||
console.log('从缓存中获取分割图像base64数据')
|
||||
} else {
|
||||
// 转换Blob为base64并缓存结果
|
||||
imageBase64 = await this.blobToBase64(request.image)
|
||||
// 将base64数据存储到缓存中
|
||||
this.base64ImagesCache.set(blobId, imageBase64)
|
||||
console.log('生成并缓存分割图像base64数据')
|
||||
}
|
||||
|
||||
// 如果base64数据为空,重新生成
|
||||
if (!imageBase64 || imageBase64.length === 0) {
|
||||
console.warn('分割图像base64数据为空,重新生成')
|
||||
imageBase64 = await this.blobToBase64(request.image)
|
||||
// 更新缓存
|
||||
this.base64ImagesCache.set(blobId, imageBase64)
|
||||
}
|
||||
|
||||
const prompt = [
|
||||
{
|
||||
text: `分析此图像并为以下对象创建分割遮罩: ${request.query}
|
||||
@@ -263,18 +522,49 @@ export class GeminiService {
|
||||
|
||||
仅分割请求的特定对象或区域。遮罩应该是二进制PNG,其中白色像素(255)表示选定区域,黑色像素(0)表示背景。`,
|
||||
},
|
||||
{
|
||||
]
|
||||
|
||||
// 确保图像数据不为空
|
||||
if (imageBase64 && imageBase64.length > 0) {
|
||||
prompt.push({
|
||||
inlineData: {
|
||||
mimeType: 'image/png',
|
||||
data: imageBase64,
|
||||
},
|
||||
},
|
||||
]
|
||||
})
|
||||
} else {
|
||||
console.warn('跳过空的分割图像数据')
|
||||
}
|
||||
|
||||
const response = await genAI.models.generateContent({
|
||||
// 检查prompt是否包含有效的图像数据或文本提示
|
||||
const hasImageData = prompt.some(item => item.inlineData && item.inlineData.data && item.inlineData.data.length > 0)
|
||||
const hasTextPrompt = prompt.some(item => item.text && item.text.length > 0)
|
||||
|
||||
// 如果既没有图像数据也没有文本提示,抛出错误
|
||||
if (!hasImageData && !hasTextPrompt) {
|
||||
throw new Error('没有有效的图像数据或文本提示用于分割')
|
||||
}
|
||||
|
||||
// 准备请求配置,包括abortSignal
|
||||
const generateContentParams: {
|
||||
model: string;
|
||||
contents: Array<{ text: string } | { inlineData: { mimeType: string; data: string } }>;
|
||||
config?: { httpOptions: { abortSignal: AbortSignal } };
|
||||
} = {
|
||||
model: 'gemini-2.5-flash-image-preview',
|
||||
contents: prompt,
|
||||
})
|
||||
}
|
||||
|
||||
// 如果提供了abortSignal,则添加到请求配置中
|
||||
if (request.abortSignal) {
|
||||
generateContentParams.config = {
|
||||
httpOptions: {
|
||||
abortSignal: request.abortSignal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await genAI.models.generateContent(generateContentParams)
|
||||
|
||||
// 检查是否有被禁止的内容
|
||||
if (response.candidates && response.candidates.length > 0) {
|
||||
@@ -282,22 +572,25 @@ export class GeminiService {
|
||||
if (candidate.finishReason === 'PROHIBITED_CONTENT') {
|
||||
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。')
|
||||
}
|
||||
if (candidate.finishReason === 'IMAGE_SAFETY') {
|
||||
throw new Error('图像安全检查失败:请求的图像内容可能不安全。请尝试调整提示词。')
|
||||
}
|
||||
// 检查finishReason为STOP但没有inlineData的情况
|
||||
if (candidate.finishReason === 'STOP') {
|
||||
// 检查是否有inlineData
|
||||
let hasInlineData = false;
|
||||
let hasInlineData = false
|
||||
if (candidate.content && candidate.content.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (part.inlineData) {
|
||||
hasInlineData = true;
|
||||
break;
|
||||
hasInlineData = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果没有inlineData,则抛出错误
|
||||
if (!hasInlineData && candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
|
||||
throw new Error(candidate.content.parts[0].text || '分割失败:未返回结果数据');
|
||||
throw new Error(candidate.content.parts[0].text || '分割失败:未返回结果数据')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,6 +599,10 @@ export class GeminiService {
|
||||
return JSON.parse(responseText)
|
||||
} catch (error) {
|
||||
console.error('分割图像时出错:', error)
|
||||
// 检查是否是由于abortSignal导致的取消
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
throw new Error('分割已取消')
|
||||
}
|
||||
if (error instanceof Error && error.message) {
|
||||
throw error
|
||||
}
|
||||
@@ -316,12 +613,19 @@ export class GeminiService {
|
||||
private buildEditPrompt(request: EditRequest): string {
|
||||
const maskInstruction = request.maskImage ? '\n\n重要: 仅在遮罩图像显示白色像素(值255)的地方应用更改。完全不更改所有其他区域。精确遵守遮罩边界并在边缘保持无缝混合。' : ''
|
||||
|
||||
return `根据以下指令编辑此图像: ${request.instruction}
|
||||
return `根据以下指令编辑此图像: ${request.instruction}\n\n保持原始图像的光照、透视和整体构图。使更改看起来自然且无缝集成。${maskInstruction}\n\n保持图像质量并确保编辑看起来专业且逼真。`
|
||||
}
|
||||
|
||||
保持原始图像的光照、透视和整体构图。使更改看起来自然且无缝集成。${maskInstruction}
|
||||
// 公共方法:清除base64图像缓存
|
||||
public clearBase64Cache(): void {
|
||||
this.base64ImagesCache.clear()
|
||||
console.log('已清除base64图像缓存')
|
||||
}
|
||||
|
||||
保持图像质量并确保编辑看起来专业且逼真。`
|
||||
// 公共方法:获取缓存大小
|
||||
public getCacheSize(): number {
|
||||
return this.base64ImagesCache.size
|
||||
}
|
||||
}
|
||||
|
||||
export const geminiService = new GeminiService()
|
||||
export const geminiService = new GeminiService()
|
||||
Reference in New Issue
Block a user