diff --git a/video-ai-system/engines/char-hero-design-packer/char-hero-design-packer.py b/video-ai-system/engines/char-hero-design-packer/char-hero-design-packer.py index 65e623f..cc30c4f 100644 --- a/video-ai-system/engines/char-hero-design-packer/char-hero-design-packer.py +++ b/video-ai-system/engines/char-hero-design-packer/char-hero-design-packer.py @@ -29,7 +29,8 @@ sys.path.insert(0, str(PROJECT_ROOT / "engines")) try: from image_api_adapter import generate_image except Exception as exc: - print(f"⚠️ Python图片生成适配器未接通: {exc}") + print(f"⚠️ 图片生成适配器未接通: {exc}") + print("💡 桥接需要 Node.js: image-api-bridge.js + image-api-adapter.js") generate_image = None try: diff --git a/video-ai-system/engines/image-api-bridge.js b/video-ai-system/engines/image-api-bridge.js new file mode 100644 index 0000000..f759f7c --- /dev/null +++ b/video-ai-system/engines/image-api-bridge.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node +/** + * image-api-bridge.js · Python ↔ Node 图片API桥接 + * D144 · 铸渊 ICE-GL-ZY001 + * + * Python 通过 subprocess 调用本脚本,stdin 输入 JSON 命令,stdout 输出 JSON 结果。 + * 解决 char-hero-design-packer.py 依赖 JS image-api-adapter 的问题。 + * + * 用法: + * echo '{"action":"generate_character_ref","charId":"CHAR-003","l0":"...","l1":"..."}' | node image-api-bridge.js + * echo '{"action":"generate_image","prompt":"...","size":"2048x2048","filename":"test.png"}' | node image-api-bridge.js + */ + +// 桥接模式:所有日志输出到 stderr,stdout 只输出最终 JSON 结果 +console.log = (...args) => process.stderr.write(args.join(' ') + '\n'); +console.warn = (...args) => process.stderr.write(args.join(' ') + '\n'); +console.error = (...args) => process.stderr.write(args.join(' ') + '\n'); + +const { generateImage, generateCharacterRef } = require('./image-api-adapter'); + +let input = ''; +process.stdin.setEncoding('utf8'); +process.stdin.on('data', chunk => input += chunk); +process.stdin.on('end', async () => { + try { + const req = JSON.parse(input || '{}'); + let result; + + switch (req.action) { + case 'generate_character_ref': { + const { charId, l0, l1 } = req; + if (!charId) throw new Error('缺少 charId'); + result = await generateCharacterRef({ + charId, + l0Descriptor: l0 || '中国古代修仙少年,16岁,亚洲面孔', + l1Config: l1 ? { clothing: l1 } : undefined, + }); + break; + } + + case 'generate_image': { + const { prompt, size, filename, outputPath } = req; + if (!prompt) throw new Error('缺少 prompt'); + result = await generateImage({ prompt, size, filename, outputPath }); + break; + } + + default: + throw new Error(`未知操作: ${req.action}`); + } + + process.stdout.write(JSON.stringify({ ok: true, data: result }) + '\n'); + process.exit(0); + } catch (err) { + process.stdout.write(JSON.stringify({ ok: false, error: err.message }) + '\n'); + process.exit(1); + } +}); + +// stdin 已关闭(pipe 模式),直接触发 end +if (process.stdin.isTTY) { + process.stderr.write('用法: echo \'{"action":"..."}\' | node image-api-bridge.js\n'); + process.exit(1); +} diff --git a/video-ai-system/engines/image_api_adapter.py b/video-ai-system/engines/image_api_adapter.py new file mode 100644 index 0000000..0e729ed --- /dev/null +++ b/video-ai-system/engines/image_api_adapter.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +image_api_adapter.py · Python端图片API适配器 +D144 · 铸渊 ICE-GL-ZY001 + +桥接到 Node.js image-api-bridge.js,为 char-hero-design-packer.py 提供 +generate_image() 函数。 + +本地密钥文件: ~/Documents/guanghulab-local-secrets/video-ai-system.env +不提交仓库,不硬编码密钥。 +""" + +import json +import os +import subprocess +from pathlib import Path + +ENGINES_DIR = Path(__file__).parent +BRIDGE_SCRIPT = ENGINES_DIR / "image-api-bridge.js" +NODE_BIN = "/Users/bingshuolingdianyuanhe/.workbuddy/binaries/node/versions/22.22.2/bin/node" + + +def _call_bridge(action: str, **kwargs) -> dict: + """ + 调用 Node.js 桥接脚本 + :param action: 操作名 (generate_character_ref | generate_image) + :param kwargs: 传给 JS 的参数 + :return: {ok: bool, data: {...} | error: str} + """ + if not BRIDGE_SCRIPT.exists(): + return {"ok": False, "error": f"桥接脚本不存在: {BRIDGE_SCRIPT}"} + + payload = {"action": action, **kwargs} + + try: + result = subprocess.run( + [NODE_BIN, str(BRIDGE_SCRIPT)], + input=json.dumps(payload, ensure_ascii=False), + capture_output=True, + text=True, + timeout=120, + cwd=str(ENGINES_DIR), + ) + except FileNotFoundError: + return {"ok": False, "error": f"Node 未找到: {NODE_BIN}"} + except subprocess.TimeoutExpired: + return {"ok": False, "error": "桥接调用超时(120秒)"} + + if result.returncode != 0: + stderr = result.stderr.strip()[:500] if result.stderr else "无输出" + return {"ok": False, "error": f"桥接退出码 {result.returncode}: {stderr}"} + + try: + return json.loads(result.stdout.strip()) + except json.JSONDecodeError: + return {"ok": False, "error": f"桥接输出非JSON: {result.stdout[:200]}"} + + +def generate_image(prompt: str, reference_image: str = None, + output_dir: str = None, output_name: str = None) -> str: + """ + 生成图片(为 char-hero-design-packer.py 提供接口) + + :param prompt: 提示词 + :param reference_image: 参考图路径(当前桥接为文生图,参考图暂不支持) + :param output_dir: 输出目录 + :param output_name: 输出文件名 + :return: 生成的图片路径,失败返回 None + """ + kwargs = {"prompt": prompt, "size": "2048x2048"} + + if output_dir and output_name: + kwargs["outputPath"] = os.path.join(output_dir, output_name) + elif output_name: + kwargs["filename"] = output_name + + result = _call_bridge("generate_image", **kwargs) + + if not result.get("ok"): + print(f" ⚠️ 图片生成失败: {result.get('error', '未知错误')}") + return None + + data = result.get("data", {}) + image_path = data.get("imagePath", "") + if image_path and os.path.isfile(image_path): + return image_path + return None + + +def generate_character_ref(char_id: str, l0_descriptor: str = None, + l1_clothing: str = None) -> str: + """ + 生成角色参考图 + + :param char_id: 角色编号,如 CHAR-003 + :param l0_descriptor: L0底层编码描述 + :param l1_clothing: L1服装配置 + :return: 生成的图片路径,失败返回 None + """ + kwargs = {"charId": char_id} + if l0_descriptor: + kwargs["l0"] = l0_descriptor + if l1_clothing: + kwargs["l1"] = l1_clothing + + result = _call_bridge("generate_character_ref", **kwargs) + + if not result.get("ok"): + print(f" ⚠️ 角色参考图生成失败: {result.get('error', '未知错误')}") + return None + + data = result.get("data", {}) + return data.get("imagePath", "")