跳到主要内容
valen

API 入门

本教程迁移旧 API Notebook:发送修改收货地址的文本、识别图片中的动物群体、判断视频中间帧里的酱汁颜色。输出包含命名答案和真实抽帧位置。完整业务流程分别在后面的三个应用教程维护。

HTTP 客户端使用 Decisions。图片以 data URL 传入;视频示例先抽帧再发送图片,不代表托管 API 支持直接视频推理。仅在所选服务需要认证时设置 VALEN_API_KEY。

安装与输入​

Python 3.11+。先下载小型源文件包并解压,然后安装所需依赖:

Bash
curl -fLO https://valen-team.github.io/cookbook/valen-cookbook.zip
python -m zipfile -e valen-cookbook.zip .
cd valen-cookbook
python -m pip install -r requirements.txt

明确执行后才获取以下小素材(图片约 58 KB,视频约 395 KB);也可将有权使用的同名文件放在 --asset-dir 指定目录。

Bash
curl -fL https://raw.githubusercontent.com/Valen-Team/Valen/main/assets/cookbook/sheep.jpg -o sheep.jpg
curl -fL https://raw.githubusercontent.com/Valen-Team/Valen/main/assets/cookbook/momos.mp4 -o momos.mp4
samples.json
{
"id": "ticket-001",
"text": "I moved. Can you ship my order to 12 Elm Street instead of the old address?",
"questions": [
{
"name": "intent",
"type": "choice",
"instructions": "Which intent does the user's message express?",
"choices": [
{
"value": "billing_question",
"description": "Asks about a charge, invoice or payment"
},
{
"value": "cancel_order",
"description": "Wants to cancel an order"
},
{
"value": "change_address",
"description": "Wants to change the delivery address"
},
{
"value": "report_damage",
"description": "Received an item that is broken or damaged"
},
{
"value": "track_order",
"description": "Wants to know where an order is or when it arrives"
}
]
},
{
"name": "address_change",
"type": "predicate",
"instructions": "The user requests a delivery address change."
},
{
"name": "explicitness",
"type": "score",
"instructions": "How explicit is the request to change the delivery address?",
"levels": [
{
"label": "none",
"description": "No request"
},
{
"label": "possible",
"description": "Asks whether a change is possible"
},
{
"label": "explicit",
"description": "Explicitly requests a change"
}
]
}
]
}

运行​

Bash
python api_quickstart.py --backend remote --model valen-2b --asset-dir . --output result.json
# An existing local HTTP deployment instead:
python api_quickstart.py --backend remote --base-url http://127.0.0.1:8000/v1 --model valen --asset-dir . --output result.json

本地 ONNX 也可直接运行:把参数改为 --backend onnx --model /path/to/Valen-0.8B-ONNX,并先安装 valen-sdk[onnx,video]==0.2.0。GGUF 服务通过 --backend remote 接入;不能把 GGUF 文件传给 Valen.local。

完整可执行代码​

api_quickstart.py
"""API basics: text, one image and a sampled video frame. Apache-2.0."""
from common import client, emit, image_bytes, image_input, middle_frame, parser, request, samples


def main():
args = parser(__doc__).parse_args()
data = samples()
with client(args) as predict:
text = request(predict, data["ticket"]["text"], data["ticket"]["questions"])
image = request(predict, image_input(image_bytes(args.asset_dir / data["product"]["image"])), [data["product"]["question"]])
payload, sampling = middle_frame(args.asset_dir / data["video"]["path"])
video_frame = request(predict, image_input(payload), [data["video"]["question"]])
emit({"text": text, "image": image, "video_frame": video_frame, "sampling": sampling}, args)


if __name__ == "__main__":
main()

共享运行器与 samples.json 已包含在下载包中,与网页展示内容使用同一个维护源。

common.py
common.py
"""Shared cookbook runner. Apache-2.0. No implicit model or media downloads."""
import argparse
import base64
from contextlib import contextmanager
import json
import math
import os
from pathlib import Path

