一个集成了语音识别、文本交互和 AI 对话功能的应用程序实现,主要基于 LVGL 图形库构建 UI 界面,结合 Vosk 进行语音识别,通过 Ollama 调用 AI 模型生成回答。
这段代码是一个集成了语音识别、文本交互和 AI 对话功能的应用程序实现,主要基于 LVGL 图形库构建 UI 界面,结合 Vosk 进行语音识别,通过 Ollama 调用 AI 模型生成回答。该代码实现了一个功能完整的语音交互 AI 助手原型,适用于嵌入式设备或本地桌面环境。用户可以通过语音或文本与 AI 对话,整个流程在本地完成,响应迅速且保护数据隐私。输入内容会被发送到本地 Ollama 服务
·
代码注释:
/**
* @file simple_test.c
* 修复Vosk结果传递和Ollama调用问题的完整实现
*/
/*********************
* INCLUDES
*********************/
#include "simple_test.h" // 本模块头文件
#include <SDL2/SDL.h> // SDL基础库,用于音频设备控制
#include <SDL2/SDL_audio.h> // SDL音频子系统,处理录音功能
#include <pthread.h> // 线程库,实现多任务并行(录音/识别/网络)
#include <semaphore.h> // 信号量,实现线程间同步(主要用于UI更新)
#include <stdio.h> // 标准I/O,用于文件操作和调试输出
#include <stdlib.h> // 标准库,提供内存管理和系统函数
#include <string.h> // 字符串处理函数库
#include <unistd.h> // Unix系统调用,提供sleep等功能
#include <arpa/inet.h> // 网络编程库,处理TCP连接和IP转换
#include <ctype.h> // 字符处理库(预留)
#include <libwebsockets.h> // WebSocket库,用于与Vosk服务通信
#include <time.h> // 时间库,处理超时判断和时间戳
#include "lvgl.h" // LVGL图形库,构建UI界面
#include "../../src/lv_100ask_pinyin_ime/lv_100ask_pinyin_ime.h" // 拼音输入法组件
#if LV_100ASK_PINYIN_IME_SIMPLE_TEST != 0 // 条件编译开关:启用拼音输入法测试
/*********************
* DEFINES
*********************/
#define RECORD_FILE "recording.wav" // 录音文件保存路径
#define AUDIO_FREQ 16000 // 音频采样率(与Vosk要求一致)
#define AUDIO_FORMAT AUDIO_S16LSB // 音频格式(16位小端PCM)
#define AUDIO_CHANNELS 1 // 单声道(Vosk推荐)
#define AUDIO_SAMPLES 1024 // 音频缓冲区大小
// 网络配置参数(Ollama服务)
#define DEEPSEEK_PORT 11434 // Ollama默认端口
#define DEEPSEEK_SERVER_IP "127.0.0.1" // Ollama服务IP(本地)
#define BUFFER_SIZE 4096 * 10 // 网络接收缓冲区大小
#define REQ_BODY_MAX 1024 // 请求体最大长度
#define REQ_HEADER_MAX 256 // 请求头最大长度
// Vosk服务器配置
#define VOSK_WS_IP "127.0.0.1" // Vosk服务IP(本地)
#define VOSK_WS_PORT 2700 // Vosk默认端口
#define VOSK_WS_URL "ws://127.0.0.1:2700" // Vosk服务WebSocket地址
#define AUDIO_CHUNK_SIZE 4096 // 音频发送块大小
#define MAX_RECOGNITION_RESULT 2048 // 语音识别结果最大长度
#define VOSK_WAIT_TIMEOUT_SEC 8 // Vosk超时时间(秒)
#define VOSK_SERVICE_INTERVAL_MS 100 // WebSocket事件循环间隔(毫秒)
// 其他模块禁用宏
#define LV_100ASK_NES_SIMPLE_TEST 0
#define LV_100ASK_2048_SIMPLE_TEST 0
#define LV_100ASK_CALC_SIMPLE_TEST 0
#define LV_100ASK_FILE_EXPLORER_TEST 0
/**********************
* TYPEDEFS
**********************/
// WAV文件头结构(用于生成标准WAV格式文件)
typedef struct __attribute__((packed)) { // packed属性:取消内存对齐,确保结构紧凑
char riff_id[4]; // "RIFF"标识
Uint32 riff_size; // 文件大小-8
char wave_id[4]; // "WAVE"标识
char fmt_id[4]; // "fmt "标识
Uint32 fmt_size; // 格式块大小(16)
Uint16 audio_format; // 音频格式(1=PCM)
Uint16 channels; // 声道数
Uint32 freq; // 采样率
Uint32 byte_rate; // 比特率(采样率*声道数*位深/8)
Uint16 block_align; // 块对齐(声道数*位深/8)
Uint16 bits_per_sample;// 位深(16)
char data_id[4]; // "data"标识
Uint32 data_size; // 音频数据大小
} WavHeader;
// 线程参数结构体(用于向Ollama请求线程传递数据)
typedef struct {
char *input_text; // 要发送给Ollama的文本
} deepseek_thread_param_t;
// WebSocket上下文结构体(管理Vosk通信状态)
typedef struct {
struct lws *wsi; // WebSocket连接实例
FILE *audio_file; // 待发送的音频文件指针
char audio_buf[AUDIO_CHUNK_SIZE]; // 音频发送缓冲区
char recv_buf[BUFFER_SIZE]; // 接收数据缓冲区
int recv_len; // 接收数据长度
int is_connected; // 连接状态(1=已连接)
int is_audio_sent; // 音频是否发送完毕(1=完毕)
int is_finished; // 识别是否完成(1=完成)
time_t audio_sent_time; // 音频发送完成时间戳
char result[MAX_RECOGNITION_RESULT]; // 最终识别结果
} vosk_ws_ctx_t;
/**********************
* STATIC PROTOTYPES
**********************/
static void ta_event_cb(lv_event_t * e); // 文本框事件回调(处理焦点/提交)
static void ui_update_timer_cb(lv_timer_t *timer); // UI更新定时器回调
static char* filter_think_tags(const char *text); // 过滤Ollama响应中的标签
static char* remove_leading_e(const char *text); // 移除响应开头的无效"e"字符
static char* clean_reply_content(const char *reply); // 清理响应内容(转义字符处理)
static char* extract_deepseek_reply(const char *http_response); // 解析Ollama的HTTP响应
static void* deepseek_send_request_thread(void *param); // Ollama请求线程
static void* record_thread_func(void *arg); // 录音线程
static void* speech_recognition_thread(void *arg); // 语音识别线程
static void init_wav_header(WavHeader *header); // 初始化WAV文件头
static char* send_audio_to_speech_recognition(const char *filename); // 发送音频到Vosk
// WebSocket相关函数
static int vosk_ws_callback(struct lws *wsi, enum lws_callback_reasons reason,
void *userdata, void *in, size_t len); // Vosk WebSocket回调
static int vosk_ws_send_config(struct lws *wsi); // 向Vosk发送配置信息
static int vosk_ws_send_audio(struct lws *wsi, vosk_ws_ctx_t *ctx); // 向Vosk发送音频数据
static void vosk_parse_result(const char *data, vosk_ws_ctx_t *ctx); // 解析Vosk返回结果
/**********************
* STATIC VARIABLES
**********************/
// SDL录音相关
static SDL_AudioDeviceID audio_dev = 0; // 音频设备ID
static volatile int is_recording = 0; // 录音状态(1=正在录音)
static FILE *audio_file = NULL; // 录音文件指针(临时)
static int sdl_audio_inited = 0; // SDL音频初始化标记
// 线程安全与UI相关
sem_t ui_update_sem; // UI更新信号量(线程同步)
char ui_update_text[2048] = {0}; // UI更新文本缓冲区
lv_obj_t *global_reply_label = NULL; // 全局应答显示框指针
lv_obj_t *global_input_ta = NULL; // 全局输入框指针
/**********************
* GLOBAL FUNCTIONS
*********************/
LV_FONT_DECLARE(lv_font_source_han_sans_normal_16); // 声明中文字体
// 文本框事件回调(处理焦点和键盘显示)
static void ta_event_cb(lv_event_t * e) {
lv_event_code_t code = lv_event_get_code(e); // 获取事件类型
lv_obj_t * ta = lv_event_get_target(e); // 获取事件目标(文本框)
lv_obj_t * kb = lv_event_get_user_data(e); // 获取用户数据(键盘)
if(code == LV_EVENT_FOCUSED) { // 文本框获得焦点
if(lv_indev_get_type(lv_indev_get_act()) != LV_INDEV_TYPE_KEYPAD) {
lv_keyboard_set_textarea(kb, ta); // 绑定键盘到文本框
lv_obj_clear_flag(kb, LV_OBJ_FLAG_HIDDEN); // 显示键盘
}
}
else if(code == LV_EVENT_READY || code == LV_EVENT_CANCEL) { // 输入完成或取消
lv_obj_add_flag(kb, LV_OBJ_FLAG_HIDDEN); // 隐藏键盘
lv_obj_clear_state(ta, LV_STATE_FOCUSED); // 清除焦点状态
lv_indev_reset(NULL, ta); // 重置输入设备
}
}
// 过滤Ollama响应中的标签(转义前的Unicode表示)
static char* filter_think_tags(const char *text) {
if (text == NULL) return NULL; // 入参校验
char *filtered = malloc(strlen(text) + 1); // 分配结果内存
if (filtered == NULL) return NULL; // 内存分配失败
const char *src = text; // 源文本指针
char *dst = filtered; // 目标文本指针
int in_think_tag = 0; // 是否在标签内标记
while (*src) { // 遍历源文本
// 检测开始标签(Unicode转义:\u003cthink\u003e)
if (strncmp(src, "\\u003cthink\\u003e", 16) == 0) {
in_think_tag = 1; // 标记进入标签内
src += 16; // 跳过标签
continue;
}
// 检测结束标签(Unicode转义:\u003c/think\u003e)
if (strncmp(src, "\\u003c/think\\u003e", 17) == 0) {
in_think_tag = 0; // 标记退出标签
src += 17; // 跳过标签
continue;
}
if (!in_think_tag) { // 不在标签内则复制字符
*dst++ = *src;
}
src++;
}
*dst = '\0'; // 字符串结束符
return filtered;
}
// 移除响应开头的无效"e"字符(处理Ollama偶尔的异常输出)
static char* remove_leading_e(const char *text) {
if (text == NULL) return NULL; // 入参校验
const char *start = text; // 起始指针
// 跳过开头的空白字符
while (*start == ' ' || *start == '\n' || *start == '\t' || *start == '\r') {
start++;
}
// 如果第一个有效字符是'e',则跳过
if (*start == 'e') {
start++;
// 跳过'e'后的空白字符
while (*start == ' ' || *start == '\n' || *start == '\t' || *start == '\r') {
start++;
}
}
return strdup(start); // 复制处理后的字符串
}
// 清理回复内容(处理转义字符)
static char* clean_reply_content(const char *reply) {
if (reply == NULL) return NULL; // 入参校验
char *cleaned = strdup(reply); // 复制原始字符串
if (cleaned == NULL) return NULL;
char *ptr = cleaned; // 遍历指针
while (*ptr) {
// 处理换行符转义
if (strncmp(ptr, "\\n", 2) == 0) {
*ptr = '\n';
memmove(ptr + 1, ptr + 2, strlen(ptr + 2) + 1); // 移除转义符
}
// 处理制表符转义
else if (strncmp(ptr, "\\t", 2) == 0) {
*ptr = '\t';
memmove(ptr + 1, ptr + 2, strlen(ptr + 2) + 1);
}
// 处理双引号转义
else if (strncmp(ptr, "\\\"", 2) == 0) {
*ptr = '"';
memmove(ptr + 1, ptr + 2, strlen(ptr + 2) + 1);
}
// 处理反斜杠转义
else if (strncmp(ptr, "\\\\", 2) == 0) {
*ptr = '\\';
memmove(ptr + 1, ptr + 2, strlen(ptr + 2) + 1);
}
ptr++;
}
// 移除开头的空白字符
char *start = cleaned;
while (*start == ' ' || *start == '\n' || *start == '\t' || *start == '\r') {
start++;
}
if (start != cleaned) { // 如果有前置空白,移动字符串
memmove(cleaned, start, strlen(start) + 1);
}
return cleaned;
}
// 解析Ollama的HTTP响应,提取response字段
static char* extract_deepseek_reply(const char *http_response) {
if (http_response == NULL) return NULL; // 入参校验
// 定位JSON数据起始位置(跳过HTTP头)
const char *json_start = strchr(http_response, '{');
if (json_start == NULL) {
printf("【解析错误】未找到JSON起始位置\n");
return NULL;
}
// 定位JSON数据结束位置
const char *json_end = strrchr(json_start, '}');
if (json_end == NULL) {
printf("【解析错误】未找到JSON结束位置\n");
return NULL;
}
// 提取完整JSON字符串
int json_len = json_end - json_start + 1;
char *json_str = (char*)malloc(json_len + 1);
if (json_str == NULL) return NULL;
strncpy(json_str, json_start, json_len);
json_str[json_len] = '\0';
char *reply = NULL; // 存储提取的响应内容
// 尝试提取"response":"xxx"格式
const char *response_start = strstr(json_str, "\"response\":\"");
if (response_start) {
response_start += strlen("\"response\":\""); // 跳过键名
const char *response_end = strchr(response_start, '"'); // 找到值的结束引号
if (response_end) {
int reply_len = response_end - response_start;
reply = (char*)malloc(reply_len + 1);
if (reply) {
strncpy(reply, response_start, reply_len);
reply[reply_len] = '\0';
}
}
}
// 如果未找到,尝试带空格的格式"response": "xxx"
if (!reply) {
response_start = strstr(json_str, "\"response\": \"");
if (response_start) {
response_start += strlen("\"response\": \"");
const char *response_end = strchr(response_start, '"');
if (response_end) {
int reply_len = response_end - response_start;
reply = (char*)malloc(reply_len + 1);
if (reply) {
strncpy(reply, response_start, reply_len);
reply[reply_len] = '\0';
}
}
}
}
// 如果仍未找到,返回原始JSON(用于调试)
if (!reply) {
printf("【解析警告】未找到response字段,返回原始JSON\n");
reply = strdup(json_str);
}
free(json_str); // 释放JSON字符串内存
// 过滤think标签
if (reply != NULL) {
char *filtered_reply = filter_think_tags(reply);
if (filtered_reply != NULL) {
free(reply);
reply = filtered_reply;
}
}
// 移除开头的无效'e'字符
if (reply != NULL) {
char *cleaned_reply = remove_leading_e(reply);
if (cleaned_reply != NULL) {
free(reply);
reply = cleaned_reply;
}
}
return reply;
}
// Ollama请求线程(发送文本请求并处理响应)
static void* deepseek_send_request_thread(void *param) {
deepseek_thread_param_t *thread_param = (deepseek_thread_param_t*)param;
// 参数校验
if (thread_param == NULL || thread_param->input_text == NULL) {
printf("【线程错误】参数为空\n");
if (thread_param != NULL) {
free(thread_param->input_text);
free(thread_param);
}
return NULL;
}
printf("【Ollama请求】开始处理: %s\n", thread_param->input_text);
int sock_fd = 0; // socket文件描述符
struct sockaddr_in server_addr; // 服务器地址结构
char recv_buffer[BUFFER_SIZE] = {0}; // 接收缓冲区
// 创建TCP socket
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
strncpy(ui_update_text, "Socket创建失败", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem); // 通知UI更新
goto cleanup; // 跳转到清理流程
}
// 设置socket超时(30秒)
struct timeval tv = {.tv_sec = 30, .tv_usec = 0};
if (setsockopt(sock_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) < 0 ||
setsockopt(sock_fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)) < 0) {
strncpy(ui_update_text, "设置Socket超时失败", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
goto cleanup;
}
// 初始化服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET; // IPv4
server_addr.sin_port = htons(DEEPSEEK_PORT); // 端口转换为网络字节序
// IP地址转换为网络字节序
if (inet_pton(AF_INET, DEEPSEEK_SERVER_IP, &server_addr.sin_addr) <= 0) {
strncpy(ui_update_text, "无效的服务器IP", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
goto cleanup;
}
// 连接Ollama服务器
if (connect(sock_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
strncpy(ui_update_text, "连接Ollama失败(服务未启动?)", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
printf("【Ollama错误】连接失败,检查服务是否启动\n");
goto cleanup;
}
// 转义输入文本中的特殊字符(JSON兼容)
char escaped_input[REQ_BODY_MAX] = {0};
const char *src = thread_param->input_text;
char *dst = escaped_input;
while (*src && (dst - escaped_input) < REQ_BODY_MAX - 10) {
if (*src == '"') { // 转义双引号
*dst++ = '\\';
*dst++ = '"';
} else if (*src == '\\') { // 转义反斜杠
*dst++ = '\\';
*dst++ = '\\';
} else if (*src == '\n') { // 转义换行符
*dst++ = '\\';
*dst++ = 'n';
} else { // 普通字符直接复制
*dst++ = *src;
}
src++;
}
*dst = '\0';
// 构建请求体(JSON格式)
char request_body[REQ_BODY_MAX] = {0};
snprintf(request_body, REQ_BODY_MAX,
"{\"model\": \"deepseek-r1:1.5b\", \"prompt\": \"%s\", \"stream\": false}",
escaped_input);
// 构建HTTP请求头
char request_header[REQ_HEADER_MAX] = {0};
snprintf(request_header, REQ_HEADER_MAX,
"POST /api/generate HTTP/1.1\r\n"
"Host: %s:%d\r\n"
"Content-Type: application/json\r\n"
"Content-Length: %zu\r\n"
"Connection: close\r\n"
"\r\n",
DEEPSEEK_SERVER_IP, DEEPSEEK_PORT, strlen(request_body));
printf("【Ollama请求】发送内容: %s\n", request_body);
// 发送HTTP请求(头 + 体)
if (send(sock_fd, request_header, strlen(request_header), 0) < 0 ||
send(sock_fd, request_body, strlen(request_body), 0) < 0) {
strncpy(ui_update_text, "发送请求失败", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
printf("【Ollama错误】发送请求失败\n");
goto cleanup;
}
// 接收响应
int total_received = 0;
int recv_len;
while ((recv_len = read(sock_fd, recv_buffer + total_received, BUFFER_SIZE - total_received - 1)) > 0) {
total_received += recv_len;
if (total_received >= BUFFER_SIZE - 1) break; // 防止缓冲区溢出
}
if (total_received > 0) { // 成功接收数据
recv_buffer[total_received] = '\0';
printf("【Ollama响应】原始数据: %s\n", recv_buffer);
// 解析响应内容
char *reply = extract_deepseek_reply(recv_buffer);
if (reply != NULL) {
char *cleaned_reply = clean_reply_content(reply); // 清理转义字符
if (cleaned_reply != NULL) {
free(reply);
reply = cleaned_reply;
}
// 更新UI文本
strncpy(ui_update_text, reply, sizeof(ui_update_text)-1);
free(reply);
printf("【Ollama响应】解析结果: %s\n", ui_update_text);
} else {
strncpy(ui_update_text, "无法解析Ollama响应", sizeof(ui_update_text)-1);
printf("【Ollama错误】无法解析响应\n");
}
sem_post(&ui_update_sem); // 通知UI更新
} else { // 接收失败
strncpy(ui_update_text, "接收响应失败(无数据或超时)", sizeof(ui_update_text)-1);
printf("【Ollama错误】接收响应失败\n");
sem_post(&ui_update_sem);
}
cleanup: // 资源清理
if (sock_fd > 0) close(sock_fd); // 关闭socket
free(thread_param->input_text); // 释放输入文本
free(thread_param); // 释放线程参数
return NULL;
}
// 初始化WAV文件头(设置音频参数)
static void init_wav_header(WavHeader *header) {
memcpy(header->riff_id, "RIFF", 4); // RIFF标识
memcpy(header->wave_id, "WAVE", 4); // WAVE标识
memcpy(header->fmt_id, "fmt ", 4); // fmt块标识
memcpy(header->data_id, "data", 4); // data块标识
header->fmt_size = 16; // 格式块大小(PCM固定为16)
header->audio_format = 1; // 音频格式(1=PCM)
header->channels = AUDIO_CHANNELS; // 声道数
header->freq = AUDIO_FREQ; // 采样率
header->bits_per_sample = 16; // 位深
header->block_align = (header->bits_per_sample / 8) * header->channels; // 块对齐
header->byte_rate = header->freq * header->block_align; // 比特率
header->data_size = 0; // 初始数据大小(后续会更新)
header->riff_size = sizeof(WavHeader) - 8 + header->data_size; // 文件总大小
}
// 语音识别线程(将录音文件发送到Vosk并处理结果)
static void* speech_recognition_thread(void *arg) {
char *filename = (char*)arg; // 录音文件名
if (!filename) {
printf("【线程错误】文件名为空\n");
return NULL;
}
// 更新UI状态
strncpy(ui_update_text, "正在进行语音识别...", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
printf("【识别流程】开始处理音频文件: %s\n", filename);
// 调用Vosk识别函数
char *recognized_text = send_audio_to_speech_recognition(filename);
// 检查识别结果有效性
if (recognized_text == NULL) {
printf("【识别错误】Vosk返回空结果\n");
strncpy(ui_update_text, "语音识别失败:未获取到有效内容", sizeof(ui_update_text)-1);
free(filename);
sem_post(&ui_update_sem);
return NULL;
}
if (strlen(recognized_text) == 0) {
printf("【识别警告】Vosk返回空字符串\n");
strncpy(ui_update_text, "语音识别完成,但结果为空", sizeof(ui_update_text)-1);
free(recognized_text);
free(filename);
sem_post(&ui_update_sem);
return NULL;
}
// 识别成功,处理结果
printf("【识别成功】结果: %s\n", recognized_text);
// 将结果填入输入框
if (global_input_ta != NULL) {
lv_textarea_set_text(global_input_ta, recognized_text);
} else {
printf("【UI警告】全局输入框指针为空\n");
}
// 自动发送到Ollama
deepseek_thread_param_t *thread_param = (deepseek_thread_param_t*)malloc(sizeof(deepseek_thread_param_t));
if (thread_param == NULL) {
strncpy(ui_update_text, "内存分配失败,无法发送请求", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
free(recognized_text);
free(filename);
return NULL;
}
thread_param->input_text = strdup(recognized_text); // 复制识别结果
if (thread_param->input_text == NULL) {
free(thread_param);
strncpy(ui_update_text, "文本复制失败,无法发送请求", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
free(recognized_text);
free(filename);
return NULL;
}
// 创建Ollama请求线程
pthread_t request_thread;
if (pthread_create(&request_thread, NULL, deepseek_send_request_thread, thread_param) == 0) {
pthread_detach(request_thread); // 分离线程(自动回收资源)
snprintf(ui_update_text, sizeof(ui_update_text),
"语音识别结果: %s\n正在向Ollama提问...", recognized_text);
printf("【流程正常】已创建Ollama请求线程\n");
} else {
strncpy(ui_update_text, "线程创建失败,无法发送请求", sizeof(ui_update_text)-1);
printf("【线程错误】无法创建Ollama请求线程\n");
free(thread_param->input_text);
free(thread_param);
}
// 清理资源
free(recognized_text);
free(filename);
sem_post(&ui_update_sem); // 通知UI更新
return NULL;
}
// Vosk WebSocket回调函数(处理连接/接收/关闭等事件)
static int vosk_ws_callback(struct lws *wsi, enum lws_callback_reasons reason,
void *userdata, void *in, size_t len) {
vosk_ws_ctx_t *ctx = (vosk_ws_ctx_t*)userdata; // 获取上下文
switch (reason) {
case LWS_CALLBACK_CLIENT_CONNECTION_ERROR: // 连接错误
printf("【Vosk连接错误】%s\n", in ? (char*)in : "未知错误");
ctx->is_connected = 0;
ctx->is_finished = 1;
break;
case LWS_CALLBACK_CLIENT_ESTABLISHED: // 连接建立
printf("【Vosk连接】已成功连接到服务\n");
ctx->is_connected = 1;
vosk_ws_send_config(wsi); // 发送配置信息
break;
case LWS_CALLBACK_CLIENT_RECEIVE: // 接收数据
if (len > 0 && len < BUFFER_SIZE - ctx->recv_len) {
// 复制接收数据到缓冲区
memcpy(ctx->recv_buf + ctx->recv_len, in, len);
ctx->recv_len += len;
ctx->recv_buf[ctx->recv_len] = '\0';
// 解析识别结果
vosk_parse_result(ctx->recv_buf, ctx);
// 检测是否收到完整结果
if (strstr(ctx->recv_buf, "\"text\"") != NULL) {
printf("【Vosk接收】已收到完整识别结果\n");
ctx->is_finished = 1;
}
// 清空缓冲区
ctx->recv_len = 0;
memset(ctx->recv_buf, 0, BUFFER_SIZE);
}
break;
case LWS_CALLBACK_CLIENT_CLOSED: // 连接关闭
printf("【Vosk连接】连接已关闭\n");
ctx->is_connected = 0;
// 未收到结果时标记完成
if (strlen(ctx->result) == 0) {
ctx->is_finished = 1;
}
break;
default:
break;
}
return 0;
}
// 向Vosk发送配置信息(采样率等参数)
static int vosk_ws_send_config(struct lws *wsi) {
const char *config = "{\"config\": {\"sample_rate\": 16000}}"; // 配置JSON
char *buf = malloc(LWS_PRE + strlen(config)); // 分配带WebSocket头的缓冲区
if (!buf) return -1;
memcpy(&buf[LWS_PRE], config, strlen(config)); // 复制数据到缓冲区(跳过头部)
// 发送文本数据
int ret = lws_write(wsi, (unsigned char*)&buf[LWS_PRE], strlen(config), LWS_WRITE_TEXT);
free(buf); // 释放缓冲区
if (ret < 0) {
printf("【Vosk发送错误】配置信息发送失败\n");
return -1;
}
printf("【Vosk发送】配置信息: %s\n", config);
return 0;
}
// 向Vosk发送音频数据(二进制)
static int vosk_ws_send_audio(struct lws *wsi, vosk_ws_ctx_t *ctx) {
if (!ctx->audio_file || ctx->is_audio_sent) return 0; // 校验条件
// 跳过WAV文件头(只发送音频数据)
static int header_skipped = 0;
if (!header_skipped) {
WavHeader header;
fread(&header, sizeof(WavHeader), 1, ctx->audio_file); // 读取并丢弃文件头
header_skipped = 1;
printf("【Vosk发送】已跳过WAV文件头,开始发送音频数据\n");
}
// 读取音频数据
size_t read_len = fread(ctx->audio_buf, 1, AUDIO_CHUNK_SIZE, ctx->audio_file);
if (read_len <= 0) {
// 音频发送完成
ctx->is_audio_sent = 1;
ctx->audio_sent_time = time(NULL); // 记录完成时间
printf("【Vosk发送】音频数据发送完成\n");
return 0;
}
// 分配带WebSocket头的缓冲区
char *buf = malloc(LWS_PRE + read_len);
if (!buf) return -1;
memcpy(&buf[LWS_PRE], ctx->audio_buf, read_len); // 复制音频数据
// 发送二进制数据
int ret = lws_write(wsi, (unsigned char*)&buf[LWS_PRE], read_len, LWS_WRITE_BINARY);
free(buf); // 释放缓冲区
if (ret < 0) {
printf("【Vosk发送错误】音频数据发送失败\n");
return -1;
}
return read_len;
}
// 解析Vosk返回的JSON结果(提取识别文本)
static void vosk_parse_result(const char *data, vosk_ws_ctx_t *ctx) {
if (!data || !ctx) return; // 入参校验
printf("【Vosk接收】原始数据: %s\n", data);
// 查找"text"字段(兼容带空格的格式)
const char *text_key = strstr(data, "\"text\"");
if (text_key) {
// 跳过"text"和可能的空格与冒号
text_key += strlen("\"text\"");
while (*text_key == ' ' || *text_key == ':') text_key++;
// 提取文本值(跳过引号)
if (*text_key == '"') {
text_key++;
const char *text_end = strchr(text_key, '"'); // 找到结束引号
if (text_end && text_end > text_key) {
int len = text_end - text_key;
if (len < MAX_RECOGNITION_RESULT - 1) {
strncpy(ctx->result, text_key, len);
ctx->result[len] = '\0';
printf("【Vosk解析】成功提取结果: %s\n", ctx->result);
return; // 成功提取后返回
}
}
}
}
// 提取中间结果(partial)用于调试
const char *partial_start = strstr(data, "\"partial\":");
if (partial_start) {
partial_start += strlen("\"partial\":");
while (*partial_start == ' ' || *partial_start == ':') partial_start++;
if (*partial_start == '"') {
partial_start++;
const char *partial_end = strchr(partial_start, '"');
if (partial_end && partial_end > partial_start) {
char partial_result[1024] = {0};
strncpy(partial_result, partial_start, partial_end - partial_start);
printf("【Vosk解析】中间识别结果: %s\n", partial_result);
}
}
}
// 未提取到有效结果
ctx->result[0] = '\0';
printf("【Vosk解析】未能提取有效结果\n");
}
// 核心函数:发送音频文件到Vosk服务并返回识别结果
static char* send_audio_to_speech_recognition(const char *filename) {
if (!filename) {
printf("【参数错误】音频文件名不能为空\n");
return NULL;
}
// 打开音频文件
FILE *audio_file = fopen(filename, "rb");
if (!audio_file) {
printf("【文件错误】无法打开音频文件: %s\n", filename);
return NULL;
}
// 初始化Vosk WebSocket上下文
vosk_ws_ctx_t ctx = {
.audio_file = audio_file,
.is_connected = 0,
.is_audio_sent = 0,
.is_finished = 0,
.recv_len = 0,
.result = {0}
};
memset(ctx.recv_buf, 0, BUFFER_SIZE);
memset(ctx.audio_buf, 0, AUDIO_CHUNK_SIZE);
// WebSocket客户端配置
struct lws_context_creation_info info;
memset(&info, 0, sizeof(info));
info.port = CONTEXT_PORT_NO_LISTEN; // 不监听端口(客户端模式)
info.protocols = (struct lws_protocols[]){ // 协议配置
{
"vosk-protocol",
vosk_ws_callback,
sizeof(vosk_ws_ctx_t),
BUFFER_SIZE,
},
{NULL, NULL, 0, 0} // 协议列表结束标记
};
// 创建WebSocket上下文
struct lws_context *context = lws_create_context(&info);
if (!context) {
printf("【WebSocket错误】创建上下文失败\n");
fclose(audio_file);
return NULL;
}
// 配置Vosk服务连接信息
struct lws_client_connect_info ccinfo;
memset(&ccinfo, 0, sizeof(ccinfo));
ccinfo.context = context;
ccinfo.address = VOSK_WS_IP;
ccinfo.port = VOSK_WS_PORT;
ccinfo.path = "/";
ccinfo.host = ccinfo.address;
ccinfo.origin = ccinfo.address;
ccinfo.protocol = "vosk-protocol";
ccinfo.userdata = &ctx; // 传递上下文
// 建立连接
ctx.wsi = lws_client_connect_via_info(&ccinfo);
if (!ctx.wsi) {
printf("【Vosk连接错误】无法连接到服务\n");
lws_context_destroy(context);
fclose(audio_file);
return NULL;
}
// WebSocket事件循环(处理通信)
while (!ctx.is_finished) {
// 处理WebSocket事件(超时时间:VOSK_SERVICE_INTERVAL_MS)
lws_service(context, VOSK_SERVICE_INTERVAL_MS);
// 发送音频数据(仅在连接成功且未发送完毕时)
if (ctx.is_connected && !ctx.is_audio_sent) {
int send_result = vosk_ws_send_audio(ctx.wsi, &ctx);
if (send_result < 0) {
printf("【Vosk发送错误】音频发送出错,终止连接\n");
break;
}
}
// 音频发送完成后等待最终结果(带超时)
if (ctx.is_audio_sent) {
time_t current_time = time(NULL);
// 超时判断
if (difftime(current_time, ctx.audio_sent_time) > VOSK_WAIT_TIMEOUT_SEC) {
printf("【Vosk超时】等待识别结果超过%d秒\n", VOSK_WAIT_TIMEOUT_SEC);
break;
}
// 已收到完整结果则退出
if (strlen(ctx.result) > 0) {
printf("【Vosk流程】已获取完整识别结果,退出等待\n");
break;
}
}
}
// 关闭连接
if (ctx.wsi && ctx.is_connected) {
lws_close_reason(ctx.wsi, LWS_CLOSE_STATUS_NORMAL, (unsigned char*)"完成", 2);
usleep(100000); // 等待关闭完成(100ms)
}
// 清理资源
lws_context_destroy(context);
fclose(audio_file);
// 调试输出最终结果
printf("【Vosk最终结果】ctx.result = \"%s\"\n", ctx.result);
printf("【Vosk最终结果长度】strlen(ctx.result) = %zu\n", strlen(ctx.result));
// 验证结果有效性(非空且非空字符串)
if (strlen(ctx.result) > 0 && strcmp(ctx.result, "\"\"") != 0) {
printf("【Vosk返回】有效结果: %s\n", ctx.result);
return strdup(ctx.result); // 返回复制的结果
} else {
printf("【Vosk错误】无效结果: %s\n", ctx.result);
return NULL;
}
}
// 录音线程(处理音频录制和文件保存)
static void *record_thread_func(void *arg) {
if (!is_recording || audio_dev == 0) return NULL; // 校验录音状态
// 计算音频缓冲区大小(16位单声道)
const int buffer_size = AUDIO_SAMPLES * AUDIO_CHANNELS * 2;
Uint8 *audio_buffer = (Uint8 *)malloc(buffer_size); // 分配缓冲区
if (audio_buffer == NULL) {
strncpy(ui_update_text, "录音缓冲区分配失败", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
return NULL;
}
// 打开录音文件
char *filename = strdup(RECORD_FILE);
FILE *f_audio = fopen(filename, "wb");
if (f_audio == NULL) {
strncpy(ui_update_text, "无法创建录音文件", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
free(audio_buffer);
free(filename);
return NULL;
}
// 写入WAV文件头
WavHeader wav_header;
init_wav_header(&wav_header);
fwrite(&wav_header, sizeof(WavHeader), 1, f_audio);
Uint32 total_data_size = 0; // 记录总音频数据大小
printf("【录音开始】缓冲区大小: %d bytes\n", buffer_size);
// 录音循环
while (is_recording) {
// 从音频设备读取数据
int read_len = SDL_DequeueAudio(audio_dev, audio_buffer, buffer_size);
if (read_len > 0) {
fwrite(audio_buffer, 1, read_len, f_audio); // 写入文件
total_data_size += read_len;
} else if (read_len < 0) { // 读取错误
strncpy(ui_update_text, "录音读取失败", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
printf("【录音错误】SDL读取失败,错误码: %d\n", read_len);
break;
}
SDL_Delay(10); // 降低CPU占用
}
// 更新WAV文件头(填充实际数据大小)
if (f_audio != NULL) {
wav_header.data_size = total_data_size;
wav_header.riff_size = sizeof(WavHeader) - 8 + total_data_size;
fseek(f_audio, 0, SEEK_SET); // 回到文件开头
fwrite(&wav_header, sizeof(WavHeader), 1, f_audio); // 重写文件头
fclose(f_audio);
f_audio = NULL;
}
// 清理资源
free(audio_buffer);
SDL_CloseAudioDevice(audio_dev);
audio_dev = 0;
printf("【录音结束】总数据量: %d bytes\n", total_data_size);
// 录音完成后启动语音识别
if (total_data_size > 0) {
pthread_t recognition_thread;
if (pthread_create(&recognition_thread, NULL, speech_recognition_thread, filename) == 0) {
pthread_detach(recognition_thread); // 分离线程
strncpy(ui_update_text, "录音完成,开始语音识别...", sizeof(ui_update_text)-1);
} else {
strncpy(ui_update_text, "录音完成但识别线程创建失败", sizeof(ui_update_text)-1);
free(filename);
}
} else {
strncpy(ui_update_text, "录音失败,无有效数据", sizeof(ui_update_text)-1);
free(filename);
}
sem_post(&ui_update_sem); // 通知UI更新
return NULL;
}
// 录音按钮回调(开始/停止录音)
void record_btn_event_cb(lv_event_t *e) {
// 初始化SDL音频子系统(首次调用时)
if (!sdl_audio_inited) {
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
strncpy(ui_update_text, "SDL音频初始化失败: ", sizeof(ui_update_text)-1);
strncat(ui_update_text, SDL_GetError(), sizeof(ui_update_text)-1 - strlen(ui_update_text));
sem_post(&ui_update_sem);
return;
}
sdl_audio_inited = 1;
printf("【SDL初始化】音频子系统初始化成功\n");
}
if (!is_recording) { // 开始录音
// 配置音频参数
SDL_AudioSpec desired_spec = {
.freq = AUDIO_FREQ,
.format = AUDIO_FORMAT,
.channels = AUDIO_CHANNELS,
.samples = AUDIO_SAMPLES,
.callback = NULL, // 无回调(使用队列模式)
.userdata = NULL
};
// 打开录音设备(1=输入设备)
audio_dev = SDL_OpenAudioDevice(NULL, 1, &desired_spec, NULL, SDL_AUDIO_ALLOW_ANY_CHANGE);
if (audio_dev == 0) {
strncpy(ui_update_text, "无法打开录音设备: ", sizeof(ui_update_text)-1);
strncat(ui_update_text, SDL_GetError(), sizeof(ui_update_text)-1 - strlen(ui_update_text));
sem_post(&ui_update_sem);
return;
}
// 启动录音
is_recording = 1;
SDL_PauseAudioDevice(audio_dev, 0); // 0=开始录音
// 创建录音线程
pthread_t record_thread;
if (pthread_create(&record_thread, NULL, record_thread_func, NULL) != 0) {
strncpy(ui_update_text, "录音线程创建失败", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
is_recording = 0;
SDL_CloseAudioDevice(audio_dev);
audio_dev = 0;
return;
}
pthread_detach(record_thread); // 分离线程
// 更新UI状态
strncpy(ui_update_text, "录音已启动,正在录制...", sizeof(ui_update_text)-1);
printf("【录音控制】录音已启动\n");
} else { // 停止录音
is_recording = 0;
strncpy(ui_update_text, "停止录音", sizeof(ui_update_text)-1);
printf("【录音控制】录音已停止\n");
}
sem_post(&ui_update_sem); // 通知UI更新
}
// 发送按钮回调(将输入框内容发送到Ollama)
void send_btn_event_cb(lv_event_t *e) {
lv_obj_t *ta2 = (lv_obj_t *)lv_event_get_user_data(e); // 获取输入框
if (ta2 == NULL) return;
// 获取输入文本
const char *input_text = lv_textarea_get_text(ta2);
if (input_text == NULL || strlen(input_text) == 0) {
strncpy(ui_update_text, "请先在输入框中输入内容!", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
return;
}
// 创建线程参数
deepseek_thread_param_t *thread_param = (deepseek_thread_param_t*)malloc(sizeof(deepseek_thread_param_t));
if (thread_param == NULL) {
strncpy(ui_update_text, "内存分配失败,无法发送", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
return;
}
// 复制输入文本
thread_param->input_text = strdup(input_text);
if (thread_param->input_text == NULL) {
free(thread_param);
strncpy(ui_update_text, "输入文本复制失败", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
return;
}
// 创建Ollama请求线程
pthread_t request_thread;
if (pthread_create(&request_thread, NULL, deepseek_send_request_thread, thread_param) != 0) {
strncpy(ui_update_text, "线程创建失败,无法发送", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
free(thread_param->input_text);
free(thread_param);
} else {
strncpy(ui_update_text, "正在请求Ollama,请稍候...", sizeof(ui_update_text)-1);
sem_post(&ui_update_sem);
pthread_detach(request_thread); // 分离线程
}
}
// UI更新定时器回调(定期检查并更新UI)
static void ui_update_timer_cb(lv_timer_t *timer) {
static int sem_value = 0;
sem_getvalue(&ui_update_sem, &sem_value); // 获取信号量值
if (sem_value > 0) { // 有更新请求
sem_wait(&ui_update_sem); // 消费信号量
// 更新应答显示框
if (global_reply_label != NULL && strlen(ui_update_text) > 0) {
lv_textarea_set_text(global_reply_label, ui_update_text);
lv_obj_scroll_to_y(global_reply_label, 0, LV_ANIM_OFF); // 滚动到顶部
}
}
}
// 界面创建函数(初始化UI组件)
void lv_100ask_pinyin_ime_simple_test(void) {
sem_init(&ui_update_sem, 0, 0); // 初始化UI更新信号量
lv_obj_t *scr = lv_scr_act(); // 获取当前屏幕
lv_coord_t scr_width = lv_obj_get_width(scr);
lv_coord_t scr_height = lv_obj_get_height(scr);
// 创建拼音输入法
lv_obj_t *pinyin_ime = lv_100ask_pinyin_ime_create(scr);
lv_obj_set_style_text_font(pinyin_ime, &lv_font_source_han_sans_normal_16, 0); // 设置中文字体
// 获取输入法键盘
lv_obj_t *kb = lv_100ask_pinyin_ime_get_kb(pinyin_ime);
lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, 0, 0); // 底部对齐
// 创建应答显示框
lv_obj_t *ta1 = lv_textarea_create(scr);
lv_obj_set_style_text_font(ta1, &lv_font_source_han_sans_normal_16, 0); // 设置字体
lv_obj_set_size(ta1, scr_width - 20, 150); // 设置大小
lv_obj_align(ta1, LV_ALIGN_TOP_LEFT, 10, 10); // 顶部左对齐
lv_textarea_set_text(ta1, "Ollama应答内容将显示在这里..."); // 初始文本
lv_textarea_set_placeholder_text(ta1, "等待应答..."); // 占位文本
lv_textarea_set_max_length(ta1, 2047); // 最大长度
lv_textarea_set_one_line(ta1, false); // 允许多行
lv_textarea_set_password_mode(ta1, false); // 非密码模式
lv_textarea_set_accepted_chars(ta1, NULL); // 接受所有字符
lv_textarea_set_text_selection(ta1, false); // 禁止选中文本
// 设置样式
lv_obj_set_style_bg_color(ta1, lv_color_hex(0xFFFFFF), 0);
lv_obj_set_style_border_width(ta1, 1, 0);
lv_obj_set_style_pad_all(ta1, 5, 0);
lv_obj_set_style_text_align(ta1, LV_TEXT_ALIGN_LEFT, 0);
global_reply_label = ta1; // 保存全局指针
// 创建提问输入框
lv_obj_t *ta2 = lv_textarea_create(scr);
lv_obj_set_style_text_font(ta2, &lv_font_source_han_sans_normal_16, 0); // 设置字体
lv_obj_set_size(ta2, scr_width - 220, 50); // 设置大小
lv_obj_align_to(ta2, ta1, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 10); // 位于应答框下方
lv_textarea_set_one_line(ta2, true); // 单行模式
lv_keyboard_set_textarea(kb, ta2); // 绑定键盘
global_input_ta = ta2; // 保存全局指针
// 绑定文本框事件
lv_obj_add_event_cb(ta1, ta_event_cb, LV_EVENT_ALL, kb);
lv_obj_add_event_cb(ta2, ta_event_cb, LV_EVENT_ALL, kb);
// 创建录音按钮
lv_obj_t *record_btn = lv_btn_create(scr);
lv_obj_set_size(record_btn, 90, 50); // 设置大小
lv_obj_align_to(record_btn, ta2, LV_ALIGN_OUT_RIGHT_MID, 10, 0); // 位于输入框右侧
lv_obj_t *record_btn_label = lv_label_create(record_btn);
lv_label_set_text(record_btn_label, "语音输入"); // 按钮文本
lv_obj_set_style_text_font(record_btn_label, &lv_font_source_han_sans_normal_16, 0);
lv_obj_center(record_btn_label); // 文本居中
lv_obj_add_event_cb(record_btn, record_btn_event_cb, LV_EVENT_CLICKED, NULL); // 绑定点击事件
// 创建发送按钮
lv_obj_t *send_btn = lv_btn_create(scr);
lv_obj_set_size(send_btn, 90, 50); // 设置大小
lv_obj_align_to(send_btn, record_btn, LV_ALIGN_OUT_RIGHT_MID, 10, 0); // 位于录音按钮右侧
lv_obj_t *send_btn_label = lv_label_create(send_btn);
lv_label_set_text(send_btn_label, "发送"); // 按钮文本
lv_obj_set_style_text_font(send_btn_label, &lv_font_source_han_sans_normal_16, 0);
lv_obj_center(send_btn_label); // 文本居中
lv_obj_add_event_cb(send_btn, send_btn_event_cb, LV_EVENT_CLICKED, ta2); // 绑定点击事件
// 创建UI更新定时器(100ms间隔)
lv_timer_create(ui_update_timer_cb, 100, NULL);
printf("【UI初始化】界面创建完成\n");
}
/**********************
* STATIC FUNCTIONS
**********************/
#endif /* LV_100ASK_PINYIN_IME_SIMPLE_TEST */
这段代码是一个集成了语音识别、文本交互和 AI 对话功能的应用程序实现,主要基于 LVGL 图形库构建 UI 界面,结合 Vosk 进行语音识别,通过 Ollama 调用 AI 模型生成回答。以下是其核心功能和工作流程的详细解释:
1. 核心功能概述
该程序实现了一个交互式对话系统,支持两种输入方式:
- 语音输入:通过麦克风录音,经 Vosk 语音识别转为文本
- 文本输入:通过屏幕键盘手动输入文本
输入内容会被发送到本地 Ollama 服务(运行 deepseek-r1 模型),AI 生成的回答会实时显示在界面上。整体流程为:录音/输入 → 语音识别(可选) → AI请求 → 显示回答。
2. 关键技术组件
- LVGL:嵌入式图形库,用于构建 UI 界面(文本框、按钮、键盘等)
- SDL2:处理音频录制,实现麦克风数据采集并保存为 WAV 文件
- Vosk:轻量级语音识别库,通过 WebSocket 通信将音频转为文本
- Ollama:本地 AI 服务接口,接收文本请求并返回 AI 生成的回答
- 多线程:使用 pthread 实现录音、语音识别、网络请求的并行处理
- 信号量:实现线程间同步,确保 UI 更新的线程安全
3. 主要模块与工作流程
(1)UI 界面模块
- 创建了两个文本框:上方显示 AI 回答,下方作为用户输入框
- 集成拼音输入法键盘,支持中文输入
- 两个功能按钮:"语音输入"(控制录音)和 "发送"(提交文本请求)
- 通过定时器(100ms 间隔)检查 UI 更新信号,安全更新界面内容
(2)录音模块
- 使用 SDL2 初始化音频设备,配置采样率 16000Hz(与 Vosk 兼容)
- "语音输入" 按钮控制录音开始 / 停止:
- 开始录音:打开音频设备,创建线程循环读取麦克风数据,保存为 WAV 文件
- 停止录音:更新 WAV 文件头(补全音频数据大小),关闭设备
- 录音完成后自动触发语音识别流程
(3)语音识别模块(Vosk)
- 通过 WebSocket 连接本地 Vosk 服务(默认地址
ws://127.0.0.1:2700) - 发送流程:
- 连接成功后发送配置信息(指定采样率)
- 读取 WAV 文件(跳过文件头),分块发送音频数据
- 接收与解析:
- 接收 Vosk 返回的 JSON 数据(包含中间结果和最终文本)
- 解析出
"text"字段作为识别结果,过滤无效内容
- 超时控制:音频发送完成后等待 8 秒,未收到结果则判定为识别失败
(4)AI 对话模块(Ollama)
- 构建 HTTP 请求:将用户输入(或语音识别结果)封装为 JSON 格式
- 发送到本地 Ollama 服务(默认端口 11434,路径
/api/generate) - 处理响应:
- 解析 HTTP 响应中的 JSON 数据,提取
"response"字段 - 清理结果(处理转义字符、过滤无效标签、移除冗余字符)
- 将处理后的回答通过信号量通知 UI 更新
- 解析 HTTP 响应中的 JSON 数据,提取
(5)线程与同步
- 录音、语音识别、AI 请求均在独立线程中执行,避免阻塞 UI
- 使用信号量(
ui_update_sem)实现线程间通信:- 后台线程更新
ui_update_text缓冲区并发送信号 - UI 定时器线程检测到信号后,安全更新界面显示
- 后台线程更新
4. 代码特点
- 模块化设计:各功能(UI、录音、识别、AI 请求)分离,便于维护
- 健壮性处理:包含内存检查、超时控制、错误提示等容错机制
- 中文支持:通过拼音输入法和中文字体配置,支持中文输入输出
- 本地部署:依赖的 Vosk 和 Ollama 服务均运行在本地,保护隐私
总结
该代码实现了一个功能完整的语音交互 AI 助手原型,适用于嵌入式设备或本地桌面环境。用户可以通过语音或文本与 AI 对话,整个流程在本地完成,响应迅速且保护数据隐私。
更多推荐


所有评论(0)