add agnes-image
This commit is contained in:
parent
195bd49236
commit
ec2e0acea2
254
skills/linggan/agnes-image/SKILL.md
Normal file
254
skills/linggan/agnes-image/SKILL.md
Normal file
@ -0,0 +1,254 @@
|
||||
---
|
||||
name: agnes-image
|
||||
description: 使用 Agnes AI(OpenAI 兼容网关)生成与理解图片、生成视频。支持文生图、图生图(保持角色一致性)、贴纸/卡通形象生成、白底转透明 PNG、多模态图片理解(描述/分析/问答/OCR),以及文生视频、图生视频、多图视频、关键帧动画。
|
||||
category: Creative Generation
|
||||
---
|
||||
|
||||
# Agnes Image
|
||||
|
||||
本 Skill 封装了 Agnes AI 的图像生成能力(模型 `agnes-image-2.1-flash`),接口兼容 OpenAI 风格,支持文生图、图生图,并内置「白底转透明」后处理,适合批量生成游戏/应用所需的角色、图标、贴纸等素材。
|
||||
|
||||
## 前置条件
|
||||
|
||||
设置 API Key(二选一):
|
||||
|
||||
```bash
|
||||
export AGNES_API_KEY="sk-xxxxxxxx"
|
||||
```
|
||||
|
||||
或在每次调用时通过 `--api-key` 传入。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 文生图
|
||||
|
||||
生成单张图片,输出可访问的图片链接:
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_image.py --prompt "一只圆润萌系的小火龙,大眼睛,贴纸风格"
|
||||
```
|
||||
|
||||
指定尺寸:
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_image.py --prompt "壮丽的山川日出" --size "1024x768"
|
||||
```
|
||||
|
||||
### 图生图(保持一致性)
|
||||
|
||||
提供参考图 URL 或 `data:image` Base64,让新图沿用参考图的角色/构图:
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_image.py \
|
||||
--prompt "把这只小龙变成展翅的成年形态,保持配色和脸部不变" \
|
||||
--image "https://example.com/baby-dragon.png"
|
||||
```
|
||||
|
||||
可多次指定 `--image` 传入多张参考图。
|
||||
|
||||
### 下载保存到本地
|
||||
|
||||
加 `--save`,脚本会下载图片并保存,输出 `SAVED:<path>`:
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_image.py --prompt "一只小猫贴纸" --save ./outputs/cat.png
|
||||
```
|
||||
|
||||
### 白底转透明 PNG
|
||||
|
||||
配合 `--save --transparent`,自动把纯白背景抠成透明(保留主体内部白色,如白描边、白肚皮):
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_image.py \
|
||||
--prompt "一只圆润萌系企鹅,白色背景,贴纸风格" \
|
||||
--save ./outputs/penguin.png --transparent
|
||||
```
|
||||
|
||||
边缘若有白色残留,可调高容差 `--thresh`(默认 60)。
|
||||
|
||||
## 参数说明
|
||||
|
||||
- `--prompt`: (必选) 图像生成的文本描述,支持中英文。
|
||||
- `--model`: (可选) 模型 ID,默认 `agnes-image-2.1-flash`。
|
||||
- `--size`: (可选) 图像尺寸,如 `1024x1024`、`1024x768`,默认 `1024x1024`。
|
||||
- `--image`: (可选) 参考图 URL 或 `data:image/...;base64,...`,用于图生图,可多次指定。
|
||||
- `--api-key`: (可选) Agnes API Key,未提供时读取 `AGNES_API_KEY` 环境变量。
|
||||
- `--save`: (可选) 下载并保存到指定本地路径。
|
||||
- `--transparent`: (可选) 配合 `--save`,把纯白背景转为透明 PNG(需 Pillow)。
|
||||
- `--thresh`: (可选) 透明化时的白色容差,默认 60,值越大清除越彻底。
|
||||
|
||||
## 工作流
|
||||
|
||||
1. 调用 `generate_image.py` 脚本。
|
||||
2. 脚本始终输出以 `MEDIA_URL: ` 开头的图片链接,用 Markdown 展示:``。
|
||||
3. 加了 `--save` 时,会额外下载到本地并输出以 `SAVED: ` 开头的文件路径(`MEDIA_URL` 仍会一并输出)。
|
||||
4. 生成的图片链接来自 Agnes 平台输出存储,可直接公网访问。
|
||||
|
||||
> 注:`--save --transparent` 时,`MEDIA_URL` 指向 Agnes 返回的原始白底图,本地 `SAVED` 文件才是抠好的透明 PNG。
|
||||
|
||||
## 批量生成小贴士
|
||||
|
||||
生成系列素材(如角色的多个进化阶段)时,建议:
|
||||
|
||||
- 在每个 prompt 中固定统一的「风格描述」(如 `chibi kawaii sticker, big eyes, white background`),保证视觉一致。
|
||||
- 需要严格保持同一角色时,用 `--image` 把上一阶段的图当参考做图生图。
|
||||
- 透明素材直接用 `--save --transparent`,省去手动抠图。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 接口兼容 OpenAI 风格,Base URL 为 `https://apihub.agnes-ai.com/v1`。
|
||||
- API Key 属于敏感信息,请勿提交到公开仓库或硬编码到代码中。
|
||||
- 图生图时,确保参考图 URL 可公开访问。
|
||||
- 透明化基于「边缘漫水填充」,仅对纯色(白)背景效果最佳;复杂背景请勿使用 `--transparent`。
|
||||
|
||||
---
|
||||
|
||||
## 图片理解(多模态)
|
||||
|
||||
使用 `understand_image.py` 让模型(默认 `agnes-2.0-flash`)基于图片进行描述、分析、问答或信息提取。支持公网图片 URL,也支持本地图片文件(自动转 Base64)。
|
||||
|
||||
### 理解图片 URL
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/understand_image.py \
|
||||
--prompt "用中文描述这张图片的内容和风格" \
|
||||
--image "https://example.com/image.jpg"
|
||||
```
|
||||
|
||||
### 理解本地图片
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/understand_image.py \
|
||||
--prompt "这是什么动物?什么颜色?" \
|
||||
--image-file ./outputs/fox.png
|
||||
```
|
||||
|
||||
### 多图对比 / 信息提取
|
||||
|
||||
可多次指定 `--image` 或 `--image-file` 传入多张图片:
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/understand_image.py \
|
||||
--prompt "比较这两张图的差异" \
|
||||
--image-file ./a.png --image-file ./b.png
|
||||
```
|
||||
|
||||
### 带系统提示
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/understand_image.py \
|
||||
--prompt "提取图中所有文字" \
|
||||
--image "https://example.com/poster.jpg" \
|
||||
--system "你是专业的 OCR 助手,只输出图中文字"
|
||||
```
|
||||
|
||||
### 图片理解参数说明
|
||||
|
||||
- `--prompt`: (必选) 对图片的问题或指令。
|
||||
- `--image`: (可选) 图片公网 URL 或 `data:image` Base64,可多次指定。
|
||||
- `--image-file`: (可选) 本地图片文件路径,自动转 Base64,可多次指定。
|
||||
- `--system`: (可选) 系统提示(system 消息)。
|
||||
- `--model`: (可选) 模型 ID,默认 `agnes-2.0-flash`。
|
||||
- `--temperature`: (可选) 采样温度,越低越确定。
|
||||
- `--max-tokens`: (可选) 最多生成的 token 数。
|
||||
- `--api-key`: (可选) Agnes API Key,未提供时读取 `AGNES_API_KEY` 环境变量。
|
||||
|
||||
### 图片理解工作流
|
||||
|
||||
1. 调用 `understand_image.py` 脚本。
|
||||
2. 脚本直接输出模型的文本回答(无前缀),可直接读取使用。
|
||||
3. 不传任何图片时,会退化为普通文本对话。
|
||||
|
||||
---
|
||||
|
||||
## 视频生成(异步)
|
||||
|
||||
使用 `generate_video.py` 生成视频(模型 `agnes-video-v2.0`)。视频生成是异步任务,脚本会自动创建任务并轮询,完成后输出视频链接。生成通常需要 1-3 分钟。
|
||||
|
||||
> ⚠️ **重要:视频生成用到的所有参考图(图生视频 / 多图 / 关键帧)必须是「可公网访问的图片 URL」**(`http://` 或 `https://`),**不支持本地文件路径,也不支持 Base64**。
|
||||
> 如果你手上只有本地图片,请先:
|
||||
> 1. 用 `generate_image.py` 生成图片,它返回的 `MEDIA_URL` 就是可直接使用的公网 URL;或
|
||||
> 2. 把本地图片上传到图床 / 对象存储(如 S3、R2)后,使用其公网 URL。
|
||||
>
|
||||
> 脚本会校验 `--image` 是否为合法 URL,传入本地路径会直接报错并给出提示。
|
||||
|
||||
### 文生视频
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_video.py \
|
||||
--prompt "A cinematic shot of a cat walking on the beach at sunset, warm golden lighting" \
|
||||
--duration 5
|
||||
```
|
||||
|
||||
### 图生视频(让单张图动起来)
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_video.py \
|
||||
--prompt "The character breathes gently, hair moving in the wind, keep face consistent" \
|
||||
--image "https://example.com/character.png" --duration 5
|
||||
```
|
||||
|
||||
### 多图视频(在多张图之间过渡)
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_video.py \
|
||||
--prompt "Smooth transformation between the two scenes, cinematic pacing" \
|
||||
--image "https://example.com/a.png" --image "https://example.com/b.png"
|
||||
```
|
||||
|
||||
### 关键帧动画
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_video.py \
|
||||
--prompt "Smooth transition between keyframes, maintain character identity" \
|
||||
--image "https://example.com/k1.png" --image "https://example.com/k2.png" \
|
||||
--keyframes
|
||||
```
|
||||
|
||||
### 下载保存
|
||||
|
||||
```bash
|
||||
python {baseDir}/scripts/generate_video.py --prompt "日出延时摄影" --duration 5 --save ./out.mp4
|
||||
```
|
||||
|
||||
### 视频参数说明
|
||||
|
||||
- `--prompt`: (必选) 视频内容的文本描述。
|
||||
- `--model`: (可选) 模型 ID,默认 `agnes-video-v2.0`。
|
||||
- `--image`: (可选) 参考图 URL;1 张=图生视频,2 张及以上=多图视频,可多次指定。
|
||||
- `--keyframes`: (可选) 关键帧动画模式(配合多张 `--image`)。
|
||||
- `--duration`: (可选) 目标时长(秒),按帧率自动换算帧数(覆盖 `--num-frames`)。
|
||||
- `--num-frames`: (可选) 帧数,必须 ≤441 且满足 8n+1,默认 121(约 5 秒);脚本会自动规整。
|
||||
- `--frame-rate`: (可选) 帧率 1-60,默认 24。
|
||||
- `--width` / `--height`: (可选) 分辨率,默认 1152×768;服务端会标准化到最接近的档位。
|
||||
- `--negative-prompt`: (可选) 负向提示词。
|
||||
- `--seed`: (可选) 随机种子,用于结果复现。
|
||||
- `--save`: (可选) 下载视频并保存到本地。
|
||||
- `--poll-interval`: (可选) 轮询间隔秒数,默认 5。
|
||||
- `--max-wait`: (可选) 最大等待秒数,默认 600。
|
||||
- `--api-key`: (可选) Agnes API Key,未提供时读取 `AGNES_API_KEY` 环境变量。
|
||||
|
||||
### 常用时长参数
|
||||
|
||||
| 目标时长 | 推荐参数 |
|
||||
|---------|---------|
|
||||
| 约 3 秒 | `--num-frames 81 --frame-rate 24` |
|
||||
| 约 5 秒 | `--num-frames 121 --frame-rate 24` |
|
||||
| 约 10 秒 | `--num-frames 241 --frame-rate 24` |
|
||||
| 约 18 秒 | `--num-frames 441 --frame-rate 24` |
|
||||
|
||||
> 也可以直接用 `--duration <秒数>` 让脚本自动换算(会规整到合法的 8n+1 帧数)。
|
||||
|
||||
### 视频生成工作流
|
||||
|
||||
1. 调用 `generate_video.py`,脚本自动创建任务(`POST /v1/videos`)。
|
||||
2. 用返回的 `video_id` 轮询查询(`GET /agnesapi?video_id=...`),进度打到 stderr。
|
||||
3. 任务 `completed` 后,stdout 输出以 `MEDIA_URL: ` 开头的视频链接。
|
||||
4. 加 `--save` 时额外下载并输出 `SAVED: ` 路径。
|
||||
|
||||
### 视频注意事项
|
||||
|
||||
- `num_frames` 必须 ≤441 且满足 8n+1(如 81、121、241、441),脚本会自动规整非法值。
|
||||
- 实际输出尺寸/时长以接口返回的 `size`、`seconds` 字段为准(服务端会标准化分辨率)。
|
||||
- **参考图必须是可公网访问的 URL(`http`/`https`),不支持本地文件或 Base64**;本地图片请先用 `generate_image.py` 拿到公网 URL,或上传图床后再传入。
|
||||
- 轮询期间对瞬时网络抖动有容错,会自动重试。
|
||||
186
skills/linggan/agnes-image/scripts/generate_image.py
Normal file
186
skills/linggan/agnes-image/scripts/generate_image.py
Normal file
@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "requests>=2.31.0",
|
||||
# "pillow>=10.0.0",
|
||||
# ]
|
||||
# ///
|
||||
"""使用 Agnes AI(OpenAI 兼容网关)生成图片。
|
||||
支持:文生图、图生图(参考图保持一致性)、URL/Base64 输出、下载保存、白底转透明。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
import requests
|
||||
|
||||
API_URL = "https://apihub.agnes-ai.com/v1/images/generations"
|
||||
DEFAULT_MODEL = "agnes-image-2.1-flash"
|
||||
|
||||
|
||||
def call_api(prompt, model, size, api_key, images=None, want_b64=False, retries=3):
|
||||
"""调用 Agnes 图像生成接口,返回解析后的 JSON。
|
||||
对瞬时网络/SSL 抖动自动重试,重试间隔递增。
|
||||
"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
extra = {"response_format": "b64_json" if want_b64 else "url"}
|
||||
if images:
|
||||
# 图生图:参考图放在 extra_body.image,支持公网 URL 或 data:image Base64
|
||||
extra["image"] = images
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"size": size,
|
||||
"extra_body": extra,
|
||||
}
|
||||
# 文生图需要 Base64 输出时,按文档使用顶层 return_base64
|
||||
if want_b64 and not images:
|
||||
payload["return_base64"] = True
|
||||
|
||||
last_err = None
|
||||
for attempt in range(1, retries + 1):
|
||||
try:
|
||||
resp = requests.post(API_URL, headers=headers, json=payload, timeout=180)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except (requests.exceptions.SSLError,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout) as e:
|
||||
# 瞬时网络抖动:重试
|
||||
last_err = e
|
||||
if attempt < retries:
|
||||
time.sleep(attempt * 2)
|
||||
continue
|
||||
raise
|
||||
raise last_err
|
||||
|
||||
|
||||
def fetch_bytes(url):
|
||||
"""下载 URL 内容为字节。"""
|
||||
with urllib.request.urlopen(url, timeout=180) as r:
|
||||
return r.read()
|
||||
|
||||
|
||||
def make_transparent(png_bytes, thresh=60, white_min=225):
|
||||
"""把纯白背景变透明(保留内部白色)。
|
||||
从图像四边多个种子点做 flood fill,只清除与边缘连通的白色区域。
|
||||
"""
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
img = Image.open(io.BytesIO(png_bytes)).convert("RGBA")
|
||||
w, h = img.size
|
||||
seeds = []
|
||||
steps = 12
|
||||
for i in range(steps + 1):
|
||||
t = int(i * (w - 1) / steps)
|
||||
seeds += [(t, 0), (t, h - 1)]
|
||||
s = int(i * (h - 1) / steps)
|
||||
seeds += [(0, s), (w - 1, s)]
|
||||
for sx, sy in seeds:
|
||||
r, g, b, a = img.getpixel((sx, sy))
|
||||
if a > 0 and r >= white_min and g >= white_min and b >= white_min:
|
||||
ImageDraw.floodfill(img, (sx, sy), (255, 255, 255, 0), thresh=thresh)
|
||||
out = io.BytesIO()
|
||||
img.save(out, "PNG")
|
||||
return out.getvalue()
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="使用 Agnes AI 生成图片。")
|
||||
parser.add_argument("--prompt", required=True, help="图像生成的文本描述(必选)")
|
||||
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"模型 ID,默认 {DEFAULT_MODEL}")
|
||||
parser.add_argument("--size", default="1024x1024", help="图像尺寸,如 1024x1024、1024x768")
|
||||
parser.add_argument("--image", action="append", default=None,
|
||||
help="参考图 URL 或 data:image Base64(图生图,可多次指定)")
|
||||
parser.add_argument("--api-key", help="Agnes API Key(也可用 AGNES_API_KEY 环境变量)")
|
||||
parser.add_argument("--save", help="下载并保存到本地路径,输出 SAVED:<path>")
|
||||
parser.add_argument("--transparent", action="store_true",
|
||||
help="配合 --save:把纯白背景转为透明 PNG")
|
||||
parser.add_argument("--thresh", type=int, default=60, help="透明化白色容差,默认 60")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
api_key = args.api_key or os.environ.get("AGNES_API_KEY")
|
||||
if not api_key:
|
||||
print("ERROR: 缺少 API Key,请用 --api-key 或设置 AGNES_API_KEY 环境变量。")
|
||||
sys.exit(1)
|
||||
|
||||
# 需要保存且要透明时,直接请求 URL 再下载处理(Pillow 处理本地字节)
|
||||
want_b64 = False
|
||||
try:
|
||||
result = call_api(
|
||||
prompt=args.prompt,
|
||||
model=args.model,
|
||||
size=args.size,
|
||||
api_key=api_key,
|
||||
images=args.image,
|
||||
want_b64=want_b64,
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"ERROR: API 请求失败: {e}")
|
||||
if getattr(e, "response", None) is not None:
|
||||
print(f"Response body: {e.response.text[:500]}")
|
||||
sys.exit(1)
|
||||
|
||||
data = result.get("data") or []
|
||||
if not data:
|
||||
print(f"ERROR: 返回中无图像数据。完整响应: {json.dumps(result)[:500]}")
|
||||
sys.exit(1)
|
||||
|
||||
item = data[0]
|
||||
url = item.get("url")
|
||||
b64 = item.get("b64_json")
|
||||
|
||||
if not args.save:
|
||||
# 仅输出链接(对齐 seedream 约定)
|
||||
if url:
|
||||
print(f"MEDIA_URL: {url}")
|
||||
elif b64:
|
||||
print("ERROR: 收到 Base64 但未指定 --save,无法直接展示。请加 --save 保存。")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"ERROR: 无 url 也无 b64_json。响应: {json.dumps(result)[:500]}")
|
||||
sys.exit(1)
|
||||
return
|
||||
|
||||
# 保存到本地
|
||||
try:
|
||||
if b64:
|
||||
import base64
|
||||
img_bytes = base64.b64decode(b64)
|
||||
elif url:
|
||||
img_bytes = fetch_bytes(url)
|
||||
else:
|
||||
print("ERROR: 无可保存的图像数据。")
|
||||
sys.exit(1)
|
||||
|
||||
if args.transparent:
|
||||
try:
|
||||
img_bytes = make_transparent(img_bytes, thresh=args.thresh)
|
||||
except ImportError:
|
||||
print("ERROR: --transparent 需要 Pillow,请先 `pip install pillow`。")
|
||||
sys.exit(1)
|
||||
|
||||
os.makedirs(os.path.dirname(os.path.abspath(args.save)), exist_ok=True)
|
||||
with open(args.save, "wb") as f:
|
||||
f.write(img_bytes)
|
||||
# 即使保存到本地,也输出原始图片链接(便于直接引用/展示)
|
||||
if url:
|
||||
print(f"MEDIA_URL: {url}")
|
||||
print(f"SAVED: {args.save}")
|
||||
except Exception as e:
|
||||
print(f"ERROR: 保存失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
206
skills/linggan/agnes-image/scripts/generate_video.py
Normal file
206
skills/linggan/agnes-image/scripts/generate_video.py
Normal file
@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "requests>=2.31.0",
|
||||
# ]
|
||||
# ///
|
||||
"""使用 Agnes AI 生成视频(异步任务:创建 -> 轮询 -> 取结果)。
|
||||
支持文生视频、图生视频、多图视频、关键帧动画。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
import requests
|
||||
|
||||
CREATE_URL = "https://apihub.agnes-ai.com/v1/videos"
|
||||
QUERY_URL = "https://apihub.agnes-ai.com/agnesapi"
|
||||
DEFAULT_MODEL = "agnes-video-v2.0"
|
||||
|
||||
|
||||
def log(msg):
|
||||
"""进度信息打到 stderr,保持 stdout 只输出最终结果。"""
|
||||
print(msg, file=sys.stderr, flush=True)
|
||||
|
||||
|
||||
def normalize_frames(nf):
|
||||
"""num_frames 必须 <=441 且满足 8n+1,规整到最接近的合法值。"""
|
||||
nf = max(9, min(441, int(nf)))
|
||||
n = round((nf - 1) / 8)
|
||||
nf = 8 * n + 1
|
||||
return max(9, min(441, nf))
|
||||
|
||||
|
||||
def create_task(payload, api_key, retries=3):
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
last_err = None
|
||||
for attempt in range(1, retries + 1):
|
||||
try:
|
||||
resp = requests.post(CREATE_URL, headers=headers, json=payload, timeout=120)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except (requests.exceptions.SSLError,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout) as e:
|
||||
last_err = e
|
||||
if attempt < retries:
|
||||
time.sleep(attempt * 2)
|
||||
continue
|
||||
raise
|
||||
raise last_err
|
||||
|
||||
|
||||
def query_result(video_id, api_key, model=None):
|
||||
"""用 video_id 查询任务结果(推荐方式)。"""
|
||||
params = f"?video_id={urllib.parse.quote(video_id)}"
|
||||
if model:
|
||||
params += f"&model_name={urllib.parse.quote(model)}"
|
||||
req = urllib.request.Request(
|
||||
QUERY_URL + params,
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def download(url, path):
|
||||
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
||||
with urllib.request.urlopen(url, timeout=300) as r, open(path, "wb") as f:
|
||||
shutil.copyfileobj(r, f)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="使用 Agnes AI 生成视频。")
|
||||
parser.add_argument("--prompt", required=True, help="视频内容的文本描述(必选)")
|
||||
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"模型 ID,默认 {DEFAULT_MODEL}")
|
||||
parser.add_argument("--image", action="append", default=None,
|
||||
help="参考图 URL(图生视频/多图/关键帧,可多次指定)")
|
||||
parser.add_argument("--keyframes", action="store_true",
|
||||
help="关键帧动画模式(配合多张 --image 使用)")
|
||||
parser.add_argument("--width", type=int, default=1152, help="视频宽度,默认 1152")
|
||||
parser.add_argument("--height", type=int, default=768, help="视频高度,默认 768")
|
||||
parser.add_argument("--num-frames", type=int, default=121,
|
||||
help="帧数,<=441 且满足 8n+1,默认 121(约 5 秒)")
|
||||
parser.add_argument("--frame-rate", type=float, default=24, help="帧率 1-60,默认 24")
|
||||
parser.add_argument("--duration", type=float,
|
||||
help="目标时长(秒),会按帧率换算 num_frames(覆盖 --num-frames)")
|
||||
parser.add_argument("--negative-prompt", help="负向提示词,描述要避免的内容")
|
||||
parser.add_argument("--seed", type=int, help="随机种子,用于结果复现")
|
||||
parser.add_argument("--api-key", help="Agnes API Key(也可用 AGNES_API_KEY 环境变量)")
|
||||
parser.add_argument("--save", help="下载视频并保存到本地路径")
|
||||
parser.add_argument("--poll-interval", type=float, default=5, help="轮询间隔秒数,默认 5")
|
||||
parser.add_argument("--max-wait", type=float, default=600, help="最大等待秒数,默认 600")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
api_key = args.api_key or os.environ.get("AGNES_API_KEY")
|
||||
if not api_key:
|
||||
print("ERROR: 缺少 API Key,请用 --api-key 或设置 AGNES_API_KEY 环境变量。")
|
||||
sys.exit(1)
|
||||
|
||||
# 计算帧数
|
||||
if args.duration:
|
||||
num_frames = normalize_frames(args.duration * args.frame_rate)
|
||||
else:
|
||||
num_frames = normalize_frames(args.num_frames)
|
||||
|
||||
# 组装请求体
|
||||
payload = {
|
||||
"model": args.model,
|
||||
"prompt": args.prompt,
|
||||
"width": args.width,
|
||||
"height": args.height,
|
||||
"num_frames": num_frames,
|
||||
"frame_rate": args.frame_rate,
|
||||
}
|
||||
if args.negative_prompt:
|
||||
payload["negative_prompt"] = args.negative_prompt
|
||||
if args.seed is not None:
|
||||
payload["seed"] = args.seed
|
||||
|
||||
images = args.image or []
|
||||
# 视频接口的参考图只支持可公网访问的 URL(不支持本地文件 / Base64)
|
||||
for u in images:
|
||||
if not (u.startswith("http://") or u.startswith("https://")):
|
||||
print("ERROR: 视频生成的参考图必须是可公网访问的图片 URL(http/https),"
|
||||
"不支持本地文件路径或 Base64。")
|
||||
print(f" 问题输入: {u}")
|
||||
print(" 建议:先用 generate_image.py 生成图片拿到其公网 URL,"
|
||||
"或把本地图片上传到图床/对象存储后再用其 URL 传入。")
|
||||
sys.exit(1)
|
||||
|
||||
if args.keyframes:
|
||||
# 关键帧动画:extra_body.image + mode=keyframes
|
||||
payload["extra_body"] = {"image": images, "mode": "keyframes"}
|
||||
elif len(images) == 1:
|
||||
# 图生视频:顶层 image
|
||||
payload["image"] = images[0]
|
||||
elif len(images) >= 2:
|
||||
# 多图视频:extra_body.image
|
||||
payload["extra_body"] = {"image": images}
|
||||
|
||||
# 1) 创建任务
|
||||
log(f"创建视频任务({num_frames} 帧 @ {args.frame_rate}fps ≈ {num_frames/args.frame_rate:.1f}s)...")
|
||||
try:
|
||||
task = create_task(payload, api_key)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"ERROR: 创建任务失败: {e}")
|
||||
if getattr(e, "response", None) is not None:
|
||||
print(f"Response body: {e.response.text[:500]}")
|
||||
sys.exit(1)
|
||||
|
||||
video_id = task.get("video_id")
|
||||
task_id = task.get("task_id") or task.get("id")
|
||||
if not video_id and not task_id:
|
||||
print(f"ERROR: 创建任务响应缺少 video_id/task_id: {json.dumps(task)[:500]}")
|
||||
sys.exit(1)
|
||||
log(f"任务已创建 video_id={video_id} task_id={task_id} status={task.get('status')}")
|
||||
|
||||
# 2) 轮询结果
|
||||
start = time.time()
|
||||
video_url = None
|
||||
while time.time() - start < args.max_wait:
|
||||
time.sleep(args.poll_interval)
|
||||
try:
|
||||
data = query_result(video_id or task_id, api_key, model=args.model)
|
||||
except Exception as e:
|
||||
log(f" 查询出错(将重试): {e}")
|
||||
continue
|
||||
status = data.get("status")
|
||||
progress = data.get("progress", 0)
|
||||
log(f" 状态={status} 进度={progress}%")
|
||||
if status == "completed":
|
||||
video_url = data.get("remixed_from_video_id") # 文档:该字段为最终视频 URL
|
||||
break
|
||||
if status == "failed":
|
||||
print(f"ERROR: 视频生成失败: {data.get('error')}")
|
||||
sys.exit(1)
|
||||
|
||||
if not video_url:
|
||||
print(f"ERROR: 等待超时({args.max_wait}s)或未返回视频 URL。")
|
||||
sys.exit(1)
|
||||
|
||||
# 3) 输出结果
|
||||
print(f"MEDIA_URL: {video_url}")
|
||||
if args.save:
|
||||
try:
|
||||
log("下载视频中 ...")
|
||||
download(video_url, args.save)
|
||||
print(f"SAVED: {args.save}")
|
||||
except Exception as e:
|
||||
print(f"ERROR: 下载保存失败: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
126
skills/linggan/agnes-image/scripts/understand_image.py
Normal file
126
skills/linggan/agnes-image/scripts/understand_image.py
Normal file
@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.10"
|
||||
# dependencies = [
|
||||
# "requests>=2.31.0",
|
||||
# ]
|
||||
# ///
|
||||
"""使用 Agnes AI 的多模态模型理解图片。
|
||||
传入图片(公网 URL 或本地文件)+ 文本问题,模型基于图片进行描述、分析、问答或信息提取。
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
API_URL = "https://apihub.agnes-ai.com/v1/chat/completions"
|
||||
DEFAULT_MODEL = "agnes-2.0-flash"
|
||||
|
||||
|
||||
def file_to_data_uri(path):
|
||||
"""把本地图片文件转成 data:image Base64。"""
|
||||
mime = mimetypes.guess_type(path)[0] or "image/png"
|
||||
with open(path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode()
|
||||
return f"data:{mime};base64,{b64}"
|
||||
|
||||
|
||||
def call_api(messages, model, api_key, temperature=None, max_tokens=None, retries=3):
|
||||
"""调用 Agnes chat/completions,返回解析后的 JSON。瞬时网络抖动自动重试。"""
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
}
|
||||
payload = {"model": model, "messages": messages}
|
||||
if temperature is not None:
|
||||
payload["temperature"] = temperature
|
||||
if max_tokens is not None:
|
||||
payload["max_tokens"] = max_tokens
|
||||
|
||||
last_err = None
|
||||
for attempt in range(1, retries + 1):
|
||||
try:
|
||||
resp = requests.post(API_URL, headers=headers, json=payload, timeout=180)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
except (requests.exceptions.SSLError,
|
||||
requests.exceptions.ConnectionError,
|
||||
requests.exceptions.Timeout) as e:
|
||||
last_err = e
|
||||
if attempt < retries:
|
||||
time.sleep(attempt * 2)
|
||||
continue
|
||||
raise
|
||||
raise last_err
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="使用 Agnes AI 理解/分析图片。")
|
||||
parser.add_argument("--prompt", required=True, help="对图片的问题或指令(必选)")
|
||||
parser.add_argument("--image", action="append", default=None,
|
||||
help="图片公网 URL 或 data:image Base64(可多次指定)")
|
||||
parser.add_argument("--image-file", action="append", default=None,
|
||||
help="本地图片文件路径,自动转 Base64(可多次指定)")
|
||||
parser.add_argument("--system", help="可选的系统提示(system 消息)")
|
||||
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"模型 ID,默认 {DEFAULT_MODEL}")
|
||||
parser.add_argument("--temperature", type=float, help="采样温度,0~1,越低越确定")
|
||||
parser.add_argument("--max-tokens", type=int, help="最多生成的 token 数")
|
||||
parser.add_argument("--api-key", help="Agnes API Key(也可用 AGNES_API_KEY 环境变量)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
api_key = args.api_key or os.environ.get("AGNES_API_KEY")
|
||||
if not api_key:
|
||||
print("ERROR: 缺少 API Key,请用 --api-key 或设置 AGNES_API_KEY 环境变量。")
|
||||
sys.exit(1)
|
||||
|
||||
# 收集所有图片输入
|
||||
image_urls = list(args.image or [])
|
||||
for p in (args.image_file or []):
|
||||
if not os.path.isfile(p):
|
||||
print(f"ERROR: 本地图片不存在: {p}")
|
||||
sys.exit(1)
|
||||
image_urls.append(file_to_data_uri(p))
|
||||
|
||||
# 组装多模态 content:文本 + 若干图片
|
||||
content = [{"type": "text", "text": args.prompt}]
|
||||
for url in image_urls:
|
||||
content.append({"type": "image_url", "image_url": {"url": url}})
|
||||
|
||||
messages = []
|
||||
if args.system:
|
||||
messages.append({"role": "system", "content": args.system})
|
||||
# 没有图片时退化为纯文本,content 用字符串更稳妥
|
||||
messages.append({"role": "user", "content": content if image_urls else args.prompt})
|
||||
|
||||
try:
|
||||
result = call_api(
|
||||
messages=messages,
|
||||
model=args.model,
|
||||
api_key=api_key,
|
||||
temperature=args.temperature,
|
||||
max_tokens=args.max_tokens,
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"ERROR: API 请求失败: {e}")
|
||||
if getattr(e, "response", None) is not None:
|
||||
print(f"Response body: {e.response.text[:500]}")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
answer = result["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
print(f"ERROR: 无法解析响应。完整响应: {json.dumps(result, ensure_ascii=False)[:500]}")
|
||||
sys.exit(1)
|
||||
|
||||
print(answer)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user