基于RTOS SDK实现拍学机
概览
本文主要介绍如何基于百度大模型互动方案实现AI拍学机功能开发;
拍学机接入有两种实现方式, 功能能力完全一致, 区别只在端侧的调用形式:
| 方式 | 说明 | 调用形式 |
|---|---|---|
| SDK 方式 | 使用 baidu_chat_agent_engine_* 系列接口, 协议细节由 SDK 封装 |
C 接口 + 回调结构体 BaiduChatAgentEvent |
| WS 方式 | 端侧与 AI Agent Server 直接建立 WebSocket 长连接, 自行收发协议文本帧 | WS 文本帧命令 [SET]: / [E]: / [F]: |
后文中的需求描述、prompt 写法、标签约定两种方式共用; 两种方式的具体调用实现分别在「方案一:SDK 方式」「方案二:WS 方式」两章独立描述, 任选其一即可完成全部功能。
开发准备
- 参考快速入门文档开通服务、创建应用并获取Appid 及License Key;
-
确定接入方式, SDK 方式或 WS 方式:
需求场景
拍学机的核心功能需求一般包含 AI对话、拍图识物、拍图问、答、拍图识字、拍照成画、拍图物效等几个部分。 其中:
- AI对话:指基本的大模型实时语音交互;
- 拍图识物:指拍图识别后,大模型理解并描述图片内容,单次交互;
- 拍图问答:指拍图识别后, 用户可针对图片内容进行多轮问答交互;
- 拍图识字:指拍图识别后, 大模型根据prompt要求返回图中物体的中英文读音、拼音、释义、例句等信息;TTS仅对中英文物体名称、单词进行一次播报,用户点击可再次播报,内容详情则进行屏幕展示, 如 :

