跳到主要内容

AI云识别

需要 AScript iOS 4002 及以上版本

低于 4002 的版本里没有这个模块,from ascript.ios.vlm import ascript_ai 会直接抛 ModuleNotFoundError请先把 AScript 升级到 4002 或更新的版本。

from ascript.ios.vlm import ascript_ai as ai
iOS 是内置的,不需要装插件

iOS 不支持加载插件,所以 AI 识别直接内置在 AScript 中,导入即用。

Android 那边为了兼容老版本,同一套能力是以 ascript_ai 插件的形式提供的, 要先 plug.load("ascript_ai")两端的功能和参数完全一致,只有导入方式不同, 跨端移植脚本时改这一行即可。

自然语言描述在屏幕上找东西、问值、问页面状态,推理跑在 AScript AI Studio 云端。

不需要模板图、不需要固定文字、不需要训练模型 —— 直接用一句话描述你要什么。

使用场景
  • 没有模板图、也没有固定文字,只能靠语义描述的目标:"那个红色的关闭按钮"
  • 界面改版频繁,写死的坐标和模板图天天失效
  • 需要理解页面语义:"当前是不是登录页"、"列表里价格最低的那一项"
  • 从屏幕上提取结构化数据:把商品列表读成 [{"name":..., "price":...}]
  • 图色/OCR 都试过但认不出来的兜底方案
什么时候不该

能用 FindImages.find()(有模板图)或 Ocr.paddleocr()(有固定文字)解决的场景, 不要用AI云识别。那些是毫秒级且免费,这个是秒级且按量计费。

AI云识别是给"前两者做不到"的场景兜底的,不是用来替代它们的。

准备工作

1. 获取密钥

登录 AScript AI Studio → 账户中心 → 「API 调用」页面创建密钥。

密钥形如 sk-as- 开头的字符串,明文只在创建时显示一次,请立即保存。丢失只能重新创建。

信息

每个账号最多 20 个密钥。密钥创建时会封存当时的登录凭证,如果账号中心让该凭证失效, 调用会返回 credential_expired,需要重新登录网页并重新创建密钥。

2. 初始化

from ascript.ios.vlm import ascript_ai as ai

ai.init(api_key="sk-as-xxxxxxxx")

也可以设置环境变量 ASCRIPT_AI_KEY,这样脚本里连 init() 都可以省掉。

快速开始

1. 第一个脚本

from ascript.ios.vlm import ascript_ai as ai

ai.init(api_key="sk-as-xxxxxxxx")

r = ai.find("右上角的购物车图标")
print(r)
# {'text': '购物车', 'rect': [960, 120, 1040, 200],
# 'center_x': 1000, 'center_y': 160, 'confidence': 1.0}

find() 的返回结构与 Ocr.paddleocr() 的每一项完全一致text / rect / center_x / center_y / confidence), OCR 认不出来的时候可以直接换过来用。

设了环境变量 ASCRIPT_AI_KEY 的话,连 ai.init() 这行都可以省掉。

2. 找到并点击

最常用的一句。click() 内部会自己截屏、定位、点中心点,找不到返回 False

ai.click("底部的立即购买按钮")

# 点不到时要有兜底,别默认它一定成功
if not ai.click("同意并继续"):
print("没找到那个按钮,换个描述试试")

想自己控制点击方式(长按、拖拽),就用 find() 拿坐标:

from ascript.ios import action

r = ai.find("列表里第一个商品的缩略图")
if r:
action.click(r["center_x"], r["center_y"], 800) # 长按 800ms

3. 判断页面状态

# 在不在
if ai.exists("登录按钮"):
ai.click("登录按钮")

# 是不是(返回真正的 bool,不是字符串)
if ai.ask_value("当前是不是支付成功页", value_type=bool):
print("下单完成")

# 让模型用自己的话描述(原文,只适合打印给人看)
print(ai.ask("当前是什么页面,用户在做什么"))
警告

ask() 的返回是模型原文,格式会飘,不要拿去做 if 判断。 判断一律走 exists()ask_value(..., value_type=bool)

4. 从屏幕上取数据

ask_value()Python 类型声明你要什么(参数名 value_type),返回值就是那个类型:

count = ai.ask_value("购物车里有几件商品", value_type=int)        # -> 3
total = ai.ask_value("订单总金额是多少", value_type=float) # -> 128.5
title = ai.ask_value("标题栏写的什么", value_type=str) # -> '订单确认'
paid = ai.ask_value("是否已经支付", value_type=bool) # -> False

