diff --git a/skills/linggan/agnes-image/SKILL.md b/skills/linggan/agnes-image/SKILL.md new file mode 100644 index 0000000..e0b18ef --- /dev/null +++ b/skills/linggan/agnes-image/SKILL.md @@ -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:`: + +```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 展示:`![image](URL)`。 +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,或上传图床后再传入。 +- 轮询期间对瞬时网络抖动有容错,会自动重试。 diff --git a/skills/linggan/agnes-image/scripts/generate_image.py b/skills/linggan/agnes-image/scripts/generate_image.py new file mode 100644 index 0000000..036387c --- /dev/null +++ b/skills/linggan/agnes-image/scripts/generate_image.py @@ -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:") + 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() diff --git a/skills/linggan/agnes-image/scripts/generate_video.py b/skills/linggan/agnes-image/scripts/generate_video.py new file mode 100644 index 0000000..1dea8eb --- /dev/null +++ b/skills/linggan/agnes-image/scripts/generate_video.py @@ -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() diff --git a/skills/linggan/agnes-image/scripts/understand_image.py b/skills/linggan/agnes-image/scripts/understand_image.py new file mode 100644 index 0000000..753d319 --- /dev/null +++ b/skills/linggan/agnes-image/scripts/understand_image.py @@ -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()