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:
1 parent
e03082629a
commit
8d36ad5267
2 files changed
+210
-16
No files matched your search
@@ -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 老用户更新指南(从旧版本升级)
|
||||
|
||||
|
||||
Executable
+170
@@ -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()
|
||||
Reference in new issue
Block a user