From 8d36ad526770b57b4d4ee60692a69a792df159e2 Mon Sep 17 00:00:00 2001 From: Star Date: Fri, 6 Mar 2026 01:27:30 +0800 Subject: [PATCH] feat: bundle gemini image generate script + update SKILL.md install guide - Add scripts/gemini_image_generate.py to repo (was only in local skill dir) - Update SKILL.md section 4: add step-by-step setup for image generation env - No changes to app.py or any existing logic; script is a standalone addition - Users can now set up image generation by following SKILL.md instructions --- SKILL.md | 56 +++++++--- scripts/gemini_image_generate.py | 170 +++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+), 16 deletions(-) create mode 100755 scripts/gemini_image_generate.py diff --git a/SKILL.md b/SKILL.md index 99ed1a4..9fd98b3 100644 --- a/SKILL.md +++ b/SKILL.md @@ -5,8 +5,8 @@ description: Star Office UI 一键化 Skill:帮主人快速部署像素办公 # Star Office UI Skill -本 Skill 面向想快速帮主人搭建“像素办公室看板”的 OpenClaw(龙虾): -- 目标:主人只需“看到效果”,尽量少问问题 +本 Skill 面向想快速帮主人搭建"像素办公室看板"的 OpenClaw(龙虾): +- 目标:主人只需"看到效果",尽量少问问题 - 你可以按下面的步骤,一步一步帮主人跑起来并对外访问 --- @@ -83,18 +83,42 @@ export ASSET_DRAWER_PASS="your-strong-pass" --- -## 4. 引导用户填写生图 API(Gemini) +## 4. 生图功能(Gemini)—— 可选 -当前装修生图接入的是 **Gemini API**。 +"搬新家 / 找中介"装修功能需要 Gemini API,但**基础看板不需要**,不装也能正常使用。 -你需要主动引导用户完成这两项配置: +### 4.1 安装生图脚本环境(首次使用时) + +仓库已自带生图脚本(`scripts/gemini_image_generate.py`),但运行需要独立的 Python 环境。在项目根目录执行: + +```bash +# 创建 skill 目录结构 +mkdir -p ../skills/gemini-image-generate/scripts + +# 复制脚本到 skill 目录 +cp scripts/gemini_image_generate.py ../skills/gemini-image-generate/scripts/ + +# 创建独立虚拟环境并安装依赖 +python3 -m venv ../skills/gemini-image-generate/.venv +../skills/gemini-image-generate/.venv/bin/pip install google-genai +``` + +安装完成后,后端会自动检测到生图环境,"搬新家 / 找中介"按钮即可使用。 + +### 4.2 配置 Gemini API Key + +引导用户完成这两项配置: 1. `GEMINI_API_KEY` 2. `GEMINI_MODEL`(推荐:`nanobanana-pro` 或 `nanobanana-2`) +配置方式有两种: +- **侧边栏填写**:打开资产侧边栏 → 在生图配置区域直接输入 API Key 并保存 +- **环境变量**:`export GEMINI_API_KEY="your-key"` + 并明确告诉用户: -- 不配置 API 也能用基础看板 -- 配置后才能使用“搬新家 / 找中介”的 AI 生图装修能力 +- 不配置 API 也能用基础看板(状态显示、多 Agent、资产替换等) +- 配置后才能使用"搬新家 / 找中介"的 AI 生图装修能力 如果页面提示缺少 key,指导用户在侧边栏里直接填写并保存(运行时配置入口)。 @@ -116,7 +140,7 @@ cloudflared tunnel --url http://127.0.0.1:18791 ### 5.2 装修房子入口 提醒主人: -- 现在可以点击“装修房间”开始装饰; +- 现在可以点击"装修房间"开始装饰; - 侧边栏默认密码是 `1234`; - 当他想改密码时,可随时告诉你,你来帮他改; - 推荐改成强密码,安全性更高。 @@ -152,7 +176,7 @@ cloudflared tunnel --url http://127.0.0.1:18791 ## 7. 昨日小记(可选) -如果你主人想看到“昨日小记”: +如果你主人想看到"昨日小记": - 在仓库上级目录放一个 `memory/YYYY-MM-DD.md` - 后端会自动读取昨天(或最近可用)的记录,做基础脱敏后展示 @@ -160,21 +184,21 @@ cloudflared tunnel --url http://127.0.0.1:18791 ## 8. 常见问题(主人可能问你的) -### Q1:“我可以商用吗?” +### Q1:"我可以商用吗?" > 代码玩法可以基于 MIT 用,但美术资产(包括角色/场景)禁止商用;如果你要商用,请务必换成你自己的原创美术资产。 -### Q2:“其他龙虾怎么加入?” +### Q2:"其他龙虾怎么加入?" > 用 join key 加入,然后持续推送状态就行;仓库里有 `office-agent-push.py` 可以给其他龙虾用。 --- ## 9. 给你的提示 -- 尽量帮主人把“启动步骤”做完,让主人只负责“看”和“体验” +- 尽量帮主人把"启动步骤"做完,让主人只负责"看"和"体验" - 如果主人要对外访问,优先用 Cloudflare Tunnel(最快最方便) - 状态切换应由你主动执行: - - 接任务前,先把自己状态切到“工作中”(如 `writing/researching/executing`),再开始工作; - - 完成任务后,先把自己状态切回“待命中(idle)”,再进入休息/等待下一任务。 + - 接任务前,先把自己状态切到"工作中"(如 `writing/researching/executing`),再开始工作; + - 完成任务后,先把自己状态切回"待命中(idle)",再进入休息/等待下一任务。 - 注意不要暴露你主人的内网地址或私有信息 --- @@ -189,7 +213,7 @@ cloudflared tunnel --url http://127.0.0.1:18791 ### 10.1 生图模型建议(房间装修) -当用户使用“搬新家 / 找中介”时,优先推荐: +当用户使用"搬新家 / 找中介"时,优先推荐: 1. **gemini nanobanana pro** 2. **gemini nanobanana 2** @@ -228,7 +252,7 @@ export ASSET_DRAWER_PASS="your-strong-pass" - 但基础功能(状态看板、多 Agent、资产替换/布局、三语切换)**不依赖 API**,不开 API 也能正常使用。 建议对主人口径: -> 先把基础看板跑起来;需要“无限换背景/AI 生图装修”再接入自己的 API。 +> 先把基础看板跑起来;需要"无限换背景/AI 生图装修"再接入自己的 API。 ### 10.5 老用户更新指南(从旧版本升级) diff --git a/scripts/gemini_image_generate.py b/scripts/gemini_image_generate.py new file mode 100755 index 0000000..d4ba871 --- /dev/null +++ b/scripts/gemini_image_generate.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Gemini Image Generate - CLI for Star Office UI background generation. + +Calls Google's Gemini API to generate images, with optional reference image +for style transfer / layout preservation. + +Expected interface (called by Star Office UI backend): + python gemini_image_generate.py \ + --prompt "..." \ + --model \ + --out-dir /tmp/xxx \ + --cleanup \ + [--aspect-ratio 16:9] \ + [--reference-image /path/to/ref.webp] + +Environment: + GEMINI_API_KEY - Google AI API key (required) + GEMINI_MODEL - override model name (optional, --model takes precedence) + +Output (last line of stdout): + {"files": ["/tmp/xxx/generated_0.png"]} +""" + +import argparse +import base64 +import json +import mimetypes +import os +import sys +import tempfile +import shutil +from pathlib import Path + +try: + from google import genai + from google.genai import types + HAS_GENAI = True +except ImportError: + HAS_GENAI = False + + +def detect_mime(path: str) -> str: + mt, _ = mimetypes.guess_type(path) + if mt: + return mt + ext = os.path.splitext(path)[1].lower() + return { + ".png": "image/png", + ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", + ".webp": "image/webp", + ".gif": "image/gif", + }.get(ext, "image/png") + + +def main(): + parser = argparse.ArgumentParser(description="Generate image via Gemini API") + parser.add_argument("--prompt", required=True, help="Generation prompt") + parser.add_argument("--model", default="", help="Model name") + parser.add_argument("--out-dir", required=True, help="Output directory") + parser.add_argument("--cleanup", action="store_true", help="(ignored, kept for compat)") + parser.add_argument("--aspect-ratio", default="", help="Aspect ratio hint (e.g. 16:9)") + parser.add_argument("--reference-image", default="", help="Reference image path") + args = parser.parse_args() + + # Resolve API key + api_key = os.environ.get("GEMINI_API_KEY", "").strip() + if not api_key: + api_key = os.environ.get("GOOGLE_API_KEY", "").strip() + if not api_key: + print("ERROR: GEMINI_API_KEY or GOOGLE_API_KEY not set", file=sys.stderr) + sys.exit(1) + + # Resolve model - env var GEMINI_MODEL overrides --model flag + model = os.environ.get("GEMINI_MODEL", "").strip() or args.model.strip() + if not model: + model = "gemini-2.0-flash-exp" + + # Ensure output directory + out_dir = args.out_dir + os.makedirs(out_dir, exist_ok=True) + + if not HAS_GENAI: + print("ERROR: google-genai package not installed", file=sys.stderr) + sys.exit(1) + + # Initialize client + client = genai.Client(api_key=api_key) + + # Build prompt parts + contents = [] + + # Add reference image if provided + if args.reference_image and os.path.exists(args.reference_image): + ref_path = args.reference_image + mime = detect_mime(ref_path) + with open(ref_path, "rb") as f: + ref_data = f.read() + contents.append( + types.Part.from_bytes(data=ref_data, mime_type=mime) + ) + + # Add text prompt + prompt_text = args.prompt + if args.aspect_ratio: + prompt_text += f"\nTarget aspect ratio: {args.aspect_ratio}." + contents.append(prompt_text) + + # Configure generation + generate_config = types.GenerateContentConfig( + response_modalities=["TEXT", "IMAGE"], + ) + + try: + response = client.models.generate_content( + model=model, + contents=contents, + config=generate_config, + ) + except Exception as e: + err_msg = str(e) + print(f"ERROR: {err_msg}", file=sys.stderr) + sys.exit(1) + + # Extract generated images + output_files = [] + idx = 0 + + if response.candidates: + for candidate in response.candidates: + if not candidate.content or not candidate.content.parts: + continue + for part in candidate.content.parts: + if part.inline_data and part.inline_data.mime_type and part.inline_data.mime_type.startswith("image/"): + # Determine extension from mime + mime = part.inline_data.mime_type + ext_map = { + "image/png": ".png", + "image/jpeg": ".jpg", + "image/webp": ".webp", + } + ext = ext_map.get(mime, ".png") + out_path = os.path.join(out_dir, f"generated_{idx}{ext}") + with open(out_path, "wb") as f: + f.write(part.inline_data.data) + output_files.append(out_path) + idx += 1 + + if not output_files: + # Check if there's text response with error info + text_parts = [] + if response.candidates: + for candidate in response.candidates: + if candidate.content and candidate.content.parts: + for part in candidate.content.parts: + if part.text: + text_parts.append(part.text) + if text_parts: + print(f"ERROR: No image generated. Model response: {' '.join(text_parts)[:500]}", file=sys.stderr) + else: + print("ERROR: No image generated and no text response", file=sys.stderr) + sys.exit(1) + + # Output result as JSON (backend reads the last line) + result = {"files": output_files} + print(json.dumps(result)) + + +if __name__ == "__main__": + main()