feat(plip): chain SD scene hint after greeting #35

Merged
electron merged 1 commits from feat/plip-hint-after-greeting into main 2026-06-18 19:32:12 +00:00
3 changed files with 65 additions and 10 deletions
+55 -6
View File
@@ -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 : "-");
}
+5 -2
View File
@@ -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);
+5 -2
View File
@@ -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");