#include "voice_interaction_internal.h" #include #include #include #include #include "esp_log.h" #include "domain.h" static const char *TAG = "voice_interaction"; #define VOICE_IMAGE_MARKER_PROMPT_MAX_LEN 800 #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_PREVIEW_TIMEOUT_MS 3000 #define VOICE_IMAGE_MARKER_TEMPLATE \ "%s,线稿风格,白色背景,边缘清晰,细节简洁,高对比,避免大面积阴影和灰度渐变,适配热敏打印机打印。" #define VOICE_IMAGE_MARKER_TEMPLATE_FALLBACK_SUBJECT "简洁卡通图案" #define VOICE_IMAGE_PRINT_DENSITY "较淡" 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 bool voice_is_space_char(char c) { return isspace((unsigned char)c) != 0; } static size_t voice_find_first_sentence_end(const char *text, size_t len) { if (text == NULL || len == 0) { return 0; } for (size_t i = 0; i < len; ++i) { unsigned char c = (unsigned char)text[i]; if (c == '.' || c == '!' || c == '?' || c == '\n' || c == '\r') { return i + 1; } if (i + 2 < len) { unsigned char b1 = (unsigned char)text[i]; unsigned char b2 = (unsigned char)text[i + 1]; unsigned char b3 = (unsigned char)text[i + 2]; if (b1 == 0xE3 && b2 == 0x80 && b3 == 0x82) { return i + 3; } if (b1 == 0xEF && b2 == 0xBC && (b3 == 0x81 || b3 == 0x9F || b3 == 0x9B)) { return i + 3; } } } return len; } static bool voice_extract_first_sentence_text(const char *text, char *out_sentence, size_t out_len) { if (text == NULL || out_sentence == NULL || out_len == 0) { return false; } size_t len = strlen(text); size_t begin = 0; while (begin < len && voice_is_space_char(text[begin])) { begin++; } if (begin >= len) { return false; } size_t end = begin + voice_find_first_sentence_end(text + begin, len - begin); while (end > begin && voice_is_space_char(text[end - 1])) { end--; } if (end <= begin) { return false; } size_t copy_len = end - begin; if (copy_len >= out_len) { copy_len = out_len - 1; } memcpy(out_sentence, text + begin, copy_len); out_sentence[copy_len] = '\0'; return copy_len > 0; } static bool voice_extract_marker_prompt(const char *text, char *out_prompt, size_t out_len) { if (text == NULL || out_prompt == NULL || out_len == 0) { return false; } const char *open = strstr(text, "/*"); if (open == NULL) { return false; } const char *close = strstr(open + 2, "*/"); if (close == NULL) { return false; } const char *begin = open + 2; const char *end = close; while (begin < end && voice_is_space_char(*begin)) { begin++; } while (end > begin && voice_is_space_char(*(end - 1))) { end--; } if (end <= begin) { return false; } size_t prompt_len = (size_t)(end - begin); if (prompt_len >= out_len) { prompt_len = out_len - 1; } memcpy(out_prompt, begin, prompt_len); out_prompt[prompt_len] = '\0'; 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 void voice_try_preview_marker_png(const voice_image_generation_task_arg_t *task_arg, const uint8_t *png, size_t png_len) { if (task_arg == NULL || png == NULL || png_len == 0) { return; } char preview_err[96] = {0}; esp_err_t preview_rc = screen_preview_show_png(png, png_len, VOICE_IMAGE_PREVIEW_TIMEOUT_MS, preview_err, sizeof(preview_err)); if (preview_rc == ESP_OK) { return; } if (preview_rc == ESP_ERR_NOT_SUPPORTED) { return; } ESP_LOGW(TAG, "marker_image_preview_failed: dialog_id=%s rc=0x%x msg=%s", task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", (unsigned)preview_rc, preview_err[0] != '\0' ? preview_err : "preview failed"); } 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; } voice_try_preview_marker_png(task_arg, result->png, result->png_len); 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_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, "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; } uint32_t job_id = 0; char submit_err[128] = {0}; printer_print_options_t print_opt = { .ignore_precheck = false, }; esp_err_t submit_rc = printer_protocol_submit_raster_job_ex(raster, raster_len, raster_width, raster_height, VOICE_IMAGE_PRINT_DENSITY, &print_opt, &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, "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 { } 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) { vTaskDelete(NULL); return; } image_generation_request_t req = { .prompt = task_arg->prompt, .size = NULL, .prompt_extend = false, .has_seed = false, .seed = 0, .generation_timeout_ms = 0, .download_timeout_ms = 0, }; image_generation_result_t result = {0}; image_generation_result_reset(&result); printer_status_poll_pause_token_t status_poll_pause_token = 0; char gen_err[160] = {0}; status_poll_pause_token = printer_protocol_status_poll_pause_acquire(); esp_err_t rc = image_generation_generate_png(&req, &result, gen_err, sizeof(gen_err)); printer_protocol_status_poll_pause_release(&status_poll_pause_token); if (rc == ESP_OK) { 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, "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, "marker_image_gen_failed: dialog_id=%s rc=0x%x msg=%s", task_arg->dialog_id[0] != '\0' ? task_arg->dialog_id : "-", (unsigned)rc, gen_err[0] != '\0' ? gen_err : "image generation failed"); } image_generation_result_free(&result); voice_marker_image_generation_release(); image_generation_schedule_prewarm(); free(task_arg); vTaskDelete(NULL); } static void voice_schedule_marker_image_generation(const char *prompt, const char *dialog_id) { if (prompt == NULL || prompt[0] == '\0') { return; } size_t prompt_len = strlen(prompt); if (prompt_len > VOICE_IMAGE_MARKER_PROMPT_MAX_LEN) { ESP_LOGW(TAG, "marker_image_gen_skip: prompt too long, len=%u", (unsigned)prompt_len); return; } if (!voice_marker_image_generation_try_acquire(prompt, dialog_id, voice_now_ms(), NULL)) { 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, "marker_image_gen_skip: no memory for task arg"); voice_marker_image_generation_release(); return; } strlcpy(task_arg->prompt, prompt, sizeof(task_arg->prompt)); if (dialog_id != NULL) { strlcpy(task_arg->dialog_id, dialog_id, sizeof(task_arg->dialog_id)); } BaseType_t ok = xTaskCreate(voice_image_generation_task, "voice_img_gen", VOICE_IMAGE_GEN_TASK_STACK, task_arg, VOICE_IMAGE_GEN_TASK_PRIO, NULL); if (ok != pdPASS) { ESP_LOGW(TAG, "marker_image_gen_skip: create task failed"); voice_marker_image_generation_release(); free(task_arg); return; } } void voice_marker_try_trigger_image_generation(const char *dialog_id, const char *final_text) { if (final_text == NULL || final_text[0] == '\0') { return; } char first_sentence[VOICE_IMAGE_MARKER_FIRST_SENTENCE_MAX_LEN] = {0}; if (!voice_extract_first_sentence_text(final_text, first_sentence, sizeof(first_sentence))) { return; } 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, "marker_image_gen_skip: build prompt failed"); return; } voice_schedule_marker_image_generation(templated_prompt, dialog_id); } bool voice_marker_image_generation_is_running(void) { bool running = false; portENTER_CRITICAL(&s_marker_image_gen_lock); running = s_marker_image_gen_running; portEXIT_CRITICAL(&s_marker_image_gen_lock); return running; }