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
This commit is contained in:
Star committed 2026-03-06 01:27:30 +08:00
1 parent e03082629a
commit 8d36ad5267
2 files changed
+210 -16

No files matched your search

+40 -16
View File
@@ -5,8 +5,8 @@ description: Star Office UI 一键化 Skill:帮主人快速部署像素办公
# 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` 1. `GEMINI_API_KEY`
2. `GEMINI_MODEL`(推荐:`nanobanana-pro` 或 `nanobanana-2`) 2. `GEMINI_MODEL`(推荐:`nanobanana-pro` 或 `nanobanana-2`)
配置方式有两种:
- **侧边栏填写**:打开资产侧边栏 → 在生图配置区域直接输入 API Key 并保存
- **环境变量**:`export GEMINI_API_KEY="your-key"`
并明确告诉用户: 并明确告诉用户:
- 不配置 API 也能用基础看板 - 不配置 API 也能用基础看板(状态显示、多 Agent、资产替换等)
- 配置后才能使用“搬新家 / 找中介”的 AI 生图装修能力 - 配置后才能使用"搬新家 / 找中介"的 AI 生图装修能力
如果页面提示缺少 key,指导用户在侧边栏里直接填写并保存(运行时配置入口)。 如果页面提示缺少 key,指导用户在侧边栏里直接填写并保存(运行时配置入口)。
@@ -116,7 +140,7 @@ cloudflared tunnel --url http://127.0.0.1:18791
### 5.2 装修房子入口 ### 5.2 装修房子入口
提醒主人: 提醒主人:
- 现在可以点击“装修房间”开始装饰; - 现在可以点击"装修房间"开始装饰;
- 侧边栏默认密码是 `1234`; - 侧边栏默认密码是 `1234`;
- 当他想改密码时,可随时告诉你,你来帮他改; - 当他想改密码时,可随时告诉你,你来帮他改;
- 推荐改成强密码,安全性更高。 - 推荐改成强密码,安全性更高。
@@ -152,7 +176,7 @@ cloudflared tunnel --url http://127.0.0.1:18791
## 7. 昨日小记(可选) ## 7. 昨日小记(可选)
如果你主人想看到“昨日小记”: 如果你主人想看到"昨日小记":
- 在仓库上级目录放一个 `memory/YYYY-MM-DD.md` - 在仓库上级目录放一个 `memory/YYYY-MM-DD.md`
- 后端会自动读取昨天(或最近可用)的记录,做基础脱敏后展示 - 后端会自动读取昨天(或最近可用)的记录,做基础脱敏后展示
@@ -160,21 +184,21 @@ cloudflared tunnel --url http://127.0.0.1:18791
## 8. 常见问题(主人可能问你的) ## 8. 常见问题(主人可能问你的)
### Q1:“我可以商用吗?” ### Q1:"我可以商用吗?"
> 代码玩法可以基于 MIT 用,但美术资产(包括角色/场景)禁止商用;如果你要商用,请务必换成你自己的原创美术资产。 > 代码玩法可以基于 MIT 用,但美术资产(包括角色/场景)禁止商用;如果你要商用,请务必换成你自己的原创美术资产。
### Q2:“其他龙虾怎么加入?” ### Q2:"其他龙虾怎么加入?"
> 用 join key 加入,然后持续推送状态就行;仓库里有 `office-agent-push.py` 可以给其他龙虾用。 > 用 join key 加入,然后持续推送状态就行;仓库里有 `office-agent-push.py` 可以给其他龙虾用。
--- ---
## 9. 给你的提示 ## 9. 给你的提示
- 尽量帮主人把“启动步骤”做完,让主人只负责“看”和“体验” - 尽量帮主人把"启动步骤"做完,让主人只负责"看"和"体验"
- 如果主人要对外访问,优先用 Cloudflare Tunnel(最快最方便) - 如果主人要对外访问,优先用 Cloudflare Tunnel(最快最方便)
- 状态切换应由你主动执行: - 状态切换应由你主动执行:
- 接任务前,先把自己状态切到“工作中”(如 `writing/researching/executing`),再开始工作; - 接任务前,先把自己状态切到"工作中"(如 `writing/researching/executing`),再开始工作;
- 完成任务后,先把自己状态切回“待命中(idle)”,再进入休息/等待下一任务。 - 完成任务后,先把自己状态切回"待命中(idle)",再进入休息/等待下一任务。
- 注意不要暴露你主人的内网地址或私有信息 - 注意不要暴露你主人的内网地址或私有信息
--- ---
@@ -189,7 +213,7 @@ cloudflared tunnel --url http://127.0.0.1:18791
### 10.1 生图模型建议(房间装修) ### 10.1 生图模型建议(房间装修)
当用户使用“搬新家 / 找中介”时,优先推荐: 当用户使用"搬新家 / 找中介"时,优先推荐:
1. **gemini nanobanana pro** 1. **gemini nanobanana pro**
2. **gemini nanobanana 2** 2. **gemini nanobanana 2**
@@ -228,7 +252,7 @@ export ASSET_DRAWER_PASS="your-strong-pass"
- 但基础功能(状态看板、多 Agent、资产替换/布局、三语切换)**不依赖 API**,不开 API 也能正常使用。 - 但基础功能(状态看板、多 Agent、资产替换/布局、三语切换)**不依赖 API**,不开 API 也能正常使用。
建议对主人口径: 建议对主人口径:
> 先把基础看板跑起来;需要“无限换背景/AI 生图装修”再接入自己的 API。 > 先把基础看板跑起来;需要"无限换背景/AI 生图装修"再接入自己的 API。
### 10.5 老用户更新指南(从旧版本升级) ### 10.5 老用户更新指南(从旧版本升级)
+170
View File
@@ -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 <model_name> \
--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()