HERE = Path(__file__).resolve().parent


def samples():
return json.loads((HERE / "samples.json").read_text())


def parser(description):
p = argparse.ArgumentParser(description=description)
p.add_argument("--backend", choices=["remote", "onnx", "native"], default="remote")
p.add_argument("--base-url", default=os.environ.get("VALEN_BASE_URL", "https://llm-proxy.openxxlab.com/v1"))
p.add_argument("--model", default=os.environ.get("VALEN_MODEL"))
p.add_argument("--threads", type=int, default=4)
p.add_argument("--asset-dir", type=Path, default=Path("."))
p.add_argument("--output", type=Path)
return p


@contextmanager
def client(args):
if args.threads < 1:
raise ValueError("--threads must be positive")
if args.backend == "native":
if not args.model or not Path(args.model).is_dir():
raise ValueError("native requires --model pointing to an existing local Valen model directory")
import torch
from transformers import AutoModel, AutoProcessor
torch.set_num_threads(args.threads)
model = AutoModel.from_pretrained(args.model, trust_remote_code=True, local_files_only=True,
dtype=torch.float32, attn_implementation="eager").float().cpu().eval()
model.backbone_autocast_dtype = None
processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True, local_files_only=True)

def predict(body):
with torch.inference_mode():
return model.predict(body, processor=processor, execution="shared_state",
temperature=body.get("temperature", 1.0))
yield predict
else:
from valen_sdk import Valen
if args.backend == "onnx":
if not args.model or not Path(args.model).is_dir():
raise ValueError("onnx requires --model pointing to an existing local ONNX bundle")
instance = Valen.local(args.model, variant="fp32", local_files_only=True, num_threads=args.threads)
else:
instance = Valen.remote(args.base_url, model=args.model or "valen-2b",
api_key=os.environ.get("VALEN_API_KEY"), max_retries=0)
with instance:
yield instance.predict


def image_input(payload):
"""Inline media works across HTTP services; never send a client-local path."""
return [{"role": "user", "content": [
{"type": "input_text", "text": "Answer using only the visible image."},
{"type": "input_image", "image_url": "data:image/jpeg;base64," + base64.b64encode(payload).decode("ascii")},
]}]


def image_bytes(path):
import io
from PIL import Image, ImageOps
with Image.open(path) as image:
image = ImageOps.exif_transpose(image).convert("RGB")
image.thumbnail((512, 512))
buffer = io.BytesIO()
image.save(buffer, format="JPEG", quality=95)
return buffer.getvalue()


def middle_frame(path):
"""Decode a bounded clip twice: count first, then keep only its middle frame."""
import av
import io
from PIL import ImageOps
with av.open(str(path)) as video:
stream = video.streams.video[0]
if stream.width * stream.height > 4096 * 2160:
raise ValueError("Clip exceeds the cookbook's 4K frame limit")
count = 0
for _ in video.decode(video=0):
count += 1
if count > 1800:
raise ValueError("Clip exceeds the cookbook's 1800-frame limit")
if not count:
raise ValueError("Clip contains no decodable frames")
target = count // 2
with av.open(str(path)) as video:
for index, frame in enumerate(video.decode(video=0)):
if index == target:
image = ImageOps.exif_transpose(frame.to_image()).convert("RGB")
image.thumbnail((512, 512))
buffer = io.BytesIO()
image.save(buffer, format="JPEG", quality=95)
return buffer.getvalue(), {"frame_index": target, "total_frames": count,
"timestamp_seconds": float(frame.time) if frame.time is not None else None}
raise ValueError("Clip changed while decoding")


def request(predict, source, questions):
response = predict({"input": source, "questions": questions, "temperature": 1.0})
answers = response["answers"]
if [a["name"] for a in answers] != [q["name"] for q in questions]:
raise ValueError("Service returned missing or reordered answers")
return {a["name"]: a for a in answers}


