From 39e04982216d85214047e18f60f3cdbb1526b314 Mon Sep 17 00:00:00 2001 From: clement Date: Thu, 18 Jun 2026 21:31:18 +0200 Subject: [PATCH] feat(plip): chain SD scene hint after greeting /debug/ring now takes an optional scene; conversation_arm_incoming locks it and, after the greeting, queues that scene's local clip /sdcard/voice/hint__l1_0.wav (played back-to-back). For a scripted offline call CONNECTED skips the gateway/model listen loop and just waits for hang-up. Scene is cleared in go_idle so a later dial-out call never inherits it. Validated: greeting 5.05s + WARNING hint 6.20s from SD on pickup, no gateway/model. --- plip_voice/main/conversation.c | 61 ++++++++++++++++++++++++++++++---- plip_voice/main/conversation.h | 7 ++-- plip_voice/main/net.c | 7 ++-- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/plip_voice/main/conversation.c b/plip_voice/main/conversation.c index 438841e..8f70492 100644 --- a/plip_voice/main/conversation.c +++ b/plip_voice/main/conversation.c @@ -84,12 +84,16 @@ static char s_sid[32] = {0}; * NOT re-read dialer_current() for the greeting/reply — that polluted number * would 404 at the gateway. Capture the clean routed number here once. */ static char s_number[16] = {0}; +/* Scene this call hints on (e.g. "SCENE_WARNING"), locked at pickup like the + * number. Empty for player-dialled calls. Drives the local SD hint clip. */ +static char s_scene[40] = {0}; /* Incoming-call mode: an NPC "calls" the phone. conversation_arm_incoming() - * stores the caller's persona number and rings; when the handset is then picked - * up from IDLE, we skip the outgoing dialtone/dial/ringback and go straight to - * GREET with this number (the NPC speaks first). */ + * stores the caller's persona number (and optional scene) and rings; when the + * handset is then picked up from IDLE, we skip the outgoing dialtone/dial/ + * ringback and go straight to GREET with this number (the NPC speaks first). */ static char s_incoming_number[16] = {0}; +static char s_incoming_scene[40] = {0}; static volatile bool s_incoming_armed = false; /* Known numbers: ringback when dialed */ @@ -105,6 +109,20 @@ static bool is_known(const char *num) return false; } +/* "SCENE_WARNING" → "warning": strip the SCENE_ prefix and lowercase, matching + * the SD pack filenames from tools/tts/generate_plip_sd_pack.py. */ +static void scene_slug(const char *scene, char *out, size_t cap) +{ + const char *p = scene; + if (strncmp(p, "SCENE_", 6) == 0) p += 6; + size_t i = 0; + for (; p[i] && i + 1 < cap; i++) { + char c = p[i]; + out[i] = (c >= 'A' && c <= 'Z') ? (char)(c - 'A' + 'a') : c; + } + out[i] = '\0'; +} + static void go_idle(void) { tones_stop(); @@ -114,6 +132,8 @@ static void go_idle(void) #if CONFIG_PLIP_DIAL_DTMF dtmf_stop(); #endif + s_scene[0] = '\0'; /* clear scripted-call scene so a later dial-out call + * doesn't inherit it (CONNECTED would skip listening). */ s_state = STATE_IDLE; ESP_LOGI(TAG, "-> IDLE"); } @@ -134,6 +154,7 @@ static void enter_incoming_greet(void) dialer_reset(); s_offhook = true; snprintf(s_number, sizeof(s_number), "%s", s_incoming_number); + snprintf(s_scene, sizeof(s_scene), "%s", s_incoming_scene); snprintf(s_sid, sizeof(s_sid), "%lld", (long long)esp_timer_get_time()); audio_pa_set(true); s_state = STATE_GREET; @@ -277,11 +298,37 @@ static void conv_task(void *arg) ESP_LOGW(TAG, "no SD clip + gateway greeting failed — silent"); } } + /* Scripted offline call: chain this scene's hint from the SD pack + * right after the greeting (the audio queue plays them back-to-back). + * Fully local — no gateway, no model. */ + if (s_scene[0] != '\0' && audio_ensure_sd()) { + char slug[40], hint[80]; + scene_slug(s_scene, slug, sizeof(slug)); + snprintf(hint, sizeof(hint), + "/sdcard/voice/hint_%s_l1_0.wav", slug); + FILE *hf = fopen(hint, "rb"); + if (hf != NULL) { + fclose(hf); + audio_play_async(hint); + ESP_LOGI(TAG, "GREET: chained SD hint %s", hint); + } else { + ESP_LOGI(TAG, "GREET: no SD hint for scene %s (%s)", + s_scene, hint); + } + } s_state = STATE_CONNECTED; ESP_LOGI(TAG, "-> CONNECTED"); break; case STATE_CONNECTED: + /* Scripted offline call (greeting + SD hint already queued): there's + * no live two-way conversation to run — just let the clips play out + * and wait for the player to hang up. Skips the gateway/model listen + * loop entirely. The global on-hook check returns us to IDLE. */ + if (s_scene[0] != '\0') { + vTaskDelay(pdMS_TO_TICKS(REPLY_POLL_MS)); + break; + } #if CONFIG_PLIP_VOICE_REPLY /* * Stage 3 — LISTEN loop. @@ -417,12 +464,14 @@ void conversation_on_hook_change(bool offhook) s_hook_changed = true; } -void conversation_arm_incoming(const char *number) +void conversation_arm_incoming(const char *number, const char *scene) { snprintf(s_incoming_number, sizeof(s_incoming_number), "%s", (number && number[0]) ? number : "17"); + snprintf(s_incoming_scene, sizeof(s_incoming_scene), "%s", + (scene && scene[0]) ? scene : ""); s_incoming_armed = true; phone_ring_start(); - ESP_LOGI(TAG, "incoming call armed (num=%s) — ringing until pickup", - s_incoming_number); + ESP_LOGI(TAG, "incoming call armed (num=%s scene=%s) — ringing until pickup", + s_incoming_number, s_incoming_scene[0] ? s_incoming_scene : "-"); } diff --git a/plip_voice/main/conversation.h b/plip_voice/main/conversation.h index a48f278..d881c12 100644 --- a/plip_voice/main/conversation.h +++ b/plip_voice/main/conversation.h @@ -6,5 +6,8 @@ void conversation_on_hook_change(bool offhook); /* Arm an INCOMING call from NPC persona `number` and start ringing. When the * handset is picked up from idle, the conversation jumps straight to the - * greeting (persona speaks first) instead of the outgoing dial-tone path. */ -void conversation_arm_incoming(const char *number); + * greeting (persona speaks first) instead of the outgoing dial-tone path. + * `scene` (optional, e.g. "SCENE_WARNING") makes the phone play that scene's + * hint from its local SD pack right after the greeting; pass NULL/"" for a + * greeting-only call. */ +void conversation_arm_incoming(const char *number, const char *scene); diff --git a/plip_voice/main/net.c b/plip_voice/main/net.c index 5847f33..a8d8f53 100644 --- a/plip_voice/main/net.c +++ b/plip_voice/main/net.c @@ -510,10 +510,13 @@ static esp_err_t handle_debug_ring(httpd_req_t *req) { /* /debug/ring[?number=NN] — arm an INCOMING call from persona NN (default * 17) and ring. On pickup the conversation jumps straight to the greeting. */ - char query[48] = {0}, number[16] = {0}; + char query[96] = {0}, number[16] = {0}, scene[40] = {0}; httpd_req_get_url_query_str(req, query, sizeof(query)); httpd_query_key_value(query, "number", number, sizeof(number)); - conversation_arm_incoming(number); /* defaults to "17" if empty; starts ringing */ + httpd_query_key_value(query, "scene", scene, sizeof(scene)); + /* scene (optional) lets the phone play that scene's hint from its SD pack + * after the greeting — fully offline, no model. */ + conversation_arm_incoming(number, scene[0] ? scene : NULL); char resp[64]; snprintf(resp, sizeof(resp), "{\"ok\":true,\"ringing\":true,\"incoming\":\"%s\"}", number[0] ? number : "17"); -- 2.52.0