Skill: markcookie/stable-diffusion-skill (version 1.0.1)

--- name: stable-diffusion description: "Complete Stable Diffusion WebUI integration skill. Supports txt2img, img2img, inpainting, ControlNet, LoRA, model switching, upscaling, and more via SD WebUI API. Use when user wants to generate images with Stable Diffusion, edit images, apply styles, use ControlNet, or manage SD models." description_zh: "完整的 Stable Diffusion 图像生成 Skill，支持文生图、图生图、局部重绘、ControlNet、LoRA、模型切换、超分辨率放大等全套功能，通过 SD WebUI API 调用本地部署实例。" description_en: "Full Stable Diffusion integration: txt2img, img2img, inpainting, ControlNet, LoRA, upscaling via local SD WebUI API." version: "1.0.0" homepage: https://github.com/AUTOMATIC1111/stable-diffusion-webui allowed-tools: Bash, Read, Write, Edit metadata: clawdbot: emoji: 🎨 requires: bins: - python env: - SD_WEBUI_URL pip: - requests - Pillow setup: - "确保本地已启动 Stable Diffusion WebUI（带 --api 参数）" - "默认地址：http://127.0.0.1:7860" - "安装依赖：pip install requests Pillow" --- # 🎨 Stable Diffusion Skill 通过本地部署的 **Stable Diffusion WebUI API** 实现完整的 AI 图像生成与编辑能力。 > **前提条件**：需要本地安装并启动 SD WebUI（AUTOMATIC1111 版本），启动时加上 `--api` 参数。 > 默认 URL：`http://127.0.0.1:7860`（可通过环境变量 `SD_WEBUI_URL` 修改） --- ## 🚀 快速开始 ### 检查连接状态 ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action status ``` ### 第一次使用流程 1. 确认 SD WebUI 已运行（`--api` 模式） 2. 检查连接：`python ${CLAUDE_SKILL_DIR}/sd_client.py --action status` 3. 查看可用模型：`python ${CLAUDE_SKILL_DIR}/sd_client.py --action list-models` 4. 生成第一张图！ --- ## 📋 功能速查表 | 功能 | 命令关键词 | 说明 | |------|-----------|------| | 文生图 | txt2img / 生成图片 / 画 | 从文字描述生成图像 | | 图生图 | img2img / 图片风格化 | 基于参考图生成新图 | | 局部重绘 | inpaint / 修图 / 重绘 | 修改图片特定区域 | | ControlNet | controlnet / 姿势控制 | 精确控制图像结构 | | 超分辨率 | upscale / 放大 / 高清 | 放大图片并增强细节 | | 模型管理 | 换模型 / 加载模型 | 切换检查点/LoRA | | 批量生成 | 批量 / batch | 一次生成多张变体 | | 提示词工具 | 优化提示词 / 翻译 | 将中文描述转为 SD 提示词 | --- ## 🖼️ 文生图（txt2img） ### 基本用法 用户说："帮我画一张..."、"生成一张..."、"txt2img..." **参数收集流程**： 1. 如果用户没提供详细参数，先询问关键信息： - 描述（正向提示词） - 风格偏好（写实/动漫/油画等） - 尺寸（512x512 / 768x768 / 512x768 等） 2. 将中文描述智能转换为 SD 提示词（见提示词转换规则） 3. 调用 API 生成 **执行命令**： ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action txt2img \ --prompt "1girl, beautiful, masterpiece, best quality" \ --negative-prompt "ugly, bad anatomy, blurry" \ --width 512 --height 768 \ --steps 20 --cfg-scale 7 \ --sampler "DPM++ 2M Karras" \ --seed -1 \ --output-dir "./sd_output" ``` **常用参数说明**： | 参数 | 默认值 | 说明 | |------|-------|------| | `--steps` | 20 | 采样步数（推荐 20-30） | | `--cfg-scale` | 7 | 提示词遵从度（1-30，推荐 5-12） | | `--sampler` | DPM++ 2M Karras | 采样器 | | `--seed` | -1 | 随机种子（-1=随机） | | `--width/height` | 512x512 | 图像尺寸（需为 8 的倍数） | | `--batch-size` | 1 | 每批生成数量 | | `--n-iter` | 1 | 批次数量 | --- ## 🔄 图生图（img2img） 用户说："把这张图片..."、"参考这张图风格化..."、"img2img..." ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action img2img \ --init-image "/path/to/image.png" \ --prompt "oil painting style, detailed" \ --denoising-strength 0.75 \ --steps 20 --cfg-scale 7 \ --output-dir "./sd_output" ``` **去噪强度（denoising-strength）指南**： - `0.1-0.3`：轻微修改，保留原图 70-90% - `0.4-0.6`：中度变化，半保留原始构图 - `0.7-0.9`：大幅改变，保留基本构图 - `1.0`：完全重新生成 --- ## 🖌️ 局部重绘（Inpainting） 用户说："把这张图的...改成..."、"修改图片中的..." ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action inpaint \ --init-image "/path/to/image.png" \ --mask-image "/path/to/mask.png" \ --prompt "new content for masked area" \ --inpainting-fill 1 \ --denoising-strength 0.85 \ --output-dir "./sd_output" ``` **蒙版填充模式**： - `0`：填充（fill） - `1`：原始（original） - `2`：潜空间噪声（latent noise） - `3`：潜空间无（latent nothing） --- ## 🎛️ ControlNet 控制 用户说："参考这个姿势..."、"用 ControlNet..."、"保持构图生成..." ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action txt2img-controlnet \ --prompt "beautiful girl, detailed" \ --control-image "/path/to/pose.png" \ --control-module "openpose" \ --control-model "control_v11p_sd15_openpose" \ --control-weight 1.0 \ --output-dir "./sd_output" ``` **常用 ControlNet 模块**： | 模块 | 用途 | |------|------| | `openpose` | 人体姿势控制 | | `canny` | 边缘线条控制 | | `depth` | 深度图控制 | | `normal` | 法线图控制 | | `scribble` | 手绘草图控制 | | `seg` | 语义分割控制 | | `tile` | 细节增强/高清修复 | | `lineart` | 线稿上色控制 | --- ## 🔍 超分辨率放大（Upscaling） 用户说："放大这张图..."、"高清化..."、"超分..." ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action upscale \ --image "/path/to/image.png" \ --upscaler "R-ESRGAN 4x+" \ --scale 2 \ --output-dir "./sd_output" ``` **可用放大算法**（需 SD WebUI 中已安装）： - `Latent`（潜空间放大，适合动漫） - `R-ESRGAN 4x+`（通用高质量） - `R-ESRGAN 4x+ Anime6B`（动漫专用） - `ESRGAN_4x`（经典ESRGAN） - `SwinIR 4x`（写实图像） --- ## 📦 模型管理 ### 查看可用模型 ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action list-models python ${CLAUDE_SKILL_DIR}/sd_client.py --action list-samplers python ${CLAUDE_SKILL_DIR}/sd_client.py --action list-upscalers python ${CLAUDE_SKILL_DIR}/sd_client.py --action list-loras python ${CLAUDE_SKILL_DIR}/sd_client.py --action list-vaes ``` ### 切换检查点模型 ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action switch-model \ --model-name "realisticVisionV60B1_v60B1VAE.safetensors" ``` ### 在提示词中使用 LoRA 在 prompt 中加入 `<lora:模型名:权重>` 语法： ``` 1girl, beautiful, <lora:koreanDollLikeness_v15:0.8>, masterpiece ``` --- ## 🎨 高清修复（Hires Fix） 在 txt2img 中启用 Hires Fix 生成高清大图： ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action txt2img \ --prompt "landscape, detailed" \ --width 512 --height 512 \ --enable-hr \ --hr-scale 2 \ --hr-upscaler "R-ESRGAN 4x+" \ --hr-steps 15 \ --denoising-strength 0.5 \ --output-dir "./sd_output" ``` --- ## 💡 提示词转换规则 当用户用中文描述时，按以下规则转换为 SD 提示词： ### 标准质量词（始终添加） **正向**（推荐加入）： ``` masterpiece, best quality, ultra-detailed, high resolution ``` **负向**（推荐加入）： ``` (worst quality:2), (low quality:2), blurry, ugly, bad anatomy, bad hands, extra limbs, deformed, mutated, poorly drawn face, text, watermark ``` ### 风格映射 | 中文 | 英文提示词 | |------|-----------| | 写实/真实 | photorealistic, ultra realistic, RAW photo | | 动漫/二次元 | anime style, 2d, cel shading | | 油画 | oil painting, painterly | | 水彩 | watercolor, soft colors | | 赛博朋克 | cyberpunk, neon lights, futuristic | | 古风/国风 | chinese traditional art, hanfu, ink wash | | 皮克斯风 | pixar style, 3d render, cartoon | ### 翻译辅助工具 ```bash python ${CLAUDE_SKILL_DIR}/prompt_helper.py \ --translate "美丽的中国女孩，穿着汉服，站在樱花树下" \ --style "anime" ``` --- ## 📊 工作流示例 ### 示例 1：生成动漫风格人物 用户："画一个穿着汉服的古风美女" ```bash # 步骤1：检查模型 python ${CLAUDE_SKILL_DIR}/sd_client.py --action list-models # 步骤2：生成图像 python ${CLAUDE_SKILL_DIR}/sd_client.py --action txt2img \ --prompt "1girl, beautiful chinese girl, hanfu, traditional chinese clothing, cherry blossom, long hair, masterpiece, best quality, anime style" \ --negative-prompt "(worst quality:2), (low quality:2), bad anatomy, ugly" \ --width 512 --height 768 \ --steps 25 --cfg-scale 7.5 \ --sampler "DPM++ 2M Karras" \ --output-dir "./sd_output" ``` ### 示例 2：风格化照片 用户："把这张照片变成油画风格" ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action img2img \ --init-image "photo.jpg" \ --prompt "oil painting, impressionist style, masterpiece, detailed brushwork" \ --negative-prompt "photo, realistic, blurry" \ --denoising-strength 0.65 \ --steps 25 --cfg-scale 8 \ --output-dir "./sd_output" ``` ### 示例 3：批量生成变体 用户："同一个主题生成4张不同变体" ```bash python ${CLAUDE_SKILL_DIR}/sd_client.py --action txt2img \ --prompt "fantasy landscape, magical forest" \ --batch-size 4 \ --seed -1 \ --output-dir "./sd_output" ``` --- ## ⚙️ 配置说明 ### 环境变量 | 变量 | 默认值 | 说明 | |------|-------|------| | `SD_WEBUI_URL` | `http://127.0.0.1:7860` | SD WebUI 地址 | | `SD_OUTPUT_DIR` | `./sd_output` | 默认输出目录 | | `SD_TIMEOUT` | `300` | 请求超时（秒） | ### 推荐采样器 | 采样器 | 适用场景 | |--------|---------| | DPM++ 2M Karras | 通用最佳（推荐） | | DPM++ SDE Karras | 细节丰富 | | Euler a | 快速多样 | | DDIM | 高一致性 | | UniPC | 快速高质量 | --- ## 🔧 故障排除 **连接失败**： - 确认 SD WebUI 已启动并带有 `--api` 参数 - 检查端口是否正确（默认 7860） - 若使用远程服务器，检查 `SD_WEBUI_URL` **生成超时**： - 增大 `SD_TIMEOUT` 环境变量 - 减少步数（`--steps`） - 降低图像尺寸 **CUDA 内存不足**： - 降低图像分辨率 - 减少 batch size - 在 SD WebUI 启动时添加 `--medvram` 或 `--lowvram` --- ## 📁 工具文件说明 | 文件 | 功能 | |------|------| | `sd_client.py` | 主 API 客户端（所有功能入口） | | `prompt_helper.py` | 提示词优化与中文翻译工具 | | `image_utils.py` | 图像预处理工具（Base64编解码等） | --- ## ⚖️ 免责声明 > **English:** > This skill is not affiliated with, endorsed by, or connected to AUTOMATIC1111 or the Stable Diffusion project in any way. It is a third-party integration tool that requires users to install SD WebUI independently. The user is solely responsible for ensuring their use of Stable Diffusion complies with applicable laws, terms of service, and licensing requirements. Generated images are subject to users' own responsibility and must comply with all relevant copyright and usage policies. > **中文：** > 本 Skill 与 AUTOMATIC1111 或 Stable Diffusion 项目没有任何隶属、认可或关联关系。它是一个第三方集成工具，需要用户自行安装 SD WebUI。用户全权负责确保其使用 Stable Diffusion 符合适用法律、服务条款和许可要求。生成的图片由用户自行承担责任。 --- ## 📜 许可证 - 本 Skill 采用 **MIT-0 License** 开源（完全开放，无需署名） - 本 Skill 不包含 Stable Diffusion 任何源码 - 使用本 Skill 需遵守 SD WebUI 的 **AGPL-3.0** 许可证