- 拍照成画:拍图上传后,用户可通过按钮切换不同的风格, 包括:涂鸦、卡通、写真、3D;
- 拍照物效:与拍照成画基本相同,物效主要针对人像生成不同的特效;
视觉prompt与标签消息(XCUSTOM)
拍图识字等场景需要约定模型的输出格式, 这部分的需求描述、prompt 写法与标签约定两种接入方式完全共用, 差异只在「下发 prompt」与「接收标签」的调用形式, 见后文两章。
拍图识字功能实际也是视觉的一种应用, 只不过, 相对于普通的图片识别,拍图识字有两点特殊要求:
- 需要对模型输出的内容进行定义,指定其输出识字卡片形式;
- 模型输出的内容有涉及需要TTS报播的, 也有不需要报播的(如释义部分,用户只需要上屏显示即可, 不需要报播 ); 这两个需求, 可以分别通过方案提供的视觉prompt自定义更新及标签功能进行实现; 具体实现方式如下:
视觉模型prompt自定义更新
默认的视觉模型prompt 只能对图片进行基本的识别、理解,针对于识字场景, 则需要更新视觉模型prompt。
拍学机常用的三套视觉 prompt 及其输出 key:
| 模式 | 输出 key | 用途 |
|---|---|---|
| identify | XCUSTOM.identify |
拍万物(中英文名 + 拼音 + 释义 + 组词 + 例句) |
| shoot_chinese | XCUSTOM.shoot_chinese |
拍图识中文 |
| shoot_english | XCUSTOM.shoot_english |
拍图识英文 |
写 prompt 的几个要点:
- 强制 JSON、禁止解释性文字; 允许 JSON 前有一句待播报的名称, 但 JSON 中间夹注释会解析失败。
- 限定 key 白名单。 端侧按固定 key 名匹配。
- 限定输出规模。 端侧数组/字符串长度有上限, 超长可能被截断;
- 明确兜底话术。 如 「请拍汉字哦!」、「我还不会这个字。」, 让模型识别失败时走 TTS 播报而不是吐半个 JSON。
- 强调 UTF-8 完整汉字。 嵌入式端按字节截断字符串, 半个汉字会让屏幕显示乱码。
按上述要点更新 prompt 后, 模型返回的内容如下, TTS 会播报中英文 “ 猫 / cat” ,其它部分不会播报, 只会当做普通消息传递到端侧进行显示.
这里 { "XCUSTOM": ...} 包裹的内容不会播报, 只会当作普通文本消息传递到端侧,这个则是我们接下来要介绍的标签消息功能。
1猫。
2cat。
3{ "XCUSTOM":
4{
5"name_ch": "猫",
6"name_en": "cat",
7"comments_ch": {
8"name": "猫",
9"strokenumber":11,
10"radical":"犭",
11"comment":"1.哺乳动物,面部略圆,躯干长,耳壳短小,眼大,瞳孔随光线强弱而缩小放大,四肢较短,掌部有肉质的垫,行动敏捷,善跳跃,能捕鼠,毛柔软,有黑、白、黄、灰褐等颜色。种类很多。词语:猫腻、狸猫。\n2. 躲藏:猫冬(指躲在家里过冬)",
12"example_sentence":"我家养了一只可爱的~。",
13},
14"comments_en": {
15"name": "cat",
16"soundmark_us":"/kæt/",
17"soundmark_en":"/kæt/",
18"comment":"n. 猫; 猫科动物;\nv. 把(锚)吊放在锚架上; 钓(鱼);\nn. (Cat) (美、俄、法、德)卡特(人名)",
19"example_sentence":"The cat is sleeping on the sofa.",
20},
21}
22}
标签功能
标签是指与大模型事先约定的、对模型输出起补充、增强、注释等意义的一些信息片段, 标签信息一般需要在大模型prompt里进行定义,然后, 模型在回复时会携带相应的标签,TTS不会对标签内容进行播报,标签内容会作为普通消息随大模型输出返回给客户端;
我们内置了若干标签在某些场景进行使用,当前开放给用户可使用的标签为 XCUSTOM 标签, 格式为 { "XCUSTOM": { ... } }(如上示例输出), 标签功能具体使用方式可参考标签消息。
- SDK 方式: RTOS SDK 有单独的标签事件回调,用户可从标签事件回调获得标签消息;
- WS 方式: 标签消息通过
[E]:[TAG_MSG]下发, 前缀之后即为标签 JSON。
标签消息的载荷统一包一层 XCUSTOM, 内层第一个 key 决定标签类型, 一条消息只承载一种类型:
| 内层 key | 必填字段 | 用途 |
|---|---|---|
identify |
name_ch |
拍万物 |
shoot_chinese |
name |
拍图识中文 |
shoot_english |
name_en |
拍图识英文 |
排查顺序建议自上而下: 先确认收到了 [E]:[TAG_MSG](说明服务端已下发), 再确认载荷里有 XCUSTOM 外层(否则是 prompt 没让模型包 XCUSTOM), 最后确认内层 key 与端侧解析的 key 字符串一致。
方案一:SDK 方式
本章使用 RTOS SDK 的 baidu_chat_agent_engine_* 接口与 BaiduChatAgentEvent 回调实现全部拍学机功能, 协议细节由 SDK 封装。
AI对话
当前的RTOS 大模型实时互动方案即可实现基本的AI 语音对话功能, 无需额外的视觉/生图配置。
拍图识物
可基于视觉理解+图片模式即可实现该功能。
- 相关接口说明:
- 更新视觉理解模式
1/**
2 * @brief 更新视觉理解模式
3 * @param engine engine 实例指针
4 * @param mode 0:图片模式, 适合单次图片识别; 1:视频流模式,适合持续的视觉理解;
5 */
6void baidu_chat_agent_engine_update_visual_mode(BaiduChatAgentEngine *engine, const int mode);
- 图片发送接口,可使用该接口发送一帧jpeg图片, 图片理解时, 一般先向服务端发送一张图片, 然后再发送图片识别的query;
1/**
2 * @brief 向 AI Agent Server发送一帧jpeg图片数据
3 * @param engine engine 实例指针
4 * @param data 一帧jpeg图片数据
5 * @param len 数据长度
6 */
7void baidu_chat_agent_engine_send_video(BaiduChatAgentEngine* engine, const uint8_t* data, size_t len);
- 文本query发送接口, 图片识别query除语音说出外, 也可通过该接口以文本形式发出;
1/**
2 * @brief 向 AI Agent Server发送文本query
3 * @param engine engine 实例指针
4 * @param text 文本内容, 如 “图片中是什么”
5 */
6void baidu_chat_agent_engine_send_text(BaiduChatAgentEngine* engine, const char* text);
- 视觉图片请求回调, 若服务端在收到图片识别query时没有接收到图片, 则服务端会通过该回调请求端侧发送一张图片。
1void (*onVisionImageRequest)(void);
- 服务端接收图片回调, 服务端收到并成功接收图片后通过该回调通知端侧, 图片识别query需在该回调之后再发出。
1void (*onVisionImageAck)(const char* name);
- 实现流程
- 初始化
1// 1. 定义智能体创建config , 配置 enable_visual 为true
2#define JSON_CONFIG_TEMPLATE_VISUAL "{\"app_id\": \"%s\", \"config\" : \"{\\\"llm\\\" : \\\"%s\\\", \\\"llm_token\\\" : \\\"no\\\", \\\"enable_visual\\\" : \\\"%s\\\", \\\"dfda\\\" : \\\"true\\\",\\\"remote_music_player\\\" : \\\"true\\\", \\\"rtc_ac\\\": \\\"pcmu\\\", \\\"lang\\\" : \\\"%s\\\"}\", \"quick_start\": true}"
3
4static bool g_enable_visual = true; // true: 视觉理解模式(依赖开启视频), 与图片生成模式互斥; false: 普通语音交互模式(默认)
5static bool g_vision_mode = VISION_MODE_IMAGE; // VISION_MODE_IMAGE :视觉理解图片模式; VISION_MODE_STREAM 视觉视频流模式 (默认)
6static bool g_enable_image_generate = false; // 开启图片生成模式(默认开启),依赖开启视频。与视觉理解模式互斥;
7...
8
9void brtc_init(void)
10{
11 BaiduChatAgentEvent events = {
12 .onError = onErrorCallback,
13 .onCallStateChange = onCallStateChangeCallback,
14 .onConnectionStateChange = onConnectionStateChangeCallback,
15 .onUserAsrSubtitle = onUserAsrSubtitleCallback,
16 .onFunctionCall = onFunctionCall,
17 .onMediaSetup = onMediaSetup,
18 .onAIAgentSubtitle = onAIAgentSubtitle,
19 .onAIAgentSpeaking = onAIAgentSpeaking,
20 .onAudioPlayerOp = onAudioPlayerOp,
21 .onAudioData = onAudioData,
22 .onVideoData = onVideoData,
23 .onLicenseResult = onLicenseResult,
24 .onVisionImageRequest = onVisionImageRequest, // 设置视觉图片请求回调
25 .onVisionImageAck = onVisionImageAck, // 设置服务端接收图片回调
26 .onMediaGenerateResult = onMediaGenerateResult, // 设置图片生成结果回调
27 };
28
29 AgentEngineParams agentParams;
30 memset(&agentParams, 0, sizeof(agentParams));
31 setUserParameters(&agentParams);
32 // 初始化
33 int result = baidu_chat_agent_engine_init(engine, &agentParams);
34 ...
35 baidu_chat_agent_engine_call(engine);
36}
37
38int sendGenerateAIAgentCall() {
39 char config_data[MAX_PARAM_LENGTH] = {0};
40 // 视觉理解与图片生成都需要开启视频功能
41 snprintf(config_data, sizeof(config_data), JSON_CONFIG_TEMPLATE_VISUAL,
42 g_appid, DEFAULT_BRTC_LLM, (g_enable_visual || g_enable_image_generate) ? "true": "false", DEFAULT_BRTC_LANG);
43
44 char request_url[MAX_PARAM_LENGTH] = {0};
45 snprintf(request_url, sizeof(request_url), "%s/generateAIAgentCall", g_platform_host);
46 return http_post(request_url, config_data, &call_resp);
47}
- 设置视觉图片模式并上传图片: 调用
baidu_chat_agent_engine_update_visual_mode(engine, VISION_MODE_IMAGE)切换到图片模式, 再调用baidu_chat_agent_engine_send_video()发送一帧jpeg图片; - 等待
onVisionImageAck回调, 确认服务端已收到图片后再发出图片识别query, 如 “图片中是什么”, 可以语音说出, 也可以调用baidu_chat_agent_engine_send_text(engine, "图片中是什么")以文本形式发送; 模型收到query后即返回对图片内容的描述。
拍图问答
基于视觉理解+视频流模式即可实现该功能。 其实现方法与上述拍图识物基本相同,主要不同在于:
- 对话状态下, 调用
baidu_chat_agent_engine_update_visual_mode(engine, VISION_MODE_STREAM)设置视频流模式; - 周期性调用
baidu_chat_agent_engine_send_video()发送图片, 建议 1000ms 采集、发送一张图片, 过高的图片发送频率可能造成视觉模型tokens的过度消耗; - 视频流模式下,视觉模型处理持续进行, 可随时发出图片识别query;
拍图识字
prompt 内容与输出约定见上文「视觉prompt与标签消息(XCUSTOM)」, SDK 方式下通过如下接口下发 prompt、并从标签事件回调获取标签消息。
涉及接口:
1/**
2 * @brief 向 AI agent server 发送事件消息, 自定义的视觉prompt通过该接口发送
3 * @param engine engine 实例指针
4 * @param event 事件消息字符串
5 */
6void baidu_chat_agent_engine_send_event_to_agent(BaiduChatAgentEngine* engine, const char* event);
使用方式参考:
1// 1. 自定义拍图识字视觉prompt, 可参考这个修改自定义的prompt, 注意转义符号;
2static const char* object_vision_prompt =
3 "# 你是一位资深的早教老师,每次会收到一张来自幼儿园或小学生的物品照片。请识别照片中的物品并输出它的中文汉字和英文单词名称及如字典一样详细的中英文释义。\\\\n\\\\n"
4 "## 中文释义要求:\\\\n"
5 "1. 根据<汉字>进行回答\\\\n"
6 "2. 回答<汉字>的笔画数量,格式:`strokenumber:{number}`\\\\n"
7 "3. 回答<汉字>的偏旁部首,格式:`radical:{word}`\\\\n"
8 "4. 按次序提供<汉字>的不同词义,并为每个词义提供1~2个包含该汉字的词语\\\\n"
9 "5. 保证内容简洁明了,易于低龄学生理解\\\\n\\\\n"
10 "## 英文释义要求:\\\\n"
11 ... ...
12 "树。\\\\n"
13 "tree。\\\\n"
14 "{\\\\n"
15 " \\\\\\\"XCUSTOM\\\\\\\": {\\\\n"
16 " \\\\\\\"name_ch\\\\\\\": \\\\\\\"花\\\\\\\",\\\\n"
17 " \\\\\\\"name_en\\\\\\\": \\\\\\\"flower\\\\\\\",\\\\n"
18 " \\\\\\\"comments_ch\\\\\\\": {\\\\n"
19 " \\\\\\\"name\\\\\\\": \\\\\\\"花\\\\\\\",\\\\n"
20 " \\\\\\\"strokenumber\\\\\\\": 7,\\\\n"
21 " \\\\\\\"radical\\\\\\\": \\\\\\\"艹\\\\\\\",\\\\n"
22 " \\\\\\\"comment\\\\\\\": \\\\\\\"1. 种子植物的有性繁殖器官,由花瓣、花萼、花托、花蕊组成。词语:一朵花。\\\\\\\\\\\\n2. 可供观赏的植物。词语:花草。\\\\\\\",\\\\n"
23 " \\\\\\\"example_sentence\\\\\\\": \\\\\\\"学校里种了许许多多五颜六色的花。\\\\\\\"\\\\n"
24 " },\\\\n"
25 ... ...
26 "}\\\\n"
27 "}\\\\n\\\\n"
28 "## 请按以下格式应答:\\\\n"
29 "<汉字>:{识别出的中文名称}。\\\\n"
30 "<单词>:{识别出的英文名称}。\\\\n"
31 "{\\\\n"
32 " \\\\\\\"XCUSTOM\\\\\\\": {\\\\n"
33 " \\\\\\\"name_ch\\\\\\\": \\\\\\\"{中文名称}\\\\\\\",\\\\n"
34 " \\\\\\\"name_en\\\\\\\": \\\\\\\"{英文名称}\\\\\\\",\\\\n"
35 " \\\\\\\"comments_ch\\\\\\\": {\\\\n"
36 " \\\\\\\"name\\\\\\\": \\\\\\\"{中文名称}\\\\\\\",\\\\n"
37 " \\\\\\\"strokenumber\\\\\\\": {笔画数},\\\\n"
38 " \\\\\\\"radical\\\\\\\": \\\\\\\"{偏旁部首}\\\\\\\",\\\\n"
39 " \\\\\\\"comment\\\\\\\": \\\\\\\"{详细释义}\\\\\\\",\\\\n"
40 " \\\\\\\"example_sentence\\\\\\\": \\\\\\\"{示例句子}\\\\\\\"\\\\n"
41 " },\\\\n"
42 ... ...
43 "}\\\\n"
44 "}";
45
46static char visual_prompt_text[4096];
47// 2. 视觉prompt 更新消息格式定义,注意括号内为 \\\ 三斜扛转义;
48static const char g_updata_vision_prompt_cmd[] = "%s{\\\"model_type\\\":\\\"%s\\\",\\\"prompt\\\":\\\"%s\\\"}";
49
50void update_vision_prompt(char *prompt) {
51 int prompt_len = prompt ? strlen(prompt) : 0;
52 int total_len = prompt_len + strlen(g_updata_vision_prompt_cmd) + 64;
53 char *update_prompt_cmd = (char *)custom_malloc_psram(total_len);
54
55 if (update_prompt_cmd) {
56 snprintf(update_prompt_cmd, total_len, g_updata_vision_prompt_cmd,
57 AGENT_EVENT_UPDATE_SYSTEM_PROMPT, "2", prompt);
58 } else {
59 os_printf("update_vision_prompt malloc failed\r\n");
60 return;
61 }
62 strncpy(visual_prompt_text, update_prompt_cmd, sizeof(visual_prompt_text) -1);
63 custom_free_psram(update_prompt_cmd);
64}
65
66void agent_update_prompt(void * text) {
67 baidu_chat_agent_engine_send_event_to_agent(g_engine, (char *)text);
68}
69
70// 3. 更新视觉prompt, 2: 更新视觉prompt; 0: 为恢复系统默认视觉prompt; 注意:退出识字场景后需要恢复系统默认prompt;
71void update_prompt(const char *prompt_mode) {
72 int mode = os_atoi(prompt_mode);
73 os_memset(visual_prompt_text, 0, sizeof(visual_prompt_text) -1);
74 if (mode == 2) {
75 update_vision_prompt(object_vision_prompt);
76 } else if (mode == 0) {
77 // reset vision prompt
78 update_vision_prompt("");
79 }
80
81 os_printf("update prompt text:%s\r\n", visual_prompt_text);
82 os_task_create("brtc_task_update_prompt", agent_update_prompt, (void*)visual_prompt_text, OS_TASK_PRIORITY_NORMAL, 0, NULL, 16 * 1024);
83}
84
85// 4. 调用视觉prompt更新
86update_prompt(param);
87
88// 5. 发出图片识别query (“如:图片中是什么, 可以语音说出也可以使用接口发送text文本”)
拍照成画、物效
拍照成画、与图片物效可基于大模型实时互动方案的图生图模型进行实现,方案内置图生图模型, 配置、使用方式如下;
-
前置条件:控制台应用配置->组件选择勾选”文生图“;