多个值用 [T]

names  = ai.ask_value("所有商品名称", value_type=[str])           # -> ['面包', '牛奶']
prices = ai.ask_value("每件商品的价格", value_type=[float]) # -> [12.5, 8.0]

返回 None 表示模型说它答不出来,要判一下再用:

n = ai.ask_value("有几条未读消息", value_type=int)
if n is None:
print("看不出来") # 图里没有相关信息
elif n == 0:
print("一条都没有") # 计数为零是真实答案,和上面不是一回事
else:
print("有 %d 条" % n)

5. 把一张列表读成结构化数据

用带字段名的 dict 声明记录,字段名会一并告诉模型,它才好对号入座:

items = ai.ask_value("列表里所有商品", value_type=[{"name": str, "price": float}])
# -> [{'name': '面包', 'price': 12.5},
# {'name': '牛奶', 'price': 8.0}]

for it in items or []:
print(it["name"], it["price"])

单条记录就不加外面那层 []

info = ai.ask_value("这个商品的信息",
value_type={"title": str, "price": float, "stock": int})
# -> {'title': '面包', 'price': 12.5, 'stock': 30}

6. 等待页面变化

r = ai.wait("加载完成的商品列表", timeout=20)
if r is None:
print("等超时了")
警告

每轮都是一次完整的云端推理(几秒)并且都要计费timeout=20 大概只够跑 三五轮。不要图省事写 timeout=300

7. 只看一块区域:又快又省

rect=[left, top, right, bottom]这是最值得养成的习惯 —— 裁剪比降采样保真得多,图更小、更快、更省钱,而返回的仍然是完整屏幕坐标, 不用你自己加偏移。

# 只看底部 1/4 屏
ai.find("确定按钮", rect=[0, 1800, 1080, 2400])

# 只看顶部状态栏
ai.ask_value("现在电量百分之多少", value_type=int, rect=[0, 0, 1080, 100])

8. 一次截屏,多次提问

默认每次调用都会自动截一次屏。同一个画面要问好几件事时,自己截一次复用:

from ascript.ios import screen

shot = screen.capture() # 返回 PIL.Image,可直接传给 ai

total = ai.ask_value("总价", value_type=float, image=shot)
count = ai.ask_value("商品件数", value_type=int, image=shot)
addr = ai.ask_value("收货地址", value_type=str, image=shot)

image= 还接受 ndarray 和图片路径,见下文

提示

iOS 还有个更省事的办法:screen.cache(True) 开启截图缓存后, 后续的 capture() / 找图 / 找色都复用同一张截图,不用自己传来传去。 用完记得 screen.cache(False) 关掉,否则画面变了还在用旧图。

9. 描述不准时,用 hint 补充上下文

ai.find("确定按钮", hint="在底部弹窗里,不是顶部导航栏那个")
ai.ask_value("图中算式的结果", value_type=int, hint="只算红框里那一道")

小目标看不清时再考虑调清晰度(但优先试 rect):

ai.find("底部那个很小的图标", image_tokens=2048)

10. 给找图 / OCR 做兜底

推荐的用法不是"全用 AI",而是快的先上,认不出来再交给 AI

from ascript.ios.screen import Ocr

hits = Ocr.paddleocr(pattern="立即购买") # 毫秒级,免费,返回 list
r = hits[0] if hits else None

if r is None:
r = ai.find("底部的立即购买按钮") # 秒级,计费,但认得出语义

if r:
action.click(r["center_x"], r["center_y"])

Ocr.paddleocr() 返回的是列表ai.find() 返回单个 dict 或 None), 取到第一项之后两者结构一致,后面的代码不用分叉。

11. 完整示例:自动下单

from ascript.ios import action, screen
from ascript.ios.vlm import ascript_ai as ai

ai.init(api_key="sk-as-xxxxxxxx")

BOTTOM = [0, 1700, 1080, 2400] # 操作区基本都在下半屏,固定裁这块

try:
# 1) 确认在商品详情页
if not ai.ask_value("当前是不是商品详情页", value_type=bool):
raise SystemExit("页面不对,先手动进详情页")

# 2) 记下价格,超预算就不买
price = ai.ask_value("这个商品的价格", value_type=float)
if price is None or price > 200:
raise SystemExit("价格不合适:%s" % price)

