244 lines
15 KiB
JavaScript
244 lines
15 KiB
JavaScript
// Emit a standalone host TU containing the actual production state machine,
|
|||
|
|
// SDK event/status callbacks and conversation task. No device or cloud access.
|
||
|
|
const fs = require('node:fs'), path = require('node:path');
|
||
|
|
const root = path.resolve(__dirname, '..');
|
||
|
|
const inc = fs.readFileSync(path.join(root, 'components/application/service/conv_service/inc/call_display_state.h'), 'utf8');
|
||
|
|
const state = fs.readFileSync(path.join(root, 'components/application/service/conv_service/src/call_display_state.c'), 'utf8').replace('#include "call_display_state.h"', '');
|
||
|
|
const task = fs.readFileSync(path.join(root, 'components/application/service/conv_service/src/volc_conv.c'), 'utf8');
|
||
|
|
const voiceHeader = fs.readFileSync(path.join(root, 'components/application/service/conv_service/inc/call_voice_end.h'), 'utf8').replace('#pragma once', '');
|
||
|
|
const voiceState = fs.readFileSync(path.join(root, 'components/application/service/conv_service/src/call_voice_end.c'), 'utf8').replace('#include "call_voice_end.h"', '');
|
||
|
|
function section(first, next) {
|
||
|
|
const start = task.indexOf(first), end = task.indexOf(next, start + first.length);
|
||
|
|
if (start < 0 || end < 0) throw new Error(`production section missing: ${first}`);
|
||
|
|
return task.slice(start, end);
|
||
|
|
}
|
||
|
|
process.stdout.write(inc + '\n' + state + '\n' + voiceHeader + '\n' + voiceState + '\n' + String.raw`
|
||
|
|
#include <assert.h>
|
||
|
|
#include <stdio.h>
|
||
|
|
#include <stdlib.h>
|
||
|
|
#include <string.h>
|
||
|
|
#include <stdatomic.h>
|
||
|
|
typedef void *volc_engine_t;
|
||
|
|
typedef void *volc_hal_display_t;
|
||
|
|
typedef void *volc_hal_capture_t;
|
||
|
|
typedef struct {int code;} volc_event_t;
|
||
|
|
typedef enum {VOLC_CONV_STATUS_LISTENING,VOLC_CONV_STATUS_THINKING,VOLC_CONV_STATUS_ANSWERING,VOLC_CONV_STATUS_INTERRUPTED,VOLC_CONV_STATUS_ANSWER_FINISH} volc_conv_status_e;
|
||
|
|
typedef struct {void *display_handle,*capture_handle[1],*player_handle[1];} volc_hal_context_t;
|
||
|
|
typedef struct {int media_type;void (*data_cb)(void);void (*audio_wakeup_cb)(void*,void*);void *user_data;} volc_hal_capture_config_t;
|
||
|
|
typedef struct {int type;} esp_gmf_afe_evt_t;
|
||
|
|
#define ESP_GMF_AFE_EVT_VAD_START 1
|
||
|
|
#define ESP_GMF_AFE_EVT_VAD_END 2
|
||
|
|
typedef struct {int media_type;} volc_hal_player_config_t;
|
||
|
|
typedef struct {int mode;char *bot_id,*params;} volc_opt_t;
|
||
|
|
typedef struct {bool is_binary;} volc_message_info_t;
|
||
|
|
typedef struct {void *engine;int wait_time;} volc_conv_service_t;
|
||
|
|
#define VOLC_EV_CONNECTED 1
|
||
|
|
#define VOLC_EV_DISCONNECTED 2
|
||
|
|
#define VOLC_DISPLAY_OBJ_STATUS 0
|
||
|
|
#define VOLC_DISPLAY_OBJ_MAIN 1
|
||
|
|
#define VOLC_DISPLAY_OBJ_SUBTITLE 2
|
||
|
|
#define VOLC_DISPLAY_TEXT 0
|
||
|
|
#define VOLC_DISPLAY_IMAGE 1
|
||
|
|
#define MMAP_EAF_HAPPY_EAF 0
|
||
|
|
#define MMAP_EAF_EMOTION_BLINK_FAST_284_126_EAF 1
|
||
|
|
#define MMAP_EAF_EMOTION_DIZZY_284_126_EAF 2
|
||
|
|
#define VOLC_HAL_PLAYER_AUDIO 0
|
||
|
|
#define VOLC_HAL_CAPTURE_AUDIO 0
|
||
|
|
#define VOLC_MEDIA_TYPE_AUDIO 0
|
||
|
|
#define VOLC_AUDIO_MODE_CAPTURE 0
|
||
|
|
#define VOLC_AUDIO_MODE_WAKEUP 1
|
||
|
|
#define VOLC_MODE_RTC 1
|
||
|
|
#define VOLC_MODE_WS 2
|
||
|
|
#define VOLC_AGENT_TYPE_LLM 0
|
||
|
|
#define VOLC_SERVICE_AI_CONVERSATION_OVER 1
|
||
|
|
#define CONV_SERVICE_TIMEOUT 60
|
||
|
|
#define LOGI(...) do { if (0) printf(__VA_ARGS__); } while (0)
|
||
|
|
#define LOGE(...) do { if (0) printf(__VA_ARGS__); } while (0)
|
||
|
|
#define pdMS_TO_TICKS(value) (value)
|
||
|
|
enum {NORMAL,DELAYED_CONNECTION,START_FAILURE,BEGIN_FAILURE,REMOTE_DROP,SLOW_END_ACK,AUDIO_STOP_FAILURE,RESPONSE_FINISHED,TIMEOUT_WITHOUT_CONNECTION,IDLE_CONNECTED,ACTIVE_LONG_CALL,VOICE_GOODBYE,VOICE_RETRACT,VOICE_MISSING_REPLY};
|
||
|
|
static int scenario,polls,frames,ended_frames,interrupted_frames,failed_frames,ending_frames,talking_frames,over_count;
|
||
|
|
static uint64_t now_ms;
|
||
|
|
static char text[64];
|
||
|
|
static bool audio_stopped,output_stopped,sdk_stopped;
|
||
|
|
static atomic_bool is_stop=true,stop_requested=false,provider_disconnected=false,rtc_connected=false,display_connected=false,user_stop_requested=false;
|
||
|
|
static call_display_state_t call_display;
|
||
|
|
static volc_hal_display_t global_display;
|
||
|
|
static bool session_started_reported,diagnostic_say_pending;
|
||
|
|
static call_voice_end_t voice_end;
|
||
|
|
static atomic_uint voice_finish_serial,last_voice_received_ms;
|
||
|
|
typedef struct {double valuedouble;} cJSON;
|
||
|
|
static cJSON *cJSON_GetObjectItemCaseSensitive(const cJSON *j,const char *k){(void)j;(void)k;return NULL;}
|
||
|
|
static const char *cJSON_GetStringValue(const cJSON *j){(void)j;return NULL;}
|
||
|
|
static bool cJSON_IsNumber(const cJSON *j){return j!=NULL;}
|
||
|
|
static bool cJSON_IsTrue(const cJSON *j){(void)j;return false;}
|
||
|
|
static void *platform_report_queue;
|
||
|
|
static int xQueueReceive(void *q,cJSON **out,unsigned ticks){(void)q;(void)out;(void)ticks;return 0;}
|
||
|
|
static void cJSON_Delete(cJSON *root){(void)root;}
|
||
|
|
#define pdTRUE 1
|
||
|
|
enum { DIAGNOSTIC_NONE, DIAGNOSTIC_SAY, DIAGNOSTIC_TTS, DIAGNOSTIC_PENDING_MASK=3 };
|
||
|
|
static atomic_uint diagnostic_state;
|
||
|
|
static void diagnostic_prepare(void){}
|
||
|
|
static void diagnostic_close(void){}
|
||
|
|
static unsigned diagnostic_take_pending(void){return 0;}
|
||
|
|
static void diagnostic_send_tts(void){}
|
||
|
|
static unsigned uxTaskPriorityGet(void *task){(void)task;return 5;}
|
||
|
|
static int xTaskGetCoreID(void *task){(void)task;return 0;}
|
||
|
|
static volc_conv_service_t conv_service;
|
||
|
|
static volc_hal_context_t hal={(void*)1,{(void*)2},{(void*)3}};
|
||
|
|
static void __on_volc_event(volc_engine_t,volc_event_t*,void*);
|
||
|
|
static void __on_volc_conversation_status(volc_engine_t,volc_conv_status_e,void*);
|
||
|
|
void conv_ai_service_task_stop(void);
|
||
|
|
static uint64_t volc_osal_get_time_ms(void){return now_ms;}
|
||
|
|
` + section('/* CALL_IDLE_BEGIN:', '/* CALL_IDLE_END */') + String.raw`
|
||
|
|
static void vTaskDelay(unsigned ms){now_ms+=ms;}
|
||
|
|
static void sleep(unsigned seconds){now_ms+=seconds*1000;}
|
||
|
|
static void volc_osal_thread_exit(void *unused){(void)unused;}
|
||
|
|
static void *volc_osal_malloc(size_t size){return malloc(size);}
|
||
|
|
static void volc_osal_free(void *p){free(p);}
|
||
|
|
static volc_hal_context_t *volc_get_global_hal_context(void){return &hal;}
|
||
|
|
static int volc_hal_display_set_content(void *display,int obj,int type,const void *content){
|
||
|
|
(void)display;(void)type;
|
||
|
|
if(obj!=VOLC_DISPLAY_OBJ_STATUS)return 0;
|
||
|
|
snprintf(text,sizeof(text),"%s",(const char*)content);frames++;
|
||
|
|
if(!strcmp(text,"通话中")){assert(atomic_load(&display_connected));talking_frames++;}
|
||
|
|
if(!strcmp(text,"正在结束"))ending_frames++;
|
||
|
|
if(!strcmp(text,"通话结束")||!strcmp(text,"连接失败")||!strcmp(text,"连接中断")){
|
||
|
|
assert(audio_stopped&&output_stopped); // never claim ended before local shutdown
|
||
|
|
if(!strcmp(text,"通话结束"))ended_frames++;
|
||
|
|
if(!strcmp(text,"连接失败"))failed_frames++;
|
||
|
|
if(!strcmp(text,"连接中断"))interrupted_frames++;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
static void platform_conversation_observe(const char *type){(void)type;}
|
||
|
|
static bool platform_runtime_rtc(void){return true;}
|
||
|
|
static bool platform_conversation_audio_authorized(void){return true;}
|
||
|
|
static int platform_conversation_begin(void){return scenario==BEGIN_FAILURE?-1:0;}
|
||
|
|
static const char *platform_last_error_code(void){return "";}
|
||
|
|
static const char *call_client_start_error(void){return "";}
|
||
|
|
static const char *platform_conversation_update_json(void){return "{}";}
|
||
|
|
static const char *platform_conversation_bot_id(void){return "test";}
|
||
|
|
static const char *volc_current_user_id(void *engine,bool remote){(void)engine;(void)remote;return "test";}
|
||
|
|
static void process_platform_reports(void){}
|
||
|
|
static void platform_conversation_stop_result(bool started,int result){(void)started;(void)result;}
|
||
|
|
static void platform_conversation_finish(bool failed,const char *error){
|
||
|
|
(void)failed;(void)error;
|
||
|
|
if(scenario==AUDIO_STOP_FAILURE){assert(!strcmp(text,"正在结束"));return;}
|
||
|
|
assert(ended_frames+failed_frames+interrupted_frames>0); // visible before HTTP ACK
|
||
|
|
assert(sdk_stopped);
|
||
|
|
if(scenario==SLOW_END_ACK)now_ms+=5000;
|
||
|
|
}
|
||
|
|
static bool conversation_output_allowed(void){return !stop_requested;}
|
||
|
|
static bool volc_hal_player_voice_quiet(void *player,unsigned ms){(void)player;(void)ms;return true;}
|
||
|
|
` + section('/* Called only by the conversation task,', '/* HTTPS reporting') + String.raw`
|
||
|
|
static void __audio_capture_cb(void){}
|
||
|
|
static void *volc_hal_capture_create(const volc_hal_capture_config_t *cfg){(void)cfg;return (void*)2;}
|
||
|
|
static int volc_hal_capture_stop(void *capture){(void)capture;if(stop_requested&&scenario==AUDIO_STOP_FAILURE)return -1;audio_stopped=true;return 0;}
|
||
|
|
static int volc_hal_capture_update_config(void *capture,const volc_hal_capture_config_t *cfg){(void)capture;(void)cfg;return 0;}
|
||
|
|
static int volc_hal_capture_start(void *capture,int mode){(void)capture;if(mode==VOLC_AUDIO_MODE_CAPTURE)audio_stopped=false;return 0;}
|
||
|
|
static void *volc_hal_player_create(const volc_hal_player_config_t *cfg){(void)cfg;return (void*)3;}
|
||
|
|
static void volc_hal_player_set_authorization_gate(void *player,bool(*gate)(void)){(void)player;(void)gate;}
|
||
|
|
static void volc_hal_player_start(void *player){(void)player;output_stopped=false;}
|
||
|
|
static int volc_hal_player_stop(void *player){(void)player;output_stopped=true;return 0;}
|
||
|
|
static void volc_hal_player_discard_pending(void *player){(void)player;}
|
||
|
|
static void volc_hal_player_destroy(void *player){(void)player;}
|
||
|
|
static int volc_start(void *engine,const volc_opt_t *opt){
|
||
|
|
(void)engine;(void)opt;
|
||
|
|
if(scenario==START_FAILURE)return -1;
|
||
|
|
if(scenario!=DELAYED_CONNECTION&&scenario!=TIMEOUT_WITHOUT_CONNECTION){volc_event_t event={VOLC_EV_CONNECTED};__on_volc_event(NULL,&event,NULL);}
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
static int volc_stop(void *engine){(void)engine;sdk_stopped=true;return 0;}
|
||
|
|
static int platform_conversation_poll(void){
|
||
|
|
polls++;
|
||
|
|
if(scenario==VOICE_GOODBYE||scenario==VOICE_RETRACT||scenario==VOICE_MISSING_REPLY){
|
||
|
|
if(polls==1)process_voice_end_transcript("subject","就聊到这吧",true);
|
||
|
|
if(scenario==VOICE_GOODBYE&&polls==2)__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_ANSWER_FINISH,NULL);
|
||
|
|
if(scenario==VOICE_RETRACT&&polls==2)process_voice_end_transcript("subject","等等,我还想聊",true);
|
||
|
|
if(scenario==VOICE_RETRACT&&polls==3){assert(!voice_end.pending);conv_ai_service_task_stop();}
|
||
|
|
assert(polls<15);return 0;
|
||
|
|
}
|
||
|
|
if(scenario==IDLE_CONNECTED){
|
||
|
|
__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_LISTENING,NULL);
|
||
|
|
return 0; // Persistent listening + cloud renewals must still time out.
|
||
|
|
}
|
||
|
|
if(scenario==ACTIVE_LONG_CALL){
|
||
|
|
if(polls<=150&&polls%30==1)call_idle_activity();
|
||
|
|
return 0; // Activity keeps a call alive beyond 60s, then silence ends it.
|
||
|
|
}
|
||
|
|
if(scenario==TIMEOUT_WITHOUT_CONNECTION){assert(!talking_frames);return 0;}
|
||
|
|
if(scenario==DELAYED_CONNECTION){assert(!talking_frames);assert(!strcmp(text,"正在连接"));}
|
||
|
|
if(scenario==REMOTE_DROP){volc_event_t event={VOLC_EV_DISCONNECTED};__on_volc_event(NULL,&event,NULL);}
|
||
|
|
else if(scenario==RESPONSE_FINISHED&&polls==1){
|
||
|
|
__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_ANSWER_FINISH,NULL);
|
||
|
|
assert(!stop_requested);assert(!strcmp(text,"通话中"));assert(!ended_frames);
|
||
|
|
}else conv_ai_service_task_stop();
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
static int volc_send_text_to_agent(void *engine,const char *text,int type,int mode){(void)engine;(void)text;(void)type;(void)mode;return 0;}
|
||
|
|
static int volc_send_message(void *engine,const char *data,size_t len,const volc_message_info_t *info){(void)engine;(void)data;(void)len;(void)info;return 0;}
|
||
|
|
static void aios_event_pub(int type,void *a,void *b){
|
||
|
|
(void)a;(void)b;assert(type==VOLC_SERVICE_AI_CONVERSATION_OVER);over_count++;
|
||
|
|
if(scenario!=AUDIO_STOP_FAILURE)assert(!strcmp(text,"待机"));
|
||
|
|
}
|
||
|
|
` + section('static void publish_call_display(', '\nstatic bool conversation_output_allowed(')
|
||
|
|
+ section('static void __on_volc_event(', '\nstatic void __on_volc_audio_data(')
|
||
|
|
+ section('void conv_ai_service_prepare_task(', '\n#if __cplusplus') + String.raw`
|
||
|
|
static void reset(int next){
|
||
|
|
scenario=next;polls=frames=ended_frames=interrupted_frames=failed_frames=ending_frames=talking_frames=over_count=0;
|
||
|
|
now_ms=10000;text[0]=0;audio_stopped=output_stopped=true;sdk_stopped=false;
|
||
|
|
memset(&call_display,0,sizeof(call_display));memset(&conv_service,0,sizeof(conv_service));
|
||
|
|
hal.player_handle[0]=(void*)3;global_display=hal.display_handle;diagnostic_say_pending=false;
|
||
|
|
conv_ai_service_prepare_task();
|
||
|
|
}
|
||
|
|
int main(void){
|
||
|
|
// State transitions are fenced by generation, even if a former terminal
|
||
|
|
// deadline or delayed CONNECTED observation is delivered after a new begin.
|
||
|
|
call_display_state_t state={0};unsigned first=call_display_begin(&state);
|
||
|
|
call_display_connected(&state,first);call_display_stopping(&state,first);
|
||
|
|
call_display_finished(&state,first,CALL_DISPLAY_STOP_NORMAL,false,100);
|
||
|
|
assert(state.phase==CALL_DISPLAY_ENDING);
|
||
|
|
call_display_finished(&state,first,CALL_DISPLAY_STOP_NORMAL,true,100);
|
||
|
|
assert(!call_display_idle_if_due(&state,first,2099));
|
||
|
|
unsigned next=call_display_begin(&state);
|
||
|
|
assert(!call_display_idle_if_due(&state,first,9999));call_display_connected(&state,first);
|
||
|
|
assert(state.phase==CALL_DISPLAY_CONNECTING);call_display_connected(&state,next);assert(state.phase==CALL_DISPLAY_TALKING);
|
||
|
|
for(int test=NORMAL;test<=VOICE_MISSING_REPLY;test++){
|
||
|
|
reset(test);conv_ai_service_task(NULL);assert(over_count==1);assert(ending_frames);
|
||
|
|
if(test==START_FAILURE||test==BEGIN_FAILURE||test==TIMEOUT_WITHOUT_CONNECTION){assert(failed_frames&& !talking_frames && !ended_frames);}
|
||
|
|
else if(test==REMOTE_DROP){assert(interrupted_frames&&!ended_frames);}
|
||
|
|
else if(test==AUDIO_STOP_FAILURE){assert(!ended_frames&&!failed_frames&&!interrupted_frames);}
|
||
|
|
else{assert(ended_frames&&!failed_frames&&!interrupted_frames);}
|
||
|
|
if(test!=AUDIO_STOP_FAILURE){assert(call_display.phase==CALL_DISPLAY_IDLE);assert(now_ms>=12000);}
|
||
|
|
if(test==IDLE_CONNECTED||test==TIMEOUT_WITHOUT_CONNECTION)assert(polls==60);
|
||
|
|
if(test==ACTIVE_LONG_CALL)assert(polls==180);
|
||
|
|
if(test==VOICE_GOODBYE)assert(polls==2&&atomic_load(&user_stop_requested));
|
||
|
|
if(test==VOICE_RETRACT)assert(polls==3);
|
||
|
|
if(test==VOICE_MISSING_REPLY)assert(polls==12&&atomic_load(&user_stop_requested));
|
||
|
|
// Late supplier turn callbacks after teardown cannot overwrite idle/end.
|
||
|
|
char before[sizeof(text)];strcpy(before,text);
|
||
|
|
__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_ANSWER_FINISH,NULL);
|
||
|
|
__on_volc_conversation_status(NULL,VOLC_CONV_STATUS_LISTENING,NULL);
|
||
|
|
assert(!strcmp(before,text));
|
||
|
|
}
|
||
|
|
// Actual VAD callback records both edges, and starts a new quiet window.
|
||
|
|
int16_t quiet[320]={0}, comfort[320], speech[320];
|
||
|
|
for(int i=0;i<320;i++){comfort[i]=(i&1)?-20:20;speech[i]=(i&1)?-500:500;}
|
||
|
|
assert(!call_idle_pcm_active(quiet,sizeof(quiet)));
|
||
|
|
assert(!call_idle_pcm_active(comfort,sizeof(comfort)));
|
||
|
|
assert(call_idle_pcm_active(speech,sizeof(speech)));
|
||
|
|
assert(!call_idle_pcm_active(NULL,640)&&!call_idle_pcm_active(speech,639));
|
||
|
|
reset(NORMAL);
|
||
|
|
esp_gmf_afe_evt_t vad={ESP_GMF_AFE_EVT_VAD_START};
|
||
|
|
now_ms+=59000;call_idle_capture_event(&vad,NULL);
|
||
|
|
assert(atomic_load(&local_speech_active)&&!call_idle_expired());
|
||
|
|
vad.type=ESP_GMF_AFE_EVT_VAD_END;now_ms+=90000;call_idle_capture_event(&vad,NULL);
|
||
|
|
assert(!atomic_load(&local_speech_active)&&!call_idle_expired());
|
||
|
|
now_ms+=59999;assert(!call_idle_expired());now_ms++;assert(call_idle_expired());
|
||
|
|
now_ms=UINT32_MAX-1000ull;call_idle_activity();
|
||
|
|
now_ms+=59999;assert(!call_idle_expired());now_ms++;assert(call_idle_expired());
|
||
|
|
puts("PASS call display + idle: actual task/callbacks, connected-only talking, local shutdown before terminal, start failure, provider drop, answer-finish continuity, slow end ACK, failed local stop, 60s quiet timeout, 180s activity call, VAD edges, silence/comfort PCM rejection, clock wrap, generation-fenced hold");
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
`);
|