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 { // 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 { 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 }; } } };