- 初始化,与拍图识物一样,智能体创建及SDK params开启视频功能;
- 模式切换,图生图功能与视觉理解不能同时使用, 使用图生图时需要视觉切换到图片模式, 同时开启图生图功能, 使用视觉理解时则需要关闭国生图功能;参考设置如下(mode 2 可适用于图生图);
1static void set_agent_mode(void *agent_mode) {
2 char *mode_str = (char *)agent_mode;
3 ...
4 int mode = os_atoi(mode_str);
5 if (mode == 0) {
6 // 普通语音交互模式;视频理解+图片模式; 适用于 普通AI对话 及 拍图识物、拍图识字
7 g_enable_visual = true;
8 g_enable_image_generate = false;
9 // 关闭文、图生图
10 baidu_chat_agent_engine_send_event_to_agent(g_engine, AGENT_EVENT_DISABLE_MEDIA_GENERATE);
11 baidu_chat_agent_engine_update_visual_mode(g_engine, VISION_MODE_IMAGE);
12 } else if (mode == 1) {
13 // 视觉理解+视频流模式; 适用于拍图问答
14 g_enable_visual = true;
15 g_enable_image_generate = false;
16 // 关闭文、图生图
17 baidu_chat_agent_engine_send_event_to_agent(g_engine, AGENT_EVENT_DISABLE_MEDIA_GENERATE);
18 baidu_chat_agent_engine_update_visual_mode(g_engine, VISION_MODE_STREAM);
19 } else if (mode == 2) {
20 // 图片生成模式;适用于文生图、图生图
21 g_enable_visual = false;
22 g_enable_image_generate = true;
23 baidu_chat_agent_engine_update_visual_mode(g_engine, VISION_MODE_IMAGE);
24 // 关闭文、图生图
25 baidu_chat_agent_engine_send_event_to_agent(g_engine, AGENT_EVENT_ENABLE_MEDIA_GENERATE);
26 }
27}
- 与拍图识物一下, 调用 baidu_chat_agent_engine_send_video(g_engine, data, len)发送一张图片给大模型;(使用文生图功能则不需要发送图片)
- 发出文生图、图生图query,如, 文生图:“生成一张小猫吃鱼的图片”、“画一张小猫吃鱼的图片”; 图生图: “编辑图片为动漫风格”、“修改图片为动漫风格”; 注意,这里的query与所配置的Function Call 相关,与Function Call 不匹配的query可能影响意图识别;
- query发送若干秒后, 生图模型会返回生成的jpeg图片数据(默认分辨率为 640 * 480)及生图回调;jpeg图片可直接用于渲染上屏;涉及接口:
jpeg图片数据回调, 一次返回一帧jpeg数据:
1// 在初始化时设置回调
2BaiduChatAgentEvent events = {
3 ... ...
4 .onVisionImageRequest = onVisionImageRequest, // 设置视觉图片请求回调
5 .onVisionImageAck = onVisionImageAck, // 设置服务端接收图片回调
6 .onMediaGenerateResult = onMediaGenerateResult, // 设置图片生成结果回调
7 };
8
9void onVideoData(const uint8_t *data, size_t len, RtcImageType imgtype, int width, int height)
10{
11 BRTC_LOG("FrameReceived video data of length: %d, width: %d, height: %d\n", len, width, height);
12 if (imgtype == RTC_IMAGE_TYPE_JPEG) {
13#ifdef DUMP_VIDEO_FRAME
14 if (len > 0) {
15 char filename[64] = {0};
16 os_sprintf(filename, "0:video_frame_%04d.jpeg", (uint32_t)os_jiffies() % 9999);
17 dumpVideoFrame(filename, data, len);
18 }
19#endif
20 // render to display
21 jpeg_photo_renderer(data, len, 320, 240);
22 }
23}
生图结果回调:
1void onMediaGenerateResult(const char* result) {
2 if (result && strlen(result) > 0) {
3 BRTC_LOG("onMediaGenerateResult content: %s\n", result);
4 }
5}
方案二:WS 方式
本章不使用 SDK 封装接口, 端侧与 AI Agent Server 直接建立 WebSocket 长连接、自行收发协议帧, 功能能力与 SDK 方式完全一致。
调用协议速查
WS 方式下所有控制指令都是一条 WebSocket 文本帧,格式为 前缀 + JSON/开关。 拍学机用到的指令如下:
- 上行(端 → 服务端)
| 指令 | 载荷 | 用途 | |
|---|---|---|---|
[SET]:[UPDATE_SYSTEM_PROMPT]: |
{"model_type":"2 或 3","prompt":"..."} |
更新视觉/生图 prompt; prompt 传空串恢复默认 | |
[SET]:[UPDATE_VISION_MODE]: |
{"mode":"0 或 1","expire":"180"} |
0=图片模式 1=视频流模式; expire 为上传图片的有效期(秒) | |
[SET]:[MEDIA_GENERATE_MODE]: |
[TRUE] / [FALSE] |
开启/关闭文生图、图生图 | |
[SET]:[ENHANCE_QUERY]: |
{"enhance_type":"0~3","pre_query":"..","post_query":".."} |
0=取消 1=前置 2=后置 3=前后包裹 | |
[E]:[IMG]: |
Base64 分片 | 上行图片, 详见「图片上行分片协议」 |
- 下行(服务端 → 端)
| 事件 | 载荷 | 用途 |
|---|---|---|
[E]:[SYSTEM_PROMPT_HAS_UPDATED]: |
<prompt> |
prompt 更新成功的确认, 回显服务端实际生效的 prompt |
[E]:[UPLOAD_IMAGE] |
无 | 请求上传图片, 收到后再上行一张图片 |
[E]:[VISION]:[ACK]: |
<file_name> |
服务端已收到上行图片的确认, 收到后再发识别 query |
[E]:[TAG_MSG] |
{"XCUSTOM":{...}} |
标签消息(识字卡片等结构化结果) |
[E]:[MEDIA_GENERATE_START]: |
JSON | 生图任务开始 |
[E]:[MEDIA_GENERATE_RESULT]: |
{"msg":"success","task":123,"resource_base64":"data:image/jpeg;base64,/9j/...","error":0,"type":"image"} |
生图结果, Base64 JPEG |
时序要求:UPDATE_SYSTEM_PROMPT / UPDATE_VISION_MODE / MEDIA_GENERATE_MODE 都要求 WS 已连通且 license 校验通过(收到 [E]:[LIC]:[RES]:[PASS]:)后才能下发, 连接就绪前发送会被丢弃。
需要固定生图分辨率时在 config JSON 里传递 screen_width / screen_height 分辨率参数;
AI对话
当前的RTOS 大模型实时互动方案即可实现基本的AI 语音对话功能, 音频上行、ASR、TTS 走既有链路, 无需额外的视觉/生图指令。 。
拍图识物
可基于视觉理解+图片模式即可实现该功能。
直接在已建立的 WS 连接上收发文本帧, 时序如下:
1端 → 服务端 [SET]:[UPDATE_VISION_MODE]:{"mode":"0","expire":"180"} // 切图片模式, expire 为上传图片的有效期
2端 → 服务端 [E]:[IMG]:<Base64 分片>... // 上行一张图片, 分片协议见下
3服务端 → 端 [E]:[VISION]:[ACK]:<file_name> // 服务端已收到图片
4端 → 服务端 [T]:图片中是什么 // 图片识别 query(也可由语音 ASR 触发)
5服务端 → 端 [A]:... // 模型描述图片内容
图片识别 query 需在收到 [E]:[VISION]:[ACK]:<file_name> 之后再发出; 若服务端在收到识别 query 时还没拿到图片, 会下发 [E]:[UPLOAD_IMAGE], 端侧收到后补发一张图片即可。
图片视觉理解有两种触发顺序, 都能实现该功能:
- 先传图、后发 query(上面的时序): 切图片模式 → 上行图片 → 收到
[E]:[VISION]:[ACK]:<file_name>确认 → 再发识别 query; - 先发 query、后传图: 用户先用 query(如 「图片中是什么」)触发视觉(语音说出或
[T]:下发均可), 服务端发现还没有图片时, 会下发[E]:[UPLOAD_IMAGE]请求上传图片; 端侧收到该消息后再上传一张图片, 服务端收到图片后即继续完成识别, 同样可实现图片视觉理解。
expire 是上传图片的有效期(秒, 建议 180), 有效期内可以针对同一张图片连续多轮交互; 超时后图片失效。
图片上行分片协议
注:图片格式支持 JPG 及 PNG。
图片上传分两种情况: 视觉场景(拍图识物/问答/识字)与 图生图场景(拍照成画/物效), 通过首片 meta 中的 [FT] 字段区分。 首片 meta 为若干 key=value 键值对, 以 ; 分隔、\n 结束, 可用 tag 定义如下:
| meta tag | 含义 | 取值 |
|---|---|---|
[T] |
消息类型 | binary(二进制图片)/ json |
[N] |
文件名 | 如 photo.jpg |
[E] |
过期配置 | 图片有效期 |
[FT] |
文件类型(区分场景) | vision(视觉)/ image_generate(图生图) |
即视觉场景首片 meta 为 [T]=binary;[N]=photo.jpg;[FT]=vision;\n, 图生图场景为 [T]=binary;[N]=photo.jpg;[FT]=image_generate;\n。
图片被切成 8092 字节一片, 每片 Base64 编码后加 [E]:[IMG]: 前缀, 以 WS 文本帧逐片发出。 每片 Base64 前有 1 字节头字节, 首片额外带一段 meta:
| 分片 | 头字节 | meta |
|---|---|---|
| 首片 | 0x18(HeaderV+Start) |
[T]=binary;[N]=photo.jpg;[FT]=vision;\n(38 B) |
| 后续片 | 0x10(HeaderV) |
无 |
| 末帧 | 0x14(HeaderV+End) |
固定 [E]:[IMG]:FA== |
图生图场景首片 meta 用 [FT]=image_generate;(46 B), 其余不变。
参考代码(ws_send_text() / base64_encode() 为端侧自有实现):
1#define IMG_PREFIX "[E]:[IMG]:"
2#define IMG_PREFIX_LEN 10
3#define IMG_FRAME_RAW_MAX 8092 // 每片原始数据上限(含头字节与 meta)
4#define IMG_FRAME_TXT_MAX (((IMG_FRAME_RAW_MAX + 64) + 2) / 3 * 4)
5#define IMG_HDR_FIRST 0x18u // HeaderV=1, Start=1
6#define IMG_HDR_CONT 0x10u // HeaderV=1, 中间帧
7#define IMG_END_FRAME "[E]:[IMG]:FA==" // 末帧固定内容(头字节 0x14)
8
9#define FILE_TYPE_VISION "vision" // 视觉场景(拍图识物/问答/识字)
10#define FILE_TYPE_IMAGE_GENERATE "image_generate" // 图生图场景(拍照成画/物效)
11
12// file_type: 使用上述常量, 写入首片 meta 的 [FT] 字段
13int send_image(const uint8_t *data, size_t len, const char *file_type)
14{
15 // 1. 只接受 JPEG(FF D8) / PNG(89 50 4E 47)
16 if (!data || len < 4 || !file_type || !file_type[0]) return -1;
17 int is_jpeg = (data[0] == 0xFFu && data[1] == 0xD8u);
18 int is_png = (data[0] == 0x89u && data[1] == 0x50u
19 && data[2] == 0x4Eu && data[3] == 0x47u);
20 if (!is_jpeg && !is_png) return -1;
21
22 char meta[64];
23 int meta_len = snprintf(meta, sizeof(meta),
24 "[T]=binary;[N]=photo.jpg;[FT]=%s;\n", file_type);
25
26 uint8_t *raw = malloc(IMG_FRAME_RAW_MAX);
27 char *txt = malloc(IMG_PREFIX_LEN + IMG_FRAME_TXT_MAX + 4);
28 if (!raw || !txt) { free(raw); free(txt); return -1; }
29 memcpy(txt, IMG_PREFIX, IMG_PREFIX_LEN); // 前缀只需拼一次
30
31 int ret = 0, first = 1;
32 size_t offset = 0;
33 while (offset < len) {
34 // 2. 组装头字节 + meta(仅首片)
35 unsigned hdr_len;
36 if (first) {
37 raw[0] = IMG_HDR_FIRST;
38 memcpy(raw + 1, meta, meta_len);
39 hdr_len = 1u + (unsigned)meta_len; // 首片可载数据随 meta 长度变化(vision: 8053 B)
40 } else {
41 raw[0] = IMG_HDR_CONT;
42 hdr_len = 1u; // 后续片可载 8091 B
43 }
44
45 // 3. 填充本片数据
46 unsigned max_data = IMG_FRAME_RAW_MAX - hdr_len;
47 unsigned chunk = (len - offset > max_data) ? max_data : (unsigned)(len - offset);
48 memcpy(raw + hdr_len, data + offset, chunk);
49
50 // 4. Base64 编码后紧跟前缀, 作为一条 WS 文本帧发出
51 int b64_len = base64_encode(raw, hdr_len + chunk,
52 txt + IMG_PREFIX_LEN, IMG_FRAME_TXT_MAX + 1);
53 if (b64_len <= 0) { ret = -1; break; }
54 txt[IMG_PREFIX_LEN + b64_len] = '\0';
55
56 ret = ws_send_text(txt);
57 if (ret < 0) break; // 中途失败不发末帧
58
59 offset += chunk;
60 first = 0;
61 }
62
63 // 5. 末帧:图片结束信号, 服务端收到后才开始拼接识别
64 if (ret >= 0) ret = ws_send_text(IMG_END_FRAME);
65
66 free(raw);
67 free(txt);
68 return ret;
69}
70
71// 调用示例:
72// send_image(data, len, FILE_TYPE_VISION); // 视觉场景(拍图识物/问答/识字)
73// send_image(data, len, FILE_TYPE_IMAGE_GENERATE); // 图生图场景(拍照成画/物效)
拍图问答
基于视觉理解+视频流模式即可实现该功能。 其实现方法与上述拍图识物基本相同,主要不同在于:
- 对话状态下, 下发
[SET]:[UPDATE_VISION_MODE]:{"mode":"1","expire":"180"}切到视频流模式; - 周期性重复上一节的
[E]:[IMG]:分片流程发送图片, 建议 1000ms 采集、发送一张图片, 过高的图片发送频率可能造成视觉模型tokens的过度消耗; - 视频流模式下,视觉模型处理持续进行, 可随时通过
[T]:发出图片识别query(也可由语音 ASR 触发);
拍图识字
prompt 内容与输出约定见上文「视觉prompt与标签消息(XCUSTOM)」; 模型返回的标签消息由 [E]:[TAG_MSG] 下发。
直接下发一条文本帧即可, 载荷为 {"model_type":"..","prompt":".."}:
1[SET]:[UPDATE_SYSTEM_PROMPT]:{"model_type":"2","prompt":"<视觉 prompt>"} // 更新视觉 prompt
2[SET]:[UPDATE_SYSTEM_PROMPT]:{"model_type":"2","prompt":""} // 恢复系统默认视觉 prompt
model_type 是 prompt 的作用域: "2" 只影响视觉理解, "3" 只影响生图, 两者互不覆盖;
结果确认: prompt 设置成功后服务端会下发 [E]:[SYSTEM_PROMPT_HAS_UPDATED]:<prompt>, 载荷是实际生效的 prompt, 可与下发内容比对。 没收到这条消息说明设置未成功, 重点检查载荷本身: 转义层数是否正确(prompt 值是 JSON 字符串, 内部又嵌了 JSON 模板)、有没有真实换行(必须写成 \n 转义, 否则 WS 文本帧被截断)、以及引号/反斜杠等特殊符号是否漏转义。
参考实现(ws_send_text() 为端侧自有实现):
1#define CMD_UPDATE_SYSTEM_PROMPT "[SET]:[UPDATE_SYSTEM_PROMPT]:"
2#define MODEL_TYPE_VISION "2" // 视觉理解 prompt
3#define MODEL_TYPE_IMAGE_GEN "3" // 生图 prompt
4
5/* 视觉 prompt 用宏拼接, 注意转义层数:
6 * \\\\n → 服务端最终看到 \n(不能写真实换行, 否则 WS 文本帧被截断)
7 * \\\\\\\" → 服务端最终看到 \"(prompt 值本身是 JSON 字符串, 内部又嵌了 JSON 模板)
8 */
9#define VISION_PROMPT_IDENTIFY \
10 "# 你是一位资深早教老师。当用户上传一张物品照片后,请按以下格式分析图片内容。\\\\n" \
11 "## 强制规则: 禁止输出除 JSON 以外的任何字符; key 必须使用白名单中的固定名称。\\\\n" \
12 "## 输出格式(必须完全一致):\\\\n" \
13 "{中文名称}。{英文名称}。" \
14 "{" \
15 " \\\\\\\"XCUSTOM\\\\\\\": {" \
16 " \\\\\\\"identify\\\\\\\": {" \
17 " \\\\\\\"name_ch\\\\\\\": \\\\\\\"{中文名称}\\\\\\\"," \
18 " \\\\\\\"name_en\\\\\\\": \\\\\\\"{英文名称}\\\\\\\"" \
19 " }}}"
20
21/* model_type: "2"=视觉 "3"=生图; prompt 为 NULL/空表示恢复服务端默认 */
22static void update_system_prompt(const char *model_type, const char *prompt)
23{
24 char cmd[4096];
25 snprintf(cmd, sizeof(cmd),
26 CMD_UPDATE_SYSTEM_PROMPT "{\"model_type\":\"%s\",\"prompt\":\"%s\"}",
27 model_type, (prompt && prompt[0]) ? prompt : "");
28 ws_send_text(cmd); // 整包发送, 不做二次转义
29}
30
31// 进入拍图识字场景: 下发识字 prompt
32update_system_prompt(MODEL_TYPE_VISION, VISION_PROMPT_IDENTIFY);
33
34// 退出识字场景回到纯语音
35update_system_prompt(MODEL_TYPE_VISION, NULL);
拍照成画、物效
前置条件: 控制台应用配置->组件选择勾选 「文生图」。 生图不依赖 enable_visual, 模式切换、发图、取结果对应的协议如下。 建连参数里带 "screen_width" / "screen_height" 可固定生图分辨率, 不带则用服务端默认值(640 × 480)。
- 模式切换(图生图与视觉理解互斥, 生图时视觉需切到图片模式); 另外, 视觉与图生图两个场景上传图片时, 需在首片 meta 的
[FT]字段分别指定vision/image_generate(见「图片上行分片协议」)
1// 普通对话 / 拍图识物、识字: 关生图 + 视觉图片模式
2[SET]:[MEDIA_GENERATE_MODE]:[FALSE]
3[SET]:[UPDATE_VISION_MODE]:{"mode":"0","expire":"180"}
4
5// 拍图问答: 关生图 + 视觉视频流模式
6[SET]:[MEDIA_GENERATE_MODE]:[FALSE]
7[SET]:[UPDATE_VISION_MODE]:{"mode":"1","expire":"180"}
8
9// 文生图 / 图生图: 开生图 + 视觉图片模式
10[SET]:[UPDATE_VISION_MODE]:{"mode":"0","expire":"180"}
11[SET]:[MEDIA_GENERATE_MODE]:[TRUE]
生图链路要两步: 先 [SET]:[MEDIA_GENERATE_MODE]:[TRUE] 打开生图模式, 再用 [SET]:[UPDATE_SYSTEM_PROMPT]:{"model_type":"3","prompt":"..."} 下发生图风格 prompt。 风格 prompt 的写法建议 先定风格、再定禁止项、最后定构图; 禁止项(如 「无灰度、无阴影、无噪点」)比正向描述更有效, 因为生图模型默认倾向加细节。
- Query 增强(可选)
用户语音很少是完整绘图指令(只说 「一只猫」), 可用 ENHANCE_QUERY 把它包成完整 query:
1[SET]:[ENHANCE_QUERY]:{"enhance_type":"3","pre_query":"生成一张","post_query":"的图片"}
2// 用户说「一只猫」 → 实际 query 「生成一张一只猫的图片」
3
4[SET]:[ENHANCE_QUERY]:{"enhance_type":"0","pre_query":"","post_query":""}
5// 离开生图场景时取消, 否则用户每句话都会被包成绘图指令
增强语要短, 长引导语放prompt里。
- 图生图上行图片: 与拍图识物完全一致, 走
[E]:[IMG]:分片协议(文生图不需要发图)。 - 接收结果: 服务端先下发
[E]:[MEDIA_GENERATE_START]:(任务开始), 随后下发
1[E]:[MEDIA_GENERATE_RESULT]:{"msg":"success","task":123,"resource_base64":"data:image/jpeg;base64,/9j/...","error":0,"type":"image"}
端侧从 "resource_base64":" 取值、跳过 data:image/jpeg;base64, 前缀、扫到下一个 " 结束, 再做 Base64 解码即得 JPEG, 可直接渲染上屏。
测试验证
在完成上述接入流程后,拍学机相关功能即可全部实现, 可以跑如下测试case进行验证:
| 功能 | 测试case | 验证结果 |
|---|---|---|
| AI对话 | query: "请一个故事" | 播报一则故事 |
| 拍图识物 | 设置视觉图片模式;上传一张图片; query:"图片中是什么" | 详细描述模型所理解的图片内容 |
| 拍图问答 | 设置视觉视频流模式;持续上传图片; query:"图片中大家在干什么" 、"一共有几个人"、“有没有戴帽子的人” | 模型根据所上传的图片,与用户多轮交互,持续回答用户问题 |
| 拍图识字 | 设置视觉图片模式;更新prompt; 上传一张图片; query:"图片中是什么" | 根据用户prompt播报图片中物体的中英文名称,并接收到翻译义标签信息 |
| 拍照成画 | 设置视觉图片模式; 设置开启图片生成;上传一张图片; query:"编辑图片切换背景为星空" | 收到模型返回的星空背景的新图片 |
| 拍照物效 | 设置视觉图片模式; 设置开启图片生成;上传一张图片; query:"编辑图片为动漫风格" | 收到模型返回的动漫风格的新图片 |
至此,拍学机相关功能开发完成。
常见问题速查
| 现象 | 原因 | 处理 |
|---|---|---|
| 下发 prompt / 视觉模式无效果 | 连接未就绪就下发 | 等 license 校验通过、连接进入就绪态后再下发 |
下发 prompt 后没收到 [E]:[SYSTEM_PROMPT_HAS_UPDATED]: |
prompt 载荷格式不合法 | 检查转义层数、真实换行(须写 \n)、引号等特殊符号 |
| 标签消息缺少 XCUSTOM 外层 | 模型没按 prompt 包装 | 优化提示词,强调输出标签 |
相关产品
评价此篇文章