# 3) 走结算流程
if not ai.click("立即购买", rect=BOTTOM):
raise SystemExit("没找到立即购买")

if ai.wait("确认订单页面的提交订单按钮", timeout=20) is None:
raise SystemExit("确认订单页没出来")

# 4) 提交前核对总价(一次截屏问两件事)
shot = screen.capture()
total = ai.ask_value("订单总金额", value_type=float, image=shot)
addr = ai.ask_value("收货地址", value_type=str, image=shot)
print("总价 %s,寄到 %s" % (total, addr))

ai.click("提交订单", rect=BOTTOM)

except ai.NotReadyError:
print("没配密钥,去 AI Studio 账户中心创建")
except ai.InsufficientBalanceError:
print("余额不足,请充值")
except ai.AsaiError as e:
print("调用失败:%s" % e)

方法总览

返回什么分成两族:

方法返回
find(target)dict / None,含标签、框、中心点
find_all(target, limit)list[dict]
ask(question)str,模型原文
ask_value(question, value_type)value_type 决定,见下文
exists(target)bool
wait(target, timeout)dict / None
click(target)bool
init() / status()配置

位置:find / find_all

find_all

按自然语言描述找出所有匹配目标,返回屏幕坐标

  • 函数
ai.find_all(target, rect=None, image=None, limit=10, confidence=0.0, image_tokens=None, hint=None)
  • 参数
参数类型是否必填说明
targetstr目标描述,越具体越准。"蓝色的登录按钮" 好过 "按钮"
rectlist[left, top, right, bottom] 限定搜索区域,强烈建议传
image-None = 自动截屏;也可传 ndarray / 图片路径 / Bitmap / PIL.Image
limitint最多返回几个,默认 10
confidencefloat置信度过滤,默认 0 不过滤
image_tokensint上传图片的清晰度预算,默认 1024,见下文
hintstr补充上下文
  • 返回

list[dict],每项形如:

{'text': '购物车', 'rect': [960, 120, 1040, 200],
'center_x': 1000, 'center_y': 160, 'confidence': 1.0}

空列表表示没找到。

  • 示例
items = ai.find_all("商品列表里的加入购物车按钮", limit=5)
for it in items:
print(it["text"], it["center_x"], it["center_y"])

find

find_all,但只返回可能性最高的一个,没找到返回 None

ai.find(target, rect=None, image=None, confidence=0.0, image_tokens=None, hint=None)
r = ai.find("确定按钮", rect=[0, 1800, 1080, 2400])
if r:
action.click(r["center_x"], r["center_y"])

值:ask / ask_value

ask

开放问答,返回模型原文

ai.ask(question, rect=None, image=None, image_tokens=None, hint=None)
print(ai.ask("当前是什么页面,用户在做什么"))
# '这是一个商品详情页,用户正在浏览一款蓝色的运动鞋……'
注意

ask() 的原文是给人看的,格式会飘,程序解析不可靠。 要拿去做判断的,一律用 ask_value()

ask_value

带类型地问一个值。返回值的类型由 value_type 决定。

ai.ask_value(question, value_type=str, rect=None, image=None, image_tokens=None, hint=None)
  • 参数
参数类型是否必填说明
questionstr要问的问题
value_type-想要什么类型,默认 str。见下文「类型写法一览」
rectlist[left, top, right, bottom] 限定区域,强烈建议传
image-None = 自动截屏;也可传 ndarray / 图片路径 / Bitmap / PIL.Image
image_tokensint上传图片的清晰度预算,默认 1024
hintstr补充上下文
  • 基本类型
ai.ask_value("有几个面包", value_type=int)              # -> 3
ai.ask_value("总价是多少", value_type=float) # -> 128.5
ai.ask_value("是不是登录页", value_type=bool) # -> True
ai.ask_value("标题栏写的什么", value_type=str) # -> '设置'
  • 多个值:用 [T]
ai.ask_value("每件商品的价格", value_type=[float])      # -> [12.5, 8.0, 30.0]
ai.ask_value("所有商品名称", value_type=[str]) # -> ['面包', '牛奶']
  • 成对的记录:用带字段名的 dict

字段名会一并告诉模型,它才好对号入座。

ai.ask_value("这个商品的信息", value_type={"title": str, "price": float, "stock": int})
# -> {'title': '面包', 'price': 12.5, 'stock': 30}

