83 lines
3.4 KiB
Python
Executable File
83 lines
3.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""MiniMax-M3 图像识别 (火山方舟 Coding Plan)
|
|
|
|
用法:
|
|
vision.py <图片路径或URL> [-p 提问] [--json] [--max-tokens N] [--detail]
|
|
vision.py a.png b.jpg -p "这两张图有什么区别?" # 多图对比
|
|
cat prompt.txt | vision.py a.png -p - # 从 stdin 读提问
|
|
|
|
Key 优先级: 环境变量 ARK_CP_API_KEY > ~/.config/ark_cp_api_key
|
|
"""
|
|
import argparse, base64, json, mimetypes, os, sys, time, urllib.request, urllib.error
|
|
|
|
ENDPOINT = "https://ark.cn-beijing.volces.com/api/coding/v3/chat/completions"
|
|
MODEL = "minimax-m3"
|
|
|
|
|
|
def load_key():
|
|
k = os.environ.get("ARK_CP_API_KEY")
|
|
if k:
|
|
return k.strip()
|
|
p = os.path.expanduser("~/.config/ark_cp_api_key")
|
|
if os.path.exists(p):
|
|
return open(p, encoding="utf-8").read().strip()
|
|
sys.exit("错误: 未找到 API key。请设置环境变量 ARK_CP_API_KEY 或写入 ~/.config/ark_cp_api_key")
|
|
|
|
|
|
def to_part(src):
|
|
"""本地文件转 base64 data URI; http(s) 链接直接透传"""
|
|
if src.startswith(("http://", "https://")):
|
|
return {"type": "image_url", "image_url": {"url": src}}
|
|
if not os.path.exists(src):
|
|
sys.exit(f"错误: 文件不存在 {src}")
|
|
mime = mimetypes.guess_type(src)[0] or "image/png"
|
|
if not mime.startswith("image/"):
|
|
sys.exit(f"错误: 不是图片文件 {src} ({mime})")
|
|
b64 = base64.b64encode(open(src, "rb").read()).decode()
|
|
return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{b64}"}}
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="MiniMax-M3 图像识别")
|
|
ap.add_argument("images", nargs="+", help="图片路径或 URL, 可多张")
|
|
ap.add_argument("-p", "--prompt", default="详细描述这张图片的内容。",
|
|
help="提问; 传 - 表示从 stdin 读")
|
|
ap.add_argument("--json", action="store_true", help="要求模型只输出 JSON")
|
|
ap.add_argument("--max-tokens", type=int, default=2048)
|
|
ap.add_argument("--detail", action="store_true", help="输出耗时/token/request_id")
|
|
a = ap.parse_args()
|
|
|
|
prompt = sys.stdin.read().strip() if a.prompt == "-" else a.prompt
|
|
if a.json:
|
|
prompt += "\n\n只输出一个 JSON 对象,不要任何解释、不要代码块标记。"
|
|
|
|
content = [{"type": "text", "text": prompt}] + [to_part(s) for s in a.images]
|
|
body = json.dumps({"model": MODEL, "max_tokens": a.max_tokens,
|
|
"messages": [{"role": "user", "content": content}]}).encode()
|
|
req = urllib.request.Request(ENDPOINT, data=body, headers={
|
|
"Authorization": f"Bearer {load_key()}", "x-api-key": load_key(),
|
|
"Content-Type": "application/json"})
|
|
|
|
t0 = time.time()
|
|
try:
|
|
d = json.load(urllib.request.urlopen(req, timeout=600))
|
|
except urllib.error.HTTPError as e:
|
|
sys.exit(f"API 错误 {e.code}: {e.read().decode('utf-8', 'replace')[:400]}")
|
|
except Exception as e:
|
|
sys.exit(f"请求失败: {e}")
|
|
dt = time.time() - t0
|
|
|
|
text = (d["choices"][0]["message"].get("content") or "").strip()
|
|
if not text:
|
|
sys.exit("模型返回空内容 (可能 max_tokens 太小被推理耗尽, 试试调大 --max-tokens)")
|
|
print(text)
|
|
|
|
if a.detail:
|
|
u = d.get("usage", {})
|
|
print(f"\n--- {dt:.1f}s | in {u.get('prompt_tokens')} / out {u.get('completion_tokens')} tok "
|
|
f"| {d.get('id')}", file=sys.stderr)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|