path: tools/image_utils.py
content: #!/usr/bin/env python3
"""
Image Utilities for Stable Diffusion Skill
- Base64 encoding/decoding
- Image resizing and preprocessing
- Mask generation helpers
- Result gallery generation
"""

import argparse
import base64
import json
import os
import sys
from pathlib import Path

try:
    from PIL import Image, ImageDraw, ImageFilter
    HAS_PIL = True
except ImportError:
    HAS_PIL = False


# ── Base64 Utilities ─────────────────────────────────────────────────────────────

def image_to_base64(image_path: str) -> str:
    """Convert image file to base64 string"""
    with open(image_path, "rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")


def base64_to_image(b64_str: str, output_path: str):
    """Save base64 string as image file"""
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    # Strip data URI prefix if present
    if "," in b64_str:
        b64_str = b64_str.split(",", 1)[1]
    with open(output_path, "wb") as f:
        f.write(base64.b64decode(b64_str))
    print(f"✅ 保存图像: {output_path}")


# ── Image Preprocessing ──────────────────────────────────────────────────────────

def resize_image(image_path: str, width: int, height: int, output_path: str = None):
    """Resize image to given dimensions"""
    if not HAS_PIL:
        print("❌ 需要 Pillow: pip install Pillow")
        sys.exit(1)

    img = Image.open(image_path)
    resized = img.resize((width, height), Image.LANCZOS)

    if output_path is None:
        base, ext = os.path.splitext(image_path)
        output_path = f"{base}_resized_{width}x{height}{ext}"

    resized.save(output_path)
    print(f"✅ 调整大小完成: {output_path} ({width}x{height})")
    return output_path