ai.ask_value("所有联系人", value_type=[{"name": str, "phone": str}])
# -> [{'name': '张三', 'phone': '138-0013-8000'}, ...]

某条记录缺某个字段时那个字段是 None,整条不会被丢掉。

  • 类型写法一览
写法含义
int / float单个数
bool / str单个布尔 / 字符串
[str]多个字符串
{"name": str, "phone": str}一条记录
[{"name": str, "age": int}]多条记录
三种复数写法都能用

AScript iOS 内嵌的是 Python 3.9,所以下面三种写法等价:

ai.ask_value("每件商品的价格", value_type=[float])              # 推荐
ai.ask_value("每件商品的价格", value_type=list[float])
ai.ask_value("每件商品的价格", value_type=typing.List[float])

推荐用 [float] 不是因为另外两种在 iOS 上有问题,而是 Android 端是 Python 3.8,写 list[float] 会在真机上抛 TypeError: 'type' object is not subscriptable。用 [T] 的脚本两端都能跑。

intfloat 不要混用

类型约束是在解码层真实生效的:问 "12.5 加 8" 时声明 int 会返回 None (答不出整数就如实拒答),而不是悄悄取整成 20。

计数、下标用 int;价格、比例、度量用 float

  • 关于返回 None

None 表示模型说它答不出来(图里没有相关信息),不是解析失败 —— 解析失败会抛 AsaiError。这两件事分开,脚本才能正确处理"没有"这种情况。

n = ai.ask_value("有几台冰箱", value_type=int)
if n is None:
print("模型看不出来")
elif n == 0:
print("确实一台都没有") # 计数为零是真实答案,和上面不是一回事
问坐标一定要用 find / find_all

不要用 ask_value 问坐标。服务端只看得到降采样后的那张图,它说出来的坐标在 那张图的像素空间里;只有 find* 这条路会把坐标换算回屏幕坐标。

ask_value 问坐标,拿到的数字直接点会点空。要点击点就用 ai.click(target), 或 ai.find(target)center_x / center_y

判断与动作

exists

目标在不在。等价于 find() 是否为 None,花费也一样。

ai.exists(target, rect=None, image=None, confidence=0.0, hint=None)
if ai.exists("弹窗上的关闭按钮"):
ai.click("弹窗上的关闭按钮")

wait

等目标出现,返回结果或 None

ai.wait(target, timeout=30, interval=0.0, rect=None, confidence=0.0, hint=None)
r = ai.wait("加载完成的商品列表", timeout=20)
警告

每轮都是一次完整的云端推理(几秒)并且都要计费,所以 timeout 设小一点。 interval 默认 0 —— 推理本身就是最大的间隔了。

wait() 刻意没有 image 参数:每轮重新截屏才是「等」的意义。

click

找到目标并点击它的中心点。找不到返回 False

ai.click(target, timeout=0, rect=None, image=None, confidence=0.0, hint=None)
ai.click("右上角的关闭按钮")
ai.click("提交订单", timeout=15) # 最多等 15 秒
信息

传了 imagetimeout 无效 —— 静态图不会变,等待没有意义。 而且那张图必须对应当前屏幕,否则算出来的坐标会点错地方。

公共参数

rect:限定区域(强烈建议传)

ai.find("确定按钮", rect=[0, 1800, 1080, 2400])

rect=[left, top, right, bottom]裁剪比降采样保真得多,图更小、更快、更省钱。 传了 rect 时坐标仍然返回完整的屏幕坐标,不需要你自己加偏移。

image:用你自己的图

wait() 外,所有方法都接受 image=。iOS 的 screen.capture() 默认返回 PIL.Image,可以直接传进来。

传入说明
None(默认)自动截取当前全屏
PIL.Imagescreen.capture() 的结果(iOS 默认就是这个)
ndarrayscreen.capture(format=screen.FORMAT_CV_MAT),或你自己处理过的图(BGR)
str图片文件路径

截一次、多次查询复用,能省下截屏开销:

from ascript.ios import screen

shot = screen.capture()
price = ai.ask_value("总价", value_type=float, image=shot)
count = ai.ask_value("商品件数", value_type=int, image=shot)

image_tokens:上传图片的清晰度

控制上传图片的清晰度(视觉 token 预算),默认 1024。

信息