def choice_status(answer, *, threshold=0.8, margin=0.2):
probabilities = [float(x["probability"]) for x in answer["probabilities"]]
if (not probabilities or any(not math.isfinite(x) or not 0 <= x <= 1 for x in probabilities)
or abs(sum(probabilities) - 1) > 1e-4):
raise ValueError("Invalid choice probability distribution")
selected = next(x for x in answer["probabilities"]
if type(x["value"]) is type(answer["choice"]) and x["value"] == answer["choice"])
ordered = sorted(probabilities, reverse=True)
confidence = float(selected["probability"])
gap = ordered[0] - (ordered[1] if len(ordered) > 1 else 0)
return confidence, confidence >= threshold and gap >= margin


def emit(value, args):
text = json.dumps(value, indent=2, ensure_ascii=False, allow_nan=False) + "\n"
if args.output:
args.output.write_text(text)
print(text, end="")

真实验证结果​

真实模型验证: 2026-10-26 · Valen-0.8B / ONNX FP32 · SDK 0.2.0 · ONNX Runtime 1.31.0 / CPU / SDK HTTP service.

源模型 revision: a0a10112716f54457bad6f60b792ad334f9e89fd. 机器可读结果

实际业务输出
{
"text": {
"intent": {
"name": "intent",
"type": "choice",
"probabilities": [
{
"value": "billing_question",
"probability": 0.0000014444915728188846
},
{
"value": "cancel_order",
"probability": 0.00013284262147016516
},
{
"value": "change_address",
"probability": 0.9997590278141875
},
{
"value": "report_damage",
"probability": 0.000004776970793180753
},
{
"value": "track_order",
"probability": 0.00010190810197642957
}
],
"confidence": 0.9996987847677343,
"choice": "change_address"
},
"address_change": {
"name": "address_change",
"type": "predicate",
"probability": 0.9862781119951404
},
"explicitness": {
"name": "explicitness",
"type": "score",
"probabilities": [
{
"label": "none",
"value": 0,
"probability": 0.015131870978155534
},
{
"label": "possible",
"value": 1,
"probability": 0.1913538155432454
},
{
"label": "explicit",
"value": 2,
"probability": 0.7935143134785991
}
],
"confidence": 0.6675736637506653,
"score": 1.7783824425004437
}
},
"image": {
"group": {
"name": "group",
"type": "choice",
"probabilities": [
{
"value": "pride",
"probability": 0.00028894281584105957
},
{
"value": "flock",
"probability": 0.9960099221422577
},
{
"value": "clowder",
"probability": 0.002903469483494964
},
{
"value": "gang",
"probability": 0.0007976655584062285
}
],
"confidence": 0.9946798961896769,
"choice": "flock"
}
},
"video_frame": {
"sauce_color": {
"name": "sauce_color",
"type": "choice",
"probabilities": [
{
"value": "brown",
"probability": 0.012937617960974548
},
{
"value": "white",
"probability": 0.9773176758616222
},
{
"value": "red",
"probability": 0.006636453849260038
},
{
"value": "orange",
"probability": 0.0031082523281432115
}
],
"confidence": 0.9697569011488296,
"choice": "white"
}
},
"sampling": {
"frame_index": 98,
"total_frames": 196,
"timestamp_seconds": 3.2666666666666666
}
}

这里记录的是指定版本与后端的实测,不意味着默认公共服务的模型版本、分数或速度完全相同。业务阈值单元测试使用构造响应,真实模型执行单独记录,二者不互相替代。

素材、许可与相关文档​

代码与作者编写的 JSON 文本样例采用 Apache-2.0。工单为虚构示例。sheep.jpg 与 momos.mp4 沿用旧 Notebook 的评测片段和稳定素材 URL;旧目录未提供原始数据集归属及单独的媒体许可记录,因此这里不授予媒体再分发或商业使用许可。生产或再分发时请替换为有权使用的素材,勿把代码许可视为媒体许可。官网不额外复制这些媒体文件。

Decisions API · SystemOne API · ONNX / SDK · Native deployment · GGUF deployment · Evaluation · Validation and performance