feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s

Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
This commit is contained in:
Codex Local
2026-10-08 10:06:44 +08:00
parent 98fb2965d0
commit bf1233d5df
861 changed files with 94614 additions and 3209 deletions
@@ -0,0 +1,40 @@
#ifndef CALL_DISPLAY_STATE_H
#define CALL_DISPLAY_STATE_H
#include <stdbool.h>
#include <stdint.h>
typedef enum {
CALL_DISPLAY_IDLE,
CALL_DISPLAY_CONNECTING,
CALL_DISPLAY_TALKING,
CALL_DISPLAY_ENDING,
CALL_DISPLAY_ENDED,
CALL_DISPLAY_FAILED,
CALL_DISPLAY_INTERRUPTED,
} call_display_phase_t;
typedef enum {
CALL_DISPLAY_STOP_NORMAL,
CALL_DISPLAY_STOP_FAILED,
CALL_DISPLAY_STOP_INTERRUPTED,
} call_display_stop_t;
// Owned by the conversation task. SDK callbacks only latch atomic observations;
// they never render text or mutate this lifecycle while the task is stopping.
typedef struct {
call_display_phase_t phase;
unsigned generation;
bool connected;
uint64_t terminal_until_ms;
} call_display_state_t;
unsigned call_display_begin(call_display_state_t *state);
void call_display_connected(call_display_state_t *state, unsigned generation);
void call_display_stopping(call_display_state_t *state, unsigned generation);
void call_display_finished(call_display_state_t *state, unsigned generation,
call_display_stop_t reason, bool audio_stopped, uint64_t now_ms);
bool call_display_idle_if_due(call_display_state_t *state, unsigned generation, uint64_t now_ms);
const char *call_display_text(const call_display_state_t *state);
#endif
@@ -0,0 +1,19 @@
#pragma once
#include <stdbool.h>
#include <stdint.h>
/* Owned by the conversation task. SDK callbacks only publish atomic counters. */
typedef struct {
bool pending;
bool reply_complete;
uint32_t requested_ms;
unsigned finish_baseline;
} call_voice_end_t;
bool call_voice_end_command(const char *text);
void call_voice_end_reset(call_voice_end_t *state);
void call_voice_end_user(call_voice_end_t *state, const char *text,
uint32_t now_ms, unsigned finish_serial);
bool call_voice_end_due(const call_voice_end_t *state, uint32_t now_ms,
unsigned finish_serial, bool playback_quiet,
bool user_speaking);
@@ -12,6 +12,7 @@ typedef struct {
} volc_conv_service_t;
void conv_ai_service_task(void *pvParameters);
void conv_ai_service_prepare_task(void);
void conv_ai_service_task_stop();
void conv_ai_service_init();
volc_conv_service_t get_conv_ai_service();
@@ -0,0 +1,57 @@
#include "call_display_state.h"
unsigned call_display_begin(call_display_state_t *state)
{
++state->generation;
state->phase = CALL_DISPLAY_CONNECTING;
state->connected = false;
state->terminal_until_ms = 0;
return state->generation;
}
void call_display_connected(call_display_state_t *state, unsigned generation)
{
if (state->generation == generation && state->phase == CALL_DISPLAY_CONNECTING) {
state->connected = true;
state->phase = CALL_DISPLAY_TALKING;
}
}
void call_display_stopping(call_display_state_t *state, unsigned generation)
{
if (state->generation == generation &&
(state->phase == CALL_DISPLAY_CONNECTING || state->phase == CALL_DISPLAY_TALKING)) {
state->phase = CALL_DISPLAY_ENDING;
}
}
void call_display_finished(call_display_state_t *state, unsigned generation,
call_display_stop_t reason, bool audio_stopped, uint64_t now_ms)
{
if (state->generation != generation || state->phase != CALL_DISPLAY_ENDING || !audio_stopped) return;
state->phase = reason == CALL_DISPLAY_STOP_NORMAL ? CALL_DISPLAY_ENDED :
state->connected ? CALL_DISPLAY_INTERRUPTED : CALL_DISPLAY_FAILED;
state->terminal_until_ms = now_ms + 2000;
}
bool call_display_idle_if_due(call_display_state_t *state, unsigned generation, uint64_t now_ms)
{
if (state->generation != generation ||
(state->phase != CALL_DISPLAY_ENDED && state->phase != CALL_DISPLAY_FAILED &&
state->phase != CALL_DISPLAY_INTERRUPTED) || now_ms < state->terminal_until_ms) return false;
state->phase = CALL_DISPLAY_IDLE;
return true;
}
const char *call_display_text(const call_display_state_t *state)
{
switch (state->phase) {
case CALL_DISPLAY_CONNECTING: return "正在连接";
case CALL_DISPLAY_TALKING: return "通话中";
case CALL_DISPLAY_ENDING: return "正在结束";
case CALL_DISPLAY_ENDED: return "通话结束";
case CALL_DISPLAY_FAILED: return "连接失败";
case CALL_DISPLAY_INTERRUPTED: return "连接中断";
default: return "待机";
}
}
@@ -0,0 +1,98 @@
#include "call_voice_end.h"
#include <stddef.h>
#include <string.h>
/* Match a whole short request, never a substring in a story, quote, question
* or negated instruction. This intentionally is a bounded command vocabulary. */
bool call_voice_end_command(const char *text)
{
if (!text) return false;
char normalized[256];
size_t n = 0;
static const char *punctuation[] = {",", "。", "!", "、"};
for (const char *p = text; *p;) {
bool skipped = false;
for (size_t i = 0; i < sizeof(punctuation) / sizeof(*punctuation); ++i) {
size_t len = strlen(punctuation[i]);
if (!strncmp(p, punctuation[i], len)) { p += len; skipped = true; break; }
}
if (skipped) continue;
if (*p == ' ' || *p == '\t' || *p == '\r' || *p == '\n' ||
*p == ',' || *p == '.' || *p == '!') { ++p; continue; }
/* Preserve question marks, quotation marks and all other words. */
if (n + 1 >= sizeof(normalized)) return false;
normalized[n++] = *p++;
}
normalized[n] = 0;
static const char *prefixes[] = {
"好了", "好啦", "好的", "嗯", "那就", "那", "我们", "咱们",
"今天", "现在", "就先", "就", "先", "请", "麻烦你", "谢谢你", "谢谢"
};
const char *p = normalized;
for (unsigned pass = 0; pass < 8; ++pass) {
bool found = false;
for (size_t i = 0; i < sizeof(prefixes) / sizeof(*prefixes); ++i) {
size_t len = strlen(prefixes[i]);
if (!strncmp(p, prefixes[i], len)) { p += len; found = true; break; }
}
if (!found) break;
}
static const char *commands[] = {
"拜拜", "再见", "下次再聊", "下回再聊", "回头再聊",
"聊到这里", "聊到这儿", "聊到这", "不聊了", "我不聊了",
"结束聊天", "结束对话", "结束通话", "结束这次聊天", "结束本次通话",
"挂断电话", "挂断通话", "挂断", "挂了", "我挂了", "我先挂了",
"我先走了", "我要休息了", "我去休息了"
};
static const char *tails[] = {
"吧", "把", "啦", "了", "啊", "呀", "哦", "哈", "谢谢你", "谢谢",
"拜拜", "再见", "下次再聊", "下回再聊", "回头再聊"
};
for (size_t i = 0; i < sizeof(commands) / sizeof(*commands); ++i) {
size_t len = strlen(commands[i]);
if (strncmp(p, commands[i], len)) continue;
const char *tail = p + len;
for (unsigned pass = 0; *tail && pass < 8; ++pass) {
bool found = false;
for (size_t j = 0; j < sizeof(tails) / sizeof(*tails); ++j) {
size_t tlen = strlen(tails[j]);
if (!strncmp(tail, tails[j], tlen)) { tail += tlen; found = true; break; }
}
if (!found) break;
}
if (!*tail) return true;
}
return false;
}
void call_voice_end_reset(call_voice_end_t *state)
{
memset(state, 0, sizeof(*state));
}
void call_voice_end_user(call_voice_end_t *state, const char *text,
uint32_t now_ms, unsigned finish_serial)
{
if (!call_voice_end_command(text)) {
/* A later complete user utterance cancels the pending goodbye. */
call_voice_end_reset(state);
} else if (!state->pending) {
state->pending = true;
state->reply_complete = false;
state->requested_ms = now_ms;
state->finish_baseline = finish_serial;
}
/* Repeated subtitles must not extend the stop deadline. */
}
bool call_voice_end_due(const call_voice_end_t *state, uint32_t now_ms,
unsigned finish_serial, bool playback_quiet,
bool user_speaking)
{
if (!state->pending || user_speaking) return false;
uint32_t elapsed = now_ms - state->requested_ms;
/* Missing response events/audio must not leave an ended chat open forever. */
if (elapsed >= 12000u) return true;
return elapsed >= 1500u && playback_quiet &&
(state->reply_complete || finish_serial != state->finish_baseline);
}
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
#include <stdio.h>
#include <string.h>
#include <stdatomic.h>
#include "aios.h"
#include "cJSON.h"
@@ -10,6 +11,9 @@
#include "util/volc_log.h"
#include "volc_service_common.h"
#include "volc_conv.h"
#include "call_display_state.h"
#include "call_voice_end.h"
#include "esp_gmf_afe.h"
#include "volc_hal.h"
#include "volc_hal_capture.h"
#include "volc_hal_display.h"
@@ -20,12 +24,18 @@
#include "mmap_generate_eaf.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include "freertos/task.h"
#include "freertos/idf_additions.h"
extern int8_t get_rssi(void);
#if __cplusplus
extern "C" {
#endif
/* Opus RTP uses a 48 kHz clock in BOTH directions (RFC 7587). We submit
* externally encoded 20 ms packets, so the SDK must timestamp them with
* 960 ticks. The separate PCM encoder/decoder still uses 16 kHz / 320 samples;
* using that PCM rate for RTP compresses the transmitted audio timeline. */
#define CONV_AI_CONFIG_FORMAT "{\
\"ver\": 1,\
\"iot\": {\
@@ -50,32 +60,150 @@ extern "C" {
\"{\\\"debug\\\":{\\\"log_to_console\\\":1}}\",\
\"{\\\"rtc\\\":{\\\"access\\\":{\\\"concurrent_requests\\\":1}}}\",\
\"{\\\"rtc\\\":{\\\"ice\\\":{\\\"concurrent_agents\\\":1}}}\",\
\"{\\\"rtc\\\":{\\\"thread\\\":{\\\"stack_in_ext\\\":1}}}\",\
\"{\\\"rtc\\\":{\\\"thread\\\":{\\\"stack_in_ext\\\":1,\\\"stack_size\\\":49152}}}\",\
\"{\\\"rtc\\\":{\\\"network\\\":{\\\"enable_audio_jitter2\\\":0}}}\",\
\"{\\\"rtc\\\":{\\\"report\\\":{\\\"enable\\\":0}}}\",\
\"{\\\"audio\\\":{\\\"codec\\\":{\\\"pcma\\\":{\\\"s_samples_per_frame\\\":480}}}}\"\
\"{\\\"audio\\\":{\\\"codec\\\":{\\\"opus\\\":{\\\"s_sample_rate\\\":48000,\\\"s_samples_per_frame\\\":960,\\\"r_sample_rate\\\":48000,\\\"r_samples_per_frame\\\":960}}}}\"\
]\
}\
}"
#define CONV_SERVICE_TIMEOUT 60
/* CALL_IDLE_BEGIN: callbacks only update atomic activity, never stop the SDK.
* Continuous microphone/silence packets and lease renewals are not activity.
* Unsigned monotonic milliseconds also handle the 32-bit clock wrap. */
static atomic_uint last_call_activity_ms;
static atomic_bool local_speech_active;
static void call_idle_activity(void)
{
atomic_store(&last_call_activity_ms, (uint32_t)volc_osal_get_time_ms());
}
static bool call_idle_expired(void)
{
uint32_t now = (uint32_t)volc_osal_get_time_ms();
return (uint32_t)(now - atomic_load(&last_call_activity_ms)) >= CONV_SERVICE_TIMEOUT * 1000u;
}
static void call_idle_capture_event(void *event, void *user_data)
{
(void)user_data;
const esp_gmf_afe_evt_t *vad = event;
if (!vad) return;
if (vad->type == ESP_GMF_AFE_EVT_VAD_START || vad->type == ESP_GMF_AFE_EVT_VAD_END) {
atomic_store(&local_speech_active, vad->type == ESP_GMF_AFE_EVT_VAD_START);
call_idle_activity();
}
}
static bool call_idle_pcm_active(const void *data, size_t bytes)
{
if (!data || bytes < sizeof(int16_t) || (bytes & 1)) return false;
const int16_t *pcm = data;
uint64_t magnitude = 0;
size_t samples = bytes / sizeof(*pcm);
for (size_t i = 0; i < samples; ++i) {
magnitude += pcm[i] < 0 ? -(int)pcm[i] : pcm[i];
}
// RTC sends decoded silence continuously, even when the agent is quiet.
// Measure synthesized audio before the user's speaker volume is applied.
return magnitude / samples >= 64;
}
/* CALL_IDLE_END */
static char config_buf[1024] = {0};
static volc_conv_service_t conv_service = {0};
static volatile bool is_stop = false;
static volatile bool provider_disconnected = false;
static atomic_bool is_stop = true;
static atomic_bool stop_requested = false;
static atomic_bool provider_disconnected = false;
static atomic_bool display_connected = false;
static atomic_bool user_stop_requested = false;
static call_display_state_t call_display;
static volc_hal_display_t global_display = NULL;
static bool session_started_reported = false;
static QueueHandle_t platform_report_queue;
static volatile bool diagnostic_say_pending;
static call_voice_end_t voice_end;
static atomic_uint voice_finish_serial;
static atomic_uint last_voice_received_ms;
/* VOICE_DIAGNOSTIC_BEGIN: local USB requests only. TTS keeps microphone uplink
* suppressed until this call ends or an explicit diag-say restores it. */
enum { DIAGNOSTIC_NONE, DIAGNOSTIC_SAY, DIAGNOSTIC_TTS, DIAGNOSTIC_PENDING_MASK = 3,
DIAGNOSTIC_MUTE = 4, DIAGNOSTIC_CLOSING = 8, DIAGNOSTIC_ACTIVE = 16 };
static atomic_uint diagnostic_state;
extern bool platform_runtime_rtc(void);
bool conv_ai_service_diagnostic_active(void)
{
return (atomic_load(&diagnostic_state) & DIAGNOSTIC_ACTIVE) != 0;
}
void conv_ai_service_diagnostic_reset(void)
{
atomic_fetch_and(&diagnostic_state, DIAGNOSTIC_CLOSING);
}
static void diagnostic_close(void)
{
atomic_store(&diagnostic_state, DIAGNOSTIC_CLOSING);
}
static void diagnostic_prepare(void)
{
atomic_fetch_and(&diagnostic_state, ~DIAGNOSTIC_CLOSING);
}
static bool diagnostic_uplink_suppressed(void)
{
return (atomic_load(&diagnostic_state) & DIAGNOSTIC_MUTE) != 0;
}
static unsigned diagnostic_take_pending(void)
{
return atomic_fetch_and(&diagnostic_state, ~DIAGNOSTIC_PENDING_MASK) & DIAGNOSTIC_PENDING_MASK;
}
static void diagnostic_request(int mode)
{
unsigned state = atomic_load(&diagnostic_state);
do {
if ((state & DIAGNOSTIC_CLOSING) && xTaskGetHandle("conv_ai")) {
LOGI("PLAYBACK diagnostic request rejected: call closing");
return;
}
} while (!atomic_compare_exchange_weak(&diagnostic_state, &state,
(unsigned)mode | DIAGNOSTIC_ACTIVE |
(mode == DIAGNOSTIC_TTS ? DIAGNOSTIC_MUTE : 0)));
/* Repeating the fixed passage reuses the existing room. */
if (!xTaskGetHandle("conv_ai") && aios_event_pub(VOLC_SERVICE_AI_CONVERSATION, NULL, NULL) != 0) {
conv_ai_service_diagnostic_reset();
}
}
void conv_ai_service_diagnostic_say(void)
{
diagnostic_say_pending = true;
aios_event_pub(VOLC_SERVICE_AI_CONVERSATION, NULL, NULL);
diagnostic_request(DIAGNOSTIC_SAY);
}
void conv_ai_service_diagnostic_tts(void)
{
diagnostic_request(DIAGNOSTIC_TTS);
}
static void diagnostic_send_tts(void)
{
static const char *passage = "这是连续语音播放测试。请您听一听,这段声音是否清楚、连贯,中间有没有突然停顿。我们用自然的语速,把这段话平稳地读完。";
int sent = platform_runtime_rtc()
? volc_send_text_to_agent(conv_service.engine, passage, VOLC_AGENT_TYPE_TTS, 1) : -2;
LOGI("PLAYBACK diagnostic TTS requested ret=%d mic_suppressed=1", sent);
if (sent != 0) conv_ai_service_diagnostic_reset();
conv_service.wait_time = 0;
call_idle_activity();
}
/* VOICE_DIAGNOSTIC_END */
/* Implemented by the platform client in the top-level firmware component. */
extern const char *platform_runtime_instance_id(void);
extern const char *platform_runtime_product_key(void);
@@ -83,12 +211,39 @@ extern const char *platform_runtime_device_name(void);
extern const char *platform_runtime_device_secret(void);
extern int platform_conversation_begin(void);
extern const char *platform_last_error_code(void);
extern const char *call_client_start_error(void);
extern const char *platform_conversation_bot_id(void);
extern const char *platform_conversation_update_json(void);
extern int platform_conversation_started(const char *provider_connection_ref);
extern int platform_conversation_poll(void);
extern bool platform_conversation_audio_authorized(void);
extern int platform_conversation_turn(const char *speaker, const char *source_id, const char *text);
extern void platform_conversation_finish(bool failed, const char *error_code);
extern bool platform_runtime_rtc(void);
extern const char *platform_runtime_rtc_app_id(void);
extern void platform_runtime_engine_ready(bool ready);
extern void platform_conversation_observe(const char *type);
extern void platform_conversation_stop_result(bool started,int result);
static atomic_bool rtc_connected;
extern const char *volc_current_user_id(volc_engine_t handle,bool remote);
static void publish_call_display(unsigned generation)
{
if (call_display.generation != generation) return;
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS,
VOLC_DISPLAY_TEXT, call_display_text(&call_display));
}
static void observe_call_connection(unsigned generation)
{
if (atomic_load(&display_connected)) call_display_connected(&call_display, generation);
publish_call_display(generation);
}
static bool conversation_output_allowed(void)
{
return !stop_requested && platform_conversation_audio_authorized();
}
volc_conv_service_t get_conv_ai_service()
{
@@ -98,11 +253,13 @@ volc_conv_service_t get_conv_ai_service()
// volc_hal_file_t* audioFile = NULL;
// int a_cnt = 0;
static void __audio_capture_cb(volc_hal_capture_t capture, const void* data, int len, volc_hal_frame_info_t* frame_info){
if (diagnostic_uplink_suppressed()) return;
volc_audio_frame_info_t info = {0};
info.commit = false;
info.data_type = frame_info->data_type;
if(!is_stop)
if(!is_stop && !stop_requested && platform_conversation_audio_authorized())
{
if (atomic_load(&local_speech_active)) call_idle_activity();
static uint64_t report_ms, magnitude;
static unsigned samples, peak, frames, failures, max_send_ms;
const int16_t *pcm = data;
@@ -140,6 +297,9 @@ static void __on_volc_event(volc_engine_t handle, volc_event_t *event, void *use
{
case VOLC_EV_CONNECTED:
LOGI("Volc Engine connected");
call_idle_activity();
if (!stop_requested) atomic_store(&display_connected, true);
if(platform_runtime_rtc()){atomic_store(&rtc_connected,true);platform_conversation_observe("MEDIA_CONNECTED");}
break;
case VOLC_EV_DISCONNECTED:
LOGI( "Volc Engine disconnected");
@@ -154,33 +314,34 @@ static void __on_volc_event(volc_engine_t handle, volc_event_t *event, void *use
static void __on_volc_conversation_status(volc_engine_t handle, volc_conv_status_e status, void *user_data)
{
LOGI("AUDIO conversation status=%d",(int)status);
// Turn events are not call lifecycle events. A delayed ANSWER_FINISH must
// neither end the call nor overwrite its terminal status.
if (stop_requested) return;
if (status != VOLC_CONV_STATUS_LISTENING) call_idle_activity();
int index = MMAP_EAF_HAPPY_EAF;
switch (status)
{
case VOLC_CONV_STATUS_LISTENING:
index = MMAP_EAF_EMOTION_BLINK_FAST_284_126_EAF;
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_MAIN,VOLC_DISPLAY_IMAGE,&index);
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_STATUS,VOLC_DISPLAY_TEXT,"智能体聆听中");
break;
case VOLC_CONV_STATUS_THINKING:
index = MMAP_EAF_EMOTION_DIZZY_284_126_EAF;
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_MAIN,VOLC_DISPLAY_IMAGE,&index);
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_STATUS,VOLC_DISPLAY_TEXT,"智能体思考中");
break;
case VOLC_CONV_STATUS_ANSWERING:
index = MMAP_EAF_HAPPY_EAF;
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_MAIN,VOLC_DISPLAY_IMAGE,&index);
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_STATUS,VOLC_DISPLAY_TEXT,"智能体说话中");
break;
case VOLC_CONV_STATUS_INTERRUPTED:
{
volc_hal_context_t *hal = volc_get_global_hal_context();
if (hal) volc_hal_player_discard_pending(hal->player_handle[VOLC_HAL_PLAYER_AUDIO]);
}
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_STATUS,VOLC_DISPLAY_TEXT,"智能体被打断");
break;
case VOLC_CONV_STATUS_ANSWER_FINISH:
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_STATUS,VOLC_DISPLAY_TEXT,"智能体说话完成");
atomic_fetch_add(&voice_finish_serial, 1);
break;
default:
break;
@@ -204,7 +365,13 @@ static void __on_volc_audio_data(volc_engine_t handle, const void *data_ptr, siz
LOGE("player pipeline is NULL");
return;
}
if(!is_stop){
if(!is_stop && !stop_requested && platform_conversation_audio_authorized()){
if (info_ptr && info_ptr->data_type == VOLC_AUDIO_DATA_TYPE_PCM &&
call_idle_pcm_active(data_ptr, data_len)) {
call_idle_activity();
atomic_store(&last_voice_received_ms, (uint32_t)volc_osal_get_time_ms());
}
if(data_len)platform_conversation_observe("FIRST_AUDIO_RECEIVED");
volc_hal_player_play_data(player_handle, data_ptr, data_len);
}
}
@@ -241,13 +408,15 @@ static void __on_subtitle_message_received(const cJSON* root) {
cJSON* text_obj = cJSON_GetObjectItem(obji, "text");
cJSON* definite_obj = cJSON_GetObjectItem(obji, "definite");
char* sub = cJSON_GetStringValue(text_obj);
if(!sub)continue;
if (sub[0] && !stop_requested) call_idle_activity();
if(sub_offset<0||(size_t)sub_offset>strlen(sub))sub_offset=0;
// print_string(sub + sub_offset -1);
if(cJSON_IsTrue(definite_obj)){
sub_offset = sub_offset == 0 ? 0 : sub_offset -1;
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_SUBTITLE,VOLC_DISPLAY_TEXT,sub + sub_offset);
sub_offset = 0;
aios_event_pub(VOLC_LOCAL_LOGIC_PROCESS_SUBTITLE, sub, NULL);
} else if(strlen(sub) - sub_offset > 18){
sub_offset = sub_offset == 0 ? 0 : sub_offset -1;
volc_hal_display_set_content(global_display,VOLC_DISPLAY_OBJ_SUBTITLE,VOLC_DISPLAY_TEXT,sub + sub_offset);
@@ -259,6 +428,62 @@ static void __on_subtitle_message_received(const cJSON* root) {
}
}
/* Called only by the conversation task, never from an SDK callback. */
static struct {
char text[512];
double round, sequence;
bool active, complete, overflow;
} voice_user_subtitle;
static void process_voice_end_transcript(const char *speaker, const char *text, bool complete)
{
if (!complete || !text || !*text || is_stop || stop_requested) return;
if (!strcmp(speaker, "subject")) {
call_voice_end_user(&voice_end, text, (uint32_t)volc_osal_get_time_ms(),
atomic_load(&voice_finish_serial));
} else if (!strcmp(speaker, "agent") && voice_end.pending) {
voice_end.reply_complete = true;
}
}
static void process_voice_end_rtc_user(const cJSON *line)
{
cJSON *round = cJSON_GetObjectItemCaseSensitive(line, "roundId");
cJSON *sequence = cJSON_GetObjectItemCaseSensitive(line, "sequence");
const char *text = cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(line, "text"));
if (!cJSON_IsNumber(round) || !cJSON_IsNumber(sequence) || !text ||
!cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(line, "definite")) || is_stop || stop_requested) return;
if (voice_user_subtitle.active && round->valuedouble == voice_user_subtitle.round &&
sequence->valuedouble <= voice_user_subtitle.sequence) return;
if (!voice_user_subtitle.active || round->valuedouble != voice_user_subtitle.round || voice_user_subtitle.complete) {
memset(&voice_user_subtitle, 0, sizeof(voice_user_subtitle));
voice_user_subtitle.active = true;
voice_user_subtitle.round = round->valuedouble;
}
voice_user_subtitle.sequence = sequence->valuedouble;
size_t used = strlen(voice_user_subtitle.text), len = strlen(text);
if (used + len + 1 >= sizeof(voice_user_subtitle.text)) voice_user_subtitle.overflow = true;
if (!voice_user_subtitle.overflow) {
memcpy(voice_user_subtitle.text + used, text, len + 1);
}
if (cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(line, "paragraph"))) {
voice_user_subtitle.complete = true;
/* Overflow disables matching but still cancels a pending goodbye. */
process_voice_end_transcript("subject", voice_user_subtitle.overflow ? "continue" : voice_user_subtitle.text, true);
}
}
static bool voice_end_ready(void)
{
if (!voice_end.pending) return false;
volc_hal_context_t *hal = volc_get_global_hal_context();
uint32_t now = (uint32_t)volc_osal_get_time_ms();
bool quiet = now - atomic_load(&last_voice_received_ms) >= 1200u && hal &&
volc_hal_player_voice_quiet(hal->player_handle[VOLC_HAL_PLAYER_AUDIO], 600u);
return call_voice_end_due(&voice_end, now, atomic_load(&voice_finish_serial),
quiet, atomic_load(&local_speech_active));
}
/* HTTPS reporting must never run inside the WebSocket receive mutex. */
static void process_platform_reports(void)
{
@@ -266,6 +491,24 @@ static void process_platform_reports(void)
while (platform_report_queue && xQueueReceive(platform_report_queue, &root, 0) == pdTRUE) {
cJSON *type_obj = cJSON_GetObjectItemCaseSensitive(root, "type");
const char *type = cJSON_IsString(type_obj) ? type_obj->valuestring : "";
if (!strcmp(type,"subtitle") && platform_runtime_rtc()) {
cJSON *line=NULL;cJSON_ArrayForEach(line,cJSON_GetObjectItemCaseSensitive(root,"data")){
const char *text=cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(line,"text"));
const char *user=cJSON_GetStringValue(cJSON_GetObjectItemCaseSensitive(line,"userId"));
cJSON *seq=cJSON_GetObjectItemCaseSensitive(line,"sequence");
if(text&&user&&cJSON_IsNumber(seq)&&cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(line,"definite"))){
char key[240];snprintf(key,sizeof(key),"%s:%.0f",user,seq->valuedouble);
const char *local=volc_current_user_id(conv_service.engine,false),*remote=volc_current_user_id(conv_service.engine,true);
const char *speaker=local&&!strcmp(user,local)?"subject":remote&&!strcmp(user,remote)?"agent":"unknown";
/* definite is a final clause; paragraph confirms the whole
* utterance, so "拜拜,不对,我还想聊" cannot end early. */
if (!strcmp(speaker, "subject")) process_voice_end_rtc_user(line);
else process_voice_end_transcript(speaker, text,
cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(line,"paragraph")));
(void)platform_conversation_turn(speaker,key,text);
}
}
} else
if ((strcmp(type, "session.updated") == 0 || strcmp(type, "session.created") == 0) && !session_started_reported) {
cJSON *session = cJSON_GetObjectItemCaseSensitive(root, "session");
cJSON *id = cJSON_IsObject(session) ? cJSON_GetObjectItemCaseSensitive(session, "id") : NULL;
@@ -280,12 +523,14 @@ static void process_platform_reports(void)
cJSON *text = cJSON_GetObjectItemCaseSensitive(root, "transcript");
cJSON *source = cJSON_GetObjectItemCaseSensitive(root, "event_id");
if (cJSON_IsString(text) && text->valuestring[0]) {
process_voice_end_transcript("subject", text->valuestring, true);
(void)platform_conversation_turn("subject", cJSON_IsString(source) ? source->valuestring : "subject", text->valuestring);
}
} else if (strcmp(type, "response.audio_transcript.done") == 0) {
cJSON *text = cJSON_GetObjectItemCaseSensitive(root, "transcript");
cJSON *source = cJSON_GetObjectItemCaseSensitive(root, "response_id");
if (cJSON_IsString(text) && text->valuestring[0]) {
process_voice_end_transcript("agent", text->valuestring, true);
(void)platform_conversation_turn("agent", cJSON_IsString(source) ? source->valuestring : "agent", text->valuestring);
}
}
@@ -293,9 +538,19 @@ static void process_platform_reports(void)
}
}
static bool subtitle_has_final_clause(const cJSON *root)
{
cJSON *line = NULL;
cJSON_ArrayForEach(line, cJSON_GetObjectItemCaseSensitive(root, "data")) {
if (cJSON_IsTrue(cJSON_GetObjectItemCaseSensitive(line, "definite"))) return true;
}
return false;
}
static void __on_volc_message_data(volc_engine_t handle, const void *message, size_t size, volc_message_info_t *info_ptr, void *user_data)
{
static char message_buffer[4096];
if(size>=sizeof(message_buffer)-1){platform_conversation_observe("GAP_DECLARED");return;}
if (size > 0 && size < sizeof(message_buffer) - 1) {
memcpy(message_buffer, message, size);
message_buffer[size] = 0;
@@ -306,9 +561,10 @@ static void __on_volc_message_data(volc_engine_t handle, const void *message, si
if (standard_protocol) {
const char *type = cJSON_GetStringValue(cJSON_GetObjectItem(root, "type"));
if (type && (!strcmp(type, "error") || !strcmp(type, "conversation.item.created"))) {
LOGI("Provider control event: %s", message_buffer);
LOGI("Provider control event type: %s", type);
}
if (type && !strcmp(type, "response.audio.done") && !is_stop) {
atomic_fetch_add(&voice_finish_serial, 1);
/* Flush the final sub-frame through the PCM decoder. */
static const uint8_t silence[2048] = {0};
volc_hal_context_t *hal = volc_get_global_hal_context();
@@ -320,8 +576,10 @@ static void __on_volc_message_data(volc_engine_t handle, const void *message, si
}
if (type && (strstr(type, "speech_") || strstr(type, "response."))) {
conv_service.wait_time = 0;
if (!stop_requested) call_idle_activity();
}
bool report = type && (!strcmp(type, "session.created") || !strcmp(type, "session.updated") ||
(!strcmp(type, "subtitle") && platform_runtime_rtc() && subtitle_has_final_clause(root)) ||
!strcmp(type, "conversation.item.input_audio_transcription.completed") ||
!strcmp(type, "response.audio_transcript.done"));
if (report && platform_report_queue && xQueueSend(platform_report_queue, &root, 0) == pdTRUE) {
@@ -333,6 +591,13 @@ static void __on_volc_message_data(volc_engine_t handle, const void *message, si
} else if (message_buffer[0] == 's' && message_buffer[1] == 'u' && message_buffer[2] == 'b' && message_buffer[3] == 'v') {
__on_subtitle_message_received(root);
conv_service.wait_time = 0;
/* Interim ASR updates must not fill the bounded control queue
* and evict the actual completed goodbye. Display still gets all. */
if(platform_runtime_rtc() && subtitle_has_final_clause(root)){
if(platform_report_queue&&xQueueSend(platform_report_queue,&root,0)==pdTRUE)root=NULL;
else platform_conversation_observe("GAP_DECLARED");
}
cJSON_Delete(root);
} else if (message_buffer[0] == 't' && message_buffer[1] == 'o' && message_buffer[2] == 'o' && message_buffer[3] == 'l') {
// function calling 消息
aios_event_pub(VOLC_FUNCTION_CALL_EXEC, root, NULL);
@@ -360,7 +625,16 @@ void conv_ai_service_init()
.on_volc_message_data = __on_volc_message_data,
};
int ret = volc_create(&conv_service.engine, config_buf, &conv_service.volc_event_handler, &conv_service);
cJSON *runtime=cJSON_Parse(config_buf);
if(platform_runtime_rtc()){
cJSON_AddStringToObject(cJSON_GetObjectItem(runtime,"iot"),"rtc_app_id",platform_runtime_rtc_app_id());
cJSON_SetNumberValue(cJSON_GetObjectItem(cJSON_GetObjectItem(cJSON_GetObjectItem(runtime,"rtc"),"audio"),"codec"),1);
}
else cJSON_DeleteItemFromObjectCaseSensitive(runtime,"rtc");
char *runtime_config=cJSON_PrintUnformatted(runtime);cJSON_Delete(runtime);
int ret = volc_create(&conv_service.engine, runtime_config, &conv_service.volc_event_handler, &conv_service);
if(runtime_config){memset(runtime_config,0,strlen(runtime_config));free(runtime_config);}
platform_runtime_engine_ready(ret==0);
if (ret != 0) {
LOGE("volc_create failed, ret: %d", ret);
return;
@@ -369,14 +643,42 @@ void conv_ai_service_init()
}
}
void conv_ai_service_prepare_task(void)
{
// Called synchronously by the serialized manager before creating the task.
diagnostic_prepare();
stop_requested = false;
is_stop = true;
atomic_store(&user_stop_requested, false);
atomic_store(&display_connected, false);
atomic_store(&local_speech_active, false);
call_voice_end_reset(&voice_end);
memset(&voice_user_subtitle, 0, sizeof(voice_user_subtitle));
atomic_store(&voice_finish_serial, 0);
atomic_store(&last_voice_received_ms, (uint32_t)volc_osal_get_time_ms());
/* The previous SDK has stopped before the manager admits a new call. */
cJSON *stale;
while (platform_report_queue && xQueueReceive(platform_report_queue, &stale, 0) == pdTRUE) cJSON_Delete(stale);
call_idle_activity();
call_display_begin(&call_display);
}
void conv_ai_service_task(void *pvParameters)
{
LOGI("TASK_DIAGNOSTIC name=conv_ai present=1 priority=%u core=%d",
(unsigned)uxTaskPriorityGet(NULL), (int)xTaskGetCoreID(NULL));
volc_hal_context_t* g_hal_context = volc_get_global_hal_context();
char* status_str = NULL;
bool provider_started = false;
bool audio_started = false;
bool local_audio_stopped = true;
call_display_stop_t display_stop = CALL_DISPLAY_STOP_FAILED;
unsigned display_generation = call_display.generation;
char *params = NULL;
is_stop = true;
session_started_reported = false;
provider_disconnected = false;
atomic_store(&rtc_connected,false);
if (g_hal_context == NULL) {
LOGE("volc_get_global_hal_context failed");
status_str = "系统未初始化,ai对话连接失败";
@@ -384,8 +686,9 @@ void conv_ai_service_task(void *pvParameters)
}
global_display = g_hal_context->display_handle;
int ret = 0;
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS,VOLC_DISPLAY_TEXT, "ai对话创建中");
publish_call_display(display_generation);
if (stop_requested) goto CONV_AI_QUIT;
ret = platform_conversation_begin();
if (ret != 0) {
LOGE("platform_conversation_begin failed, ret: %d", ret);
@@ -393,8 +696,14 @@ void conv_ai_service_task(void *pvParameters)
if (strcmp(platform_last_error_code(), "FIRMWARE_UPDATE_REQUIRED") == 0) {
status_str = "平台要求更新固件";
}
if (strcmp(call_client_start_error(), "DEVICE_UNBOUND") == 0) {
status_str = "设备没有绑定人员,请先绑定";
} else if (strcmp(call_client_start_error(), "VOICE_AGENT_NOT_READY") == 0) {
status_str = "老人的语音助手尚未就绪";
}
goto CONV_AI_QUIT;
}
if (stop_requested || !platform_conversation_audio_authorized()) goto CONV_AI_QUIT;
/* Capture may run while the WebSocket is still handshaking. Hold frames
* until volc_start has moved the engine into STARTED state. */
@@ -404,7 +713,7 @@ void conv_ai_service_task(void *pvParameters)
volc_hal_capture_config_t capture_audio_config = {0};
capture_audio_config.media_type = VOLC_MEDIA_TYPE_AUDIO;
capture_audio_config.data_cb = __audio_capture_cb;
// capture_audio_config.audio_wakeup_cb = (volc_hal_audio_wakeup_cb_t)rec_engine_cb;
capture_audio_config.audio_wakeup_cb = call_idle_capture_event;
capture_audio_config.user_data = g_hal_context;
volc_hal_capture_t capture_handle = g_hal_context->capture_handle[VOLC_HAL_PLAYER_AUDIO];
@@ -413,9 +722,13 @@ void conv_ai_service_task(void *pvParameters)
capture_handle = g_hal_context->capture_handle[VOLC_HAL_PLAYER_AUDIO];
}
audio_started = true;
volc_hal_capture_stop(g_hal_context->capture_handle[VOLC_HAL_CAPTURE_AUDIO]);
volc_hal_capture_update_config(capture_handle, &capture_audio_config);
volc_hal_capture_start(g_hal_context->capture_handle[VOLC_HAL_PLAYER_AUDIO], VOLC_AUDIO_MODE_CAPTURE);
if (!capture_handle ||
volc_hal_capture_stop(capture_handle) != 0 ||
volc_hal_capture_update_config(capture_handle, &capture_audio_config) != 0 ||
volc_hal_capture_start(capture_handle, VOLC_AUDIO_MODE_CAPTURE) != 0) {
status_str = "收音启动失败";
goto CONV_AI_QUIT;
}
volc_hal_player_config_t player_audio_config = {0};
player_audio_config.media_type = VOLC_MEDIA_TYPE_AUDIO;
@@ -423,20 +736,22 @@ void conv_ai_service_task(void *pvParameters)
g_hal_context->player_handle[VOLC_HAL_PLAYER_AUDIO] = volc_hal_player_create(&player_audio_config);
}
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS, VOLC_DISPLAY_TEXT, "ai对话连接中");
publish_call_display(display_generation);
volc_hal_player_set_authorization_gate(g_hal_context->player_handle[VOLC_HAL_PLAYER_AUDIO], conversation_output_allowed);
volc_hal_player_start(g_hal_context->player_handle[VOLC_HAL_PLAYER_AUDIO]);
// step 2: start ai conversation
const char *session_update = platform_conversation_update_json();
size_t params_size = strlen(session_update) + 64;
char *params = volc_osal_malloc(params_size);
params = volc_osal_malloc(params_size);
if (params == NULL) {
status_str = "会话配置内存不足";
goto CONV_AI_QUIT;
}
snprintf(params, params_size, "{\"audio\":{\"codec\":0},\"session_update\":%s}", session_update);
if(platform_runtime_rtc())snprintf(params,params_size,"%s",session_update);
else snprintf(params, params_size, "{\"audio\":{\"codec\":0},\"session_update\":%s}", session_update);
volc_opt_t opt = {
.mode = VOLC_MODE_WS,
.mode = platform_runtime_rtc()?VOLC_MODE_RTC:VOLC_MODE_WS,
.bot_id = (char *)platform_conversation_bot_id(),
.params = params,
};
@@ -444,46 +759,91 @@ void conv_ai_service_task(void *pvParameters)
aios_event_pub(VOLC_LOCAL_LOGIC_PLAY_WELCOME, NULL, NULL);
#endif
ret = volc_start(conv_service.engine, &opt);
volc_osal_free(params);
if (ret != 0) {
LOGE("volc_start failed, ret: %d", ret);
status_str = "ai对话连接失败,准备退出";
goto CONV_AI_QUIT;
}
provider_started = true;
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS, VOLC_DISPLAY_TEXT, "ai对话已连接");
call_idle_activity();
if (stop_requested || !platform_conversation_audio_authorized()) goto CONV_AI_QUIT;
// A successful start request does not prove that the media transport joined.
observe_call_connection(display_generation);
is_stop = false;
while (!is_stop && !provider_disconnected) {
while (!is_stop && !stop_requested && !provider_disconnected && platform_conversation_audio_authorized()) {
observe_call_connection(display_generation);
if(platform_runtime_rtc()&&atomic_load(&rtc_connected))session_started_reported=true;
process_platform_reports();
if (session_started_reported && platform_conversation_poll()) {
status_str = "平台已结束本次对话";
if (voice_end_ready()) {
LOGI("VOICE_END user request completed");
status_str = "本次对话已结束";
atomic_store(&user_stop_requested, true);
display_stop = CALL_DISPLAY_STOP_NORMAL;
goto CONV_AI_QUIT;
}
if (diagnostic_say_pending && session_started_reported) {
diagnostic_say_pending = false;
if ((platform_runtime_rtc()||session_started_reported) && platform_conversation_poll()) {
status_str = "平台已结束本次对话";
display_stop = atomic_load(&display_connected) ? CALL_DISPLAY_STOP_NORMAL : CALL_DISPLAY_STOP_FAILED;
goto CONV_AI_QUIT;
}
if ((atomic_load(&diagnostic_state) & DIAGNOSTIC_PENDING_MASK) != DIAGNOSTIC_NONE && session_started_reported && (!platform_runtime_rtc() || volc_current_user_id(conv_service.engine,true))) {
unsigned diagnostic = diagnostic_take_pending();
if (diagnostic == DIAGNOSTIC_TTS) {
diagnostic_send_tts();
} else if (diagnostic == DIAGNOSTIC_SAY) {
char request[640];
snprintf(request, sizeof(request), "{\"event_id\":\"diag-%llu\",\"type\":\"conversation.item.create\",\"item\":{\"type\":\"message\",\"role\":\"user\",\"content\":[{\"type\":\"input_text\",\"text\":\"请只说这句话:你好,这是语音播放测试。现在使用自然的语速,连续清楚地说完这句话。\"}],\"interrupt_mode\":1}}", (unsigned long long)volc_osal_get_time_ms());
volc_message_info_t info = {.is_binary = false};
int sent = volc_send_message(conv_service.engine, request, strlen(request), &info);
int sent = platform_runtime_rtc()
? volc_send_text_to_agent(conv_service.engine,"请只说这句话:你好,这是语音播放测试。",VOLC_AGENT_TYPE_LLM,1)
: volc_send_message(conv_service.engine, request, strlen(request), &info);
LOGI("PLAYBACK diagnostic sentence requested ret=%d", sent);
const char *respond = "{\"type\":\"response.create\",\"response\":{\"modalities\":[\"text\",\"audio\"]}}";
volc_send_message(conv_service.engine, respond, strlen(respond), &info);
if (!platform_runtime_rtc()) volc_send_message(conv_service.engine, respond, strlen(respond), &info);
conv_service.wait_time = 0;
call_idle_activity();
}
}
sleep(1);
conv_service.wait_time++;
if(conv_service.wait_time >= CONV_SERVICE_TIMEOUT){
if (call_idle_expired()) {
LOGI("CALL_IDLE timeout seconds=%d", CONV_SERVICE_TIMEOUT);
status_str = "ai对话已超时,准备退出";
display_stop = atomic_load(&display_connected) ? CALL_DISPLAY_STOP_NORMAL : CALL_DISPLAY_STOP_FAILED;
goto CONV_AI_QUIT;
}
}
status_str = "ai对话已断开";
display_stop = atomic_load(&provider_disconnected) ? CALL_DISPLAY_STOP_INTERRUPTED :
atomic_load(&display_connected) ? CALL_DISPLAY_STOP_NORMAL : CALL_DISPLAY_STOP_FAILED;
CONV_AI_QUIT:
// Latch a connection that arrived immediately before hangup, without
// publishing a misleading new TALKING frame while teardown has started.
if (atomic_load(&display_connected)) call_display_connected(&call_display, display_generation);
is_stop = true;
// volc_hal_file_close(audioFile);
volc_stop(conv_service.engine);
stop_requested = true;
diagnostic_close();
call_display_stopping(&call_display, display_generation);
publish_call_display(display_generation);
// Local privacy boundary precedes SDK shutdown and all HTTP requests.
if (g_hal_context && audio_started) {
local_audio_stopped = volc_hal_capture_stop(g_hal_context->capture_handle[VOLC_HAL_CAPTURE_AUDIO]) == 0;
if (g_hal_context->player_handle[VOLC_HAL_PLAYER_AUDIO]) {
volc_hal_player_discard_pending(g_hal_context->player_handle[VOLC_HAL_PLAYER_AUDIO]);
if (volc_hal_player_stop(g_hal_context->player_handle[VOLC_HAL_PLAYER_AUDIO]) != 0) local_audio_stopped = false;
}
}
int stop_result=volc_stop(conv_service.engine);
platform_conversation_stop_result(provider_started,stop_result);
if (atomic_load(&user_stop_requested)) display_stop = CALL_DISPLAY_STOP_NORMAL;
if (provider_started && stop_result != 0) display_stop = CALL_DISPLAY_STOP_INTERRUPTED;
// Show the local terminal state before an end-report HTTP request can block.
call_display_finished(&call_display, display_generation, display_stop,
local_audio_stopped, volc_osal_get_time_ms());
publish_call_display(display_generation);
// The SDK may retain opt.params until stop has released the session.
if (params) { volc_osal_free(params); params = NULL; }
process_platform_reports();
platform_conversation_finish(!provider_started || !session_started_reported,
!provider_started ? "VOLC_START_FAILED" : (!session_started_reported ? "SESSION_UPDATE_NOT_CONFIRMED" : NULL));
@@ -497,25 +857,40 @@ CONV_AI_QUIT:
}
}
if(status_str != NULL){
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_STATUS, VOLC_DISPLAY_TEXT, status_str);
}
if (status_str != NULL) LOGI("Call finished: %s", status_str);
volc_hal_display_set_content(global_display, VOLC_DISPLAY_OBJ_SUBTITLE, VOLC_DISPLAY_TEXT, "");
conv_service.wait_time = 0;
// just show the status: Ai对话已断开
sleep(1);
// No detached timer: the old task owns this brief terminal display. Restore
// idle before publishing OVER, so a new call can never be overwritten by an
// old timeout. Slow end-report requests already count toward the hold time.
if (local_audio_stopped) {
while (!call_display_idle_if_due(&call_display, display_generation, volc_osal_get_time_ms())) {
vTaskDelay(pdMS_TO_TICKS(50));
}
publish_call_display(display_generation);
}
if (g_hal_context && audio_started) {
volc_hal_capture_start(g_hal_context->capture_handle[VOLC_HAL_CAPTURE_AUDIO],VOLC_AUDIO_MODE_WAKEUP);
}
aios_event_pub(VOLC_SERVICE_AI_CONVERSATION_OVER, NULL, NULL);
#ifdef ESP_PLATFORM
// The manager releases this WithCaps task from another task's context.
// Plain vTaskDelete(NULL) does not free WithCaps stacks/TCBs.
for (;;) vTaskSuspend(NULL);
#else
volc_osal_thread_exit(NULL);
#endif
}
void conv_ai_service_task_stop(void)
{
// sleep for AI Agent say goodbye
sleep(1);
atomic_store(&user_stop_requested, true);
stop_requested = true;
is_stop = true;
diagnostic_close();
// Capture callbacks drop frames immediately. The independent audio worker
// closes its feeder through the authorization gate, including blocked HTTP.
// Only the conversation task destroys handles; hook callbacks never race it.
}
#if __cplusplus
@@ -11,6 +11,11 @@
#include "volc_service_common.h"
#include "volc_conv_service.h"
#include "volc_conv.h"
#ifdef ESP_PLATFORM
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#endif
extern void conv_ai_service_diagnostic_reset(void);
#if __cplusplus
extern "C" {
@@ -31,6 +36,14 @@ static aios_ret_t __state_conversation(volc_conv_service_manager_t * const me, a
}
volc_hal_display_t global_display = g_hal_context->display_handle;
switch (e->id) {
case VOLC_SERVICE_AI_CONVERSATION_TOGGLE:
// This manager owns admission. Connecting and stopping tasks remain
// active until OVER, so repeated taps cannot create another call.
if (me->conv_thread_id != NULL) {
conv_ai_service_task_stop();
return AIOS_Ret_Handled;
}
/* fall through */
case VOLC_SERVICE_AI_CONVERSATION_START:
if (me->conv_thread_id == NULL) {
volc_osal_thread_param_t param = {0};
@@ -39,7 +52,10 @@ static aios_ret_t __state_conversation(volc_conv_service_manager_t * const me, a
param.stack_size = 8*1024;
param.priority = 5;
param.stack_in_ext = 1;
volc_osal_thread_create(&me->conv_thread_id, &param, conv_ai_service_task,NULL);
conv_ai_service_prepare_task();
if (volc_osal_thread_create(&me->conv_thread_id, &param, conv_ai_service_task,NULL) != 0) {
conv_ai_service_diagnostic_reset();
}
}
return AIOS_Ret_Handled;
case VOLC_SERVICE_AI_CONVERSATION_INTERRUPT:
@@ -48,12 +64,20 @@ static aios_ret_t __state_conversation(volc_conv_service_manager_t * const me, a
case VOLC_SERVICE_AI_CONVERSATION_QUIT:
if (me->conv_thread_id != NULL) {
conv_ai_service_task_stop();
me->conv_thread_id = NULL;
// The old task owns SDK teardown and reports OVER after cleanup.
// Keep admission closed until then, including rapid hook toggles.
}
return AIOS_Ret_Handled;
// notice the Ai_Conversation over by task
case VOLC_SERVICE_AI_CONVERSATION_OVER:
if (me->conv_thread_id != NULL) {
#ifdef ESP_PLATFORM
// OSAL stores the native handle in an allocated handle slot.
// OVER is the worker's final action; reclaim its WithCaps
// stack and TCB before admitting another conversation.
vTaskDeleteWithCaps(*(TaskHandle_t *)me->conv_thread_id);
#endif
volc_osal_thread_destroy(me->conv_thread_id);
me->conv_thread_id = NULL;
}
return AIOS_Ret_Handled;
@@ -65,6 +89,7 @@ static aios_ret_t __state_conversation(volc_conv_service_manager_t * const me, a
static aios_ret_t __state_init(volc_conv_service_manager_t * const me, aios_event_t const * const e)
{
AIOS_EVENT_SUB(VOLC_SERVICE_AI_CONVERSATION_START); // 订阅事件
AIOS_EVENT_SUB(VOLC_SERVICE_AI_CONVERSATION_TOGGLE);
AIOS_EVENT_SUB(VOLC_SERVICE_AI_CONVERSATION_INTERRUPT); // 订阅事件
AIOS_EVENT_SUB(VOLC_SERVICE_AI_CONVERSATION_QUIT); // 订阅事件
AIOS_EVENT_SUB(VOLC_SERVICE_AI_CONVERSATION_OVER); // 订阅事件
@@ -14,6 +14,7 @@ enum {
VOLC_SERVICE_AI_CONVERSATION_INTERRUPT = 101,
VOLC_SERVICE_AI_CONVERSATION_QUIT = 102, // control the Ai_Conversation task quit
VOLC_SERVICE_AI_CONVERSATION_OVER = 103, // notice the Ai_Conversation over by task
VOLC_SERVICE_AI_CONVERSATION_TOGGLE = 104, // screen tap: start idle or stop active
// function_call event:
VOLC_FUNCTION_CALL_EXEC = 200,
@@ -21,7 +21,6 @@ typedef struct volc_local_logic_service {
} volc_local_logic_service_t;
volc_local_logic_service_t g_volc_local_logic_service;
static int has_quit = 0;
static void __play_welcome(){
volc_hal_context_t* g_hal_context = volc_get_global_hal_context();
if(g_hal_context == NULL) return;
@@ -42,50 +41,20 @@ static void __play_welcome(){
return;
}
static int __has_substring(const char *haystack, const char *needle) {
// 处理边界:子串为空串,按惯例返回包含(可根据需求调整)
if (needle[0] == '\0') {
return 1;
}
// strstr 返回非 NULL 表示找到子串
return strstr(haystack, needle) != NULL ? 1 : 0;
}
static void __parser_and_process_subtitle(char *subtitle)
{
volc_conv_service_t conv_service = get_conv_ai_service();
if (__has_substring(subtitle, "退下吧") || __has_substring(subtitle, "拜拜"))
{
if (!has_quit)
{
volc_send_text_to_agent(conv_service.engine, "好的拜拜", VOLC_AGENT_TYPE_TTS, 1);
has_quit = 1;
}
}
if (__has_substring(subtitle, "好的拜拜"))
{
aios_event_pub(VOLC_SERVICE_AI_CONVERSATION_QUIT, NULL, NULL);
has_quit = 0;
}
}
static aios_ret_t __state_conversation(volc_local_logic_service_t * const me, aios_event_t const * const e)
{
switch (e->id) {
case VOLC_LOCAL_LOGIC_PLAY_WELCOME:
__play_welcome();
return AIOS_Ret_Handled;
case VOLC_LOCAL_LOGIC_PROCESS_SUBTITLE:
__parser_and_process_subtitle(e->data);
return AIOS_Ret_Handled;
}
}
static aios_ret_t __state_init(volc_local_logic_service_t * const me, aios_event_t const * const e)
{
AIOS_EVENT_SUB(VOLC_LOCAL_LOGIC_PLAY_WELCOME); // 订阅事件
AIOS_EVENT_SUB(VOLC_LOCAL_LOGIC_PROCESS_SUBTITLE); // 订阅事件
// Voice ending is handled by the conversation task with speaker identity,
// complete user transcripts and playback drain; never by substring events.
return AIOS_TRAN(__state_conversation);
}