def resize_to_sd_size(image_path: str, output_path: str = None) -> str:
    """Resize image to nearest SD-compatible size (multiple of 8, max 1024)"""
    if not HAS_PIL:
        print("❌ 需要 Pillow: pip install Pillow")
        sys.exit(1)

    img = Image.open(image_path)
    w, h = img.size

    # Round to nearest multiple of 8
    def round_to_8(x):
        return max(8, (x // 8) * 8)

    # Scale down if too large
    max_dim = 1024
    if w > max_dim or h > max_dim:
        ratio = min(max_dim / w, max_dim / h)
        w = int(w * ratio)
        h = int(h * ratio)

    new_w = round_to_8(w)
    new_h = round_to_8(h)

    if (new_w, new_h) != img.size:
        img = img.resize((new_w, new_h), Image.LANCZOS)
        if output_path is None:
            base, ext = os.path.splitext(image_path)
            output_path = f"{base}_sd{ext}"
        img.save(output_path)
        print(f"✅ 调整为 SD 兼容尺寸: {new_w}x{new_h} -> {output_path}")
        return output_path
    else:
        print(f"ℹ️  图像尺寸 {w}x{h} 已是 SD 兼容格式")
        return image_path


def get_image_info(image_path: str):
    """Get image dimensions and format"""
    if not HAS_PIL:
        # Fallback: just show file size
        size = os.path.getsize(image_path)
        print(f"📸 文件: {image_path}")
        print(f"   大小: {size/1024:.1f} KB")
        return

    img = Image.open(image_path)
    w, h = img.size
    mode = img.mode
    fmt = img.format or "Unknown"

    print(f"📸 图像信息: {image_path}")
    print(f"   尺寸: {w} x {h}")
    print(f"   格式: {fmt}")
    print(f"   颜色模式: {mode}")
    print(f"   文件大小: {os.path.getsize(image_path)/1024:.1f} KB")

    # Check SD compatibility
    if w % 8 != 0 or h % 8 != 0:
        print(f"   ⚠️  尺寸不是8的倍数，建议调整")
    if w > 1024 or h > 1024:
        print(f"   ⚠️  尺寸过大，建议缩小到1024以内")


# ── Mask Utilities ───────────────────────────────────────────────────────────────

def create_blank_mask(image_path: str, output_path: str = None):
    """Create a blank (all black) mask with same size as input image"""
    if not HAS_PIL:
        print("❌ 需要 Pillow: pip install Pillow")
        sys.exit(1)

    img = Image.open(image_path)
    mask = Image.new("L", img.size, 0)  # All black = not masked

    if output_path is None:
        base, ext = os.path.splitext(image_path)
        output_path = f"{base}_mask.png"

    mask.save(output_path)
    print(f"✅ 创建空白蒙版: {output_path}")
    print("   提示: 用图像编辑器在需要重绘的区域涂白色")
    return output_path


def create_rect_mask(image_path: str, x1: int, y1: int, x2: int, y2: int,
                     output_path: str = None, blur: int = 0):
    """Create a rectangular mask"""
    if not HAS_PIL:
        print("❌ 需要 Pillow: pip install Pillow")
        sys.exit(1)

    img = Image.open(image_path)
    mask = Image.new("L", img.size, 0)
    draw = ImageDraw.Draw(mask)
    draw.rectangle([x1, y1, x2, y2], fill=255)

    if blur > 0:
        mask = mask.filter(ImageFilter.GaussianBlur(blur))

    if output_path is None:
        base, ext = os.path.splitext(image_path)
        output_path = f"{base}_mask_rect.png"

    mask.save(output_path)
    print(f"✅ 创建矩形蒙版: {output_path} (区域: {x1},{y1} - {x2},{y2})")
    return output_path


# ── Gallery Generator ────────────────────────────────────────────────────────────

def generate_gallery(image_paths: list, output_html: str = "gallery.html", title: str = "SD Generation Gallery"):
    """Generate a simple HTML gallery from image paths"""
    images_html = []
    for path in image_paths:
        abs_path = os.path.abspath(path)
        filename = os.path.basename(path)
        images_html.append(f'''
        <div class="card">
            <img src="file:///{abs_path.replace(chr(92), "/")}" alt="{filename}">
            <div class="caption">{filename}</div>
        </div>''')

    html = f"""<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>{title}</title>
<style>
  body {{ font-family: Arial, sans-serif; background: #1a1a2e; color: #eee; margin: 20px; }}
  h1 {{ color: #e94560; text-align: center; }}
  .grid {{ display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }}
  .card {{ background: #16213e; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 16px rgba(0,0,0,0.4); }}
  .card img {{ width: 100%; height: auto; display: block; }}
  .caption {{ padding: 8px 12px; font-size: 12px; color: #aaa; word-break: break-all; }}
</style>
</head>
<body>
<h1>🎨 {title}</h1>
<p style="text-align:center;color:#888">共 {len(image_paths)} 张图像</p>
<div class="grid">{''.join(images_html)}
</div>
</body>
</html>"""

    with open(output_html, "w", encoding="utf-8") as f:
        f.write(html)
    print(f"✅ 图库已生成: {os.path.abspath(output_html)}")
    return output_html


def scan_output_dir(directory: str = "./sd_output") -> list:
    """Scan directory for generated images"""
    if not os.path.exists(directory):
        print(f"⚠️  目录不存在: {directory}")
        return []

    exts = {".png", ".jpg", ".jpeg", ".webp"}
    images = []
    for f in sorted(Path(directory).iterdir()):
        if f.suffix.lower() in exts:
            images.append(str(f))

    print(f"📂 找到 {len(images)} 张图像 in {directory}")
    return images


# ── Main ────────────────────────────────────────────────────────────────────────

def main():
    parser = argparse.ArgumentParser(description="SD Image Utilities")
    sub = parser.add_subparsers(dest="cmd")

    # info
    p_info = sub.add_parser("info", help="查看图像信息")
    p_info.add_argument("image")

    # resize
    p_resize = sub.add_parser("resize", help="调整图像尺寸")
    p_resize.add_argument("image")
    p_resize.add_argument("--width", type=int)
    p_resize.add_argument("--height", type=int)
    p_resize.add_argument("--sd-compat", action="store_true", help="自动调整为SD兼容尺寸")
    p_resize.add_argument("--output", "-o")

    # mask
    p_mask = sub.add_parser("mask", help="创建重绘蒙版")
    p_mask.add_argument("image")
    p_mask.add_argument("--blank", action="store_true", help="创建空白蒙版")
    p_mask.add_argument("--rect", nargs=4, type=int, metavar=("X1", "Y1", "X2", "Y2"),
                         help="创建矩形蒙版")
    p_mask.add_argument("--blur", type=int, default=0, help="蒙版边缘模糊半径")
    p_mask.add_argument("--output", "-o")

    # to-b64
    p_b64 = sub.add_parser("to-b64", help="图像转Base64")
    p_b64.add_argument("image")

    # from-b64
    p_fb64 = sub.add_parser("from-b64", help="Base64转图像")
    p_fb64.add_argument("b64_string")
    p_fb64.add_argument("--output", "-o", required=True)

    # gallery
    p_gallery = sub.add_parser("gallery", help="生成HTML图库")
    p_gallery.add_argument("--dir", default="./sd_output", help="图像目录")
    p_gallery.add_argument("--output", "-o", default="gallery.html")
    p_gallery.add_argument("--title", default="SD Generation Gallery")

    args = parser.parse_args()

    if args.cmd == "info":
        get_image_info(args.image)
    elif args.cmd == "resize":
        if args.sd_compat:
            resize_to_sd_size(args.image, args.output)
        elif args.width and args.height:
            resize_image(args.image, args.width, args.height, args.output)
        else:
            print("❌ 需要提供 --width --height 或 --sd-compat")
    elif args.cmd == "mask":
        if args.blank:
            create_blank_mask(args.image, args.output)
        elif args.rect:
            create_rect_mask(args.image, *args.rect, output_path=args.output, blur=args.blur)
        else:
            print("❌ 需要 --blank 或 --rect x1 y1 x2 y2")
    elif args.cmd == "to-b64":
        b64 = image_to_base64(args.image)
        print(b64[:100] + "...")
        print(f"(总长度: {len(b64)} 字符)")
    elif args.cmd == "from-b64":
        base64_to_image(args.b64_string, args.output)
    elif args.cmd == "gallery":
        images = scan_output_dir(args.dir)
        if images:
            generate_gallery(images, args.output, args.title)
    else:
        parser.print_help()


if __name__ == "__main__":
    main()


path: tools/prompt_helper.py
content: #!/usr/bin/env python3
"""
Stable Diffusion Prompt Helper
- Chinese to English prompt translation
- Prompt optimization and quality tag injection
- Style preset management
"""

import argparse
import json
import sys
import os
import re


# ── Style Presets ───────────────────────────────────────────────────────────────
STYLE_PRESETS = {
    "realistic": {
        "positive": "photorealistic, ultra realistic, RAW photo, 8k uhd, masterpiece",
        "negative": "painting, cartoon, anime, blurry, bad anatomy"
    },
    "anime": {
        "positive": "anime style, 2d, cel shading, best quality, masterpiece",
        "negative": "3d, photorealistic, bad anatomy, bad hands"
    },
    "oil_painting": {
        "positive": "oil painting, painterly, impressionist, detailed brushwork, canvas texture",
        "negative": "photo, digital art, blurry"
    },
    "watercolor": {
        "positive": "watercolor painting, soft colors, transparent, fluid, artistic",
        "negative": "digital, sharp edges, photorealistic"
    },
    "cyberpunk": {
        "positive": "cyberpunk, neon lights, futuristic, sci-fi, high tech, dark atmosphere",
        "negative": "natural, rural, historical"
    },
    "chinese_art": {
        "positive": "chinese traditional art, ink wash painting, hanfu, guofeng, elegant",
        "negative": "western, modern clothing, photorealistic"
    },
    "pixar": {
        "positive": "pixar style, 3d render, disney, cartoon, vibrant colors, cute",
        "negative": "dark, realistic, 2d"
    },
    "sketch": {
        "positive": "pencil sketch, lineart, monochrome, detailed lines, hand drawn",
        "negative": "color, photorealistic, painting"
    },
    "fantasy": {
        "positive": "fantasy art, magical, ethereal, dramatic lighting, epic",
        "negative": "mundane, realistic, modern"
    },
    "portrait": {
        "positive": "portrait photography, studio lighting, shallow depth of field, detailed face",
        "negative": "landscape, bad face, distorted"
    }
}

# ── Chinese Keyword Mapping ─────────────────────────────────────────────────────
ZH_EN_MAP = {
    # 人物
    "女孩": "1girl", "男孩": "1boy", "女人": "woman", "男人": "man",
    "美女": "beautiful woman", "帅哥": "handsome man", "少女": "young girl",
    "老人": "elderly person", "儿童": "child",
    # 外貌
    "长发": "long hair", "短发": "short hair", "金发": "blonde hair",
    "黑发": "black hair", "卷发": "curly hair", "蓝眼": "blue eyes",
    "微笑": "smiling", "表情严肃": "serious expression",
    # 服装
    "汉服": "hanfu, traditional chinese clothing",
    "旗袍": "cheongsam, qipao",
    "和服": "kimono",
    "西装": "suit", "裙子": "dress", "T恤": "t-shirt",
    "盔甲": "armor", "战袍": "battle robe",
    # 场景
    "樱花": "cherry blossom, sakura",
    "森林": "forest", "城市": "city, urban",
    "海边": "beach, seaside", "山景": "mountain landscape",
    "夜晚": "night, nighttime", "日落": "sunset",
    "古代": "ancient, historical", "未来": "futuristic",
    "室内": "indoor, interior", "室外": "outdoor, exterior",
    # 光照
    "自然光": "natural lighting", "柔光": "soft lighting",
    "逆光": "backlit, rim lighting", "戏剧光": "dramatic lighting",
    "霓虹灯": "neon lights",
    # 质量
    "高质量": "high quality, masterpiece", "超详细": "ultra detailed",
    "4K": "4k resolution", "8K": "8k resolution",
    "超清": "ultra clear, high definition",
}

# ── Functions ───────────────────────────────────────────────────────────────────

def contains_chinese(text: str) -> bool:
    return bool(re.search(r'[\u4e00-\u9fff]', text))


def translate_zh_keywords(text: str) -> str:
    """Replace known Chinese keywords with English equivalents"""
    for zh, en in ZH_EN_MAP.items():
        text = text.replace(zh, en)
    return text


def optimize_prompt(prompt: str, style: str = None, add_quality: bool = True) -> dict:
    """
    Optimize a prompt by:
    1. Translating Chinese keywords
    2. Adding style preset
    3. Adding quality tags
    """
    # Translate Chinese
    if contains_chinese(prompt):
        print("ℹ️  检测到中文，正在转换...")
        translated = translate_zh_keywords(prompt)
        # For remaining Chinese, provide a reminder
        if contains_chinese(translated):
            remaining = re.findall(r'[\u4e00-\u9fff]+', translated)
            if remaining:
                print(f"⚠️  以下内容需手动翻译: {', '.join(set(remaining))}")
    else:
        translated = prompt

    positive_parts = [translated]
    negative_parts = []

    # Add style preset
    if style and style in STYLE_PRESETS:
        preset = STYLE_PRESETS[style]
        positive_parts.append(preset["positive"])
        negative_parts.append(preset["negative"])
        print(f"✨ 应用风格: {style}")

    # Add quality tags
    if add_quality:
        quality_positive = "masterpiece, best quality, ultra-detailed"
        quality_negative = "(worst quality:2), (low quality:2), blurry, ugly, bad anatomy, bad hands, extra fingers, deformed, mutated, poorly drawn face, watermark, text, signature"
        if quality_positive not in " ".join(positive_parts):
            positive_parts.append(quality_positive)
        negative_parts.append(quality_negative)

    final_positive = ", ".join(filter(None, positive_parts))
    final_negative = ", ".join(filter(None, negative_parts))

    return {
        "positive": final_positive,
        "negative": final_negative
    }


def list_styles():
    print("\n🎨 可用风格预设:")
    for name, preset in STYLE_PRESETS.items():
        print(f"\n  [{name}]")
        print(f"    正向: {preset['positive'][:60]}...")
        print(f"    负向: {preset['negative'][:60]}...")


def interactive_build():
    """Interactive prompt builder"""
    print("🛠️  交互式提示词生成器\n")

    subject = input("1. 主体描述 (人物/物体/场景): ").strip()
    style_input = input(f"2. 风格 ({'/'.join(STYLE_PRESETS.keys())}，留空跳过): ").strip()
    extra = input("3. 额外细节 (可选): ").strip()

    style = style_input if style_input in STYLE_PRESETS else None

    combined = ", ".join(filter(None, [subject, extra]))
    result = optimize_prompt(combined, style=style, add_quality=True)

    print(f"\n📋 优化后提示词:")
    print(f"  正向: {result['positive']}")
    print(f"  负向: {result['negative']}")

    # Output as command hint
    print(f"\n💡 使用命令:")
    print(f"  python sd_client.py --action txt2img \\")
    print(f"    --prompt \"{result['positive']}\" \\")
    print(f"    --negative-prompt \"{result['negative']}\"")


# ── Main ────────────────────────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(description="SD Prompt Helper")
    parser.add_argument("--translate", "-t", help="翻译并优化提示词")
    parser.add_argument("--style", "-s", help="应用风格预设")
    parser.add_argument("--list-styles", action="store_true", help="列出所有风格预设")
    parser.add_argument("--no-quality", action="store_true", help="不添加质量标签")
    parser.add_argument("--interactive", "-i", action="store_true", help="交互式构建")
    parser.add_argument("--output-format", default="text", choices=["text", "json"],
                        help="输出格式")
    args = parser.parse_args()

    if args.list_styles:
        list_styles()
        return

    if args.interactive:
        interactive_build()
        return

    if args.translate:
        result = optimize_prompt(
            args.translate,
            style=args.style,
            add_quality=not args.no_quality
        )
        if args.output_format == "json":
            print(json.dumps(result, ensure_ascii=False, indent=2))
        else:
            print(f"\n✅ 正向提示词:\n{result['positive']}")
            print(f"\n❌ 负向提示词:\n{result['negative']}")
        return

    parser.print_help()


if __name__ == "__main__":
    main()


path: tools/sd_client.py
content: #!/usr/bin/env python3
"""
Stable Diffusion WebUI API Client
Supports: txt2img, img2img, inpaint, ControlNet, upscale, model management
"""

import argparse
import base64
import json
import os
import sys
import time
from pathlib import Path

# Try to import requests, provide helpful error if missing
try:
    import requests
except ImportError:
    print("ERROR: 'requests' library not found. Install with: pip install requests")
    sys.exit(1)

# ── Configuration ──────────────────────────────────────────────────────────────
SD_WEBUI_URL = os.environ.get("SD_WEBUI_URL", "http://127.0.0.1:7860")
SD_TIMEOUT = int(os.environ.get("SD_TIMEOUT", "300"))
SD_OUTPUT_DIR = os.environ.get("SD_OUTPUT_DIR", "./sd_output")


# ── Helpers ────────────────────────────────────────────────────────────────────
def api_get(endpoint: str) -> dict:
    url = f"{SD_WEBUI_URL.rstrip('/')}{endpoint}"
    resp = requests.get(url, timeout=30)
    resp.raise_for_status()
    return resp.json()


def api_post(endpoint: str, payload: dict) -> dict:
    url = f"{SD_WEBUI_URL.rstrip('/')}{endpoint}"
    resp = requests.post(url, json=payload, timeout=SD_TIMEOUT)
    resp.raise_for_status()
    return resp.json()


def img_to_b64(path: str) -> str:
    with open(path, "rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")


def b64_to_img(b64_str: str, output_path: str):
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    with open(output_path, "wb") as f:
        f.write(base64.b64decode(b64_str))


def save_images(images_b64: list, output_dir: str, prefix: str = "output") -> list:
    Path(output_dir).mkdir(parents=True, exist_ok=True)
    timestamp = int(time.time())
    saved = []
    for i, img_b64 in enumerate(images_b64):
        path = os.path.join(output_dir, f"{prefix}_{timestamp}_{i+1}.png")
        b64_to_img(img_b64, path)
        saved.append(path)
    return saved


def print_result(saved_paths: list, info: dict = None):
    print("\n✅ 生成成功！")
    for p in saved_paths:
        print(f"  📁 {os.path.abspath(p)}")
    if info:
        seed = info.get("seed", "unknown") if isinstance(info, dict) else "unknown"
        print(f"  🌱 Seed: {seed}")


# ── Actions ────────────────────────────────────────────────────────────────────

def action_status():
    """Check connection and WebUI version"""
    try:
        data = api_get("/internal/sysinfo")
        print("✅ SD WebUI 连接正常")
        print(f"  版本: {data.get('Version', 'unknown')}")
        print(f"  Python: {data.get('Python', 'unknown')}")
    except Exception:
        # fallback
        try:
            data = api_get("/sdapi/v1/options")
            print("✅ SD WebUI 连接正常")
            model = data.get("sd_model_checkpoint", "unknown")
            print(f"  当前模型: {model}")
        except Exception as e:
            print(f"❌ 连接失败: {e}")
            print(f"  请确认 SD WebUI 已启动（带 --api 参数），地址: {SD_WEBUI_URL}")
            sys.exit(1)


def action_list_models():
    models = api_get("/sdapi/v1/sd-models")
    print(f"\n📦 可用模型 ({len(models)} 个):")
    for m in models:
        active = " ✓" if m.get("is_active") else ""
        print(f"  {m['title']}{active}")


def action_list_samplers():
    samplers = api_get("/sdapi/v1/samplers")
    print(f"\n🎲 可用采样器 ({len(samplers)} 个):")
    for s in samplers:
        print(f"  {s['name']}")


def action_list_upscalers():
    upscalers = api_get("/sdapi/v1/upscalers")
    print(f"\n🔍 可用放大算法 ({len(upscalers)} 个):")
    for u in upscalers:
        print(f"  {u['name']}")


def action_list_loras():
    try:
        loras = api_get("/sdapi/v1/loras")
        print(f"\n🎭 可用 LoRA ({len(loras)} 个):")
        for l in loras:
            print(f"  {l['name']}  (用法: <lora:{l['name']}:0.8>)")
    except Exception as e:
        print(f"⚠️  无法获取 LoRA 列表: {e}")


def action_list_vaes():
    try:
        vaes = api_get("/sdapi/v1/sd-vae")
        print(f"\n🗜️  可用 VAE ({len(vaes)} 个):")
        for v in vaes:
            print(f"  {v['model_name']}")
    except Exception as e:
        print(f"⚠️  无法获取 VAE 列表: {e}")


def action_switch_model(model_name: str):
    print(f"🔄 正在切换模型: {model_name} ...")
    api_post("/sdapi/v1/options", {"sd_model_checkpoint": model_name})
    print(f"✅ 模型已切换为: {model_name}")


def action_txt2img(args):
    payload = {
        "prompt": args.prompt or "",
        "negative_prompt": args.negative_prompt or "(worst quality:2),(low quality:2),blurry,ugly,bad anatomy,bad hands,extra limbs,deformed,mutated,watermark,text",
        "steps": args.steps,
        "cfg_scale": args.cfg_scale,
        "width": args.width,
        "height": args.height,
        "seed": args.seed,
        "batch_size": args.batch_size,
        "n_iter": args.n_iter,
        "sampler_name": args.sampler,
        "restore_faces": args.restore_faces,
        "tiling": args.tiling,
    }

    # Hires fix
    if args.enable_hr:
        payload.update({
            "enable_hr": True,
            "hr_scale": args.hr_scale,
            "hr_upscaler": args.hr_upscaler,
            "hr_second_pass_steps": args.hr_steps,
            "denoising_strength": args.denoising_strength,
        })

    # Styles
    if args.styles:
        payload["styles"] = args.styles

    print(f"🎨 正在生成图像 ({args.width}x{args.height}, {args.steps} steps)...")
    print(f"   提示词: {args.prompt[:80]}{'...' if len(args.prompt or '') > 80 else ''}")

    result = api_post("/sdapi/v1/txt2img", payload)
    images = result.get("images", [])
    info = json.loads(result.get("info", "{}"))
    saved = save_images(images, args.output_dir, "txt2img")
    print_result(saved, info)


def action_img2img(args):
    if not args.init_image:
        print("❌ 需要提供 --init-image 参数")
        sys.exit(1)

    payload = {
        "init_images": [img_to_b64(args.init_image)],
        "prompt": args.prompt or "",
        "negative_prompt": args.negative_prompt or "(worst quality:2),(low quality:2),blurry,ugly",
        "steps": args.steps,
        "cfg_scale": args.cfg_scale,
        "width": args.width,
        "height": args.height,
        "seed": args.seed,
        "batch_size": args.batch_size,
        "denoising_strength": args.denoising_strength,
        "sampler_name": args.sampler,
        "resize_mode": args.resize_mode,
    }

    print(f"🔄 正在图生图 (强度: {args.denoising_strength})...")
    result = api_post("/sdapi/v1/img2img", payload)
    images = result.get("images", [])
    info = json.loads(result.get("info", "{}"))
    saved = save_images(images, args.output_dir, "img2img")
    print_result(saved, info)


def action_inpaint(args):
    if not args.init_image:
        print("❌ 需要提供 --init-image 参数")
        sys.exit(1)
    if not args.mask_image:
        print("❌ 需要提供 --mask-image 参数")
        sys.exit(1)

    payload = {
        "init_images": [img_to_b64(args.init_image)],
        "mask": img_to_b64(args.mask_image),
        "prompt": args.prompt or "",
        "negative_prompt": args.negative_prompt or "(worst quality:2),(low quality:2),blurry",
        "steps": args.steps,
        "cfg_scale": args.cfg_scale,
        "width": args.width,
        "height": args.height,
        "seed": args.seed,
        "denoising_strength": args.denoising_strength,
        "sampler_name": args.sampler,
        "inpainting_fill": args.inpainting_fill,
        "inpaint_full_res": args.inpaint_full_res,
        "inpaint_full_res_padding": 32,
        "mask_blur": 4,
    }

    print(f"🖌️  正在局部重绘...")
    result = api_post("/sdapi/v1/img2img", payload)
    images = result.get("images", [])
    info = json.loads(result.get("info", "{}"))
    saved = save_images(images, args.output_dir, "inpaint")
    print_result(saved, info)


def action_txt2img_controlnet(args):
    if not args.control_image:
        print("❌ 需要提供 --control-image 参数")
        sys.exit(1)

    controlnet_unit = {
        "input_image": img_to_b64(args.control_image),
        "module": args.control_module,
        "model": args.control_model,
        "weight": args.control_weight,
        "enabled": True,
        "lowvram": False,
        "pixel_perfect": True,
        "guidance_start": 0.0,
        "guidance_end": 1.0,
        "control_mode": 0,
        "resize_mode": 1,
    }

    payload = {
        "prompt": args.prompt or "",
        "negative_prompt": args.negative_prompt or "(worst quality:2),(low quality:2),blurry,ugly",
        "steps": args.steps,
        "cfg_scale": args.cfg_scale,
        "width": args.width,
        "height": args.height,
        "seed": args.seed,
        "batch_size": args.batch_size,
        "sampler_name": args.sampler,
        "alwayson_scripts": {
            "ControlNet": {
                "args": [controlnet_unit]
            }
        }
    }

    print(f"🎛️  正在使用 ControlNet ({args.control_module}) 生成...")
    result = api_post("/sdapi/v1/txt2img", payload)
    images = result.get("images", [])
    info = json.loads(result.get("info", "{}"))
    saved = save_images(images, args.output_dir, "controlnet")
    print_result(saved, info)


def action_upscale(args):
    if not args.image:
        print("❌ 需要提供 --image 参数")
        sys.exit(1)

    payload = {
        "resize_mode": 0,
        "show_extras_results": True,
        "gfpgan_visibility": 0,
        "codeformer_visibility": args.codeformer_visibility,
        "codeformer_weight": 0.5,
        "upscaling_resize": args.scale,
        "upscaler_1": args.upscaler,
        "upscaler_2": "None",
        "extras_upscaler_2_visibility": 0,
        "image": img_to_b64(args.image),
    }

    print(f"🔍 正在放大图像 ({args.scale}x, {args.upscaler})...")
    result = api_post("/sdapi/v1/extra-single-image", payload)
    image_b64 = result.get("image")
    if not image_b64:
        print("❌ 放大失败")
        sys.exit(1)
    saved = save_images([image_b64], args.output_dir, "upscale")
    print_result(saved)


def action_interrogate(args):
    """Use CLIP/DeepDanbooru to analyze image and get prompts"""
    if not args.image:
        print("❌ 需要提供 --image 参数")
        sys.exit(1)

    payload = {
        "image": img_to_b64(args.image),
        "model": args.interrogate_model,
    }

    print(f"🔎 正在分析图像提示词 ({args.interrogate_model})...")
    result = api_post("/sdapi/v1/interrogate", payload)
    caption = result.get("caption", "")
    print(f"\n✅ 图像提示词:\n{caption}")


def action_png_info(args):
    """Extract generation info from PNG"""
    if not args.image:
        print("❌ 需要提供 --image 参数")
        sys.exit(1)

    payload = {"image": img_to_b64(args.image)}
    result = api_post("/sdapi/v1/png-info", payload)
    info = result.get("info", "")
    print(f"\n📋 图像生成信息:\n{info}")


def action_progress():
    """Check current generation progress"""
    result = api_get("/sdapi/v1/progress")
    progress = result.get("progress", 0)
    state = result.get("state", {})
    eta = result.get("eta_relative", 0)

    print(f"⏳ 生成进度: {progress*100:.1f}%")
    if state.get("job"):
        print(f"   任务: {state['job']}")
    if eta > 0:
        print(f"   预计剩余: {eta:.1f}秒")


def action_skip():
    """Skip current generation"""
    api_post("/sdapi/v1/skip", {})
    print("⏭️  已跳过当前生成")


def action_interrupt():
    """Interrupt current generation"""
    api_post("/sdapi/v1/interrupt", {})
    print("⏹️  已中断生成")


# ── Argument Parser ────────────────────────────────────────────────────────────
def build_parser():
    parser = argparse.ArgumentParser(
        description="Stable Diffusion WebUI API Client",
        formatter_class=argparse.RawDescriptionHelpFormatter
    )

    parser.add_argument("--action", required=True, choices=[
        "status", "list-models", "list-samplers", "list-upscalers",
        "list-loras", "list-vaes", "switch-model",
        "txt2img", "img2img", "inpaint", "txt2img-controlnet",
        "upscale", "interrogate", "png-info", "progress", "skip", "interrupt"
    ], help="执行的操作")

    # Common generation params
    gen = parser.add_argument_group("生成参数")
    gen.add_argument("--prompt", "-p", help="正向提示词")
    gen.add_argument("--negative-prompt", "-n", help="负向提示词")
    gen.add_argument("--steps", type=int, default=20, help="采样步数 (默认: 20)")
    gen.add_argument("--cfg-scale", type=float, default=7.0, help="提示词遵从度 (默认: 7.0)")
    gen.add_argument("--width", type=int, default=512, help="图像宽度 (默认: 512)")
    gen.add_argument("--height", type=int, default=512, help="图像高度 (默认: 512)")
    gen.add_argument("--seed", type=int, default=-1, help="随机种子 (-1=随机)")
    gen.add_argument("--sampler", default="DPM++ 2M Karras", help="采样器名称")
    gen.add_argument("--batch-size", type=int, default=1, help="每批生成数量")
    gen.add_argument("--n-iter", type=int, default=1, help="批次数量")
    gen.add_argument("--denoising-strength", type=float, default=0.75, help="去噪强度 (0-1)")
    gen.add_argument("--restore-faces", action="store_true", help="启用人脸修复")
    gen.add_argument("--tiling", action="store_true", help="启用无缝平铺")
    gen.add_argument("--styles", nargs="+", help="应用的样式名称")

    # Hires fix
    hr = parser.add_argument_group("高清修复 (Hires Fix)")
    hr.add_argument("--enable-hr", action="store_true", help="启用高清修复")
    hr.add_argument("--hr-scale", type=float, default=2.0, help="放大倍率")
    hr.add_argument("--hr-upscaler", default="R-ESRGAN 4x+", help="放大算法")
    hr.add_argument("--hr-steps", type=int, default=15, help="高清修复步数")

    # Image inputs
    img = parser.add_argument_group("图像输入")
    img.add_argument("--init-image", help="初始图像路径 (img2img/inpaint)")
    img.add_argument("--mask-image", help="蒙版图像路径 (inpaint)")
    img.add_argument("--image", help="输入图像路径 (upscale/interrogate)")
    img.add_argument("--resize-mode", type=int, default=0, help="调整大小模式 (0=缩放, 1=裁剪, 2=填充)")

    # Inpainting
    inpaint_grp = parser.add_argument_group("局部重绘参数")
    inpaint_grp.add_argument("--inpainting-fill", type=int, default=1,
                              help="蒙版填充模式 (0=填充,1=原始,2=潜空间噪声,3=潜空间无)")
    inpaint_grp.add_argument("--inpaint-full-res", action="store_true", help="全分辨率重绘")

    # ControlNet
    cn = parser.add_argument_group("ControlNet 参数")
    cn.add_argument("--control-image", help="ControlNet 参考图像路径")
    cn.add_argument("--control-module", default="openpose", help="ControlNet 模块 (预处理器)")
    cn.add_argument("--control-model", default="", help="ControlNet 模型名称")
    cn.add_argument("--control-weight", type=float, default=1.0, help="ControlNet 权重")

    # Upscaling
    up = parser.add_argument_group("放大参数")
    up.add_argument("--upscaler", default="R-ESRGAN 4x+", help="放大算法")
    up.add_argument("--scale", type=float, default=2.0, help="放大倍率")
    up.add_argument("--codeformer-visibility", type=float, default=0.0, help="CodeFormer 人脸修复强度")

    # Interrogate
    interr = parser.add_argument_group("图像分析参数")
    interr.add_argument("--interrogate-model", default="clip", choices=["clip", "deepdanbooru"],
                        help="图像分析模型")

    # Model
    model_grp = parser.add_argument_group("模型参数")
    model_grp.add_argument("--model-name", help="模型名称 (用于 switch-model)")

    # Output
    out = parser.add_argument_group("输出参数")
    out.add_argument("--output-dir", default=SD_OUTPUT_DIR, help="输出目录")

    return parser


# ── Main ───────────────────────────────────────────────────────────────────────
def main():
    parser = build_parser()
    args = parser.parse_args()

    action_map = {
        "status": action_status,
        "list-models": action_list_models,
        "list-samplers": action_list_samplers,
        "list-upscalers": action_list_upscalers,
        "list-loras": action_list_loras,
        "list-vaes": action_list_vaes,
        "progress": action_progress,
        "skip": action_skip,
        "interrupt": action_interrupt,
    }

    # No-arg actions
    if args.action in action_map:
        if args.action == "switch-model":
            if not args.model_name:
                print("❌ 需要提供 --model-name 参数")
                sys.exit(1)
            action_switch_model(args.model_name)
        else:
            action_map[args.action]()
        return

    # Actions with args
    try:
        if args.action == "switch-model":
            if not args.model_name:
                print("❌ 需要提供 --model-name 参数")
                sys.exit(1)
            action_switch_model(args.model_name)
        elif args.action == "txt2img":
            action_txt2img(args)
        elif args.action == "img2img":
            action_img2img(args)
        elif args.action == "inpaint":
            action_inpaint(args)
        elif args.action == "txt2img-controlnet":
            action_txt2img_controlnet(args)
        elif args.action == "upscale":
            action_upscale(args)
        elif args.action == "interrogate":
            action_interrogate(args)
        elif args.action == "png-info":
            action_png_info(args)
    except requests.exceptions.ConnectionError:
        print(f"\n❌ 无法连接到 SD WebUI ({SD_WEBUI_URL})")
        print("   请确认:")
        print("   1. SD WebUI 已启动并添加 --api 参数")
        print("   2. SD_WEBUI_URL 环境变量设置正确")
        sys.exit(1)
    except requests.exceptions.Timeout:
        print(f"\n⏰ 请求超时 (>{SD_TIMEOUT}秒)")
        print("   可设置 SD_TIMEOUT 环境变量增加超时时间")
        sys.exit(1)
    except Exception as e:
        print(f"\n❌ 错误: {e}")
        sys.exit(1)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)