Files
story/firmware/echoear-platform-agent/tests/voice-end-host.cjs
T

106 lines
8.5 KiB
JavaScript
Raw Normal View History

// Actual command recognizer, message/report parsing and stop-readiness boundary.
const fs = require('node:fs'), path = require('node:path');
const root = path.resolve(__dirname, '..');
const read = p => fs.readFileSync(path.join(root, p), 'utf8');
const src = read('components/application/service/conv_service/src/volc_conv.c');
const json = path.join('E:/Codex/toolchains/esp-idf-v5.5.1/components/json/cJSON');
function section(start, end) {
const a = src.indexOf(start), b = src.indexOf(end, a + start.length);
if (a < 0 || b < 0) throw new Error('Missing production section ' + start);
return src.slice(a, b);
}
process.stdout.write(read('components/application/service/conv_service/inc/call_voice_end.h').replace('#pragma once', '') + '\n' +
read('components/application/service/conv_service/src/call_voice_end.c').replace('#include "call_voice_end.h"', '') + '\n' +
fs.readFileSync(path.join(json, 'cJSON.h'), 'utf8') + '\n' +
fs.readFileSync(path.join(json, 'cJSON.c'), 'utf8').replace('#include "cJSON.h"', '') + String.raw`
#include <assert.h>
#include <stdatomic.h>
#include <stdio.h>
typedef void *volc_engine_t;
typedef void *volc_hal_player_t;
typedef struct {int unused;} volc_message_info_t;
typedef struct {void *player_handle[1];} volc_hal_context_t;
static volc_hal_context_t hal={{(void*)1}};
static struct {void *engine;int wait_time;} conv_service;
static bool is_stop,stop_requested,session_started_reported,rtc=true,player_quiet=true;
static call_voice_end_t voice_end;
static atomic_uint voice_finish_serial,last_voice_received_ms;
static atomic_bool local_speech_active;
static uint64_t now_ms;
static cJSON *queue[8];static unsigned head,tail,turns,round_id;
static void *platform_report_queue=(void*)1;
#define pdTRUE 1
#define VOLC_HAL_PLAYER_AUDIO 0
#define VOLC_FUNCTION_CALL_EXEC 1
#define VOLC_FUNCTION_CALL_TRIGGER 2
#define LOGI(...) ((void)0)
#define LOGE(...) ((void)0)
static uint64_t volc_osal_get_time_ms(void){return now_ms;}
static bool platform_runtime_rtc(void){return rtc;}
static volc_hal_context_t *volc_get_global_hal_context(void){return &hal;}
static bool volc_hal_player_voice_quiet(void *p,unsigned ms){assert(p&&(ms==600));return player_quiet;}
static void volc_hal_player_play_data(void *p,const void *d,size_t n){(void)p;(void)d;(void)n;}
static void volc_hal_player_finish_segment(void *p){(void)p;}
static void call_idle_activity(void){}
static void platform_conversation_observe(const char *type){(void)type;}
static void __on_subtitle_message_received(const cJSON *j){(void)j;}
static const char *volc_current_user_id(void *e,bool remote){(void)e;return remote?"bot":"user";}
static int platform_conversation_turn(const char *s,const char *id,const char *text){assert(s&&id&&text);turns++;return 0;}
static int platform_conversation_started(const char *ref){assert(ref);return 0;}
static void aios_event_pub(int id,cJSON *j,void *u){(void)id;(void)u;cJSON_Delete(j);}
static int xQueueSend(void *q,cJSON **p,unsigned ticks){(void)q;(void)ticks;if(tail-head==8)return 0;queue[tail++%8]=*p;return pdTRUE;}
static int xQueueReceive(void *q,cJSON **p,unsigned ticks){(void)q;(void)ticks;if(head==tail)return 0;*p=queue[head++%8];return pdTRUE;}
` + section('/* Called only by the conversation task,', '\nvoid conv_ai_service_init()') + String.raw`
static void subtitle(const char *user,const char *text,bool definite,bool paragraph){
cJSON *j=cJSON_CreateObject(),*list=cJSON_AddArrayToObject(j,"data"),*line=cJSON_CreateObject();
cJSON_AddStringToObject(j,"type","subtitle");cJSON_AddItemToArray(list,line);
cJSON_AddStringToObject(line,"userId",user);cJSON_AddStringToObject(line,"text",text);
cJSON_AddNumberToObject(line,"sequence",tail);cJSON_AddBoolToObject(line,"definite",definite);cJSON_AddBoolToObject(line,"paragraph",paragraph);
cJSON_AddNumberToObject(line,"roundId",++round_id);
char *s=cJSON_PrintUnformatted(j);cJSON_Delete(j);char b[1024]="subv0000";
assert(strlen(s)+8<sizeof(b));strcpy(b+8,s);free(s);
__on_volc_message_data(NULL,b,strlen(b),NULL,NULL);
assert(!voice_end.pending); // receive callback never stops or mutates the FSM
process_platform_reports();
}
static void reset(void){call_voice_end_reset(&voice_end);memset(&voice_user_subtitle,0,sizeof(voice_user_subtitle));now_ms=1000;atomic_store(&voice_finish_serial,0);atomic_store(&last_voice_received_ms,0);atomic_store(&local_speech_active,false);is_stop=stop_requested=false;player_quiet=true;}
int main(void){
const char *yes[]={"拜拜吧","拜拜","再见了","就聊到这把","就聊到这吧","今天就聊到这里吧","好了,今天我们就先聊到这儿吧。谢谢你,拜拜!","那就先不聊了","请挂断电话","结束这次聊天吧","我先走了","我要休息了","下次再聊"};
const char *no[]={"不要拜拜","先别挂断","不要结束聊天","他说拜拜","她跟我说再见","你会说拜拜吗","拜拜是什么意思","就聊到这里吗?","\"拜拜\"","‘拜拜’","拜拜?","昨天就聊到这吧","好的拜拜之后继续聊","我还不想结束聊天","不聊了才怪","拜拜,不对,我还想聊","谢谢","今天的天气很好",NULL};
for(size_t i=0;i<sizeof(yes)/sizeof(*yes);i++)assert(call_voice_end_command(yes[i]));
for(size_t i=0;i<sizeof(no)/sizeof(*no);i++)assert(!call_voice_end_command(no[i]));
char huge[2048];memset(huge,'x',sizeof(huge));huge[sizeof(huge)-1]=0;assert(!call_voice_end_command(huge));
reset();subtitle("bot","拜拜吧",true,true);assert(!voice_end.pending);
subtitle("other","拜拜吧",true,true);assert(!voice_end.pending);
subtitle("user","拜拜吧",false,true);assert(!voice_end.pending);
const char *interim="{\"type\":\"subtitle\",\"data\":[{\"userId\":\"user\",\"sequence\":3,\"roundId\":2,\"definite\":false,\"paragraph\":false,\"text\":\"拜拜\"}]}";
for(int i=0;i<100;i++)__on_volc_message_data(NULL,interim,strlen(interim),NULL,NULL);
assert(head==tail&&!voice_end.pending); // partial floods cannot consume control slots
subtitle("user","拜拜吧",true,false);assert(!voice_end.pending);
subtitle("user","拜拜,不对,我还想聊",true,true);assert(!voice_end.pending);
cJSON *part=cJSON_CreateObject();cJSON_AddNumberToObject(part,"roundId",++round_id);cJSON_AddNumberToObject(part,"sequence",1);cJSON_AddBoolToObject(part,"definite",true);cJSON_AddBoolToObject(part,"paragraph",false);cJSON_AddStringToObject(part,"text","你跟我说一句,");
process_voice_end_rtc_user(part);assert(!voice_end.pending);
cJSON_SetNumberValue(cJSON_GetObjectItem(part,"sequence"),2);cJSON_SetBoolValue(cJSON_GetObjectItem(part,"paragraph"),true);cJSON_SetValuestring(cJSON_GetObjectItem(part,"text"),"再见");
process_voice_end_rtc_user(part);assert(!voice_end.pending);cJSON_Delete(part);
subtitle("user","就聊到这把",true,true);assert(voice_end.pending&&!voice_end_ready());
uint32_t deadline=voice_end.requested_ms;now_ms=2000;
call_voice_end_user(&voice_end,"就聊到这把",now_ms,0);assert(voice_end.requested_ms==deadline);
atomic_store(&voice_finish_serial,1);now_ms=2600;player_quiet=false;assert(!voice_end_ready());
player_quiet=true;atomic_store(&last_voice_received_ms,2500);assert(!voice_end_ready());
now_ms=3800;assert(voice_end_ready());atomic_store(&local_speech_active,true);assert(!voice_end_ready());
process_voice_end_transcript("subject","等一下,我还想问个问题",true);assert(!voice_end.pending);
reset();subtitle("user","拜拜",true,true);now_ms=12999;assert(!voice_end_ready());now_ms=13000;assert(voice_end_ready());
reset();subtitle("user","拜拜",true,true);is_stop=true;process_voice_end_transcript("subject","再见",true); // teardown cannot arm another close
call_voice_end_reset(&voice_end);process_voice_end_transcript("subject","拜拜",true);assert(!voice_end.pending);
reset();now_ms=UINT32_MAX-500;call_voice_end_user(&voice_end,"拜拜",now_ms,0);now_ms+=12000;assert(call_voice_end_due(&voice_end,(uint32_t)now_ms,0,false,false));
reset();rtc=false;
const char *msg="{\"type\":\"conversation.item.input_audio_transcription.completed\",\"event_id\":\"e1\",\"transcript\":\"拜拜吧\"}";
__on_volc_message_data(NULL,msg,strlen(msg),NULL,NULL);assert(!voice_end.pending);process_platform_reports();assert(voice_end.pending);
const char *done="{\"type\":\"response.audio.done\"}";
__on_volc_message_data(NULL,done,strlen(done),NULL,NULL);assert(atomic_load(&voice_finish_serial)==1);
now_ms+=2000;assert(voice_end_ready());
puts("PASS voice end: actual RTC/WS parsing, complete user-only commands, negation/quotes/questions, playback wait, cancel, duplicate deadline, 12s fallback, clock wrap, teardown fencing");
return 0;
}
`);