@clawhub-2393970875-792e66ede5
AI 图片与视频异步生成技能,调用 AI Artist API 根据文本提示词生成图片或视频,自动轮询直到任务完成。 ⚠️ 使用前必须设置环境变量 AI_ARTIST_TOKEN 为你自己的 API Key! 获取 API Key:访问 https://ai.deepsop.com/ 注册登录后创建。 支持图片模...
---
name: ai-image-generator
description: |
AI 图片与视频异步生成技能,调用 AI Artist API 根据文本提示词生成图片或视频,自动轮询直到任务完成。
⚠️ 使用前必须设置环境变量 AI_ARTIST_TOKEN 为你自己的 API Key!
获取 API Key:访问 https://ai.deepsop.com/ 注册登录后创建。
支持图片模型:**3.1Nano2-Evo(默认)**、S5.0L、N2、W2.7、W2.7Pro、Nano2-Beta-Evo。
支持视频模型:**V3.1FB(默认)**、S1.5Pro、V3.1PB、V3.1Fast、W2.6t / W2.6i / W2.6r、klingV3Omni、W2.7t / W2.7i / W2.7r。
查看当前服务端激活的模型请运行:`python3 scripts/generate_image.py --list-models`。
触发场景:
- 用户要求生成图片,如"生成一匹狼"、"画一只猫"、"风景画"、"帮我画"等。
- 用户要求生成视频,如"生成视频"、"文生视频"、"图生视频"、"生成一段...的视频"等。
- 用户指定模型:N2、S5.0L、W2.7、W2.7Pro、3.1Nano2-Evo、Nano2-Beta-Evo、S1.5Pro、V3.1FB、V3.1PB、V3.1Fast、W2.6t、W2.6i、W2.6r、klingV3Omni、W2.7t、W2.7i、W2.7r。
- 用户上传参考图/参考视频时,自动先调用文件上传 API 转换为可访问 URL。
---
# AI Image Generator
异步生成 AI 图片与视频的技能。
## ⚠️ 首次使用必读
### 1. 获取 API Key
访问 [https://ai.deepsop.com/](https://ai.deepsop.com/) 注册并登录,然后创建你的 API Key。
### 2. 设置环境变量
**在使用前,你必须先设置自己的 API Key:**
```bash
# Linux/macOS/Git Bash (Windows)
export AI_ARTIST_TOKEN="sk-your_api_key_here"
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
```
或在项目根目录放一个 `.env` 文件(需 `pip install python-dotenv`,脚本会自动加载):
```ini
AI_ARTIST_TOKEN=sk-your_api_key_here
FEISHU_WEBHOOK_URL= # 可选,用于结果通知
```
### 3. 验证配置
**验证配置是否正确:**
```bash
python3 scripts/test_config.py
```
详细配置说明请查看下方"环境配置"章节。
## 快速开始
```bash
python3 scripts/generate_image.py "提示词"
```
## 意图澄清指南(重要)
**调用前必须做的事**:当用户的请求涉及参数复杂的模型,或关键信息缺失时,**先向用户提问确认意图**,再执行生成,避免浪费配额生成不符合预期的作品。
### 通用判断流程
1. **先分辨媒介**:图片 vs 视频(关键词:"画/生成图片/海报/插画" → 图片;"视频/动画/片段/动起来" → 视频)。
2. **判断输入材料**:
- 纯文字 → 文生模式(TEXT)
- 有一张首帧图 → 首帧图生视频(FIRST&LAST)
- 有首尾两张图 → 首尾帧控制(FIRST&LAST,需首帧+尾帧)
- 有参考视频 → 续写(CONTINUATION)、编辑(EDIT)、参考生成(FEATURE/REFERENCE)
- 有多张参考图(要求角色/元素一致性) → 参考图模式(REFERENCE)
3. **若用户意图不明确或关键材料缺失,必须提问**,不要擅自假设。
### 按模型列出"必须澄清的关键点"
**所有视频模型通用**:
- 时长(秒)? 比例?(16:9 横屏 / 9:16 竖屏 / 1:1 正方)
- 是否需要生成声音 / 配音 / 音乐?
- 提示词含有人物时,是否希望保持角色一致性?
**`klingV3Omni`(最复杂)**:5 种生成类型 + 多镜头模式,务必确认:
- **生成类型**:文生(TEXT)/ 首尾帧(FIRST&LAST)/ 参考图生视频(REFERENCE)/ 编辑已有视频(EDIT)/ 参考视频再创作(FEATURE)?
- **镜头模式**:单镜头(single)/ 智能多镜头(multi)/ 自定义分镜(customize,需要用户给出每个分镜的描述 + 时长)?
- **生成模式**:`std` 标准 / `pro` 专家级?
- 若是 EDIT/FEATURE:需要参考视频 URL,并确认"是否保留原音"(`keep_original_sound` yes/no)
**`W2.6r` / `W2.7r`(参考视频模式)**:
- 参考图片 + 参考视频的总数 ≤ 5,询问用户是否都准备好了 URL / 本地文件
- 是否想保留原视频的角色音色?
- 希望迁移到什么场景?迁移的主体是什么?(让用户把场景描述写进 prompt)
**`W2.7i`(图生视频,支持续写)**:
- 输入是"一张首帧图"要让它动起来?→ FIRST&LAST(可选提供尾帧,让首尾过渡更可控)
- 输入是"一段已有视频"要让它继续播?→ CONTINUATION(需要 `first_clip_url`)
- 动作/运镜希望如何展开?请用户描述(写进 prompt)
**`W2.6t` / `W2.7t`(文生视频)**:
- 是否需要多镜头叙事?若是 → `shot_type="multi"`(智能分镜)
- 是否有反向提示词(不希望出现的内容)?
- 是否需要智能改写提示词(`prompt_extend=True`,默认 false)?
- 是否需要传入自定义音频?
**`V3.1Fast`(V3.1 系列的复杂款)**:
- 是否需要翻译为英文提示词(`enhance_prompt`)?
- 是否允许生成人物(`personGeneration=allow_adult/dont_allow`)?
- 图像缩放模式(`resize_mode=pad/crop`)?
- 时长 4 秒还是 8 秒?
**`V3.1FB` / `V3.1PB`**:时长固定 8 秒,不必问;但要确认比例 / 分辨率。
**`S1.5Pro`(影视级)**:
- 是否追求"音画同步 + 口型对齐"?(说明场景是否包含对话)
- 时长在 4-12 秒之间,默认 10 秒,可问用户。
**图片复杂款 `W2.7` / `W2.7Pro` / `N2` / `3.1Nano2-Evo`**:
- 有无参考图?做"风格迁移"、"角色一致性"、"文字渲染"时参考图能显著提升质量。
- 是否需要特定比例?(默认 1:1,横图/竖图需指定)
- 质量档位(1K/2K/4K,详见每个模型表)
### 提问姿态(给 Claude 的指令)
- **一次最多问 2-3 个最关键的问题**,别堆 10 个选项让用户懵。
- **优先问对画面/成本影响最大的参数**(生成类型 > 时长 > 分辨率 > 次要参数)。
- **提供默认建议**,让用户说"就这样"也能继续,不要强制用户全部自选。
示例:"我打算用 `klingV3Omni` 做参考图生视频,比例 16:9、时长 10s、生成声音。你有几张想作为参考的图片吗?要不要保留原音?"
- **材料缺失时必须停下来要素材**(URL / 本地文件路径),不要用占位符或假 URL 代替。
- 用户若说"随便/都行",按默认值直接执行,并在生成后告知用了哪些默认。
### 何时可以不提问直接执行
- 用户请求非常明确(提示词清晰 + 指定了模型 + 提供了必要的参考材料 URL)
- 用户明确说"快速来一张就行" / "随便出个视频":用默认模型与默认参数,生成后告知用了什么。
- 用户只要一张插画/头像/风景图 → 直接用默认 `3.1Nano2-Evo` 图片模型。
## 参考图/视频上传流程
当用户提供本地文件作为参考图或参考视频时,需要先调用文件上传 API 转换为可访问的 URL:
### 文件上传 API
```bash
curl --location --request POST 'https://ai.deepsop.com/prod-api/system/fileUpload/upload' \
--header 'x-api-key: sk-your_api_key_here' \
--form 'file=@"C:\\Users\\admin\\Downloads\\image.png"'
```
**返回结果:**
```json
{
"msg": "操作成功",
"fileName": "image.png",
"code": 200,
"url": "https://kocgo-ai-sales-test.oss-cn-hangzhou.aliyuncs.com/material/100/xxx.png"
}
```
### 使用上传后的 URL
获取到 `url` 后,可作为 `firstImageUrl`、`lastImageUrl` 或其他图片参数传入生成接口。
## 在对话中直接返回图片
### 方式 1: Markdown 图片语法(推荐)
生成图片后,直接在回复中使用 Markdown 语法:
```markdown

```
**平台支持情况:**
- ✅ WebChat、Discord、Telegram:完全支持
- ✅ 飞书:支持(需公开 URL)
- ❌ WhatsApp:不支持
### 方式 2: 下载后发送(需要 message 工具)
使用 `--download` 参数下载图片,然后通过 message 工具发送:
```bash
python3 scripts/generate_image.py "风景画" --download
```
然后在代码中读取图片并发送:
```python
from scripts.generate_image import generate_image
import base64
result = generate_image(prompt="风景画", download=True)
if result and result["status"] == "SUCCESS":
# 方式 A: 使用 data URI
image_uri = result["data_uri"] # data:image/png;base64,...
# 方式 B: 读取本地文件
with open(result["local_path"], "rb") as f:
image_data = f.read()
base64_data = base64.b64encode(image_data).decode()
```
## 参数说明
### 通用参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `prompt` | 必填 | 生成提示词(图片或视频描述)|
| `--model` | 自动推断 | 生成模型。**未指定时根据 prompt 关键词自动推断**:包含 `视频/动画/短片/动起来/镜头/clip/motion/video` 等 → `V3.1FB`;其余 → `3.1Nano2-Evo`。图片:`3.1Nano2-Evo`、`S5.0L`、`N2`、`W2.7`、`W2.7Pro`、`Nano2-Beta-Evo`;视频:`V3.1FB`、`S1.5Pro`、`V3.1PB`、`V3.1Fast`、`W2.6t`、`W2.6i`、`W2.6r`、`klingV3Omni`、`W2.7t`、`W2.7i`、`W2.7r` |
| `--list-models` | - | 列出当前服务端激活的模型(hiddenState=0)后退出,不需 prompt |
| `--dry-run` | - | 仅构建并打印最终 payload,不提交任务(调试用)|
| `--json-output` | - | 以单行 JSON 向 **stdout** 输出最终结果 `{status,url,message,local_path?}`,便于 openclaw 等编排器解析 |
| `--interval` | `5` | 轮询间隔(秒) |
| `--max-wait` | 图片 600 / 视频 1200 | 任务轮询最长等待秒数 |
#### 输出契约(给编排器/openclaw)
- **stdout**:任务完成后**恰好一行**最终结果
- 默认:成功时输出 `URL`,失败时留空
- `--json-output`:始终输出一行 JSON,形如 `{"status":"SUCCESS","url":"https://...","message":"..."}`
- `--markdown-output`:成功时输出 ``
- **stderr**:所有人类可读进度日志(`[auto]`、`[upload]`、预估费用、任务 ID、轮询状态变化、`⚠️` 警告、错误说明)
- **退出码**:`0` = 成功,`1` = 失败/超时
脚本会**始终轮询到终态(SUCCESS / FAILED / TIMEOUT)才退出**,无需调用方自己再查询结果。
### 图片专属参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--quality` | `2K` | 图片质量 (2K/4K) |
| `--size` | 模型默认值 | 图片尺寸。`S5.0L` / `W2.7` / `W2.7Pro`: `2048x2048`,`N2` / `3.1Nano2-Evo` / `Nano2-Beta-Evo`: `1:1` |
| `--download` | - | 下载图片到本地 |
| `--output-dir` | `workspace/images` | 图片保存目录 |
| `--markdown-output` | - | 以 Markdown 格式输出图片链接 |
| `--reference-image` | - | 参考图本地路径,自动上传后作为 image-to-image 参考 |
| `--reference-image-url` | - | 已上传的参考图 URL(跳过上传流程)|
| `--web-search` / `--no-web-search` | - | 启用/关闭联网搜索(仅 `S5.0L`、`3.1Nano2-Evo`)|
### 视频专属参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--ratio` | `16:9` | 画面比例,如 `16:9`、`9:16`、`1:1` |
| `--resolution` | `720p` | 视频分辨率,如 `720p`、`1080p` |
| `--duration` | `10` | 视频时长(秒)|
| `--first-image-url` | - | 首帧参考图 URL |
| `--last-image-url` | - | 尾帧参考图 URL |
| `--first-image` | - | 首帧参考图本地路径,自动上传后转换为 URL |
| `--last-image` | - | 尾帧参考图本地路径,自动上传后转换为 URL |
| `--generate-audio` | - | 开启音频生成(按模型能力生效) |
| `--no-audio` | - | 关闭音频生成(按模型能力生效) |
## 支持的模型
### 图片模型
| 模型 | sourceName | methodType | 默认尺寸 | 特点 |
|------|-----------|-----------|---------|------|
| `S5.0L` | DeepSop·S5.0L | `4` | `2048x2048` | 默认模型,质量 2K/3K,支持联网,像素尺寸 WxH |
| `N2` | DeepSop·N2 | `2` | `1:1` | 多模态输入,精细参数调节,卓越文字渲染与角色一致性(比例格式)|
| `W2.7` | DeepSop.W2.7 | `6` | `2048*2048` | 文生图/图生图多模态输入,质量 1K/2K,size 用 `*` 分隔 |
| `W2.7Pro` | DeepSop.W2.7Pro | `7` | `2048*2048` | 精准控图与风格迁移,质量 1K/2K,size 用 `*` 分隔 |
| `3.1Nano2-Evo` | DeepSop·3.1Nano2-Evo | `8` | `1:1` | N2 Evo 版,多模态输入、文字渲染与角色一致性 |
| `Nano2-Beta-Evo` | DeepSop·Nano2 Beta-Evo | `9` | `1:1` | N2 Beta Evo 版,多模态输入、文字渲染与角色一致性 |
### 视频模型
| 模型 | sourceName | methodType | 默认比例 | 默认分辨率 | 默认时长 | 特点 |
|------|-----------|-----------|---------|-----------|---------|------|
| `S1.5Pro` | DeepSop·S1.5Pro | `2` | `16:9` | `720p` | 10s | 影视级连贯叙事,音画同步与精准口型对齐 |
| `V3.1FB` | DeepSop·V3.1FB | `3` | `16:9` | `1080p` | 8s | 快速生成,**时长固定 8 秒** |
| `V3.1PB` | DeepSop·V3.1PB | `4` | `adaptive` | `720p` | 8s | V3.1Pro 多图参考,**时长固定 8 秒** |
| `V3.1Fast` | DeepSop·V3.1Fast | `5` | `16:9` | `720p` | 8s | 快速生成,音画同步,时长 4s/8s |
| `W2.6t` | DeepSop·W2.6t | `7` | `16:9` | `720p` | 10s | 文生视频,3-15s,size 用 `*` 像素,15s 1080P |
| `W2.6i` | DeepSop·W2.6i | `8` | `16:9` | `720p` | 10s | 图生视频,3-15s,size 用 ratio,无尾帧支持 |
| `W2.6r` | DeepSop·W2.6r | `9` | `16:9` | `720p` | 10s | 参考视频,**3-10s**,size 用 `*` 像素 |
| `klingV3Omni` | DeepSop.klingV3Omni | `10` | `16:9` | `720p` | 10s | 多模态融合,**3-15s**,按张计费,支持分镜 |
| `W2.7i` | DeepSop·W2.7i | `14` | `16:9` | `720p` | 10s | 图生视频,首尾帧平滑过渡,动作延展与视频续写 |
| `W2.7t` | DeepSop.W2.7t | `15` | `16:9` | `720p` | 10s | 文生视频,智能多镜头剪辑,自动配音,2K 高清 |
| `W2.7r` | DeepSop.W2.7r | `16` | `16:9` | `720p` | 10s | 参考视频生成,保留角色音色,多模态融合编辑 |
**V3.1 系列时长(来自前端 `matchVideoDurationInfo`):**
- `V3.1FB` / `V3.1PB`:**时长固定为 8 秒**
- `V3.1Fast`:4 秒 或 8 秒
- 分辨率可选:720p / 1080p / 4K;比例 16:9 / 9:16 / adaptive
**WAN2.6 / WAN2.7 / klingV3Omni 系列:**
- `*t`:纯文生视频 · `*i`:首帧图生视频 · `*r`:参考图/视频生成
- 时长范围:`W2.6r` 为 **3-10s**;其余(包含 `klingV3Omni`)为 **3-15s**
- `size` 序列化规则:**仅 `W2.6t` / `W2.6r`** 使用 `宽*高` 像素格式;`W2.6i` / `W2.7t/i/r` / `klingV3Omni` 的 `size` 为比例字符串(如 `16:9`)
- 分辨率可选:720p / 1080p(`klingV3Omni` 无分辨率选项)
- 比例:`W2.6t` / `W2.6r` / `W2.7t` / `W2.7r` 支持 1:1 / 3:4 / 4:3 / 16:9 / 9:16;`W2.6i` / `W2.7i` 不可选比例(由首帧决定);`klingV3Omni` 仅 1:1 / 16:9 / 9:16
- `W2.6i` / Sora2 系列不支持尾帧图片(仅 `W2.7i` 支持)
- `W2.6t` / `W2.6i` / `W2.7*` 支持传入自定义音频(`audioUrl`)
## 使用示例
```bash
# 查看当前服务端激活的模型
python3 scripts/generate_image.py --list-models
# 基础用法 - 默认图片模型 3.1Nano2-Evo
python3 scripts/generate_image.py "一匹狼"
# 使用 N2 模型(比例尺寸)
python3 scripts/generate_image.py "生成一只狗" --model N2 --size "16:9"
# W2.7 图片模型
python3 scripts/generate_image.py "复古海报" --model W2.7 --quality "4K"
# W2.7Pro 精准控图
python3 scripts/generate_image.py "角色三视图" --model W2.7Pro
# 3.1Nano2-Evo / Nano2-Beta-Evo(N2 进化版)
python3 scripts/generate_image.py "赛博朋克街景" --model 3.1Nano2-Evo --size "16:9"
python3 scripts/generate_image.py "少女肖像" --model Nano2-Beta-Evo --size "3:4"
# 下载图片
python3 scripts/generate_image.py "风景画" --download
# 高质量生成(S5.0L)
python3 scripts/generate_image.py "风景画" --quality "4K" --size "4096x4096"
# 直接输出 Markdown 图片链接
python3 scripts/generate_image.py "一只可爱的猫" --markdown-output
# 使用参考图生成(自动上传本地图片并转换为 URL)
python3 scripts/generate_image.py "基于这张图生成变体" --reference-image "./reference.png"
# 生成视频 - 默认 V3.1FB(快速、固定 8 秒)
python3 scripts/generate_image.py "现代轻奢吊灯" --model V3.1FB
# 生成视频 - S1.5Pro(默认 16:9 / 720p / 10s)
python3 scripts/generate_image.py "小骏马祝福大家新年快乐" --model S1.5Pro
# 生成视频 - 指定比例和分辨率
python3 scripts/generate_image.py "海边日落风景" --model S1.5Pro --ratio "9:16" --resolution "1080p"
# V3.1FB - 快速基础(8 秒)
python3 scripts/generate_image.py "现代轻奢吊灯" --model V3.1FB --ratio "16:9" --resolution "1080p" --duration 8
# V3.1PB - 自适应比例(8 秒)
python3 scripts/generate_image.py "水晶灯特写" --model V3.1PB --ratio "adaptive" --resolution "720p" --duration 8
# V3.1Fast - 首帧图生视频(4 秒)
python3 scripts/generate_image.py "灯具展示" --model V3.1Fast --first-image "./lamp.jpg" --duration 4
# klingV3Omni - 多模态融合(按张计费)
python3 scripts/generate_image.py "多模态融合镜头" --model klingV3Omni --ratio "16:9" --duration 8
# W2.6t / W2.7t - 文生视频(10 秒)
python3 scripts/generate_image.py "现代轻奢吊灯宣传" --model W2.6t --ratio "16:9" --resolution "1080p" --duration 10
python3 scripts/generate_image.py "品牌短片自动配音 2K" --model W2.7t --ratio "16:9" --resolution "1080p" --duration 10
# W2.6i / W2.7i - 首帧图生视频(8 秒)
python3 scripts/generate_image.py "水晶灯展示" --model W2.6i --first-image "./lamp.jpg" --ratio "9:16" --resolution "720p" --duration 8
python3 scripts/generate_image.py "角色动作延展" --model W2.7i --first-image "./char.jpg" --last-image "./char_end.jpg" --duration 8
# W2.6r / W2.7r - 参考视频生成(CLI 需传已上传 URL,或使用程序化调用)
python3 scripts/generate_image.py "参考素材风格生成" --model W2.6r --ratio "16:9" --resolution "720p" --duration 10
python3 scripts/generate_image.py "保留角色音色迁移场景" --model W2.7r --ratio "16:9" --resolution "720p" --duration 10
```
## 程序化调用
```python
from scripts.generate_image import generate_image, generate_video
# 图片 - 默认 3.1Nano2-Evo
result = generate_image(prompt="一只可爱的猫咪")
# 查询当前激活模型(预览用)
from scripts.generate_image import list_active_models
print(list_active_models())
# 图片 - N2(比例尺寸)
result = generate_image(prompt="生成一只狗", model="N2", size="16:9")
# 图片 - W2.7Pro 精准控图
result = generate_image(prompt="角色三视图", model="W2.7Pro", quality="4K")
# 图片 - 下载到本地
result = generate_image(prompt="风景画", model="S5.0L", download=True, output_dir="./images")
# V3.1FB - 文生视频
result = generate_video(
prompt="现代轻奢吊灯",
model="V3.1FB",
ratio="16:9",
resolution="1080p",
duration=8
)
# V3.1Fast - 首帧图生视频
result = generate_video(
prompt="灯具展示",
model="V3.1Fast",
first_image_url="https://example.com/lamp.jpg",
ratio="9:16",
resolution="1080p",
duration=8
)
# V3.1PB - 首尾帧控制
result = generate_video(
prompt="灯具变形动画",
model="V3.1PB",
first_image_url="https://example.com/start.jpg",
last_image_url="https://example.com/end.jpg",
ratio="16:9",
resolution="1080p",
duration=8
)
# W2.7r - 参考视频生成(多模态融合)
result = generate_video(
prompt="保留角色音色迁移到新场景",
model="W2.7r",
image_url_list=["https://example.com/ref1.jpg", "https://example.com/ref2.jpg"],
video_url_list=["https://example.com/ref.mp4"],
ratio="16:9",
resolution="720p",
duration=10
)
# klingV3Omni - 多模态融合(按张计费)
result = generate_video(
prompt="镜头一致性多图融合",
model="klingV3Omni",
image_url_list=["https://example.com/scene1.jpg", "https://example.com/scene2.jpg"],
ratio="16:9",
duration=8
)
if result and result["status"] == "SUCCESS":
print(f"链接: {result['url']}")
# 视频 - 默认 V3.1FB
result = generate_video(prompt="小骏马祝福大家新年快乐")
# 视频 - 指定比例、分辨率、时长
result = generate_video(
prompt="海边日落风景",
model="S1.5Pro",
ratio="9:16",
resolution="1080p",
duration=5
)
if result and result["status"] == "SUCCESS":
print(f"视频链接: {result['url']}")
```
## 返回字段
| 字段 | 说明 |
|------|------|
| `status` | SUCCESS / FAILED / TIMEOUT |
| `url` | 图片URL |
| `message` | 状态描述 |
| `local_path` | 本地保存路径(需 --download) |
| `data_uri` | Base64 Data URI(需 --download) |
| `image_data` | 原始图片字节(需 --download) |
## 环境配置
### 必需配置 - API Key
**重要:使用前必须设置你自己的 API Key!**
#### 获取 API Key
1. 访问 [https://ai.deepsop.com/](https://ai.deepsop.com/)
2. 注册并登录账号
3. 在控制台创建你的 API Key
4. 复制生成的 API Key(格式:`sk-xxxxxx...`)
#### 方式 1:使用 .env 文件(推荐)
1. 复制 `.env.example` 为 `.env`:
```bash
cp .env.example .env
```
2. 编辑 `.env` 文件,填入你的 API Key:
```bash
AI_ARTIST_TOKEN=sk-your_api_key_here
```
3. 在运行脚本前加载环境变量:
```bash
# Linux/macOS/Git Bash
source .env
# 或使用 export
export $(cat .env | xargs)
```
#### 方式 2:直接设置环境变量
##### Linux / macOS / Git Bash (Windows)
```bash
export AI_ARTIST_TOKEN="sk-your_api_key_here"
```
为了永久生效,将上述命令添加到 `~/.bashrc` 或 `~/.zshrc` 文件中。
##### Windows PowerShell
```powershell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
```
永久设置(系统级):
```powershell
[System.Environment]::SetEnvironmentVariable('AI_ARTIST_TOKEN', 'sk-your_api_key_here', 'User')
```
##### Windows CMD
```cmd
set AI_ARTIST_TOKEN=sk-your_api_key_here
```
#### 验证配置
运行以下命令验证 API Key 是否设置成功:
```bash
# Linux/macOS/Git Bash
echo $AI_ARTIST_TOKEN
# Windows PowerShell
echo $env:AI_ARTIST_TOKEN
# Windows CMD
echo %AI_ARTIST_TOKEN%
```
如果输出为空或显示默认值,说明环境变量未正确设置。
#### 测试配置(推荐)
运行配置测试脚本,验证 API Key 是否正确设置:
```bash
python3 scripts/test_config.py
```
该脚本会检查:
- API Key 是否已设置
- 是否使用了默认 Key(需要替换为你自己的)
- 配置是否可以正常使用
### 可选配置 - 飞书通知
```bash
export FEISHU_WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
```
## 相关文件
- `scripts/generate_image.py` - 主脚本
- `references/api.md` - API 详细文档
FILE:README.md
# AI Image Generator
基于 AI Artist API 的图片/视频异步生成工具。
- 支持图片与视频任务创建
- 自动轮询任务状态直到完成
- 支持本地参考图自动上传
- 创建任务前自动调用费用预估,余额不足时会拦截并提示充值
## 🚀 快速开始
### 1) 获取 API Key
访问 [https://ai.deepsop.com/](https://ai.deepsop.com/) 注册登录后,在控制台创建 API Key。
### 2) 设置环境变量
```bash
# Linux/macOS/Git Bash
export AI_ARTIST_TOKEN="sk-your_api_key_here"
```
```powershell
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
```
### 3) 验证配置
```bash
python3 scripts/test_config.py
```
### 4) 开始生成
```bash
# 查看当前服务端激活的模型
python3 scripts/generate_image.py --list-models
# 默认图片模型(3.1Nano2-Evo)
python3 scripts/generate_image.py "一只可爱的猫"
```
## 🎨 支持模型
### 图片模型(以 API sourceName 命名)
- `3.1Nano2-Evo`(默认)— DeepSop·3.1Nano2-Evo,N2 进化版
- `S5.0L` — DeepSop·S5.0L,生成快、风格全、支持联网
- `N2` — DeepSop·N2,多模态输入、卓越文字渲染
- `W2.7` — DeepSop.W2.7,文生图/图生图多模态输入
- `W2.7Pro` — DeepSop.W2.7Pro,精准控图与风格迁移
- `Nano2-Beta-Evo` — DeepSop·Nano2 Beta-Evo,N2 Beta 进化版
### 视频模型(以 API sourceName 命名)
- `V3.1FB`(默认)— DeepSop·V3.1FB,快速生成基础流畅,固定 8 秒
- `S1.5Pro` — DeepSop·S1.5Pro,影视级连贯叙事
- `V3.1PB` — DeepSop·V3.1PB,多图参考角色一致性
- `V3.1Fast` — DeepSop·V3.1Fast,音画同步、竖屏适配
- `W2.6t` / `W2.6i` / `W2.6r` — DeepSop·W2.6 系列(文生/图生/参考视频)
- `klingV3Omni` — DeepSop.klingV3Omni,多模态融合(按张计费)
- `W2.7i` / `W2.7t` / `W2.7r` — DeepSop·W2.7 系列(文生 2K 自配音 / 图生首尾帧 / 参考视频)
## 📝 常用示例
```bash
# 图片:指定模型(比例尺寸)
python3 scripts/generate_image.py "一只柴犬" --model N2 --size "1:1"
# 图片:W2.7Pro 精准控图
python3 scripts/generate_image.py "角色三视图" --model W2.7Pro --quality "4K"
# 图片:下载到本地
python3 scripts/generate_image.py "海边日落" --download
# 图片:参考图生成(本地文件自动上传)
python3 scripts/generate_image.py "做成赛博朋克风格" --reference-image "./ref.png"
# 视频:基础文生视频(S1.5Pro)
python3 scripts/generate_image.py "城市夜景延时" --model S1.5Pro
# 视频:V3.1PB 首尾帧控制
python3 scripts/generate_image.py "灯具变形动画" --model V3.1PB --first-image "./start.jpg" --last-image "./end.jpg" --duration 8
# 视频:W2.7t 文生视频(2K 自配音)
python3 scripts/generate_image.py "品牌短片" --model W2.7t --resolution "1080p" --duration 10
```
## 📖 文档
完整参数说明与更多示例见 `SKILL.md`。
## 🧪 调试与测试
```bash
# 预览最终 payload,不消耗 K 币
python3 scripts/generate_image.py "测试提示词" --dry-run
# 查看当前激活的模型
python3 scripts/generate_image.py --list-models
# 运行回归测试(需 pytest)
pytest tests -q
```
## 🔧 环境要求
- Python 3.6+
- `requests`
- `python-dotenv`(可选;用于自动加载项目根 `.env`)
## ⚠️ 注意事项
- 必须使用你自己的 `AI_ARTIST_TOKEN`
- 任务创建前会执行费用预估;若余额不足将不会提交任务
- 请遵守 AI Artist API 的使用条款
FILE:tests/test_generate_image.py
"""Regression tests for scripts/generate_image.py.
Mocks out the HTTP layer (requests.post / requests.get) and exercises the
per-model validation chain: duration/size/ratio/resolution coercion, prompt
whitelisting, target* restrictions, klingV3Omni special serialization, etc.
Run: pytest tests -q
"""
from __future__ import annotations
import importlib.util
import json
import os
import sys
from pathlib import Path
import pytest
import requests
ROOT = Path(__file__).resolve().parents[1]
SCRIPT_PATH = ROOT / "scripts" / "generate_image.py"
@pytest.fixture(autouse=True)
def _require_token():
os.environ.setdefault("AI_ARTIST_TOKEN", "sk-test-dummy")
@pytest.fixture()
def gi(monkeypatch):
"""Import a fresh copy of the module with network + estimator stubbed."""
# Force a fresh import so each test has an isolated cache
spec = importlib.util.spec_from_file_location("generate_image_under_test", SCRIPT_PATH)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) # type: ignore[union-attr]
# Seed the model-list cache so check_model_available passes for every test
mod._MODEL_LIST_CACHE["rows"] = [
{"sourceType": "IMAGE_MODEL", "sourceValue": cfg["methodType"],
"hiddenState": "0", "sourceName": cfg["source_name"]}
for k, cfg in mod.MODEL_CONFIGS.items() if cfg["media_type"] == "image"
] + [
{"sourceType": "VIDEO_MODEL", "sourceValue": cfg["methodType"],
"hiddenState": "0", "sourceName": cfg["source_name"]}
for k, cfg in mod.MODEL_CONFIGS.items() if cfg["media_type"] == "video"
]
mod._MODEL_LIST_CACHE["expires_at"] = float("inf")
captured = {"payloads": []}
class Resp:
status_code = 200
def json(self):
return {"msg": "ok", "code": 200, "data": ["task-xxx"]}
def raise_for_status(self):
pass
def fake_post(url, **kw):
if "consumeSource/list" in url:
r = Resp()
r.json = lambda: {"code": 200, "msg": "ok", "rows": mod._MODEL_LIST_CACHE["rows"]} # type: ignore
return r
if "estimate" in url.lower() or "cost" in url.lower():
r = Resp()
r.json = lambda: {"msg": "ok", "code": 200,
"data": {"estimatedCost": 1.0, "sufficientBalance": True}} # type: ignore
return r
body = json.loads(kw["data"]) if "data" in kw else kw.get("json", {})
captured["payloads"].append(body)
return Resp()
monkeypatch.setattr(requests, "post", fake_post)
monkeypatch.setattr(mod, "estimate_generation_cost", lambda _: True)
return mod, captured
def _last_parameter(captured):
return json.loads(captured["payloads"][-1]["parameter"])
# ---------------------------------------------------------------------------
# Prompt validation
# ---------------------------------------------------------------------------
class TestPromptRequired:
@pytest.mark.parametrize("model", ["S5.0L", "N2", "W2.7"])
def test_image_empty_prompt_rejected(self, gi, model):
mod, _ = gi
assert mod.create_generation_task("", model=model) is None
assert mod.create_generation_task(" ", model=model) is None
assert mod.create_generation_task(None, model=model) is None
@pytest.mark.parametrize("model", ["S1.5Pro", "V3.1FB", "W2.6t", "klingV3Omni"])
def test_video_empty_prompt_rejected(self, gi, model):
mod, _ = gi
assert mod.create_video_task("", model=model) is None
def test_w26i_allows_empty_prompt(self, gi):
mod, captured = gi
assert mod.create_video_task("", model="W2.6i", first_image_url="x") == "task-xxx"
def test_kling_customize_allows_empty_prompt(self, gi):
mod, _ = gi
result = mod.create_video_task(
"", model="klingV3Omni", shot_type="customize",
multi_prompt=[{"index": 1, "prompt": "a", "duration": 5}],
)
assert result == "task-xxx"
# ---------------------------------------------------------------------------
# Duration enforcement
# ---------------------------------------------------------------------------
class TestDurationRules:
@pytest.mark.parametrize("model", ["V3.1FB", "V3.1PB"])
def test_v31fb_pb_fixed_eight(self, gi, model):
mod, captured = gi
mod.create_video_task("hi", model=model, duration=4)
assert _last_parameter(captured)["duration"] == 8
def test_v31fast_snaps_to_valid(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="V3.1Fast", duration=5)
assert _last_parameter(captured)["duration"] == 8
mod.create_video_task("hi", model="V3.1Fast", duration=4)
assert _last_parameter(captured)["duration"] == 4
def test_kling_3_to_15(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="klingV3Omni", duration=20)
assert _last_parameter(captured)["duration"] == 10
def test_w26r_capped_at_10(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="W2.6r", duration=15)
assert _last_parameter(captured)["duration"] == 10
def test_w27r_with_video_capped_at_10(self, gi):
mod, captured = gi
mod.create_video_task(
"hi", model="W2.7r", duration=15,
video_url_list=["https://x.mp4"],
)
assert _last_parameter(captured)["duration"] == 10
def test_w27r_without_video_allows_15(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="W2.7r", duration=15)
assert _last_parameter(captured)["duration"] == 15
# ---------------------------------------------------------------------------
# Size serialization
# ---------------------------------------------------------------------------
class TestSizeSerialization:
def test_w26t_uses_star_pixel_size(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="W2.6t")
assert "*" in _last_parameter(captured)["size"]
def test_w26r_uses_star_pixel_size(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="W2.6r")
assert "*" in _last_parameter(captured)["size"]
def test_w27_series_uses_ratio_string(self, gi):
mod, captured = gi
for model in ["W2.7t", "W2.7i", "W2.7r"]:
kw = {"first_image_url": "x"} if model == "W2.7i" else {}
mod.create_video_task("hi", model=model, **kw)
size = _last_parameter(captured)["size"]
assert "*" not in size, f"{model} should NOT use pixel format"
def test_image_w27_uses_star_default(self, gi):
mod, captured = gi
mod.create_generation_task("hi", model="W2.7")
assert "*" in _last_parameter(captured)["size"]
# ---------------------------------------------------------------------------
# Whitelist filtering
# ---------------------------------------------------------------------------
class TestFieldWhitelist:
def test_s15pro_strips_v3_fields(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="S1.5Pro")
p = _last_parameter(captured)
for k in ("n", "personGeneration", "resizeMode", "enhancePrompt",
"shotType", "promptExtend"):
assert k not in p, f"S1.5Pro should not carry {k}"
def test_w26t_strips_veo_fields(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="W2.6t")
p = _last_parameter(captured)
for k in ("n", "personGeneration", "resizeMode", "enhancePrompt",
"generateAudio"):
assert k not in p
def test_kling_has_exclusives(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="klingV3Omni")
p = _last_parameter(captured)
for k in ("mode", "multiShot", "keepOriginalSound", "shotType"):
assert k in p
assert "resolution" not in p
def test_w26i_omits_ratio_and_last_image(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="W2.6i", first_image_url="x",
last_image_url="y")
p = _last_parameter(captured)
assert "ratio" not in p
assert "lastImageUrl" not in p
def test_image_websearch_only_where_supported(self, gi):
mod, captured = gi
mod.create_generation_task("hi", model="S5.0L", web_search=True)
assert _last_parameter(captured)["webSearch"] is True
mod.create_generation_task("hi", model="W2.7")
assert "webSearch" not in _last_parameter(captured)
# ---------------------------------------------------------------------------
# Target* restrictions
# ---------------------------------------------------------------------------
class TestRestrictions:
@pytest.mark.parametrize("model,exp", [
("S1.5Pro", {"targetMaxSize": 30, "targetMinLength": 300, "targetMaxLength": 6000}),
("V3.1FB", {"targetMaxSize": 10, "targetMinLength": 300, "targetMaxLength": 6000}),
("W2.6t", {"targetMaxSize": 10, "targetMinLength": 360, "targetMaxLength": 2000}),
("W2.6r", {"targetMaxSize": 10, "targetMinLength": 240, "targetMaxLength": 5000}),
("W2.7i", {"targetMaxSize": 20, "targetMinLength": 240, "targetMaxLength": 8000}),
("W2.7r", {"targetMaxSize": 10, "targetMinLength": 240, "targetMaxLength": 5000}),
])
def test_video_target_values(self, gi, model, exp):
mod, captured = gi
kw = {"first_image_url": "x"} if model == "W2.7i" else {}
mod.create_video_task("hi", model=model, **kw)
p = _last_parameter(captured)
for key, val in exp.items():
assert p.get(key) == val, f"{model} {key}"
def test_kling_has_no_maxlength(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="klingV3Omni")
p = _last_parameter(captured)
assert p["targetMaxSize"] == 10
assert p["targetMinLength"] == 300
assert "targetMaxLength" not in p
@pytest.mark.parametrize("model,length,limit", [
("S5.0L", 500, 300),
("W2.7", 3000, 2500),
])
def test_image_prompt_truncated(self, gi, model, length, limit):
mod, captured = gi
mod.create_generation_task("a" * length, model=model)
assert len(_last_parameter(captured)["prompt"]) == limit
def test_video_negative_prompt_truncated(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="V3.1Fast", negative_prompt="n" * 500)
assert len(_last_parameter(captured)["negativePrompt"]) == 250
# ---------------------------------------------------------------------------
# Value whitelisting (coerce_value)
# ---------------------------------------------------------------------------
class TestValueCoercion:
def test_bad_ratio_falls_back(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="klingV3Omni", ratio="3:4") # not allowed
assert _last_parameter(captured)["ratio"] == "16:9"
def test_bad_resolution_falls_back(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="S1.5Pro", resolution="4K")
assert _last_parameter(captured)["resolution"] == "720p"
def test_bad_generation_type_falls_back(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="W2.6t", generation_type="FIRST&LAST")
assert _last_parameter(captured)["generationType"] == "TEXT"
def test_bad_image_quality_falls_back(self, gi):
mod, captured = gi
mod.create_generation_task("hi", model="S5.0L", quality="4K")
assert _last_parameter(captured)["quality"] == "2K"
# ---------------------------------------------------------------------------
# klingV3Omni-specific serialization
# ---------------------------------------------------------------------------
class TestKlingSerialization:
def test_shot_type_multi_becomes_intelligence(self, gi):
mod, captured = gi
mod.create_video_task("hi", model="klingV3Omni", shot_type="multi")
assert _last_parameter(captured)["shotType"] == "intelligence"
def test_edit_packs_videolist(self, gi):
mod, captured = gi
mod.create_video_task(
"hi", model="klingV3Omni",
generation_type="EDIT",
first_clip_url="https://x.mp4",
keep_original_sound="yes",
)
p = _last_parameter(captured)
assert "firstClipUrl" not in p
assert p["videoList"] == [{
"video_url": "https://x.mp4",
"refer_type": "base",
"keep_original_sound": "yes",
}]
assert p["generateAudio"] is False
def test_feature_videolist_uses_feature_refer_type(self, gi):
mod, captured = gi
mod.create_video_task(
"hi", model="klingV3Omni",
generation_type="FEATURE",
first_clip_url="https://x.mp4",
)
p = _last_parameter(captured)
assert p["videoList"][0]["refer_type"] == "feature"
# ---------------------------------------------------------------------------
# Media type inference
# ---------------------------------------------------------------------------
class TestInferMediaType:
@pytest.mark.parametrize("prompt", [
"生成一段海边日落的视频",
"让这张照片动起来",
"一个 8 秒的动画短片",
"a smooth motion clip of a cat jumping",
"镜头推进展示产品",
])
def test_video_prompts(self, gi, prompt):
mod, _ = gi
assert mod._infer_media_type(prompt) == "video"
@pytest.mark.parametrize("prompt", [
"画一只可爱的柴犬",
"赛博朋克风格的海报",
"一只戴墨镜的猫", # neutral
"",
None,
"生成头像插画",
])
def test_image_prompts(self, gi, prompt):
mod, _ = gi
assert mod._infer_media_type(prompt) == "image"
def test_mixed_prompt_prefers_image(self, gi):
"""When both video and image cues coexist, fall back to image (safer default)."""
mod, _ = gi
assert mod._infer_media_type("一张海报,画面略微动起来") == "image"
# ---------------------------------------------------------------------------
# Model availability guard
# ---------------------------------------------------------------------------
class TestModelAvailability:
def test_rejects_hidden_model(self, gi):
mod, _ = gi
# Mark S4.5 as hidden in the cache
for row in mod._MODEL_LIST_CACHE["rows"]:
if row["sourceValue"] == "0" and row["sourceType"] == "IMAGE_MODEL":
row["hiddenState"] = "1"
assert mod.create_generation_task("hi", model="S4.5") is None
FILE:tests/__init__.py
FILE:scripts/generate_image.py
#!/usr/bin/env python3
"""
AI Image Generator - Async Image Generation Script
Calls the AI Artist API to generate images from text prompts.
Handles async task polling until completion.
Supports Feishu webhook callback for result notification.
Set FEISHU_WEBHOOK_URL environment variable to enable.
Supports local file upload for reference images/videos.
Local files are automatically uploaded to get public URLs before calling generation APIs.
"""
import requests
import json
import time
import sys
import argparse
import os
import base64
from pathlib import Path
# Configuration
API_PREFIX = "https://ai.deepsop.com/prod-api/"
BASE_URL = f"{API_PREFIX.rstrip('/')}/ai"
FILE_UPLOAD_URL = f"{API_PREFIX.rstrip('/')}/system/fileUpload/upload"
ESTIMATE_COST_URL = f"{BASE_URL}/estimate/cost"
MODEL_LIST_URL = f"{BASE_URL}/consumeSource/list?pageNum=1&pageSize=999"
RECHARGE_URL = "https://ai.deepsop.com/"
# In-process cache for the model list (TTL seconds). Models can be toggled
# on/off server-side at any time, so we re-fetch periodically instead of
# hard-coding hiddenState values. A disk-backed fallback avoids hammering the
# API across short-lived CLI invocations on the same machine.
_MODEL_LIST_CACHE = {"rows": None, "expires_at": 0.0}
_MODEL_LIST_TTL = 300 # 5 minutes
import tempfile as _tempfile
_MODEL_LIST_DISK_CACHE = os.path.join(_tempfile.gettempdir(), "deepsop_model_list.json")
# Optionally load a .env file from the project root (best-effort; no hard dep)
try:
from dotenv import load_dotenv as _load_dotenv # type: ignore
_load_dotenv()
except Exception:
pass
# Get API key from environment variable (required)
API_KEY = os.environ.get("AI_ARTIST_TOKEN")
# Feishu webhook configuration (optional)
FEISHU_WEBHOOK_URL = os.environ.get("FEISHU_WEBHOOK_URL")
# Dry-run toggle: when True, task creators print the payload and skip network
# submission. Set via the CLI `--dry-run` flag or programmatically.
DRY_RUN = False
# Keep stdout reserved for machine-readable final output (URL / JSON) so
# orchestrators like openclaw can parse it reliably. Human progress logs go
# to stderr via _progress().
try:
sys.stdout.reconfigure(line_buffering=True) # Python 3.7+
except Exception:
pass
def _progress(msg):
"""Write a human-facing progress line to stderr (flushed immediately)."""
print(msg, file=sys.stderr, flush=True)
def _emit_cli_result(result, args, markdown_label=""):
"""Always emit a single terminal line on stdout for orchestrators.
Behavior:
- `--json-output` → one-line JSON `{"status","url","message","local_path"?}`
- `--markdown-output` → `` when SUCCESS
- default → raw URL when SUCCESS, nothing on stdout when failed (errors on stderr)
Failures always emit a clear stderr message so humans still see them.
"""
status = (result or {}).get("status") or "FAILED"
url = (result or {}).get("url")
message = (result or {}).get("message") or "未知错误"
if getattr(args, "json_output", False):
payload = {
"status": status,
"url": url,
"message": message,
}
if isinstance(result, dict) and result.get("local_path"):
payload["local_path"] = result["local_path"]
print(json.dumps(payload, ensure_ascii=False), flush=True)
return
if status == "SUCCESS" and url:
if getattr(args, "markdown_output", False):
print(f"", flush=True)
else:
print(url, flush=True)
else:
# Failure: keep stdout empty, surface a clear human message on stderr
print(f"任务未成功:status={status},message={message}", file=sys.stderr, flush=True)
def check_api_key():
"""Check if user has set their API key."""
if not API_KEY:
print("错误:未配置 AI_ARTIST_TOKEN 环境变量", file=sys.stderr)
print("", file=sys.stderr)
print("请先设置你的 API Key:", file=sys.stderr)
print(" export AI_ARTIST_TOKEN=\"sk-your_api_key_here\"", file=sys.stderr)
print("", file=sys.stderr)
print("验证配置:", file=sys.stderr)
print(" python3 scripts/test_config.py", file=sys.stderr)
print("", file=sys.stderr)
sys.exit(1)
return True
def get_headers():
"""Build request headers with API key."""
return {
"Content-Type": "application/json",
"X-Api-Key": API_KEY
}
def estimate_generation_cost(payload):
try:
response = requests.post(ESTIMATE_COST_URL, json=payload, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
print(f"费用预估失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return False
data = result.get("data") or {}
estimated_cost = data.get("estimatedCost")
sufficient_balance = data.get("sufficientBalance")
if estimated_cost is not None:
_progress(f"预估费用:{estimated_cost} K币")
if sufficient_balance is True:
_progress("余额充足,正在创建任务")
return True
if sufficient_balance is False:
print(f"余额不足,无法提交创建任务。请前往 {RECHARGE_URL} 充值 K 币后重试。", file=sys.stderr)
return False
print("费用预估返回结果不完整", file=sys.stderr)
return False
except requests.exceptions.HTTPError as e:
print(_explain_http_error(e, context="费用预估"), file=sys.stderr)
return False
except requests.exceptions.RequestException as e:
print(f"费用预估网络错误:{e}", file=sys.stderr)
return False
except ValueError as e:
print(f"费用预估响应解析失败:{e}", file=sys.stderr)
return False
def _load_disk_cache():
"""Load the disk cache file if present and still fresh; return rows or None."""
import time
try:
if not os.path.exists(_MODEL_LIST_DISK_CACHE):
return None
with open(_MODEL_LIST_DISK_CACHE, "r", encoding="utf-8") as f:
blob = json.load(f)
if not isinstance(blob, dict) or "rows" not in blob:
return None
if blob.get("expires_at", 0) < time.time():
return None
return blob["rows"]
except Exception:
return None
def _save_disk_cache(rows, expires_at):
try:
with open(_MODEL_LIST_DISK_CACHE, "w", encoding="utf-8") as f:
json.dump({"rows": rows, "expires_at": expires_at}, f, ensure_ascii=False)
except Exception:
pass # best-effort
def fetch_model_list(force_refresh=False):
"""Fetch the full model list from consumeSource/list with TTL caching.
Caching layers (fastest first):
1. process-local `_MODEL_LIST_CACHE`
2. disk cache at `_MODEL_LIST_DISK_CACHE` (survives across CLI runs)
3. network call to `consumeSource/list`
Returns a list of dicts (possibly empty on total failure).
"""
import time
now = time.time()
# (1) in-process cache
if (not force_refresh
and _MODEL_LIST_CACHE["rows"] is not None
and _MODEL_LIST_CACHE["expires_at"] > now):
return _MODEL_LIST_CACHE["rows"]
# (2) disk cache seed
if not force_refresh and _MODEL_LIST_CACHE["rows"] is None:
disk_rows = _load_disk_cache()
if disk_rows is not None:
_MODEL_LIST_CACHE["rows"] = disk_rows
_MODEL_LIST_CACHE["expires_at"] = now + _MODEL_LIST_TTL
return disk_rows
# (3) network
try:
response = requests.post(
MODEL_LIST_URL,
json={"sourceTypeList": ["IMAGE_MODEL", "VIDEO_MODEL"]},
headers=get_headers(),
timeout=15,
)
response.raise_for_status()
payload = response.json()
if payload.get("code") != 200:
print(f"模型列表查询失败:{payload.get('msg', '未知错误')}", file=sys.stderr)
return _MODEL_LIST_CACHE["rows"] or []
rows = payload.get("rows") or []
expires_at = now + _MODEL_LIST_TTL
_MODEL_LIST_CACHE["rows"] = rows
_MODEL_LIST_CACHE["expires_at"] = expires_at
_save_disk_cache(rows, expires_at)
return rows
except requests.exceptions.HTTPError as e:
print(_explain_http_error(e, context="模型列表查询"), file=sys.stderr)
return _MODEL_LIST_CACHE["rows"] or []
except Exception as e:
print(f"[warn] 模型列表查询异常,使用上次缓存:{e}", file=sys.stderr)
return _MODEL_LIST_CACHE["rows"] or []
def check_model_available(model_key):
"""Verify the given model is currently active (hiddenState == '0').
Returns True if usable, False if disabled or not found. On total network
failure (no cache + request error) we return True so the user isn't blocked.
"""
if model_key not in MODEL_CONFIGS:
print(f"未知模型:{model_key}", file=sys.stderr)
return False
config = MODEL_CONFIGS[model_key]
want_type = "IMAGE_MODEL" if config["media_type"] == "image" else "VIDEO_MODEL"
want_value = str(config["methodType"])
rows = fetch_model_list()
if not rows:
print(f"[warn] 无法确认 {model_key} 启用状态(模型列表为空),跳过校验", file=sys.stderr)
return True
for row in rows:
if row.get("sourceType") == want_type and str(row.get("sourceValue")) == want_value:
hidden = str(row.get("hiddenState"))
if hidden == "1":
print(
f"模型 {model_key} ({row.get('sourceName')}) 当前已停用 "
f"(hiddenState=1),拒绝提交任务。可访问 {RECHARGE_URL} 查看最新可用模型。",
file=sys.stderr,
)
return False
return True
print(
f"模型 {model_key} (sourceType={want_type}, sourceValue={want_value}) "
f"不在服务端模型列表中,可能已下线,拒绝提交任务。",
file=sys.stderr,
)
return False
_VIDEO_KEYWORDS = (
"视频", "动画", "短片", "片段", "动起来", "动图",
"镜头", "运镜", "画面动", "跳动", "挥手", "旋转", "奔跑",
"video", "clip", "motion", "animation", "animate", "mp4",
)
_IMAGE_KEYWORDS = (
"图片", "图像", "画一", "插画", "海报", "壁纸", "封面",
"肖像", "写真", "头像", "logo",
"image", "picture", "poster", "wallpaper", "illustration",
)
def _infer_media_type(prompt):
"""Infer 'video' or 'image' from prompt text. Defaults to 'image' when ambiguous."""
if not prompt:
return "image"
p = str(prompt).lower()
has_video = any(k.lower() in p for k in _VIDEO_KEYWORDS)
has_image = any(k.lower() in p for k in _IMAGE_KEYWORDS)
# Prefer video only if it's the dominant cue: a "video" keyword is present
# AND no image-specific keyword is competing with it.
if has_video and not has_image:
return "video"
return "image"
def list_active_models():
"""Return active (hiddenState == '0') models grouped by image/video.
Cross-references the server's consumeSource/list with local MODEL_CONFIGS
so only models the script actually knows how to dispatch are listed.
"""
rows = fetch_model_list()
active_by_type = {"IMAGE_MODEL": [], "VIDEO_MODEL": []}
for row in rows:
if str(row.get("hiddenState")) != "0":
continue
stype = row.get("sourceType")
if stype not in active_by_type:
continue
value = str(row.get("sourceValue"))
# match back to a MODEL_CONFIGS key
local_key = next(
(k for k, cfg in MODEL_CONFIGS.items()
if str(cfg["methodType"]) == value
and ((cfg["media_type"] == "image" and stype == "IMAGE_MODEL")
or (cfg["media_type"] == "video" and stype == "VIDEO_MODEL"))),
None,
)
active_by_type[stype].append({
"key": local_key,
"sourceName": row.get("sourceName"),
"sourceValue": value,
"description": row.get("sourceDescription") or "",
})
return {
"image": active_by_type["IMAGE_MODEL"],
"video": active_by_type["VIDEO_MODEL"],
}
def print_active_models():
"""Human-readable dump of currently active models."""
data = list_active_models()
print("=== 当前可用的图片模型 (hiddenState=0) ===")
for m in data["image"]:
key_hint = f" key={m['key']}" if m["key"] else " (脚本未注册)"
print(f"- {m['sourceName']} [sourceValue={m['sourceValue']}]{key_hint}")
if m["description"]:
print(f" {m['description']}")
print("\n=== 当前可用的视频模型 (hiddenState=0) ===")
for m in data["video"]:
key_hint = f" key={m['key']}" if m["key"] else " (脚本未注册)"
print(f"- {m['sourceName']} [sourceValue={m['sourceValue']}]{key_hint}")
if m["description"]:
print(f" {m['description']}")
print("\n默认模型:图片 → 3.1Nano2-Evo;视频 → V3.1FB")
_UPLOAD_SOFT_LIMIT_MB = 100 # generous cap; specific per-model caps are checked separately
def _explain_http_error(exc, context=""):
"""Produce a user-friendly message for common HTTP failure modes."""
status = getattr(getattr(exc, "response", None), "status_code", None)
prefix = f"{context} " if context else ""
if status == 401:
return (f"{prefix}认证失败 (401)。请确认环境变量 AI_ARTIST_TOKEN 已设置且未过期,"
f"并在 {RECHARGE_URL} 重新生成 API Key。")
if status == 403:
return f"{prefix}权限不足 (403)。当前 API Key 可能未授权该模型或功能。"
if status == 429:
return f"{prefix}请求过于频繁 (429)。请稍候 10-30 秒再重试,或降低并发。"
if status and 500 <= status < 600:
return f"{prefix}服务端错误 ({status})。请稍后重试;若持续发生请联系管理员。"
return f"{prefix}网络/请求错误:{exc}"
def upload_file(file_path):
"""Upload a local file to the file server and get a public URL.
Pre-checks file existence and size (soft cap 100MB) before uploading to
avoid wasting bandwidth. Returns the public URL or None on failure.
"""
if not os.path.exists(file_path):
print(f"文件不存在:{file_path}", file=sys.stderr)
return None
try:
file_size = os.path.getsize(file_path)
except OSError as e:
print(f"无法读取文件大小:{e}", file=sys.stderr)
return None
size_mb = file_size / (1024 * 1024)
if size_mb > _UPLOAD_SOFT_LIMIT_MB:
print(
f"文件过大 ({size_mb:.1f} MB > {_UPLOAD_SOFT_LIMIT_MB} MB),拒绝上传。"
f"请压缩或分段后重试。",
file=sys.stderr,
)
return None
_progress(f"[upload] 开始上传 {os.path.basename(file_path)} ({size_mb:.2f} MB)…")
try:
with open(file_path, 'rb') as f:
files = {'file': (os.path.basename(file_path), f)}
headers = {'X-Api-Key': API_KEY}
response = requests.post(FILE_UPLOAD_URL, headers=headers, files=files, timeout=120)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
url = result.get("url")
_progress(f"[upload] ✓ 上传完成:{url}")
return url
print(f"文件上传失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except requests.exceptions.HTTPError as e:
print(_explain_http_error(e, context="文件上传"), file=sys.stderr)
return None
except Exception as e:
print(f"文件上传错误:{e}", file=sys.stderr)
return None
def download_image(url, output_path=None):
"""
Download image from URL.
Args:
url: Image URL
output_path: Optional path to save the image
Returns:
bytes: Image data, or None if failed
"""
try:
response = requests.get(url, timeout=60)
response.raise_for_status()
image_data = response.content
# Save to file if path provided
if output_path:
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(image_data)
_progress(f"图片已保存:{output_path}")
return image_data
except Exception as e:
print(f"下载图片失败:{e}", file=sys.stderr)
return None
def image_to_data_uri(image_data, mime_type="image/png"):
"""
Convert image bytes to data URI.
Args:
image_data: Raw image bytes
mime_type: MIME type of the image
Returns:
str: Data URI string
"""
base64_data = base64.b64encode(image_data).decode('utf-8')
return f"data:{mime_type};base64,{base64_data}"
def send_feishu_message(prompt, result, media_type="image"):
"""Send generation result to Feishu chat (supports image or video)."""
if not FEISHU_WEBHOOK_URL:
return False
label = "图片" if media_type == "image" else "视频"
open_btn = "打开" + label
try:
if result and result["status"] == "SUCCESS":
content = {
"msg_type": "interactive",
"card": {
"header": {
"title": {"tag": "plain_text", "content": f"{label}生成成功"},
"template": "green"
},
"elements": [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": f"**提示词**: {prompt}\n\n**{label}链接**: [点击查看]({result['url']})"
}
},
{
"tag": "action",
"actions": [{
"tag": "button",
"text": {"tag": "plain_text", "content": open_btn},
"url": result["url"],
"type": "default"
}]
}
]
}
}
else:
error_msg = result.get("message", "未知错误") if result else "未知错误"
content = {
"msg_type": "interactive",
"card": {
"header": {
"title": {"tag": "plain_text", "content": f"{label}生成失败"},
"template": "red"
},
"elements": [{
"tag": "div",
"text": {
"tag": "lark_md",
"content": f"**提示词**: {prompt}\n\n**错误**: {error_msg}"
}
}]
}
}
response = requests.post(
FEISHU_WEBHOOK_URL,
json=content,
headers={"Content-Type": "application/json"},
timeout=30
)
response.raise_for_status()
return True
except Exception as e:
print(f"[Feishu] 发送通知失败:{e}", file=sys.stderr)
return False
# Model configurations
# media_type: "image" or "video" — determines task creation and output handling
# Keys follow API sourceName (DeepSop·X). Only hiddenState=0 (active) models are included.
# source_name / description mirror the API metadata for traceability.
# Note: per-model extra_params intentionally carry ONLY the fields each model
# actually accepts (cross-referenced with VIDEO_FIELD_SUPPORT / IMAGE_FIELD_SUPPORT).
# Target constraints (targetMaxSize / targetMinLength / targetMaxLength) are
# populated by `_apply_restriction()` at runtime and should NOT be duplicated here.
# ---------------------------------------------------------------------------
# Field → supported-models whitelist (mirrors frontend `handleParameterVisibility`).
# After the payload is built, fields not in the active model's whitelist are
# stripped so we don't send parameters the model does not understand.
# ---------------------------------------------------------------------------
# Image-side: webSearch only applies to S5.0L (mt=4) and 3.1Nano2-Evo (mt=8).
IMAGE_FIELD_SUPPORT = {
"webSearch": {"S5.0L", "3.1Nano2-Evo"},
}
# Video-side: each key = optional field; value = set of model keys that accept it.
# Fields NOT listed here (methodType, text, size, duration, generationType,
# imageUrlList, firstImageUrl, targetMax*) are considered universal/contextual
# and pass through unfiltered.
VIDEO_FIELD_SUPPORT = {
# Negative prompt: V3.1Fast + Wan series (mt 5,6,7,8,9,14,15,16)
"negativePrompt": {"V3.1Fast", "W2.6t", "W2.6i", "W2.6r",
"W2.7t", "W2.7i", "W2.7r"},
# Audio toggle: S1.5Pro, V3.1Fast, klingV3Omni (mt 2,5,10)
"generateAudio": {"S1.5Pro", "V3.1Fast", "klingV3Omni"},
# English enhancement: V3.1 series (mt 3,4,5)
"enhancePrompt": {"V3.1FB", "V3.1PB", "V3.1Fast"},
# Smart rewrite: Wan series (mt 7,8,9,14,15,16)
"promptExtend": {"W2.6t", "W2.6i", "W2.6r", "W2.7t", "W2.7i", "W2.7r"},
# Generation count / people / resize: V3.1Fast (mt 5)
"n": {"V3.1Fast"},
"personGeneration": {"V3.1Fast"},
"resizeMode": {"V3.1Fast"},
# Shot mode: Wan2.6 + klingV3Omni (mt 7,8,9,10)
"shotType": {"W2.6t", "W2.6i", "W2.6r", "klingV3Omni"},
# Duration switch (manual/intelligent): only S1.5Pro (mt 2)
"durationSwitch": {"S1.5Pro"},
# klingV3Omni exclusives (mt 10)
"mode": {"klingV3Omni"},
"multiShot": {"klingV3Omni"},
"multiPrompt": {"klingV3Omni"},
"keepOriginalSound": {"klingV3Omni"},
"elementList": {"klingV3Omni"},
"videoList": {"klingV3Omni"},
# Continuation / reference clip: klingV3Omni + W2.7i (mt 10,14)
"firstClipUrl": {"klingV3Omni", "W2.7i"},
# Reference-video list: W2.6r / W2.7r (mt 9,16)
"videoUrlList": {"W2.6r", "W2.7r"},
# Audio URL: Wan text/image/W2.7 series (mt 7,8,14,15,16)
"audioUrl": {"W2.6t", "W2.6i", "W2.7t", "W2.7i", "W2.7r"},
# lastImageUrl: NOT supported by W2.6i (mt=8) or Sora2 variants. All other
# active video models support it.
"lastImageUrl": {"S1.5Pro", "V3.1FB", "V3.1PB", "V3.1Fast",
"W2.6t", "W2.6r", "klingV3Omni",
"W2.7t", "W2.7i", "W2.7r"},
# ratio: W2.6i (mt=8) and W2.7i (mt=14) derive ratio from the first frame.
"ratio": {"S1.5Pro", "V3.1FB", "V3.1PB", "V3.1Fast",
"W2.6t", "W2.6r", "klingV3Omni",
"W2.7t", "W2.7r"},
# resolution: klingV3Omni (mt=10) does not expose a resolution selector.
"resolution": {"S1.5Pro", "V3.1FB", "V3.1PB", "V3.1Fast",
"W2.6t", "W2.6i", "W2.6r",
"W2.7t", "W2.7i", "W2.7r"},
}
def _filter_by_whitelist(parameter, model, support_matrix):
"""Drop keys from `parameter` that the whitelist says this model doesn't accept."""
for field, allowed_models in support_matrix.items():
if field in parameter and model not in allowed_models:
parameter.pop(field, None)
return parameter
# ---------------------------------------------------------------------------
# Allowed-value tables per model (mirrors frontend match* option builders).
# Values not listed will be auto-replaced with a safe fallback + warning.
# ---------------------------------------------------------------------------
# generationType whitelist per model (matchGenerationTypeOptions)
VIDEO_GENERATION_TYPES = {
"S1.5Pro": ["TEXT", "FIRST&LAST"],
"V3.1FB": ["TEXT", "FIRST&LAST", "REFERENCE"],
"V3.1PB": ["TEXT", "FIRST&LAST"],
"V3.1Fast": ["TEXT", "FIRST&LAST"],
"W2.6t": ["TEXT"],
"W2.6i": ["FIRST&LAST"],
"W2.6r": ["REFERENCE"],
"klingV3Omni": ["TEXT", "FIRST&LAST", "REFERENCE", "EDIT", "FEATURE"],
"W2.7i": ["FIRST&LAST", "CONTINUATION"],
"W2.7t": ["TEXT"],
"W2.7r": ["REFERENCE"],
}
# ratio whitelist per model (matchVideoRatioOptions). W2.6i / W2.7i derive from
# the first frame so ratio is not submitted at all (handled by VIDEO_FIELD_SUPPORT).
VIDEO_RATIOS = {
"S1.5Pro": ["1:1", "3:4", "4:3", "16:9", "9:16", "21:9", "adaptive"],
"V3.1FB": ["16:9", "9:16", "adaptive"],
"V3.1PB": ["16:9", "9:16", "adaptive"],
"V3.1Fast": ["16:9", "9:16", "adaptive"],
"W2.6t": ["1:1", "3:4", "4:3", "16:9", "9:16"],
"W2.6r": ["1:1", "3:4", "4:3", "16:9", "9:16"],
"klingV3Omni": ["1:1", "16:9", "9:16"],
"W2.7t": ["1:1", "3:4", "4:3", "16:9", "9:16"],
"W2.7r": ["1:1", "3:4", "4:3", "16:9", "9:16"],
}
# resolution whitelist per model (matchVideoQualityOptions). klingV3Omni does
# not submit a resolution at all (handled by VIDEO_FIELD_SUPPORT).
VIDEO_RESOLUTIONS = {
"S1.5Pro": ["480p", "720p", "1080p"],
"V3.1FB": ["720p", "1080p", "4K"],
"V3.1PB": ["720p", "1080p", "4K"],
"V3.1Fast": ["720p", "1080p", "4K"],
"W2.6t": ["720p", "1080p"],
"W2.6i": ["720p", "1080p"],
"W2.6r": ["720p", "1080p"],
"W2.7i": ["720p", "1080p"],
"W2.7t": ["720p", "1080p"],
"W2.7r": ["720p", "1080p"],
}
# Image quality whitelist (matchImageQualityOptions, active models only)
IMAGE_QUALITIES = {
"N2": ["1K", "2K", "4K"],
"S5.0L": ["2K", "3K"],
"W2.7": ["1K", "2K"],
"W2.7Pro": ["1K", "2K"],
"3.1Nano2-Evo": ["1K", "2K", "4K"],
"Nano2-Beta-Evo": ["1K", "2K", "4K"],
}
# Image ratio/size exclusions (matchImageRatioOptions excludedRatios).
# Values listed are NOT allowed; empty set means any ratio allowed.
# Note: only relevant when the user passes a ratio-style size (e.g. "16:9");
# pixel-size strings like "2048x2048" / "2048*2048" pass through unchecked.
IMAGE_SIZE_EXCLUDED = {
"N2": [],
"S5.0L": ["auto"],
"W2.7": ["auto", "21:9"],
"W2.7Pro": ["auto", "21:9"],
"3.1Nano2-Evo": [],
"Nano2-Beta-Evo": [],
}
def _coerce_value(current, allowed, fallback, label, model):
"""If current not in allowed, warn and return fallback; else return current."""
if current is None:
return current
if current in allowed:
return current
print(
f"{model} 不支持 {label}={current!r}(可选:{allowed}),自动调整为 {fallback!r}",
file=sys.stderr,
)
return fallback
# ---------------------------------------------------------------------------
# Per-model restrictions (sourced from frontend `Restrictions` mixin).
# - textLength / negativeTextLength → prompt length caps (chars)
# - targetMaxSize / targetMinLength / targetMaxLength → reference-image
# constraints that MUST be forwarded to the API via the `targetMax*` fields.
# ---------------------------------------------------------------------------
VIDEO_RESTRICTIONS = {
"S1.5Pro": {"textLength": 500, "targetMaxSize": 30, "targetMinLength": 300, "targetMaxLength": 6000},
"V3.1FB": {"textLength": 1000, "negativeTextLength": 250, "targetMaxSize": 10, "targetMinLength": 300, "targetMaxLength": 6000},
"V3.1PB": {"textLength": 1000, "negativeTextLength": 250, "targetMaxSize": 10, "targetMinLength": 300, "targetMaxLength": 6000},
"V3.1Fast": {"textLength": 1000, "negativeTextLength": 250, "targetMaxSize": 10, "targetMinLength": 300, "targetMaxLength": 6000},
"W2.6t": {"textLength": 750, "negativeTextLength": 250, "targetMaxSize": 10, "targetMinLength": 360, "targetMaxLength": 2000},
"W2.6i": {"textLength": 750, "negativeTextLength": 250, "targetMaxSize": 10, "targetMinLength": 360, "targetMaxLength": 2000},
"W2.6r": {"textLength": 750, "negativeTextLength": 250, "targetMaxSize": 10, "targetMinLength": 240, "targetMaxLength": 5000},
"klingV3Omni": {"textLength": 1250, "targetMaxSize": 10, "targetMinLength": 300},
"W2.7i": {"textLength": 2500, "negativeTextLength": 250, "targetMaxSize": 20, "targetMinLength": 240, "targetMaxLength": 8000},
"W2.7t": {"textLength": 2500, "negativeTextLength": 250, "targetMaxSize": 20, "targetMinLength": 240, "targetMaxLength": 8000},
"W2.7r": {"textLength": 2500, "negativeTextLength": 250, "targetMaxSize": 10, "targetMinLength": 240, "targetMaxLength": 5000},
}
IMAGE_RESTRICTIONS = {
"N2": {"textLength": 1000, "targetMaxSize": 10, "targetMaxLength": 6000},
"S5.0L": {"textLength": 300, "targetMaxSize": 10, "targetMaxLength": 6000},
"W2.7": {"textLength": 2500, "targetMaxSize": 20, "targetMaxLength": 8000, "targetMinLength": 240},
"W2.7Pro": {"textLength": 2500, "targetMaxSize": 20, "targetMaxLength": 8000, "targetMinLength": 240},
"3.1Nano2-Evo": {"textLength": 1000, "targetMaxSize": 20, "targetMaxLength": 6000},
"Nano2-Beta-Evo": {"textLength": 1000, "targetMaxSize": 10, "targetMaxLength": 6000},
}
def _apply_restriction(parameter, restriction):
"""Overwrite targetMaxSize / targetMinLength / targetMaxLength per restriction."""
for key in ("targetMaxSize", "targetMinLength", "targetMaxLength"):
if key in restriction:
parameter[key] = restriction[key]
else:
parameter.pop(key, None)
def _check_text_length(text, limit, label, model):
"""Warn (but don't block) when text exceeds the model's limit."""
if text and limit and len(str(text)) > limit:
print(
f"⚠️ {model} {label} 长度 {len(text)} 超过限制 {limit},已截断末尾 {len(text) - limit} 字符后提交",
file=sys.stderr,
)
return str(text)[:limit]
return text
MODEL_CONFIGS = {
# ===== Image models (type=10) =====
"N2": {
"media_type": "image",
"type": "10",
"methodType": "2",
"source_name": "DeepSop·N2",
"description": "N2 支持多模态输入 精细参数调节 卓越的文字渲染和角色一致性",
"default_size": "1:1",
"default_quality": "2K",
"extra_params": {}
},
"S5.0L": {
"media_type": "image",
"type": "10",
"methodType": "4",
"source_name": "DeepSop·S5.0L",
"description": "生成快、风格全、易用,支持联网,适合快速出图",
"default_size": "2048x2048",
"default_quality": "2K",
"extra_params": {"duration": 10}
},
"W2.7": {
"media_type": "image",
"type": "10",
"methodType": "6",
"source_name": "DeepSop.W2.7",
"description": "W2.7 支持文生图、图生图多模态输入,画质清晰,细节丰富",
"default_size": "2048*2048",
"default_quality": "2K",
"extra_params": {}
},
"W2.7Pro": {
"media_type": "image",
"type": "10",
"methodType": "7",
"source_name": "DeepSop.W2.7Pro",
"description": "W2.7Pro 精准控图与风格迁移,角色一致性更优,画质细节更优",
"default_size": "2048*2048",
"default_quality": "2K",
"extra_params": {}
},
"3.1Nano2-Evo": {
"media_type": "image",
"type": "10",
"methodType": "8",
"source_name": "DeepSop·3.1Nano2-Evo",
"description": "N2 支持多模态输入 精细参数调节 卓越的文字渲染和角色一致性",
"default_size": "1:1",
"default_quality": "2K",
"extra_params": {}
},
"Nano2-Beta-Evo": {
"media_type": "image",
"type": "10",
"methodType": "9",
"source_name": "DeepSop·Nano2 Beta-Evo",
"description": "N2 支持多模态输入 精细参数调节 卓越的文字渲染和角色一致性",
"default_size": "1:1",
"default_quality": "2K",
"extra_params": {}
},
# ----- Image models currently hiddenState=1 (kept for future reactivation) -----
"S4.5": {
"media_type": "image",
"type": "10",
"methodType": "0",
"source_name": "DeepSop·S4.5",
"description": "S4.5 支持电影级画质4K 角色一致性",
"default_size": "2048x2048",
"default_quality": "2K",
"extra_params": {}
},
"N1": {
"media_type": "image",
"type": "10",
"methodType": "1",
"source_name": "DeepSop·N1",
"description": "N1 支持多模态输入 精细参数调节 卓越的文字渲染和角色一致性",
"default_size": "1:1",
"default_quality": "1K",
"extra_params": {}
},
"N2-147": {
"media_type": "image",
"type": "10",
"methodType": "3",
"source_name": "DeepSop·3-Nano2-147",
"description": "N2 支持多模态输入 精细参数调节 卓越的文字渲染和角色一致性",
"default_size": "1:1",
"default_quality": "2K",
"extra_params": {}
},
"N2Pro-147": {
"media_type": "image",
"type": "10",
"methodType": "5",
"source_name": "DeepSop·3.1Nano2-147",
"description": "N2 支持多模态输入 精细参数调节 卓越的文字渲染和角色一致性",
"default_size": "1:1",
"default_quality": "2K",
"extra_params": {}
},
# ===== Video models (type=9) =====
"S1.5Pro": {
"media_type": "video",
"type": "9",
"methodType": "2",
"source_name": "DeepSop·S1.5Pro",
"description": "S1.5Pro 影视级连贯叙事视频 音画同步与精准口型对齐",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"firstImageUrl": None,
"lastImageUrl": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 30,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"V3.1FB": {
"media_type": "video",
"type": "9",
"methodType": "3",
"source_name": "DeepSop·V3.1FB",
"description": "V3.1FB 快速生成 基础流畅",
"default_ratio": "16:9",
"default_resolution": "1080p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"enhancePrompt": False,
"durationList": [],
}
},
"V3.1PB": {
"media_type": "video",
"type": "9",
"methodType": "4",
"source_name": "DeepSop·V3.1PB",
"description": "V3.1Pro 多图参考 角色一致性",
"default_ratio": "adaptive",
"default_resolution": "720p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"enhancePrompt": False,
"durationList": [],
}
},
"V3.1Fast": {
"media_type": "video",
"type": "9",
"methodType": "5",
"source_name": "DeepSop·V3.1Fast",
"description": "V3.1Fast 快速生成 音画同步 竖屏适配",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"durationList": [],
}
},
"W2.6t": {
"media_type": "video",
"type": "9",
"methodType": "7",
"source_name": "DeepSop·W2.6t",
"description": "W2.6t 文生视频 智能多镜头叙事 15秒 1080P高清",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "TEXT",
"negativePrompt": "",
"promptExtend": False,
"shotType": "single",
"durationList": [],
}
},
"W2.6i": {
"media_type": "video",
"type": "9",
"methodType": "8",
"source_name": "DeepSop·W2.6i",
"description": "W2.6i 适合让插画或照片\"活起来\" 动作延展与场景叙事",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"promptExtend": False,
"shotType": "single",
"durationList": [],
}
},
"W2.6r": {
"media_type": "video",
"type": "9",
"methodType": "9",
"source_name": "DeepSop·W2.6r",
"description": "W2.6r 参考视频生成视频 保留角色和音色 可跨场景迁移与互动",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "REFERENCE",
"negativePrompt": "",
"promptExtend": False,
"shotType": "single",
"durationList": [],
}
},
"klingV3Omni": {
"media_type": "video",
"type": "9",
"methodType": "10",
"source_name": "DeepSop.klingV3Omni",
"description": "支持多模态融合输入,画面细节丰富,角色与场景一致性更优(按张计费)",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"firstClipUrl": None,
"elementList": [],
"durationList": [],
"mode": "pro",
"multiShot": False,
"keepOriginalSound": "yes",
"generateAudio": True,
"shotType": "single",
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000,
}
},
"W2.7i": {
"media_type": "video",
"type": "9",
"methodType": "14",
"source_name": "DeepSop·W2.7i",
"description": "W2.7i 图生视频 首尾帧平滑过渡 动作延展与视频续写 更流畅自然",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"promptExtend": False,
"durationList": [],
}
},
"W2.7t": {
"media_type": "video",
"type": "9",
"methodType": "15",
"source_name": "DeepSop.W2.7t",
"description": "W2.7t 文生视频 智能多镜头剪辑 自动配音 2K高清 成片更高效",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "TEXT",
"negativePrompt": "",
"promptExtend": False,
"durationList": [],
}
},
"W2.7r": {
"media_type": "video",
"type": "9",
"methodType": "16",
"source_name": "DeepSop.W2.7r",
"description": "W2.7r 参考视频生成 保留角色音色 多模态融合编辑 跨场景迁移",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "REFERENCE",
"negativePrompt": "",
"promptExtend": False,
"durationList": [],
}
},
# ----- Video models currently hiddenState=1 (kept for future reactivation) -----
"Sora2-BetaMax": {
"media_type": "video",
"type": "9",
"methodType": "1",
"source_name": "DeepSop·Sora2 Beta Max Evolink",
"description": "Sora 2 Beta Max Evolink",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {}
},
"V3.1Pro": {
"media_type": "video",
"type": "9",
"methodType": "6",
"source_name": "DeepSop·V3.1Pro",
"description": "专业版模型 4K超清 多图参考角色跨场景一致性 商业级",
"default_ratio": "16:9",
"default_resolution": "1080p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"durationList": [],
}
},
"Sora2-147": {
"media_type": "video",
"type": "9",
"methodType": "11",
"source_name": "DeepSop·Sora2.147",
"description": "物理真实、叙事连贯、音画同步,电影级质感",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {}
},
"Sora2Pro-147": {
"media_type": "video",
"type": "9",
"methodType": "12",
"source_name": "DeepSop·Sora2 Pro.147",
"description": "物理真实、时长更长、音画同步、画质专业、影视级可控性强",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {}
},
"Sora2Pro-Evolink": {
"media_type": "video",
"type": "9",
"methodType": "13",
"source_name": "DeepSop·Sora2 Pro Evolink",
"description": "原生视频生成,具备帧级动态控制、音画同步等视频专属能力",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {}
}
}
def create_video_task(prompt, model="V3.1FB", ratio=None, resolution=None,
duration=None, first_image_url=None, last_image_url=None,
generate_audio=None, scale_factor=None, generation_type=None,
enhance_prompt=None, prompt_extend=None, audio_url=None,
image_url_list=None, video_url_list=None,
mode=None, keep_original_sound=None, shot_type=None,
element_list=None, first_clip_url=None, multi_shot=None,
n=None, person_generation=None, resize_mode=None,
negative_prompt=None, duration_switch=None,
multi_prompt=None):
"""Create a video generation task.
Args:
prompt: Text description of the video
model: Video model key (e.g. S1.5Pro, V3.1FB, V3.1PB, V3.1Fast,
W2.6t, W2.6i, W2.6r, klingV3Omni, W2.7i, W2.7t, W2.7r)
ratio: Aspect ratio, e.g. '16:9', '9:16', '1:1'
resolution: Video resolution, e.g. '720p', '1080p'
duration: Video duration in seconds
first_image_url: URL of the first frame image (FIRST&LAST mode)
last_image_url: URL of the last frame image (FIRST&LAST mode)
generate_audio: Whether to generate audio (True/False)
scale_factor: Optional scaleFactor override
generation_type: Generation type override, e.g. 'FIRST&LAST', 'TEXT', 'REFERENCE'
enhance_prompt: Whether to enhance the prompt
prompt_extend: Whether to extend the prompt
audio_url: URL of audio file (WAN series)
image_url_list: List of image URLs for reference (WAN *r / multimodal)
video_url_list: List of video URLs for reference (WAN *r)
"""
url = f"{BASE_URL}/AiArtistRecord"
if model not in MODEL_CONFIGS or MODEL_CONFIGS[model]["media_type"] != "video":
print(f"不支持的视频模型:{model}", file=sys.stderr)
return None
# Prompt requirement (mirrors frontend handleVerifyParams):
# - W2.6i / W2.7i (image-to-video) can omit prompt
# - klingV3Omni with shotType='customize' uses per-shot prompts, not top-level
# - all other video models require a non-empty prompt
_image_to_video = model in {"W2.6i", "W2.7i"}
_kling_customize = (model == "klingV3Omni" and shot_type == "customize")
if not _image_to_video and not _kling_customize:
if not prompt or not str(prompt).strip():
print(f"模型 {model} 必须提供非空的生成提示词 (prompt)", file=sys.stderr)
return None
# Runtime availability check: consumeSource/list 可能随时将模型切成 hiddenState=1
if not check_model_available(model):
return None
# Apply per-model length caps (truncate with warning, match frontend maxlength)
restriction = VIDEO_RESTRICTIONS.get(model, {})
prompt = _check_text_length(prompt, restriction.get("textLength"), "prompt", model)
if negative_prompt is not None:
negative_prompt = _check_text_length(
negative_prompt, restriction.get("negativeTextLength"),
"negativePrompt", model,
)
config = MODEL_CONFIGS[model]
effective_ratio = ratio or config.get("default_ratio", "16:9")
effective_resolution = resolution or config.get("default_resolution", "720p")
effective_duration = duration or config.get("default_duration", 10)
# Validate ratio / resolution / generationType against per-model whitelists.
if model in VIDEO_RATIOS:
effective_ratio = _coerce_value(
effective_ratio, VIDEO_RATIOS[model],
config.get("default_ratio", "16:9"), "ratio", model,
)
if model in VIDEO_RESOLUTIONS:
effective_resolution = _coerce_value(
effective_resolution, VIDEO_RESOLUTIONS[model],
config.get("default_resolution", "720p"), "resolution", model,
)
if generation_type is not None and model in VIDEO_GENERATION_TYPES:
generation_type = _coerce_value(
generation_type, VIDEO_GENERATION_TYPES[model],
VIDEO_GENERATION_TYPES[model][0], "generationType", model,
)
parameter = dict(config["extra_params"]) # copy defaults
# Resolve pixel size from ratio + resolution
resolution_size_map = {
("16:9", "720p"): "1280x720",
("16:9", "1080p"): "1920x1080",
("9:16", "720p"): "720x1280",
("9:16", "1080p"): "1080x1920",
("1:1", "720p"): "720x720",
("1:1", "1080p"): "1080x1080",
("3:4", "720p"): "720x960",
("3:4", "1080p"): "1080x1440",
("4:3", "720p"): "960x720",
("4:3", "1080p"): "1440x1080",
}
pixel_size = resolution_size_map.get((effective_ratio, effective_resolution), effective_ratio)
parameter.update({
"methodType": config["methodType"],
"text": prompt,
"resolution": effective_resolution,
"ratio": effective_ratio,
"size": pixel_size,
"duration": effective_duration,
})
# --- Duration rules (aligned with frontend `matchVideoDurationInfo`) ---
# V3.1FB (mt=3), V3.1PB (mt=4): fixed 8 seconds
if model in ["V3.1FB", "V3.1PB"]:
if effective_duration != 8:
print(f"{model} 时长固定为 8 秒,当前 {effective_duration} 秒,自动调整为 8 秒")
effective_duration = 8
parameter["duration"] = effective_duration
parameter["size"] = effective_ratio
# V3.1Fast (mt=5): 4 or 8 seconds
if model == "V3.1Fast":
if effective_duration not in [4, 8]:
print(f"{model} 时长必须是 4 或 8 秒,当前 {effective_duration} 秒,自动调整为 8 秒")
effective_duration = 8
parameter["duration"] = effective_duration
parameter["size"] = effective_ratio
# WAN2.6 / WAN2.7 series & klingV3Omni: size + duration rules
# Size format (per frontend): only W2.6t (mt=7) and W2.6r (mt=9) use '*'-separated pixels;
# W2.6i, W2.7*, klingV3Omni all submit size = ratio string.
wan_text_models = {"W2.6t", "W2.7t"}
wan_image_models = {"W2.6i", "W2.7i"}
wan_ref_models = {"W2.6r", "W2.7r"}
wan_models = wan_text_models | wan_image_models | wan_ref_models
pixel_size_models = {"W2.6t", "W2.6r"} # only these use '1280*720' form
if model in wan_models or model == "klingV3Omni":
# Duration range
if model == "W2.6r":
min_d, max_d, default_d = 3, 10, 10
elif model == "W2.7r" and video_url_list:
# W2.7r with reference video(s): 3-10s (frontend videoUrlList?.length)
min_d, max_d, default_d = 3, 10, 10
else:
# W2.6t/W2.6i, W2.7t/W2.7i, klingV3Omni, W2.7r (no ref video) → 3-15s
min_d, max_d, default_d = 3, 15, 10
if effective_duration < min_d or effective_duration > max_d:
print(f"{model} 时长必须是 {min_d}-{max_d} 秒,当前 {effective_duration} 秒,自动调整为 {default_d} 秒")
effective_duration = default_d
parameter["duration"] = effective_duration
# Size serialization
if model in pixel_size_models:
parameter["size"] = pixel_size.replace("x", "*") # e.g., "1280*720"
else:
parameter["size"] = effective_ratio # e.g., "16:9"
# Image-to-video: auto-switch generationType based on first_image_url
if model in wan_image_models and generation_type is None:
parameter["generationType"] = "FIRST&LAST"
# Reference-to-video: force REFERENCE generationType (W2.6r / W2.7r)
if model in wan_ref_models:
parameter["generationType"] = "REFERENCE"
# Apply optional overrides
if first_image_url is not None:
parameter["firstImageUrl"] = first_image_url
if last_image_url is not None:
parameter["lastImageUrl"] = last_image_url
if generate_audio is not None:
parameter["generateAudio"] = generate_audio
if scale_factor is not None:
parameter["scaleFactor"] = scale_factor
if generation_type is not None:
parameter["generationType"] = generation_type
if enhance_prompt is not None:
parameter["enhancePrompt"] = enhance_prompt
if prompt_extend is not None:
parameter["promptExtend"] = prompt_extend
# WAN series: audio_url, image_url_list, video_url_list
if audio_url is not None:
parameter["audioUrl"] = audio_url
if image_url_list is not None:
parameter["imageUrlList"] = image_url_list
if video_url_list is not None:
parameter["videoUrlList"] = video_url_list
# Model-specific exclusives
if mode is not None:
parameter["mode"] = mode
if keep_original_sound is not None:
parameter["keepOriginalSound"] = keep_original_sound
if shot_type is not None:
parameter["shotType"] = shot_type
if element_list is not None:
parameter["elementList"] = element_list
if first_clip_url is not None:
parameter["firstClipUrl"] = first_clip_url
if multi_shot is not None:
parameter["multiShot"] = multi_shot
if n is not None:
parameter["n"] = n
if person_generation is not None:
parameter["personGeneration"] = person_generation
if resize_mode is not None:
parameter["resizeMode"] = resize_mode
if negative_prompt is not None:
parameter["negativePrompt"] = negative_prompt
if duration_switch is not None:
parameter["durationSwitch"] = duration_switch
if multi_prompt is not None:
parameter["multiPrompt"] = multi_prompt
# klingV3Omni customize shotType requires multiPrompt
if model == "klingV3Omni" and parameter.get("shotType") == "customize" \
and not parameter.get("multiPrompt"):
print(
"klingV3Omni shotType='customize' 需要传入 multi_prompt(分镜列表),"
"当前为空,可能会被 API 拒绝",
file=sys.stderr,
)
# klingV3Omni-specific serialization (mirrors frontend `buildNewParams`):
# - shotType 'multi' must be emitted as 'intelligence'
# - firstClipUrl + keep_original_sound are packed into a `videoList` array
# whose `refer_type` depends on generationType (base for EDIT, feature
# for FEATURE). generateAudio is disabled when a reference clip is given.
if model == "klingV3Omni":
if parameter.get("shotType") == "multi":
parameter["shotType"] = "intelligence"
clip_url = parameter.pop("firstClipUrl", None)
if clip_url:
gen_type = parameter.get("generationType")
refer_type = "base" if gen_type == "EDIT" else "feature"
parameter["videoList"] = [{
"video_url": clip_url,
"refer_type": refer_type,
"keep_original_sound": parameter.get("keepOriginalSound", "yes"),
}]
# When a reference clip is supplied, mute generated audio (frontend rule)
parameter["generateAudio"] = False
# Reapply the text length-capped prompt / negativePrompt
parameter["text"] = prompt
if negative_prompt is not None:
parameter["negativePrompt"] = negative_prompt
# Overwrite targetMaxSize / targetMinLength / targetMaxLength per model
_apply_restriction(parameter, restriction)
# Strip fields this model does not accept (mirrors frontend visibility rules)
_filter_by_whitelist(parameter, model, VIDEO_FIELD_SUPPORT)
payload = {
"type": config["type"],
"methodType": config["methodType"],
"parameter": json.dumps(parameter)
}
if DRY_RUN:
_progress("[dry-run] 视频任务 payload(未提交):")
_progress(json.dumps(payload, ensure_ascii=False, indent=2))
return "DRY_RUN_TASK_ID"
if not estimate_generation_cost(payload):
return None
try:
response = requests.post(url, json=payload, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") == 200 and result.get("data"):
return result["data"][0]
else:
print(f"创建视频任务失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except requests.exceptions.HTTPError as e:
print(_explain_http_error(e, context="创建视频任务"), file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
print(f"网络错误:{e}", file=sys.stderr)
return None
def generate_video(prompt, model="V3.1FB", ratio=None, resolution=None,
duration=None, poll_interval=5, first_image_url=None,
last_image_url=None, generate_audio=None, scale_factor=None,
generation_type=None, enhance_prompt=None, prompt_extend=None,
first_image_path=None, last_image_path=None, audio_url=None,
image_url_list=None, video_url_list=None, audio_path=None,
mode=None, keep_original_sound=None, shot_type=None,
element_list=None, first_clip_url=None, multi_shot=None,
n=None, person_generation=None, resize_mode=None,
negative_prompt=None, duration_switch=None,
multi_prompt=None, max_wait=1200):
"""Generate a video from a text prompt.
Args:
prompt: Text description of the video
model: Video model key (e.g. S1.5Pro, V3.1FB, V3.1PB, V3.1Fast,
W2.6t, W2.6i, W2.6r, klingV3Omni, W2.7i, W2.7t, W2.7r)
ratio: Aspect ratio (e.g. '16:9')
resolution: Video resolution (e.g. '720p')
duration: Video duration in seconds
poll_interval: Polling interval in seconds
first_image_url: URL of the first frame image (FIRST&LAST mode)
last_image_url: URL of the last frame image (FIRST&LAST mode)
generate_audio: Whether to generate audio
scale_factor: Optional scaleFactor override
generation_type: Generation type override
enhance_prompt: Whether to enhance the prompt
prompt_extend: Whether to extend the prompt
first_image_path: Local path to first frame image (auto-uploaded)
last_image_path: Local path to last frame image (auto-uploaded)
audio_url: URL of audio file (WAN series)
audio_path: Local path to audio file (auto-uploaded, WAN series)
image_url_list: List of image URLs for reference (WAN *r / multimodal)
video_url_list: List of video URLs for reference (WAN *r)
Returns:
dict with 'status', 'url', 'message'
"""
# Upload local files to get URLs if provided
if first_image_path and not first_image_url:
first_image_url = upload_file(first_image_path)
if last_image_path and not last_image_url:
last_image_url = upload_file(last_image_path)
if audio_path and not audio_url:
audio_url = upload_file(audio_path)
config = MODEL_CONFIGS.get(model, {})
effective_ratio = ratio or config.get("default_ratio", "16:9")
effective_resolution = resolution or config.get("default_resolution", "720p")
effective_duration = duration or config.get("default_duration", 10)
_progress(f"正在生成视频:{prompt}")
_progress(f" 模型:{model} | 分辨率:{effective_resolution} | 比例:{effective_ratio} | 时长:{effective_duration}s")
if first_image_url:
_progress(f" 首帧图片:{first_image_url}")
if last_image_url:
_progress(f" 尾帧图片:{last_image_url}")
if audio_url:
_progress(f" 音频:{audio_url}")
if image_url_list:
_progress(f" 参考图片:{image_url_list}")
if video_url_list:
_progress(f" 参考视频:{video_url_list}")
task_id = create_video_task(
prompt, model, ratio, resolution, duration,
first_image_url=first_image_url,
last_image_url=last_image_url,
generate_audio=generate_audio,
scale_factor=scale_factor,
generation_type=generation_type,
enhance_prompt=enhance_prompt,
prompt_extend=prompt_extend,
audio_url=audio_url,
image_url_list=image_url_list,
video_url_list=video_url_list,
mode=mode,
keep_original_sound=keep_original_sound,
shot_type=shot_type,
element_list=element_list,
first_clip_url=first_clip_url,
multi_shot=multi_shot,
n=n,
person_generation=person_generation,
resize_mode=resize_mode,
negative_prompt=negative_prompt,
duration_switch=duration_switch,
multi_prompt=multi_prompt,
)
if not task_id:
return None
_progress(f" 任务 ID: {task_id}")
_progress(f" 开始轮询任务结果(间隔 {poll_interval}s,最长等待 {max_wait}s)…")
result = poll_task_status(task_id, interval=poll_interval, max_wait=max_wait)
if result and result["status"] == "SUCCESS":
_progress(f"视频生成成功!链接:{result['url']}")
else:
print(f"视频生成失败:{result.get('message', '未知错误')}", file=sys.stderr)
return result
def create_generation_task(prompt, quality="2K", size=None, model="3.1Nano2-Evo",
reference_image_url=None, web_search=None):
"""Create an image generation task.
Args:
prompt: Text description of the image
quality: Image quality (2K/4K)
size: Image dimensions. S5.0L / W2.7 / W2.7Pro use e.g. '2048x2048';
N2 / 3.1Nano2-Evo / Nano2-Beta-Evo use e.g. '1:1'
model: Image model key (N2, S5.0L, W2.7, W2.7Pro, 3.1Nano2-Evo, Nano2-Beta-Evo)
reference_image_url: Optional reference image URL for image-to-image generation
"""
url = f"{BASE_URL}/AiArtistRecord"
if model not in MODEL_CONFIGS:
print(f"不支持的模型:{model},可用模型:{list(MODEL_CONFIGS.keys())}", file=sys.stderr)
return None
# Image generation always requires a non-empty prompt (frontend: required rule)
if not prompt or not str(prompt).strip():
print(f"模型 {model} 必须提供非空的生成提示词 (prompt)", file=sys.stderr)
return None
# Runtime availability check: consumeSource/list 可能随时将模型切成 hiddenState=1
if not check_model_available(model):
return None
# Apply per-model prompt length cap
image_restriction = IMAGE_RESTRICTIONS.get(model, {})
prompt = _check_text_length(prompt, image_restriction.get("textLength"), "prompt", model)
config = MODEL_CONFIGS[model]
# Validate quality against per-model whitelist (matchImageQualityOptions)
if model in IMAGE_QUALITIES:
quality = _coerce_value(
quality, IMAGE_QUALITIES[model],
config.get("default_quality", "2K"), "quality", model,
)
# Use model's default size if not specified
if size is None:
size = config["default_size"]
else:
# Validate ratio-style size (e.g. "16:9"); pixel sizes contain 'x' or '*'.
is_pixel_size = ("x" in size and any(c.isdigit() for c in size.split("x")[0])) \
or ("*" in size and any(c.isdigit() for c in size.split("*")[0]))
if not is_pixel_size and model in IMAGE_SIZE_EXCLUDED:
excluded = IMAGE_SIZE_EXCLUDED[model]
if size in excluded:
print(
f"{model} 不支持 size={size!r}(被排除:{excluded}),"
f"自动调整为默认值 {config['default_size']!r}",
file=sys.stderr,
)
size = config["default_size"]
# Build image array - support reference image for image-to-image
image_array = []
if reference_image_url:
image_array = [reference_image_url]
parameter = {
"methodType": config["methodType"],
"prompt": prompt,
"image": image_array,
"quality": quality,
"size": size,
"webSearch": bool(web_search) if web_search is not None else False,
"targetMaxSize": 10,
"targetMaxLength": 6000,
}
# Merge model-specific extra params
parameter.update(config["extra_params"])
# Overwrite targetMaxSize / targetMinLength / targetMaxLength per model
_apply_restriction(parameter, image_restriction)
# Strip image fields this model does not accept (mirrors frontend rules)
_filter_by_whitelist(parameter, model, IMAGE_FIELD_SUPPORT)
payload = {
"type": config["type"],
"methodType": config["methodType"],
"parameter": json.dumps(parameter)
}
if DRY_RUN:
_progress("[dry-run] 图片任务 payload(未提交):")
_progress(json.dumps(payload, ensure_ascii=False, indent=2))
return "DRY_RUN_TASK_ID"
if not estimate_generation_cost(payload):
return None
try:
response = requests.post(url, json=payload, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") == 200 and result.get("data"):
return result["data"][0]
else:
print(f"创建任务失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except requests.exceptions.HTTPError as e:
print(_explain_http_error(e, context="创建图片任务"), file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
print(f"网络错误:{e}", file=sys.stderr)
return None
def poll_task_status(task_id, interval=5, max_wait=1200):
"""Poll the task status until completion or failure."""
if task_id == "DRY_RUN_TASK_ID":
return {"status": "SUCCESS", "url": None,
"message": "dry-run 模式,未提交真实任务"}
url = f"{BASE_URL}/AiArtistImage/getInfoByArtistId/{task_id}"
elapsed = 0
last_status = None
while elapsed < max_wait:
try:
response = requests.get(url, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
time.sleep(interval)
elapsed += interval
continue
data = result.get("data", {})
status = data.get("status", "")
# Only print status when it changes
if status != last_status:
_progress(f"{status} - {data.get('message', '')}")
last_status = status
if status == "SUCCESS":
return {
"status": "SUCCESS",
"url": data.get("url"),
"message": data.get("message", "生成成功")
}
elif status == "FAILED":
return {
"status": "FAILED",
"url": None,
"message": data.get("message", "生成失败")
}
else:
time.sleep(interval)
elapsed += interval
except requests.exceptions.RequestException as e:
print(f"查询状态出错:{e}", file=sys.stderr)
time.sleep(interval)
elapsed += interval
return {
"status": "TIMEOUT",
"url": None,
"message": f"超时({max_wait}秒)"
}
def generate_image(prompt, quality="2K", size=None, poll_interval=5,
download=False, output_dir=None, model="3.1Nano2-Evo",
reference_image_path=None, reference_image_url=None,
web_search=None, max_wait=1200):
"""
Main function to generate an image from a prompt.
Args:
prompt: Text description of the image
quality: Image quality (2K/4K)
size: Image dimensions. Defaults to model's default size if not specified.
S5.0L / W2.7 / W2.7Pro: e.g. '2048x2048'
N2 / 3.1Nano2-Evo / Nano2-Beta-Evo: e.g. '1:1'
poll_interval: Polling interval in seconds
download: Whether to download the image
output_dir: Directory to save the image (default: workspace/images)
model: Image model key (N2, S5.0L, W2.7, W2.7Pro, 3.1Nano2-Evo, Nano2-Beta-Evo)
reference_image_path: Local path to reference image (auto-uploaded)
reference_image_url: URL of reference image (if already uploaded)
Returns:
dict with generation result including 'url', 'local_path', 'data_uri' if successful
"""
config = MODEL_CONFIGS.get(model, {})
effective_size = size or config.get("default_size", "2048x2048")
# Upload reference image if local path provided
if reference_image_path and not reference_image_url:
reference_image_url = upload_file(reference_image_path)
_progress(f"正在生成:{prompt}")
_progress(f" 模型:{model} | 质量:{quality} | 尺寸:{effective_size}")
if reference_image_url:
_progress(f" 参考图:{reference_image_url}")
# Step 1: Create task
task_id = create_generation_task(prompt, quality, size, model, reference_image_url,
web_search=web_search)
if not task_id:
return None
_progress(f" 任务 ID: {task_id}")
_progress(f" 开始轮询任务结果(间隔 {poll_interval}s,最长等待 {max_wait}s)…")
# Step 2: Poll until complete
result = poll_task_status(task_id, interval=poll_interval, max_wait=max_wait)
if result and result["status"] == "SUCCESS":
_progress(f"生成成功!链接:{result['url']}")
# Download image if requested
if download and result.get("url"):
if not output_dir:
output_dir = os.path.join(os.path.expanduser("~"), ".openclaw", "workspace", "images")
# Generate filename from prompt
safe_prompt = "".join(c if c.isalnum() or c in (' ', '-', '_') else '_' for c in prompt)
safe_prompt = safe_prompt[:50].strip().replace(' ', '_')
filename = f"{safe_prompt}_{int(time.time())}.png"
output_path = os.path.join(output_dir, filename)
image_data = download_image(result["url"], output_path)
if image_data:
result["local_path"] = output_path
result["data_uri"] = image_to_data_uri(image_data)
result["image_data"] = image_data # Raw bytes for programmatic use
return result
else:
print(f"生成失败:{result.get('message', '未知错误')}", file=sys.stderr)
return result
if __name__ == "__main__":
# Check API key before proceeding
check_api_key()
image_models = [k for k, v in MODEL_CONFIGS.items() if v["media_type"] == "image"]
video_models = [k for k, v in MODEL_CONFIGS.items() if v["media_type"] == "video"]
all_models = list(MODEL_CONFIGS.keys())
parser = argparse.ArgumentParser(
description="AI 图片/视频生成器",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"图片模型:{', '.join(image_models)}\n视频模型:{', '.join(video_models)}"
)
parser.add_argument("prompt", nargs="?", default=None,
help="生成提示词(使用 --list-models 时可省略)")
parser.add_argument("--list-models", action="store_true",
help="列出当前服务端激活的可用模型 (hiddenState=0) 后退出")
parser.add_argument("--model", default=None,
choices=all_models,
help="生成模型。未指定时根据 prompt 自动推断:视频关键词 → V3.1FB,其余 → 3.1Nano2-Evo")
# 图片专属参数
parser.add_argument("--quality", default="2K", help="[图片] 图片质量 (默认:2K)")
parser.add_argument("--size", default=None, help="[图片] 图片尺寸,不传则使用模型默认值")
parser.add_argument("--download", action="store_true", help="[图片] 下载图片到本地")
parser.add_argument("--output-dir", help="[图片] 图片保存目录")
parser.add_argument("--markdown-output", action="store_true", help="以 Markdown 格式输出图片链接")
parser.add_argument("--reference-image", default=None, help="[图片] 参考图本地路径,自动上传后作为 image-to-image 参考")
parser.add_argument("--reference-image-url", default=None, help="[图片] 已上传的参考图 URL")
parser.add_argument("--web-search", dest="web_search", action="store_true", default=None,
help="[图片] 启用联网搜索 (仅 S5.0L / 3.1Nano2-Evo)")
parser.add_argument("--no-web-search", dest="web_search", action="store_false",
help="[图片] 关闭联网搜索")
# 视频专属参数
parser.add_argument("--ratio", default=None, help="[视频] 画面比例,如 16:9、9:16、1:1 (默认:16:9)")
parser.add_argument("--resolution", default=None, help="[视频] 分辨率,如 720p、1080p (默认:720p)")
parser.add_argument("--duration", type=int, default=None, help="[视频] 视频时长 (秒) (默认:10)")
# 视频通用参数(首尾帧 / 音频 / 生成模式)
parser.add_argument("--first-image-url", default=None, help="[视频] 首帧图片 URL(FIRST&LAST 模式)")
parser.add_argument("--last-image-url", default=None, help="[视频] 尾帧图片 URL(FIRST&LAST 模式)")
parser.add_argument("--first-image", default=None, help="[视频] 首帧图片本地路径,自动上传")
parser.add_argument("--last-image", default=None, help="[视频] 尾帧图片本地路径,自动上传")
parser.add_argument("--generate-audio", action="store_true", default=None, help="[视频] 生成音频")
parser.add_argument("--no-audio", action="store_true", help="[视频] 不生成音频")
parser.add_argument("--scale-factor", type=float, default=None, help="[视频] 可选 scaleFactor 覆盖值")
parser.add_argument("--generation-type", default=None, help="[视频] 生成类型,如 FIRST&LAST、TEXT、REFERENCE、CONTINUATION、EDIT、FEATURE")
parser.add_argument("--negative-prompt", default=None, help="[视频] 反向提示词 (V3.1Fast/Wan系列)")
parser.add_argument("--enhance-prompt", action="store_true", default=None, help="[视频] 翻译成英文 (V3.1 系列)")
parser.add_argument("--prompt-extend", action="store_true", default=None, help="[视频] 智能改写 (Wan 系列)")
parser.add_argument("--shot-type", default=None, help="[视频] 镜头模式:single/multi/customize (Wan2.6/klingV3Omni)")
parser.add_argument("--mode", default=None, help="[视频] 生成模式:std/pro (仅 klingV3Omni)")
parser.add_argument("--keep-original-sound", default=None, help="[视频] yes/no (仅 klingV3Omni)")
parser.add_argument("--multi-shot", action="store_true", default=None, help="[视频] 多镜头模式 (仅 klingV3Omni)")
parser.add_argument("--n", type=int, default=None, help="[视频] 生成数量 1-4 (仅 V3.1Fast)")
parser.add_argument("--person-generation", default=None, help="[视频] allow_adult/dont_allow (仅 V3.1Fast)")
parser.add_argument("--resize-mode", default=None, help="[视频] pad/crop (仅 V3.1Fast)")
parser.add_argument("--duration-switch", default=None, help="[视频] 1=手选秒数, 2=智能时长 (仅 S1.5Pro)")
# 通用参数
parser.add_argument("--interval", type=int, default=5, help="轮询间隔秒数")
parser.add_argument("--max-wait", type=int, default=1200, help="任务轮询最长等待秒数 (默认 1200)")
parser.add_argument("--dry-run", action="store_true",
help="仅构建并打印最终 payload,不实际调用 API(用于调试)")
parser.add_argument("--json-output", action="store_true",
help="以单行 JSON 向 stdout 输出最终结果 {status,url,message},便于外部编排解析")
args = parser.parse_args()
# --list-models short-circuit
if args.list_models:
print_active_models()
sys.exit(0)
if not args.prompt:
parser.error("prompt 为必填参数(查看可用模型请加 --list-models)")
# Toggle dry-run globally so all downstream task creators honor it
if args.dry_run:
DRY_RUN = True
# Auto-select default model when the user did NOT pass --model explicitly
if args.model is None:
inferred = _infer_media_type(args.prompt)
args.model = "V3.1FB" if inferred == "video" else "3.1Nano2-Evo"
print(f"[auto] 根据提示词推断媒介 → {inferred},使用默认模型 {args.model}",
file=sys.stderr)
media_type = MODEL_CONFIGS[args.model]["media_type"]
if media_type == "video":
# Resolve audio flag
gen_audio = None
if args.no_audio:
gen_audio = False
elif args.generate_audio:
gen_audio = True
result = generate_video(
prompt=args.prompt,
model=args.model,
ratio=args.ratio,
resolution=args.resolution,
duration=args.duration,
poll_interval=args.interval,
first_image_url=args.first_image_url,
last_image_url=args.last_image_url,
first_image_path=args.first_image,
last_image_path=args.last_image,
generate_audio=gen_audio,
scale_factor=args.scale_factor,
generation_type=args.generation_type,
negative_prompt=args.negative_prompt,
enhance_prompt=args.enhance_prompt,
prompt_extend=args.prompt_extend,
shot_type=args.shot_type,
mode=args.mode,
keep_original_sound=args.keep_original_sound,
multi_shot=args.multi_shot,
n=args.n,
person_generation=args.person_generation,
resize_mode=args.resize_mode,
duration_switch=args.duration_switch,
max_wait=args.max_wait,
)
# Send result to Feishu if webhook is configured
if FEISHU_WEBHOOK_URL:
send_feishu_message(args.prompt, result, media_type="video")
_emit_cli_result(result, args, markdown_label=args.prompt)
sys.exit(0 if (result and result.get("status") == "SUCCESS") else 1)
else:
result = generate_image(
prompt=args.prompt,
quality=args.quality,
size=args.size,
poll_interval=args.interval,
download=args.download,
output_dir=args.output_dir,
model=args.model,
reference_image_path=args.reference_image,
reference_image_url=args.reference_image_url,
web_search=args.web_search,
max_wait=args.max_wait
)
# Send result to Feishu if webhook is configured
if FEISHU_WEBHOOK_URL:
send_feishu_message(args.prompt, result, media_type="image")
_emit_cli_result(result, args, markdown_label=args.prompt)
sys.exit(0 if (result and result.get("status") == "SUCCESS") else 1)
FILE:references/api.md
# AI Artist API 详细文档
## API 端点
### 1. 预估生成费用
**POST** `/ai/estimate/cost`
**请求头:**
```
Content-Type: application/json
X-Api-Key: <api_key>
```
**请求体:**
```json
{
"type": "10",
"methodType": "4",
"parameter": "{...}"
}
```
说明:请求体与创建生成任务时使用的参数完全一致,需要在正式创建任务前先调用本接口。
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"estimatedCost": 3.500000,
"sufficientBalance": true
}
}
```
当 `sufficientBalance` 为 `false` 时,表示余额不足,不应继续提交创建任务,需要提醒用户先充值 K 币。
### 2. 创建生成任务
**POST** `/ai/AiArtistRecord`
**请求头:**
```
Content-Type: application/json
X-Api-Key: <api_key>
```
**请求体:**
```json
{
"type": "10",
"methodType": "4",
"parameter": "{...}"
}
```
**支持的图片模型(`type="10"`):**
| 模型 Key | sourceName | methodType | 默认尺寸 | 说明 |
|---------|-----------|-----------|---------|------|
| `S5.0L` | DeepSop·S5.0L | `"4"` | `2048x2048` | 默认模型,生成快、风格全、支持联网 |
| `N2` | DeepSop·N2 | `"2"` | `1:1` | 多模态输入,卓越文字渲染与角色一致性 |
| `W2.7` | DeepSop.W2.7 | `"6"` | `2048x2048` | 文生图/图生图多模态输入 |
| `W2.7Pro` | DeepSop.W2.7Pro | `"7"` | `2048x2048` | 精准控图与风格迁移 |
| `3.1Nano2-Evo` | DeepSop·3.1Nano2-Evo | `"8"` | `1:1` | N2 Evo 版 |
| `Nano2-Beta-Evo` | DeepSop·Nano2 Beta-Evo | `"9"` | `1:1` | N2 Beta Evo 版 |
**支持的视频模型(`type="9"`):** `S1.5Pro`(2)、`V3.1FB`(3)、`V3.1PB`(4)、`V3.1Fast`(5)、`W2.6t`(7)、`W2.6i`(8)、`W2.6r`(9)、`klingV3Omni`(10)、`W2.7i`(14)、`W2.7t`(15)、`W2.7r`(16)。
> 模型列表来源:`POST /ai/consumeSource/list?pageNum=1&pageSize=999`,Body:`{"sourceTypeList":["IMAGE_MODEL"|"VIDEO_MODEL"],"hiddenState":"0"}`;`hiddenState=1` 表示已停用。
**parameter 字段说明(图片):**
| 字段 | 类型 | 说明 |
|------|------|------|
| `methodType` | string | API sourceValue,对应表中的 methodType |
| `prompt` | string | 图片生成提示词 |
| `image` | array | 参考图片(可选) |
| `quality` | string | 图片质量: "2K" / "4K" |
| `size` | string | 尺寸格式因模型而异:`S5.0L`/`W2.7`/`W2.7Pro` 用 "2048x2048",`N2`/`3.1Nano2-Evo`/`Nano2-Beta-Evo` 用 "1:1" |
| `webSearch` | boolean | 是否启用网络搜索 |
| `targetMaxSize` | number | 目标最大尺寸 |
| `targetMaxLength` | number | 目标最大长度 |
| `duration` | number | 持续时间(仅 `S5.0L`)|
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": ["<task_id>"]
}
```
**失败响应:**
```json
{
"msg": "错误信息",
"code": 400,
"data": null
}
```
### 3. 查询任务状态
**GET** `/ai/AiArtistImage/getInfoByArtistId/{artistId}`
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"message": "生成成功",
"url": "https://...",
"status": "SUCCESS"
}
}
```
**状态值说明:**
| 状态 | 含义 |
|------|------|
| `PENDING` | 等待中 |
| `RUNNING` / `GENERATING` | 生成中 |
| `SUCCESS` | 生成成功 |
| `FAILED` | 生成失败 |
## 错误码
| Code | 含义 |
|------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权(token无效) |
| 429 | 请求过于频繁 |
| 500 | 服务器内部错误 |
## 完整请求示例
```bash
# 使用 S5.0L(DeepSop·S5.0L)模型创建图片任务
curl -X POST "https://ai.deepsop.com/prod-api/ai/AiArtistRecord" \
-H "Content-Type: application/json" \
-H "X-Api-Key: <api_key>" \
-d '{
"type": "10",
"methodType": "4",
"parameter": "{\"methodType\":\"4\",\"prompt\":\"风景画\",\"image\":[],\"quality\":\"2K\",\"size\":\"2048x2048\",\"webSearch\":false,\"targetMaxSize\":10,\"targetMaxLength\":6000,\"duration\":10}"
}'
# 使用 N2(DeepSop·N2)模型创建图片任务
curl -X POST "https://ai.deepsop.com/prod-api/ai/AiArtistRecord" \
-H "Content-Type: application/json" \
-H "X-Api-Key: <api_key>" \
-d '{
"type": "10",
"methodType": "2",
"parameter": "{\"methodType\":\"2\",\"prompt\":\"生成一只狗\",\"image\":[],\"quality\":\"2K\",\"size\":\"1:1\",\"webSearch\":false,\"targetMaxSize\":10,\"targetMaxLength\":6000}"
}'
# 查询状态
curl -X GET "https://ai.deepsop.com/prod-api/ai/AiArtistImage/getInfoByArtistId/<task_id>" \
-H "X-Api-Key: <api_key>"
```
FILE:references/chat-integration.md
# 在对话中直接返回图片的示例
## 示例 1: 使用 Markdown(最简单)
当用户请求生成图片时,直接返回:
```python
from scripts.generate_image import generate_image
result = generate_image(prompt="风景画")
if result and result["status"] == "SUCCESS":
# 直接在回复中使用 Markdown 图片语法
reply = f"生成成功!\n\n"
```
## 示例 2: 使用 message 工具发送图片
如果需要使用 message 工具发送图片(支持更多平台):
```python
from scripts.generate_image import generate_image
import base64
# 1. 生成并下载图片
result = generate_image(prompt="风景画", download=True)
if result and result["status"] == "SUCCESS":
# 2. 读取图片数据
with open(result["local_path"], "rb") as f:
image_bytes = f.read()
# 3. 转换为 base64
base64_image = base64.b64encode(image_bytes).decode('utf-8')
# 4. 使用 message 工具发送
# 注意:实际使用时通过 message 工具的 buffer 参数发送
```
## 示例 3: 直接在回复中包含图片
对于支持 Markdown 图片的平台(如 Discord、Telegram、WebChat):
```
用户: 生成一张风景画
助手: 正在生成...
[生成完成后]
助手: 生成成功!🎨

```
## 平台兼容性
| 平台 | Markdown 图片 | 说明 |
|------|--------------|------|
| WebChat | ✅ 支持 | 直接显示 |
| Discord | ✅ 支持 | 直接显示 |
| Telegram | ✅ 支持 | 直接显示 |
| 飞书 | ⚠️ 部分支持 | 建议使用卡片消息 |
| WhatsApp | ❌ 不支持 | 需要下载后发送 |
## 最佳实践
1. **优先使用 Markdown**: 最简单,大多数平台支持
2. **同时提供链接**: 以防图片加载失败
3. **下载选项**: 对于不支持 Markdown 的平台,使用 `--download` 参数
FILE:references/feishu-integration.md
# 飞书图片发送指南
## 飞书支持的图片发送方式
### 方式 1: 使用 message 工具发送图片(推荐)
通过 `message` 工具的 `buffer` 参数直接发送图片:
```python
import base64
# 读取图片文件
with open("image.png", "rb") as f:
image_data = f.read()
# 使用 message 工具发送
# message(
# action="send",
# buffer=base64.b64encode(image_data).decode(),
# filename="image.png",
# mimeType="image/png"
# )
```
### 方式 2: 使用飞书卡片消息(富文本)
飞书支持发送带有图片的交互式卡片:
```python
{
"msg_type": "interactive",
"card": {
"header": {
"title": {
"tag": "plain_text",
"content": "✅ 图片生成成功"
},
"template": "green"
},
"elements": [
{
"tag": "img",
"img_key": "", # 需要先在飞书上传图片获取 img_key
"alt": {
"tag": "plain_text",
"content": "生成的图片"
}
},
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**提示词**: 风景画"
}
}
]
}
}
```
**注意**:使用 `img_key` 需要先将图片上传到飞书平台。
### 方式 3: 使用 Markdown 图片链接
飞书支持 Markdown 图片语法,但图片需要是公开可访问的 URL:
```markdown

```
**限制**:
- 图片 URL 必须是公网可访问的
- 不支持 base64 内嵌图片
## 最佳实践
### 对于 AI 图片生成场景
1. **生成图片后获取公开 URL**(如阿里云 OSS、AWS S3 等)
2. **使用 Markdown 语法发送**:
```markdown
生成成功!🎨

图片链接:https://your-cdn.com/image.png
```
3. **或者使用 message 工具直接发送图片数据**
### 代码示例
```python
from scripts.generate_image import generate_image
# 生成图片
result = generate_image(prompt="风景画")
if result and result["status"] == "SUCCESS":
# 方式 1: 使用 Markdown 发送图片 URL
reply = f"""生成成功!🎨

图片链接:{result['url']}"""
# 方式 2: 下载后使用 message 工具发送
# result = generate_image(prompt="风景画", download=True)
# with open(result["local_path"], "rb") as f:
# image_data = f.read()
# message(action="send", buffer=base64.b64encode(image_data).decode(), ...)
```
## 平台对比
| 平台 | Markdown 图片 | Base64 图片 | 卡片消息 | 说明 |
|------|--------------|-------------|----------|------|
| WebChat | ✅ 支持 | ✅ 支持 | ❌ 不支持 | 最灵活 |
| Discord | ✅ 支持 | ⚠️ 有限 | ✅ 支持 | 支持 embed |
| Telegram | ✅ 支持 | ✅ 支持 | ✅ 支持 | 支持多种方式 |
| **飞书** | ✅ 支持 | ❌ 不支持 | ✅ 支持 | 需公开 URL 或上传 |
| WhatsApp | ❌ 不支持 | ✅ 支持 | ❌ 不支持 | 需发送文件 |
## 飞书特殊说明
1. **Base64 图片不支持**:飞书不支持直接通过 base64 内嵌图片
2. **需要先上传**:使用卡片消息的 `img_key` 需要先将图片上传到飞书
3. **公开 URL 最方便**:使用 CDN 或对象存储的公开链接最简单
4. **Webhook 发送**:通过 Webhook 发送时,图片需要是公开可访问的
## 推荐的飞书集成方案
```python
# 方案 1: 使用公开 URL(最简单)
def send_image_to_feishu(image_url, prompt):
content = f"""✅ 图片生成成功
**提示词**: {prompt}

[打开图片]({image_url})"""
# 使用 message 工具发送 Markdown
# message(action="send", message=content)
# 方案 2: 下载后作为文件发送
def send_image_file(image_path):
with open(image_path, "rb") as f:
image_data = f.read()
# 使用 message 工具发送文件
# message(
# action="send",
# buffer=base64.b64encode(image_data).decode(),
# filename="generated_image.png",
# mimeType="image/png"
# )
```
声音复刻技能,使用 AI Artist API 进行音色克隆和语音合成。支持查询已有音色、上传音频创建新音色、使用指定音色合成语音。 ⚠️ 使用前必须设置环境变量 AI_ARTIST_TOKEN 为你的 API Key! 获取 API Key:访问 https://ai.deepsop.com/ 注册登录后创建。...
---
name: voice-clone
description: |
声音复刻技能,使用 AI Artist API 进行音色克隆和语音合成。支持查询已有音色、上传音频创建新音色、使用指定音色合成语音。
⚠️ 使用前必须设置环境变量 AI_ARTIST_TOKEN 为你的 API Key!
获取 API Key:访问 https://ai.deepsop.com/ 注册登录后创建。
触发场景:
- 用户要求生成语音,如"用蔡总的音色说..."、"生成一段语音"、"语音合成"等。
- 用户要求克隆音色,如"上传音频创建音色"、"复刻这个声音"、"创建我的音色"等。
- 用户查询已有音色,如"有哪些音色"、"列出音色"、"查看音色列表"等。
- 用户指定音色名称或 ID 进行语音合成。
- 用户发送语音消息后要求用该声音合成其他内容。
---
# Voice Clone - 声音复刻技能
使用 AI Artist API 进行音色克隆和语音合成的完整解决方案。基于 CosyVoice v3.5 Plus 模型,支持高质量的音色复刻和文本转语音。
## 🎯 技能概述
本技能提供三大核心功能:
| 功能 | 说明 | 典型场景 |
|------|------|----------|
| **查询音色** | 列出系统中所有可用音色 | 查看已有音色库,选择合适的声音 |
| **音色克隆** | 上传音频创建新的音色 | 复刻自己的声音、领导的声音、明星声音等 |
| **语音合成** | 使用指定音色生成语音 | 用特定声音朗读文本、生成配音、制作语音消息 |
## ⚠️ 首次使用必读
### 1. 获取 API Key
访问 [https://ai.deepsop.com/](https://ai.deepsop.com/) 注册并登录,然后在控制台创建你的 API Key。
### 2. 设置环境变量
**在使用前,你必须先设置自己的 API Key:**
```bash
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
# Linux/macOS/Git Bash (Windows)
export AI_ARTIST_TOKEN="sk-your_api_key_here"
```
### 3. 验证配置
```bash
python scripts/voice_clone.py --list
```
如果看到音色列表,说明配置成功!
## 🚀 快速开始
### 基础用法
```bash
# 1. 列出所有可用音色
python scripts/voice_clone.py --list
# 2. 使用音色 ID 合成语音
python scripts/voice_clone.py --synthesize --id 10 --text "大家好,我是测试语音"
# 3. 使用音色名称合成语音
python scripts/voice_clone.py --synthesize --name "蔡总的音色" --text "你好世界"
# 4. 下载合成的音频到本地
python scripts/voice_clone.py --synthesize --id 10 --text "你好" --download
```
### 创建新音色
```bash
# 使用本地音频文件创建音色
python scripts/voice_clone.py --create --name "我的音色" --audio "./my_voice.mp3"
# 使用在线音频 URL 创建音色
python scripts/voice_clone.py --create --name "我的音色" --audio-url "https://example.com/voice.mp3"
# 指定音色前缀
python scripts/voice_clone.py --create --name "客服音色" --audio "./cs.mp3" --prefix "CustomerService"
```
## 📋 详细使用指南
### 一、查询可用音色
列出系统中所有音色及其状态:
```bash
python scripts/voice_clone.py --list
```
**输出示例:**
```
[INFO] 共有 4 个音色
可用音色列表:
[13] 王俏的音色 [OK] - cosyvoice-v3.5-plus
[12] 测试 11 [OK] - cosyvoice-v3.5-plus
[10] 蔡总的音色 [OK] - cosyvoice-v3.5-plus
[4] 测试音色 [OK] - cosyvoice-v3.5-plus
```
**状态说明:**
| 状态 | 说明 | 是否可用 |
|------|------|----------|
| `OK` | 音色已就绪 | ✅ 可用 |
| `DEPLOYING` | 音色部署中 | ❌ 暂不可用 |
| 其他 | 音色异常 | ❌ 不可用 |
### 二、语音合成
#### 方式 1:使用音色 ID
```bash
python scripts/voice_clone.py --synthesize --id 13 --text "真正重要的东西,用眼睛是看不见的,只有用心才能看清。"
```
#### 方式 2:使用音色名称
```bash
python scripts/voice_clone.py --synthesize --name "王俏的音色" --text "你好,欢迎使用库阔 AI"
```
#### 方式 3:合成并下载
```bash
# 下载到默认目录 (~/.openclaw/workspace/audio/)
python scripts/voice_clone.py --synthesize --id 13 --text "测试语音" --download
# 下载到指定目录
python scripts/voice_clone.py --synthesize --id 13 --text "测试语音" --download --output-dir "./my_audio"
```
### 三、创建新音色
#### 从本地音频文件创建
```bash
# 支持 MP3、WAV 等常见格式
python scripts/voice_clone.py --create --name "我的声音" --audio "./my_voice.mp3"
# 使用完整路径
python scripts/voice_clone.py --create --name "领导音色" --audio "C:\Users\admin\Downloads\leader_voice.wav"
```
#### 从在线 URL 创建
```bash
python scripts/voice_clone.py --create --name "网络音色" --audio-url "https://example.com/voice.mp3"
```
#### 指定音色前缀
```bash
python scripts/voice_clone.py --create --name "客服小王" --audio "./wang.mp3" --prefix "CustomerService"
```
## 🎙️ 音色克隆最佳实践
### 音频素材要求
| 要求 | 说明 |
|------|------|
| **格式** | MP3、WAV、M4A 等常见音频格式 |
| **时长** | 10-60 秒(推荐 30 秒左右) |
| **音质** | 清晰的人声,无明显背景噪音 |
| **内容** | 纯人声朗读,无背景音乐 |
| **采样率** | 16kHz 或以上 |
### 录制建议
1. **环境安静** - 选择安静的房间,关闭空调、风扇等噪音源
2. **距离适中** - 麦克风距离嘴巴 10-15 厘米
3. **语速均匀** - 用正常语速朗读,不要过快或过慢
4. **情感自然** - 用自然的情感朗读,不要过于夸张
5. **内容多样** - 包含不同的音调、韵律,有助于模型学习
### 推荐的录音文本
```
你好,我是 XXX。这是一段用于音色克隆的录音样本。
我希望用我的声音来生成各种语音内容,包括问候语、通知、
故事朗读等。请确保录音清晰,语速适中,情感自然。
谢谢你的配合。
```
## 📊 参数说明
### 全局参数
| 参数 | 必填 | 说明 |
|------|------|------|
| `--list` | 三选一 | 列出所有可用音色 |
| `--synthesize` | 三选一 | 语音合成模式 |
| `--create` | 三选一 | 创建新音色模式 |
### 合成模式参数
| 参数 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `--id` | 与 --name 二选一 | 音色 ID | `--id 13` |
| `--name` | 与 --id 二选一 | 音色名称 | `--name "王俏的音色"` |
| `--text` | ✅ | 要合成的文本 | `--text "你好世界"` |
| `--download` | 否 | 下载音频到本地 | `--download` |
| `--output-dir` | 否 | 音频保存目录 | `--output-dir "./audio"` |
### 创建音色参数
| 参数 | 必填 | 说明 | 示例 |
|------|------|------|------|
| `--name` | ✅ | 音色名称 | `--name "我的音色"` |
| `--audio` | 与 --audio-url 二选一 | 本地音频路径 | `--audio "./voice.mp3"` |
| `--audio-url` | 与 --audio 二选一 | 在线音频 URL | `--audio-url "https://..."` |
| `--prefix` | 否 | 音色前缀 | `--prefix "DeepSop"` |
## 🔧 环境配置
### 方式 1:临时设置(当前终端有效)
```bash
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-5c6c262755dc43d59ec5a742a7e80202"
# Linux/macOS
export AI_ARTIST_TOKEN="sk-5c6c262755dc43d59ec5a742a7e80202"
```
### 方式 2:永久设置(推荐)
创建 `.env` 文件(在脚本同目录或技能根目录):
```bash
AI_ARTIST_TOKEN=sk-your_api_key_here
```
### 方式 3:系统环境变量
**Windows:**
```powershell
[System.Environment]::SetEnvironmentVariable('AI_ARTIST_TOKEN', 'sk-your_api_key_here', 'User')
```
**Linux/macOS:**
```bash
echo 'export AI_ARTIST_TOKEN="sk-your_api_key_here"' >> ~/.bashrc
source ~/.bashrc
```
## 💡 实用场景示例
### 场景 1:用特定音色发送语音消息
```bash
# 用蔡总的音色发送通知
python scripts/voice_clone.py --synthesize --name "蔡总的音色" \
--text "各位同事,下午三点在会议室召开周会,请准时参加。" --download
```
### 场景 2:批量生成语音
```bash
# 生成多个语音片段
python scripts/voice_clone.py --synthesize --id 13 --text "第一章:开始" --download --output-dir "./audiobook/ch1"
python scripts/voice_clone.py --synthesize --id 13 --text "第二章:发展" --download --output-dir "./audiobook/ch2"
python scripts/voice_clone.py --synthesize --id 13 --text "第三章:高潮" --download --output-dir "./audiobook/ch3"
```
### 场景 3:创建多人音色库
```bash
# 为团队创建音色库
python scripts/voice_clone.py --create --name "客服小王" --audio "./wang.mp3"
python scripts/voice_clone.py --create --name "客服小李" --audio "./li.mp3"
python scripts/voice_clone.py --create --name "客服小张" --audio "./zhang.mp3"
# 查看音色列表
python scripts/voice_clone.py --list
```
### 场景 4:语音消息回复
```bash
# 收到语音后,用相同音色回复
# 1. 从语音消息提取音频
# 2. 创建音色(如果不存在)
python scripts/voice_clone.py --create --name "用户音色" --audio "./user_voice.wav"
# 3. 用该音色合成回复
python scripts/voice_clone.py --synthesize --name "用户音色" --text "收到,我会尽快处理。" --download
```
## ⚠️ 注意事项
### 必须遵守
1. **API Key 安全**
- 不要将 API Key 提交到代码仓库
- 使用 `.env` 文件时加入 `.gitignore`
- 定期更换 API Key
2. **音色状态检查**
- 只有 `status: "OK"` 的音色可用于语音合成
- `DEPLOYING` 状态的音色需要等待部署完成
3. **音频格式要求**
- 上传的音频建议为 MP3 或 WAV 格式
- 时长 10-60 秒效果最佳
- 确保音频清晰,无明显噪音
4. **文本长度限制**
- 合成文本建议控制在 500 字以内
- 过长文本可能失败或效果不佳
### 性能优化
| 优化项 | 建议 |
|--------|------|
| 音频素材 | 使用 30 秒左右的清晰录音 |
| 文本长度 | 单次合成不超过 200 字 |
| 并发请求 | 避免同时发起多个合成请求 |
| 错误处理 | 检查返回状态码,失败时重试 |
## 🔍 故障排查
### 问题 1:提示 "未配置 API_ARTIST_TOKEN"
**原因:** 环境变量未设置
**解决:**
```bash
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
# 或创建 .env 文件
echo "AI_ARTIST_TOKEN=sk-your_api_key_here" > .env
```
### 问题 2:音色状态为 DEPLOYING
**原因:** 音色正在部署中
**解决:** 等待几分钟后重新查询状态
```bash
python scripts/voice_clone.py --list
```
### 问题 3:语音合成失败
**可能原因:**
- 音色状态不是 OK
- 文本过长
- 网络问题
**解决:**
1. 检查音色状态:`python scripts/voice_clone.py --list`
2. 缩短文本长度
3. 检查网络连接
### 问题 4:文件上传失败
**可能原因:**
- 文件路径不正确
- 文件格式不支持
- 文件过大
**解决:**
1. 确认文件路径正确(使用绝对路径)
2. 转换为 MP3 或 WAV 格式
3. 确保文件大小合理(< 10MB)
## 📁 相关文件
| 文件 | 说明 |
|------|------|
| `scripts/voice_clone.py` | 主脚本,包含所有功能实现 |
| `references/api.md` | API 详细文档,包含接口说明 |
| `.env` | 环境配置文件(需自行创建) |
## 📚 API 接口速查
| 接口 | 方法 | 说明 |
|------|------|------|
| `/ai/voice/clone/list` | GET | 查询音色列表 |
| `/ai/voice/clone/sync/create` | POST | 创建新音色 |
| `/ai/voice/clone/synthesize` | POST | 语音合成 |
| `/system/fileUpload/upload` | POST | 文件上传 |
详细 API 文档请查看 `references/api.md`
## 🎯 后续扩展
本技能支持以下扩展场景:
- **批量合成** - 循环调用合成接口生成多个语音文件
- **音色管理** - 添加删除、重命名音色的功能
- **音频处理** - 集成音频剪辑、合并功能
- **Web 界面** - 构建图形化操作界面
- **API 服务** - 封装为 REST API 供其他系统调用
---
_如有问题或建议,请联系技能维护者。_
FILE:README.md
# Voice Clone - 声音复刻技能
AI Artist API 驱动的声音克隆与语音合成工具。
## 🚀 快速开始
### 1. 获取 API Key
访问 [https://ai.deepsop.com/](https://ai.deepsop.com/) 注册并登录,然后在控制台创建你的 API Key。
### 2. 设置 API Key
```bash
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
# Linux/macOS
export AI_ARTIST_TOKEN="sk-your_api_key_here"
```
### 3. 验证配置
```bash
python scripts/voice_clone.py --list
```
### 4. 使用示例
```bash
# 列出所有可用音色
python scripts/voice_clone.py --list
# 使用音色合成语音
python scripts/voice_clone.py --synthesize --id 10 --text "你好,这是测试语音"
# 使用音色名称合成
python scripts/voice_clone.py --synthesize --name "蔡总的音色" --text "你好世界"
# 创建新音色
python scripts/voice_clone.py --create --name "我的音色" --audio "./my_voice.mp3"
```
## 📖 完整文档
详细使用说明请查看 [SKILL.md](SKILL.md)
## 🎯 功能特性
- **查询音色** - 列出系统中所有可用音色
- **语音合成** - 使用指定音色生成语音
- **音色克隆** - 上传音频创建新的音色
- **自动上传** - 本地音频自动上传到 OSS 获取 URL
## 🔧 环境要求
- Python 3.6+
- requests 库
## 📄 许可证
请遵守 AI Artist API 的使用条款。
FILE:scripts/voice_clone.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Voice Clone - AI 声音复刻与语音合成工具
调用 AI Artist API 进行音色克隆和语音合成
"""
import os
import sys
import json
import time
import argparse
import requests
from pathlib import Path
# API 配置
BASE_URL = "https://ai.deepsop.com/prod-api"
FILE_UPLOAD_URL = f"{BASE_URL}/system/fileUpload/upload"
# 环境配置
API_KEY_ENV = "AI_ARTIST_TOKEN"
def get_api_key():
"""获取 API Key"""
api_key = os.environ.get(API_KEY_ENV)
if not api_key:
# Try loading from .env file (in script directory)
env_file = Path(__file__).parent / ".env"
if env_file.exists():
with open(env_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and line.startswith(f"{API_KEY_ENV}="):
api_key = line.split("=", 1)[1].strip('"\'')
break
# Also check parent directory (skill root)
if not api_key:
env_file = Path(__file__).parent.parent / ".env"
if env_file.exists():
with open(env_file, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if line and not line.startswith("#") and line.startswith(f"{API_KEY_ENV}="):
api_key = line.split("=", 1)[1].strip('"\'')
break
return api_key
def check_api_key():
"""检查 API Key 是否配置"""
api_key = get_api_key()
if not api_key:
print(f"[ERROR] 未配置 {API_KEY_ENV} 环境变量", file=sys.stderr)
print(f"\n请设置 API Key:")
print(f" Windows PowerShell: $env:{API_KEY_ENV}=\"sk-your_api_key_here\"")
print(f" Linux/macOS: export {API_KEY_ENV}=\"sk-your_api_key_here\"")
return None
return api_key
def get_headers():
"""获取请求头"""
api_key = get_api_key()
return {
"x-api-key": api_key,
"Content-Type": "application/json"
}
def upload_file(file_path):
"""上传本地文件到 OSS 获取 URL"""
if not os.path.exists(file_path):
print(f"[ERROR] 文件不存在:{file_path}", file=sys.stderr)
return None
try:
with open(file_path, "rb") as f:
files = {"file": (os.path.basename(file_path), f)}
response = requests.post(
FILE_UPLOAD_URL,
headers={"x-api-key": get_api_key()},
files=files,
timeout=60
)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
url = result.get("url")
print(f"[SUCCESS] 文件已上传:{file_path} -> {url}")
return url
else:
print(f"[ERROR] 文件上传失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
print(f"[ERROR] 文件上传错误:{e}", file=sys.stderr)
return None
def list_voices():
"""查询音色列表"""
url = f"{BASE_URL}/ai/voice/clone/list"
params = {"pageNum": 1, "pageSize": 10}
try:
response = requests.get(url, headers=get_headers(), params=params, timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
print(f"[ERROR] 查询失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
rows = result.get("rows", [])
total = result.get("total", 0)
print(f"[INFO] 共有 {total} 个音色")
print("\n可用音色列表:")
available = []
for voice in rows:
vid = voice.get("id")
name = voice.get("name")
status = voice.get("status")
model = voice.get("targetModel", "unknown")
status_mark = "[OK]" if status == "OK" else f"[{status}]"
print(f" [{vid}] {name} {status_mark} - {model}")
if status == "OK":
available.append(voice)
return available
except requests.exceptions.RequestException as e:
print(f"[ERROR] 查询音色列表出错:{e}", file=sys.stderr)
return None
def create_voice(name, audio_url=None, audio_path=None, prefix="DeepSop", remark=None):
"""创建新音色"""
# 如果提供本地文件路径,先上传获取 URL
if audio_path and not audio_url:
audio_url = upload_file(audio_path)
if not audio_url:
return None
if not audio_url:
print("[ERROR] 必须提供 audio_url 或 audio_path", file=sys.stderr)
return None
payload = {
"name": name,
"prefix": prefix,
"audioUrl": audio_url,
"remark": remark
}
url = f"{BASE_URL}/ai/voice/clone/sync/create"
try:
response = requests.post(url, headers=get_headers(), json=payload, timeout=60)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
print(f"[ERROR] 创建失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
data = result.get("data", {})
vid = data.get("id")
voice_id = data.get("voiceId")
status = data.get("status")
print(f"[SUCCESS] 音色创建成功!")
print(f" ID: {vid}")
print(f" VoiceID: {voice_id}")
print(f" 名称:{name}")
print(f" 状态:{status}")
return data
except requests.exceptions.RequestException as e:
print(f"[ERROR] 创建音色出错:{e}", file=sys.stderr)
return None
def get_balance():
"""查询 K 币余额"""
url = f"{BASE_URL}/ai/vip/balance"
try:
response = requests.get(url, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
print(f"[ERROR] 查询余额失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
balance = result.get("data")
if balance is None:
print("[ERROR] 查询余额失败:返回数据缺少余额信息", file=sys.stderr)
return None
print(f"[INFO] 当前 K 币余额:{balance}")
return float(balance)
except (ValueError, TypeError):
print("[ERROR] 查询余额失败:余额数据格式异常", file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
print(f"[ERROR] 查询余额出错:{e}", file=sys.stderr)
return None
def synthesize_voice(text, voice_id=None, voice_name=None):
"""语音合成"""
# 如果提供名称,先查询获取 ID
if voice_name and not voice_id:
voices = list_voices()
if voices:
for v in voices:
if v.get("name") == voice_name:
if v.get("status") != "OK":
print(f"[ERROR] 音色 '{voice_name}' 状态为 {v.get('status')},不可用", file=sys.stderr)
return None
voice_id = v.get("id")
break
if not voice_id:
print(f"[ERROR] 未找到音色:{voice_name}", file=sys.stderr)
return None
if not voice_id:
print("[ERROR] 必须提供 voice_id 或 voice_name", file=sys.stderr)
return None
balance = get_balance()
if balance is None:
return None
if balance <= 0:
print("[ERROR] K 币余额不足,无法进行语音合成,请先充值 K 币", file=sys.stderr)
return None
payload = {
"text": text,
"id": voice_id
}
url = f"{BASE_URL}/ai/voice/clone/synthesize"
try:
response = requests.post(url, headers=get_headers(), json=payload, timeout=60)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
print(f"[ERROR] 合成失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
audio_url = result.get("msg")
print(f"[SUCCESS] 语音合成成功!")
print(f" 音频链接:{audio_url}")
return {
"status": "SUCCESS",
"url": audio_url
}
except requests.exceptions.RequestException as e:
print(f"[ERROR] 语音合成出错:{e}", file=sys.stderr)
return None
def download_audio(url, output_dir=None):
"""下载音频文件"""
if not output_dir:
output_dir = os.path.join(os.path.expanduser("~"), ".openclaw", "workspace", "audio")
os.makedirs(output_dir, exist_ok=True)
try:
response = requests.get(url, timeout=60)
response.raise_for_status()
# Generate filename from URL
filename = url.split("/")[-1].split("?")[0]
if not filename.endswith(".mp3"):
filename = f"{int(time.time())}.mp3"
output_path = os.path.join(output_dir, filename)
with open(output_path, "wb") as f:
f.write(response.content)
print(f"[SAVE] 音频已保存:{output_path}")
return output_path
except requests.exceptions.RequestException as e:
print(f"[WARNING] 下载音频失败:{e}", file=sys.stderr)
return None
def main():
parser = argparse.ArgumentParser(
description="AI 声音复刻与语音合成工具",
formatter_class=argparse.RawDescriptionHelpFormatter
)
# 模式选择
mode_group = parser.add_mutually_exclusive_group(required=True)
mode_group.add_argument("--list", action="store_true", help="列出所有可用音色")
mode_group.add_argument("--synthesize", action="store_true", help="语音合成模式")
mode_group.add_argument("--create", action="store_true", help="创建新音色模式")
# 通用参数
parser.add_argument("--id", type=int, help="音色 ID")
parser.add_argument("--name", help="音色名称")
parser.add_argument("--text", help="要合成的文本内容")
# 创建音色参数
parser.add_argument("--audio", help="本地音频文件路径(创建音色时使用)")
parser.add_argument("--audio-url", help="在线音频 URL(创建音色时使用)")
parser.add_argument("--prefix", default="DeepSop", help="音色前缀(默认:DeepSop)")
# 下载参数
parser.add_argument("--download", action="store_true", help="下载合成的音频到本地")
parser.add_argument("--output-dir", help="音频保存目录")
args = parser.parse_args()
# 检查 API Key
if not check_api_key():
sys.exit(1)
# 列出音色
if args.list:
voices = list_voices()
sys.exit(0 if voices is not None else 1)
# 语音合成
elif args.synthesize:
if not args.text:
print("[ERROR] 合成模式必须提供 --text 参数", file=sys.stderr)
sys.exit(1)
if not args.id and not args.name:
print("[ERROR] 合成模式必须提供 --id 或 --name 参数", file=sys.stderr)
sys.exit(1)
result = synthesize_voice(args.text, voice_id=args.id, voice_name=args.name)
if result and result.get("status") == "SUCCESS":
if args.download:
download_audio(result["url"], args.output_dir)
print(result["url"])
sys.exit(0)
else:
sys.exit(1)
# 创建音色
elif args.create:
if not args.name:
print("[ERROR] 创建模式必须提供 --name 参数", file=sys.stderr)
sys.exit(1)
if not args.audio and not args.audio_url:
print("[ERROR] 创建模式必须提供 --audio 或 --audio-url 参数", file=sys.stderr)
sys.exit(1)
result = create_voice(
name=args.name,
audio_url=args.audio_url,
audio_path=args.audio,
prefix=args.prefix
)
if result:
sys.exit(0)
else:
sys.exit(1)
if __name__ == "__main__":
main()
FILE:references/api.md
# Voice Clone API 详细文档
## 基础信息
**Base URL:** `https://ai.deepsop.com/prod-api`
**认证方式:** 所有请求需要在 Header 中携带 `x-api-key: <your_api_key>`
**API Key 获取:** 访问 https://ai.deepsop.com/ 注册登录后创建
---
## 1. 查询音色列表
获取当前用户可用的所有音色。
### 请求
```http
GET /ai/voice/clone/list?pageNum=1&pageSize=10
x-api-key: sk-your_api_key_here
```
### 参数
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| pageNum | int | 否 | 页码,默认 1 |
| pageSize | int | 否 | 每页数量,默认 10 |
### 响应示例
```json
{
"total": 3,
"rows": [
{
"id": 10,
"deleted": 0,
"createUser": "1",
"createTime": "2026-03-16 20:17:34",
"updateUser": "1",
"updateTime": "2026-03-16 20:17:40",
"userId": 1,
"deptId": 100,
"name": "蔡总的音色",
"voiceId": "cosyvoice-v3.5-plus-deepsop-2689c5e102004891ac158340547fa44a",
"targetModel": "cosyvoice-v3.5-plus",
"prefix": "DeepSop",
"audioUrl": "https://kocgo-ai-sales-test.oss-cn-hangzhou.aliyuncs.com/timbre/100/1773663443610_c8733ade.mp3",
"status": "OK",
"remark": null
}
],
"code": 200,
"msg": "查询成功"
}
```
### 字段说明
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 音色 ID,用于语音合成 |
| name | string | 音色名称 |
| voiceId | string | 声音唯一标识 |
| targetModel | string | 使用的模型,如 cosyvoice-v3.5-plus |
| prefix | string | 前缀标识 |
| audioUrl | string | 音色样本音频 URL |
| status | string | 状态:OK=可用,DEPLOYING=部署中 |
| createTime | string | 创建时间 |
### 状态码
| 状态 | 说明 |
|------|------|
| OK | 音色可用,可用于语音合成 |
| DEPLOYING | 音色正在部署,暂不可用 |
| 其他 | 音色不可用 |
---
## 2. 创建新音色
上传音频文件创建新的音色克隆。
### 请求
```http
POST /ai/voice/clone/sync/create
x-api-key: sk-your_api_key_here
Content-Type: application/json
```
### 请求体
```json
{
"name": "测试音色",
"prefix": "DeepSop",
"audioUrl": "https://kocgo-ai-sales-test.oss-cn-hangzhou.aliyuncs.com/timbre/100/xxx.mp3",
"remark": null
}
```
### 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| name | string | 是 | 音色名称 |
| prefix | string | 否 | 前缀标识,默认 "DeepSop" |
| audioUrl | string | 是 | 音频文件 OSS URL |
| remark | string | 否 | 备注信息 |
### 响应示例
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"id": 12,
"deleted": 0,
"createUser": "1",
"createTime": "2026-04-02 13:59:44",
"updateUser": "1",
"updateTime": "2026-04-02 13:59:50",
"userId": 1,
"deptId": 100,
"name": "测试 11",
"voiceId": "cosyvoice-v3.5-plus-deepsop-19ce1b6d3dce43ceabae661e6c3ead0e",
"targetModel": "cosyvoice-v3.5-plus",
"prefix": "DeepSop",
"audioUrl": "https://kocgo-ai-sales-test.oss-cn-hangzhou.aliyuncs.com/timbre/100/1775109577087_813ce67c.mp3",
"status": "OK",
"remark": null
}
}
```
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 新创建的音色 ID |
| voiceId | string | 声音唯一标识 |
| status | string | 初始状态,通常为 OK 或 DEPLOYING |
---
## 3. 语音合成
使用指定音色合成语音。
### 请求
```http
POST /ai/voice/clone/synthesize
x-api-key: sk-your_api_key_here
Content-Type: application/json
```
### 请求体
```json
{
"text": "大家好啊,我是库阔 AI 的销售经理王志勇",
"id": 10
}
```
### 参数说明
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| text | string | 是 | 要合成的文本内容 |
| id | int | 是 | 音色 ID(从音色列表获取) |
### 响应示例
```json
{
"msg": "https://kocgo-ai-sales-test.oss-cn-hangzhou.aliyuncs.com/voice_clone/1/3c48a33a-8bdb-4272-81c4-b5607a19928c.mp3",
"code": 200
}
```
### 响应说明
- `msg` 字段包含合成后的音频文件 URL
- 可直接访问该 URL 播放或下载音频
---
## 4. 文件上传
将本地文件上传到 OSS 获取可访问 URL。
### 请求
```http
POST /system/fileUpload/upload
x-api-key: sk-your_api_key_here
Content-Type: multipart/form-data
```
### 请求体 (form-data)
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| file | file | 是 | 要上传的文件 |
### cURL 示例
```bash
curl --location --request POST 'https://ai.deepsop.com/prod-api/system/fileUpload/upload' \
--header 'x-api-key: sk-your_api_key_here' \
--form 'file=@"C:\Users\admin\Downloads\voice.mp3"'
```
### 响应示例
```json
{
"msg": "操作成功",
"fileName": "1773660081867_f4eec03c.mp3",
"code": 200,
"url": "https://kocgo-ai-sales-test.oss-cn-hangzhou.aliyuncs.com/material/100/6f5a70ba-cb60-4474-a579-ef5326037b5c.mp3"
}
```
### 响应字段
| 字段 | 类型 | 说明 |
|------|------|------|
| fileName | string | 上传后的文件名 |
| url | string | OSS 可访问 URL |
---
## 错误码说明
| code | 说明 |
|------|------|
| 200 | 操作成功 |
| 400 | 请求参数错误 |
| 401 | 认证失败(API Key 无效) |
| 403 | 权限不足 |
| 500 | 服务器内部错误 |
---
## 最佳实践
### 1. 音色选择
- 优先选择 `status: "OK"` 的音色
- 避免使用 `DEPLOYING` 状态的音色(可能失败)
### 2. 音频上传
- 推荐 MP3 格式
- 时长建议 10-60 秒
- 清晰的人声录音效果最佳
### 3. 文本合成
- 文本长度控制在 500 字以内
- 支持中文、英文混合
- 标点符号会影响停顿
### 4. 错误处理
- 检查响应 `code` 是否为 200
- 音色创建后可能需要等待部署完成
- 网络超时建议重试机制
人机协作台技能。用户输入自然语言销售指令,AI自动分析拆解任务参数,调用 deepsop 平台接口提交任务,等待后查询结果并推送。触发场景:用户说「帮我找客户」「挖掘XXX行业客户」「找XXX个客户」「提交任务」等与客户挖掘、销售任务相关的指令;「发TikTok视频」「生成视频发布到TikTok」等TikTok视...
---
name: human-ai-collab
description: 人机协作台技能。用户输入自然语言销售指令,AI自动分析拆解任务参数,调用 deepsop 平台接口提交任务,等待后查询结果并推送。触发场景:用户说「帮我找客户」「挖掘XXX行业客户」「找XXX个客户」「提交任务」等与客户挖掘、销售任务相关的指令;「发TikTok视频」「生成视频发布到TikTok」等TikTok视频发布指令;或收到包含 [DeepSOP-AutoQuery] 标记的系统定时事件(cron 回调,用于自动查询并推送任务结果)。需要提前配置环境变量 DEEPSOP_API_KEY。
---
# 人机协作台(Human-AI Collaboration)
## 功能简介
人机协作台是基于 deepsop 平台的智能销售任务助手,能够:
- **理解自然语言指令**:直接描述需求,如「帮我找50个美国做服装的客户」
- **智能任务拆解**:自动识别目标数量、行业、地区、执行周期等参数
- **多员工协作**:根据任务类型自动分配对应职能员工
- **AiWa**:客户挖掘(找客户、行业客户等)
- **Frank**:邮件销售
- **Fran**:电话销售
- **Lisa**:短信销售
- **Toby**:AI 视频生成并发布到 TikTok
- **自动提交任务**:调用 deepsop API 提交任务,后台异步执行
- **定时查询结果**:任务提交后询问用户期望等待时长,按用户指定时间自动查询并推送结果(默认 8 分钟)
- **生成 xlsx 报表**:AiWa 客户数据自动生成带样式的 Excel 文件返回
- **Frank 邮件统计**:查询邮件发送总数、成功数、已读数、回复数、点击数,并展示发送详情
- **Fran 电话销售**:自动查询号码池与场景库,由用户选择后提交电话销售任务(必须与 AiWa 搭配使用)
- **Lisa 短信统计**:查询短信发送总数、成功数、失败数,并展示发送详情(必须与 AiWa 搭配使用)
- **Toby TikTok 发布统计**:查询视频发布数、播放量、点赞、评论、分享等数据,并展示每条视频明细和 TikTok 链接
---
## 前置条件:获取 API Key
1. 访问 [https://ai.deepsop.com](https://ai.deepsop.com) 注册并登录账号
2. 登录后进入「设置」或「API 管理」页面
3. 新建 API Key,复制以 `sk-` 开头的密钥
4. 在 OpenClaw 中配置环境变量:
```
DEEPSOP_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxx
```
> 所有 API 请求头需携带:`x-api-key: $DEEPSOP_API_KEY`
> API Base URL:`https://ai.deepsop.com/prod-api/`
---
## 完整执行流程
### Step 0:触发类型判断(每次进入技能必须首先执行)
检查当前输入内容是否包含 `[DeepSOP-AutoQuery]` 标记:
- **包含该标记**:这是 cron 定时回调。**不得询问用户是否继续,不得等待确认,不得说「我将开始查询」。立即从输入文本中解析变量(`taskId`、`aiwaDagTaskId`、`aiwaCustomerPoolId`、`frankDagTaskId`、`franDagTaskId`、`franCustomerPoolId`、`lisaDagTaskId`、`lisaCustomerPoolId`、`tobyDagTaskId`、`tobyCustomerPoolId`、`taskName`、`totalTarget`、`employeeList`、`feishuChatId`),跳过 Step 1~4 直接执行 Step 5 的全部内容(查询接口 → 生成 xlsx → 发送文件 → 回复文字摘要),直到所有参与员工的结果都处理完毕。
- **不包含该标记**:这是用户主动指令,继续执行 Step 1。
---
### Step 1:第一轮 AI 分析(任务拆解)
用以下 prompt 分析用户指令,严格返回 JSON,不含任何额外文字:
```
根据【指令】描述,Json格式返回数据
不需要多余的描述,不要过度解读,没有提及的内容请不要擅自理解,识别结果除了Json数据其他文字不要出现
规则如下:{
"taskName": "根据描述总结出一个简洁的任务名称"
"executionMode": "判断描述中是否明确提及每日/每天/周期性,如果提及则返回周期性任务,未提及则返回定额任务"
"totalTarget": "提取描述中提及的数量(无单位纯数字)"
"employeeList": "首先将描述按逗号、顿号等分隔符拆分成多个子任务,然后为每个子任务匹配对应员工:
- 挖掘客户职能(AiWa):匹配任何包含“找”、“开发”、“行业”、“客户”等与客户挖掘相关的描述,以及没有明确匹配其他职能的单子任务
- 邮件销售职能(Frank):匹配包含“邮件”、“发邮件”等关键词的描述
- 电话销售职能(Fran):匹配包含“电话”、“打电话”、“电话销售”等关键词的描述
- 短信销售职能(Lisa):匹配包含“短信”、“发短信”等关键词的描述
- TikTok职能(Toby):匹配包含“TikTok”、“抖音国际版”等关键词的描述
- 生产视频职能(Jack):匹配包含“视频”、“生产视频”等关键词的描述
- 智能SEO优化职能(Sophia):匹配包含“SEO”、“优化”、“搜索引擎”等关键词的描述
- AI剪辑师职能(Alex):匹配包含“剪辑”、“视频剪辑”等关键词的描述
- 独立站客服职能(Leo):匹配包含“客服”、“客户服务”、“咨询”等关键词的描述
如果拆分后只有一个子任务且没有匹配上员工,则默认匹配挖掘客户职能(AiWa)
最后汇总所有匹配到的员工名称组成一个,拼接的字符串并返回(去重)",
"language": "判断描述中是否明确提及国家或地区,若提及了国家或地区但和中国没有关联则返回'英文'其他情况返回'中文'",
"tiktokContent": "根据描述总结出一个TikTok内容发布的内容主题"
}
```
解析结果字段:
- `totalTarget`:目标数量(数字)
- `employeeList`:参与员工逗号字符串,如 `"AiWa"` 或 `"AiWa,Frank"`
- `language`:`"中文"` 或 `"英文"`
- `taskName`:任务名称
- `executionMode`:`"定额任务"` 或 `"周期性任务"`(接口参数:定额=1,周期=2)
- `tiktokContent`:任务描述中涉及 TikTok 发布的内容主题(仅当 employeeList 包含 Toby 时使用)
**员工组合校验:**
1. **不支持的员工拦截**:当 `employeeList` 包含 `Jack`、`Leo`、`Sophia`、`Alex` 中的任意一个时,**终止任务**,回复:
> ⚠️ 数字员工「{员工名}」尚未接入人机协作台,当前支持的员工为:AiWa、Frank、Fran、Lisa、Toby。请调整指令后重试。
2. **销售员工必须搭配 AiWa**:当 `employeeList` 包含 `Frank`、`Fran`、`Lisa` 中的任意一个或多个,但**不包含** `AiWa` 时,**禁止**继续下任务,直接回复用户(`{缺失员工}` 替换为实际缺失的员工名称列表,如 `Frank`、`Frank、Fran`):
> ⚠️ {缺失员工}(邮件/电话/短信销售)必须与 AiWa(客户挖掘)一起使用,无法单独执行销售动作。请在指令中补充客户挖掘需求,例如「帮我找50个美国做服装的客户并发邮件/打电话/发短信」。
并终止当前流程,等待用户补充指令后重新从 Step 1 开始。
3. **Toby 可独立执行**:Toby(TikTok 视频发布)不依赖 AiWa 客户池,可以单独执行或与其他员工组合使用。
> 说明:Frank(邮件)、Fran(电话)、Lisa(短信)均属于“销售动作”员工,必须依赖 AiWa 产出的客户池,因此不能脱离 AiWa 单独下任务。Toby 不受此限制。
---
### Step 1.5:数字员工可用性校验(Step 1 完成后立即执行,所有任务均须)
**接口:** `GET https://ai.deepsop.com/prod-api/ai/presetEmployee/list`
**请求头:** `x-api-key: $DEEPSOP_API_KEY`
响应 `data` 数组中每条记录关键字段:
- `name`:员工名称(与 employeeList 中的名称对应,如 `AiWa`、`Frank`、`Fran`、`Lisa`、`Toby`)
- `status`:启用状态,`0` = 启用,`1` = 禁用
- `remainingDays`:剩余可用天数(可为 null)
**逐一检查 employeeList 中每个员工,规则如下:**
1. **禁用状态(status = 1)→ 终止任务**,回复:
> ⚠️ 数字员工「{name}」当前处于禁用状态,无法执行任务。请联系管理员启用后再试。
2. **剩余天数耗尽(status = 0 且 remainingDays ≤ 0)→ 警告并终止任务**,回复:
> ⚠️ 数字员工「{name}」的使用天数已耗尽(剩余 {remainingDays} 天),请前往 https://ai.deepsop.com 购买/续费后再执行任务。
3. **剩余天数不足(status = 0 且 remainingDays > 0 且 remainingDays ≤ 7)→ 提醒用户,但允许继续**:
> ⚡ 提示:数字员工「{name}」剩余可用天数仅剩 **{remainingDays} 天**,建议尽快前往 https://ai.deepsop.com 续费,以免中断服务。
4. **正常(status = 0 且 remainingDays > 7 或 remainingDays 为 null)→ 继续流程**
**所有员工均通过校验后,方可继续后续步骤。任一员工触发规则 1 或规则 2 立即停止,不得继续下任务。**
---
### Step 2:第二轮 AI 分析(仅当 employeeList 包含 AiWa)
用以下 prompt 对同一用户指令做第二轮分析,严格返回 JSON:
```
根据【指令】描述,Json格式返回数据,其中数值部分用字符串输出
涉及数值规则仅处理描述中明确出现的数字和比较词,最小值规则为 'X以上'=X,'X以下'=空,'X左右'=X; 最大值规则为 'X以上'=空,'X以下'=X,'X左右'=X;
涉及七大洲和国家,如果提及了详细某些国家,七大洲则不用去识别,如果没提及国家则去识别有没有提及七大洲
涉及地址,如果是中国地址的则原文放入,如果是非中国的地址则以英文放入
不需要多余的描述,不要过度解读,没有提及的内容请不要擅自理解,识别结果除了Json数据其他文字不要出现
规则如下:{
"keywordList": "首先识别描述中与客户挖掘相关的部分(匹配‘找’、‘开发’、‘挖掘’、‘拓展’、‘寻找’等关键词),仅从该部分提取核心名词作为关键词;若描述中无客户挖掘相关内容,则识别整个描述来提取核心名词作为关键词。提取核心名词后,排除地理位置相关的关键词(如省、市、区、县、镇、国家、大洲名称),然后添加这些关键词相关的中文同义词和英文对应词,最终返回关键词用英文逗号分隔的结果(如:眼镜店,optical shop,眼镜零售,eyewear store)",
"continent": "明确提及的七大洲(如:亚洲)",
"country": "明确提及的国家,多个用英文逗号分隔(如:中国,英国)",
"countryCodeList": "对应国家的ISO代码,多个用英文逗号分隔(如:CN,GB)",
"addressObjList": "优先识别并排除所有出现在公司名称、品牌名称、企业全称、组织机构名称中的地理位置(如【巨龙光学(福建)有限公司】中的福建、【XX上海分公司】中的上海),这些地理位置不参与提取。排除后,仅从剩余描述中提取明确提及的国家层级之下的地理位置(如:描述为【找中国浙江眼镜店】排除福建后,仅提取浙江);并拆分为一级地址(如:省)二级地址(如:市)三级地址(如:区、县、镇)最后把一二三级中的有效地址通过,拼接返回(如:【浙江宁波】提取返回 浙江,宁波)。若排除公司名称后无其他地理位置,则返回空字符串",
"employeeNumberRangeStart": "只有当描述中明确提及员工数量并且使用'员工X人以上/以下/左右'等范围描述时,按照最小值规则提取数字;否则为空字符串",
"employeeNumberRangeEnd": "只有当描述中明确提及员工数量并且使用'员工X人以上/以下/左右'等范围描述时,按照最大值规则提取数字;否则为空字符串",
"storeNumberRangeStart": "只有当描述中明确提及门店数量并且使用'门店X家以上/以下/左右'或'X家门店以上/以下/左右'等范围描述时,按照最小值规则提取数字;否则为空字符串。单纯的'找X家门店'属于目标数量,不在此字段提取",
"storeNumberRangeEnd": "只有当描述中明确提及门店数量并且使用'门店X家以上/以下/左右'或'X家门店以上/以下/左右'等范围描述时,按照最大值规则提取数字;否则为空字符串。单纯的'找X家门店'属于目标数量,不在此字段提取",
"industryList": "根据以上字段推断行业分类,多个用英文逗号分隔(如:服装,数码,家居)"
}
```
---
### Step 3:构建并提交任务
**接口:** `POST https://ai.deepsop.com/prod-api/ai/presetEmployee/submitTask`
**请求头:**
```
Content-Type: application/json
x-api-key: $DEEPSOP_API_KEY
```
**参数构建规则:**
**前置 A:Fran 号码池与场景库查询(当 employeeList 包含 Fran 时必须先执行)**
**0. 检查外呼实例可用性**
接口:`GET https://ai.deepsop.com/prod-api/ai/outBound/describeInstance`
请求头:`x-api-key: $DEEPSOP_API_KEY`
检查 `data.body.instance.maxConcurrentConversation`:
- 大于 0:继续执行步骤 1(查询号码池)
- 等于 0:**终止任务**,回复用户:
> ⚠️ 当前外呼账号并发数为 0,无法提交电话销售任务,请联系管理员开通并发资源后再试。
**1. 查询号码池**
接口:`GET https://ai.deepsop.com/prod-api/ai/outBound/callerNumber/list`
请求头:`x-api-key: $DEEPSOP_API_KEY`
返回示例:
```json
{
"total": 1,
"rows": [
{
"id": 7,
"callNumber": "30350903",
"nickName": "Kocgo"
}
],
"code": 200,
"msg": "查询成功"
}
```
处理规则:
- `rows` 为空(`total=0`):**终止任务**,回复用户:
> ⚠️ 当前账号下没有可用的外呼号码,无法提交电话销售任务,请联系管理员开通号码后再试。
- `rows` 只有 1 条:自动选用该 `callNumber`,无需用户确认。
- `rows` 有多条:列出所有号码供用户选择(支持多选),格式:
```
检测到多个可用外呼号码,请选择本次任务要使用的号码(可多选,用逗号分隔序号):
1. {callNumber}({nickName})
2. {callNumber}({nickName})
...
```
**等待用户回复后**,解析出被选中的 `callNumber` 列表(数组形式),赋值给 `callingNumber`。未收到选择不得继续。
**2. 查询场景库**
接口:`POST https://ai.deepsop.com/prod-api/ai/outBound/listScripts`
请求头:
```
Content-Type: application/json
x-api-key: $DEEPSOP_API_KEY
```
请求体:
```json
{"pageNumber": 1, "pageSize": 20, "scriptName": ""}
```
返回结构(重点字段):
- `data.body.scripts.list[]`:场景库列表
- `scriptId`:场景库 ID
- `scriptName`:场景库名称
- `industry` / `scene`:行业 / 场景
- `status`:状态,**必须为 `PUBLISHED` 才可用**
- `data.chatbotIdList[]`:与场景库配套的 chatbot id 列表,取第一个作为 `agentProfileId`
处理规则:
- `list` 为空,或过滤后无 `status === "PUBLISHED"` 的场景:**终止任务**,回复用户:
> ⚠️ 当前账号下没有可用(已发布)的场景库,请先登录 https://ai.deepsop.com 创建场景库,并将其状态发布为 `PUBLISHED` 后再试。
- 仅 1 条 `PUBLISHED` 场景:**不得自动选用**,必须列出并等待用户明确确认,格式:
```
检测到以下可用场景库,请确认是否使用(回复「确认」即可):
1. {scriptName}(行业:{industry},场景:{scene})
```
**等待用户明确回复「确认」后**,取对应 `scriptId`。未收到确认不得继续。
- 多条 `PUBLISHED` 场景:列出供用户**单选**,格式:
```
请选择本次电话销售任务要使用的场景库(回复序号):
1. {scriptName}(行业:{industry},场景:{scene})
2. ...
```
**等待用户回复后**,取对应 `scriptId`。未收到选择不得继续。
- `agentProfileId` 统一取 `data.chatbotIdList[0]`(若为空数组则终止并提示联系管理员)。
**前置 B0:Frank 邮箱绑定检查(当 employeeList 包含 Frank 时必须先执行)**
接口:`GET https://ai.deepsop.com/prod-api/ai/emailconfig/list?pageSize=1000&pageNum=1&status=1`
请求头:`x-api-key: $DEEPSOP_API_KEY`
检查 `rows` 列表:
- `rows` 不为空(至少 1 条):继续执行前置 B(获取用户 Profile)
- `rows` 为空(`total=0`):**终止任务**,回复用户:
> ⚠️ 当前账号未绑定可用邮箱,无法提交邮件销售任务,请先登录 https://ai.deepsop.com 前往「邮件配置」绑定邮箱后再试。
**前置 B:获取用户 Profile(当 employeeList 包含 Frank 时必须先执行)**
```bash
curl -s -H "x-api-key: $DEEPSOP_API_KEY" 'https://ai.deepsop.com/prod-api/ai/user/profile'
```
提取以下字段用于邮件署名:
- `nickName`:发件人姓名
- `position`:职位(可能为空,直接取 profile 中的 `position` 字段)
- `dept.deptName`:公司名称
- `phonenumber`:电话(注意字段名全小写)
- `email`:邮箱(作为 `senderEmail`)
**前置 C:AI 生成邮件内容(当 employeeList 包含 Frank 时必须先执行)**
根据用户指令和 profile 信息,用 LLM 生成邮件主题和正文,严格返回 JSON 数组:
```
生成对应语言【{language}】的内容,请直接输出纯净的JSON数组,不包含任何额外文本、代码标记、说明或包装。
输出示例:[{"emailSubject": "邮件主题", "emailText": "邮件内容"}]
邮件生成规则:
1. 开头:使用标准问候语(中文:"尊敬的先生/女士:")
2. 正文:根据【{taskDescription}】生成开发信,必须至少包含以下一项:
- 产品关键词:从 taskDescription 中提取
- 价值主张:包含「功能+场景+风格」三要素(如:【防风防水】男士户外工装夹克 春秋季通勤休闲外套)
- 痛点:具体描述需求未被满足的场景
- 解决方案:突出技术/设计优势与使用场景
- 行动呼吁:包含「稀缺性+权益+行动指令」(如:区域独家授权:仅开放3个地区代理名额!签约即享首单5%折扣→ 立即WhatsApp发送需求)
- 证明点:包含「原始痛点+解决方案+量化结果」的客户案例
- 服务吸引物:分点列出,覆盖供应链/物流/市场支持/售后/定制化5大类
3. 结尾:自然添加对应语言祝福语
4. 署名(每项另起一行,共4行):
{nickName}({position})
{companyName}(若 nickName 与 companyName 相同则省略此行)
{phoneNumber}
{email}
5. 风格:专业、直接、有帮助且富有亲和力;避免使用「免费」「优惠」「限时」等推销词汇
6. 主题:简洁引人入胜,避免垃圾邮件词汇
7. 禁止出现 [Name] 等变量占位符
```
生成结果提取 `emailSubject` 和 `emailText` 用于 Frank 参数。
**前置 D:Lisa 短信模板查询与变量填写(当 employeeList 包含 Lisa 时必须先执行)**
**1. 查询短信模板列表**
接口:`GET https://ai.deepsop.com/prod-api/ai/sms/querySmsTemplateList?pageNum=1&pageSize=20&pageNumber=1`
请求头:`x-api-key: $DEEPSOP_API_KEY`
关键字段:
- `data.smsTemplateList[]`:模板列表
- `auditStatus`:必须为 `AUDIT_STATE_PASS` 才可用
- `templateCode`:模板编码
- `templateName`:模板名称
- `templateContent`:模板内容(含 `xxx` 占位符)
- `signatureName`:签名名称
- `templateType`:模板类型(0=通知, 1=推广, 2=验证码)
- `outerTemplateType`:提交时使用的模板类型参数
处理规则:
- 过滤后无 `auditStatus === "AUDIT_STATE_PASS"` 的模板:**终止任务**,回复用户:
> ⚠️ 当前账号下没有已审核通过的短信模板,请先登录 https://ai.deepsop.com 创建并审核通过短信模板(状态需为 `AUDIT_STATE_PASS`)后再试。
- 仅 1 条 `AUDIT_STATE_PASS` 模板:**不得自动选用**,必须列出并等待用户明确确认,格式:
```
检测到以下可用短信模板,请确认是否使用(回复「确认」即可):
1. {templateName}(类型:{templateType中文})
内容:{templateContent}
```
**等待用户明确回复「确认」后**,取该模板。未收到确认不得继续。
- 多条 `AUDIT_STATE_PASS` 模板:列出供用户**单选**,格式:
```
请选择本次短信销售要使用的模板(回复序号):
1. {templateName}(类型:{templateType中文})
内容:{templateContent}
2. ...
```
**2. 模板变量填写**
选定模板后,解析 `templateContent` 中的 `xxx` 占位符,就每个变量告知用户并求其填写。如模板无变量,跳过此步。
根据 `templateType` 匹配对应变量规则集并告知用户填写要求:
| templateType | 模板类型 | 应用变量规则集 |
|---|---|---|
| 2 | 验证码短信 | verify(验证码类规则) |
| 0 | 通知短信 | notify(通知类规则) |
| 1 | 推广短信 | market(推广类规则) |
**主要变量类型与校验规则:**
| 变量类型名 | code | 适用范围 | 校验规则 |
|---|---|---|---|
| 仅数字(验证码) | numberCaptcha | verify | 纯数字4–6位 |
| 数字+字母组合或仅字母 | characterWithNumber2 | verify | 长度4–6位 |
| 验证码时间(1–2位数字) | verifyTime | verify | 1–99的整数 |
| 时间/日期 | time | notify/market | YYYY-MM-DD、hh:mm、上午/下午等标准时间格式 |
| 金额/数量 | money | notify/market | 纯数字或小数,不含单位符号 |
| 用户昵称 | user_nick | notify/market | 不超过20个字符,不含表情/QQ/微信号 |
| 个人姓名 | name | notify/market | 2–5个简体中文 |
| 企业/组织名称 | unit_name | notify | 仅中文,不超过20字符 |
| 地址 | address | notify | 不超过30字符,不含 QQ/微信号 |
| 车牌号 | license_plate_number | notify | 省份简称+字母+数字组合,不超过10字符 |
| 快递单号 | tracking_number | notify | 8–16位数字,或字母开头+数字字母 |
| 取件码 | pick_up_code | notify | 4–8位数字/短横线/下划线 |
| 其他号码 | other_number2 | notify | 不超过35字符字母数字组合 |
| 电话号码 | phone_number2 | notify | 3–12位纯数字,每模板最多2个号码变量 |
| 链接参数 | link_param | notify/market | 1–8位英文数字,不含完整链接/IP |
| 邮筱地址 | email_address | notify | 7–30字符,包含@ |
| 其他 | others | notify/market | 不超过35字符,不含 QQ/微信/手机/网址 |
**变量匹配逻辑:**
1. 根据变量名(如 `conference`、`address`、`time`)在对应规则集中按变量类型名称匹配:
- `time`/`date`/`day`/`year`/`month` 类 → `time`
- `money`/`price`/`amount` 类 → `money`
- `phone`/`tel`/`mobile` 类 → `phone_number2`
- `address`/`addr`/`location` 类 → `address`
- `name`/姓名类 → `name`
- `user_nick`/昵称类 → `user_nick`
- `conference`/`unit`/组织类 → `unit_name`
- 其他 → `others`
2. 求用户为每个变量填写具体值,并说明类型和校验规则,格式:
> 模板内容为:「{templateContent}」
> 包含以下变量需要填写:
> - `conference`:企业/组织名称(仅中文,不超过20字符)
> - `address`:地址(不超过30字符)
> - `time`:时间(如 2026-04-20 14:30)
> 请为每个变量填写具体内容
3. 用户回复后校验每个变量值是否符合对应规则,不符则进行提示并要求重新填写。
4. 校验通过后,构建 `templateParamList`:
```json
[
{"variableLabel": "conference", "variableAttribute": "unit_name", "variableValue": "用户填写的值"},
{"variableLabel": "address", "variableAttribute": "address", "variableValue": "用户填写的值"},
{"variableLabel": "time", "variableAttribute": "time", "variableValue": "用户填写的值"}
]
```
其中 `variableLabel` = 占位符名(不含 `${}`),`variableAttribute` = 匹配到的 code。
**前置 E:Toby TikTok 账号与发布参数配置(当 employeeList 包含 Toby 时必须先执行)**
**E-1:查询 TikTok 绑定账号**
接口:`GET https://ai.deepsop.com/prod-api/ai/authaccount/list?pageNum=1&pageSize=999&platform=1&status=1`
请求头:`x-api-key: $DEEPSOP_API_KEY`
关键字段:
- `rows[].id`:账号 ID
- `rows[].account`:TikTok 账号名
- `rows[].fansNum`:粉丝数
- `rows[].groupNames`:分组名称
- `rows[].expiredTime`:授权过期时间
处理规则:
- `rows` 为空:**终止任务**,回复:
> ⚠️ 当前账号未绑定任何 TikTok 授权账号,请先登录 https://ai.deepsop.com 添加 TikTok 授权账号后再试。
- `rows` 只有 1 条:付列出并等待用户确认,格式:
```
检测到以下 TikTok 账号,请确认是否使用(回复「确认」即可):
1. @{account}(粉丝:{fansNum},分组:{groupNames})
```
- `rows` 有多条:列出供用户多选,格式:
```
检测到以下 TikTok 授权账号,请选择本次要发布的账号(可多选,用逗号分隔序号):
1. @{account}(粉丝:{fansNum},分组:{groupNames})
2. ...
```
**等待用户确认/选择后**,将选中账号的 `id` 列表记为 `selectedAccountIds`。未收到确认不得继续。
**E-2:获取账号权限信息**
针对第一个选中账号调用:
接口:`GET https://ai.deepsop.com/prod-api/ai/auth/tiktok/getCreatorInfo?authAccountId={selectedAccountIds[0]}`
请求头:`x-api-key: $DEEPSOP_API_KEY`
提取字段(用于构建 `accountConfigList`):
- `data.privacyLevelOptions[]`:可用隐私级别列表
- `data.commentDisabled`:是否禁评
- `data.duetDisabled`:是否禁合拍
- `data.stitchDisabled`:是否禁缝合
若 `privacyLevelOptions` 有多个选项,让用户选择隐私级别,格式:
```
请选择该账号的视频隐私设置(回复序号):
1. PUBLIC_TO_EVERYONE — 全公开
2. MUTUAL_FOLLOW_FRIENDS — 互关好友
3. SELF_ONLY — 仅自己可见
```
**E-3:AI 视频生成模型(默认)**
`param.methodType` **默认固定为 `"3"`**,无需用户选择。如需查看全部可用模型,可调用以下接口获取列表并告知用户当前默认模型名称(展示对应 `sourceValue === "3"` 的 `sourceName`):
接口:`POST https://ai.deepsop.com/prod-api/ai/consumeSource/list?pageNum=1&pageSize=999`
请求体:`{"sourceTypeList":["VIDEO_MODEL"],"hiddenState":"0"}`
视频其他参数亦默认如下,无需用户配置:
- 分辨率:`720p`
- 画面比例:`16:9`
- 视频时长:`8` 秒
**E-4:视频生成提示词确认(必题用户,禁止跳过)**
以 Step 1 解析出的 `tiktokContent` 作为默认提示词,强制询问用户是否需要修改:
```
当前 AI 视频生成提示词为:「{tiktokContent}」
是否需要修改?(回复「不用」直接使用,或直接输入新的提示词)
```
- 用户回复「不用」或类似否定语:保持 `tiktokContent` 不变
- 用户输入新提示词:将 `content` 和 `param.text` 替换为用户输入的内容
**未收到用户回复不得继续。**
**E-5:发布参数配置(必须由用户指定,禁止自动填充)**
针对每个选中的账号,强制用户指定以下参数(如选了多个账号,依次询问每个):
```
请为账号 @{account} 配置发布参数:
- 每天发布视频数(publishCount,如 3):
- 定时发布开始时间(startTime,HH:mm 格式,如 09:30):
- 视频发布间隔(publishInterval,分钟,如 60):
```
**等待用户回复后**,构建该账号的 `publishTemplates` 条目。未收到所有账号的参数不得继续。
**AiWa 参数构建规则:**
- `totalTarget`:定额模式下填 Step 1 的 totalTarget,周期模式下为 null
- `incrementalTarget`:必填,固定填 5000(不可为 null)
- `upperLimitTarget`:固定填 5000
- `keywordList`:Step 2 的 keywordList 拆分成数组
- `continent`:Step 2 的 continent(无则 null)
- `country`:Step 2 的 country(无则 null)
- `countryCodeList`:Step 2 的 countryCodeList 拆分成数组(无则空数组 `[]`)
- `addressObjList`:根据 Step 2 的 address 构建,无则 `[{"type":1,"province":"","city":"","county":"","address":""}]`
- `industryList`:Step 2 的 industryList 拆分成数组
**Frank 参数构建规则:**
- `incrementalTarget`:固定填 1000
- `upperLimitTarget`:固定填 1000
- `senderEmail`:来自 profile 的 `email`
- `language`:来自 Step 1 的 `language`(`"中文"` 或 `"英文"`)
- `templateId`:固定为 null
- `emailPlanList`:包含一个对象,字段:
- `delayDay`:0
- `emailSubject`:AI 生成的邮件主题
- `emailText`:AI 生成的邮件正文(HTML 格式)
- `loading`:0
**Fran 参数构建规则:**
- `ringingDuration`:固定填 25
- `incrementalTarget`:固定填 1000
- `upperLimitTarget`:固定填 1000
- `minConcurrency`:固定填 1
- `priority`:固定填 `"Daily"`
- `callingNumber`:前置 A 第 1 步用户选定的号码数组(如 `["30350903"]`)
- `scriptId`:前置 A 第 2 步用户选定的场景库 `scriptId`
- `agentProfileId`:前置 A 第 2 步 `data.chatbotIdList[0]`
**Fran 任务请求体示例(AiWa + Fran 联合任务):**
```json
{
"collaborationSubmitTaskParam": {
"taskName": "启动财务课程电话销售",
"taskDescription": "帮我找客户并启动电话销售",
"executionMode": 1,
"employeeParams": {
"AiWa": { "...": "同上" },
"Fran": {
"ringingDuration": 25,
"incrementalTarget": 1000,
"upperLimitTarget": 1000,
"callingNumber": ["30350903"],
"minConcurrency": 1,
"priority": "Daily",
"scriptId": "c92d016f-03c8-47a3-95d9-61d75e192181",
"agentProfileId": "chatbot-cn-RYRmV3jjzb"
}
},
"sourceSettings": {
"groupId": [], "stageId": [], "labelId": [], "level": [],
"seasGroupIds": [], "addressId": [], "fileList": [],
"updateSupport": 1, "cascader": null, "aiMining": null,
"customerMining": null, "seasMining": null, "uploadMining": null,
"countryId": null, "addressMining": null
},
"currentModule": "content"
},
"completed": true
}
```
> ⚠️ 当 `employeeList` 包含 `Fran` 或 `Lisa` 时,`sourceSettings` 必须按上述完整对象填充(不能为 `null`),且 `currentModule` 固定为 `"content"`。
**Lisa 参数构建规则:**
- `incrementalTarget`:固定填 100
- `upperLimitTarget`:固定填 100
- `signName`:选定模板的 `signatureName`
- `qualificationName`:同 `signName`(如两者不同由用户确认)
- `templateCode`:选定模板的 `templateCode`
- `templateContent`:选定模板的 `templateContent`
- `templateType`:选定模板的 `outerTemplateType`(注意:是 `outerTemplateType` 而非 `templateType`)
- `templateParamList`:前置 D 第 2 步构建的变量数组(无变量则为 `[]`)
**Lisa 任务请求体示例(AiWa + Lisa 联合任务):**
```json
{
"collaborationSubmitTaskParam": {
"taskName": "双十一老客户短信推广",
"taskDescription": "帮我找客户并给老客户发短信",
"executionMode": 1,
"employeeParams": {
"AiWa": { "...": "同上" },
"Lisa": {
"incrementalTarget": 100,
"upperLimitTarget": 100,
"qualificationName": "杭州库阔数字科技",
"signName": "杭州库阔数字科技",
"templateCode": "SMS_500460013",
"templateParamList": [
{"variableLabel": "conference", "variableAttribute": "unit_name", "variableValue": "库阔科技"},
{"variableLabel": "address", "variableAttribute": "address", "variableValue": "杭州"},
{"variableLabel": "time", "variableAttribute": "time", "variableValue": "2026-04-20"}
],
"templateType": 1,
"templateContent": "温馨提醒:conference会议将在address地点,于time时间开始,请您准时参加。"
}
},
"sourceSettings": {
"groupId": [], "stageId": [], "labelId": [], "level": [],
"seasGroupIds": [], "addressId": [], "fileList": [],
"updateSupport": 1
},
"currentModule": "content"
},
"completed": true
}
```
**Toby 参数构建规则:**
- `totalTarget`:定额模式下填 Step 1 的 totalTarget,周期模式下为 null
- `incrementalTarget`:周期模式下填用户指定的每天发布数,定额模式下固定填 10
- `upperLimitTarget`:固定 10
- `content`:来自 Step 1 的 `tiktokContent`
- `staffId`:固定为空字符串 `""`
- `param`:
- `methodType`:默认 `"3"`(来自 E-3)
- `text`:同 `content`(E-4 确认后的最终提示词)
- `resolution`:`"720p"`
- `ratio`:`"16:9"`
- `duration`:`8`
- 其他字段按以下示例固定填:`multiShot=false`、`generationType="FIRST&LAST"`、`negativePrompt=""`、`imageUrlList=[]`、`firstImageUrl=null`、`lastImageUrl=null`、`firstClipUrl=null`、`elementList=[]`、`videoUrlList=[]`、`audioUrl=null`、`keepOriginalSound="yes"`、`durationList=[]`、`mode="pro"`、`resolution="720p"`、`ratio="16:9"`、`generateAudio=true`、`enhancePrompt=false`、`n=1`、`personGeneration="allow_adult"`、`resizeMode="pad"`、`promptExtend=false`、`shotType="single"`、`durationSwitch="1"`、`duration=8`、`multiPrompt=[]`
- `videoItems`:固定为 `[]`
- `publishTemplates`:每个选中账号一条,字段:
- `publishCount`:用户指定(字符串)
- `releaseType`:固定 `"1"`
- `timeZone`:固定 `"1"`
- `intervalType`:固定 `"1"`
- `startTime`:用户指定(HH:mm)
- `accountId`:对应账号的 `id`(字符串)
- `publishInterval`:用户指定(整数,分钟)
- `accountConfigList`:仅一条,取前置 E-2 中第一个选中账号的权限信息,字段:
- `accountId`:`selectedAccountIds[0]`(字符串)
- `privacyLevel`:用户选定的隐私级别
- `disableDuet`:来自 `data.duetDisabled`(布尔转字符串)
- `disableStitch`:来自 `data.stitchDisabled`
- `disableComment`:来自 `data.commentDisabled`
- `expand`:固定 `false`
- `brandContentToggle`:固定 `"false"`
- `brandOrganicToggle`:固定 `"false"`
- `isPublicAccount`:固定 `true`
- `commentDisabled`:同 `data.commentDisabled`(布尔转字符串)
- `duetDisabled`:同 `data.duetDisabled`
- `stitchDisabled`:同 `data.stitchDisabled`
**Toby 任务请求体示例:**
```json
{
"collaborationSubmitTaskParam": {
"taskName": "AI宣传视频TikTok分发",
"taskDescription": "生成库阔AI宣传视频分发到tiktok",
"executionMode": 1,
"employeeParams": {
"Toby": {
"totalTarget": 1,
"incrementalTarget": 10,
"upperLimitTarget": 10,
"content": "库阔AI宣传视频",
"staffId": "",
"param": {
"methodType": "3",
"multiShot": false,
"generationType": "FIRST&LAST",
"text": "库阔AI宣传视频",
"multiPrompt": [],
"negativePrompt": "",
"imageUrlList": [],
"firstImageUrl": null,
"lastImageUrl": null,
"firstClipUrl": null,
"elementList": [],
"videoUrlList": [],
"audioUrl": null,
"keepOriginalSound": "yes",
"durationList": [],
"mode": "pro",
"resolution": "720p",
"ratio": "16:9",
"generateAudio": true,
"enhancePrompt": false,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": false,
"shotType": "single",
"durationSwitch": "1",
"duration": 8
},
"videoItems": [],
"publishTemplates": [
{
"publishCount": "1",
"releaseType": "1",
"timeZone": "1",
"intervalType": "1",
"startTime": "15:10",
"accountId": "130",
"publishInterval": 60
}
],
"accountConfigList": [
{
"accountId": "130",
"privacyLevel": "PUBLIC_TO_EVERYONE",
"disableDuet": "false",
"disableStitch": "false",
"disableComment": "false",
"expand": false,
"brandContentToggle": "false",
"brandOrganicToggle": "false",
"isPublicAccount": true,
"commentDisabled": "false",
"duetDisabled": "false",
"stitchDisabled": "false"
}
]
}
},
"sourceSettings": null,
"currentModule": "analysis"
},
"completed": true
}
```
---
**⚠️ 提交前必须执行参数完整性校验(缺少任意一项禁止提交)**
根据本次任务包含的员工,逐项对照以下清单检查构建好的请求体,确认每个字段都存在且有合法值:
**根结构(必须):**
- `collaborationSubmitTaskParam.taskName`:非空字符串
- `collaborationSubmitTaskParam.taskDescription`:非空字符串
- `collaborationSubmitTaskParam.executionMode`:值为 `1`
- `collaborationSubmitTaskParam.employeeParams`:对象,包含至少一个员工
- `collaborationSubmitTaskParam.sourceSettings`:仅含 AiWa+Frank 时可为 `null`;含 Fran 或 Lisa 时必须为完整对象(见 Fran/Lisa 示例)
- `completed`:值为 `true`
**AiWa(当 employeeList 包含 AiWa 时):**
- `totalTarget`:用户指定的目标数量(正整数)
- `incrementalTarget`:`5000`
- `upperLimitTarget`:`5000`
- `keywordList`:非空数组
- `continent`:字符串或 `null`
- `country`:字符串或 `null`
- `countryCodeList`:数组(可为空数组 `[]`)
- `addressObjList`:包含至少一个对象,每个对象含 `type`/`province`/`city`/`county`/`address` 五个字段
- `industryList`:非空数组
**Frank(当 employeeList 包含 Frank 时):**
- `incrementalTarget`:`1000`
- `upperLimitTarget`:`1000`
- `senderEmail`:来自 profile 的 email,非空字符串
- `language`:`"中文"` 或 `"英文"`
- `templateId`:`null`
- `emailPlanList`:包含一个对象,该对象必须含以下四个字段:
- `delayDay`:`0`
- `emailSubject`:AI 生成的主题,非空字符串
- `emailText`:AI 生成的正文 HTML,非空字符串
- `loading`:`0`
**Fran(当 employeeList 包含 Fran 时):**
- `ringingDuration`:`25`
- `incrementalTarget`:`1000`
- `upperLimitTarget`:`1000`
- `minConcurrency`:`1`
- `priority`:`"Daily"`
- `callingNumber`:非空数组,来自号码池选择
- `scriptId`:非空字符串,来自场景库选择
- `agentProfileId`:非空字符串,来自 `data.chatbotIdList[0]`
**Lisa(当 employeeList 包含 Lisa 时):**
- `incrementalTarget`:`100`
- `upperLimitTarget`:`100`
- `signName`:非空字符串,来自模板 `signatureName`
- `qualificationName`:非空字符串,与 `signName` 相同
- `templateCode`:非空字符串,来自模板 `templateCode`
- `templateContent`:非空字符串,来自模板 `templateContent`
- `templateType`:数字,来自模板 `outerTemplateType`
- `templateParamList`:数组(无变量时为 `[]`,不可缺少此字段)
**Toby(当 employeeList 包含 Toby 时):**
- `totalTarget`:定额模式下为正整数,周期模式下为 null
- `incrementalTarget`:正整数
- `upperLimitTarget`:`10`
- `content`:非空字符串,来自 `tiktokContent`
- `staffId`:空字符串 `""`
- `param.methodType`:非空字符串,来自用户选定模型的 `sourceValue`
- `param.text`:非空字符串
- `publishTemplates`:非空数组,每个账号一条,且 `publishCount`/`startTime`/`publishInterval`/`accountId` 均非空
- `accountConfigList`:包含且仅一条,`accountId`/`privacyLevel` 非空
- `sourceSettings`:`null`
- `currentModule`:`"analysis"`
**发现任何字段缺失或值不合法时,停止提交,先补全后再执行提交。**
**用户确认清单(以下各项必须已获得用户明确确认,缺一不可提交):**
- Fran 参与时:✅ `callingNumber` 非空(多号码时用户已选择)
- Fran 参与时:✅ 用户已选择/确认场景库(`scriptId` 非空,且用户有明确回复确认)
- Lisa 参与时:✅ 用户已选择/确认短信模板(`templateCode` 非空,且用户有明确回复确认)
- Lisa 参与时(模板含变量):✅ 用户已填写并校验通过所有模板变量(`templateParamList` 构建完整)
- Frank 参与时:✅ 用户 profile 已获取(`senderEmail` 非空),邮件内容已 AI 生成
- Toby 参与时:✅ 用户已选择/确认 TikTok 账号(`selectedAccountIds` 非空)
- Toby 参与时:✅ 用户已确认或修改视频生成提示词(`content` 已确定)
- Toby 参与时:✅ 用户已为每个账号填写 `publishCount`、`startTime`、`publishInterval`
- Toby 参与时:✅ 用户已选择隐私级别(`privacyLevel` 非空)
**若上述任一项未完成,禁止调用提交接口。**
---
**请求体示例(AiWa + Frank 联合任务):**
```json
{
"collaborationSubmitTaskParam": {
"taskName": "找服装客户并发邮件",
"taskDescription": "帮我找10个做服装的客户并发邮件",
"executionMode": 1,
"employeeParams": {
"AiWa": {
"totalTarget": 10,
"incrementalTarget": 5000,
"upperLimitTarget": 5000,
"keywordList": ["服装", "clothing"],
"continent": null,
"country": null,
"countryCodeList": [],
"addressObjList": [{"type": 1, "province": "", "city": "", "county": "", "address": ""}],
"industryList": ["服装"]
},
"Frank": {
"incrementalTarget": 1000,
"upperLimitTarget": 1000,
"senderEmail": "{profile.email}",
"language": "中文",
"templateId": null,
"emailPlanList": [{
"delayDay": 0,
"emailSubject": "{AI生成的邮件主题}",
"emailText": "{AI生成的邮件正文HTML}",
"loading": 0
}]
}
},
"sourceSettings": null,
"currentModule": "content"
},
"completed": true
}
```
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"employeeList": [
{
"dagTaskId": "<frankDagTaskId>",
"nodeType": "FRANK",
"customerPoolId": 1065
},
{
"dagTaskId": "<aiwaDagTaskId>",
"nodeType": "AIWA",
"customerPoolId": 1066
}
],
"taskId": "<taskId>"
}
}
```
**响应字段提取规则:**
- `taskId`:取 `data.taskId`
- `aiwaDagTaskId`:遍历 `data.employeeList`,找到 `nodeType === "AIWA"` 的条目,取其 `dagTaskId`,用于 **AiWa** 客户查询;无则为 null
- `aiwaCustomerPoolId`:遍历 `data.employeeList`,找到 `nodeType === "AIWA"` 的条目,取其 `customerPoolId`,用于 **AiWa** 客户查询;无则为 null
- `frankDagTaskId`:遍历 `data.employeeList`,找到 `nodeType === "FRANK"` 的条目,取其 `dagTaskId`,用于 **Frank** 邮件查询;无则为 null
- `franDagTaskId`:遍历 `data.employeeList`,找到 `nodeType === "Fran"` 的条目,取其 `dagTaskId`,用于 **Fran** 电话查询;无则为 null
- `franCustomerPoolId`:遍历 `data.employeeList`,找到 `nodeType === "Fran"` 的条目,取其 `customerPoolId`,用于 **Fran** 电话统计/详情查询;无则为 null
- `lisaDagTaskId`:遍历 `data.employeeList`,找到 `nodeType === "Lisa"` 的条目,取其 `dagTaskId`,用于 **Lisa** 短信查询;无则为 null
- `lisaCustomerPoolId`:遍历 `data.employeeList`,找到 `nodeType === "Lisa"` 的条目,取其 `customerPoolId`,用于 **Lisa** 短信统计/详情查询;无则为 null
- `tobyDagTaskId`:遍历 `data.employeeList`,找到 `nodeType === "Toby"` 的条目,取其 `dagTaskId`,用于 **Toby** 任务查询;无则为 null
- `tobyCustomerPoolId`:遍历 `data.employeeList`,找到 `nodeType === "Toby"` 的条目,取其 `customerPoolId`,用于 **Toby** 视频统计/列表查询;无则为 null
提交成功后,告知用户并询问等待时间:
> 任务已提交!任务名:{taskName},目标数量:{totalTarget},任务ID:{taskId}。
>
> 后台正在执行,**你希望多久后查询结果并推送给你?**(直接告诉我时间,例如「8分钟」「半小时」「20分钟后」,直接回复「好」或不填则默认8分钟)
---
### Step 3.5:解析用户指定的等待时间
等待用户回复后,解析其意图为秒数:
| 用户说 | 解析为秒数 |
|--------|----------|
| N分钟 / N分 | N × 60 |
| N小时 | N × 3600 |
| 半小时 | 1800 |
| 一刻钟 | 900 |
| 好 / 默认 / ok / 回车 / 不填 | 480(8分钟)|
| 无法识别 | 再询问一次,若仍无效则使用 480 |
解析成功后,回复确认:
> 好的,将在 {用户指定时间描述}(约 {N} 分钟)后为你查询结果,请稍候 ☕
---
### Step 4:按用户指定时间设置自动查询
根据 Step 3.5 解析出的秒数(变量:`waitSeconds`),使用 `cron` 工具设置一次性定时任务:
```json
{
"action": "add",
"job": {
"name": "aiwa-query-{taskId前8位}",
"schedule": { "kind": "at", "at": "{当前时间 + waitSeconds 的ISO8601字符串,如2026-03-19T15:00:00+08:00}" },
"sessionTarget": "main",
"wakeMode": "now",
"payload": {
"kind": "systemEvent",
"text": "[DeepSOP-AutoQuery] 人机协作台定时结果推送,请立即跳转 Step 5 执行结果查询并主动推送,不要等待用户提问,不要执行 Step 1-4。taskId={taskId},aiwaDagTaskId={aiwaDagTaskId},aiwaCustomerPoolId={aiwaCustomerPoolId},frankDagTaskId={frankDagTaskId},franDagTaskId={franDagTaskId},franCustomerPoolId={franCustomerPoolId},lisaDagTaskId={lisaDagTaskId},lisaCustomerPoolId={lisaCustomerPoolId},tobyDagTaskId={tobyDagTaskId},tobyCustomerPoolId={tobyCustomerPoolId},任务名:{taskName},目标数量:{totalTarget},参与员工:{employeeList},feishuChatId={feishuChatId}。【AiWa部分,仅当employeeList包含AiWa时执行】1. 调用 POST https://ai.deepsop.com/prod-api/ai/presetEmployee/getCustomerPoolDetail?pageNum=1&pageSize=10 查询结果,参数 {"taskId":"{aiwaDagTaskId}","customerPoolId":{aiwaCustomerPoolId},"startTime":null,"endTime":null};2. 将完整响应JSON传给脚本生成xlsx:python3 ~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_customers.py '<JSON>' '/tmp/aiwa_{aiwaDagTaskId前8位}.xlsx';3. 执行 cp /tmp/aiwa_{aiwaDagTaskId前8位}.xlsx ~/.openclaw/workspace/aiwa_{aiwaDagTaskId前8位}.xlsx 并执行 openclaw message send --channel feishu --target {feishuChatId} --media ~/.openclaw/workspace/aiwa_{aiwaDagTaskId前8位}.xlsx --message 'AiWa 客户挖掘完成,共找到客户数据,详见附件' 将文件发送到飞书群;4. 同时在当前会话回复前5条客户摘要。【Toby部分,仅当employeeList包含Toby且tobyDagTaskId不为null时执行】1. 调用 GET https://ai.deepsop.com/prod-api/ai/data/count?taskId={tobyDagTaskId}&customerPoolId={tobyCustomerPoolId}&platform=1 查询统计;2. 调用 GET https://ai.deepsop.com/prod-api/ai/data/list?pageNum=1&pageSize=10&taskId={tobyDagTaskId}&customerPoolId={tobyCustomerPoolId}&platform=1 查询视频列表;3. 展示统计数据(播放、点赞、评论、分享、发布总数)并列出每条视频的titleName、platformUrl、播放量、点赞数、评论数、转发数、displayCreateTime;4. 在当前会话回复结果摘要。【Frank部分,仅当employeeList包含Frank且frankDagTaskId不为null时执行】1. 调用 GET https://ai.deepsop.com/prod-api/ai/email/getTaskEmailCount?taskId={frankDagTaskId} 查询邮件统计(使用frankDagTaskId);2. 调用 GET https://ai.deepsop.com/prod-api/ai/email/taskList?pageNum=1&pageSize=2000&taskId={frankDagTaskId} 查询邮件列表(使用frankDagTaskId);3. 生成xlsx:python3 ~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_emails.py '<JSON>' '/tmp/frank_{frankDagTaskId前8位}.xlsx';4. 执行 cp /tmp/frank_{frankDagTaskId前8位}.xlsx ~/.openclaw/workspace/frank_{frankDagTaskId前8位}.xlsx 并执行 openclaw message send --channel feishu --target {feishuChatId} --media ~/.openclaw/workspace/frank_{frankDagTaskId前8位}.xlsx --message 'Frank 邮件发送完成,详见附件' 将文件发送到飞书群;5. 同时在当前会话回复邮件统计摘要和前5条详情。【Lisa部分,仅当employeeList包含Lisa且lisaCustomerPoolId不为null时执行】1. 调用 POST https://ai.deepsop.com/prod-api/ai/sms/getTaskSmsCount 查询短信统计,参数 {"taskId":"{taskId}","customerPoolId":{lisaCustomerPoolId}};2. 调用 POST https://ai.deepsop.com/prod-api/ai/sms/getSmsResultList?pageNum=1&pageSize=10 查询短信列表,参数 {"taskId":"{taskId}","customerPoolId":{lisaCustomerPoolId},"success":null,"startTime":null,"endTime":null};3. 生成xlsx:python3 ~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_sms.py '<JSON>' '/tmp/lisa_{taskId前8位}.xlsx';4. 发送文件并在当前会话展示短信统计摘要和前5条短信详情。【Toby部分,仅当employeeList包含Toby且tobyDagTaskId不为null时执行】1. 调用 GET https://ai.deepsop.com/prod-api/ai/data/count?taskId={tobyDagTaskId}&customerPoolId={tobyCustomerPoolId}&platform=1 查询视频统计;2. 调用 GET https://ai.deepsop.com/prod-api/ai/data/list?pageNum=1&pageSize=10&taskId={tobyDagTaskId}&customerPoolId={tobyCustomerPoolId}&platform=1 查询视频列表;3. 在当前会话回复统计概览(发布视频数/播放/点赞/评论/分享)并列出每条视频的标题、链接、各项数据及发布时间。"
},
"deleteAfterRun": true
}
}
```
`schedule.at` = 当前时间 + `waitSeconds`,ISO8601 格式,含时区(如 `+08:00`)。
cron 设置成功后,回复用户确认并进入等待状态:
> ✅ 定时任务已设置!将在 **{N} 分钟后**({schedule.at})自动查询结果并推送,请安心等候 ⏰
> 如需提前查询,可说「现在就查结果」,我会立即执行。
> ⚠️ **等待期间处理规则**:
> - cron 设置完成到 [DeepSOP-AutoQuery] 到达之前,**不得主动执行 Step 5**。
> - 如果用户在等待期间间起其他话题,正常回应,但**不要提前查询结果**。
> - 如果用户说「现在就查结果」或「提前查」,立即执行 Step 5(此为唯一允许的提前触发方式)。
---
### Step 5:查询结果并返回给用户
> � **触发锁定:Step 5 只允许在以下两种情况下执行,其他任何情况一律不执行:**
> 1. 收到含 `[DeepSOP-AutoQuery]` 标记的 systemEvent(cron 自动触发)
> 2. 用户在等待期间明确说「现在就查结果」或「提前查」
>
> 🚨 **强制执行规则:执行 Step 5 时,以下规则一条都不得违反:**
> 1. **立即开始执行,不得发出任何询问或确认语句(如「要开始查询了吗」「是否需要推送」)**
> 2. **必须完成全流程:调接口 → 生成 xlsx → 发送文件 → 文字摘要,缺任一不算完成**
> 3. **文件必须透过 `openclaw message send` 主动发送到对应 channel,不得只告知文件路径**
> 4. **发送完成后在当前会话回复结果摘要,让用户对当前分话框也能看到结果**
> 5. **每个参与员工的结果必须按顺序全部处理,不得跳过任一员工**
根据 employeeList 包含的员工依次执行对应的 Step 5-A / 5-B / 5-C / 5-D / 5-E。
---
#### Step 5-A:AiWa 结果处理(仅当 employeeList 包含 AiWa)
> ⚠️ AiWa 查询接口使用 `aiwaDagTaskId`(`nodeType=AIWA` 的 `dagTaskId`)+ `aiwaCustomerPoolId`(同条目的 `customerPoolId`)。
**接口:** `POST https://ai.deepsop.com/prod-api/ai/presetEmployee/getCustomerPoolDetail?pageNum=1&pageSize=10`
**请求头:** `Content-Type: application/json`、`x-api-key: $DEEPSOP_API_KEY`
**请求体:**
```json
{"taskId": "{aiwaDagTaskId}", "customerPoolId": {aiwaCustomerPoolId}, "startTime": null, "endTime": null}
```
**响应关键字段:**
- `total`:总条数
- `rows[]`:客户列表(根层)
- `personName` / `position`:联系人 / 职位
- `companyName`:公司名称
- `systemIndustryName`:标准化行业名称
- `phone` / `email`:电话 / 邮筱(多个用逗号分隔)
- `countryName`:国家(如 `中国/China`)
- `whatsapp` / `linkedin` / `facebook` 等社媒字段
- `url`:公司网址
**情况一:有数据(rows 非空)**
1. 将完整 API 响应 JSON 传给脚本生成 xlsx 文件:
```bash
python3 ~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_customers.py '<完整响应JSON>' '/tmp/aiwa_{aiwaDagTaskId前8位}.xlsx'
```
2. 根据当前 channel 决定如何返回文件:
**飞书(feishu):** 必须执行,不得跳过
```bash
cp /tmp/aiwa_{aiwaDagTaskId前8位}.xlsx ~/.openclaw/workspace/aiwa_{aiwaDagTaskId前8位}.xlsx
openclaw message send --channel feishu --target {feishuChatId} --media ~/.openclaw/workspace/aiwa_{aiwaDagTaskId前8位}.xlsx --message 'AiWa 客户挖掘完成!任务「{taskName}」共找到 {total} 位客户,详情见附件。'
```
**Telegram / WhatsApp:** 必须执行,不得跳过
```
openclaw message send --channel telegram --target {chat_id} --media ~/.openclaw/workspace/aiwa_{aiwaDagTaskId前8位}.xlsx --message 'AiWa 客户挖掘完成!任务「{taskName}」共找到 {total} 位客户,详情见附件。'
```
**webchat 或其他不支持文件的 channel:**
> ✅ xlsx 文件已生成:`/tmp/aiwa_{aiwaDagTaskId前8位}.xlsx`,共 {total} 位客户。请从服务器下载该文件。
3. 同时以文字形式展示前5条客户预览:
```
序号. 👤 {personName}({position})
🏢 公司:{companyName}
🏭 行业:{systemIndustryName}
🌍 国家:{countryName}
📧 邮筱:{email}
📱 手机:{phone}
💬 WhatsApp:{whatsapp}
🔗 LinkedIn:{linkedin}
```
社媒字段若为 null 则整行不显示。超过5条附上:`...共 {N} 位,完整数据见 xlsx 文件`
**情况二:rows 为空或 code 非 200**
> 已到查询时间,暂未获取到客户数据,任务可能仍在执行中。
> aiwaDagTaskId:{aiwaDagTaskId},aiwaCustomerPoolId:{aiwaCustomerPoolId}
> 你可以告诉我「再查一次」,我会立即重新查询。
---
#### Step 5-C:Fran 结果处理(仅当 employeeList 包含 Fran 且 franDagTaskId 不为 null)
> ⚠️ Fran 的两个查询接口均使用 `franDagTaskId`(来自 `data.employeeList` 中 `nodeType=Fran` 的 `dagTaskId`)+ `franCustomerPoolId`(同条目的 `customerPoolId`)。
**第一步:查询电话任务统计**
接口:`GET https://ai.deepsop.com/prod-api/ai/presetEmployee/collaborationTaskStatistics?taskId={franDagTaskId}&customerPoolId={franCustomerPoolId}`
请求头:`x-api-key: $DEEPSOP_API_KEY`
返回字段说明:
- `taskCallPhoneCount`:总呼叫数
- `taskSuccessCallPhoneCount`:总通话数(接通并成功完成的数量)
- `taskAnswerCount`:总回复数
**第二步:查询电话任务详情列表**
接口:`POST https://ai.deepsop.com/prod-api/ai/presetEmployee/collaborationCallResult?pageNum=1&pageSize=10`
请求头:
```
Content-Type: application/json
x-api-key: $DEEPSOP_API_KEY
```
请求体(默认拉全部呼叫,`status` 可根据需求切换):
```json
{
"taskId": "{franDagTaskId}",
"customerPoolId": {franCustomerPoolId},
"status": "All",
"startTime": "",
"endTime": ""
}
```
`status` 可选值:
- `All`:全部呼叫(默认,对应 `taskCallPhoneCount`)
- `Succeeded`:通话成功记录(对应 `taskSuccessCallPhoneCount`)
- `Answer`:有回复记录(对应 `taskAnswerCount`)
响应关键字段:
- `rows[].jobStatus`:呼叫任务状态(如 `Succeeded`、`Failed`)
- `rows[].jobTaskStatus`:任务执行细分状态(如 `SucceededFinish`)
- `rows[].companyName` / `personName` / `phoneNumber` / `userName`:客户信息(可能为 null)
- `rows[].createTime` / `updateTime`:创建/更新时间
- `rows[].describeJobJson`:完整通话详情 JSON(字符串,需二次解析),关键内容在 `body.job.tasks[]`:
- `contact.contactName` / `phoneNumber`:联系人与号码
- `calledNumber` / `callingNumber`:被呼号 / 呼出号
- `duration` / `realRingingDuration`:通话时长毫秒 / 实际振铃秒
- `endReason`:挂断原因(如 `FINISHED`)
- `conversation[]`:对话明细(`speaker`=`Robot`/`Contact`,`script`=话术内容)
**第三步:生成 xlsx 文件**
```bash
curl 结果存 /tmp/fran_{franDagTaskId前8位}_raw.json
python3 ~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_calls.py "$(cat /tmp/fran_{franDagTaskId前8位}_raw.json)" '/tmp/fran_{franDagTaskId前8位}.xlsx'
```
根据当前 channel 必须发送文件,不得跳过:
- **飞书**:必须执行 `cp /tmp/fran_{franDagTaskId前8位}.xlsx ~/.openclaw/workspace/fran_{franDagTaskId前8位}.xlsx`,再用 `openclaw message send --channel feishu --target {feishuChatId} --media ~/.openclaw/workspace/fran_{franDagTaskId前8位}.xlsx --message 'Fran 电话销售完成!任务「{taskName}」共呼叫 {taskCallPhoneCount} 人,详情见附件。'`
- **Telegram / WhatsApp**:必须执行,media 路径用 workspace 路径,message 同上
- **webchat**:输出文字摘要和文件路径 `/tmp/fran_{franDagTaskId前8位}.xlsx`
**第四步:展示结果**
先展示统计摘要:
```
☎️ Fran 电话销售任务结果 — {taskName}
📊 呼叫统计:
📞 总呼叫数:{taskCallPhoneCount}
✅ 总通话数:{taskSuccessCallPhoneCount}
💬 总回复数:{taskAnswerCount}
```
再展示列表中前 5 条呼叫详情(从 `rows[].describeJobJson` 解析):
```
序号. 👤 {contactName}({contactPhone})
🔄 状态:{jobStatus} / {endReason}
⏱ 通话时长:{duration_s}s(振铃 {realRingingDuration}s)
📞 呼出号码:{callingNumber}
📝 对话摘要:取 conversation 中第一条 Robot 的 script 前80字
```
超过 5 条附上:`...共 {total} 条通话记录,完整数据见 xlsx 文件`。
**情况:统计接口全为 0 或列表为空**
> Fran 电话任务数据暂未就绪,可能仍在拨号或已呼叫但未接通。
> franDagTaskId:{franDagTaskId},franCustomerPoolId:{franCustomerPoolId}
> 你可以告诉我「再查Fran结果」,我会立即重新查询。
---
#### Step 5-D:Lisa 结果处理(仅当 employeeList 包含 Lisa 且 lisaDagTaskId 不为 null)
> ⚠️ Lisa 的两个查询接口均使用 `lisaDagTaskId`(来自 `data.employeeList` 中 `nodeType=Lisa` 的 `dagTaskId`)+ `lisaCustomerPoolId`(同条目的 `customerPoolId`)。
**第一步:查询短信任务统计**
接口:`POST https://ai.deepsop.com/prod-api/ai/sms/getTaskSmsCount`
请求头:`Content-Type: application/json`、`x-api-key: $DEEPSOP_API_KEY`
请求体:
```json
{"taskId": "{lisaDagTaskId}", "customerPoolId": {lisaCustomerPoolId}}
```
返回字段说明:
- `totalCount`:已发送短信数
- `successCount`:触达短信数(发送成功)
- `failCount`:失败短信数
**第二步:查询短信详情列表**
接口:`POST https://ai.deepsop.com/prod-api/ai/sms/getSmsResultList?pageNum=1&pageSize=10`
请求头:
```
Content-Type: application/json
x-api-key: $DEEPSOP_API_KEY
```
请求体:
```json
{"taskId": "{lisaDagTaskId}", "customerPoolId": {lisaCustomerPoolId}, "success": null, "startTime": null, "endTime": null}
```
关键字段:
- `data.total`:总条数
- `data.rows[].phoneNumber`:手机号码
- `data.rows[].success`:1=发送成功,0=发送失败
- `data.rows[].errMsg`:状态描述(如「用户接收成功」)
- `data.rows[].errCode`:状态码(如 `DELIVERED`)
- `data.rows[].content`:实际发送的短信内容
- `data.rows[].smsSize`:短信条数
- `data.rows[].sendTime`:发送时间
- `data.rows[].reportTime`:回执时间
**第三步:生成 xlsx 文件**
```bash
curl 结果存 /tmp/lisa_{lisaDagTaskId前8位}_raw.json
python3 ~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_sms.py "$(cat /tmp/lisa_{lisaDagTaskId前8位}_raw.json)" '/tmp/lisa_{lisaDagTaskId前8位}.xlsx'
```
根据当前 channel 必须发送文件,不得跳过:
- **飞书**:必须执行 `cp /tmp/lisa_{lisaDagTaskId前8位}.xlsx ~/.openclaw/workspace/lisa_{lisaDagTaskId前8位}.xlsx`,再用 `openclaw message send --channel feishu --target {feishuChatId} --media ~/.openclaw/workspace/lisa_{lisaDagTaskId前8位}.xlsx --message 'Lisa 短信发送完成!任务「{taskName}」共发送 {totalCount} 条,详情见附件。'`
- **Telegram / WhatsApp**:必须执行,media 路径用 workspace 路径,message 同上
- **webchat**:输出文字摘要和文件路径 `/tmp/lisa_{lisaDagTaskId前8位}.xlsx`
**第四步:展示结果**
先展示统计摘要:
```
📱 Lisa 短信销售任务结果 — {taskName}
📊 发送统计:
📨 总发送:{totalCount} 条
✅ 触达成功:{successCount} 条
❌ 发送失败:{failCount} 条
```
再展示列表中前 5 条短信详情:
```
序号. 📱 {phoneNumber}
🔄 状态:{success中文} / {errCode}
📝 内容:{content前60字}
📅 发送:{sendTime}(回执 {reportTime})
```
超过 5 条附上:`...共 {total} 条短信记录,完整数据见 xlsx 文件`。
**情况:统计接口全为 0 或列表为空**
> Lisa 短信任务数据暂未就绪,可能仍在发送中或等待运营商回执。
> lisaDagTaskId:{lisaDagTaskId},lisaCustomerPoolId:{lisaCustomerPoolId}
> 你可以告诉我「再查Lisa结果」,我会立即重新查询。
---
#### Step 5-B:Frank 结果处理(仅当 employeeList 包含 Frank 且 frankDagTaskId 不为 null)
> ⚠️ Frank 的两个查询接口均使用 `frankDagTaskId`(来自提交响应 `data.employeeList` 中 `nodeType=FRANK` 的 `dagTaskId`),**不是** `taskId`。
**第一步:查询邮件统计**
接口:`GET https://ai.deepsop.com/prod-api/ai/email/getTaskEmailCount?taskId={frankDagTaskId}`
请求头:`x-api-key: $DEEPSOP_API_KEY`
返回字段说明:
- `taskSendEmailCount`:任务发送邮件总数
- `taskSuccessEmailCount`:发送成功数量
- `taskOpenEmailCount`:已读数量
- `taskReceiveEmailCount`:收到回复数量
- `taskClickEmailCount`:点击链接数量
**第二步:查询邮件列表**
接口:`GET https://ai.deepsop.com/prod-api/ai/email/taskList?pageNum=1&pageSize=2000&taskId={frankDagTaskId}`
请求头:`x-api-key: $DEEPSOP_API_KEY`
关键字段:
- `rows[].recipientEmailAddress`:收件人邮箱
- `rows[].companyName`:公司名称
- `rows[].personName`:联系人姓名
- `rows[].position`:职位
- `rows[].emailSubject`:邮件主题
- `rows[].sendTime`:发送时间
- `rows[].emailStatus`:发送状态(0=未发送,1=发送失败,2=发送成功)
- `rows[].round`:轮次
**第三步:生成 xlsx 文件**
将邮件列表 JSON 传给脚本生成 xlsx:
```bash
curl 结果存 /tmp/frank_{frankDagTaskId前8位}_raw.json
python3 ~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_emails.py "$(cat /tmp/frank_{frankDagTaskId前8位}_raw.json)" '/tmp/frank_{frankDagTaskId前8位}.xlsx'
```
根据当前 channel 必须发送文件,不得跳过:
- **飞书**:必须执行 `cp /tmp/frank_{frankDagTaskId前8位}.xlsx ~/.openclaw/workspace/frank_{frankDagTaskId前8位}.xlsx`,再用 `openclaw message send --channel feishu --target {feishuChatId} --media ~/.openclaw/workspace/frank_{frankDagTaskId前8位}.xlsx --message 'Frank 邮件发送完成!任务「{taskName}」共发送 {taskSendEmailCount} 封,详情见附件。'`
- **Telegram / WhatsApp**:必须执行,media 路径用 workspace 路径,message 同上
- **webchat**:输出文字摘要和文件路径 `/tmp/frank_{frankDagTaskId前8位}.xlsx`
**第四步:展示结果**
先展示统计摘要:
```
📧 Frank 邮件任务结果 — {taskName}
📊 发送统计:
📤 总发送:{taskSendEmailCount} 封
✅ 发送成功:{taskSuccessEmailCount} 封(emailStatus=2)
❌ 发送失败:{taskSendEmailCount - taskSuccessEmailCount} 封(emailStatus=1)
👁 已读:{taskOpenEmailCount} 封
💬 收到回复:{taskReceiveEmailCount} 封
🔗 点击链接:{taskClickEmailCount} 封
```
再展示前5条邮件发送详情:
```
序号. 📧 {emailSubject}
👤 收件人:{personName}({position})
🏢 公司:{companyName}
📮 邮箱:{recipientEmailAddress}
📅 发送时间:{sendTime}
状态:✅ 成功 / ❌ 失败
```
超过5条附上:`...共 {total} 封,如需完整列表请告知`
**情况:统计接口或列表接口返回非 200 / data 为空**
> Frank 邮件任务数据暂未就绪,可能仍在发送中。
> 任务ID:{taskId}
> 你可以告诉我「再查Frank结果」,我会立即重新查询。
#### Step 5-E:Toby 结果处理(仅当 employeeList 包含 Toby 且 tobyDagTaskId 不为 null)
**5-E-1:查询统计数据**
接口:`GET https://ai.deepsop.com/prod-api/ai/data/count?taskId={tobyDagTaskId}&customerPoolId={tobyCustomerPoolId}&platform=1`
请求头:`x-api-key: $DEEPSOP_API_KEY`
关键字段:
- `data.playCount`:总播放量
- `data.likeCount`:总点赞数
- `data.commentCount`:总评论数
- `data.shareCount`:总分享数
- `data.totalTiktokCount`:已发布视频数
**5-E-2:查询视频列表**
接口:`GET https://ai.deepsop.com/prod-api/ai/data/list?pageNum=1&pageSize=10&taskId={tobyDagTaskId}&customerPoolId={tobyCustomerPoolId}&platform=1`
请求头:`x-api-key: $DEEPSOP_API_KEY`
关键字段:
- `rows[].titleName`:视频标题
- `rows[].platformUrl`:TikTok 链接
- `rows[].url`:视频文件地址
- `rows[].playNum`:播放量
- `rows[].likesNum`:点赞数
- `rows[].commentNum`:评论数
- `rows[].transmitNum`:转发数
- `rows[].displayCreateTime`:发布时间
- `total`:列表总数
**5-E-3:回复结果摘要(在当前会话回复,不需发文件)**
格式:
```
🎥 Toby TikTok 视频发布结果
任务:{taskName}
� 数据概览:
发布视频数:{totalTiktokCount}
总播放量:{playCount}
总点赞数:{likeCount}
总评论数:{commentCount}
总分享数:{shareCount}
📋 视频明细(共 {total} 条):
1. 《{titleName}》
播放:{playNum} | 点赞:{likesNum} | 评论:{commentNum} | 转发:{transmitNum}
发布时间:{displayCreateTime}
TikTok 链接:{platformUrl}
2. ...
```
**情况:两个接口均返回非 200 或 data 为空**
> Toby TikTok 视频任务数据暂未就绪,可能仍在生成/发布中。
> 任务ID:{tobyDagTaskId}
> 你可以告诉我「再查Toby结果」,我会立即重新查询。
---
## 实现方式
- **AI 分析**:直接在当前对话中用 LLM 完成,分析时告知用户正在处理
- **HTTP 请求**:使用 `exec` 工具调用 `curl`
- **定时等待**:使用 `cron(action=add)` 设置 8 分钟后触发的 systemEvent
- **xlsx 生成**:使用 `exec` 调用 Python 脚本
---
## 依赖
- Python 3(系统自带)
- openpyxl:`python3 -m pip install openpyxl --user --break-system-packages`
- AiWa 生成脚本:`~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_customers.py`
- Frank 生成脚本:`~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_emails.py`
- Fran 生成脚本:`~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_calls.py`
- Lisa 生成脚本:`~/.openclaw/workspace/skills/deepsop-human-ai-collab/scripts/format_sms.py`
---
## 错误处理
- `DEEPSOP_API_KEY` 未设置:提示用户前往 https://ai.deepsop.com 注册登录后新建 API Key,配置环境变量后再使用
- POST 接口返回非 200:展示错误信息,提示检查参数或稍后重试
- AiWa GET 接口 data 为空:提示任务可能仍在执行,给出 taskId 供用户告知「再查一次」
- Frank 邮件统计/列表接口异常:提示邮件任务可能仍在发送中,给出 taskId 供用户告知「再查Frank结果」
- Frank / Fran / Lisa 单独出现(未与 AiWa 搭配):终止任务,提示用户补充客户挖掘需求
- Fran 外呼实例并发数为 0:终止任务,提示用户联系管理员开通并发资源
- Fran 号码池为空:终止任务,提示用户联系管理员开通外呼号码
- Frank 邮箱未绑定:终止任务,提示用户登录 https://ai.deepsop.com 前往「邮件配置」绑定邮箱
- Fran 场景库为空或无 `PUBLISHED` 状态:终止任务,提示用户前往 https://ai.deepsop.com 创建并发布场景库
- Lisa 短信模板为空或无 `AUDIT_STATE_PASS` 状态:终止任务,提示用户前往 https://ai.deepsop.com 创建并提交审核短信模板
- Lisa 变量校验失败:明确告知用户不符合的具体规则并要求重新填写,不中断整个流程
- Lisa 统计/详情接口异常或计数全为 0:提示短信任务可能仍在发送中,给出 taskId 和 lisaCustomerPoolId 供用户告知「再查Lisa结果」
- Fran 统计/详情接口异常或计数全为 0:提示电话任务可能仍在拨号中,给出 taskId 和 franCustomerPoolId 供用户告知「再查Fran结果」
- Python 脚本执行失败:直接以文字列表格式返回客户数据,不中断流程
- Toby TikTok 账号为空:终止任务,提示用户登录 https://ai.deepsop.com 添加 TikTok 授权账号
- Toby 视频模型列表为空:终止任务,提示用户联系管理员开通视频生成权限
- Toby 获取账号权限失败:提示用户重新授权该 TikTok 账号
- Toby 统计/列表接口异常或数据为空:提示视频任务可能仍在生成/发布中,给出 tobyDagTaskId 供用户告知「再查Toby结果」
- 数字员工禁用(status=1):终止任务,提示联系管理员启用该员工
- 数字员工使用天数耗尽(remainingDays≤0):终止任务,提示前往 https://ai.deepsop.com 购买/续费
- 不支持的员工(Jack/Leo/Sophia/Alex):终止任务,提示当前仅支持 AiWa、Frank、Fran、Lisa、Toby
- 网络请求失败:展示 curl 错误信息
FILE:scripts/format_calls.py
#!/usr/bin/env python3
"""
format_calls.py
将 Fran 电话任务 collaborationCallResult 返回的 JSON 数据生成 xlsx 文件
用法: python3 format_calls.py '<json_string>' '<output_path>'
输出: xlsx 文件路径
"""
import sys
import json
import os
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
STATUS_LABEL = {
'Succeeded': '通话成功',
'Failed': '呼叫失败',
'Answer': '有回复',
}
def parse_job(row):
"""从 describeJobJson 中解析通话详情,返回扁平化字典"""
result = {}
raw = row.get('describeJobJson') or ''
if not raw:
return result
try:
obj = json.loads(raw)
body = obj.get('body') or {}
job = body.get('job') or {}
tasks = job.get('tasks') or []
if not tasks:
return result
t = tasks[0]
contact = t.get('contact') or {}
result['contactName'] = contact.get('contactName', '')
result['contactPhone'] = contact.get('phoneNumber', '')
result['calledNumber'] = t.get('calledNumber', '')
result['callingNumber'] = t.get('callingNumber', '')
result['duration_s'] = round(t.get('duration', 0) / 1000, 1)
result['realRingingDuration'] = t.get('realRingingDuration', '')
result['endReason'] = t.get('endReason', '')
# 取第一条 Robot 的 script 作为对话摘要
conv = t.get('conversation') or []
for c in conv:
if c.get('speaker') == 'Robot' and c.get('script'):
result['dialogSummary'] = c['script'][:120]
break
else:
result['dialogSummary'] = ''
except Exception:
pass
return result
def main():
if len(sys.argv) < 3:
print("用法: python3 format_calls.py '<json_string>' '<output_path>'", file=sys.stderr)
sys.exit(1)
try:
raw = json.loads(sys.argv[1])
if isinstance(raw, dict):
data = raw.get('rows', raw.get('data', []))
else:
data = raw
except json.JSONDecodeError as e:
print(f'JSON 解析失败: {e}', file=sys.stderr)
sys.exit(1)
if not isinstance(data, list) or len(data) == 0:
print('无通话数据', file=sys.stderr)
sys.exit(1)
output_path = sys.argv[2]
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
# 表头定义: (显示名, 字段key, 列宽)
headers = [
('序号', 'index', 8),
('联系人', 'contactName', 14),
('被呼号码', 'contactPhone', 18),
('呼出号码', 'callingNumber', 18),
('公司名称', 'companyName', 30),
('姓名', 'personName', 14),
('呼叫状态', 'jobStatus', 14),
('任务状态', 'jobTaskStatus', 18),
('通话时长(s)', 'duration_s', 12),
('振铃时长(s)', 'realRingingDuration', 12),
('挂断原因', 'endReason', 16),
('对话摘要', 'dialogSummary', 60),
('创建时间', 'createTime', 20),
('更新时间', 'updateTime', 20),
]
wb = Workbook()
ws = wb.active
ws.title = 'Fran电话数据'
# 表头样式(绿色主题区别于 AiWa 蓝色 / Frank 橙色)
header_font = Font(name='微软雅黑', bold=True, color='FFFFFF', size=11)
header_fill = PatternFill(start_color='276749', end_color='276749', fill_type='solid')
header_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
for col_idx, (label, key, width) in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col_idx, value=label)
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_align
ws.column_dimensions[get_column_letter(col_idx)].width = width
ws.row_dimensions[1].height = 28
fill_success = PatternFill(start_color='C6F6D5', end_color='C6F6D5', fill_type='solid')
fill_fail = PatternFill(start_color='FED7D7', end_color='FED7D7', fill_type='solid')
alt_fill = PatternFill(start_color='F0FFF4', end_color='F0FFF4', fill_type='solid')
data_align = Alignment(vertical='center', wrap_text=False)
for row_idx, item in enumerate(data, start=2):
job_info = parse_job(item)
status = item.get('jobStatus', '')
for col_idx, (label, key, width) in enumerate(headers, start=1):
if key == 'index':
value = row_idx - 1
elif key == 'jobStatus':
value = STATUS_LABEL.get(status, status)
elif key in job_info:
value = job_info[key]
else:
value = item.get(key)
if value is None:
value = ''
cell = ws.cell(row=row_idx, column=col_idx, value=value)
cell.alignment = data_align
if status == 'Succeeded':
cell.fill = fill_success
elif status == 'Failed':
cell.fill = fill_fail
elif row_idx % 2 == 0:
cell.fill = alt_fill
ws.row_dimensions[row_idx].height = 20
ws.freeze_panes = 'A2'
ws.auto_filter.ref = ws.dimensions
wb.save(output_path)
print(output_path)
if __name__ == '__main__':
main()
FILE:scripts/format_customers.py
#!/usr/bin/env python3
"""
format_customers.py
将 AiWa 返回的客户 JSON 数据生成 xlsx 文件
用法: python3 format_customers.py '<json_string>' '<output_path>'
输出: xlsx 文件路径
"""
import sys
import json
import os
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
def main():
if len(sys.argv) < 3:
print('用法: python3 format_customers.py \'<json_string>\' \'<output_path>\'', file=sys.stderr)
sys.exit(1)
# 解析 JSON
try:
raw = json.loads(sys.argv[1])
if isinstance(raw, dict):
if 'rows' in raw:
data = raw['rows']
elif 'data' in raw:
inner = raw['data']
data = inner['rows'] if isinstance(inner, dict) and 'rows' in inner else inner
else:
data = raw
else:
data = raw
except json.JSONDecodeError as e:
print(f'JSON 解析失败: {e}', file=sys.stderr)
sys.exit(1)
if not isinstance(data, list) or len(data) == 0:
print('无客户数据', file=sys.stderr)
sys.exit(1)
output_path = sys.argv[2]
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
# 表头定义
headers = [
('序号', 'index', 8),
('联系人', 'personName', 14),
('职位', 'position', 14),
('公司名称', 'companyName', 40),
('标准行业', 'systemIndustryName', 18),
('国家', 'countryName', 16),
('网址', 'url', 28),
('公司官网', 'companyUrl', 28),
('公司规模', 'companySize', 12),
('公司电话', 'companyPhone', 16),
('公司营收', 'companyRevenue', 14),
('公司简介', 'companyIntroduction', 50),
('邮箱', 'email', 28),
('电话', 'phone', 16),
('WhatsApp', 'whatsapp', 18),
('LinkedIn', 'linkedin', 24),
('Facebook', 'facebook', 24),
('Instagram','instagram', 20),
('TikTok', 'tiktok', 16),
('Twitter', 'twitter', 16),
('YouTube', 'youtube', 16),
('Line', 'line', 14),
]
wb = Workbook()
ws = wb.active
ws.title = 'AiWa客户数据'
# 表头样式
header_font = Font(name='微软雅黑', bold=True, color='FFFFFF', size=11)
header_fill = PatternFill(start_color='2B6CB0', end_color='2B6CB0', fill_type='solid')
header_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
for col_idx, (label, key, width) in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col_idx, value=label)
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_align
ws.column_dimensions[get_column_letter(col_idx)].width = width
ws.row_dimensions[1].height = 28
# 数据行样式
data_align = Alignment(vertical='center', wrap_text=False)
alt_fill = PatternFill(start_color='EBF4FF', end_color='EBF4FF', fill_type='solid')
for row_idx, item in enumerate(data, start=2):
for col_idx, (label, key, width) in enumerate(headers, start=1):
if key == 'index':
value = row_idx - 1
else:
value = item.get(key)
if value is None:
value = ''
cell = ws.cell(row=row_idx, column=col_idx, value=value)
cell.alignment = data_align
if row_idx % 2 == 0:
cell.fill = alt_fill
ws.row_dimensions[row_idx].height = 20
# 冻结首行
ws.freeze_panes = 'A2'
# 自动筛选
ws.auto_filter.ref = ws.dimensions
wb.save(output_path)
print(output_path)
if __name__ == '__main__':
main()
FILE:scripts/format_emails.py
#!/usr/bin/env python3
"""
format_emails.py
将 Frank 返回的邮件任务 JSON 数据生成 xlsx 文件
用法: python3 format_emails.py '<json_string>' '<output_path>'
输出: xlsx 文件路径
"""
import sys
import json
import os
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
# emailStatus 映射
STATUS_MAP = {
0: '未发送',
1: '发送失败',
2: '发送成功',
}
def main():
if len(sys.argv) < 3:
print('用法: python3 format_emails.py \'<json_string>\' \'<output_path>\'', file=sys.stderr)
sys.exit(1)
# 解析 JSON
try:
raw = json.loads(sys.argv[1])
# 支持 {rows: [...]} 或直接数组
if isinstance(raw, dict):
data = raw.get('rows', raw.get('data', []))
else:
data = raw
except json.JSONDecodeError as e:
print(f'JSON 解析失败: {e}', file=sys.stderr)
sys.exit(1)
if not isinstance(data, list) or len(data) == 0:
print('无邮件数据', file=sys.stderr)
sys.exit(1)
output_path = sys.argv[2]
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
# 表头定义: (显示名, 字段key或特殊标记, 列宽)
headers = [
('序号', 'index', 8),
('收件人邮箱', 'recipientEmailAddress', 30),
('公司名称', 'companyName', 36),
('公司官网', 'companyUrl', 28),
('公司电话', 'companyPhone', 16),
('公司规模', 'companySize', 12),
('公司营收', 'companyRevenue', 14),
('公司简介', 'companyIntroduction', 50),
('联系人', 'personName', 14),
('职位', 'position', 14),
('联系电话', 'phone', 16),
('WhatsApp', 'whatsapp', 18),
('LinkedIn', 'linkedin', 24),
('邮件主题', 'emailSubject', 40),
('发送状态', 'emailStatus', 12),
('发送时间', 'sendTime', 20),
('轮次', 'round', 8),
('错误信息', 'errMsg', 30),
]
wb = Workbook()
ws = wb.active
ws.title = 'Frank邮件数据'
# 表头样式(橙色主题区别于 AiWa 蓝色)
header_font = Font(name='微软雅黑', bold=True, color='FFFFFF', size=11)
header_fill = PatternFill(start_color='C05621', end_color='C05621', fill_type='solid')
header_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
for col_idx, (label, key, width) in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col_idx, value=label)
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_align
ws.column_dimensions[get_column_letter(col_idx)].width = width
ws.row_dimensions[1].height = 28
# 状态颜色
fill_success = PatternFill(start_color='C6F6D5', end_color='C6F6D5', fill_type='solid')
fill_fail = PatternFill(start_color='FED7D7', end_color='FED7D7', fill_type='solid')
fill_pending = PatternFill(start_color='FEFCBF', end_color='FEFCBF', fill_type='solid')
alt_fill = PatternFill(start_color='FFF5EB', end_color='FFF5EB', fill_type='solid')
data_align = Alignment(vertical='center', wrap_text=False)
for row_idx, item in enumerate(data, start=2):
email_status = item.get('emailStatus')
for col_idx, (label, key, width) in enumerate(headers, start=1):
if key == 'index':
value = row_idx - 1
elif key == 'emailStatus':
value = STATUS_MAP.get(email_status, str(email_status) if email_status is not None else '')
elif key == 'personName':
# 优先取顶层,再取 taskCustomer
value = item.get('personName') or (item.get('taskCustomer') or {}).get('personName') or ''
elif key == 'position':
value = item.get('position') or (item.get('taskCustomer') or {}).get('position') or ''
elif key == 'companyName':
value = item.get('companyName') or (item.get('taskCustomer') or {}).get('companyName') or ''
elif key in ('companyUrl', 'companyPhone', 'companySize', 'companyRevenue', 'companyIntroduction'):
value = item.get(key) or (item.get('taskCustomer') or {}).get(key) or ''
elif key in ('phone', 'whatsapp', 'linkedin'):
value = item.get(key) or (item.get('taskCustomer') or {}).get(key) or ''
else:
value = item.get(key)
if value is None:
value = ''
cell = ws.cell(row=row_idx, column=col_idx, value=value)
cell.alignment = data_align
# 按状态着色整行
if email_status == 2:
cell.fill = fill_success
elif email_status == 1:
cell.fill = fill_fail
elif email_status == 0:
cell.fill = fill_pending
elif row_idx % 2 == 0:
cell.fill = alt_fill
ws.row_dimensions[row_idx].height = 20
# 冻结首行
ws.freeze_panes = 'A2'
# 自动筛选
ws.auto_filter.ref = ws.dimensions
wb.save(output_path)
print(output_path)
if __name__ == '__main__':
main()
FILE:scripts/format_sms.py
#!/usr/bin/env python3
"""
format_sms.py
将 Lisa 短信任务 getSmsResultList 返回的 JSON 数据生成 xlsx 文件
用法: python3 format_sms.py '<json_string>' '<output_path>'
输出: xlsx 文件路径
"""
import sys
import json
import os
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment
from openpyxl.utils import get_column_letter
STATUS_LABEL = {
1: '发送成功',
0: '发送失败',
}
def main():
if len(sys.argv) < 3:
print("用法: python3 format_sms.py '<json_string>' '<output_path>'", file=sys.stderr)
sys.exit(1)
try:
raw = json.loads(sys.argv[1])
if isinstance(raw, dict):
data_root = raw.get('data', raw)
data = data_root.get('rows', data_root) if isinstance(data_root, dict) else data_root
else:
data = raw
except json.JSONDecodeError as e:
print(f'JSON 解析失败: {e}', file=sys.stderr)
sys.exit(1)
if not isinstance(data, list) or len(data) == 0:
print('无短信数据', file=sys.stderr)
sys.exit(1)
output_path = sys.argv[2]
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
# 表头定义: (显示名, 字段key, 列宽)
headers = [
('序号', 'index', 8),
('手机号码', 'phoneNumber', 18),
('发送状态', 'success', 14),
('状态描述', 'errMsg', 20),
('状态码', 'errCode', 20),
('短信内容', 'content', 60),
('短信条数', 'smsSize', 10),
('发送时间', 'sendTime', 20),
('回执时间', 'reportTime', 20),
('业务ID', 'bizId', 32),
]
wb = Workbook()
ws = wb.active
ws.title = 'Lisa短信数据'
# 表头样式(紫色主题)
header_font = Font(name='微软雅黑', bold=True, color='FFFFFF', size=11)
header_fill = PatternFill(start_color='553C9A', end_color='553C9A', fill_type='solid')
header_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
for col_idx, (label, key, width) in enumerate(headers, start=1):
cell = ws.cell(row=1, column=col_idx, value=label)
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_align
ws.column_dimensions[get_column_letter(col_idx)].width = width
ws.row_dimensions[1].height = 28
fill_success = PatternFill(start_color='E9D8FD', end_color='E9D8FD', fill_type='solid')
fill_fail = PatternFill(start_color='FED7D7', end_color='FED7D7', fill_type='solid')
alt_fill = PatternFill(start_color='F5F0FF', end_color='F5F0FF', fill_type='solid')
data_align = Alignment(vertical='center', wrap_text=False)
for row_idx, item in enumerate(data, start=2):
status_raw = item.get('success')
for col_idx, (label, key, width) in enumerate(headers, start=1):
if key == 'index':
value = row_idx - 1
elif key == 'success':
value = STATUS_LABEL.get(status_raw, str(status_raw) if status_raw is not None else '')
else:
value = item.get(key)
if value is None:
value = ''
cell = ws.cell(row=row_idx, column=col_idx, value=value)
cell.alignment = data_align
if status_raw == 1:
cell.fill = fill_success
elif status_raw == 0:
cell.fill = fill_fail
elif row_idx % 2 == 0:
cell.fill = alt_fill
ws.row_dimensions[row_idx].height = 20
ws.freeze_panes = 'A2'
ws.auto_filter.ref = ws.dimensions
wb.save(output_path)
print(output_path)
if __name__ == '__main__':
main()
AI 图片与视频异步生成技能,调用 AI Artist API 根据文本提示词生成图片或视频,自动轮询直到任务完成。 ⚠️ 使用前必须设置环境变量 AI_ARTIST_TOKEN 为你自己的 API Key! 获取 API Key:访问 https://ai.deepsop.com/ 注册登录后创建。 支持图片模...
---
name: ai-image-generator
description: |
AI 图片与视频异步生成技能,调用 AI Artist API 根据文本提示词生成图片或视频,自动轮询直到任务完成。
⚠️ 使用前必须设置环境变量 AI_ARTIST_TOKEN 为你自己的 API Key!
获取 API Key:访问 https://ai.deepsop.com/ 注册登录后创建。
支持图片模型:DeepSop系列图片模型(S4.5、S5.0L、N1、N2系列、W2.7系列等,共11个模型)。
支持视频模型:DeepSop系列视频模型(S1.5Pro、Sora2系列、Veo3.1系列、Wan2.6/Wan2.7系列、Kling V3 Omni等,共15个模型)。
触发场景:
- 用户要求生成图片,如"生成一匹狼"、"画一只猫"、"风景画"、"帮我画"等。
- 用户要求生成视频,如"生成视频"、"文生视频"、"图生视频"、"生成一段...的视频"等。
- 用户指定具体模型(详见下方模型列表)。
- 用户上传参考图/参考视频时,自动先调用文件上传 API 转换为可访问 URL。
---
# AI Image Generator
异步生成 AI 图片与视频的技能。
## ⚠️ 首次使用必读
### 1. 获取 API Key
访问 [https://ai.deepsop.com/](https://ai.deepsop.com/) 注册并登录,然后创建你的 API Key。
### 2. 设置环境变量
**在使用前,你必须先设置自己的 API Key:**
```bash
# Linux/macOS/Git Bash (Windows)
export AI_ARTIST_TOKEN="sk-your_api_key_here"
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
```
### 3. 验证配置
**验证配置是否正确:**
```bash
python3 scripts/test_config.py
```
详细配置说明请查看下方"环境配置"章节。
## 快速开始
```bash
# 图片生成(默认 DeepSop·3.1Nano2-Evo)
python3 scripts/generate_image.py "一只可爱的猫"
# 视频生成(默认 DeepSop·V3.1FB)
python3 scripts/generate_video.py "海边日落风景"
```
## 参考图/视频上传流程
当用户提供本地文件作为参考图或参考视频时,需要先调用文件上传 API 转换为可访问的 URL:
### 文件上传 API
```bash
curl --location --request POST 'https://ai.deepsop.com/prod-api/system/fileUpload/upload' \
--header 'x-api-key: sk-your_api_key_here' \
--form 'file=@"C:\\Users\\admin\\Downloads\\image.png"'
```
**返回结果:**
```json
{
"msg": "操作成功",
"fileName": "image.png",
"code": 200,
"url": "https://kocgo-ai-sales-test.oss-cn-hangzhou.aliyuncs.com/material/100/xxx.png"
}
```
### 使用上传后的 URL
获取到 `url` 后,可作为 `firstImageUrl`、`lastImageUrl`、`imageUrlList`、`videoUrlList` 或 `elementList `等参数传入生成接口。
## 在对话中直接返回图片/视频
### 方式 1: Markdown 语法(推荐)
生成图片后,直接在回复中使用 Markdown 语法:
```markdown


```
**平台支持情况:**
- ✅ WebChat、Discord、Telegram:完全支持
- ✅ 飞书:支持(需公开 URL)
- ❌ WhatsApp:不支持
### 方式 2: 下载后发送(需要 message 工具)
使用 `--download` 参数下载媒体文件,然后通过 message 工具发送:
```bash
python3 scripts/generate_image.py "风景画" --download
python3 scripts/generate_video.py "海边" --download
```
比如图片生成接着在代码中读取图片并发送:
```python
from scripts.generate_image import generate_image
import base64
result = generate_image(prompt="风景画", download=True)
if result and result["status"] == "SUCCESS":
# 方式 A: 使用 data URI
image_uri = result["data_uri"] # data:image/png;base64,...
# 方式 B: 读取本地文件
with open(result["local_path"], "rb") as f:
image_data = f.read()
base64_data = base64.b64encode(image_data).decode()
```
## 参数说明
### 通用参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `prompt` | 必填 | 生成提示词(图片或视频描述)|
| `--model` | 图片: `DeepSop·3.1Nano2-Evo` / 视频: `DeepSop·V3.1FB` | 生成模型(详见下方模型列表) |
| `--interval` | `5` | 轮询间隔(秒) |
| `--download` | - | 下载媒体文件到本地 |
| `--output-dir` | `workspace/images`(图片) / `workspace/videos`(视频) | 文件保存目录 |
### 图片专属参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--quality` | 按模型自动匹配 | 图片质量:`1K`、`2K`、`3K`、`4K`(具体支持见下方模型能力表) |
| `--size` | 按模型自动匹配 | 图片比例:`1:1`、`3:4`、`4:3`、`16:9`、`9:16`、`2:3`、`3:2`、`4:5`、`5:4`、`1:4`、`4:1`、`1:8`、`8:1`、`21:9`、`auto`(具体支持见下方模型能力表) |
| `--download` | - | 下载图片到本地 |
| `--output-dir` | `workspace/images` | 图片保存目录 |
| `--markdown-output` | - | 以 Markdown 格式输出图片链接 |
| `--reference-image` | - | 参考图本地路径,自动上传后作为 image-to-image 参考 |
| `--web-search` | - | 开启联网搜索(仅 S5.0L 和 Nano2-Evo 支持) |
### 视频专属参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--generation-type` | `TEXT` | 生成类型:`TEXT`(文生视频)、`FIRST&LAST`(首尾帧生视频)、`REFERENCE`(参考图生视频)、`CONTINUATION`(视频续写)、`EDIT`(视频编辑)、`FEATURE`(参考视频生视频) |
| `--ratio` | 按模型自动匹配 | 画面比例(具体支持见下方模型能力表) |
| `--resolution` | 按模型自动匹配 | 视频分辨率:`480p`、`720p`、`1080p`、`2K`、`4K`(具体支持见下方模型能力表) |
| `--duration` | 按模型自动匹配 | 视频时长(秒),不同模型支持范围不同 |
| `--mode` | `std` | 生成模式:std(标准模式)、pro(专家模式/高品质)(仅 Kling V3 Omni 支持) |
| `--first-image-url` | - | 首帧参考图 URL |
| `--last-image-url` | - | 尾帧参考图 URL |
| `--first-image` | - | 首帧参考图本地路径,自动上传后转换为 URL |
| `--last-image` | - | 尾帧参考图本地路径,自动上传后转换为 URL |
| `--first-clip-url` | - | 续写/编辑参考视频 URL |
| `--first-clip` | - | 续写/编辑参考视频本地路径,自动上传后转换为 URL |
| `--image-url-list` | - | 参考图片 URL 列表(用于参考图生视频) |
| `--video-url-list` | - | 参考视频 URL 列表(用于 R2V 模型) |
| `--element-list` | - | 参考主体 URL 列表(用于 Kling V3 Omni) |
| `--generate-audio` | - | 开启音频生成(按模型能力生效) |
| `--no-audio` | - | 关闭音频生成(按模型能力生效) |
| `--keep-original-sound` | - | 保留视频原声(仅 Kling V3 Omni) |
| `--prompt-extend` | - | 开启智能提示词改写(Wan系列支持)
| `--enhance-prompt` | - | 开启提示词翻译成英文(Veo3.1系列支持)
| `--negative-prompt` | - | 负向提示词(Veo3.1 Fast/Pro、Wan系列支持)
| `--shot-type` | `single` | 镜头模式:`single`(单镜头)、`multi`(智能分镜)、`customize`(自定义分镜)
| `--duration-switch` | - | 时长模式开关(仅 S1.5Pro)
| `--person-generation` | `allow_adult` | 是否允许生成人物:`allow_adult`、`dont_allow`(仅 Veo3.1 Fast/Pro)
| `--resize-mode` | `pad` | 图像缩放模式:`pad`(调整图片)、`crop`(裁剪图片)(仅 Veo3.1 Fast/Pro)
| `--multi-shot` | - | 是否多镜头(仅 Kling V3 Omni)
| `--n` | `1` | 生成视频数量(仅 Veo3.1 Fast/Pro)
| `--audio-url` | - | 参考音频 URL(Wan系列 T2V/I2V 支持)
## 支持的模型
### 图片模型
| 模型 | methodType | 支持质量 | 支持比例 | 联网搜索 | 特点 |
|------|-----------|---------|------|
| `S4.5` | `0` | 2K, 4K | 除 auto 外所有比例 | ❌ | 电影级画质4K,角色一致性
| `N1` | `1` | 1K | 除 21:9、4:5、5:4、1:4、4:1、1:8、8:1 外 | ❌ | 支持多模态输入,精细参数调节
| `N2` | `2` | 1K, 2K, 4K | 所有比例 | ❌ | 卓越的文字渲染和角色一致性
| `N2-147` | `3` | 1K, 2K, 4K | 除 auto、1:4、4:1、1:8、8:1 外 | ❌ | 147版本,支持多模态输入
| `S5.0L` | `4` | 2K, 3K | 除 auto 外所有比例 | ✅ | 默认模型,生成快、风格全、易用
| `N2-Pro` | `5` | 1K, 2K, 4K | 除 auto、1:4、4:1、1:8、8:1 外 | ✅ | Pro版本,画质细节更优
| `W2.7` | `6` | 1K, 2K | 除 auto、21:9 外 | ❌ | 画质清晰,细节丰富
| `W2.7Pro` | `7` | 1K, 2K | 除 auto、21:9 外 | ❌ | 精准控图与风格迁移
| `N2-Evo` | `8` | 1K, 2K, 4K | 所有比例 | ✅ | Evo版本,卓越的文字渲染
| `N2-Beta` | `9` | 1K, 2K, 4K | 所有比例 | ❌ | Beta测试版
| `Auto` | `auto` | 2K 除 auto、1:4、4:1、1:8、8:1、21:9 外 | ❌ | 自动选择最佳模型
### 视频模型
| 模型名称 | methodType | 支持生成类型 | 支持比例 | 支持分辨率 | 时长范围 | 特殊能力 |
|---------|-----------|------------|---------|-----------|---------|---------|
| `S1.5Pro` | `2` | TEXT, FIRST&LAST | 1:1, 3:4, 4:3, 16:9, 9:16, 21:9, adaptive | 480p, 720p, 1080p | 4-12s | 影视级叙事,支持音频生成、时长模式 |
| `Sora2 Beta` | `1` | TEXT, FIRST&LAST | 16:9, 9:16 | 720p | 10-15s | Beta版本 |
| `Sora2` | `11` | TEXT, FIRST&LAST | 16:9, 9:16 | 720p | 4-12s | 基础版本 |
| `Sora2 Pro` | `12` | TEXT, FIRST&LAST | 16:9, 9:16, 7:4, 4:7 | 720p, 2K | 4-12s | Pro版本 |
| `V3.1FB` | `3` | TEXT, FIRST&LAST, REFERENCE | 16:9, 9:16, adaptive | 720p, 1080p, 4K | 8s | 快速轻量版,支持提示词翻译 |
| `V3.1PB` | `4` | TEXT, FIRST&LAST, REFERENCE | 16:9, 9:16, adaptive | 720p, 1080p, 4K | 8s | 专业轻量版,多图参考 |
| `V3.1Fast` | `5` | TEXT, FIRST&LAST | 16:9, 9:16, adaptive | 720p, 1080p, 4K | 4s, 8s | 快速版,支持音画同步 |
| `V3.1Pro` | `6` | TEXT, FIRST&LAST | 16:9, 9:16, adaptive | 720p, 1080p, 4K | 4s, 8s | 专业版,4K超清,商业级 |
| `W2.6t` | `7` | TEXT | 1:1, 3:4, 4:3, 16:9, 9:16 | 720p, 1080p | 3-15s | 文生视频,支持音频、提示词改写 |
| `W2.6i` | `8` | FIRST&LAST | 固定 | 720p, 1080p | 3-15s | 首帧图生视频,比例由图片决定 |
| `W2.6r` | `9` | REFERENCE | 1:1, 3:4, 4:3, 16:9, 9:16 | 720p, 1080p | 3-10s | 参考视频生视频 |
| `W2.7i` | `14` | FIRST&LAST, CONTINUATION | 固定 | 720p, 1080p | 3-15s | 首帧图生视频,支持续写 |
| `W2.7t` | `15` | TEXT | 1:1, 3:4, 4:3, 16:9, 9:16 | 720p, 1080p | 3-15s | 文生视频,支持音频、提示词改写 |
| `W2.7r` | `16` | REFERENCE | 1:1, 3:4, 4:3, 16:9, 9:16 | 720p, 1080p | 3-15s(无视频引用)<br>3-10s(有视频引用) | 参考视频生视频 |
| `Kling V3 Omni` | `10` | TEXT, FIRST&LAST, REFERENCE, EDIT, FEATURE | 1:1, 16:9, 9:16 | 720p, 1080p | 3-15s | 全能模型,支持主体参考、多镜头 |
| `Auto` | `auto` | FIRST&LAST | 16:9, 9:16 | 720p | 4-12s | 自动选择最佳模型 |
**VEO3.1 系列(V3.1FB、V3.1PB、V3.1Fast、V3.1Pro)共同说明:**
| 模型名称 | 支持特性 |
|---------|-----------|
| `V3.1FB` / `V3.1PB` | 支持 `--enhance-prompt`(提示词翻译成英文) |
| `V3.1Fast` / `V3.1Pro` | 支持 `--n`、`--person-generation`、`--resize-mode`、`--negative-prompt`、`--enhance-prompt`、`--generate-audio` |
**WAN2.6 系列共同说明:**
| 模型名称 | 支持特性 |
|---------|-----------|
| `W2.6t` / `W2.7t` | 文生视频,支持 `--audio-url`(自定义音频) |
| `W2.6i` / `W2.7i` | 首帧图生视频,不支持 `--ratio` 参数(比例由首帧图决定),W2.7i 支持 `--first-clip-url`(续写) |
| `W2.6r` / `W2.7r` | 参考视频生视频,支持 `--video-url-list`(参考视频列表),W2.7r 时长根据是否有视频引用动态变化 |
| 全系列 | 支持 `--prompt-extend`(智能提示词改写)、`--negative-prompt`(负向提示词) |
**Kling V3 Omni 特有能力:**
| 能力 | 说明 |
|---------|-----------|
| `--element-list` | 参考主体选择 |
| `--keep-original-sound` | 保留视频原声 |
| `--mode` | 生成模式(std/pro) |
| `--multi-shot` | 是否多镜头 |
| `--shot-type` | 镜头模式(single/multi/customize) |
| `--generate-audio` | 生成声音 |
| 不支持 `--resolution` | 分辨率固定 |
## 参数联动规则(自动处理)
**图片质量按模型自动过滤**
| model | 支持质量 |
|-------|---------|
| Auto | 2K |
| S4.5 (`0`) | 2K, 4K |
| N1 (`1`) | 1K |
| N2 (`2`)、N2-147 (`3`)、N2-Pro (`5`)、N2-Evo (`8`)、N2-Beta (`9`) | 1K, 2K, 4K |
| S5.0L (`4`) | 2K, 3K |
| W2.7 (`6`)、W2.7Pro (`7`) | 1K, 2K |
**图片比例按模型自动过滤**
| model | 排除比例 |
|-------|---------|
| Auto | `auto`、`1:4`、`4:1`、`1:8`、`8:1`、`21:9` |
| S4.5 (`0`)、S5.0L (`4`) | `auto` |
| N1 (`1`) | `21:9`、`4:5`、`5:4`、`1:4`、`4:1`、`1:8`、`8:1` |
| N2-147 (`3`)、N2-Pro (`5`) | `auto`、`1:4`、`4:1`、`1:8`、`8:1` |
| W2.7 (`6`)、W2.7Pro (`7`) | `auto`、`21:9` |
| N2 (`2`)、N2-Evo (`8`)、N2-Beta (`9`) | 无(支持所有比例) |
**视频生成类型按模型自动过滤**
| model | 支持生成类型 |
|-------|------------|
| Auto | FIRST&LAST |
| Sora2 Beta (`1`)、S1.5Pro (`2`)、V3.1PB (`4`)、V3.1Fast (`5`)、V3.1Pro (`6`)、Sora2 (`11`)、Sora2 Pro (`12`) | TEXT, FIRST&LAST |
| W2.6t (`7`)、W2.7t (`15`) | TEXT |
| W2.6i (`8`) | FIRST&LAST |
| W2.7i (`14`) | FIRST&LAST, CONTINUATION |
| W2.6r (`9`)、W2.7r (`16`) | REFERENCE |
| Kling V3 Omni (`10`) | TEXT, FIRST&LAST, REFERENCE, EDIT, FEATURE |
| V3.1FB (`3`) | TEXT, FIRST&LAST, REFERENCE |
**视频分辨率按模型自动过滤**
| model | 支持分辨率 |
|-------|-----------|
| Auto、Sora2 Beta (`1`)、Sora2 (`11`) | 720p |
| S1.5Pro (`2`) | 480p, 720p, 1080p |
| V3.1FB (`3`)、V3.1PB (`4`)、V3.1Fast (`5`)、V3.1Pro (`6`) | 720p, 1080p, 4K |
| W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`)、Kling V3 Omni (`10`)、W2.7i (`14`)、W2.7t (`15`)、W2.7r (`16`) | 720p, 1080p |
| Sora2 Pro (`12`) | 720p, 2K |
**视频比例按模型自动过滤**
| model | 支持比例 |
|-------|---------|
| Auto、Sora2 Beta (`1`) | `16:9`, `9:16` |
| S1.5Pro (`2`) | `1:1`, `3:4`, `4:3`, `16:9`, `9:16`, `21:9`, `adaptive` |
| V3.1FB (`3`)、V3.1PB (`4`)、V3.1Fast (`5`)、V3.1Pro (`6`) | `16:9`, `9:16`, `adaptive` |
| Kling V3 Omni (`10`) | `1:1`, `16:9`, `9:16` |
| W2.6t (`7`)、W2.6r (`9`)、W2.7t (`15`)、W2.7r (`16`) | `1:1`, `3:4`, `4:3`, `16:9`, `9:16` |
| W2.6i (`8`)、W2.7i (`14`) | 固定(由首帧图比例决定) |
| Sora2 Pro (`12`) | `16:9`, `9:16`, `7:4`, `4:7` |
**视频时长按模型自动配置**
| model | 时长范围 | 可选档位 |
|-------|---------|---------|
| Sora2 Beta (`1`) | 5-15s | `10s`、`15s` |
| V3.1FB (`3`)、V3.1PB (`4`) | 8s(固定) | `8s` |
| V3.1Fast (`5`)、V3.1Pro (`6`) | 4-8s | `4s`、`8s` |
| W2.6t (`7`)、W2.6i (`8`)、Kling V3 Omni (`10`)、W2.7i (`14`)、W2.7t (`15`) | 3-15s | `3s`、`15s` |
| W2.6r (`9`) | 3-10s | `3s`、`10s` |
| W2.7r (`16`) | 3-15s(无视频引用)<br>3-10s(有视频引用) | `3s`、`10s` 或 `3s`、`15s` |
| Sora2 (`11`)、Sora2 Pro (`12`) | 4-12s | `4s`、`12s` |
| S1.5Pro (`2`)、Auto | 4-12s | `4s`、`12s` |
**镜头模式按模型自动过滤**
| model | 支持镜头模式 |
|-------|------------|
| W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`) | `single`、`multi` |
| Kling V3 Omni (`10`) | `single`、`multi`、`customize` |
| 其他 | `single`(默认) |
## 参数显隐逻辑(自动处理)
**按模型显示的参数**
| 参数 | 支持的 model (methodType) |
|------|-------------------------|
| `web_search`(联网搜索) | S5.0L (`4`)、N2-Evo (`8`) |
| `audio_url`(参考音频) | W2.6t (`7`)、W2.6i (`8`)、W2.7i (`14`)、W2.7t (`15`)、W2.7r (`16`) |
| `prompt_extend`(智能改写) | W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`)、W2.7i (`14`)、W2.7t (`15`)、W2.7r (`16`) |
| `first_clip_url`(续写视频) | Kling V3 Omni (`10`)、W2.7i (`14`) |
| `keep_original_sound`(保留原声) | Kling V3 Omni (`10`) |
| `element_list`(参考主体) | Kling V3 Omni (`10`) |
| `video_url_list`(参考视频) | W2.6r (`9`)、W2.7r (`16`) |
| `mode`(生成模式) | Kling V3 Omni (`10`) |
| `duration_switch`(时长模式) | S1.5Pro (`2`) |
| `generate_audio`(生成声音) | S1.5Pro (`2`)、V3.1Fast (`5`)、V3.1Pro (`6`)、Kling V3 Omni (`10`) |
| `enhance_prompt`(翻译英文) | V3.1FB (`3`)、V3.1PB (`4`)、V3.1Fast (`5`)、V3.1Pro (`6`) |
| `n`(生成数量) | V3.1Fast (`5`)、V3.1Pro (`6`) |
| `person_generation`(人物生成) | V3.1Fast (`5`)、V3.1Pro (`6`) |
| `resize_mode`(缩放模式) | V3.1Fast (`5`)、V3.1Pro (`6`) |
| `negative_prompt`(负向提示词) | V3.1Fast (`5`)、V3.1Pro (`6`)、W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`)、W2.7i (`14`)、W2.7t (`15`)、W2.7r (`16`) |
| `multi_shot`(多镜头) | Kling V3 Omni (`10`) |
| `shot_type`(镜头模式) | W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`)、Kling V3 Omni (`10`) |
**按模型隐藏的参数**
| 参数 | 不支持该参数的 model |
|------|---------------------|
| `last_image_url`(尾帧图片) | Auto、Sora2 Beta (`1`)、W2.6i (`8`)、Sora2 (`11`)、Sora2 Pro (`12`) |
| `ratio`(生成比例) | W2.6i (`8`)、W2.7i (`14`) |
| `resolution`(分辨率) | Kling V3 Omni (`10`) |
| `duration`(时长) | Auto |
**参数联动显隐(同模型下受其他参数影响)**
| 参数 | 依赖参数 | 显示条件 |
|------|---------|---------|
| `text`(提示词) | `shot_type` | `shot_type` = `'customize'` |
| `multi_prompt`(多镜头内容) | `shot_type` | `shot_type` = `'customize'` |
| `image_url_list`(参考图片) | `generation_type` | `generation_type` 为 REFERENCE、EDIT、FEATURE |
| `first_image_url`(首帧图) | `generation_type` | `generation_type` = FIRST&LAST |
| `last_image_url`(尾帧图) | `generation_type` | `generation_type` = FIRST&LAST |
| `first_clip_url`(续写视频) | `generation_type` | `generation_type` 为 CONTINUATION、EDIT、FEATURE |
| `keep_original_sound`(保留原声) | `first_clip_url` | `first_clip_url` 有值 |
| `element_list`(参考主体) | `generation_type` | `generation_type` ≠ TEXT |
| `ratio`(比例) | `generation_type` | Kling V3 Omni 除外:`generation_type` ≠ FIRST&LAST 且 ≠ EDIT |
| `duration`(时长) | `duration_switch` | `duration_switch` = `'1'` |
## 使用示例
**图片生成**
```bash
# 基础用法 - 默认模型 DeepSop·3.1Nano2-Evo
python3 scripts/generate_image.py "一匹狼"
# 指定质量
python3 scripts/generate_image.py "风景画" --quality "4K"
# 指定比例
python3 scripts/generate_image.py "风景画" --ratio "16:9"
# 使用 N2 模型
python3 scripts/generate_image.py "生成一只狗" --model N2
# 使用 N2-Pro 并开启联网搜索
python3 scripts/generate_image.py "2024年流行的装修风格" --model N2-Pro --web-search
# 使用 W2.7Pro
python3 scripts/generate_image.py "山水画" --model W2.7Pro --quality "2K" --ratio "9:16"
# 使用 N2-Evo
python3 scripts/generate_image.py "赛博朋克城市" --model N2-Evo --quality "4K" --ratio "16:9"
# 下载图片到本地
python3 scripts/generate_image.py "风景画" --download
# 直接输出 Markdown 图片链接
python3 scripts/generate_image.py "一只可爱的猫" --markdown-output
# 使用参考图生成
python3 scripts/generate_image.py "基于这张图生成变体" --reference-image "./reference.png"
```
**图片生成**
```bash
# 基础用法 - 默认 DeepSop·V3.1FB
python3 scripts/generate_video.py "海边日落风景"
# 指定比例和分辨率
python3 scripts/generate_video.py "海边日落风景" --ratio "9:16" --resolution "1080p"
# 指定时长
python3 scripts/generate_video.py "一只猫在玩耍" --duration 5
# 专家模式
python3 scripts/generate_video.py "海边日落风景" --mode pro
# 首尾帧生视频
python3 scripts/generate_video.py "花朵绽放" --generation-type FIRST&LAST --first-image "./flower_start.jpg" --last-image "./flower_end.jpg"
# 参考图生视频
python3 scripts/generate_video.py "产品展示" --generation-type REFERENCE --image-url-list "https://example.com/product1.jpg,https://example.com/product2.jpg"
# 视频续写
python3 scripts/generate_video.py "继续这个视频" --generation-type CONTINUATION --first-clip "./my_video.mp4" --duration 5
# Veo3.1 系列 - 文生视频
python3 scripts/generate_video.py "现代轻奢吊灯" --model V3.1FB --ratio "16:9" --duration 8
# Veo3.1 系列 - 首尾帧控制
python3 scripts/generate_video.py "灯具变形动画" --model V3.1Pro --first-image "./start.jpg" --last-image "./end.jpg" --duration 8
# Veo3.1 系列 - 负向提示词
python3 scripts/generate_video.py "人物奔跑" --model V3.1Pro --negative-prompt "模糊, 抖动" --duration 8
# Veo3.1Fast - 生成多个视频
python3 scripts/generate_video.py "产品广告" --model V3.1Fast --n 3 --duration 4
# W2.7t - 文生视频
python3 scripts/generate_video.py "现代轻奢吊灯宣传" --model W2.7t --ratio "16:9" --duration 10 --prompt-extend
# W2.7t - 带参考音频
python3 scripts/generate_video.py "产品展示" --model W2.7t --audio-url "https://example.com/audio.mp3" --duration 10
# W2.7i - 首帧图生视频
python3 scripts/generate_video.py "水晶灯展示" --model W2.7i --first-image "./lamp.jpg" --duration 8
# W2.7i - 视频续写
python3 scripts/generate_video.py "继续这个动画" --model W2.7i --first-image "./lamp.jpg" --first-clip "./lamp_animation.mp4" --duration 5
# W2.7r - 参考视频生视频
python3 scripts/generate_video.py "参考素材风格生成" --model W2.7r --video-url-list "https://example.com/video.mp4" --duration 10
# W2.7r - 多参考视频
python3 scripts/generate_video.py "风格迁移" --model W2.7r --video-url-list "https://example.com/style1.mp4,https://example.com/style2.mp4" --duration 8
# Kling V3 Omni - 多镜头分镜
python3 scripts/generate_video.py "电影预告片" --model "Kling V3 Omni" --shot-type multi --multi-shot --mode pro
# Kling V3 Omni - 参考主体
python3 scripts/generate_video.py "角色在行走" --model "Kling V3 Omni" --element-list "https://example.com/character.jpg"
# Kling V3 Omni - 保留原声的视频编辑
python3 scripts/generate_video.py "编辑这段视频" --model "Kling V3 Omni" --generation-type EDIT --first-clip "./original.mp4" --keep-original-sound
# Sora2 Pro - 高分辨率
python3 scripts/generate_video.py "风景大片" --model Sora2Pro --ratio "7:4" --resolution "2K" --duration 12
```
## 模型名称速查表
**图片模型(methodType → 模型名称)**
| methodType | 模型名称 | CLI 参数 |
|-----------|---------|---------|
| `0` | DeepSop·S4.5 | `S4.5` |
| `1` | DeepSop·N1 | `N1` |
| `2` | DeepSop·N2 | `N2` |
| `3` | DeepSop·3-Nano2-147 | `N2-147` |
| `4` | DeepSop·S5.0L | `S5.0L` |
| `5` | DeepSop·3.1Nano2-147 | `N2-Pro` |
| `6` | DeepSop.W2.7 | `W2.7` |
| `7` | DeepSop.W2.7Pro | `W2.7Pro` |
| `8` | DeepSop·3.1Nano2-Evo | `N2-Evo`(默认) |
| `9` | DeepSop·Nano2 Beta-Evo | `N2-Beta` |
| `auto` | DeepSop·Auto | `Auto` |
**视频模型(methodType → 模型名称)**
| methodType | 模型名称 | CLI 参数 |
|-----------|---------|---------|
| `1` | DeepSop·Sora2 Beta Max Evolink | `Sora2Beta` |
| `2` | DeepSop·S1.5Pro | `S1.5Pro` |
| `3` | DeepSop·V3.1FB | `V3.1FB`(默认) |
| `4` | DeepSop·V3.1PB | `V3.1PB` |
| `5` | DeepSop·V3.1Fast | `V3.1Fast` |
| `6` | DeepSop·V3.1Pro | `V3.1Pro` |
| `7` | DeepSop·W2.6t | `W2.6t` |
| `8` | DeepSop·W2.6i | `W2.6i` |
| `9` | DeepSop·W2.6r | `W2.6r` |
| `10` | DeepSop.klingV3Omni | `KlingV3Omni` |
| `11` | DeepSop·Sora2.147 | `Sora2` |
| `12` | DeepSop·Sora2 Pro.147 | `Sora2Pro` |
| `14` | DeepSop·W2.7i | `W2.7i` |
| `15` | DeepSop·W2.7t | `W2.7t` |
| `16` | DeepSop·W2.7r | `W2.7r` |
| `auto` | DeepSop·Auto | `Auto` |
## 程序化调用
```python
from scripts.generate_image import generate_image, generate_video
# 图片 - 默认 DeepSop·3.1Nano2-Evo
result = generate_image(prompt="一只可爱的猫咪")
# 图片 - N2 模型
result = generate_image(prompt="生成一只狗", model="N2")
# 图片 - 带联网搜索
result = generate_image(prompt="2024年流行的装修风格", model="N2-Pro", web_search=True)
# 图片 - 下载到本地
result = generate_image(prompt="风景画", model="S5.0L", download=True, output_dir="./images")
# 视频 - 默认 DeepSop·V3.1FB
result = generate_video(prompt="小骏马祝福大家新年快乐")
# 视频 - S1.5Pro 带音频
result = generate_video(
prompt="海边日落风景",
model="S1.5Pro",
ratio="9:16",
resolution="1080p",
duration=5,
generate_audio=True
)
# 视频 - V3.1Pro 首尾帧控制
result = generate_video(
prompt="灯具变形动画",
model="V3.1Pro",
first_image_url="https://example.com/start.jpg",
last_image_url="https://example.com/end.jpg",
ratio="16:9",
resolution="1080p",
duration=8
)
# 视频 - V3.1Fast 生成多个
result = generate_video(
prompt="产品广告",
model="V3.1Fast",
n=3,
duration=4,
person_generation="allow_adult"
)
# 视频 - W2.7t 带参考音频和提示词改写
result = generate_video(
prompt="产品宣传片",
model="W2.7t",
ratio="16:9",
resolution="1080p",
duration=10,
audio_url="https://example.com/music.mp3",
prompt_extend=True
)
# 视频 - W2.7r 多参考视频
result = generate_video(
prompt="风格迁移视频",
model="W2.7r",
video_url_list=["https://example.com/style1.mp4", "https://example.com/style2.mp4"],
ratio="16:9",
duration=10
)
# 视频 - Kling V3 Omni 多镜头模式
result = generate_video(
prompt="电影预告片",
model="KlingV3Omni",
generation_type="TEXT",
shot_type="multi",
multi_shot=True,
mode="pro"
)
# 视频 - Kling V3 Omni 参考主体
result = generate_video(
prompt="角色在奔跑",
model="KlingV3Omni",
generation_type="REFERENCE",
element_list=["https://example.com/character.jpg"],
keep_original_sound=False
)
if result and result["status"] == "SUCCESS":
print(f"媒体链接: {result['url']}")
print(f"本地路径: {result.get('local_path')}")
```
## 图像生成前处理与参数变动
### 模型切换时的自动参数调整
当用户切换生成模型时,系统会自动调整以下参数:
| 切换场景 | 自动调整规则 |
|---------|-------------|
| 切换到 N1 (methodType=1) | `quality` 自动设置为 `1K` |
| 切换到其他模型 | `quality` 自动设置为 `2K`(默认)|
| 切换到 S5.0L (methodType=4) | `web_search` 自动开启 |
| 切换到其他模型 | `web_search` 自动关闭 |
### 模型与尺寸/质量的关系
图片生成时,`size` 参数会根据 `methodType`、`quality` 和用户选择的 `ratio` 自动计算:
| 模型类型 | methodType | size 格式 | 计算公式 |
|---------|-----------|----------|---------|
| S4.5、S5.0L | 0, 4 | `{width}x{height}` | 根据 quality 和 ratio 解析宽高后拼接 |
| W2.7、W2.7Pro | 6, 7 | `{width}*{height}` | 根据 quality 和 ratio 解析宽高后用 `*` 拼接 |
| N1、N2 系列 | 1, 2, 3, 5, 8, 9 | 比例字符串 | 直接使用用户选择的 ratio 值(如 `16:9`)|
| Auto | auto | 比例字符串 | 直接使用用户选择的 ratio 值 |
### 生成前预处理参数
在调用生成 API 前,系统会自动添加以下限制参数:
| 参数 | 说明 | 来源 |
|------|------|------|
| `targetMaxSize` | 目标图片最大尺寸(字节)| 根据模型类型自动匹配 |
| `targetMinLength` | 提示词最小长度 | 根据模型类型自动匹配 |
| `targetMaxLength` | 提示词最大长度 | 根据模型类型自动匹配 |
## 图片生成限制参数说明
### 各模型的输入限制参数
根据选择的模型,系统会自动应用以下限制参数(`targetMaxSize`、`targetMinLength`、`targetMaxLength`):
| methodType | 模型名称 | maxSize (MB) | minLength (字) | maxLength (字) | maxQuantity (张) | 上传说明 |
|------------|---------|--------------|----------------|----------------|------------------|---------|
| auto | Auto | .jpeg,.jpg,.png,.webp | 10 | 2000 | 360 | 500 | - | 最长边≤2000px,最短边≥360px |
| 0 | S4.5 | .jpeg,.jpg,.png,.webp,.bmp,.tiff,.gif | 30 | 6000 | 300 | 500 | 14 | 宽高比 (0.4, 2.5),最多生成15张 |
| 1 | N1 | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 5 | 最长边≤6000px |
| 2 | N2 | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 10 | 最长边≤6000px,最多5张真人图像 |
| 3 | N2-147 | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 5 | 最长边≤6000px |
| 4 | S5.0L | .jpeg,.jpg,.png,.webp,.bmp,.tiff,.gif | 10 | 6000 | - | 300 | 14 | 宽高比 [1/16, 16],最多生成15张 |
| 5 | N2-Pro | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 5 | 最长边≤6000px |
| 6 | W2.7 | .jpeg,.jpg,.png,.bmp,.webp | 20 | 8000 | 240 | 2500 | 9 | 不支持透明通道,宽高比 [1:8, 8:1] |
| 7 | W2.7Pro | .jpeg,.jpg,.png,.bmp,.webp | 20 | 8000 | 240 | 2500 | 9 | 不支持透明通道,宽高比 [1:8, 8:1] |
| 8 | N2-Evo | .jpeg,.jpg,.png,.webp | 20 | 6000 | - | 1000 | 14 | 最长边≤6000px,最多4张真人图像 |
| 9 | N2-Beta | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 14 | 最长边≤6000px,最多4张真人图像 |
### 图片生成提示词长度限制
| methodType | 模型名称 | textLength (最大提示词字数) |
|------------|---------|---------------------------|
| 0 | S4.5 | 500 |
| 1,2,3,5,8,9 | N1/N2 系列 | 1000 |
| 4 | S5.0L | 300 |
| 6,7 | W2.7/W2.7Pro | 2500 |
| auto | Auto | 500 |
### 参数说明
| 参数 | 类型 | 说明 |
|------|------|------|
| `targetAccept` | string | 支持的图片文件格式 |
| `targetMaxSize` | int (MB) | 上传图片的最大文件大小限制 |
| `targetMaxLength` | int (px) | 图片最长边的最大像素限制 |
| `targetMinLength` | int (px) | 图片最短边的最小像素限制 |
| `targetTextLength` | int (字) | 提示词的最大长度限制 |
| `targetMaxQuantity` | int (张) | 参考图片的最大上传数量 |
| `targetUploadTips` | string | 上传说明和合规性提示 |
### 图片上传合规性要求
**通用要求:**
- 支持格式:JPEG、JPG、PNG、WEBP(部分模型支持 BMP、TIFF、GIF)
- 文件大小:根据模型不同,限制为 10MB-30MB
- 最长边限制:根据模型不同,限制为 2000px-8000px
**内容审查要求(Sora2/Veo 系列):**
1. 不得包含真人或拟真人图像
2. 提示词禁止暴力、色情、版权侵权或涉及名人信息
**Wan 系列特殊要求:**
1. 不支持透明通道(PNG 透明部分会被处理)
2. 宽高比必须在 [1:8, 8:1] 范围内
**Seedance 系列特殊要求:**
1. 宽高比(宽/高)必须在 (0.4, 2.5) 范围内
2. 上传图片最长边 ≤ 6000px,最短边 ≥ 300px
## 图片分辨率映射规则
### 质量等级与分辨率对照表
系统根据选择的 `quality`(图片质量)和 `ratio`(画面比例)自动计算输出图片的分辨率(宽 x 高)。
| 质量 | 1:1 | 16:9 | 9:16 | 3:4 | 4:3 | 2:3 | 3:2 | 4:5 | 5:4 | 1:4 | 4:1 | 1:8 | 8:1 | 21:9 |
|------|-----|------|------|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|------|
| **1K** | 1024x1024 | 1920x1080 | 1080x1920 | 768x1024 | 1024x768 | 682x1024 | 1024x682 | 1024x1280 | 1280x1024 | 512x2048 | 2048x512 | 362x2896 | 2896x362 | 2560x1080 |
| **2K** | 2048x2048 | 2560x1440 | 1440x2560 | 1728x2304 | 2304x1728 | 1664x2496 | 2496x1664 | 1843x2304 | 2304x1843 | 1024x4096 | 4096x1024 | 724x5792 | 5792x724 | 3584x1536 |
| **3K** | 3072x3072 | 4096x2304 | 2304x4096 | 2592x3456 | 3456x2592 | 2496x3744 | 3744x2496 | 2884x3605 | 3605x2884 | 1536x6144 | 6144x1536 | 1088x8704 | 8704x1088 | 4704x2016 |
| **4K** | 4096x4096 | 3840x2160 | 2160x3840 | 3072x4096 | 4096x3072 | 2730x4096 | 4096x2730 | 3277x4096 | 4096x3277 | 2048x8192 | 8192x2048 | 1448x11584 | 11584x1448 | 5040x2160 |
### 不同模型的分辨率格式
| 模型类型 | methodType | 输出格式 | 示例 |
|---------|-----------|---------|------|
| S4.5、S5.0L | 0, 4 | `{width}x{height}` | `2048x2048` |
| W2.7、W2.7Pro | 6, 7 | `{width}*{height}` | `2048*2048` |
| N1、N2 系列 | 1, 2, 3, 5, 8, 9 | 比例字符串 | `1:1`、`16:9` |
| Auto | auto | 比例字符串 | `1:1`、`16:9` |
### 分辨率计算示例
```python
from scripts.generate_image import get_image_resolution
# 获取 2K 质量、16:9 比例的分辨率
resolution = get_image_resolution(quality="2K", ratio="16:9")
print(resolution) # 输出: [2560, 1440]
# 获取 4K 质量、1:1 比例的分辨率
resolution = get_image_resolution(quality="4K", ratio="1:1")
print(resolution) # 输出: [4096, 4096]
# 仅获取质量对应的所有分辨率
resolutions = get_image_resolution(quality="2K")
print(resolutions) # 输出: {'1:1': [2048, 2048], '16:9': [2560, 1440], ...}
```
## 视频生成前处理与参数变动
### 模型切换时的自动参数调整
当用户切换视频模型时,系统会自动调整以下参数:
| 切换场景 | 自动调整规则 |
|---------|-------------|
| 切换到 W2.6t/W2.7t (methodType=7) | `generation_type` 自动设置为 `TEXT`(文生视频)|
| 切换到 W2.6r/W2.7r (methodType=9,16) | `generation_type` 自动设置为 `REFERENCE`(参考图/视频生视频)|
| 切换到 Kling V3 Omni (methodType=10) | `generation_type` 自动设置为 `REFERENCE` |
| 切换到其他模型 | `generation_type` 自动设置为 `FIRST&LAST`(首尾帧生视频)|
| 切换到 Kling V3 Omni (methodType=10) | `shot_type` 自动设置为 `multi`(智能分镜)|
| 切换到其他模型 | `shot_type` 自动设置为 `single`(单镜头)|
| 切换到 V3.1系列/Sora2系列 (3,4,5,6,11,12) | `duration` 自动设置为 `8` 秒 |
| 切换到其他视频模型 | `duration` 自动设置为 `10` 秒 |
### 镜头模式切换规则
当用户切换镜头模式时,系统会自动调整以下参数:
| 切换场景 | 自动调整规则 |
|---------|-------------|
| 切换到 Kling 多镜头模式(multi/customize) | `multi_shot` 自动设置为 `true` |
| 切换到 Kling 自定义多镜头(customize) | `text` 参数清空,`multi_prompt` 初始化为 `[{ index: 1, prompt: text, duration }]` |
| 切换到 Kling 智能分镜(multi) | `text` 参数设置为 `multi_prompt[0].prompt`,`multi_prompt` 清空 |
| 切换到单镜头模式(single) | `text` 参数设置为 `multi_prompt[0].prompt`,`multi_prompt` 清空,`multi_shot` 设置为 `false` |
| Kling 多镜头模式下禁止首尾帧生视频 | 如果 `generation_type` 为 `FIRST&LAST`,自动切换为 `REFERENCE` |
### 分辨率与比例的联动规则
| 模型 | 分辨率 | 比例联动规则 |
|------|--------|-------------|
| Sora2 Pro (methodType=12) | 720p | 支持比例:16:9、9:16 |
| Sora2 Pro (methodType=12) | 2K | 支持比例:7:4、4:7 |
| 其他模型 | - | 无特殊联动 |
### 生成类型切换时的参数重置
当用户切换生成类型时,系统会自动清空以下关联参数:
| 清空的参数 | 说明 |
|-----------|------|
| `image_url_list` | 参考图片列表 |
| `first_image_url` | 首帧图片 |
| `last_image_url` | 尾帧图片 |
| `first_clip_url` | 续写/编辑参考视频 |
| `element_list` | 参考主体列表 |
| `video_url_list` | 参考视频列表 |
| `audio_url` | 参考音频 |
| `duration_list` | 参考视频时长列表 |
| `generate_audio` | 视频编辑/参考视频生视频模式下自动关闭音频生成 |
在调用生成 API 前,系统会自动进行以下处理:
| 处理项 | 规则说明 |
|--------|---------|
| `size`(尺寸)| methodType=7,9(Wan系列)转换为 `{width}*{height}` 格式<br>methodType=11,12(Sora2系列)转换为 `{width}x{height}` 格式<br>其他模型保持比例字符串 |
| `duration`(时长)| durationSwitch='2' 时设置为 `-1`(智能时长)<br>否则使用用户选择的值 |
| `shot_type`(镜头类型)| Kling 多镜头模式(shot_type='multi')转换为 `intelligence`<br>其他保持原值 |
| `generate_audio`(生成声音)| Kling 视频编辑模式(first_clip_url 有值)时自动设置为 `false` |
| `video_list`(视频列表)| Kling 视频编辑/参考视频生视频模式时构建视频对象 |
### 参数校验规则
生成前系统会进行以下校验:
| 校验项 | 条件 | 错误提示 |
|--------|------|---------|
| 提示词 | 非 Wan I2V 模式且无提示词,且非 Kling 自定义多镜头 | 请填写生成视频的提示词! |
| Wan I2V 首帧 | Wan I2V 模式且生成类型为首尾帧生视频,无首帧图片 | 请上传首帧图片! |
| Wan2.7 I2V 续写 | methodType=14 且生成类型为续写模式,无续写视频 | 请上传续写视频! |
| Kling 首尾帧/参考图 | Kling 首尾帧模式无首帧图片且无参考主体<br>或参考图模式无参考图片且无参考主体 | 请上传首帧图片或选择参考主体!<br>或:请至少上传一张参考图片或选择一个参考主体! |
| Kling 自定义多镜头 | Kling 自定义多镜头模式,分镜时长或提示词为空 | 分镜信息的时长不能为空或为0,镜头描述不能为空! |
| Kling 视频编辑 | Kling 视频编辑模式且生成类型为 EDIT/FEATURE,无编辑视频 | 请上传编辑视频/参考视频! |
| Wan R2V 数量 | Wan R2V 模式,参考图片+参考视频总数为0或大于5 | 上传的参考图片+参考视频总数不能为0且不能大于5! |
| 尾帧图片 | 有尾帧图片但无首帧图片 | 请上传首帧图片! |
### 使用示例
```python
from scripts.generate_video import generate_video
# 1. 模型切换示例 - 切换到 W2.6t 自动变为文生视频
result = generate_video(
prompt="海边日落",
model="W2.6t"
# generation_type 会自动设置为 "TEXT"
)
# 2. 模型切换示例 - 切换到 Kling 自动变为多镜头模式
result = generate_video(
prompt="电影预告片",
model="KlingV3Omni"
# shot_type 会自动设置为 "multi"
# multi_shot 会自动设置为 True
)
# 3. 自定义多镜头模式
result = generate_video(
prompt="",
model="KlingV3Omni",
shot_type="customize",
multi_prompt=[
{"index": 1, "prompt": "镜头1描述", "duration": 3},
{"index": 2, "prompt": "镜头2描述", "duration": 3}
],
duration=6
)
# 4. Kling 视频编辑模式(自动关闭音频生成)
result = generate_video(
prompt="编辑这段视频",
model="KlingV3Omni",
generation_type="EDIT",
first_clip_url="https://example.com/video.mp4",
keep_original_sound=True
# generate_audio 会自动设置为 False
)
# 5. Sora2 Pro - 分辨率与比例联动
result = generate_video(
prompt="风景大片",
model="Sora2Pro",
resolution="2K", # 2K 分辨率时比例会自动推荐 7:4
ratio="7:4"
)
# 6. Wan R2V - 多参考素材
result = generate_video(
prompt="风格迁移",
model="W2.7r",
image_url_list=["https://example.com/img1.jpg", "https://example.com/img2.jpg"],
video_url_list=["https://example.com/style.mp4"]
# 总数不能超过 5 个(图片+视频)
)
```
### 使用示例
```python
from scripts.generate_image import generate_image
# 模型切换时的自动参数调整示例
# 1. 切换到 N1 模型时,quality 自动变为 "1K"
result = generate_image(
prompt="一只猫",
model="N1" # quality 会自动设为 "1K"
)
# 2. 切换到 S5.0L 模型时,web_search 自动开启
result = generate_image(
prompt="2024年流行的设计趋势",
model="S5.0L" # web_search 会自动设为 True
)
# 3. 手动覆盖自动参数(按此优先级:用户指定 > 系统默认)
result = generate_image(
prompt="一只猫",
model="N1",
quality="2K" # 手动指定会覆盖系统的 "1K" 默认值
)
```
## 视频模型输入限制参数
### 各视频模型的输入限制
根据选择的模型,系统会自动应用以下限制参数(图片、音频、视频上传):
#### 图片上传限制
| methodType | 模型名称 | 支持格式 | maxSize (MB) | maxLength (px) | minLength (px) | textLength (字) | maxQuantity (张) | 特殊说明 |
|------------|---------|---------|--------------|----------------|----------------|-----------------|------------------|---------|
| auto | Auto | .jpeg,.jpg,.png,.webp | 10 | 2000 | 360 | 500 | - | 最长边≤2000px,最短边≥360px |
| 0 | Seedance1.0 Pro | .jpeg,.jpg,.png,.webp,.bmp,.tiff,.gif | 30 | 6000 | 300 | 500 | - | 宽高比 (0.4, 2.5) |
| 1 | Sora2 Beta | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 不得包含真人或拟真人图像 |
| 2 | Seedance1.5 Pro | .jpeg,.jpg,.png,.webp,.bmp,.heic,.heif,.tiff,.gif | 30 | 6000 | 300 | 500 | - | 宽高比 (0.4, 2.5) |
| 3 | Veo3.1 Fast Lite | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | 2 | 支持负向提示词(250字) |
| 4 | Veo3.1 Pro Lite | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 支持负向提示词(250字) |
| 5 | Veo3.1 Fast | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 支持负向提示词(250字) |
| 6 | Veo3.1 Pro | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 支持负向提示词(250字) |
| 8 | Wan2.6 i2v | .jpeg,.jpg,.png,.bmp,.webp | 10 | 2000 | 360 | 750 | - | 宽高比 [1:8, 8:1] |
| 9 | Wan2.6 r2v | .jpeg,.jpg,.png,.bmp,.webp | 10 | 5000 | 240 | 750 | 5 | 图片+视频≤5 |
| 10 | Kling V3 Omni | .jpeg,.jpg,.png | 10 | - | 300 | 1250 | 7 | 宽高比 [1:2.5, 2.5:1] |
| 11 | Sora2 | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 图片比例必须符合生成比例 |
| 12 | Sora2 Pro | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 图片比例必须符合生成比例 |
| 14 | Wan2.7 i2v | .jpeg,.jpg,.png,.bmp,.webp | 20 | 8000 | 240 | 2500 | - | 宽高比 [1:8, 8:1] |
| 16 | Wan2.7 r2v | .jpeg,.jpg,.png,.bmp,.webp | 10 | 8000 | 240 | 2500 | 5 | 图片+视频≤5,宽高比 [1:8, 8:1] |
#### 音频上传限制
| methodType | 模型名称 | 支持格式 | maxSize (MB) | maxLength (秒) | minLength (秒) | 说明 |
|------------|---------|---------|--------------|----------------|----------------|------|
| 7 | Wan2.6 t2v | .wav,.mp3 | 15 | 30 | 3 | 时长超出视频则截取,不足则无声 |
| 8 | Wan2.6 i2v | .wav,.mp3 | 15 | 30 | 3 | 时长超出视频则截取,不足则无声 |
| 14 | Wan2.7 i2v | .wav,.mp3 | 15 | 30 | 3 | 时长超出视频则截取,不足则无声 |
| 15 | Wan2.7 t2v | .wav,.mp3 | 15 | 30 | 3 | 时长超出视频则截取,不足则无声 |
| 16 | Wan2.7 r2v | .wav,.mp3 | 15 | 10 | 2 | 用于指定参考素材中主体角色的音色 |
#### 视频上传限制
| methodType | 模型名称 | 支持格式 | maxSize (MB) | maxLength (秒) | minLength (秒) | maxQuantity | 说明 |
|------------|---------|---------|--------------|----------------|----------------|-------------|------|
| 9 | Wan2.6 r2v | .mp4,.mov | 100 | 30 | 1 | 3 | 图片+视频≤5 |
| 10 | Kling V3 Omni | .mp4,.mov | 200 | 10 | 3 | - | 视频编辑/参考视频生视频 |
| 14 | Wan2.7 i2v | .mp4,.mov | 100 | 10 | 2 | 1 | 视频续写模式,宽高比 [1:8, 8:1] |
| 16 | Wan2.7 r2v | .mp4,.mov | 100 | 30 | 1 | 3 | 图片+视频≤5 |
### 负向提示词支持
| methodType | 模型名称 | negativeTextLength (字) |
|------------|---------|------------------------|
| 3 | Veo3.1 Fast Lite | 250 |
| 4 | Veo3.1 Pro Lite | 250 |
| 5 | Veo3.1 Fast | 250 |
| 6 | Veo3.1 Pro | 250 |
| 7 | Wan2.6 t2v | 250 |
| 8 | Wan2.6 i2v | 250 |
| 9 | Wan2.6 r2v | 250 |
| 14 | Wan2.7 i2v | 250 |
| 15 | Wan2.7 t2v | 250 |
| 16 | Wan2.7 r2v | 250 |
### Kling V3 Omni 特殊限制
当使用 Kling V3 Omni 模型时,参考图片数量限制根据是否有编辑视频动态变化:
| 场景 | 参考图片 + 多图主体数量限制 |
|------|---------------------------|
| 无编辑视频/参考视频 | ≤ 7 |
| 有编辑视频/参考视频 | ≤ 4 |
### 参数说明
| 参数 | 类型 | 说明 |
|------|------|------|
| `targetMaxSize` | int (MB) | 上传文件的最大大小限制 |
| `targetMinLength` | int (px/秒) | 图片最短边像素 / 音视频最短时长 |
| `targetMaxLength` | int (px/秒) | 图片最长边像素 / 音视频最长时长 |
| `targetTextLength` | int (字) | 提示词的最大长度限制 |
| `targetNegativeTextLength` | int (字) | 负向提示词的最大长度限制 |
| `targetMaxQuantity` | int | 单次最多上传文件数量 |
| `targetAccept` | string | 支持的文件格式 |
| `targetUploadTips` | string | 上传说明提示 |
## 视频分辨率映射规则
### 质量等级与分辨率对照表
系统根据选择的 `resolution`(视频质量)和 `ratio`(画面比例)自动计算输出视频的分辨率(宽 x 高)。
| 质量 | 1:1 | 16:9 | 9:16 | 3:4 | 4:3 | 7:4 | 4:7 |
|------|-----|------|------|-----|-----|-----|-----|
| **720p** | 960x960 | 1280x720 | 720x1280 | 832x1088 | 1088x832 | - | - |
| **1080p** | 1440x1440 | 1920x1080 | 1080x1920 | 1248x1632 | 1632x1248 | - | - |
| **2K** | - | - | - | - | - | 1792x1024 | 1024x1792 |
### 不同视频模型的尺寸输出格式
| 模型类型 | methodType | 输出格式 | 示例 |
|---------|-----------|---------|------|
| Wan2.6/2.7 系列 (T2V/R2V) | 7, 9, 15, 16 | `{width}*{height}` | `1280*720` |
| Sora2 系列 | 11, 12 | `{width}x{height}` | `1280x720` |
| 其他视频模型 | 其他 | 比例字符串 | `16:9`、`9:16` |
### 分辨率计算示例
```python
from scripts.generate_video import get_video_resolution
# 获取 1080p 质量、16:9 比例的分辨率
resolution = get_video_resolution(quality="1080p", ratio="16:9")
print(resolution) # 输出: [1920, 1080]
# 获取 720p 质量、1:1 比例的分辨率
resolution = get_video_resolution(quality="720p", ratio="1:1")
print(resolution) # 输出: [960, 960]
# 获取 2K 质量、7:4 比例的分辨率
resolution = get_video_resolution(quality="2K", ratio="7:4")
print(resolution) # 输出: [1792, 1024]
# 仅获取质量对应的所有分辨率
resolutions = get_video_resolution(quality="1080p")
print(resolutions) # 输出: {'1:1': [1440, 1440], '16:9': [1920, 1080], '9:16': [1080, 1920], '3:4': [1248, 1632], '4:3': [1632, 1248]}
```
## 视频提示词写作建议
推荐书写模版:主体 + 运动,背景 + 运动,镜头 + 运动 ...
1. 基础结构:图生视频已经有了场景,因此尽量减少(甚至避免)对静止/无变化部分的描述,在明确指出运动对象的情况下,多描述运动的部分,包括主体的运动、背景的运动/变化、以及镜头的运动。
2. 简单直接:尽量使用简单词语和句子结构,模型会根据我们的表达与对图像画面的理解进行提示词扩写,生成符合预期的视频。
3. 特征描述:当主体具有一些突出特征时,可以加上突出特征来更好定位主体,比如老人、戴墨镜的女人等。描述运动时,关键的程度副词一定要明确,比如快速、幅度大。
4. 遵从图片:需要基于输入的图片内容来写,需要明确写出主体以及想做的动作或者运镜,需注意提示词不要与图片内容/基础参数存在事实矛盾。
5. 负向提示词:部分模型不响应负向提示词(如 Kling V3 Omni),请查阅上方各模型说明。
## 返回字段
| 字段 | 说明 |
|------|------|
| `status` | SUCCESS / FAILED / TIMEOUT |
| `url` | 媒体文件URL |
| `message` | 状态描述 |
| `local_path` | 本地保存路径(需 --download) |
| `data_uri` | Base64 Data URI(需 --download) |
| `image_data` | 原始图片字节(需 --download) |
## 环境配置
### 必需配置 - API Key
**重要:使用前必须设置你自己的 API Key!**
#### 获取 API Key
1. 访问 [https://ai.deepsop.com/](https://ai.deepsop.com/)
2. 注册并登录账号
3. 在控制台创建你的 API Key
4. 复制生成的 API Key(格式:`sk-xxxxxx...`)
#### 方式 1:使用 .env 文件(推荐)
1. 复制 `.env.example` 为 `.env`:
```bash
cp .env.example .env
```
2. 编辑 `.env` 文件,填入你的 API Key:
```bash
AI_ARTIST_TOKEN=sk-your_api_key_here
```
3. 在运行脚本前加载环境变量:
```bash
# Linux/macOS/Git Bash
source .env
# 或使用 export
export $(cat .env | xargs)
```
#### 方式 2:直接设置环境变量
##### Linux / macOS / Git Bash (Windows)
```bash
export AI_ARTIST_TOKEN="sk-your_api_key_here"
```
为了永久生效,将上述命令添加到 `~/.bashrc` 或 `~/.zshrc` 文件中。
##### Windows PowerShell
```powershell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
```
永久设置(系统级):
```powershell
[System.Environment]::SetEnvironmentVariable('AI_ARTIST_TOKEN', 'sk-your_api_key_here', 'User')
```
##### Windows CMD
```cmd
set AI_ARTIST_TOKEN=sk-your_api_key_here
```
#### 验证配置
运行以下命令验证 API Key 是否设置成功:
```bash
# Linux/macOS/Git Bash
echo $AI_ARTIST_TOKEN
# Windows PowerShell
echo $env:AI_ARTIST_TOKEN
# Windows CMD
echo %AI_ARTIST_TOKEN%
```
如果输出为空或显示默认值,说明环境变量未正确设置。
#### 测试配置(推荐)
运行配置测试脚本,验证 API Key 是否正确设置:
```bash
python3 scripts/test_config.py
```
该脚本会检查:
- API Key 是否已设置
- 是否使用了默认 Key(需要替换为你自己的)
- 配置是否可以正常使用
### 可选配置 - 飞书通知
```bash
export FEISHU_WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
```
## 相关文件
- `scripts/generate_image.py` - 图片生成脚本
- `scripts/generate_video.py` - 视频生成脚本
- `references/api.md` - API 详细文档
FILE:README.md
# AI Image Generator
基于 AI Artist API 的图片/视频异步生成工具。
- 支持图片与视频任务创建
- 自动轮询任务状态直到完成
- 支持本地参考图自动上传
- 创建任务前自动调用费用预估,余额不足时会拦截并提示充值
## 🚀 快速开始
### 1) 获取 API Key
访问 [https://ai.deepsop.com/](https://ai.deepsop.com/) 注册登录后,在控制台创建 API Key。
### 2) 设置环境变量
```bash
# Linux/macOS/Git Bash
export AI_ARTIST_TOKEN="sk-your_api_key_here"
```
```powershell
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
```
### 3) 验证配置
```bash
python3 scripts/test_config.py
```
### 4) 开始生成
```bash
# 默认图片模型(SEEDREAM5_0)
python3 scripts/generate_image.py "一只可爱的猫"
```
## 🎨 支持模型
### 图片模型
- `SEEDREAM5_0`(默认)
- `NANO_BANANA_2`
### 视频模型
- `SEEDANCE_1_5_PRO`
- `VEO3.1FAST_LITE`
- `VEO3.1PRO_LITE`
- `VEO3.1FAST`
- `VEO3.1PRO`
- `WAN2.6_T2V`
- `WAN2.6_I2V`
- `WAN2.6_R2V`
## 📝 常用示例
```bash
# 图片:指定模型
python3 scripts/generate_image.py "一只柴犬" --model NANO_BANANA_2
# 图片:下载到本地
python3 scripts/generate_image.py "海边日落" --download
# 图片:参考图生成(本地文件自动上传)
python3 scripts/generate_image.py "做成赛博朋克风格" --reference-image "./ref.png"
# 视频:基础文生视频
python3 scripts/generate_image.py "城市夜景延时" --model SEEDANCE_1_5_PRO
# 视频:首尾帧控制
python3 scripts/generate_image.py "灯具变形动画" --model VEO3.1PRO --first-image "./start.jpg" --last-image "./end.jpg"
```
## 📖 文档
完整参数说明与更多示例见 `SKILL.md`。
## 🔧 环境要求
- Python 3.6+
- `requests`
## ⚠️ 注意事项
- 必须使用你自己的 `AI_ARTIST_TOKEN`
- 任务创建前会执行费用预估;若余额不足将不会提交任务
- 请遵守 AI Artist API 的使用条款
FILE:scripts/generate_image.py
#!/usr/bin/env python3
"""
AI Image Generator - Async Image Generation Script
Calls the AI Artist API to generate images from text prompts.
Handles async task polling until completion.
Supports Feishu webhook callback for result notification.
Set FEISHU_WEBHOOK_URL environment variable to enable.
Supports local file upload for reference images/videos.
Local files are automatically uploaded to get public URLs before calling generation APIs.
"""
import requests
import json
import time
import sys
import argparse
import os
import base64
from pathlib import Path
# Configuration
API_PREFIX = "https://ai.deepsop.com/prod-api/"
BASE_URL = f"{API_PREFIX.rstrip('/')}/ai"
FILE_UPLOAD_URL = f"{API_PREFIX.rstrip('/')}/system/fileUpload/upload"
ESTIMATE_COST_URL = f"{BASE_URL}/estimate/cost"
RECHARGE_URL = "https://ai.deepsop.com/"
# Get API key from environment variable (required)
API_KEY = os.environ.get("AI_ARTIST_TOKEN")
# Feishu webhook configuration (optional)
FEISHU_WEBHOOK_URL = os.environ.get("FEISHU_WEBHOOK_URL")
def check_api_key():
"""Check if user has set their API key."""
if not API_KEY:
print("错误:未配置 AI_ARTIST_TOKEN 环境变量", file=sys.stderr)
print("", file=sys.stderr)
print("请先设置你的 API Key:", file=sys.stderr)
print(" export AI_ARTIST_TOKEN=\"sk-your_api_key_here\"", file=sys.stderr)
print("", file=sys.stderr)
print("验证配置:", file=sys.stderr)
print(" python3 scripts/test_config.py", file=sys.stderr)
print("", file=sys.stderr)
sys.exit(1)
return True
def get_headers():
"""Build request headers with API key."""
return {
"Content-Type": "application/json",
"X-Api-Key": API_KEY
}
def estimate_generation_cost(payload):
try:
response = requests.post(ESTIMATE_COST_URL, json=payload, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
print(f"费用预估失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return False
data = result.get("data") or {}
estimated_cost = data.get("estimatedCost")
sufficient_balance = data.get("sufficientBalance")
if estimated_cost is not None:
print(f"预估费用:{estimated_cost} K币")
if sufficient_balance is True:
print("余额充足,正在创建任务")
return True
if sufficient_balance is False:
print(f"余额不足,无法提交创建任务。请前往 {RECHARGE_URL} 充值 K 币后重试。", file=sys.stderr)
return False
print("费用预估返回结果不完整", file=sys.stderr)
return False
except requests.exceptions.RequestException as e:
print(f"费用预估网络错误:{e}", file=sys.stderr)
return False
except ValueError as e:
print(f"费用预估响应解析失败:{e}", file=sys.stderr)
return False
def upload_file(file_path):
"""
Upload a local file to the file server and get a public URL.
Args:
file_path: Path to the local file
Returns:
str: Public URL of the uploaded file, or None if failed
"""
if not os.path.exists(file_path):
print(f"文件不存在:{file_path}", file=sys.stderr)
return None
try:
with open(file_path, 'rb') as f:
files = {'file': (os.path.basename(file_path), f)}
headers = {'X-Api-Key': API_KEY}
response = requests.post(FILE_UPLOAD_URL, headers=headers, files=files, timeout=60)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
url = result.get("url")
print(f"文件已上传:{file_path} → {url}")
return url
else:
print(f"文件上传失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except Exception as e:
print(f"文件上传错误:{e}", file=sys.stderr)
return None
def download_image(url, output_path=None):
"""
Download image from URL.
Args:
url: Image URL
output_path: Optional path to save the image
Returns:
bytes: Image data, or None if failed
"""
try:
response = requests.get(url, timeout=60)
response.raise_for_status()
image_data = response.content
# Save to file if path provided
if output_path:
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(image_data)
print(f"图片已保存:{output_path}")
return image_data
except Exception as e:
print(f"下载图片失败:{e}", file=sys.stderr)
return None
def image_to_data_uri(image_data, mime_type="image/png"):
"""
Convert image bytes to data URI.
Args:
image_data: Raw image bytes
mime_type: MIME type of the image
Returns:
str: Data URI string
"""
base64_data = base64.b64encode(image_data).decode('utf-8')
return f"data:{mime_type};base64,{base64_data}"
def send_feishu_message(prompt, result):
"""Send generation result to Feishu chat."""
if not FEISHU_WEBHOOK_URL:
return False
try:
if result and result["status"] == "SUCCESS":
content = {
"msg_type": "interactive",
"card": {
"header": {
"title": {"tag": "plain_text", "content": "图片生成成功"},
"template": "green"
},
"elements": [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": f"**提示词**: {prompt}\n\n**图片链接**: [点击查看]({result['url']})"
}
},
{
"tag": "action",
"actions": [{
"tag": "button",
"text": {"tag": "plain_text", "content": "打开图片"},
"url": result["url"],
"type": "default"
}]
}
]
}
}
else:
error_msg = result.get("message", "未知错误") if result else "未知错误"
content = {
"msg_type": "interactive",
"card": {
"header": {
"title": {"tag": "plain_text", "content": "图片生成失败"},
"template": "red"
},
"elements": [{
"tag": "div",
"text": {
"tag": "lark_md",
"content": f"**提示词**: {prompt}\n\n**错误**: {error_msg}"
}
}]
}
}
response = requests.post(
FEISHU_WEBHOOK_URL,
json=content,
headers={"Content-Type": "application/json"},
timeout=30
)
response.raise_for_status()
return True
except Exception as e:
print(f"[Feishu] 发送通知失败:{e}", file=sys.stderr)
return False
# Model configurations
# media_type: "image" or "video" — determines task creation and output handling
MODEL_CONFIGS = {
"SEEDREAM5_0": {
"media_type": "image",
"type": "10",
"methodType": "4",
"default_size": "2048x2048",
"default_quality": "2K",
"extra_params": {"duration": 10}
},
"NANO_BANANA_2": {
"media_type": "image",
"type": "10",
"methodType": "5",
"default_size": "1:1",
"default_quality": "2K",
"extra_params": {}
},
"SEEDANCE_1_5_PRO": {
"media_type": "video",
"type": "9",
"methodType": "2",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"firstImageUrl": None,
"lastImageUrl": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 30,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"SORA2": {
"media_type": "video",
"type": "9",
"methodType": "11",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 4,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"firstImageUrl": None,
"lastImageUrl": None,
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"scaleFactor": 0.5,
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000,
"imageUrlList": [],
"videoUrlList": [],
"durationList": []
}
},
"VEO3.1FAST_LITE": {
"media_type": "video",
"type": "9",
"methodType": "3",
"default_ratio": "16:9",
"default_resolution": "1080p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"VEO3.1PRO_LITE": {
"media_type": "video",
"type": "9",
"methodType": "4",
"default_ratio": "adaptive",
"default_resolution": "720p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"VEO3.1FAST": {
"media_type": "video",
"type": "9",
"methodType": "5",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"VEO3.1PRO": {
"media_type": "video",
"type": "9",
"methodType": "6",
"default_ratio": "16:9",
"default_resolution": "1080p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"WAN2.6_T2V": {
"media_type": "video",
"type": "9",
"methodType": "7",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "TEXT",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 360,
"targetMaxLength": 2000
}
},
"WAN2.6_I2V": {
"media_type": "video",
"type": "9",
"methodType": "8",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 360,
"targetMaxLength": 2000
}
},
"WAN2.6_R2V": {
"media_type": "video",
"type": "9",
"methodType": "9",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "REFERENCE",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 240,
"targetMaxLength": 5000
}
}
}
def create_video_task(prompt, model="SEEDANCE_1_5_PRO", ratio=None, resolution=None,
duration=None, first_image_url=None, last_image_url=None,
generate_audio=None, scale_factor=None, generation_type=None,
enhance_prompt=None, prompt_extend=None, audio_url=None,
image_url_list=None, video_url_list=None):
"""Create a video generation task.
Args:
prompt: Text description of the video
model: Video model to use (e.g. SEEDANCE_1_5_PRO, SORA2)
ratio: Aspect ratio, e.g. '16:9', '9:16', '1:1'
resolution: Video resolution, e.g. '720p', '1080p'
duration: Video duration in seconds
first_image_url: URL of the first frame image (SORA2 FIRST&LAST mode)
last_image_url: URL of the last frame image (SORA2 FIRST&LAST mode)
generate_audio: Whether to generate audio (True/False)
scale_factor: Scale factor for SORA2 (e.g. 0.5)
generation_type: Generation type override, e.g. 'FIRST&LAST', 'TEXT'
enhance_prompt: Whether to enhance the prompt
prompt_extend: Whether to extend the prompt
audio_url: URL of audio file (WAN2.6 series)
image_url_list: List of image URLs for reference (WAN2.6_R2V)
video_url_list: List of video URLs for reference (WAN2.6_R2V)
"""
url = f"{BASE_URL}/AiArtistRecord"
if model not in MODEL_CONFIGS or MODEL_CONFIGS[model]["media_type"] != "video":
print(f"不支持的视频模型:{model}", file=sys.stderr)
return None
config = MODEL_CONFIGS[model]
effective_ratio = ratio or config.get("default_ratio", "16:9")
effective_resolution = resolution or config.get("default_resolution", "720p")
effective_duration = duration or config.get("default_duration", 10)
parameter = dict(config["extra_params"]) # copy defaults
# Resolve pixel size from ratio + resolution
resolution_size_map = {
("16:9", "720p"): "1280x720",
("16:9", "1080p"): "1920x1080",
("9:16", "720p"): "720x1280",
("9:16", "1080p"): "1080x1920",
("1:1", "720p"): "720x720",
("1:1", "1080p"): "1080x1080",
("3:4", "720p"): "720x960",
("3:4", "1080p"): "1080x1440",
("4:3", "720p"): "960x720",
("4:3", "1080p"): "1440x1080",
}
pixel_size = resolution_size_map.get((effective_ratio, effective_resolution), effective_ratio)
parameter.update({
"methodType": config["methodType"],
"text": prompt,
"resolution": effective_resolution,
"ratio": effective_ratio,
"size": pixel_size,
"duration": effective_duration,
})
# VEO3.1 series: duration must be 4 or 8 seconds
if model in ["VEO3.1FAST_LITE", "VEO3.1PRO_LITE", "VEO3.1FAST", "VEO3.1PRO"]:
# Validate duration (must be 4 or 8)
if effective_duration not in [4, 8]:
print(f"VEO3.1 系列模型时长必须是 4 或 8 秒,当前 {effective_duration} 秒,自动调整为 8 秒")
effective_duration = 8
parameter["duration"] = effective_duration
# For VEO3.1, size should match ratio (not pixel resolution)
parameter["size"] = effective_ratio
# WAN2.6 series: duration must be 3-15 seconds
if model in ["WAN2.6_T2V", "WAN2.6_I2V", "WAN2.6_R2V"]:
# Validate duration (must be 3-15)
if effective_duration < 3 or effective_duration > 15:
print(f"WAN2.6 系列模型时长必须是 3-15 秒,当前 {effective_duration} 秒,自动调整为 10 秒")
effective_duration = 10
parameter["duration"] = effective_duration
# For WAN2.6, size should be pixel format for some models
if model == "WAN2.6_T2V":
parameter["size"] = f"{pixel_size.replace('x', '*')}" # e.g., "1280*720"
elif model == "WAN2.6_I2V":
parameter["size"] = effective_ratio # e.g., "16:9"
elif model == "WAN2.6_R2V":
parameter["size"] = f"{pixel_size.replace('x', '*')}" # e.g., "1280*720"
# SORA2: auto-switch generationType based on image inputs
if model == "SORA2" and generation_type is None:
if first_image_url or last_image_url:
parameter["generationType"] = "FIRST&LAST"
else:
parameter["generationType"] = "FIRST&LAST" # text-to-video also uses FIRST&LAST with null image URLs
# WAN2.6_I2V: auto-switch generationType based on first_image_url
if model == "WAN2.6_I2V" and generation_type is None:
if first_image_url:
parameter["generationType"] = "FIRST&LAST"
else:
parameter["generationType"] = "FIRST&LAST"
# WAN2.6_R2V: set generationType to REFERENCE
if model == "WAN2.6_R2V":
parameter["generationType"] = "REFERENCE"
# Apply optional overrides
if first_image_url is not None:
parameter["firstImageUrl"] = first_image_url
if last_image_url is not None:
parameter["lastImageUrl"] = last_image_url
if generate_audio is not None:
parameter["generateAudio"] = generate_audio
if scale_factor is not None:
parameter["scaleFactor"] = scale_factor
if generation_type is not None:
parameter["generationType"] = generation_type
if enhance_prompt is not None:
parameter["enhancePrompt"] = enhance_prompt
if prompt_extend is not None:
parameter["promptExtend"] = prompt_extend
# WAN2.6 series: audio_url, image_url_list, video_url_list
if audio_url is not None:
parameter["audioUrl"] = audio_url
if image_url_list is not None:
parameter["imageUrlList"] = image_url_list
if video_url_list is not None:
parameter["videoUrlList"] = video_url_list
payload = {
"type": config["type"],
"methodType": config["methodType"],
"parameter": json.dumps(parameter)
}
if not estimate_generation_cost(payload):
return None
try:
response = requests.post(url, json=payload, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") == 200 and result.get("data"):
return result["data"][0]
else:
print(f"创建视频任务失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
print(f"网络错误:{e}", file=sys.stderr)
return None
def generate_video(prompt, model="SEEDANCE_1_5_PRO", ratio=None, resolution=None,
duration=None, poll_interval=5, first_image_url=None,
last_image_url=None, generate_audio=None, scale_factor=None,
generation_type=None, enhance_prompt=None, prompt_extend=None,
first_image_path=None, last_image_path=None, audio_url=None,
image_url_list=None, video_url_list=None, audio_path=None):
"""Generate a video from a text prompt.
Args:
prompt: Text description of the video
model: Video model to use (e.g. SEEDANCE_1_5_PRO, SORA2)
ratio: Aspect ratio (e.g. '16:9')
resolution: Video resolution (e.g. '720p')
duration: Video duration in seconds
poll_interval: Polling interval in seconds
first_image_url: URL of the first frame image (SORA2 FIRST&LAST mode)
last_image_url: URL of the last frame image (SORA2 FIRST&LAST mode)
generate_audio: Whether to generate audio
scale_factor: Scale factor for SORA2
generation_type: Generation type override
enhance_prompt: Whether to enhance the prompt
prompt_extend: Whether to extend the prompt
first_image_path: Local path to first frame image (auto-uploaded)
last_image_path: Local path to last frame image (auto-uploaded)
audio_url: URL of audio file (WAN2.6 series)
audio_path: Local path to audio file (auto-uploaded, WAN2.6 series)
image_url_list: List of image URLs for reference (WAN2.6_R2V)
video_url_list: List of video URLs for reference (WAN2.6_R2V)
Returns:
dict with 'status', 'url', 'message'
"""
# Upload local files to get URLs if provided
if first_image_path and not first_image_url:
first_image_url = upload_file(first_image_path)
if last_image_path and not last_image_url:
last_image_url = upload_file(last_image_path)
if audio_path and not audio_url:
audio_url = upload_file(audio_path)
config = MODEL_CONFIGS.get(model, {})
effective_ratio = ratio or config.get("default_ratio", "16:9")
effective_resolution = resolution or config.get("default_resolution", "720p")
effective_duration = duration or config.get("default_duration", 10)
print(f"正在生成视频:{prompt}")
print(f" 模型:{model} | 分辨率:{effective_resolution} | 比例:{effective_ratio} | 时长:{effective_duration}s")
if first_image_url:
print(f" 首帧图片:{first_image_url}")
if last_image_url:
print(f" 尾帧图片:{last_image_url}")
if audio_url:
print(f" 音频:{audio_url}")
if image_url_list:
print(f" 参考图片:{image_url_list}")
if video_url_list:
print(f" 参考视频:{video_url_list}")
task_id = create_video_task(
prompt, model, ratio, resolution, duration,
first_image_url=first_image_url,
last_image_url=last_image_url,
generate_audio=generate_audio,
scale_factor=scale_factor,
generation_type=generation_type,
enhance_prompt=enhance_prompt,
prompt_extend=prompt_extend,
audio_url=audio_url,
image_url_list=image_url_list,
video_url_list=video_url_list
)
if not task_id:
return None
print(f" 任务 ID: {task_id}")
result = poll_task_status(task_id, interval=poll_interval, max_wait=600)
if result and result["status"] == "SUCCESS":
print(f"视频生成成功!")
print(f" 视频链接:{result['url']}")
else:
print(f"视频生成失败:{result.get('message', '未知错误')}", file=sys.stderr)
return result
def create_generation_task(prompt, quality="2K", size=None, model="SEEDREAM5_0", reference_image_url=None):
"""Create an image generation task.
Args:
prompt: Text description of the image
quality: Image quality (2K/4K)
size: Image dimensions. SEEDREAM5_0 uses e.g. '2048x2048', NANO_BANANA_2 uses e.g. '1:1'
model: Model to use, one of: SEEDREAM5_0, NANO_BANANA_2
reference_image_url: Optional reference image URL for image-to-image generation
"""
url = f"{BASE_URL}/AiArtistRecord"
if model not in MODEL_CONFIGS:
print(f"不支持的模型:{model},可用模型:{list(MODEL_CONFIGS.keys())}", file=sys.stderr)
return None
config = MODEL_CONFIGS[model]
# Use model's default size if not specified
if size is None:
size = config["default_size"]
# Build image array - support reference image for image-to-image
image_array = []
if reference_image_url:
image_array = [reference_image_url]
parameter = {
"methodType": config["methodType"],
"prompt": prompt,
"image": image_array,
"quality": quality,
"size": size,
"webSearch": False,
"targetMaxSize": 10,
"targetMaxLength": 6000,
}
# Merge model-specific extra params
parameter.update(config["extra_params"])
payload = {
"type": config["type"],
"methodType": config["methodType"],
"parameter": json.dumps(parameter)
}
if not estimate_generation_cost(payload):
return None
try:
response = requests.post(url, json=payload, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") == 200 and result.get("data"):
return result["data"][0]
else:
print(f"创建任务失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
print(f"网络错误:{e}", file=sys.stderr)
return None
def poll_task_status(task_id, interval=5, max_wait=600):
"""Poll the task status until completion or failure."""
url = f"{BASE_URL}/AiArtistImage/getInfoByArtistId/{task_id}"
elapsed = 0
last_status = None
while elapsed < max_wait:
try:
response = requests.get(url, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
time.sleep(interval)
elapsed += interval
continue
data = result.get("data", {})
status = data.get("status", "")
# Only print status when it changes
if status != last_status:
print(f"{status} - {data.get('message', '')}")
last_status = status
if status == "SUCCESS":
return {
"status": "SUCCESS",
"url": data.get("url"),
"message": data.get("message", "生成成功")
}
elif status == "FAILED":
return {
"status": "FAILED",
"url": None,
"message": data.get("message", "生成失败")
}
else:
time.sleep(interval)
elapsed += interval
except requests.exceptions.RequestException as e:
print(f"查询状态出错:{e}", file=sys.stderr)
time.sleep(interval)
elapsed += interval
return {
"status": "TIMEOUT",
"url": None,
"message": f"超时({max_wait}秒)"
}
def generate_image(prompt, quality="2K", size=None, poll_interval=5,
download=False, output_dir=None, model="SEEDREAM5_0",
reference_image_path=None, reference_image_url=None):
"""
Main function to generate an image from a prompt.
Args:
prompt: Text description of the image
quality: Image quality (2K/4K)
size: Image dimensions. Defaults to model's default size if not specified.
SEEDREAM5_0: e.g. '2048x2048' | NANO_BANANA_2: e.g. '1:1'
poll_interval: Polling interval in seconds
download: Whether to download the image
output_dir: Directory to save the image (default: workspace/images)
model: Model to use. Options: SEEDREAM5_0, NANO_BANANA_2
reference_image_path: Local path to reference image (auto-uploaded)
reference_image_url: URL of reference image (if already uploaded)
Returns:
dict with generation result including 'url', 'local_path', 'data_uri' if successful
"""
config = MODEL_CONFIGS.get(model, {})
effective_size = size or config.get("default_size", "2048x2048")
# Upload reference image if local path provided
if reference_image_path and not reference_image_url:
reference_image_url = upload_file(reference_image_path)
print(f"正在生成:{prompt}")
print(f" 模型:{model} | 质量:{quality} | 尺寸:{effective_size}")
if reference_image_url:
print(f" 参考图:{reference_image_url}")
# Step 1: Create task
task_id = create_generation_task(prompt, quality, size, model, reference_image_url)
if not task_id:
return None
print(f" 任务 ID: {task_id}")
# Step 2: Poll until complete
result = poll_task_status(task_id, interval=poll_interval)
if result and result["status"] == "SUCCESS":
print(f"生成成功!")
print(f" 图片链接:{result['url']}")
# Download image if requested
if download and result.get("url"):
if not output_dir:
output_dir = os.path.join(os.path.expanduser("~"), ".openclaw", "workspace", "images")
# Generate filename from prompt
safe_prompt = "".join(c if c.isalnum() or c in (' ', '-', '_') else '_' for c in prompt)
safe_prompt = safe_prompt[:50].strip().replace(' ', '_')
filename = f"{safe_prompt}_{int(time.time())}.png"
output_path = os.path.join(output_dir, filename)
image_data = download_image(result["url"], output_path)
if image_data:
result["local_path"] = output_path
result["data_uri"] = image_to_data_uri(image_data)
result["image_data"] = image_data # Raw bytes for programmatic use
return result
else:
print(f"生成失败:{result.get('message', '未知错误')}", file=sys.stderr)
return result
if __name__ == "__main__":
# Check API key before proceeding
check_api_key()
image_models = [k for k, v in MODEL_CONFIGS.items() if v["media_type"] == "image"]
video_models = [k for k, v in MODEL_CONFIGS.items() if v["media_type"] == "video"]
all_models = list(MODEL_CONFIGS.keys())
parser = argparse.ArgumentParser(
description="AI 图片/视频生成器",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"图片模型:{', '.join(image_models)}\n视频模型:{', '.join(video_models)}"
)
parser.add_argument("prompt", help="生成提示词")
parser.add_argument("--model", default="SEEDREAM5_0",
choices=all_models,
help="生成模型 (默认:SEEDREAM5_0)")
# 图片专属参数
parser.add_argument("--quality", default="2K", help="[图片] 图片质量 (默认:2K)")
parser.add_argument("--size", default=None, help="[图片] 图片尺寸,不传则使用模型默认值")
parser.add_argument("--download", action="store_true", help="[图片] 下载图片到本地")
parser.add_argument("--output-dir", help="[图片] 图片保存目录")
parser.add_argument("--markdown-output", action="store_true", help="以 Markdown 格式输出图片链接")
parser.add_argument("--reference-image", default=None, help="[图片] 参考图本地路径,自动上传后作为 image-to-image 参考")
# 视频专属参数
parser.add_argument("--ratio", default=None, help="[视频] 画面比例,如 16:9、9:16、1:1 (默认:16:9)")
parser.add_argument("--resolution", default=None, help="[视频] 分辨率,如 720p、1080p (默认:720p)")
parser.add_argument("--duration", type=int, default=None, help="[视频] 视频时长 (秒) (默认:10)")
# SORA2 专属参数
parser.add_argument("--first-image-url", default=None, help="[SORA2] 首帧图片 URL(FIRST&LAST 模式)")
parser.add_argument("--last-image-url", default=None, help="[SORA2] 尾帧图片 URL(FIRST&LAST 模式)")
parser.add_argument("--first-image", default=None, help="[SORA2] 首帧图片本地路径,自动上传")
parser.add_argument("--last-image", default=None, help="[SORA2] 尾帧图片本地路径,自动上传")
parser.add_argument("--generate-audio", action="store_true", default=None, help="[SORA2] 生成音频")
parser.add_argument("--no-audio", action="store_true", help="[SORA2] 不生成音频")
parser.add_argument("--scale-factor", type=float, default=None, help="[SORA2] 缩放系数 (默认:0.5)")
parser.add_argument("--generation-type", default=None, help="[SORA2] 生成类型,如 FIRST&LAST、TEXT")
# 通用参数
parser.add_argument("--interval", type=int, default=5, help="轮询间隔秒数")
args = parser.parse_args()
media_type = MODEL_CONFIGS[args.model]["media_type"]
if media_type == "video":
# Resolve audio flag
gen_audio = None
if args.no_audio:
gen_audio = False
elif args.generate_audio:
gen_audio = True
result = generate_video(
prompt=args.prompt,
model=args.model,
ratio=args.ratio,
resolution=args.resolution,
duration=args.duration,
poll_interval=args.interval,
first_image_url=args.first_image_url,
last_image_url=args.last_image_url,
first_image_path=args.first_image,
last_image_path=args.last_image,
generate_audio=gen_audio,
scale_factor=args.scale_factor,
generation_type=args.generation_type
)
if result and result["status"] == "SUCCESS" and result.get("url"):
print(result["url"])
sys.exit(0)
elif result:
sys.exit(0 if result["status"] == "SUCCESS" else 1)
else:
sys.exit(1)
else:
result = generate_image(
prompt=args.prompt,
quality=args.quality,
size=args.size,
poll_interval=args.interval,
download=args.download,
output_dir=args.output_dir,
model=args.model,
reference_image_path=args.reference_image
)
# Send result to Feishu if webhook is configured
if FEISHU_WEBHOOK_URL:
send_feishu_message(args.prompt, result)
# Output based on --markdown-output flag
if args.markdown_output and result and result["status"] == "SUCCESS" and result.get("url"):
print(f"")
sys.exit(0)
elif result and result["status"] == "SUCCESS" and result.get("url"):
print(result["url"])
sys.exit(0)
elif result:
sys.exit(0 if result["status"] == "SUCCESS" else 1)
else:
sys.exit(1)
FILE:references/api.md
# AI Artist API 详细文档
## API 端点
### 1. 预估生成费用
**POST** `/ai/estimate/cost`
**请求头:**
```
Content-Type: application/json
X-Api-Key: <api_key>
```
**请求体:**
```json
{
"type": "10",
"methodType": "4",
"parameter": "{...}"
}
```
说明:请求体与创建生成任务时使用的参数完全一致,需要在正式创建任务前先调用本接口。
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"estimatedCost": 3.500000,
"sufficientBalance": true
}
}
```
当 `sufficientBalance` 为 `false` 时,表示余额不足,不应继续提交创建任务,需要提醒用户先充值 K 币。
### 2. 创建生成任务
**POST** `/ai/AiArtistRecord`
**请求头:**
```
Content-Type: application/json
X-Api-Key: <api_key>
```
**请求体:**
```json
{
"type": "10",
"methodType": "4",
"parameter": "{...}"
}
```
**支持的模型:**
| 模型名称 | methodType | 默认尺寸 | 说明 |
|----------|-----------|---------|------|
| `SEEDREAM5_0` | `"4"` | `2048x2048` | 默认模型,高质量图片生成 |
| `NANO_BANANA_2` | `"5"` | `1:1` | 轻量模型,支持比例尺寸格式 |
**parameter 字段说明:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `methodType` | string | 模型对应值:SEEDREAM5_0="4",NANO_BANANA_2="5" |
| `prompt` | string | 图片生成提示词 |
| `image` | array | 参考图片(可选) |
| `quality` | string | 图片质量: "2K" / "4K" |
| `size` | string | 尺寸格式因模型而异:SEEDREAM5_0 用 "2048x2048",NANO_BANANA_2 用 "1:1" |
| `webSearch` | boolean | 是否启用网络搜索 |
| `targetMaxSize` | number | 目标最大尺寸 |
| `targetMaxLength` | number | 目标最大长度 |
| `duration` | number | 持续时间(仅 SEEDREAM5_0)|
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": ["<task_id>"]
}
```
**失败响应:**
```json
{
"msg": "错误信息",
"code": 400,
"data": null
}
```
### 3. 查询任务状态
**GET** `/ai/AiArtistImage/getInfoByArtistId/{artistId}`
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"message": "生成成功",
"url": "https://...",
"status": "SUCCESS"
}
}
```
**状态值说明:**
| 状态 | 含义 |
|------|------|
| `PENDING` | 等待中 |
| `RUNNING` / `GENERATING` | 生成中 |
| `SUCCESS` | 生成成功 |
| `FAILED` | 生成失败 |
## 错误码
| Code | 含义 |
|------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权(token无效) |
| 429 | 请求过于频繁 |
| 500 | 服务器内部错误 |
## 完整请求示例
```bash
# 使用 SEEDREAM5_0 模型创建任务
curl -X POST "https://ai.deepsop.com/prod-api/ai/AiArtistRecord" \
-H "Content-Type: application/json" \
-H "X-Api-Key: <api_key>" \
-d '{
"type": "10",
"methodType": "4",
"parameter": "{\"methodType\":\"4\",\"prompt\":\"风景画\",\"image\":[],\"quality\":\"2K\",\"size\":\"2048x2048\",\"webSearch\":false,\"targetMaxSize\":10,\"targetMaxLength\":6000,\"duration\":10}"
}'
# 使用 NANO_BANANA_2 模型创建任务
curl -X POST "https://ai.deepsop.com/prod-api/ai/AiArtistRecord" \
-H "Content-Type: application/json" \
-H "X-Api-Key: <api_key>" \
-d '{
"type": "10",
"methodType": "5",
"parameter": "{\"methodType\":\"5\",\"prompt\":\"生成一只狗\",\"image\":[],\"quality\":\"2K\",\"size\":\"1:1\",\"webSearch\":false,\"targetMaxSize\":10,\"targetMaxLength\":6000}"
}'
# 查询状态
curl -X GET "https://ai.deepsop.com/prod-api/ai/AiArtistImage/getInfoByArtistId/<task_id>" \
-H "X-Api-Key: <api_key>"
```
FILE:references/chat-integration.md
# 在对话中直接返回图片的示例
## 示例 1: 使用 Markdown(最简单)
当用户请求生成图片时,直接返回:
```python
from scripts.generate_image import generate_image
result = generate_image(prompt="风景画")
if result and result["status"] == "SUCCESS":
# 直接在回复中使用 Markdown 图片语法
reply = f"生成成功!\n\n"
```
## 示例 2: 使用 message 工具发送图片
如果需要使用 message 工具发送图片(支持更多平台):
```python
from scripts.generate_image import generate_image
import base64
# 1. 生成并下载图片
result = generate_image(prompt="风景画", download=True)
if result and result["status"] == "SUCCESS":
# 2. 读取图片数据
with open(result["local_path"], "rb") as f:
image_bytes = f.read()
# 3. 转换为 base64
base64_image = base64.b64encode(image_bytes).decode('utf-8')
# 4. 使用 message 工具发送
# 注意:实际使用时通过 message 工具的 buffer 参数发送
```
## 示例 3: 直接在回复中包含图片
对于支持 Markdown 图片的平台(如 Discord、Telegram、WebChat):
```
用户: 生成一张风景画
助手: 正在生成...
[生成完成后]
助手: 生成成功!🎨

```
## 平台兼容性
| 平台 | Markdown 图片 | 说明 |
|------|--------------|------|
| WebChat | ✅ 支持 | 直接显示 |
| Discord | ✅ 支持 | 直接显示 |
| Telegram | ✅ 支持 | 直接显示 |
| 飞书 | ⚠️ 部分支持 | 建议使用卡片消息 |
| WhatsApp | ❌ 不支持 | 需要下载后发送 |
## 最佳实践
1. **优先使用 Markdown**: 最简单,大多数平台支持
2. **同时提供链接**: 以防图片加载失败
3. **下载选项**: 对于不支持 Markdown 的平台,使用 `--download` 参数
FILE:references/feishu-integration.md
# 飞书图片发送指南
## 飞书支持的图片发送方式
### 方式 1: 使用 message 工具发送图片(推荐)
通过 `message` 工具的 `buffer` 参数直接发送图片:
```python
import base64
# 读取图片文件
with open("image.png", "rb") as f:
image_data = f.read()
# 使用 message 工具发送
# message(
# action="send",
# buffer=base64.b64encode(image_data).decode(),
# filename="image.png",
# mimeType="image/png"
# )
```
### 方式 2: 使用飞书卡片消息(富文本)
飞书支持发送带有图片的交互式卡片:
```python
{
"msg_type": "interactive",
"card": {
"header": {
"title": {
"tag": "plain_text",
"content": "✅ 图片生成成功"
},
"template": "green"
},
"elements": [
{
"tag": "img",
"img_key": "", # 需要先在飞书上传图片获取 img_key
"alt": {
"tag": "plain_text",
"content": "生成的图片"
}
},
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**提示词**: 风景画"
}
}
]
}
}
```
**注意**:使用 `img_key` 需要先将图片上传到飞书平台。
### 方式 3: 使用 Markdown 图片链接
飞书支持 Markdown 图片语法,但图片需要是公开可访问的 URL:
```markdown

```
**限制**:
- 图片 URL 必须是公网可访问的
- 不支持 base64 内嵌图片
## 最佳实践
### 对于 AI 图片生成场景
1. **生成图片后获取公开 URL**(如阿里云 OSS、AWS S3 等)
2. **使用 Markdown 语法发送**:
```markdown
生成成功!🎨

图片链接:https://your-cdn.com/image.png
```
3. **或者使用 message 工具直接发送图片数据**
### 代码示例
```python
from scripts.generate_image import generate_image
# 生成图片
result = generate_image(prompt="风景画")
if result and result["status"] == "SUCCESS":
# 方式 1: 使用 Markdown 发送图片 URL
reply = f"""生成成功!🎨

图片链接:{result['url']}"""
# 方式 2: 下载后使用 message 工具发送
# result = generate_image(prompt="风景画", download=True)
# with open(result["local_path"], "rb") as f:
# image_data = f.read()
# message(action="send", buffer=base64.b64encode(image_data).decode(), ...)
```
## 平台对比
| 平台 | Markdown 图片 | Base64 图片 | 卡片消息 | 说明 |
|------|--------------|-------------|----------|------|
| WebChat | ✅ 支持 | ✅ 支持 | ❌ 不支持 | 最灵活 |
| Discord | ✅ 支持 | ⚠️ 有限 | ✅ 支持 | 支持 embed |
| Telegram | ✅ 支持 | ✅ 支持 | ✅ 支持 | 支持多种方式 |
| **飞书** | ✅ 支持 | ❌ 不支持 | ✅ 支持 | 需公开 URL 或上传 |
| WhatsApp | ❌ 不支持 | ✅ 支持 | ❌ 不支持 | 需发送文件 |
## 飞书特殊说明
1. **Base64 图片不支持**:飞书不支持直接通过 base64 内嵌图片
2. **需要先上传**:使用卡片消息的 `img_key` 需要先将图片上传到飞书
3. **公开 URL 最方便**:使用 CDN 或对象存储的公开链接最简单
4. **Webhook 发送**:通过 Webhook 发送时,图片需要是公开可访问的
## 推荐的飞书集成方案
```python
# 方案 1: 使用公开 URL(最简单)
def send_image_to_feishu(image_url, prompt):
content = f"""✅ 图片生成成功
**提示词**: {prompt}

[打开图片]({image_url})"""
# 使用 message 工具发送 Markdown
# message(action="send", message=content)
# 方案 2: 下载后作为文件发送
def send_image_file(image_path):
with open(image_path, "rb") as f:
image_data = f.read()
# 使用 message 工具发送文件
# message(
# action="send",
# buffer=base64.b64encode(image_data).decode(),
# filename="generated_image.png",
# mimeType="image/png"
# )
```
AI 图片与视频异步生成技能,调用 AI Artist API 根据文本提示词生成图片或视频,自动轮询直到任务完成。 ⚠️ 使用前必须设置环境变量 AI_ARTIST_TOKEN 为你自己的 API Key! 获取 API Key:访问 https://ai.deepsop.com/ 注册登录后创建。 支持图片模...
---
name: ai-image-generator
description: |
AI 图片与视频异步生成技能,调用 AI Artist API 根据文本提示词生成图片或视频,自动轮询直到任务完成。
⚠️ 使用前必须设置环境变量 AI_ARTIST_TOKEN 为你自己的 API Key!
获取 API Key:访问 https://ai.deepsop.com/ 注册登录后创建。
支持图片模型:DeepSop系列图片模型(S4.5、S5.0L、N1、N2系列、W2.7系列等,共11个模型)。
支持视频模型:DeepSop系列视频模型(S1.5Pro、Sora2系列、Veo3.1系列、Wan2.6/Wan2.7系列、Kling V3 Omni等,共15个模型)。
触发场景:
- 用户要求生成图片,如"生成一匹狼"、"画一只猫"、"风景画"、"帮我画"等。
- 用户要求生成视频,如"生成视频"、"文生视频"、"图生视频"、"生成一段...的视频"等。
- 用户指定具体模型(详见下方模型列表)。
- 用户上传参考图/参考视频时,自动先调用文件上传 API 转换为可访问 URL。
---
# AI Image Generator
异步生成 AI 图片与视频的技能。
## ⚠️ 首次使用必读
### 1. 获取 API Key
访问 [https://ai.deepsop.com/](https://ai.deepsop.com/) 注册并登录,然后创建你的 API Key。
### 2. 设置环境变量
**在使用前,你必须先设置自己的 API Key:**
```bash
# Linux/macOS/Git Bash (Windows)
export AI_ARTIST_TOKEN="sk-your_api_key_here"
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
```
### 3. 验证配置
**验证配置是否正确:**
```bash
python3 scripts/test_config.py
```
详细配置说明请查看下方"环境配置"章节。
## 快速开始
```bash
# 图片生成(默认 DeepSop·3.1Nano2-Evo)
python3 scripts/generate_image.py "一只可爱的猫"
# 视频生成(默认 DeepSop·V3.1FB)
python3 scripts/generate_video.py "海边日落风景"
```
## 参考图/视频上传流程
当用户提供本地文件作为参考图或参考视频时,需要先调用文件上传 API 转换为可访问的 URL:
### 文件上传 API
```bash
curl --location --request POST 'https://ai.deepsop.com/prod-api/system/fileUpload/upload' \
--header 'x-api-key: sk-your_api_key_here' \
--form 'file=@"C:\\Users\\admin\\Downloads\\image.png"'
```
**返回结果:**
```json
{
"msg": "操作成功",
"fileName": "image.png",
"code": 200,
"url": "https://kocgo-ai-sales-test.oss-cn-hangzhou.aliyuncs.com/material/100/xxx.png"
}
```
### 使用上传后的 URL
获取到 `url` 后,可作为 `firstImageUrl`、`lastImageUrl`、`imageUrlList`、`videoUrlList` 或 `elementList `等参数传入生成接口。
## 在对话中直接返回图片/视频
### 方式 1: Markdown 语法(推荐)
生成图片后,直接在回复中使用 Markdown 语法:
```markdown


```
**平台支持情况:**
- ✅ WebChat、Discord、Telegram:完全支持
- ✅ 飞书:支持(需公开 URL)
- ❌ WhatsApp:不支持
### 方式 2: 下载后发送(需要 message 工具)
使用 `--download` 参数下载媒体文件,然后通过 message 工具发送:
```bash
python3 scripts/generate_image.py "风景画" --download
python3 scripts/generate_video.py "海边" --download
```
比如图片生成接着在代码中读取图片并发送:
```python
from scripts.generate_image import generate_image
import base64
result = generate_image(prompt="风景画", download=True)
if result and result["status"] == "SUCCESS":
# 方式 A: 使用 data URI
image_uri = result["data_uri"] # data:image/png;base64,...
# 方式 B: 读取本地文件
with open(result["local_path"], "rb") as f:
image_data = f.read()
base64_data = base64.b64encode(image_data).decode()
```
## 参数说明
### 通用参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `prompt` | 必填 | 生成提示词(图片或视频描述)|
| `--model` | 图片: `DeepSop·3.1Nano2-Evo` / 视频: `DeepSop·V3.1FB` | 生成模型(详见下方模型列表) |
| `--interval` | `5` | 轮询间隔(秒) |
| `--download` | - | 下载媒体文件到本地 |
| `--output-dir` | `workspace/images`(图片) / `workspace/videos`(视频) | 文件保存目录 |
### 图片专属参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--quality` | 按模型自动匹配 | 图片质量:`1K`、`2K`、`3K`、`4K`(具体支持见下方模型能力表) |
| `--size` | 按模型自动匹配 | 图片比例:`1:1`、`3:4`、`4:3`、`16:9`、`9:16`、`2:3`、`3:2`、`4:5`、`5:4`、`1:4`、`4:1`、`1:8`、`8:1`、`21:9`、`auto`(具体支持见下方模型能力表) |
| `--download` | - | 下载图片到本地 |
| `--output-dir` | `workspace/images` | 图片保存目录 |
| `--markdown-output` | - | 以 Markdown 格式输出图片链接 |
| `--reference-image` | - | 参考图本地路径,自动上传后作为 image-to-image 参考 |
| `--web-search` | - | 开启联网搜索(仅 S5.0L 和 Nano2-Evo 支持) |
### 视频专属参数
| 参数 | 默认值 | 说明 |
|------|--------|------|
| `--generation-type` | `TEXT` | 生成类型:`TEXT`(文生视频)、`FIRST&LAST`(首尾帧生视频)、`REFERENCE`(参考图生视频)、`CONTINUATION`(视频续写)、`EDIT`(视频编辑)、`FEATURE`(参考视频生视频) |
| `--ratio` | 按模型自动匹配 | 画面比例(具体支持见下方模型能力表) |
| `--resolution` | 按模型自动匹配 | 视频分辨率:`480p`、`720p`、`1080p`、`2K`、`4K`(具体支持见下方模型能力表) |
| `--duration` | 按模型自动匹配 | 视频时长(秒),不同模型支持范围不同 |
| `--mode` | `std` | 生成模式:std(标准模式)、pro(专家模式/高品质)(仅 Kling V3 Omni 支持) |
| `--first-image-url` | - | 首帧参考图 URL |
| `--last-image-url` | - | 尾帧参考图 URL |
| `--first-image` | - | 首帧参考图本地路径,自动上传后转换为 URL |
| `--last-image` | - | 尾帧参考图本地路径,自动上传后转换为 URL |
| `--first-clip-url` | - | 续写/编辑参考视频 URL |
| `--first-clip` | - | 续写/编辑参考视频本地路径,自动上传后转换为 URL |
| `--image-url-list` | - | 参考图片 URL 列表(用于参考图生视频) |
| `--video-url-list` | - | 参考视频 URL 列表(用于 R2V 模型) |
| `--element-list` | - | 参考主体 URL 列表(用于 Kling V3 Omni) |
| `--generate-audio` | - | 开启音频生成(按模型能力生效) |
| `--no-audio` | - | 关闭音频生成(按模型能力生效) |
| `--keep-original-sound` | - | 保留视频原声(仅 Kling V3 Omni) |
| `--prompt-extend` | - | 开启智能提示词改写(Wan系列支持)
| `--enhance-prompt` | - | 开启提示词翻译成英文(Veo3.1系列支持)
| `--negative-prompt` | - | 负向提示词(Veo3.1 Fast/Pro、Wan系列支持)
| `--shot-type` | `single` | 镜头模式:`single`(单镜头)、`multi`(智能分镜)、`customize`(自定义分镜)
| `--duration-switch` | - | 时长模式开关(仅 S1.5Pro)
| `--person-generation` | `allow_adult` | 是否允许生成人物:`allow_adult`、`dont_allow`(仅 Veo3.1 Fast/Pro)
| `--resize-mode` | `pad` | 图像缩放模式:`pad`(调整图片)、`crop`(裁剪图片)(仅 Veo3.1 Fast/Pro)
| `--multi-shot` | - | 是否多镜头(仅 Kling V3 Omni)
| `--n` | `1` | 生成视频数量(仅 Veo3.1 Fast/Pro)
| `--audio-url` | - | 参考音频 URL(Wan系列 T2V/I2V 支持)
## 支持的模型
### 图片模型
| 模型 | methodType | 支持质量 | 支持比例 | 联网搜索 | 特点 |
|------|-----------|---------|------|
| `S4.5` | `0` | 2K, 4K | 除 auto 外所有比例 | ❌ | 电影级画质4K,角色一致性
| `N1` | `1` | 1K | 除 21:9、4:5、5:4、1:4、4:1、1:8、8:1 外 | ❌ | 支持多模态输入,精细参数调节
| `N2` | `2` | 1K, 2K, 4K | 所有比例 | ❌ | 卓越的文字渲染和角色一致性
| `N2-147` | `3` | 1K, 2K, 4K | 除 auto、1:4、4:1、1:8、8:1 外 | ❌ | 147版本,支持多模态输入
| `S5.0L` | `4` | 2K, 3K | 除 auto 外所有比例 | ✅ | 默认模型,生成快、风格全、易用
| `N2-Pro` | `5` | 1K, 2K, 4K | 除 auto、1:4、4:1、1:8、8:1 外 | ✅ | Pro版本,画质细节更优
| `W2.7` | `6` | 1K, 2K | 除 auto、21:9 外 | ❌ | 画质清晰,细节丰富
| `W2.7Pro` | `7` | 1K, 2K | 除 auto、21:9 外 | ❌ | 精准控图与风格迁移
| `N2-Evo` | `8` | 1K, 2K, 4K | 所有比例 | ✅ | Evo版本,卓越的文字渲染
| `N2-Beta` | `9` | 1K, 2K, 4K | 所有比例 | ❌ | Beta测试版
| `Auto` | `auto` | 2K 除 auto、1:4、4:1、1:8、8:1、21:9 外 | ❌ | 自动选择最佳模型
### 视频模型
| 模型名称 | methodType | 支持生成类型 | 支持比例 | 支持分辨率 | 时长范围 | 特殊能力 |
|---------|-----------|------------|---------|-----------|---------|---------|
| `S1.5Pro` | `2` | TEXT, FIRST&LAST | 1:1, 3:4, 4:3, 16:9, 9:16, 21:9, adaptive | 480p, 720p, 1080p | 4-12s | 影视级叙事,支持音频生成、时长模式 |
| `Sora2 Beta` | `1` | TEXT, FIRST&LAST | 16:9, 9:16 | 720p | 10-15s | Beta版本 |
| `Sora2` | `11` | TEXT, FIRST&LAST | 16:9, 9:16 | 720p | 4-12s | 基础版本 |
| `Sora2 Pro` | `12` | TEXT, FIRST&LAST | 16:9, 9:16, 7:4, 4:7 | 720p, 2K | 4-12s | Pro版本 |
| `V3.1FB` | `3` | TEXT, FIRST&LAST, REFERENCE | 16:9, 9:16, adaptive | 720p, 1080p, 4K | 8s | 快速轻量版,支持提示词翻译 |
| `V3.1PB` | `4` | TEXT, FIRST&LAST, REFERENCE | 16:9, 9:16, adaptive | 720p, 1080p, 4K | 8s | 专业轻量版,多图参考 |
| `V3.1Fast` | `5` | TEXT, FIRST&LAST | 16:9, 9:16, adaptive | 720p, 1080p, 4K | 4s, 8s | 快速版,支持音画同步 |
| `V3.1Pro` | `6` | TEXT, FIRST&LAST | 16:9, 9:16, adaptive | 720p, 1080p, 4K | 4s, 8s | 专业版,4K超清,商业级 |
| `W2.6t` | `7` | TEXT | 1:1, 3:4, 4:3, 16:9, 9:16 | 720p, 1080p | 3-15s | 文生视频,支持音频、提示词改写 |
| `W2.6i` | `8` | FIRST&LAST | 固定 | 720p, 1080p | 3-15s | 首帧图生视频,比例由图片决定 |
| `W2.6r` | `9` | REFERENCE | 1:1, 3:4, 4:3, 16:9, 9:16 | 720p, 1080p | 3-10s | 参考视频生视频 |
| `W2.7i` | `14` | FIRST&LAST, CONTINUATION | 固定 | 720p, 1080p | 3-15s | 首帧图生视频,支持续写 |
| `W2.7t` | `15` | TEXT | 1:1, 3:4, 4:3, 16:9, 9:16 | 720p, 1080p | 3-15s | 文生视频,支持音频、提示词改写 |
| `W2.7r` | `16` | REFERENCE | 1:1, 3:4, 4:3, 16:9, 9:16 | 720p, 1080p | 3-15s(无视频引用)<br>3-10s(有视频引用) | 参考视频生视频 |
| `Kling V3 Omni` | `10` | TEXT, FIRST&LAST, REFERENCE, EDIT, FEATURE | 1:1, 16:9, 9:16 | 720p, 1080p | 3-15s | 全能模型,支持主体参考、多镜头 |
| `Auto` | `auto` | FIRST&LAST | 16:9, 9:16 | 720p | 4-12s | 自动选择最佳模型 |
**VEO3.1 系列(V3.1FB、V3.1PB、V3.1Fast、V3.1Pro)共同说明:**
| 模型名称 | 支持特性 |
|---------|-----------|
| `V3.1FB` / `V3.1PB` | 支持 `--enhance-prompt`(提示词翻译成英文) |
| `V3.1Fast` / `V3.1Pro` | 支持 `--n`、`--person-generation`、`--resize-mode`、`--negative-prompt`、`--enhance-prompt`、`--generate-audio` |
**WAN2.6 系列共同说明:**
| 模型名称 | 支持特性 |
|---------|-----------|
| `W2.6t` / `W2.7t` | 文生视频,支持 `--audio-url`(自定义音频) |
| `W2.6i` / `W2.7i` | 首帧图生视频,不支持 `--ratio` 参数(比例由首帧图决定),W2.7i 支持 `--first-clip-url`(续写) |
| `W2.6r` / `W2.7r` | 参考视频生视频,支持 `--video-url-list`(参考视频列表),W2.7r 时长根据是否有视频引用动态变化 |
| 全系列 | 支持 `--prompt-extend`(智能提示词改写)、`--negative-prompt`(负向提示词) |
**Kling V3 Omni 特有能力:**
| 能力 | 说明 |
|---------|-----------|
| `--element-list` | 参考主体选择 |
| `--keep-original-sound` | 保留视频原声 |
| `--mode` | 生成模式(std/pro) |
| `--multi-shot` | 是否多镜头 |
| `--shot-type` | 镜头模式(single/multi/customize) |
| `--generate-audio` | 生成声音 |
| 不支持 `--resolution` | 分辨率固定 |
## 参数联动规则(自动处理)
**图片质量按模型自动过滤**
| model | 支持质量 |
|-------|---------|
| Auto | 2K |
| S4.5 (`0`) | 2K, 4K |
| N1 (`1`) | 1K |
| N2 (`2`)、N2-147 (`3`)、N2-Pro (`5`)、N2-Evo (`8`)、N2-Beta (`9`) | 1K, 2K, 4K |
| S5.0L (`4`) | 2K, 3K |
| W2.7 (`6`)、W2.7Pro (`7`) | 1K, 2K |
**图片比例按模型自动过滤**
| model | 排除比例 |
|-------|---------|
| Auto | `auto`、`1:4`、`4:1`、`1:8`、`8:1`、`21:9` |
| S4.5 (`0`)、S5.0L (`4`) | `auto` |
| N1 (`1`) | `21:9`、`4:5`、`5:4`、`1:4`、`4:1`、`1:8`、`8:1` |
| N2-147 (`3`)、N2-Pro (`5`) | `auto`、`1:4`、`4:1`、`1:8`、`8:1` |
| W2.7 (`6`)、W2.7Pro (`7`) | `auto`、`21:9` |
| N2 (`2`)、N2-Evo (`8`)、N2-Beta (`9`) | 无(支持所有比例) |
**视频生成类型按模型自动过滤**
| model | 支持生成类型 |
|-------|------------|
| Auto | FIRST&LAST |
| Sora2 Beta (`1`)、S1.5Pro (`2`)、V3.1PB (`4`)、V3.1Fast (`5`)、V3.1Pro (`6`)、Sora2 (`11`)、Sora2 Pro (`12`) | TEXT, FIRST&LAST |
| W2.6t (`7`)、W2.7t (`15`) | TEXT |
| W2.6i (`8`) | FIRST&LAST |
| W2.7i (`14`) | FIRST&LAST, CONTINUATION |
| W2.6r (`9`)、W2.7r (`16`) | REFERENCE |
| Kling V3 Omni (`10`) | TEXT, FIRST&LAST, REFERENCE, EDIT, FEATURE |
| V3.1FB (`3`) | TEXT, FIRST&LAST, REFERENCE |
**视频分辨率按模型自动过滤**
| model | 支持分辨率 |
|-------|-----------|
| Auto、Sora2 Beta (`1`)、Sora2 (`11`) | 720p |
| S1.5Pro (`2`) | 480p, 720p, 1080p |
| V3.1FB (`3`)、V3.1PB (`4`)、V3.1Fast (`5`)、V3.1Pro (`6`) | 720p, 1080p, 4K |
| W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`)、Kling V3 Omni (`10`)、W2.7i (`14`)、W2.7t (`15`)、W2.7r (`16`) | 720p, 1080p |
| Sora2 Pro (`12`) | 720p, 2K |
**视频比例按模型自动过滤**
| model | 支持比例 |
|-------|---------|
| Auto、Sora2 Beta (`1`) | `16:9`, `9:16` |
| S1.5Pro (`2`) | `1:1`, `3:4`, `4:3`, `16:9`, `9:16`, `21:9`, `adaptive` |
| V3.1FB (`3`)、V3.1PB (`4`)、V3.1Fast (`5`)、V3.1Pro (`6`) | `16:9`, `9:16`, `adaptive` |
| Kling V3 Omni (`10`) | `1:1`, `16:9`, `9:16` |
| W2.6t (`7`)、W2.6r (`9`)、W2.7t (`15`)、W2.7r (`16`) | `1:1`, `3:4`, `4:3`, `16:9`, `9:16` |
| W2.6i (`8`)、W2.7i (`14`) | 固定(由首帧图比例决定) |
| Sora2 Pro (`12`) | `16:9`, `9:16`, `7:4`, `4:7` |
**视频时长按模型自动配置**
| model | 时长范围 | 可选档位 |
|-------|---------|---------|
| Sora2 Beta (`1`) | 5-15s | `10s`、`15s` |
| V3.1FB (`3`)、V3.1PB (`4`) | 8s(固定) | `8s` |
| V3.1Fast (`5`)、V3.1Pro (`6`) | 4-8s | `4s`、`8s` |
| W2.6t (`7`)、W2.6i (`8`)、Kling V3 Omni (`10`)、W2.7i (`14`)、W2.7t (`15`) | 3-15s | `3s`、`15s` |
| W2.6r (`9`) | 3-10s | `3s`、`10s` |
| W2.7r (`16`) | 3-15s(无视频引用)<br>3-10s(有视频引用) | `3s`、`10s` 或 `3s`、`15s` |
| Sora2 (`11`)、Sora2 Pro (`12`) | 4-12s | `4s`、`12s` |
| S1.5Pro (`2`)、Auto | 4-12s | `4s`、`12s` |
**镜头模式按模型自动过滤**
| model | 支持镜头模式 |
|-------|------------|
| W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`) | `single`、`multi` |
| Kling V3 Omni (`10`) | `single`、`multi`、`customize` |
| 其他 | `single`(默认) |
## 参数显隐逻辑(自动处理)
**按模型显示的参数**
| 参数 | 支持的 model (methodType) |
|------|-------------------------|
| `web_search`(联网搜索) | S5.0L (`4`)、N2-Evo (`8`) |
| `audio_url`(参考音频) | W2.6t (`7`)、W2.6i (`8`)、W2.7i (`14`)、W2.7t (`15`)、W2.7r (`16`) |
| `prompt_extend`(智能改写) | W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`)、W2.7i (`14`)、W2.7t (`15`)、W2.7r (`16`) |
| `first_clip_url`(续写视频) | Kling V3 Omni (`10`)、W2.7i (`14`) |
| `keep_original_sound`(保留原声) | Kling V3 Omni (`10`) |
| `element_list`(参考主体) | Kling V3 Omni (`10`) |
| `video_url_list`(参考视频) | W2.6r (`9`)、W2.7r (`16`) |
| `mode`(生成模式) | Kling V3 Omni (`10`) |
| `duration_switch`(时长模式) | S1.5Pro (`2`) |
| `generate_audio`(生成声音) | S1.5Pro (`2`)、V3.1Fast (`5`)、V3.1Pro (`6`)、Kling V3 Omni (`10`) |
| `enhance_prompt`(翻译英文) | V3.1FB (`3`)、V3.1PB (`4`)、V3.1Fast (`5`)、V3.1Pro (`6`) |
| `n`(生成数量) | V3.1Fast (`5`)、V3.1Pro (`6`) |
| `person_generation`(人物生成) | V3.1Fast (`5`)、V3.1Pro (`6`) |
| `resize_mode`(缩放模式) | V3.1Fast (`5`)、V3.1Pro (`6`) |
| `negative_prompt`(负向提示词) | V3.1Fast (`5`)、V3.1Pro (`6`)、W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`)、W2.7i (`14`)、W2.7t (`15`)、W2.7r (`16`) |
| `multi_shot`(多镜头) | Kling V3 Omni (`10`) |
| `shot_type`(镜头模式) | W2.6t (`7`)、W2.6i (`8`)、W2.6r (`9`)、Kling V3 Omni (`10`) |
**按模型隐藏的参数**
| 参数 | 不支持该参数的 model |
|------|---------------------|
| `last_image_url`(尾帧图片) | Auto、Sora2 Beta (`1`)、W2.6i (`8`)、Sora2 (`11`)、Sora2 Pro (`12`) |
| `ratio`(生成比例) | W2.6i (`8`)、W2.7i (`14`) |
| `resolution`(分辨率) | Kling V3 Omni (`10`) |
| `duration`(时长) | Auto |
**参数联动显隐(同模型下受其他参数影响)**
| 参数 | 依赖参数 | 显示条件 |
|------|---------|---------|
| `text`(提示词) | `shot_type` | `shot_type` = `'customize'` |
| `multi_prompt`(多镜头内容) | `shot_type` | `shot_type` = `'customize'` |
| `image_url_list`(参考图片) | `generation_type` | `generation_type` 为 REFERENCE、EDIT、FEATURE |
| `first_image_url`(首帧图) | `generation_type` | `generation_type` = FIRST&LAST |
| `last_image_url`(尾帧图) | `generation_type` | `generation_type` = FIRST&LAST |
| `first_clip_url`(续写视频) | `generation_type` | `generation_type` 为 CONTINUATION、EDIT、FEATURE |
| `keep_original_sound`(保留原声) | `first_clip_url` | `first_clip_url` 有值 |
| `element_list`(参考主体) | `generation_type` | `generation_type` ≠ TEXT |
| `ratio`(比例) | `generation_type` | Kling V3 Omni 除外:`generation_type` ≠ FIRST&LAST 且 ≠ EDIT |
| `duration`(时长) | `duration_switch` | `duration_switch` = `'1'` |
## 使用示例
**图片生成**
```bash
# 基础用法 - 默认模型 DeepSop·3.1Nano2-Evo
python3 scripts/generate_image.py "一匹狼"
# 指定质量
python3 scripts/generate_image.py "风景画" --quality "4K"
# 指定比例
python3 scripts/generate_image.py "风景画" --ratio "16:9"
# 使用 N2 模型
python3 scripts/generate_image.py "生成一只狗" --model N2
# 使用 N2-Pro 并开启联网搜索
python3 scripts/generate_image.py "2024年流行的装修风格" --model N2-Pro --web-search
# 使用 W2.7Pro
python3 scripts/generate_image.py "山水画" --model W2.7Pro --quality "2K" --ratio "9:16"
# 使用 N2-Evo
python3 scripts/generate_image.py "赛博朋克城市" --model N2-Evo --quality "4K" --ratio "16:9"
# 下载图片到本地
python3 scripts/generate_image.py "风景画" --download
# 直接输出 Markdown 图片链接
python3 scripts/generate_image.py "一只可爱的猫" --markdown-output
# 使用参考图生成
python3 scripts/generate_image.py "基于这张图生成变体" --reference-image "./reference.png"
```
**图片生成**
```bash
# 基础用法 - 默认 DeepSop·V3.1FB
python3 scripts/generate_video.py "海边日落风景"
# 指定比例和分辨率
python3 scripts/generate_video.py "海边日落风景" --ratio "9:16" --resolution "1080p"
# 指定时长
python3 scripts/generate_video.py "一只猫在玩耍" --duration 5
# 专家模式
python3 scripts/generate_video.py "海边日落风景" --mode pro
# 首尾帧生视频
python3 scripts/generate_video.py "花朵绽放" --generation-type FIRST&LAST --first-image "./flower_start.jpg" --last-image "./flower_end.jpg"
# 参考图生视频
python3 scripts/generate_video.py "产品展示" --generation-type REFERENCE --image-url-list "https://example.com/product1.jpg,https://example.com/product2.jpg"
# 视频续写
python3 scripts/generate_video.py "继续这个视频" --generation-type CONTINUATION --first-clip "./my_video.mp4" --duration 5
# Veo3.1 系列 - 文生视频
python3 scripts/generate_video.py "现代轻奢吊灯" --model V3.1FB --ratio "16:9" --duration 8
# Veo3.1 系列 - 首尾帧控制
python3 scripts/generate_video.py "灯具变形动画" --model V3.1Pro --first-image "./start.jpg" --last-image "./end.jpg" --duration 8
# Veo3.1 系列 - 负向提示词
python3 scripts/generate_video.py "人物奔跑" --model V3.1Pro --negative-prompt "模糊, 抖动" --duration 8
# Veo3.1Fast - 生成多个视频
python3 scripts/generate_video.py "产品广告" --model V3.1Fast --n 3 --duration 4
# W2.7t - 文生视频
python3 scripts/generate_video.py "现代轻奢吊灯宣传" --model W2.7t --ratio "16:9" --duration 10 --prompt-extend
# W2.7t - 带参考音频
python3 scripts/generate_video.py "产品展示" --model W2.7t --audio-url "https://example.com/audio.mp3" --duration 10
# W2.7i - 首帧图生视频
python3 scripts/generate_video.py "水晶灯展示" --model W2.7i --first-image "./lamp.jpg" --duration 8
# W2.7i - 视频续写
python3 scripts/generate_video.py "继续这个动画" --model W2.7i --first-image "./lamp.jpg" --first-clip "./lamp_animation.mp4" --duration 5
# W2.7r - 参考视频生视频
python3 scripts/generate_video.py "参考素材风格生成" --model W2.7r --video-url-list "https://example.com/video.mp4" --duration 10
# W2.7r - 多参考视频
python3 scripts/generate_video.py "风格迁移" --model W2.7r --video-url-list "https://example.com/style1.mp4,https://example.com/style2.mp4" --duration 8
# Kling V3 Omni - 多镜头分镜
python3 scripts/generate_video.py "电影预告片" --model "Kling V3 Omni" --shot-type multi --multi-shot --mode pro
# Kling V3 Omni - 参考主体
python3 scripts/generate_video.py "角色在行走" --model "Kling V3 Omni" --element-list "https://example.com/character.jpg"
# Kling V3 Omni - 保留原声的视频编辑
python3 scripts/generate_video.py "编辑这段视频" --model "Kling V3 Omni" --generation-type EDIT --first-clip "./original.mp4" --keep-original-sound
# Sora2 Pro - 高分辨率
python3 scripts/generate_video.py "风景大片" --model Sora2Pro --ratio "7:4" --resolution "2K" --duration 12
```
## 模型名称速查表
**图片模型(methodType → 模型名称)**
| methodType | 模型名称 | CLI 参数 |
|-----------|---------|---------|
| `0` | DeepSop·S4.5 | `S4.5` |
| `1` | DeepSop·N1 | `N1` |
| `2` | DeepSop·N2 | `N2` |
| `3` | DeepSop·3-Nano2-147 | `N2-147` |
| `4` | DeepSop·S5.0L | `S5.0L` |
| `5` | DeepSop·3.1Nano2-147 | `N2-Pro` |
| `6` | DeepSop.W2.7 | `W2.7` |
| `7` | DeepSop.W2.7Pro | `W2.7Pro` |
| `8` | DeepSop·3.1Nano2-Evo | `N2-Evo`(默认) |
| `9` | DeepSop·Nano2 Beta-Evo | `N2-Beta` |
| `auto` | DeepSop·Auto | `Auto` |
**视频模型(methodType → 模型名称)**
| methodType | 模型名称 | CLI 参数 |
|-----------|---------|---------|
| `1` | DeepSop·Sora2 Beta Max Evolink | `Sora2Beta` |
| `2` | DeepSop·S1.5Pro | `S1.5Pro` |
| `3` | DeepSop·V3.1FB | `V3.1FB`(默认) |
| `4` | DeepSop·V3.1PB | `V3.1PB` |
| `5` | DeepSop·V3.1Fast | `V3.1Fast` |
| `6` | DeepSop·V3.1Pro | `V3.1Pro` |
| `7` | DeepSop·W2.6t | `W2.6t` |
| `8` | DeepSop·W2.6i | `W2.6i` |
| `9` | DeepSop·W2.6r | `W2.6r` |
| `10` | DeepSop.klingV3Omni | `KlingV3Omni` |
| `11` | DeepSop·Sora2.147 | `Sora2` |
| `12` | DeepSop·Sora2 Pro.147 | `Sora2Pro` |
| `14` | DeepSop·W2.7i | `W2.7i` |
| `15` | DeepSop·W2.7t | `W2.7t` |
| `16` | DeepSop·W2.7r | `W2.7r` |
| `auto` | DeepSop·Auto | `Auto` |
## 程序化调用
```python
from scripts.generate_image import generate_image, generate_video
# 图片 - 默认 DeepSop·3.1Nano2-Evo
result = generate_image(prompt="一只可爱的猫咪")
# 图片 - N2 模型
result = generate_image(prompt="生成一只狗", model="N2")
# 图片 - 带联网搜索
result = generate_image(prompt="2024年流行的装修风格", model="N2-Pro", web_search=True)
# 图片 - 下载到本地
result = generate_image(prompt="风景画", model="S5.0L", download=True, output_dir="./images")
# 视频 - 默认 DeepSop·V3.1FB
result = generate_video(prompt="小骏马祝福大家新年快乐")
# 视频 - S1.5Pro 带音频
result = generate_video(
prompt="海边日落风景",
model="S1.5Pro",
ratio="9:16",
resolution="1080p",
duration=5,
generate_audio=True
)
# 视频 - V3.1Pro 首尾帧控制
result = generate_video(
prompt="灯具变形动画",
model="V3.1Pro",
first_image_url="https://example.com/start.jpg",
last_image_url="https://example.com/end.jpg",
ratio="16:9",
resolution="1080p",
duration=8
)
# 视频 - V3.1Fast 生成多个
result = generate_video(
prompt="产品广告",
model="V3.1Fast",
n=3,
duration=4,
person_generation="allow_adult"
)
# 视频 - W2.7t 带参考音频和提示词改写
result = generate_video(
prompt="产品宣传片",
model="W2.7t",
ratio="16:9",
resolution="1080p",
duration=10,
audio_url="https://example.com/music.mp3",
prompt_extend=True
)
# 视频 - W2.7r 多参考视频
result = generate_video(
prompt="风格迁移视频",
model="W2.7r",
video_url_list=["https://example.com/style1.mp4", "https://example.com/style2.mp4"],
ratio="16:9",
duration=10
)
# 视频 - Kling V3 Omni 多镜头模式
result = generate_video(
prompt="电影预告片",
model="KlingV3Omni",
generation_type="TEXT",
shot_type="multi",
multi_shot=True,
mode="pro"
)
# 视频 - Kling V3 Omni 参考主体
result = generate_video(
prompt="角色在奔跑",
model="KlingV3Omni",
generation_type="REFERENCE",
element_list=["https://example.com/character.jpg"],
keep_original_sound=False
)
if result and result["status"] == "SUCCESS":
print(f"媒体链接: {result['url']}")
print(f"本地路径: {result.get('local_path')}")
```
## 图像生成前处理与参数变动
### 模型切换时的自动参数调整
当用户切换生成模型时,系统会自动调整以下参数:
| 切换场景 | 自动调整规则 |
|---------|-------------|
| 切换到 N1 (methodType=1) | `quality` 自动设置为 `1K` |
| 切换到其他模型 | `quality` 自动设置为 `2K`(默认)|
| 切换到 S5.0L (methodType=4) | `web_search` 自动开启 |
| 切换到其他模型 | `web_search` 自动关闭 |
### 模型与尺寸/质量的关系
图片生成时,`size` 参数会根据 `methodType`、`quality` 和用户选择的 `ratio` 自动计算:
| 模型类型 | methodType | size 格式 | 计算公式 |
|---------|-----------|----------|---------|
| S4.5、S5.0L | 0, 4 | `{width}x{height}` | 根据 quality 和 ratio 解析宽高后拼接 |
| W2.7、W2.7Pro | 6, 7 | `{width}*{height}` | 根据 quality 和 ratio 解析宽高后用 `*` 拼接 |
| N1、N2 系列 | 1, 2, 3, 5, 8, 9 | 比例字符串 | 直接使用用户选择的 ratio 值(如 `16:9`)|
| Auto | auto | 比例字符串 | 直接使用用户选择的 ratio 值 |
### 生成前预处理参数
在调用生成 API 前,系统会自动添加以下限制参数:
| 参数 | 说明 | 来源 |
|------|------|------|
| `targetMaxSize` | 目标图片最大尺寸(字节)| 根据模型类型自动匹配 |
| `targetMinLength` | 提示词最小长度 | 根据模型类型自动匹配 |
| `targetMaxLength` | 提示词最大长度 | 根据模型类型自动匹配 |
## 图片生成限制参数说明
### 各模型的输入限制参数
根据选择的模型,系统会自动应用以下限制参数(`targetMaxSize`、`targetMinLength`、`targetMaxLength`):
| methodType | 模型名称 | maxSize (MB) | minLength (字) | maxLength (字) | maxQuantity (张) | 上传说明 |
|------------|---------|--------------|----------------|----------------|------------------|---------|
| auto | Auto | .jpeg,.jpg,.png,.webp | 10 | 2000 | 360 | 500 | - | 最长边≤2000px,最短边≥360px |
| 0 | S4.5 | .jpeg,.jpg,.png,.webp,.bmp,.tiff,.gif | 30 | 6000 | 300 | 500 | 14 | 宽高比 (0.4, 2.5),最多生成15张 |
| 1 | N1 | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 5 | 最长边≤6000px |
| 2 | N2 | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 10 | 最长边≤6000px,最多5张真人图像 |
| 3 | N2-147 | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 5 | 最长边≤6000px |
| 4 | S5.0L | .jpeg,.jpg,.png,.webp,.bmp,.tiff,.gif | 10 | 6000 | - | 300 | 14 | 宽高比 [1/16, 16],最多生成15张 |
| 5 | N2-Pro | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 5 | 最长边≤6000px |
| 6 | W2.7 | .jpeg,.jpg,.png,.bmp,.webp | 20 | 8000 | 240 | 2500 | 9 | 不支持透明通道,宽高比 [1:8, 8:1] |
| 7 | W2.7Pro | .jpeg,.jpg,.png,.bmp,.webp | 20 | 8000 | 240 | 2500 | 9 | 不支持透明通道,宽高比 [1:8, 8:1] |
| 8 | N2-Evo | .jpeg,.jpg,.png,.webp | 20 | 6000 | - | 1000 | 14 | 最长边≤6000px,最多4张真人图像 |
| 9 | N2-Beta | .jpeg,.jpg,.png,.webp | 10 | 6000 | - | 1000 | 14 | 最长边≤6000px,最多4张真人图像 |
### 图片生成提示词长度限制
| methodType | 模型名称 | textLength (最大提示词字数) |
|------------|---------|---------------------------|
| 0 | S4.5 | 500 |
| 1,2,3,5,8,9 | N1/N2 系列 | 1000 |
| 4 | S5.0L | 300 |
| 6,7 | W2.7/W2.7Pro | 2500 |
| auto | Auto | 500 |
### 参数说明
| 参数 | 类型 | 说明 |
|------|------|------|
| `targetAccept` | string | 支持的图片文件格式 |
| `targetMaxSize` | int (MB) | 上传图片的最大文件大小限制 |
| `targetMaxLength` | int (px) | 图片最长边的最大像素限制 |
| `targetMinLength` | int (px) | 图片最短边的最小像素限制 |
| `targetTextLength` | int (字) | 提示词的最大长度限制 |
| `targetMaxQuantity` | int (张) | 参考图片的最大上传数量 |
| `targetUploadTips` | string | 上传说明和合规性提示 |
### 图片上传合规性要求
**通用要求:**
- 支持格式:JPEG、JPG、PNG、WEBP(部分模型支持 BMP、TIFF、GIF)
- 文件大小:根据模型不同,限制为 10MB-30MB
- 最长边限制:根据模型不同,限制为 2000px-8000px
**内容审查要求(Sora2/Veo 系列):**
1. 不得包含真人或拟真人图像
2. 提示词禁止暴力、色情、版权侵权或涉及名人信息
**Wan 系列特殊要求:**
1. 不支持透明通道(PNG 透明部分会被处理)
2. 宽高比必须在 [1:8, 8:1] 范围内
**Seedance 系列特殊要求:**
1. 宽高比(宽/高)必须在 (0.4, 2.5) 范围内
2. 上传图片最长边 ≤ 6000px,最短边 ≥ 300px
## 图片分辨率映射规则
### 质量等级与分辨率对照表
系统根据选择的 `quality`(图片质量)和 `ratio`(画面比例)自动计算输出图片的分辨率(宽 x 高)。
| 质量 | 1:1 | 16:9 | 9:16 | 3:4 | 4:3 | 2:3 | 3:2 | 4:5 | 5:4 | 1:4 | 4:1 | 1:8 | 8:1 | 21:9 |
|------|-----|------|------|-----|-----|-----|-----|-----|-----|-----|-----|-----|-----|------|
| **1K** | 1024x1024 | 1920x1080 | 1080x1920 | 768x1024 | 1024x768 | 682x1024 | 1024x682 | 1024x1280 | 1280x1024 | 512x2048 | 2048x512 | 362x2896 | 2896x362 | 2560x1080 |
| **2K** | 2048x2048 | 2560x1440 | 1440x2560 | 1728x2304 | 2304x1728 | 1664x2496 | 2496x1664 | 1843x2304 | 2304x1843 | 1024x4096 | 4096x1024 | 724x5792 | 5792x724 | 3584x1536 |
| **3K** | 3072x3072 | 4096x2304 | 2304x4096 | 2592x3456 | 3456x2592 | 2496x3744 | 3744x2496 | 2884x3605 | 3605x2884 | 1536x6144 | 6144x1536 | 1088x8704 | 8704x1088 | 4704x2016 |
| **4K** | 4096x4096 | 3840x2160 | 2160x3840 | 3072x4096 | 4096x3072 | 2730x4096 | 4096x2730 | 3277x4096 | 4096x3277 | 2048x8192 | 8192x2048 | 1448x11584 | 11584x1448 | 5040x2160 |
### 不同模型的分辨率格式
| 模型类型 | methodType | 输出格式 | 示例 |
|---------|-----------|---------|------|
| S4.5、S5.0L | 0, 4 | `{width}x{height}` | `2048x2048` |
| W2.7、W2.7Pro | 6, 7 | `{width}*{height}` | `2048*2048` |
| N1、N2 系列 | 1, 2, 3, 5, 8, 9 | 比例字符串 | `1:1`、`16:9` |
| Auto | auto | 比例字符串 | `1:1`、`16:9` |
### 分辨率计算示例
```python
from scripts.generate_image import get_image_resolution
# 获取 2K 质量、16:9 比例的分辨率
resolution = get_image_resolution(quality="2K", ratio="16:9")
print(resolution) # 输出: [2560, 1440]
# 获取 4K 质量、1:1 比例的分辨率
resolution = get_image_resolution(quality="4K", ratio="1:1")
print(resolution) # 输出: [4096, 4096]
# 仅获取质量对应的所有分辨率
resolutions = get_image_resolution(quality="2K")
print(resolutions) # 输出: {'1:1': [2048, 2048], '16:9': [2560, 1440], ...}
```
## 视频生成前处理与参数变动
### 模型切换时的自动参数调整
当用户切换视频模型时,系统会自动调整以下参数:
| 切换场景 | 自动调整规则 |
|---------|-------------|
| 切换到 W2.6t/W2.7t (methodType=7) | `generation_type` 自动设置为 `TEXT`(文生视频)|
| 切换到 W2.6r/W2.7r (methodType=9,16) | `generation_type` 自动设置为 `REFERENCE`(参考图/视频生视频)|
| 切换到 Kling V3 Omni (methodType=10) | `generation_type` 自动设置为 `REFERENCE` |
| 切换到其他模型 | `generation_type` 自动设置为 `FIRST&LAST`(首尾帧生视频)|
| 切换到 Kling V3 Omni (methodType=10) | `shot_type` 自动设置为 `multi`(智能分镜)|
| 切换到其他模型 | `shot_type` 自动设置为 `single`(单镜头)|
| 切换到 V3.1系列/Sora2系列 (3,4,5,6,11,12) | `duration` 自动设置为 `8` 秒 |
| 切换到其他视频模型 | `duration` 自动设置为 `10` 秒 |
### 镜头模式切换规则
当用户切换镜头模式时,系统会自动调整以下参数:
| 切换场景 | 自动调整规则 |
|---------|-------------|
| 切换到 Kling 多镜头模式(multi/customize) | `multi_shot` 自动设置为 `true` |
| 切换到 Kling 自定义多镜头(customize) | `text` 参数清空,`multi_prompt` 初始化为 `[{ index: 1, prompt: text, duration }]` |
| 切换到 Kling 智能分镜(multi) | `text` 参数设置为 `multi_prompt[0].prompt`,`multi_prompt` 清空 |
| 切换到单镜头模式(single) | `text` 参数设置为 `multi_prompt[0].prompt`,`multi_prompt` 清空,`multi_shot` 设置为 `false` |
| Kling 多镜头模式下禁止首尾帧生视频 | 如果 `generation_type` 为 `FIRST&LAST`,自动切换为 `REFERENCE` |
### 分辨率与比例的联动规则
| 模型 | 分辨率 | 比例联动规则 |
|------|--------|-------------|
| Sora2 Pro (methodType=12) | 720p | 支持比例:16:9、9:16 |
| Sora2 Pro (methodType=12) | 2K | 支持比例:7:4、4:7 |
| 其他模型 | - | 无特殊联动 |
### 生成类型切换时的参数重置
当用户切换生成类型时,系统会自动清空以下关联参数:
| 清空的参数 | 说明 |
|-----------|------|
| `image_url_list` | 参考图片列表 |
| `first_image_url` | 首帧图片 |
| `last_image_url` | 尾帧图片 |
| `first_clip_url` | 续写/编辑参考视频 |
| `element_list` | 参考主体列表 |
| `video_url_list` | 参考视频列表 |
| `audio_url` | 参考音频 |
| `duration_list` | 参考视频时长列表 |
| `generate_audio` | 视频编辑/参考视频生视频模式下自动关闭音频生成 |
在调用生成 API 前,系统会自动进行以下处理:
| 处理项 | 规则说明 |
|--------|---------|
| `size`(尺寸)| methodType=7,9(Wan系列)转换为 `{width}*{height}` 格式<br>methodType=11,12(Sora2系列)转换为 `{width}x{height}` 格式<br>其他模型保持比例字符串 |
| `duration`(时长)| durationSwitch='2' 时设置为 `-1`(智能时长)<br>否则使用用户选择的值 |
| `shot_type`(镜头类型)| Kling 多镜头模式(shot_type='multi')转换为 `intelligence`<br>其他保持原值 |
| `generate_audio`(生成声音)| Kling 视频编辑模式(first_clip_url 有值)时自动设置为 `false` |
| `video_list`(视频列表)| Kling 视频编辑/参考视频生视频模式时构建视频对象 |
### 参数校验规则
生成前系统会进行以下校验:
| 校验项 | 条件 | 错误提示 |
|--------|------|---------|
| 提示词 | 非 Wan I2V 模式且无提示词,且非 Kling 自定义多镜头 | 请填写生成视频的提示词! |
| Wan I2V 首帧 | Wan I2V 模式且生成类型为首尾帧生视频,无首帧图片 | 请上传首帧图片! |
| Wan2.7 I2V 续写 | methodType=14 且生成类型为续写模式,无续写视频 | 请上传续写视频! |
| Kling 首尾帧/参考图 | Kling 首尾帧模式无首帧图片且无参考主体<br>或参考图模式无参考图片且无参考主体 | 请上传首帧图片或选择参考主体!<br>或:请至少上传一张参考图片或选择一个参考主体! |
| Kling 自定义多镜头 | Kling 自定义多镜头模式,分镜时长或提示词为空 | 分镜信息的时长不能为空或为0,镜头描述不能为空! |
| Kling 视频编辑 | Kling 视频编辑模式且生成类型为 EDIT/FEATURE,无编辑视频 | 请上传编辑视频/参考视频! |
| Wan R2V 数量 | Wan R2V 模式,参考图片+参考视频总数为0或大于5 | 上传的参考图片+参考视频总数不能为0且不能大于5! |
| 尾帧图片 | 有尾帧图片但无首帧图片 | 请上传首帧图片! |
### 使用示例
```python
from scripts.generate_video import generate_video
# 1. 模型切换示例 - 切换到 W2.6t 自动变为文生视频
result = generate_video(
prompt="海边日落",
model="W2.6t"
# generation_type 会自动设置为 "TEXT"
)
# 2. 模型切换示例 - 切换到 Kling 自动变为多镜头模式
result = generate_video(
prompt="电影预告片",
model="KlingV3Omni"
# shot_type 会自动设置为 "multi"
# multi_shot 会自动设置为 True
)
# 3. 自定义多镜头模式
result = generate_video(
prompt="",
model="KlingV3Omni",
shot_type="customize",
multi_prompt=[
{"index": 1, "prompt": "镜头1描述", "duration": 3},
{"index": 2, "prompt": "镜头2描述", "duration": 3}
],
duration=6
)
# 4. Kling 视频编辑模式(自动关闭音频生成)
result = generate_video(
prompt="编辑这段视频",
model="KlingV3Omni",
generation_type="EDIT",
first_clip_url="https://example.com/video.mp4",
keep_original_sound=True
# generate_audio 会自动设置为 False
)
# 5. Sora2 Pro - 分辨率与比例联动
result = generate_video(
prompt="风景大片",
model="Sora2Pro",
resolution="2K", # 2K 分辨率时比例会自动推荐 7:4
ratio="7:4"
)
# 6. Wan R2V - 多参考素材
result = generate_video(
prompt="风格迁移",
model="W2.7r",
image_url_list=["https://example.com/img1.jpg", "https://example.com/img2.jpg"],
video_url_list=["https://example.com/style.mp4"]
# 总数不能超过 5 个(图片+视频)
)
```
### 使用示例
```python
from scripts.generate_image import generate_image
# 模型切换时的自动参数调整示例
# 1. 切换到 N1 模型时,quality 自动变为 "1K"
result = generate_image(
prompt="一只猫",
model="N1" # quality 会自动设为 "1K"
)
# 2. 切换到 S5.0L 模型时,web_search 自动开启
result = generate_image(
prompt="2024年流行的设计趋势",
model="S5.0L" # web_search 会自动设为 True
)
# 3. 手动覆盖自动参数(按此优先级:用户指定 > 系统默认)
result = generate_image(
prompt="一只猫",
model="N1",
quality="2K" # 手动指定会覆盖系统的 "1K" 默认值
)
```
## 视频模型输入限制参数
### 各视频模型的输入限制
根据选择的模型,系统会自动应用以下限制参数(图片、音频、视频上传):
#### 图片上传限制
| methodType | 模型名称 | 支持格式 | maxSize (MB) | maxLength (px) | minLength (px) | textLength (字) | maxQuantity (张) | 特殊说明 |
|------------|---------|---------|--------------|----------------|----------------|-----------------|------------------|---------|
| auto | Auto | .jpeg,.jpg,.png,.webp | 10 | 2000 | 360 | 500 | - | 最长边≤2000px,最短边≥360px |
| 0 | Seedance1.0 Pro | .jpeg,.jpg,.png,.webp,.bmp,.tiff,.gif | 30 | 6000 | 300 | 500 | - | 宽高比 (0.4, 2.5) |
| 1 | Sora2 Beta | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 不得包含真人或拟真人图像 |
| 2 | Seedance1.5 Pro | .jpeg,.jpg,.png,.webp,.bmp,.heic,.heif,.tiff,.gif | 30 | 6000 | 300 | 500 | - | 宽高比 (0.4, 2.5) |
| 3 | Veo3.1 Fast Lite | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | 2 | 支持负向提示词(250字) |
| 4 | Veo3.1 Pro Lite | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 支持负向提示词(250字) |
| 5 | Veo3.1 Fast | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 支持负向提示词(250字) |
| 6 | Veo3.1 Pro | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 支持负向提示词(250字) |
| 8 | Wan2.6 i2v | .jpeg,.jpg,.png,.bmp,.webp | 10 | 2000 | 360 | 750 | - | 宽高比 [1:8, 8:1] |
| 9 | Wan2.6 r2v | .jpeg,.jpg,.png,.bmp,.webp | 10 | 5000 | 240 | 750 | 5 | 图片+视频≤5 |
| 10 | Kling V3 Omni | .jpeg,.jpg,.png | 10 | - | 300 | 1250 | 7 | 宽高比 [1:2.5, 2.5:1] |
| 11 | Sora2 | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 图片比例必须符合生成比例 |
| 12 | Sora2 Pro | .jpeg,.jpg,.png,.webp | 10 | 6000 | 300 | 2500 | - | 图片比例必须符合生成比例 |
| 14 | Wan2.7 i2v | .jpeg,.jpg,.png,.bmp,.webp | 20 | 8000 | 240 | 2500 | - | 宽高比 [1:8, 8:1] |
| 16 | Wan2.7 r2v | .jpeg,.jpg,.png,.bmp,.webp | 10 | 8000 | 240 | 2500 | 5 | 图片+视频≤5,宽高比 [1:8, 8:1] |
#### 音频上传限制
| methodType | 模型名称 | 支持格式 | maxSize (MB) | maxLength (秒) | minLength (秒) | 说明 |
|------------|---------|---------|--------------|----------------|----------------|------|
| 7 | Wan2.6 t2v | .wav,.mp3 | 15 | 30 | 3 | 时长超出视频则截取,不足则无声 |
| 8 | Wan2.6 i2v | .wav,.mp3 | 15 | 30 | 3 | 时长超出视频则截取,不足则无声 |
| 14 | Wan2.7 i2v | .wav,.mp3 | 15 | 30 | 3 | 时长超出视频则截取,不足则无声 |
| 15 | Wan2.7 t2v | .wav,.mp3 | 15 | 30 | 3 | 时长超出视频则截取,不足则无声 |
| 16 | Wan2.7 r2v | .wav,.mp3 | 15 | 10 | 2 | 用于指定参考素材中主体角色的音色 |
#### 视频上传限制
| methodType | 模型名称 | 支持格式 | maxSize (MB) | maxLength (秒) | minLength (秒) | maxQuantity | 说明 |
|------------|---------|---------|--------------|----------------|----------------|-------------|------|
| 9 | Wan2.6 r2v | .mp4,.mov | 100 | 30 | 1 | 3 | 图片+视频≤5 |
| 10 | Kling V3 Omni | .mp4,.mov | 200 | 10 | 3 | - | 视频编辑/参考视频生视频 |
| 14 | Wan2.7 i2v | .mp4,.mov | 100 | 10 | 2 | 1 | 视频续写模式,宽高比 [1:8, 8:1] |
| 16 | Wan2.7 r2v | .mp4,.mov | 100 | 30 | 1 | 3 | 图片+视频≤5 |
### 负向提示词支持
| methodType | 模型名称 | negativeTextLength (字) |
|------------|---------|------------------------|
| 3 | Veo3.1 Fast Lite | 250 |
| 4 | Veo3.1 Pro Lite | 250 |
| 5 | Veo3.1 Fast | 250 |
| 6 | Veo3.1 Pro | 250 |
| 7 | Wan2.6 t2v | 250 |
| 8 | Wan2.6 i2v | 250 |
| 9 | Wan2.6 r2v | 250 |
| 14 | Wan2.7 i2v | 250 |
| 15 | Wan2.7 t2v | 250 |
| 16 | Wan2.7 r2v | 250 |
### Kling V3 Omni 特殊限制
当使用 Kling V3 Omni 模型时,参考图片数量限制根据是否有编辑视频动态变化:
| 场景 | 参考图片 + 多图主体数量限制 |
|------|---------------------------|
| 无编辑视频/参考视频 | ≤ 7 |
| 有编辑视频/参考视频 | ≤ 4 |
### 参数说明
| 参数 | 类型 | 说明 |
|------|------|------|
| `targetMaxSize` | int (MB) | 上传文件的最大大小限制 |
| `targetMinLength` | int (px/秒) | 图片最短边像素 / 音视频最短时长 |
| `targetMaxLength` | int (px/秒) | 图片最长边像素 / 音视频最长时长 |
| `targetTextLength` | int (字) | 提示词的最大长度限制 |
| `targetNegativeTextLength` | int (字) | 负向提示词的最大长度限制 |
| `targetMaxQuantity` | int | 单次最多上传文件数量 |
| `targetAccept` | string | 支持的文件格式 |
| `targetUploadTips` | string | 上传说明提示 |
## 视频分辨率映射规则
### 质量等级与分辨率对照表
系统根据选择的 `resolution`(视频质量)和 `ratio`(画面比例)自动计算输出视频的分辨率(宽 x 高)。
| 质量 | 1:1 | 16:9 | 9:16 | 3:4 | 4:3 | 7:4 | 4:7 |
|------|-----|------|------|-----|-----|-----|-----|
| **720p** | 960x960 | 1280x720 | 720x1280 | 832x1088 | 1088x832 | - | - |
| **1080p** | 1440x1440 | 1920x1080 | 1080x1920 | 1248x1632 | 1632x1248 | - | - |
| **2K** | - | - | - | - | - | 1792x1024 | 1024x1792 |
### 不同视频模型的尺寸输出格式
| 模型类型 | methodType | 输出格式 | 示例 |
|---------|-----------|---------|------|
| Wan2.6/2.7 系列 (T2V/R2V) | 7, 9, 15, 16 | `{width}*{height}` | `1280*720` |
| Sora2 系列 | 11, 12 | `{width}x{height}` | `1280x720` |
| 其他视频模型 | 其他 | 比例字符串 | `16:9`、`9:16` |
### 分辨率计算示例
```python
from scripts.generate_video import get_video_resolution
# 获取 1080p 质量、16:9 比例的分辨率
resolution = get_video_resolution(quality="1080p", ratio="16:9")
print(resolution) # 输出: [1920, 1080]
# 获取 720p 质量、1:1 比例的分辨率
resolution = get_video_resolution(quality="720p", ratio="1:1")
print(resolution) # 输出: [960, 960]
# 获取 2K 质量、7:4 比例的分辨率
resolution = get_video_resolution(quality="2K", ratio="7:4")
print(resolution) # 输出: [1792, 1024]
# 仅获取质量对应的所有分辨率
resolutions = get_video_resolution(quality="1080p")
print(resolutions) # 输出: {'1:1': [1440, 1440], '16:9': [1920, 1080], '9:16': [1080, 1920], '3:4': [1248, 1632], '4:3': [1632, 1248]}
```
## 视频提示词写作建议
推荐书写模版:主体 + 运动,背景 + 运动,镜头 + 运动 ...
1. 基础结构:图生视频已经有了场景,因此尽量减少(甚至避免)对静止/无变化部分的描述,在明确指出运动对象的情况下,多描述运动的部分,包括主体的运动、背景的运动/变化、以及镜头的运动。
2. 简单直接:尽量使用简单词语和句子结构,模型会根据我们的表达与对图像画面的理解进行提示词扩写,生成符合预期的视频。
3. 特征描述:当主体具有一些突出特征时,可以加上突出特征来更好定位主体,比如老人、戴墨镜的女人等。描述运动时,关键的程度副词一定要明确,比如快速、幅度大。
4. 遵从图片:需要基于输入的图片内容来写,需要明确写出主体以及想做的动作或者运镜,需注意提示词不要与图片内容/基础参数存在事实矛盾。
5. 负向提示词:部分模型不响应负向提示词(如 Kling V3 Omni),请查阅上方各模型说明。
## 返回字段
| 字段 | 说明 |
|------|------|
| `status` | SUCCESS / FAILED / TIMEOUT |
| `url` | 媒体文件URL |
| `message` | 状态描述 |
| `local_path` | 本地保存路径(需 --download) |
| `data_uri` | Base64 Data URI(需 --download) |
| `image_data` | 原始图片字节(需 --download) |
## 环境配置
### 必需配置 - API Key
**重要:使用前必须设置你自己的 API Key!**
#### 获取 API Key
1. 访问 [https://ai.deepsop.com/](https://ai.deepsop.com/)
2. 注册并登录账号
3. 在控制台创建你的 API Key
4. 复制生成的 API Key(格式:`sk-xxxxxx...`)
#### 方式 1:使用 .env 文件(推荐)
1. 复制 `.env.example` 为 `.env`:
```bash
cp .env.example .env
```
2. 编辑 `.env` 文件,填入你的 API Key:
```bash
AI_ARTIST_TOKEN=sk-your_api_key_here
```
3. 在运行脚本前加载环境变量:
```bash
# Linux/macOS/Git Bash
source .env
# 或使用 export
export $(cat .env | xargs)
```
#### 方式 2:直接设置环境变量
##### Linux / macOS / Git Bash (Windows)
```bash
export AI_ARTIST_TOKEN="sk-your_api_key_here"
```
为了永久生效,将上述命令添加到 `~/.bashrc` 或 `~/.zshrc` 文件中。
##### Windows PowerShell
```powershell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
```
永久设置(系统级):
```powershell
[System.Environment]::SetEnvironmentVariable('AI_ARTIST_TOKEN', 'sk-your_api_key_here', 'User')
```
##### Windows CMD
```cmd
set AI_ARTIST_TOKEN=sk-your_api_key_here
```
#### 验证配置
运行以下命令验证 API Key 是否设置成功:
```bash
# Linux/macOS/Git Bash
echo $AI_ARTIST_TOKEN
# Windows PowerShell
echo $env:AI_ARTIST_TOKEN
# Windows CMD
echo %AI_ARTIST_TOKEN%
```
如果输出为空或显示默认值,说明环境变量未正确设置。
#### 测试配置(推荐)
运行配置测试脚本,验证 API Key 是否正确设置:
```bash
python3 scripts/test_config.py
```
该脚本会检查:
- API Key 是否已设置
- 是否使用了默认 Key(需要替换为你自己的)
- 配置是否可以正常使用
### 可选配置 - 飞书通知
```bash
export FEISHU_WEBHOOK_URL="https://open.feishu.cn/open-apis/bot/v2/hook/xxx"
```
## 相关文件
- `scripts/generate_image.py` - 图片生成脚本
- `scripts/generate_video.py` - 视频生成脚本
- `references/api.md` - API 详细文档
FILE:README.md
# AI Image Generator
基于 AI Artist API 的图片/视频异步生成工具。
- 支持图片与视频任务创建
- 自动轮询任务状态直到完成
- 支持本地参考图自动上传
- 创建任务前自动调用费用预估,余额不足时会拦截并提示充值
## 🚀 快速开始
### 1) 获取 API Key
访问 [https://ai.deepsop.com/](https://ai.deepsop.com/) 注册登录后,在控制台创建 API Key。
### 2) 设置环境变量
```bash
# Linux/macOS/Git Bash
export AI_ARTIST_TOKEN="sk-your_api_key_here"
```
```powershell
# Windows PowerShell
$env:AI_ARTIST_TOKEN="sk-your_api_key_here"
```
### 3) 验证配置
```bash
python3 scripts/test_config.py
```
### 4) 开始生成
```bash
# 默认图片模型(SEEDREAM5_0)
python3 scripts/generate_image.py "一只可爱的猫"
```
## 🎨 支持模型
### 图片模型
- `SEEDREAM5_0`(默认)
- `NANO_BANANA_2`
### 视频模型
- `SEEDANCE_1_5_PRO`
- `VEO3.1FAST_LITE`
- `VEO3.1PRO_LITE`
- `VEO3.1FAST`
- `VEO3.1PRO`
- `WAN2.6_T2V`
- `WAN2.6_I2V`
- `WAN2.6_R2V`
## 📝 常用示例
```bash
# 图片:指定模型
python3 scripts/generate_image.py "一只柴犬" --model NANO_BANANA_2
# 图片:下载到本地
python3 scripts/generate_image.py "海边日落" --download
# 图片:参考图生成(本地文件自动上传)
python3 scripts/generate_image.py "做成赛博朋克风格" --reference-image "./ref.png"
# 视频:基础文生视频
python3 scripts/generate_image.py "城市夜景延时" --model SEEDANCE_1_5_PRO
# 视频:首尾帧控制
python3 scripts/generate_image.py "灯具变形动画" --model VEO3.1PRO --first-image "./start.jpg" --last-image "./end.jpg"
```
## 📖 文档
完整参数说明与更多示例见 `SKILL.md`。
## 🔧 环境要求
- Python 3.6+
- `requests`
## ⚠️ 注意事项
- 必须使用你自己的 `AI_ARTIST_TOKEN`
- 任务创建前会执行费用预估;若余额不足将不会提交任务
- 请遵守 AI Artist API 的使用条款
FILE:scripts/generate_image.py
#!/usr/bin/env python3
"""
AI Image Generator - Async Image Generation Script
Calls the AI Artist API to generate images from text prompts.
Handles async task polling until completion.
Supports Feishu webhook callback for result notification.
Set FEISHU_WEBHOOK_URL environment variable to enable.
Supports local file upload for reference images/videos.
Local files are automatically uploaded to get public URLs before calling generation APIs.
"""
import requests
import json
import time
import sys
import argparse
import os
import base64
from pathlib import Path
# Configuration
API_PREFIX = "https://ai.deepsop.com/prod-api/"
BASE_URL = f"{API_PREFIX.rstrip('/')}/ai"
FILE_UPLOAD_URL = f"{API_PREFIX.rstrip('/')}/system/fileUpload/upload"
ESTIMATE_COST_URL = f"{BASE_URL}/estimate/cost"
RECHARGE_URL = "https://ai.deepsop.com/"
# Get API key from environment variable (required)
API_KEY = os.environ.get("AI_ARTIST_TOKEN")
# Feishu webhook configuration (optional)
FEISHU_WEBHOOK_URL = os.environ.get("FEISHU_WEBHOOK_URL")
def check_api_key():
"""Check if user has set their API key."""
if not API_KEY:
print("错误:未配置 AI_ARTIST_TOKEN 环境变量", file=sys.stderr)
print("", file=sys.stderr)
print("请先设置你的 API Key:", file=sys.stderr)
print(" export AI_ARTIST_TOKEN=\"sk-your_api_key_here\"", file=sys.stderr)
print("", file=sys.stderr)
print("验证配置:", file=sys.stderr)
print(" python3 scripts/test_config.py", file=sys.stderr)
print("", file=sys.stderr)
sys.exit(1)
return True
def get_headers():
"""Build request headers with API key."""
return {
"Content-Type": "application/json",
"X-Api-Key": API_KEY
}
def estimate_generation_cost(payload):
try:
response = requests.post(ESTIMATE_COST_URL, json=payload, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
print(f"费用预估失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return False
data = result.get("data") or {}
estimated_cost = data.get("estimatedCost")
sufficient_balance = data.get("sufficientBalance")
if estimated_cost is not None:
print(f"预估费用:{estimated_cost} K币")
if sufficient_balance is True:
print("余额充足,正在创建任务")
return True
if sufficient_balance is False:
print(f"余额不足,无法提交创建任务。请前往 {RECHARGE_URL} 充值 K 币后重试。", file=sys.stderr)
return False
print("费用预估返回结果不完整", file=sys.stderr)
return False
except requests.exceptions.RequestException as e:
print(f"费用预估网络错误:{e}", file=sys.stderr)
return False
except ValueError as e:
print(f"费用预估响应解析失败:{e}", file=sys.stderr)
return False
def upload_file(file_path):
"""
Upload a local file to the file server and get a public URL.
Args:
file_path: Path to the local file
Returns:
str: Public URL of the uploaded file, or None if failed
"""
if not os.path.exists(file_path):
print(f"文件不存在:{file_path}", file=sys.stderr)
return None
try:
with open(file_path, 'rb') as f:
files = {'file': (os.path.basename(file_path), f)}
headers = {'X-Api-Key': API_KEY}
response = requests.post(FILE_UPLOAD_URL, headers=headers, files=files, timeout=60)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
url = result.get("url")
print(f"文件已上传:{file_path} → {url}")
return url
else:
print(f"文件上传失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except Exception as e:
print(f"文件上传错误:{e}", file=sys.stderr)
return None
def download_image(url, output_path=None):
"""
Download image from URL.
Args:
url: Image URL
output_path: Optional path to save the image
Returns:
bytes: Image data, or None if failed
"""
try:
response = requests.get(url, timeout=60)
response.raise_for_status()
image_data = response.content
# Save to file if path provided
if output_path:
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'wb') as f:
f.write(image_data)
print(f"图片已保存:{output_path}")
return image_data
except Exception as e:
print(f"下载图片失败:{e}", file=sys.stderr)
return None
def image_to_data_uri(image_data, mime_type="image/png"):
"""
Convert image bytes to data URI.
Args:
image_data: Raw image bytes
mime_type: MIME type of the image
Returns:
str: Data URI string
"""
base64_data = base64.b64encode(image_data).decode('utf-8')
return f"data:{mime_type};base64,{base64_data}"
def send_feishu_message(prompt, result):
"""Send generation result to Feishu chat."""
if not FEISHU_WEBHOOK_URL:
return False
try:
if result and result["status"] == "SUCCESS":
content = {
"msg_type": "interactive",
"card": {
"header": {
"title": {"tag": "plain_text", "content": "图片生成成功"},
"template": "green"
},
"elements": [
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": f"**提示词**: {prompt}\n\n**图片链接**: [点击查看]({result['url']})"
}
},
{
"tag": "action",
"actions": [{
"tag": "button",
"text": {"tag": "plain_text", "content": "打开图片"},
"url": result["url"],
"type": "default"
}]
}
]
}
}
else:
error_msg = result.get("message", "未知错误") if result else "未知错误"
content = {
"msg_type": "interactive",
"card": {
"header": {
"title": {"tag": "plain_text", "content": "图片生成失败"},
"template": "red"
},
"elements": [{
"tag": "div",
"text": {
"tag": "lark_md",
"content": f"**提示词**: {prompt}\n\n**错误**: {error_msg}"
}
}]
}
}
response = requests.post(
FEISHU_WEBHOOK_URL,
json=content,
headers={"Content-Type": "application/json"},
timeout=30
)
response.raise_for_status()
return True
except Exception as e:
print(f"[Feishu] 发送通知失败:{e}", file=sys.stderr)
return False
# Model configurations
# media_type: "image" or "video" — determines task creation and output handling
MODEL_CONFIGS = {
"SEEDREAM5_0": {
"media_type": "image",
"type": "10",
"methodType": "4",
"default_size": "2048x2048",
"default_quality": "2K",
"extra_params": {"duration": 10}
},
"NANO_BANANA_2": {
"media_type": "image",
"type": "10",
"methodType": "5",
"default_size": "1:1",
"default_quality": "2K",
"extra_params": {}
},
"SEEDANCE_1_5_PRO": {
"media_type": "video",
"type": "9",
"methodType": "2",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"firstImageUrl": None,
"lastImageUrl": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 30,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"SORA2": {
"media_type": "video",
"type": "9",
"methodType": "11",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 4,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"firstImageUrl": None,
"lastImageUrl": None,
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"scaleFactor": 0.5,
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000,
"imageUrlList": [],
"videoUrlList": [],
"durationList": []
}
},
"VEO3.1FAST_LITE": {
"media_type": "video",
"type": "9",
"methodType": "3",
"default_ratio": "16:9",
"default_resolution": "1080p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"VEO3.1PRO_LITE": {
"media_type": "video",
"type": "9",
"methodType": "4",
"default_ratio": "adaptive",
"default_resolution": "720p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"VEO3.1FAST": {
"media_type": "video",
"type": "9",
"methodType": "5",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"VEO3.1PRO": {
"media_type": "video",
"type": "9",
"methodType": "6",
"default_ratio": "16:9",
"default_resolution": "1080p",
"default_duration": 8,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 300,
"targetMaxLength": 6000
}
},
"WAN2.6_T2V": {
"media_type": "video",
"type": "9",
"methodType": "7",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "TEXT",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 360,
"targetMaxLength": 2000
}
},
"WAN2.6_I2V": {
"media_type": "video",
"type": "9",
"methodType": "8",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "FIRST&LAST",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 360,
"targetMaxLength": 2000
}
},
"WAN2.6_R2V": {
"media_type": "video",
"type": "9",
"methodType": "9",
"default_ratio": "16:9",
"default_resolution": "720p",
"default_duration": 10,
"extra_params": {
"generationType": "REFERENCE",
"negativePrompt": "",
"imageUrlList": None,
"firstImageUrl": None,
"lastImageUrl": None,
"audioUrl": None,
"videoUrlList": None,
"durationList": [],
"enhancePrompt": False,
"generateAudio": True,
"n": 1,
"personGeneration": "allow_adult",
"resizeMode": "pad",
"promptExtend": False,
"shotType": "single",
"durationSwitch": "1",
"targetMaxSize": 10,
"targetMinLength": 240,
"targetMaxLength": 5000
}
}
}
def create_video_task(prompt, model="SEEDANCE_1_5_PRO", ratio=None, resolution=None,
duration=None, first_image_url=None, last_image_url=None,
generate_audio=None, scale_factor=None, generation_type=None,
enhance_prompt=None, prompt_extend=None, audio_url=None,
image_url_list=None, video_url_list=None):
"""Create a video generation task.
Args:
prompt: Text description of the video
model: Video model to use (e.g. SEEDANCE_1_5_PRO, SORA2)
ratio: Aspect ratio, e.g. '16:9', '9:16', '1:1'
resolution: Video resolution, e.g. '720p', '1080p'
duration: Video duration in seconds
first_image_url: URL of the first frame image (SORA2 FIRST&LAST mode)
last_image_url: URL of the last frame image (SORA2 FIRST&LAST mode)
generate_audio: Whether to generate audio (True/False)
scale_factor: Scale factor for SORA2 (e.g. 0.5)
generation_type: Generation type override, e.g. 'FIRST&LAST', 'TEXT'
enhance_prompt: Whether to enhance the prompt
prompt_extend: Whether to extend the prompt
audio_url: URL of audio file (WAN2.6 series)
image_url_list: List of image URLs for reference (WAN2.6_R2V)
video_url_list: List of video URLs for reference (WAN2.6_R2V)
"""
url = f"{BASE_URL}/AiArtistRecord"
if model not in MODEL_CONFIGS or MODEL_CONFIGS[model]["media_type"] != "video":
print(f"不支持的视频模型:{model}", file=sys.stderr)
return None
config = MODEL_CONFIGS[model]
effective_ratio = ratio or config.get("default_ratio", "16:9")
effective_resolution = resolution or config.get("default_resolution", "720p")
effective_duration = duration or config.get("default_duration", 10)
parameter = dict(config["extra_params"]) # copy defaults
# Resolve pixel size from ratio + resolution
resolution_size_map = {
("16:9", "720p"): "1280x720",
("16:9", "1080p"): "1920x1080",
("9:16", "720p"): "720x1280",
("9:16", "1080p"): "1080x1920",
("1:1", "720p"): "720x720",
("1:1", "1080p"): "1080x1080",
("3:4", "720p"): "720x960",
("3:4", "1080p"): "1080x1440",
("4:3", "720p"): "960x720",
("4:3", "1080p"): "1440x1080",
}
pixel_size = resolution_size_map.get((effective_ratio, effective_resolution), effective_ratio)
parameter.update({
"methodType": config["methodType"],
"text": prompt,
"resolution": effective_resolution,
"ratio": effective_ratio,
"size": pixel_size,
"duration": effective_duration,
})
# VEO3.1 series: duration must be 4 or 8 seconds
if model in ["VEO3.1FAST_LITE", "VEO3.1PRO_LITE", "VEO3.1FAST", "VEO3.1PRO"]:
# Validate duration (must be 4 or 8)
if effective_duration not in [4, 8]:
print(f"VEO3.1 系列模型时长必须是 4 或 8 秒,当前 {effective_duration} 秒,自动调整为 8 秒")
effective_duration = 8
parameter["duration"] = effective_duration
# For VEO3.1, size should match ratio (not pixel resolution)
parameter["size"] = effective_ratio
# WAN2.6 series: duration must be 3-15 seconds
if model in ["WAN2.6_T2V", "WAN2.6_I2V", "WAN2.6_R2V"]:
# Validate duration (must be 3-15)
if effective_duration < 3 or effective_duration > 15:
print(f"WAN2.6 系列模型时长必须是 3-15 秒,当前 {effective_duration} 秒,自动调整为 10 秒")
effective_duration = 10
parameter["duration"] = effective_duration
# For WAN2.6, size should be pixel format for some models
if model == "WAN2.6_T2V":
parameter["size"] = f"{pixel_size.replace('x', '*')}" # e.g., "1280*720"
elif model == "WAN2.6_I2V":
parameter["size"] = effective_ratio # e.g., "16:9"
elif model == "WAN2.6_R2V":
parameter["size"] = f"{pixel_size.replace('x', '*')}" # e.g., "1280*720"
# SORA2: auto-switch generationType based on image inputs
if model == "SORA2" and generation_type is None:
if first_image_url or last_image_url:
parameter["generationType"] = "FIRST&LAST"
else:
parameter["generationType"] = "FIRST&LAST" # text-to-video also uses FIRST&LAST with null image URLs
# WAN2.6_I2V: auto-switch generationType based on first_image_url
if model == "WAN2.6_I2V" and generation_type is None:
if first_image_url:
parameter["generationType"] = "FIRST&LAST"
else:
parameter["generationType"] = "FIRST&LAST"
# WAN2.6_R2V: set generationType to REFERENCE
if model == "WAN2.6_R2V":
parameter["generationType"] = "REFERENCE"
# Apply optional overrides
if first_image_url is not None:
parameter["firstImageUrl"] = first_image_url
if last_image_url is not None:
parameter["lastImageUrl"] = last_image_url
if generate_audio is not None:
parameter["generateAudio"] = generate_audio
if scale_factor is not None:
parameter["scaleFactor"] = scale_factor
if generation_type is not None:
parameter["generationType"] = generation_type
if enhance_prompt is not None:
parameter["enhancePrompt"] = enhance_prompt
if prompt_extend is not None:
parameter["promptExtend"] = prompt_extend
# WAN2.6 series: audio_url, image_url_list, video_url_list
if audio_url is not None:
parameter["audioUrl"] = audio_url
if image_url_list is not None:
parameter["imageUrlList"] = image_url_list
if video_url_list is not None:
parameter["videoUrlList"] = video_url_list
payload = {
"type": config["type"],
"methodType": config["methodType"],
"parameter": json.dumps(parameter)
}
if not estimate_generation_cost(payload):
return None
try:
response = requests.post(url, json=payload, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") == 200 and result.get("data"):
return result["data"][0]
else:
print(f"创建视频任务失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
print(f"网络错误:{e}", file=sys.stderr)
return None
def generate_video(prompt, model="SEEDANCE_1_5_PRO", ratio=None, resolution=None,
duration=None, poll_interval=5, first_image_url=None,
last_image_url=None, generate_audio=None, scale_factor=None,
generation_type=None, enhance_prompt=None, prompt_extend=None,
first_image_path=None, last_image_path=None, audio_url=None,
image_url_list=None, video_url_list=None, audio_path=None):
"""Generate a video from a text prompt.
Args:
prompt: Text description of the video
model: Video model to use (e.g. SEEDANCE_1_5_PRO, SORA2)
ratio: Aspect ratio (e.g. '16:9')
resolution: Video resolution (e.g. '720p')
duration: Video duration in seconds
poll_interval: Polling interval in seconds
first_image_url: URL of the first frame image (SORA2 FIRST&LAST mode)
last_image_url: URL of the last frame image (SORA2 FIRST&LAST mode)
generate_audio: Whether to generate audio
scale_factor: Scale factor for SORA2
generation_type: Generation type override
enhance_prompt: Whether to enhance the prompt
prompt_extend: Whether to extend the prompt
first_image_path: Local path to first frame image (auto-uploaded)
last_image_path: Local path to last frame image (auto-uploaded)
audio_url: URL of audio file (WAN2.6 series)
audio_path: Local path to audio file (auto-uploaded, WAN2.6 series)
image_url_list: List of image URLs for reference (WAN2.6_R2V)
video_url_list: List of video URLs for reference (WAN2.6_R2V)
Returns:
dict with 'status', 'url', 'message'
"""
# Upload local files to get URLs if provided
if first_image_path and not first_image_url:
first_image_url = upload_file(first_image_path)
if last_image_path and not last_image_url:
last_image_url = upload_file(last_image_path)
if audio_path and not audio_url:
audio_url = upload_file(audio_path)
config = MODEL_CONFIGS.get(model, {})
effective_ratio = ratio or config.get("default_ratio", "16:9")
effective_resolution = resolution or config.get("default_resolution", "720p")
effective_duration = duration or config.get("default_duration", 10)
print(f"正在生成视频:{prompt}")
print(f" 模型:{model} | 分辨率:{effective_resolution} | 比例:{effective_ratio} | 时长:{effective_duration}s")
if first_image_url:
print(f" 首帧图片:{first_image_url}")
if last_image_url:
print(f" 尾帧图片:{last_image_url}")
if audio_url:
print(f" 音频:{audio_url}")
if image_url_list:
print(f" 参考图片:{image_url_list}")
if video_url_list:
print(f" 参考视频:{video_url_list}")
task_id = create_video_task(
prompt, model, ratio, resolution, duration,
first_image_url=first_image_url,
last_image_url=last_image_url,
generate_audio=generate_audio,
scale_factor=scale_factor,
generation_type=generation_type,
enhance_prompt=enhance_prompt,
prompt_extend=prompt_extend,
audio_url=audio_url,
image_url_list=image_url_list,
video_url_list=video_url_list
)
if not task_id:
return None
print(f" 任务 ID: {task_id}")
result = poll_task_status(task_id, interval=poll_interval, max_wait=600)
if result and result["status"] == "SUCCESS":
print(f"视频生成成功!")
print(f" 视频链接:{result['url']}")
else:
print(f"视频生成失败:{result.get('message', '未知错误')}", file=sys.stderr)
return result
def create_generation_task(prompt, quality="2K", size=None, model="SEEDREAM5_0", reference_image_url=None):
"""Create an image generation task.
Args:
prompt: Text description of the image
quality: Image quality (2K/4K)
size: Image dimensions. SEEDREAM5_0 uses e.g. '2048x2048', NANO_BANANA_2 uses e.g. '1:1'
model: Model to use, one of: SEEDREAM5_0, NANO_BANANA_2
reference_image_url: Optional reference image URL for image-to-image generation
"""
url = f"{BASE_URL}/AiArtistRecord"
if model not in MODEL_CONFIGS:
print(f"不支持的模型:{model},可用模型:{list(MODEL_CONFIGS.keys())}", file=sys.stderr)
return None
config = MODEL_CONFIGS[model]
# Use model's default size if not specified
if size is None:
size = config["default_size"]
# Build image array - support reference image for image-to-image
image_array = []
if reference_image_url:
image_array = [reference_image_url]
parameter = {
"methodType": config["methodType"],
"prompt": prompt,
"image": image_array,
"quality": quality,
"size": size,
"webSearch": False,
"targetMaxSize": 10,
"targetMaxLength": 6000,
}
# Merge model-specific extra params
parameter.update(config["extra_params"])
payload = {
"type": config["type"],
"methodType": config["methodType"],
"parameter": json.dumps(parameter)
}
if not estimate_generation_cost(payload):
return None
try:
response = requests.post(url, json=payload, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") == 200 and result.get("data"):
return result["data"][0]
else:
print(f"创建任务失败:{result.get('msg', '未知错误')}", file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
print(f"网络错误:{e}", file=sys.stderr)
return None
def poll_task_status(task_id, interval=5, max_wait=600):
"""Poll the task status until completion or failure."""
url = f"{BASE_URL}/AiArtistImage/getInfoByArtistId/{task_id}"
elapsed = 0
last_status = None
while elapsed < max_wait:
try:
response = requests.get(url, headers=get_headers(), timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") != 200:
time.sleep(interval)
elapsed += interval
continue
data = result.get("data", {})
status = data.get("status", "")
# Only print status when it changes
if status != last_status:
print(f"{status} - {data.get('message', '')}")
last_status = status
if status == "SUCCESS":
return {
"status": "SUCCESS",
"url": data.get("url"),
"message": data.get("message", "生成成功")
}
elif status == "FAILED":
return {
"status": "FAILED",
"url": None,
"message": data.get("message", "生成失败")
}
else:
time.sleep(interval)
elapsed += interval
except requests.exceptions.RequestException as e:
print(f"查询状态出错:{e}", file=sys.stderr)
time.sleep(interval)
elapsed += interval
return {
"status": "TIMEOUT",
"url": None,
"message": f"超时({max_wait}秒)"
}
def generate_image(prompt, quality="2K", size=None, poll_interval=5,
download=False, output_dir=None, model="SEEDREAM5_0",
reference_image_path=None, reference_image_url=None):
"""
Main function to generate an image from a prompt.
Args:
prompt: Text description of the image
quality: Image quality (2K/4K)
size: Image dimensions. Defaults to model's default size if not specified.
SEEDREAM5_0: e.g. '2048x2048' | NANO_BANANA_2: e.g. '1:1'
poll_interval: Polling interval in seconds
download: Whether to download the image
output_dir: Directory to save the image (default: workspace/images)
model: Model to use. Options: SEEDREAM5_0, NANO_BANANA_2
reference_image_path: Local path to reference image (auto-uploaded)
reference_image_url: URL of reference image (if already uploaded)
Returns:
dict with generation result including 'url', 'local_path', 'data_uri' if successful
"""
config = MODEL_CONFIGS.get(model, {})
effective_size = size or config.get("default_size", "2048x2048")
# Upload reference image if local path provided
if reference_image_path and not reference_image_url:
reference_image_url = upload_file(reference_image_path)
print(f"正在生成:{prompt}")
print(f" 模型:{model} | 质量:{quality} | 尺寸:{effective_size}")
if reference_image_url:
print(f" 参考图:{reference_image_url}")
# Step 1: Create task
task_id = create_generation_task(prompt, quality, size, model, reference_image_url)
if not task_id:
return None
print(f" 任务 ID: {task_id}")
# Step 2: Poll until complete
result = poll_task_status(task_id, interval=poll_interval)
if result and result["status"] == "SUCCESS":
print(f"生成成功!")
print(f" 图片链接:{result['url']}")
# Download image if requested
if download and result.get("url"):
if not output_dir:
output_dir = os.path.join(os.path.expanduser("~"), ".openclaw", "workspace", "images")
# Generate filename from prompt
safe_prompt = "".join(c if c.isalnum() or c in (' ', '-', '_') else '_' for c in prompt)
safe_prompt = safe_prompt[:50].strip().replace(' ', '_')
filename = f"{safe_prompt}_{int(time.time())}.png"
output_path = os.path.join(output_dir, filename)
image_data = download_image(result["url"], output_path)
if image_data:
result["local_path"] = output_path
result["data_uri"] = image_to_data_uri(image_data)
result["image_data"] = image_data # Raw bytes for programmatic use
return result
else:
print(f"生成失败:{result.get('message', '未知错误')}", file=sys.stderr)
return result
if __name__ == "__main__":
# Check API key before proceeding
check_api_key()
image_models = [k for k, v in MODEL_CONFIGS.items() if v["media_type"] == "image"]
video_models = [k for k, v in MODEL_CONFIGS.items() if v["media_type"] == "video"]
all_models = list(MODEL_CONFIGS.keys())
parser = argparse.ArgumentParser(
description="AI 图片/视频生成器",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=f"图片模型:{', '.join(image_models)}\n视频模型:{', '.join(video_models)}"
)
parser.add_argument("prompt", help="生成提示词")
parser.add_argument("--model", default="SEEDREAM5_0",
choices=all_models,
help="生成模型 (默认:SEEDREAM5_0)")
# 图片专属参数
parser.add_argument("--quality", default="2K", help="[图片] 图片质量 (默认:2K)")
parser.add_argument("--size", default=None, help="[图片] 图片尺寸,不传则使用模型默认值")
parser.add_argument("--download", action="store_true", help="[图片] 下载图片到本地")
parser.add_argument("--output-dir", help="[图片] 图片保存目录")
parser.add_argument("--markdown-output", action="store_true", help="以 Markdown 格式输出图片链接")
parser.add_argument("--reference-image", default=None, help="[图片] 参考图本地路径,自动上传后作为 image-to-image 参考")
# 视频专属参数
parser.add_argument("--ratio", default=None, help="[视频] 画面比例,如 16:9、9:16、1:1 (默认:16:9)")
parser.add_argument("--resolution", default=None, help="[视频] 分辨率,如 720p、1080p (默认:720p)")
parser.add_argument("--duration", type=int, default=None, help="[视频] 视频时长 (秒) (默认:10)")
# SORA2 专属参数
parser.add_argument("--first-image-url", default=None, help="[SORA2] 首帧图片 URL(FIRST&LAST 模式)")
parser.add_argument("--last-image-url", default=None, help="[SORA2] 尾帧图片 URL(FIRST&LAST 模式)")
parser.add_argument("--first-image", default=None, help="[SORA2] 首帧图片本地路径,自动上传")
parser.add_argument("--last-image", default=None, help="[SORA2] 尾帧图片本地路径,自动上传")
parser.add_argument("--generate-audio", action="store_true", default=None, help="[SORA2] 生成音频")
parser.add_argument("--no-audio", action="store_true", help="[SORA2] 不生成音频")
parser.add_argument("--scale-factor", type=float, default=None, help="[SORA2] 缩放系数 (默认:0.5)")
parser.add_argument("--generation-type", default=None, help="[SORA2] 生成类型,如 FIRST&LAST、TEXT")
# 通用参数
parser.add_argument("--interval", type=int, default=5, help="轮询间隔秒数")
args = parser.parse_args()
media_type = MODEL_CONFIGS[args.model]["media_type"]
if media_type == "video":
# Resolve audio flag
gen_audio = None
if args.no_audio:
gen_audio = False
elif args.generate_audio:
gen_audio = True
result = generate_video(
prompt=args.prompt,
model=args.model,
ratio=args.ratio,
resolution=args.resolution,
duration=args.duration,
poll_interval=args.interval,
first_image_url=args.first_image_url,
last_image_url=args.last_image_url,
first_image_path=args.first_image,
last_image_path=args.last_image,
generate_audio=gen_audio,
scale_factor=args.scale_factor,
generation_type=args.generation_type
)
if result and result["status"] == "SUCCESS" and result.get("url"):
print(result["url"])
sys.exit(0)
elif result:
sys.exit(0 if result["status"] == "SUCCESS" else 1)
else:
sys.exit(1)
else:
result = generate_image(
prompt=args.prompt,
quality=args.quality,
size=args.size,
poll_interval=args.interval,
download=args.download,
output_dir=args.output_dir,
model=args.model,
reference_image_path=args.reference_image
)
# Send result to Feishu if webhook is configured
if FEISHU_WEBHOOK_URL:
send_feishu_message(args.prompt, result)
# Output based on --markdown-output flag
if args.markdown_output and result and result["status"] == "SUCCESS" and result.get("url"):
print(f"")
sys.exit(0)
elif result and result["status"] == "SUCCESS" and result.get("url"):
print(result["url"])
sys.exit(0)
elif result:
sys.exit(0 if result["status"] == "SUCCESS" else 1)
else:
sys.exit(1)
FILE:references/api.md
# AI Artist API 详细文档
## API 端点
### 1. 预估生成费用
**POST** `/ai/estimate/cost`
**请求头:**
```
Content-Type: application/json
X-Api-Key: <api_key>
```
**请求体:**
```json
{
"type": "10",
"methodType": "4",
"parameter": "{...}"
}
```
说明:请求体与创建生成任务时使用的参数完全一致,需要在正式创建任务前先调用本接口。
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"estimatedCost": 3.500000,
"sufficientBalance": true
}
}
```
当 `sufficientBalance` 为 `false` 时,表示余额不足,不应继续提交创建任务,需要提醒用户先充值 K 币。
### 2. 创建生成任务
**POST** `/ai/AiArtistRecord`
**请求头:**
```
Content-Type: application/json
X-Api-Key: <api_key>
```
**请求体:**
```json
{
"type": "10",
"methodType": "4",
"parameter": "{...}"
}
```
**支持的模型:**
| 模型名称 | methodType | 默认尺寸 | 说明 |
|----------|-----------|---------|------|
| `SEEDREAM5_0` | `"4"` | `2048x2048` | 默认模型,高质量图片生成 |
| `NANO_BANANA_2` | `"5"` | `1:1` | 轻量模型,支持比例尺寸格式 |
**parameter 字段说明:**
| 字段 | 类型 | 说明 |
|------|------|------|
| `methodType` | string | 模型对应值:SEEDREAM5_0="4",NANO_BANANA_2="5" |
| `prompt` | string | 图片生成提示词 |
| `image` | array | 参考图片(可选) |
| `quality` | string | 图片质量: "2K" / "4K" |
| `size` | string | 尺寸格式因模型而异:SEEDREAM5_0 用 "2048x2048",NANO_BANANA_2 用 "1:1" |
| `webSearch` | boolean | 是否启用网络搜索 |
| `targetMaxSize` | number | 目标最大尺寸 |
| `targetMaxLength` | number | 目标最大长度 |
| `duration` | number | 持续时间(仅 SEEDREAM5_0)|
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": ["<task_id>"]
}
```
**失败响应:**
```json
{
"msg": "错误信息",
"code": 400,
"data": null
}
```
### 3. 查询任务状态
**GET** `/ai/AiArtistImage/getInfoByArtistId/{artistId}`
**成功响应:**
```json
{
"msg": "操作成功",
"code": 200,
"data": {
"message": "生成成功",
"url": "https://...",
"status": "SUCCESS"
}
}
```
**状态值说明:**
| 状态 | 含义 |
|------|------|
| `PENDING` | 等待中 |
| `RUNNING` / `GENERATING` | 生成中 |
| `SUCCESS` | 生成成功 |
| `FAILED` | 生成失败 |
## 错误码
| Code | 含义 |
|------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权(token无效) |
| 429 | 请求过于频繁 |
| 500 | 服务器内部错误 |
## 完整请求示例
```bash
# 使用 SEEDREAM5_0 模型创建任务
curl -X POST "https://ai.deepsop.com/prod-api/ai/AiArtistRecord" \
-H "Content-Type: application/json" \
-H "X-Api-Key: <api_key>" \
-d '{
"type": "10",
"methodType": "4",
"parameter": "{\"methodType\":\"4\",\"prompt\":\"风景画\",\"image\":[],\"quality\":\"2K\",\"size\":\"2048x2048\",\"webSearch\":false,\"targetMaxSize\":10,\"targetMaxLength\":6000,\"duration\":10}"
}'
# 使用 NANO_BANANA_2 模型创建任务
curl -X POST "https://ai.deepsop.com/prod-api/ai/AiArtistRecord" \
-H "Content-Type: application/json" \
-H "X-Api-Key: <api_key>" \
-d '{
"type": "10",
"methodType": "5",
"parameter": "{\"methodType\":\"5\",\"prompt\":\"生成一只狗\",\"image\":[],\"quality\":\"2K\",\"size\":\"1:1\",\"webSearch\":false,\"targetMaxSize\":10,\"targetMaxLength\":6000}"
}'
# 查询状态
curl -X GET "https://ai.deepsop.com/prod-api/ai/AiArtistImage/getInfoByArtistId/<task_id>" \
-H "X-Api-Key: <api_key>"
```
FILE:references/chat-integration.md
# 在对话中直接返回图片的示例
## 示例 1: 使用 Markdown(最简单)
当用户请求生成图片时,直接返回:
```python
from scripts.generate_image import generate_image
result = generate_image(prompt="风景画")
if result and result["status"] == "SUCCESS":
# 直接在回复中使用 Markdown 图片语法
reply = f"生成成功!\n\n"
```
## 示例 2: 使用 message 工具发送图片
如果需要使用 message 工具发送图片(支持更多平台):
```python
from scripts.generate_image import generate_image
import base64
# 1. 生成并下载图片
result = generate_image(prompt="风景画", download=True)
if result and result["status"] == "SUCCESS":
# 2. 读取图片数据
with open(result["local_path"], "rb") as f:
image_bytes = f.read()
# 3. 转换为 base64
base64_image = base64.b64encode(image_bytes).decode('utf-8')
# 4. 使用 message 工具发送
# 注意:实际使用时通过 message 工具的 buffer 参数发送
```
## 示例 3: 直接在回复中包含图片
对于支持 Markdown 图片的平台(如 Discord、Telegram、WebChat):
```
用户: 生成一张风景画
助手: 正在生成...
[生成完成后]
助手: 生成成功!🎨

```
## 平台兼容性
| 平台 | Markdown 图片 | 说明 |
|------|--------------|------|
| WebChat | ✅ 支持 | 直接显示 |
| Discord | ✅ 支持 | 直接显示 |
| Telegram | ✅ 支持 | 直接显示 |
| 飞书 | ⚠️ 部分支持 | 建议使用卡片消息 |
| WhatsApp | ❌ 不支持 | 需要下载后发送 |
## 最佳实践
1. **优先使用 Markdown**: 最简单,大多数平台支持
2. **同时提供链接**: 以防图片加载失败
3. **下载选项**: 对于不支持 Markdown 的平台,使用 `--download` 参数
FILE:references/feishu-integration.md
# 飞书图片发送指南
## 飞书支持的图片发送方式
### 方式 1: 使用 message 工具发送图片(推荐)
通过 `message` 工具的 `buffer` 参数直接发送图片:
```python
import base64
# 读取图片文件
with open("image.png", "rb") as f:
image_data = f.read()
# 使用 message 工具发送
# message(
# action="send",
# buffer=base64.b64encode(image_data).decode(),
# filename="image.png",
# mimeType="image/png"
# )
```
### 方式 2: 使用飞书卡片消息(富文本)
飞书支持发送带有图片的交互式卡片:
```python
{
"msg_type": "interactive",
"card": {
"header": {
"title": {
"tag": "plain_text",
"content": "✅ 图片生成成功"
},
"template": "green"
},
"elements": [
{
"tag": "img",
"img_key": "", # 需要先在飞书上传图片获取 img_key
"alt": {
"tag": "plain_text",
"content": "生成的图片"
}
},
{
"tag": "div",
"text": {
"tag": "lark_md",
"content": "**提示词**: 风景画"
}
}
]
}
}
```
**注意**:使用 `img_key` 需要先将图片上传到飞书平台。
### 方式 3: 使用 Markdown 图片链接
飞书支持 Markdown 图片语法,但图片需要是公开可访问的 URL:
```markdown

```
**限制**:
- 图片 URL 必须是公网可访问的
- 不支持 base64 内嵌图片
## 最佳实践
### 对于 AI 图片生成场景
1. **生成图片后获取公开 URL**(如阿里云 OSS、AWS S3 等)
2. **使用 Markdown 语法发送**:
```markdown
生成成功!🎨

图片链接:https://your-cdn.com/image.png
```
3. **或者使用 message 工具直接发送图片数据**
### 代码示例
```python
from scripts.generate_image import generate_image
# 生成图片
result = generate_image(prompt="风景画")
if result and result["status"] == "SUCCESS":
# 方式 1: 使用 Markdown 发送图片 URL
reply = f"""生成成功!🎨

图片链接:{result['url']}"""
# 方式 2: 下载后使用 message 工具发送
# result = generate_image(prompt="风景画", download=True)
# with open(result["local_path"], "rb") as f:
# image_data = f.read()
# message(action="send", buffer=base64.b64encode(image_data).decode(), ...)
```
## 平台对比
| 平台 | Markdown 图片 | Base64 图片 | 卡片消息 | 说明 |
|------|--------------|-------------|----------|------|
| WebChat | ✅ 支持 | ✅ 支持 | ❌ 不支持 | 最灵活 |
| Discord | ✅ 支持 | ⚠️ 有限 | ✅ 支持 | 支持 embed |
| Telegram | ✅ 支持 | ✅ 支持 | ✅ 支持 | 支持多种方式 |
| **飞书** | ✅ 支持 | ❌ 不支持 | ✅ 支持 | 需公开 URL 或上传 |
| WhatsApp | ❌ 不支持 | ✅ 支持 | ❌ 不支持 | 需发送文件 |
## 飞书特殊说明
1. **Base64 图片不支持**:飞书不支持直接通过 base64 内嵌图片
2. **需要先上传**:使用卡片消息的 `img_key` 需要先将图片上传到飞书
3. **公开 URL 最方便**:使用 CDN 或对象存储的公开链接最简单
4. **Webhook 发送**:通过 Webhook 发送时,图片需要是公开可访问的
## 推荐的飞书集成方案
```python
# 方案 1: 使用公开 URL(最简单)
def send_image_to_feishu(image_url, prompt):
content = f"""✅ 图片生成成功
**提示词**: {prompt}

[打开图片]({image_url})"""
# 使用 message 工具发送 Markdown
# message(action="send", message=content)
# 方案 2: 下载后作为文件发送
def send_image_file(image_path):
with open(image_path, "rb") as f:
image_data = f.read()
# 使用 message 工具发送文件
# message(
# action="send",
# buffer=base64.b64encode(image_data).decode(),
# filename="generated_image.png",
# mimeType="image/png"
# )
```