修复 快捷键无效的问题;

修复 重复上传参考图的问题;
重新编写了README文档;
This commit is contained in:
2025-09-16 22:51:50 +08:00
parent ca8f086c93
commit a4583eb1f0
5 changed files with 217 additions and 97 deletions

View File

@@ -51,15 +51,15 @@ export const useImageGeneration = () => {
// 上传生成的图像和参考图像
if (accessToken) {
try {
// 上传生成的图像
// 上传生成的图像(跳过缓存,因为这些是新生成的图像)
const imageUrls = outputAssets.map(asset => asset.url);
const outputUploadResults = await uploadImages(imageUrls, accessToken);
const outputUploadResults = await uploadImages(imageUrls, accessToken, true);
// 上传参考图像(如果存在)
// 上传参考图像(如果存在,使用缓存机制
let referenceUploadResults: any[] = [];
if (request.referenceImages && request.referenceImages.length > 0) {
const referenceUrls = request.referenceImages.map(img => `data:image/png;base64,${img}`);
referenceUploadResults = await uploadImages(referenceUrls, accessToken);
referenceUploadResults = await uploadImages(referenceUrls, accessToken, false);
}
// 合并上传结果
@@ -300,7 +300,8 @@ export const useImageEditing = () => {
if (accessToken) {
try {
const imageUrls = outputAssets.map(asset => asset.url);
uploadResults = await uploadImages(imageUrls, accessToken);
// 上传编辑后的图像(跳过缓存,因为这些是新生成的图像)
uploadResults = await uploadImages(imageUrls, accessToken, true);
// 检查上传结果
const failedUploads = uploadResults.filter(r => !r.success);

View File

@@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { useAppStore } from '../store/useAppStore';
import { useImageGeneration, useImageEditing } from '../hooks/useImageGeneration';
export const useKeyboardShortcuts = () => {
const {
@@ -9,9 +10,19 @@ export const useKeyboardShortcuts = () => {
setShowPromptPanel,
showPromptPanel,
currentPrompt,
isGenerating
isGenerating,
selectedTool,
editReferenceImages,
canvasImage,
setCanvasImage,
temperature,
seed,
uploadedImages: generateUploadedImages
} = useAppStore();
const { generate } = useImageGeneration();
const { edit } = useImageEditing();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Ignore if user is typing in an input
@@ -21,7 +32,21 @@ export const useKeyboardShortcuts = () => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault();
if (!isGenerating && currentPrompt.trim()) {
console.log('Generate via keyboard shortcut');
// 触发生成操作
if (selectedTool === 'generate') {
const referenceImages = generateUploadedImages
.filter(img => img.includes('base64,'))
.map(img => img.split('base64,')[1]);
generate({
prompt: currentPrompt,
referenceImages: referenceImages.length > 0 ? referenceImages : undefined,
temperature,
seed: seed !== null ? seed : undefined
});
} else if (selectedTool === 'edit' || selectedTool === 'mask') {
edit(currentPrompt);
}
}
}
return;
@@ -54,10 +79,47 @@ export const useKeyboardShortcuts = () => {
console.log('Re-roll variants');
}
break;
case 'enter':
// 如果按Enter键且有提示词则触发生成
if (currentPrompt.trim() && !isGenerating) {
event.preventDefault();
if (selectedTool === 'generate') {
const referenceImages = generateUploadedImages
.filter(img => img.includes('base64,'))
.map(img => img.split('base64,')[1]);
generate({
prompt: currentPrompt,
referenceImages: referenceImages.length > 0 ? referenceImages : undefined,
temperature,
seed: seed !== null ? seed : undefined
});
} else if (selectedTool === 'edit' || selectedTool === 'mask') {
edit(currentPrompt);
}
}
break;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [setSelectedTool, setShowHistory, showHistory, setShowPromptPanel, showPromptPanel, currentPrompt, isGenerating]);
}, [
setSelectedTool,
setShowHistory,
showHistory,
setShowPromptPanel,
showPromptPanel,
currentPrompt,
isGenerating,
selectedTool,
generateUploadedImages,
editReferenceImages,
canvasImage,
setCanvasImage,
temperature,
seed,
generate,
edit
]);
};

View File

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

View File

@@ -4,13 +4,41 @@ import { UploadResult } from '../types'
// 上传接口URL
const UPLOAD_URL = 'https://api.pandorastudio.cn/auth/OSSupload'
// 创建一个Map来缓存已上传的图像
const uploadCache = new Map<string, UploadResult>()
/**
* 生成图像的唯一标识符
* @param base64Data - base64编码的图像数据
* @returns 图像的唯一标识符
*/
function getImageHash(base64Data: string): string {
// 使用简单的哈希函数生成图像标识符
let hash = 0;
for (let i = 0; i < base64Data.length; i++) {
const char = base64Data.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // 转换为32位整数
}
return hash.toString();
}
/**
* 将base64图像数据上传到指定接口
* @param base64Data - base64编码的图像数据
* @param accessToken - 访问令牌
* @param skipCache - 是否跳过缓存检查
* @returns 上传结果
*/
export const uploadImage = async (base64Data: string, accessToken: string): Promise<{ success: boolean; url?: string; error?: string }> => {
export const uploadImage = async (base64Data: string, accessToken: string, skipCache: boolean = false): Promise<{ success: boolean; url?: string; error?: string }> => {
// 检查缓存中是否已有该图像的上传结果
const imageHash = getImageHash(base64Data)
if (!skipCache && uploadCache.has(imageHash)) {
console.log('从缓存中获取上传结果')
return uploadCache.get(imageHash)!
}
try {
// 将base64数据转换为Blob
const byteString = atob(base64Data.split(',')[1])
@@ -44,13 +72,29 @@ export const uploadImage = async (base64Data: string, accessToken: string): Prom
// 使用环境变量中的VITE_UPLOAD_ASSET_URL作为前缀
const uploadAssetUrl = import.meta.env.VITE_UPLOAD_ASSET_URL || ''
const fullUrl = uploadAssetUrl ? `${uploadAssetUrl}/${result.data}` : result.data
return { success: true, url: fullUrl, error: undefined }
// 将上传结果存储到缓存中
const uploadResult = { success: true, url: fullUrl, error: undefined }
uploadCache.set(imageHash, {
...uploadResult,
timestamp: Date.now()
})
return uploadResult
} else {
throw new Error(`上传失败: ${result.msg}`)
}
} catch (error) {
console.error('上传图像时出错:', error)
return { success: false, url: undefined, error: error instanceof Error ? error.message : String(error) }
const errorResult = { success: false, url: undefined, error: error instanceof Error ? error.message : String(error) }
// 将失败的上传结果也存储到缓存中(可选)
uploadCache.set(imageHash, {
...errorResult,
timestamp: Date.now()
})
return errorResult
}
}
@@ -58,16 +102,17 @@ export const uploadImage = async (base64Data: string, accessToken: string): Prom
* 上传多个图像
* @param base64Images - base64编码的图像数组
* @param accessToken - 访问令牌
* @param skipCache - 是否跳过缓存检查
* @returns 上传结果数组
*/
export const uploadImages = async (base64Images: string[], accessToken: string): Promise<UploadResult[]> => {
export const uploadImages = async (base64Images: string[], accessToken: string, skipCache: boolean = false): Promise<UploadResult[]> => {
try {
const results: UploadResult[] = []
for (let i = 0; i < base64Images.length; i++) {
const base64Data = base64Images[i]
try {
const uploadResult = await uploadImage(base64Data, accessToken)
const uploadResult = await uploadImage(base64Data, accessToken, skipCache)
const result: UploadResult = {
success: uploadResult.success,
url: uploadResult.url,
@@ -101,3 +146,10 @@ export const uploadImages = async (base64Images: string[], accessToken: string):
throw error
}
}
/**
* 清除上传缓存
*/
export const clearUploadCache = (): void => {
uploadCache.clear()
}