代码注释:

/**
 * @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
  • 发送流程:
    1. 连接成功后发送配置信息(指定采样率)
    2. 读取 WAV 文件(跳过文件头),分块发送音频数据
  • 接收与解析:
    1. 接收 Vosk 返回的 JSON 数据(包含中间结果和最终文本)
    2. 解析出"text"字段作为识别结果,过滤无效内容
  • 超时控制:音频发送完成后等待 8 秒,未收到结果则判定为识别失败
(4)AI 对话模块(Ollama)
  • 构建 HTTP 请求:将用户输入(或语音识别结果)封装为 JSON 格式
  • 发送到本地 Ollama 服务(默认端口 11434,路径/api/generate
  • 处理响应:
    1. 解析 HTTP 响应中的 JSON 数据,提取"response"字段
    2. 清理结果(处理转义字符、过滤无效标签、移除冗余字符)
    3. 将处理后的回答通过信号量通知 UI 更新
(5)线程与同步
  • 录音、语音识别、AI 请求均在独立线程中执行,避免阻塞 UI
  • 使用信号量(ui_update_sem)实现线程间通信:
    • 后台线程更新ui_update_text缓冲区并发送信号
    • UI 定时器线程检测到信号后,安全更新界面显示

4. 代码特点

  • 模块化设计:各功能(UI、录音、识别、AI 请求)分离,便于维护
  • 健壮性处理:包含内存检查、超时控制、错误提示等容错机制
  • 中文支持:通过拼音输入法和中文字体配置,支持中文输入输出
  • 本地部署:依赖的 Vosk 和 Ollama 服务均运行在本地,保护隐私

总结

该代码实现了一个功能完整的语音交互 AI 助手原型,适用于嵌入式设备或本地桌面环境。用户可以通过语音或文本与 AI 对话,整个流程在本地完成,响应迅速且保护数据隐私。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