From 3f11e48ecaadf734c0b85135b568b854de365510 Mon Sep 17 00:00:00 2001 From: admin Date: Wed, 25 Feb 2026 18:41:48 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=AD=E9=9F=B3=E6=89=93=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dependencies.lock | 27 ++- main/domain/src/voice_interaction_ws.c | 235 ++++++++++++++++++++++++- main/idf_component.yml | 1 + 3 files changed, 259 insertions(+), 4 deletions(-) diff --git a/dependencies.lock b/dependencies.lock index 6037eb8..9595df2 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -1,4 +1,14 @@ dependencies: + espressif/cmake_utilities: + component_hash: 351350613ceafba240b761b4ea991e0f231ac7a9f59a9ee901f751bddc0bb18f + dependencies: + - name: idf + require: private + version: '>=4.1' + source: + registry_url: https://components.espressif.com + type: service + version: 0.5.3 espressif/esp_audio_codec: component_hash: af4663830bc73eca58febcc34a976d003465e78ed57458aa00a14b231e392ca1 dependencies: @@ -19,6 +29,20 @@ dependencies: registry_url: https://components.espressif.com/ type: service version: 1.5.4 + espressif/esp_lcd_gc9a01: + component_hash: e60bc2b5649c7679bd010797a875ae64b2a57cf9cdbd6a2bf7a0ade66913edc0 + dependencies: + - name: espressif/cmake_utilities + registry_url: https://components.espressif.com + require: private + version: 0.* + - name: idf + require: private + version: '>=4.4' + source: + registry_url: https://components.espressif.com/ + type: service + version: 2.0.4 espressif/esp_websocket_client: component_hash: c5a067a9fddea370c478017e66fac302f4b79c3d4027e9bdd42a019786cceb92 dependencies: @@ -60,9 +84,10 @@ dependencies: direct_dependencies: - espressif/esp_audio_codec - espressif/esp_codec_dev +- espressif/esp_lcd_gc9a01 - espressif/esp_websocket_client - espressif/libpng - idf -manifest_hash: 3274b53ce89a58abbad0a03dfaff3410e35deb81305a21289606c4d2177a111b +manifest_hash: ee0dd489986ab012202f5537ca2249a0c4e3ca234a23194dddb81cc25075b54c target: esp32s3 version: 2.0.0 diff --git a/main/domain/src/voice_interaction_ws.c b/main/domain/src/voice_interaction_ws.c index 967e824..0f7c9e8 100644 --- a/main/domain/src/voice_interaction_ws.c +++ b/main/domain/src/voice_interaction_ws.c @@ -15,6 +15,8 @@ #include "esp_audio_enc.h" #include "esp_log.h" #include "image_generation.h" +#include "printer_protocol.h" +#include "raster_tools.h" #include "voice_audio.h" static const char *TAG = "voice_interaction"; @@ -23,12 +25,78 @@ static const char *TAG = "voice_interaction"; #define VOICE_IMAGE_MARKER_FIRST_SENTENCE_MAX_LEN 1024 #define VOICE_IMAGE_GEN_TASK_STACK 16384 #define VOICE_IMAGE_GEN_TASK_PRIO 3 +#define VOICE_IMAGE_MARKER_REPEAT_SUPPRESS_MS 45000 +#define VOICE_IMAGE_PRINT_THRESHOLD 160 +#define VOICE_IMAGE_PRINT_MAX_HEIGHT 2200 +#define VOICE_IMAGE_MARKER_TEMPLATE \ + "%s,黑白线稿风格,白色背景,主体居中,边缘清晰,细节简洁,高对比,避免大面积阴影和灰度渐变,适配热敏打印机打印。" +#define VOICE_IMAGE_MARKER_TEMPLATE_FALLBACK_SUBJECT "简洁卡通图案" typedef struct { char prompt[VOICE_IMAGE_MARKER_PROMPT_MAX_LEN + 1]; char dialog_id[40]; } voice_image_generation_task_arg_t; +static portMUX_TYPE s_marker_image_gen_lock = portMUX_INITIALIZER_UNLOCKED; +static bool s_marker_image_gen_running; +static int64_t s_marker_last_success_ms; +static char s_marker_last_success_prompt[VOICE_IMAGE_MARKER_PROMPT_MAX_LEN + 1]; +static char s_marker_last_success_dialog_id[40]; + +static bool voice_marker_image_generation_try_acquire(const char *prompt, + const char *dialog_id, + int64_t now_ms, + uint32_t *out_remain_ms) { + bool acquired = false; + uint32_t remain_ms = 0; + const char *safe_prompt = prompt != NULL ? prompt : ""; + const char *safe_dialog = dialog_id != NULL ? dialog_id : ""; + + portENTER_CRITICAL(&s_marker_image_gen_lock); + if (s_marker_image_gen_running) { + acquired = false; + } else if (s_marker_last_success_ms > 0 && + strcmp(safe_prompt, s_marker_last_success_prompt) == 0 && + strcmp(safe_dialog, s_marker_last_success_dialog_id) == 0) { + int64_t elapsed_ms = now_ms - s_marker_last_success_ms; + if (elapsed_ms >= 0 && elapsed_ms < VOICE_IMAGE_MARKER_REPEAT_SUPPRESS_MS) { + remain_ms = (uint32_t)(VOICE_IMAGE_MARKER_REPEAT_SUPPRESS_MS - elapsed_ms); + acquired = false; + } else { + s_marker_image_gen_running = true; + acquired = true; + } + } else { + s_marker_image_gen_running = true; + acquired = true; + } + portEXIT_CRITICAL(&s_marker_image_gen_lock); + + if (out_remain_ms != NULL) { + *out_remain_ms = remain_ms; + } + return acquired; +} + +static void voice_marker_image_generation_release(void) { + portENTER_CRITICAL(&s_marker_image_gen_lock); + s_marker_image_gen_running = false; + portEXIT_CRITICAL(&s_marker_image_gen_lock); +} + +static void voice_marker_image_generation_record_success(const char *prompt, + const char *dialog_id, + int64_t now_ms) { + const char *safe_prompt = prompt != NULL ? prompt : ""; + const char *safe_dialog = dialog_id != NULL ? dialog_id : ""; + + portENTER_CRITICAL(&s_marker_image_gen_lock); + strlcpy(s_marker_last_success_prompt, safe_prompt, sizeof(s_marker_last_success_prompt)); + strlcpy(s_marker_last_success_dialog_id, safe_dialog, sizeof(s_marker_last_success_dialog_id)); + s_marker_last_success_ms = now_ms; + portEXIT_CRITICAL(&s_marker_image_gen_lock); +} + static void *voice_cjson_malloc_hook(size_t size) { return voice_malloc_prefer_psram(size); } @@ -418,6 +486,111 @@ static bool voice_extract_marker_prompt(const char *text, char *out_prompt, size return prompt_len > 0; } +static void voice_build_marker_image_prompt(const char *subject, + char *out_prompt, + size_t out_len) { + if (out_prompt == NULL || out_len == 0) { + return; + } + + const char *safe_subject = (subject != NULL && subject[0] != '\0') ? + subject : + VOICE_IMAGE_MARKER_TEMPLATE_FALLBACK_SUBJECT; + int written = snprintf(out_prompt, + out_len, + VOICE_IMAGE_MARKER_TEMPLATE, + safe_subject); + if (written < 0) { + out_prompt[0] = '\0'; + return; + } + + if ((size_t)written >= out_len) { + out_prompt[out_len - 1] = '\0'; + } +} + +static esp_err_t voice_submit_marker_generated_image(const voice_image_generation_task_arg_t *task_arg, + const image_generation_result_t *result, + char *err, + size_t err_len) { + if (task_arg == NULL || result == NULL || result->png == NULL || result->png_len == 0) { + voice_fill_err(err, err_len, "invalid image result"); + return ESP_ERR_INVALID_ARG; + } + + uint16_t raster_width = 0; + uint16_t raster_height = 0; + uint8_t *raster = NULL; + size_t raster_len = 0; + char decode_err[128] = {0}; + ESP_LOGI(TAG, + "[stage] marker_image_print_decode_start: dialog_id=%s request_id=%s png_bytes=%u", + task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", + result->request_id[0] != '\0' ? result->request_id : "-", + (unsigned)result->png_len); + esp_err_t decode_rc = raster_tools_convert_png_to_raster_384(result->png, + result->png_len, + true, + VOICE_IMAGE_PRINT_THRESHOLD, + false, + VOICE_IMAGE_PRINT_MAX_HEIGHT, + &raster_width, + &raster_height, + &raster, + &raster_len, + decode_err, + sizeof(decode_err)); + if (decode_rc != ESP_OK) { + voice_fill_err(err, + err_len, + decode_err[0] != '\0' ? decode_err : "decode generated image failed"); + ESP_LOGW(TAG, + "[stage] marker_image_print_decode_failed: dialog_id=%s rc=0x%x msg=%s", + task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", + (unsigned)decode_rc, + err != NULL && err[0] != '\0' ? err : "decode generated image failed"); + return decode_rc; + } + + ESP_LOGI(TAG, + "[stage] marker_image_print_decode_done: dialog_id=%s raster_size=%ux%u raster_bytes=%u", + task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", + (unsigned)raster_width, + (unsigned)raster_height, + (unsigned)raster_len); + + uint32_t job_id = 0; + char submit_err[128] = {0}; + esp_err_t submit_rc = printer_protocol_submit_raster_job(raster, + raster_len, + raster_width, + raster_height, + NULL, + &job_id, + submit_err, + sizeof(submit_err)); + if (submit_rc != ESP_OK) { + voice_fill_err(err, + err_len, + submit_err[0] != '\0' ? submit_err : "submit print job failed"); + ESP_LOGW(TAG, + "[stage] marker_image_print_submit_failed: dialog_id=%s rc=0x%x msg=%s", + task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", + (unsigned)submit_rc, + err != NULL && err[0] != '\0' ? err : "submit print job failed"); + } else { + ESP_LOGI(TAG, + "[stage] marker_image_print_submit_ok: dialog_id=%s request_id=%s job_id=%u", + task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", + result->request_id[0] != '\0' ? result->request_id : "-", + (unsigned)job_id); + } + + free(raster); + return submit_rc; +} + static void voice_image_generation_task(void *arg) { voice_image_generation_task_arg_t *task_arg = (voice_image_generation_task_arg_t *)arg; if (task_arg == NULL) { @@ -438,12 +611,17 @@ static void voice_image_generation_task(void *arg) { image_generation_result_t result = {0}; image_generation_result_reset(&result); + bool status_poll_paused = false; char gen_err[160] = {0}; ESP_LOGI(TAG, "[stage] marker_image_gen_start: dialog_id=%s prompt_len=%u", task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", (unsigned)strlen(task_arg->prompt)); + printer_protocol_set_status_poll_paused(true); + status_poll_paused = true; esp_err_t rc = image_generation_generate_png(&req, &result, gen_err, sizeof(gen_err)); + printer_protocol_set_status_poll_paused(false); + status_poll_paused = false; if (rc == ESP_OK) { ESP_LOGI(TAG, "[stage] marker_image_gen_done: dialog_id=%s request_id=%s size=%ux%u png_bytes=%u", @@ -452,6 +630,23 @@ static void voice_image_generation_task(void *arg) { (unsigned)result.width, (unsigned)result.height, (unsigned)result.png_len); + + char submit_err[160] = {0}; + esp_err_t submit_rc = voice_submit_marker_generated_image(task_arg, + &result, + submit_err, + sizeof(submit_err)); + if (submit_rc == ESP_OK) { + voice_marker_image_generation_record_success(task_arg->prompt, + task_arg->dialog_id, + voice_now_ms()); + } else { + ESP_LOGW(TAG, + "[stage] marker_image_print_failed: dialog_id=%s rc=0x%x msg=%s", + task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", + (unsigned)submit_rc, + submit_err[0] != '\0' ? submit_err : "marker image print failed"); + } } else { ESP_LOGW(TAG, "[stage] marker_image_gen_failed: dialog_id=%s rc=0x%x msg=%s", @@ -460,7 +655,11 @@ static void voice_image_generation_task(void *arg) { gen_err[0] != '\0' ? gen_err : "image generation failed"); } + if (status_poll_paused) { + printer_protocol_set_status_poll_paused(false); + } image_generation_result_free(&result); + voice_marker_image_generation_release(); image_generation_schedule_prewarm(); free(task_arg); vTaskDelete(NULL); @@ -479,10 +678,26 @@ static void voice_schedule_marker_image_generation(const char *prompt, const cha return; } + uint32_t repeat_remain_ms = 0; + if (!voice_marker_image_generation_try_acquire(prompt, + dialog_id, + voice_now_ms(), + &repeat_remain_ms)) { + if (repeat_remain_ms > 0) { + ESP_LOGI(TAG, + "[stage] marker_image_gen_skip: duplicate prompt suppressed, remain_ms=%u", + (unsigned)repeat_remain_ms); + } else { + ESP_LOGI(TAG, "[stage] marker_image_gen_skip: previous marker task running"); + } + return; + } + voice_image_generation_task_arg_t *task_arg = (voice_image_generation_task_arg_t *)calloc(1, sizeof(*task_arg)); if (task_arg == NULL) { ESP_LOGW(TAG, "[stage] marker_image_gen_skip: no memory for task arg"); + voice_marker_image_generation_release(); return; } @@ -499,6 +714,7 @@ static void voice_schedule_marker_image_generation(const char *prompt, const cha NULL); if (ok != pdPASS) { ESP_LOGW(TAG, "[stage] marker_image_gen_skip: create task failed"); + voice_marker_image_generation_release(); free(task_arg); return; } @@ -519,15 +735,28 @@ static void voice_try_trigger_marker_image_generation(const char *dialog_id, con return; } - char prompt[VOICE_IMAGE_MARKER_PROMPT_MAX_LEN + 1] = {0}; - if (!voice_extract_marker_prompt(first_sentence, prompt, sizeof(prompt))) { + char marker_subject[VOICE_IMAGE_MARKER_PROMPT_MAX_LEN + 1] = {0}; + if (!voice_extract_marker_prompt(first_sentence, marker_subject, sizeof(marker_subject))) { + return; + } + + char templated_prompt[VOICE_IMAGE_MARKER_PROMPT_MAX_LEN + 1] = {0}; + voice_build_marker_image_prompt(marker_subject, + templated_prompt, + sizeof(templated_prompt)); + if (templated_prompt[0] == '\0') { + ESP_LOGW(TAG, "[stage] marker_image_gen_skip: build prompt failed"); return; } ESP_LOGI(TAG, "[stage] marker_image_gen_detected: first_sentence=%s", first_sentence); - voice_schedule_marker_image_generation(prompt, dialog_id); + ESP_LOGI(TAG, + "[stage] marker_image_gen_subject: keyword=%s", + marker_subject); + voice_log_final_text("marker_image_gen_prompt_templated", templated_prompt); + voice_schedule_marker_image_generation(templated_prompt, dialog_id); } static void voice_handle_output_event(cJSON *output) { diff --git a/main/idf_component.yml b/main/idf_component.yml index eb66955..f67b170 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -18,3 +18,4 @@ dependencies: espressif/esp_codec_dev: ^1.5.4 espressif/esp_audio_codec: ^2.4.1 espressif/libpng: ^1.6.55 + espressif/esp_lcd_gc9a01: ^2.0.4