Files
AI_Printer/components/domain/src/voice_interaction_marker.c
2026-04-30 00:38:05 +08:00

455 lines
17 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "voice_interaction_internal.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}