AI云识别
from ascript.android import plug
plug.load("ascript_ai")
import ascript_ai as ai
为了让老版本 AScript 也能用上,AI 识别不再随主程序发布,
而是抽取成 ascript_ai 插件独立维护、独立更新。
所以使用前必须先 plug.load("ascript_ai") 把插件装载进来,之后
import ascript_ai 才能拿到模块。这一步是一次性的,脚本里写一次即可。
用自然语言描述在屏幕上找东西、问值、问页面状态,推理跑在 AScript AI Studio 云端。
不需要模板图、不需要固定文字、不需要训练模型 —— 直接用一句话描述你要什么。
- 没有模板图、也没有固定文字,只能靠语义描述的目标:"那个红色的关闭按钮"
- 界面改版频繁,写死的坐标和模板图天天失效
- 需要理解页面语义:"当前是不是登录页"、"列表里价格最低的那一项"
- 从屏幕上提取结构化数据:把商品列表读成
[{"name":..., "price":...}] - 图色/OCR 都试过但认不出来的兜底方案
能用 FindImages.find()(有模板图)或 Ocr.find()(有固定文字)解决的场景,
不要用AI云识别。那些是毫秒级且免费,这个是秒级且按量计费。
AI云识别是给"前两者做不到"的场景兜底的,不是用来替代它们的。
准备工作
1. 获取密钥
登录 AScript AI Studio → 账户中心 → 「API 调用」页面创建密钥。
密钥形如 sk-as- 开头的字符串,明文只在创建时显示一次,请立即保存。丢失只能重新创建。
每个账号最多 20 个密钥。密钥创建时会封存当时的登录凭证,如果账号中心让该凭证失效,
调用会返回 credential_expired,需要重新登录网页并重新创建密钥。
2. 初始化
from ascript.android import plug
plug.load("ascript_ai")
import ascript_ai as ai
ai.init(api_key="sk-as-xxxxxxxx")
也可以设置环境 变量 ASCRIPT_AI_KEY,这样脚本里连 init() 都可以省掉。
快速开始
1. 第一个脚本
from ascript.android import plug
plug.load("ascript_ai")
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() 的返回结构与 ascript.android.screen.Ocr.find() 完全一致,
Ocr 认不出来的时候可以直接换一行过来。
设了环境变量 ASCRIPT_AI_KEY 的话,连 ai.init() 这行都可以省掉。
2. 找到并点击
最常用的一句。click() 内部会自己截屏、定位、点中心点,找不到返回 False。
ai.click("底部的立即购买按钮")
# 点不到时要有兜底,别默认它一定成功
if not ai.click("同意并继续"):
print("没找到那个按钮,换个描述试试")
想自己控制点击方式(长按、拖拽),就用 find() 拿坐标:
from ascript.android 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.android import screen
shot = screen.capture_cv()
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= 还接受图片路径、Bitmap、PIL.Image,见下文;
capture_cv() / capture_pil() / capture_bitmap() 的说明见屏幕图像 · 截图快捷方法。
9. 描述不准时,用 hint 补充上下文
ai.find("确定按钮", hint="在底部弹窗里,不是顶部导航栏那个")
ai.ask_value("图中算式的结果", value_type=int, hint="只算红框里那一道")
小目标看不清时再考虑调清晰度(但优先试 rect):
ai.find("底部那个很小的图标", image_tokens=2048)
10. 给找图 / OCR 做兜底
推荐的用法不是"全用 AI",而是快的先上,认不出来再交给 AI:
from ascript.android.screen import Ocr
r = Ocr.find("立即购买") # 毫秒级,免费
if r is None:
r = ai.find("底部的立即购买按钮") # 秒级,计费,但认得出语义
if r:
action.click(r["center_x"], r["center_y"])
两者返回结构一致,所以后面的代码不用分叉。
11. 完整示例:自动下单
from ascript.android import action, screen, plug
plug.load("ascript_ai")
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_cv()
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)
- 参数
| 参数 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| target | str | 是 | 目标描述,越具体越准。"蓝色的登录按钮" 好过 "按钮" |
| rect | list | 否 | [left, top, right, bottom] 限定搜索区域,强烈建议传 |
| image | - | 否 | None = 自动截屏;也可传 ndarray / 图片路径 / Bitmap / PIL.Image |
| limit | int | 否 | 最多返回几个,默认 10 |
| confidence | float | 否 | 置信度过滤,默认 0 不过滤 |
| image_tokens | int | 否 | 上传图片的清晰度预算,默认 1024,见下文 |
| hint | str | 否 | 补充上下文 |
- 返回
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)
- 参数
| 参数 | 类型 | 是否必填 | 说明 |
|---|---|---|---|
| question | str | 是 | 要问的问题 |
| value_type | - | 否 | 想要什么类型,默认 str。见下文「类型写法一览」 |
| rect | list | 否 | [left, top, right, bottom] 限定区域,强烈建议传 |
| image | - | 否 | None = 自动截屏;也可传 ndarray / 图片路径 / Bitmap / PIL.Image |
| image_tokens | int | 否 | 上传图片的清晰度预算,默认 1024 |
| hint | str | 否 | 补充上下文 |
- 基本类型
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}] | 多条记录 |
list[float]设备上是 Python 3.8,内建泛型下标要 3.9+,写了会在真机上抛
TypeError: 'type' object is not subscriptable。
用 [float],或 typing.List[float]。
int 和 float 不要混用类型约束是在解码层真实生效的:问 "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("确实一台都没有") # 计数为零是真实答案,和上面不是一回事
不要用 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 秒
传了 image 时 timeout 无效 —— 静态图不会变,等待没有意义。
而且那张图必须对应当前屏幕,否则算出来的坐标会点错地方。
公共参数
rect:限定区域(强烈建议传)
ai.find("确定按钮", rect=[0, 1800, 1080, 2400])
rect=[left, top, right, bottom]。裁剪比降采样保真得多,图更小、更快、更省钱。
传了 rect 时坐标仍然返回完整的屏幕坐标,不需要你自己加偏移。
image:用你自己的图
除 wait() 外,所有方法都接受 image=。
| 传入 | 说明 |
|---|---|
None(默认) | 自动截取当前全屏 |
ndarray | cv2 读出来的、或你自己处理过的图(BGR) |
str | 图片文件路径 |
Android Bitmap | screen.capture_bitmap() 的结果 |
PIL.Image | screen.capture_pil() 的结果 |
截一次、多次查询复用,能省下截屏开销:
from ascript.android import screen
shot = screen.capture_cv()
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 | 上传尺寸 | 占原图 |
|---|---|---|
| 256 | 352×768 | 10% |
| 512 | 480×1088 | 20% |
| 1024(默认) | 672×1536 | 40% |
| 2048 | 960×2144 | 79% |
ai.find("底部那个很小的图标", image_tokens=2048) # 小目标看不清时调大
ai.find("屏幕中间的大按钮", image_tokens=512) # 大目标够用了,省钱
同样是减少 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_key | str | 否 | sk-as- 开头的密钥。不传则读环境变量 ASCRIPT_AI_KEY |
| base_url | str | 否 | 自建/测试环境才需要传 |
| timeout | int | 否 | 单次请求超时秒数,默认 60 |
| coord_space | str | 否 | 极少用。"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_code | HTTP 状态码,本地错误时为 None |
request_id | 服务端请求 ID,找我们排查时报这个 |
from ascript.android import plug
plug.load("ascript_ai")
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= 传得越小,图越小,越省。
关于端侧视觉
端侧视觉(在手机本地跑视觉模型)目前不开放使用 —— 本地小模型单次推理 15~30 秒、输出精度不足以支撑业务。
Android 上的 AI 识别请统一使用本页的 AI云识别。