初始化提交

This commit is contained in:
2025-09-19 20:23:07 +08:00
parent c5ee5dd2a3
commit 7172b16917
36 changed files with 7302 additions and 100 deletions

View File

@@ -0,0 +1,327 @@
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?: Blob[] // Blob数组
temperature?: number
seed?: number
}
export interface EditRequest {
instruction: string
originalImage: Blob // Blob
referenceImages?: Blob[] // Blob数组
maskImage?: Blob // Blob
temperature?: number
seed?: number
}
export interface UsageMetadata {
totalTokenCount?: number
promptTokenCount?: number
candidatesTokenCount?: number
}
export interface SegmentationRequest {
image: Blob // Blob
query: string // "像素(x,y)处的对象" 或 "红色汽车"
}
export class GeminiService {
// 将Blob转换为base64的辅助函数
private async blobToBase64(blob: Blob): Promise<string> {
return new Promise((resolve, reject) => {
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);
});
}
async generateImage(request: GenerationRequest): Promise<{ images: Blob[]; usageMetadata?: any }> {
try {
const contents: any[] = [{ 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))
);
base64Images.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: Blob[] = []
// 检查响应是否存在以及是否有内容
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) {
// 将返回的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);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeString });
images.push(blob);
}
}
}
// 获取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: Blob[]; usageMetadata?: any }> {
try {
// 将Blob转换为base64以发送到API
const originalImageBase64 = await this.blobToBase64(request.originalImage);
const contents = [
{ text: this.buildEditPrompt(request) },
{
inlineData: {
mimeType: 'image/png',
data: originalImageBase64,
},
},
]
// 如果提供了参考图像则添加
if (request.referenceImages && request.referenceImages.length > 0) {
// 将Blob转换为base64以发送到API
const base64ReferenceImages = await Promise.all(
request.referenceImages.map(blob => this.blobToBase64(blob))
);
base64ReferenceImages.forEach(image => {
contents.push({
inlineData: {
mimeType: 'image/png',
data: image,
},
})
})
}
if (request.maskImage) {
// 将Blob转换为base64以发送到API
const maskImageBase64 = await this.blobToBase64(request.maskImage);
contents.push({
inlineData: {
mimeType: 'image/png',
data: maskImageBase64,
},
})
}
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: Blob[] = []
// 检查响应是否存在以及是否有内容
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) {
// 将返回的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);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blob = new Blob([ab], { type: mimeString });
images.push(blob);
}
}
}
// 获取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 {
// 将Blob转换为base64以发送到API
const imageBase64 = await this.blobToBase64(request.image);
const prompt = [
{
text: `分析此图像并为以下对象创建分割遮罩: ${request.query}
返回具有此确切结构的JSON对象:
{
"masks": [
{
"label": "分割对象的描述",
"box_2d": [x, y, width, height],
"mask": "base64编码的二进制遮罩图像"
}
]
}
仅分割请求的特定对象或区域。遮罩应该是二进制PNG其中白色像素(255)表示选定区域,黑色像素(0)表示背景。`,
},
{
inlineData: {
mimeType: 'image/png',
data: imageBase64,
},
},
]
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()