它管的是输入的图,不是输出长度 —— 和 OpenAI 那个 max_tokens 是两回事。

视觉模型按 32×32 像素算一个 token,所以一张 1080×2400 的全屏图原本是 2550 个 token, 默认会被等比压到 1024:

image_tokens上传尺寸占原图
256352×76810%
512480×108820%
1024(默认)672×153640%
2048960×214479%
ai.find("底部那个很小的图标", image_tokens=2048)   # 小目标看不清时调大
ai.find("屏幕中间的大按钮", image_tokens=512) # 大目标够用了,省钱
优先考虑 rect,不是调这个

同样是减少 token,rect 是"只看这块但看得清",降采样是"整屏都看但都变糊"。

hint:补充上下文

所有方法都接受可选的 hint=

ai.find("确定按钮", hint="在底部弹窗里,不是顶部导航栏那个")
ai.ask_value("图中算式的结果", value_type=int, hint="只算红框里那一道")

hint 只补充"要找什么",输出格式仍由服务端决定 —— 这样服务端换模型时你的脚本不用改。

其它

init

ai.init(api_key=None, base_url=None, timeout=None, coord_space=None)
参数类型是否必填说明
api_keystrsk-as- 开头的密钥。不传则读环境变量 ASCRIPT_AI_KEY
base_urlstr自建/测试环境才需要传
timeoutint单次请求超时秒数,默认 60
coord_spacestr极少用。"abs" / "norm1000" / "ratio",见下

coord_space 默认自动判定模型输出的坐标空间。已知有一个信息论上的死角: 模型吐 0~1000 归一化、目标又恰好在左上角时,单看一帧无法区分。 真遇到系统性偏移时才手动钉死:

ai.init(api_key="sk-as-...", coord_space="norm1000")

status

当前配置状态,排查问题时用。

ai.status()
# {'configured': True, 'base_url': 'https://ai.ascript.cn/v1', 'coord_space': 'auto'}

异常

所有异常都挂在模块上,用 ai.XXX 取,不需要单独 import。 它们全部继承自 ai.AsaiError,只想粗粒度兜底的话捕获它一个就够。

异常含义该怎么处理
NotReadyError还没配密钥,请求根本没发出去init() 或设 ASCRIPT_AI_KEY
AuthenticationError密钥无效或已被删除去账户中心重新创建
CredentialExpiredError密钥还在,但它绑定的登录状态失效了先重新登录网页,再重新创建密钥
InsufficientBalanceError余额不足提示用户充值,重试没用
InvalidRequestError参数有问题改代码,重试没用
RateLimitError调用太频繁e.retry_after 秒后再试
ServiceUnavailableError服务端暂时不可用可以稍后重试
UpstreamError上游模型服务故障(服务端已自动重试过一次)稍后重试
NetworkError连不上服务端检查网络
ProtocolError服务端返回了读不懂的内容正常不该发生,报 request_id 给我们

CredentialExpiredError 继承自 AuthenticationError,所以 except ai.AuthenticationError 能把两者一起兜住。

每个异常都带这几个属性,排查时有用:

属性说明
message给人看的说明(服务端返回的中文原文)
type机器可读的错误类型,如 "insufficient_balance"
status_codeHTTP 状态码,本地错误时为 None
request_id服务端请求 ID,找我们排查时报这个
from ascript.ios.vlm import ascript_ai as ai
import time

try:
r = ai.find("登录按钮")

except ai.NotReadyError:
print("还没配密钥")

except ai.InsufficientBalanceError:
print("余额不足,请充值") # 重试没用,直接退出

except ai.RateLimitError as e:
time.sleep(e.retry_after or 5) # 服务端建议的等待秒数
r = ai.find("登录按钮")

except ai.AuthenticationError:
print("密钥有问题,去账户中心重新创建") # 顺带兜住 CredentialExpiredError

except ai.AsaiError as e:
print("调用失败:%s (request_id=%s)" % (e.message, e.request_id))
提示

写脚本时至少把 InsufficientBalanceError 单独拎出来 —— 余额不足和网络抖动看起来都是"调用失败",但一个该停、一个该重试, 混在一起兜会让脚本在没钱的情况下疯狂空转重试。

计费

按实际 Token 用量从 AI Studio 账户余额扣费,与网页对话走同一套计费和日志链路。 每次调用都能在账户中心的「使用日志」里查到明细。

rect= 传得越小,图越小,越省。