Initial clean project import
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
import { ipcMain } from "electron";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs";
|
||||
import { execFile } from "child_process";
|
||||
import axios from "axios";
|
||||
import { AppEnv } from "../env";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// 定义贴纸生成参数接口
|
||||
interface GenerateStickerParams {
|
||||
prompt: string;
|
||||
apiKey: string;
|
||||
provider?: string; // 暂时默认支持 Google/Gemini
|
||||
}
|
||||
|
||||
// 临时和输出目录
|
||||
// 获取目录路径的辅助函数
|
||||
const getDirs = () => {
|
||||
// 确保 AppEnv 已初始化
|
||||
if (!AppEnv.userData || !AppEnv.appRoot) {
|
||||
throw new Error("AppEnv not initialized");
|
||||
}
|
||||
const tempDir = path.join(AppEnv.userData, "temp", "stickers", "raw");
|
||||
// 🔧 修复:使用 userData 目录存储生成的贴纸,确保在生产环境(如 Program Files)中有写入权限
|
||||
const outputDir = path.join(AppEnv.userData, "stickers"); // 最终贴纸目录
|
||||
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(tempDir)) {
|
||||
fs.mkdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
return { tempDir, outputDir };
|
||||
};
|
||||
|
||||
/**
|
||||
* 调用 Google Imagen / Gemini API 生成图像
|
||||
* 注意:这里假设使用 Gemini 的图像生成能力,具体 API 格式可能需要根据实际使用的模型调整。
|
||||
* 为了简化,我们暂时模拟一个请求或使用通用的 HTTP 请求结构。
|
||||
*
|
||||
* 如果是 Vertex AI 或 Gemini Pro Vision,API 可能会有所不同。
|
||||
* 这里演示一个通用的 fetch 流程,实际对接时可能需要调整 Endpoint。
|
||||
*/
|
||||
async function callImageGenerationApi(prompt: string, apiKey: string): Promise<Buffer> {
|
||||
// TODO: 替换为真实的 Imagen 3 / Gemini 图像生成 API 端点
|
||||
// 目前 Google AI Studio 的 Gemini API 原生支持生图的 endpoint 比较新
|
||||
// 这里暂时为了演示流程,我们假设有一个兼容 OpenAI DALL-E 格式或类似的接口
|
||||
// 或者我们直接使用 Google GenAI 的 REST API
|
||||
|
||||
// 注意:Gemini API 的生图功能目前通过 vertex ai 或特定 endpoint 暴露
|
||||
// 为保证可用性,如果用户还没有配置特定的生图模型,我们可能需要一个 fallback 或者明确的报错
|
||||
|
||||
// 这里为了演示,我们先写一个占位逻辑,实际开发中需要替换为真实的 API 调用
|
||||
// 如果没有真实的 API 可用,演示阶段可以先返回一个随机的本地图片或报错
|
||||
|
||||
// 假设使用 OpenAI 格式的 DALL-E 3 (如果用户配置了 OpenAI)
|
||||
// 或者使用 Google 的 API。
|
||||
// 由于用户明确提到 Gemini 3,我们需要确认 Endpoint。
|
||||
//
|
||||
// 如果无法直接通过 HTTP 调用 Gemini 生图,目前许多集成是分开的。
|
||||
//
|
||||
// **临时方案**:为了跑通流程,我们先模拟一个下载图片的逻辑 (比如从一些免费图库或者 placeholder 服务),
|
||||
// 等用户提供明确的生图 API Key 和 Endpoint 后再替换。
|
||||
//
|
||||
// 但根据任务要求,我是要实现 "AI Sticker Generation"。
|
||||
//
|
||||
// 让我们尝试使用 Google Generative Language API 的生图 (Imagen)
|
||||
// https://generativelanguage.googleapis.com/v1beta/models/image-generation:predict (假设)
|
||||
|
||||
// 由于不知道用户具体的 Key 权限,我们先写通用的 Axios 调用结构。
|
||||
|
||||
console.log('[Sticker] Generating image for prompt:', prompt);
|
||||
|
||||
// ⚠️ 占位:暂时抛出错误,提示需要真实 API 实现
|
||||
// 实际代码中,我会尝试调用一个公开的测试 API 或者 DALL-E 接口
|
||||
|
||||
// 尝试调用 OpenAI DALL-E (如果 keys 兼容) 或者是 Google 的 request
|
||||
//
|
||||
// 这是一个模拟的 Image Buffer 返回 (读取之前的 explosion.png 作为测试)
|
||||
const { app } = require('electron');
|
||||
const isDev = !app.isPackaged;
|
||||
const mockTestFile = isDev
|
||||
? path.join(app.getAppPath(), "resources", "extra", "stickers", "explosion.png")
|
||||
: path.join(process.resourcesPath, "extra", "stickers", "explosion.png");
|
||||
if (fs.existsSync(mockTestFile)) {
|
||||
// 为了测试流程,我们先返回这个文件的 buffer,假装这是 AI 生成的
|
||||
// 并在文件名上加随机后缀以示区别
|
||||
console.log('[Sticker] (Mock) Using placeholder image as generated result');
|
||||
return fs.readFileSync(mockTestFile);
|
||||
}
|
||||
|
||||
throw new Error("Image generation API not fully configured yet.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Python 脚本移除背景
|
||||
*/
|
||||
async function removeBackground(inputPath: string): Promise<string> {
|
||||
const { app } = require('electron');
|
||||
const { outputDir } = getDirs(); // 获取目录
|
||||
const isDev = !app.isPackaged;
|
||||
|
||||
let pythonPath: string;
|
||||
let scriptPath: string;
|
||||
const pythonExe = process.platform === 'win32' ? 'python.exe' : 'python3';
|
||||
|
||||
if (isDev) {
|
||||
pythonPath = path.join(app.getAppPath(), "resources", "extra", "common", "python", pythonExe);
|
||||
scriptPath = path.join(app.getAppPath(), "resources", "extra", "common", "python-scripts", "sticker_processor.py");
|
||||
console.log('[Sticker] 开发模式 - Python路径:', pythonPath);
|
||||
console.log('[Sticker] 开发模式 - 脚本路径:', scriptPath);
|
||||
} else {
|
||||
pythonPath = path.join(process.resourcesPath, "extra", "common", "python", pythonExe);
|
||||
scriptPath = path.join(process.resourcesPath, "extra", "common", "python-scripts", "sticker_processor.py");
|
||||
console.log('[Sticker] 生产模式 - Python路径:', pythonPath);
|
||||
console.log('[Sticker] 生产模式 - 脚本路径:', scriptPath);
|
||||
}
|
||||
|
||||
const outputFilename = `sticker_${uuidv4()}.png`;
|
||||
const outputPath = path.join(outputDir, outputFilename);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('[Sticker] Running rembg...', inputPath, '->', outputPath);
|
||||
execFile(pythonPath, [scriptPath, "-i", inputPath, "-o", outputPath], (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error('[Sticker] Rembg error:', error);
|
||||
console.error('[Sticker] Rembg stderr:', stderr);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
console.log('[Sticker] Rembg output:', stdout);
|
||||
// 返回相对于 resources/extra/stickers 的路径,或者绝对路径
|
||||
// 前端通常需要 correct url,这里返回文件名,由前端拼凑
|
||||
resolve(outputFilename);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 生成贴纸的主入口
|
||||
*/
|
||||
async generate(event: any, params: GenerateStickerParams) {
|
||||
try {
|
||||
console.log('[Sticker] Receive generate request:', params);
|
||||
const { prompt, apiKey } = params;
|
||||
const { tempDir, outputDir } = getDirs(); // 获取目录
|
||||
|
||||
// 1. 调用 AI 生成图片 (获得 Buffer)
|
||||
// 真实场景下:const imageBuffer = await callImageGenerationApi(prompt, apiKey);
|
||||
//
|
||||
// 🚧 MOCK: 为了演示流程,我们暂时跳过 API 调用,直接复制一个现有的图模拟生成的 "Raw Image"
|
||||
// 实际对接时,这里会是 axios output
|
||||
const mockRawPath = path.join(tempDir, `raw_${uuidv4()}.png`);
|
||||
|
||||
// 模拟:根据关键词选择预置的高质量贴纸 (Mock AI)
|
||||
let mockSourceFile = "comic_explosion.png"; // 默认
|
||||
const lowerPrompt = prompt.toLowerCase();
|
||||
|
||||
if (lowerPrompt.includes("sparkle") || lowerPrompt.includes("magic") || lowerPrompt.includes("star")) {
|
||||
mockSourceFile = "magic_sparkle.png";
|
||||
} else if (lowerPrompt.includes("electric") || lowerPrompt.includes("zap") || lowerPrompt.includes("lightning") || lowerPrompt.includes("bolt")) {
|
||||
mockSourceFile = "electric_zap.png";
|
||||
} else if (lowerPrompt.includes("fire") || lowerPrompt.includes("flame")) {
|
||||
// 如果有 fire_flame 再加,现在 fallback 到 comic_explosion 因为它也是暖色
|
||||
mockSourceFile = "comic_explosion.png";
|
||||
}
|
||||
|
||||
const placeholderSrc = path.join(outputDir, mockSourceFile); // 直接从 outputDir 取 (因为我们已经处理过了)
|
||||
|
||||
// 注意:因为其实这些图已经是处理好的(透明背景),逻辑上我们应该跳过 removeBackground?
|
||||
// 但是为了演示完整流程(模拟 raw -> processed),我们还是复制一份作为 "raw",再跑一次 rembg (虽然有点多余但逻辑通顺)
|
||||
// 或者:直接返回现有的 processed 文件。
|
||||
|
||||
// 为了保持流程一致性 (Mock Raw -> Rembg -> Output),我们假设这些 "High Quality" 图片是 AI 生成的"原始图" (哪怕它们已经透明了,rembg 处理透明图通常也没问题)
|
||||
|
||||
if (fs.existsSync(placeholderSrc)) {
|
||||
console.log(`[Sticker] Mocking generation using optimized asset: ${mockSourceFile}`);
|
||||
fs.copyFileSync(placeholderSrc, mockRawPath);
|
||||
} else {
|
||||
// Fallback to explosion.png if high quality asset missing
|
||||
const { app } = require('electron');
|
||||
const isDev = !app.isPackaged;
|
||||
const fallbackSrc = isDev
|
||||
? path.join(app.getAppPath(), "resources", "extra", "stickers", "explosion.png")
|
||||
: path.join(process.resourcesPath, "extra", "stickers", "explosion.png");
|
||||
if (fs.existsSync(fallbackSrc)) {
|
||||
fs.copyFileSync(fallbackSrc, mockRawPath);
|
||||
} else {
|
||||
throw new Error("Placeholder asset missing, cannot mock generation.");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 调用 Python 移除背景
|
||||
const finalFileName = await removeBackground(mockRawPath);
|
||||
|
||||
// 3. 返回结果
|
||||
return {
|
||||
code: 0,
|
||||
msg: "Success",
|
||||
data: {
|
||||
fileName: finalFileName,
|
||||
fullPath: path.join(outputDir, finalFileName),
|
||||
// 前端可能需要 file://协议的路径来预览
|
||||
previewUrl: `file://${path.join(outputDir, finalFileName).replace(/\\/g, '/')}`
|
||||
}
|
||||
};
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[Sticker] Generation failed:', error);
|
||||
return {
|
||||
code: -1,
|
||||
msg: error.message || "Unknown error"
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// 注册 IPC
|
||||
registerIpcHandlers() {
|
||||
ipcMain.handle("Sticker:generate", this.generate);
|
||||
ipcMain.handle("Sticker:list", this.list);
|
||||
},
|
||||
|
||||
/**
|
||||
* 列出所有可用贴纸
|
||||
*/
|
||||
async list() {
|
||||
try {
|
||||
const { outputDir } = getDirs();
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
return { code: 0, data: [] };
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(outputDir);
|
||||
const stickers = files
|
||||
.filter(file => /\.(png|jpg|jpeg|webp)$/i.test(file))
|
||||
.map(file => {
|
||||
const fullPath = path.join(outputDir, file);
|
||||
// ID 就是文件名 (不含扩展名可能更好,但为了唯一性先用文件名)
|
||||
// 或者我们用文件名作为ID,方便后续引用
|
||||
const id = file.split('.')[0];
|
||||
|
||||
// 读取文件转换为 Base64
|
||||
let previewUrl = '';
|
||||
try {
|
||||
const fileBuffer = fs.readFileSync(fullPath);
|
||||
const base64 = fileBuffer.toString('base64');
|
||||
const mimeType = file.endsWith('.png') ? 'image/png' : 'image/jpeg';
|
||||
previewUrl = `data:${mimeType};base64,${base64}`;
|
||||
} catch (e) {
|
||||
console.error(`[Sticker] Failed to read file ${file}`, e);
|
||||
// Fallback to file protocol if reading fails
|
||||
previewUrl = `file://${fullPath.replace(/\\/g, '/')}`;
|
||||
}
|
||||
|
||||
return {
|
||||
id: id,
|
||||
fileName: file,
|
||||
previewUrl: previewUrl,
|
||||
label: id // 暂时用 ID,前端可以根据 ID 映射中文名
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
code: 0,
|
||||
msg: "Success",
|
||||
data: stickers
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error('[Sticker] List failed:', error);
|
||||
return {
|
||||
code: -1,
|
||||
msg: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user