455 lines
17 KiB
C
455 lines
17 KiB
C
#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;
|
||
}
|