Files
Nano-Banana-AI-Image-Editor/src/services/geminiService.ts

278 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { GoogleGenAI } from '@google/genai'
// 注意:在生产环境中,这应该通过后端代理处理
const API_KEY = import.meta.env.VITE_GEMINI_API_KEY || 'demo-key'
const genAI = new GoogleGenAI({ apiKey: API_KEY })
export interface GenerationRequest {
prompt: string
referenceImages?: string[] // base64数组
temperature?: number
seed?: number
}
export interface EditRequest {
instruction: string
originalImage: string // base64
referenceImages?: string[] // base64数组
maskImage?: string // base64
temperature?: number
seed?: number
}
export interface UsageMetadata {
totalTokenCount?: number
promptTokenCount?: number
candidatesTokenCount?: number
}
export interface SegmentationRequest {
image: string // base64
query: string // "像素(x,y)处的对象" 或 "红色汽车"
}
export class GeminiService {
async generateImage(request: GenerationRequest): Promise<{ images: string[]; usageMetadata?: any }> {
try {
const contents: any[] = [{ text: request.prompt }]
// 如果提供了参考图像则添加
if (request.referenceImages && request.referenceImages.length > 0) {
request.referenceImages.forEach(image => {
contents.push({
inlineData: {
mimeType: 'image/png',
data: image,
},
})
})
}
const response = await genAI.models.generateContent({
model: 'gemini-2.5-flash-image-preview',
contents,
})
// 检查是否有被禁止的内容
if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0]
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;
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
if (part.inlineData) {
hasInlineData = true;
break;
}
}
}
// 如果没有inlineData则抛出错误
if (!hasInlineData && candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
throw new Error(candidate.content.parts[0].text || '生成失败:未返回图像数据');
}
}
}
const images: string[] = []
// 检查响应是否存在以及是否有内容
if (response.candidates && response.candidates.length > 0 && response.candidates[0].content && response.candidates[0].content.parts) {
for (const part of response.candidates[0].content.parts) {
if (part.inlineData) {
images.push(part.inlineData.data)
}
}
}
// 获取usageMetadata如果存在
const usageMetadata = response.usageMetadata
return { images, usageMetadata }
} catch (error) {
console.error('生成图像时出错:', error)
if (error instanceof Error && error.message) {
throw error
}
throw new Error(`生成图像失败: ${error instanceof Error ? error.message : '未知错误'}`)
}
}
async editImage(request: EditRequest): Promise<{ images: string[]; usageMetadata?: any }> {
try {
const contents = [
{ text: this.buildEditPrompt(request) },
{
inlineData: {
mimeType: 'image/png',
data: request.originalImage,
},
},
]
// 如果提供了参考图像则添加
if (request.referenceImages && request.referenceImages.length > 0) {
request.referenceImages.forEach(image => {
contents.push({
inlineData: {
mimeType: 'image/png',
data: image,
},
})
})
}
if (request.maskImage) {
contents.push({
inlineData: {
mimeType: 'image/png',
data: request.maskImage,
},
})
}
const response = await genAI.models.generateContent({
model: 'gemini-2.5-flash-image-preview',
contents,
})
// 检查是否有被禁止的内容
if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0]
if (candidate.finishReason === 'PROHIBITED_CONTENT') {
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。')
}
// 检查finishReason为STOP但没有inlineData的情况
if (candidate.finishReason === 'STOP') {
// 检查是否有inlineData
let hasInlineData = false;
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
if (part.inlineData) {
hasInlineData = true;
break;
}
}
}
// 如果没有inlineData则抛出错误
if (!hasInlineData && candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
throw new Error(candidate.content.parts[0].text || '编辑失败:未返回图像数据');
}
}
}
const images: string[] = []
// 检查响应是否存在以及是否有内容
if (response.candidates && response.candidates.length > 0 && response.candidates[0].content && response.candidates[0].content.parts) {
for (const part of response.candidates[0].content.parts) {
if (part.inlineData) {
images.push(part.inlineData.data)
}
}
}
// 获取usageMetadata如果存在
const usageMetadata = response.usageMetadata
return { images, usageMetadata }
} catch (error) {
console.error('编辑图像时出错:', error)
if (error instanceof Error && error.message) {
throw error
}
throw new Error(`编辑图像失败: ${error instanceof Error ? error.message : '未知错误'}`)
}
}
async segmentImage(request: SegmentationRequest): Promise<any> {
try {
const prompt = [
{
text: `分析此图像并为以下对象创建分割遮罩: ${request.query}
返回具有此确切结构的JSON对象:
{
"masks": [
{
"label": "分割对象的描述",
"box_2d": [x, y, width, height],
"mask": "base64编码的二进制遮罩图像"
}
]
}
仅分割请求的特定对象或区域。遮罩应该是二进制PNG其中白色像素(255)表示选定区域,黑色像素(0)表示背景。`,
},
{
inlineData: {
mimeType: 'image/png',
data: request.image,
},
},
]
const response = await genAI.models.generateContent({
model: 'gemini-2.5-flash-image-preview',
contents: prompt,
})
// 检查是否有被禁止的内容
if (response.candidates && response.candidates.length > 0) {
const candidate = response.candidates[0]
if (candidate.finishReason === 'PROHIBITED_CONTENT') {
throw new Error('内容被禁止:您的请求包含不允许的内容。请尝试其他提示。')
}
// 检查finishReason为STOP但没有inlineData的情况
if (candidate.finishReason === 'STOP') {
// 检查是否有inlineData
let hasInlineData = false;
if (candidate.content && candidate.content.parts) {
for (const part of candidate.content.parts) {
if (part.inlineData) {
hasInlineData = true;
break;
}
}
}
// 如果没有inlineData则抛出错误
if (!hasInlineData && candidate.content && candidate.content.parts && candidate.content.parts.length > 0) {
throw new Error(candidate.content.parts[0].text || '分割失败:未返回结果数据');
}
}
}
const responseText = response.candidates[0].content.parts[0].text
return JSON.parse(responseText)
} catch (error) {
console.error('分割图像时出错:', error)
if (error instanceof Error && error.message) {
throw error
}
throw new Error(`分割图像失败: ${error instanceof Error ? error.message : '未知错误'}`)
}
}
private buildEditPrompt(request: EditRequest): string {
const maskInstruction = request.maskImage ? '\n\n重要: 仅在遮罩图像显示白色像素(值255)的地方应用更改。完全不更改所有其他区域。精确遵守遮罩边界并在边缘保持无缝混合。' : ''
return `根据以下指令编辑此图像: ${request.instruction}
保持原始图像的光照、透视和整体构图。使更改看起来自然且无缝集成。${maskInstruction}
保持图像质量并确保编辑看起来专业且逼真。`
}
}
export const geminiService = new GeminiService()