chore(repo): keep only current main snapshot
History squashed to one root commit to permanently drop old branch history and objects.
This commit is contained in:
962
components/domain/src/image_generation.c
Normal file
962
components/domain/src/image_generation.c
Normal file
@@ -0,0 +1,962 @@
|
||||
#include "domain.h"
|
||||
#include "platform.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "esp_coexist.h"
|
||||
#include "esp_crt_bundle.h"
|
||||
#include "esp_heap_caps.h"
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#ifndef CONFIG_TQ_Z_IMAGE_API_ENDPOINT
|
||||
#define CONFIG_TQ_Z_IMAGE_API_ENDPOINT "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_Z_IMAGE_MODEL
|
||||
#define CONFIG_TQ_Z_IMAGE_MODEL "z-image-turbo"
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_Z_IMAGE_DEFAULT_SIZE
|
||||
#define CONFIG_TQ_Z_IMAGE_DEFAULT_SIZE "512*768"
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_Z_IMAGE_TIMEOUT_MS
|
||||
#define CONFIG_TQ_Z_IMAGE_TIMEOUT_MS 45000
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_Z_IMAGE_DOWNLOAD_TIMEOUT_MS
|
||||
#define CONFIG_TQ_Z_IMAGE_DOWNLOAD_TIMEOUT_MS 15000
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_Z_IMAGE_MAX_RESPONSE_BYTES
|
||||
#define CONFIG_TQ_Z_IMAGE_MAX_RESPONSE_BYTES 65536
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_Z_IMAGE_MAX_PNG_BYTES
|
||||
#define CONFIG_TQ_Z_IMAGE_MAX_PNG_BYTES (4 * 1024 * 1024)
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_Z_IMAGE_HTTP_READ_CHUNK_BYTES
|
||||
#define CONFIG_TQ_Z_IMAGE_HTTP_READ_CHUNK_BYTES 16384
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_TQ_Z_IMAGE_HTTP_BUFFER_BYTES
|
||||
#define CONFIG_TQ_Z_IMAGE_HTTP_BUFFER_BYTES 16384
|
||||
#endif
|
||||
|
||||
#define IMAGE_HTTP_READ_CHUNK_BYTES ((size_t)CONFIG_TQ_Z_IMAGE_HTTP_READ_CHUNK_BYTES)
|
||||
#define IMAGE_HTTP_BUFFER_BYTES CONFIG_TQ_Z_IMAGE_HTTP_BUFFER_BYTES
|
||||
#define IMAGE_PREWARM_TIMEOUT_MS 2500
|
||||
#define IMAGE_PREWARM_STACK 5120
|
||||
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
#define IMAGE_WORKER_CORE_ID 0
|
||||
#else
|
||||
#define IMAGE_WORKER_CORE_ID 1
|
||||
#endif
|
||||
|
||||
static const char *TAG = "image_generation";
|
||||
|
||||
typedef struct {
|
||||
char oss_origin[160];
|
||||
} image_prewarm_task_arg_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t *data;
|
||||
size_t len;
|
||||
size_t cap;
|
||||
size_t max_len;
|
||||
} bytes_buffer_t;
|
||||
|
||||
static SemaphoreHandle_t s_prewarm_lock;
|
||||
static bool s_prewarm_running;
|
||||
static char s_last_oss_origin[160] = "https://dashscope-result-bj.oss-cn-beijing.aliyuncs.com/";
|
||||
|
||||
static bool image_generation_boost_coex_for_download(void) {
|
||||
#if CONFIG_ESP_COEX_SW_COEXIST_ENABLE && CONFIG_TQ_Z_IMAGE_COEX_PREFER_WIFI_DURING_DOWNLOAD
|
||||
esp_err_t rc = esp_coex_preference_set(ESP_COEX_PREFER_WIFI);
|
||||
if (rc != ESP_OK) {
|
||||
ESP_LOGW(TAG, "failed to prefer Wi-Fi coexist mode, rc=0x%x", (unsigned)rc);
|
||||
return false;
|
||||
}
|
||||
ESP_LOGI(TAG, "coexist prefer Wi-Fi enabled for image download");
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void image_generation_restore_coex_after_download(bool boosted) {
|
||||
#if CONFIG_ESP_COEX_SW_COEXIST_ENABLE && CONFIG_TQ_Z_IMAGE_COEX_PREFER_WIFI_DURING_DOWNLOAD
|
||||
if (!boosted) {
|
||||
return;
|
||||
}
|
||||
esp_err_t rc = esp_coex_preference_set(ESP_COEX_PREFER_BALANCE);
|
||||
if (rc != ESP_OK) {
|
||||
ESP_LOGW(TAG, "failed to restore coexist balance mode, rc=0x%x", (unsigned)rc);
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "coexist preference restored to balance");
|
||||
#else
|
||||
(void)boosted;
|
||||
#endif
|
||||
}
|
||||
|
||||
static void image_generation_fill_err(char *err, size_t err_len, const char *msg) {
|
||||
if (err != NULL && err_len > 0) {
|
||||
snprintf(err, err_len, "%s", msg != NULL ? msg : "unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
static void *malloc_prefer_psram(size_t size) {
|
||||
void *ptr = heap_caps_malloc(size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (ptr == NULL) {
|
||||
ptr = malloc(size);
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
static void *realloc_prefer_psram(void *old, size_t size) {
|
||||
if (old == NULL) {
|
||||
return malloc_prefer_psram(size);
|
||||
}
|
||||
return realloc(old, size);
|
||||
}
|
||||
|
||||
static esp_err_t bytes_buffer_reserve(bytes_buffer_t *buf, size_t need_extra) {
|
||||
if (buf == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (need_extra > SIZE_MAX - buf->len) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
size_t required = buf->len + need_extra;
|
||||
if (required > buf->max_len) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
if (required <= buf->cap) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
size_t new_cap = buf->cap == 0 ? 1024 : buf->cap;
|
||||
while (new_cap < required) {
|
||||
if (new_cap > (SIZE_MAX / 2)) {
|
||||
new_cap = required;
|
||||
break;
|
||||
}
|
||||
new_cap *= 2;
|
||||
}
|
||||
|
||||
uint8_t *new_data = (uint8_t *)realloc_prefer_psram(buf->data, new_cap);
|
||||
if (new_data == NULL) {
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
buf->data = new_data;
|
||||
buf->cap = new_cap;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t bytes_buffer_append(bytes_buffer_t *buf, const void *data, size_t len) {
|
||||
if (buf == NULL || data == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (len == 0) {
|
||||
return ESP_OK;
|
||||
}
|
||||
esp_err_t rc = bytes_buffer_reserve(buf, len);
|
||||
if (rc != ESP_OK) {
|
||||
return rc;
|
||||
}
|
||||
memcpy(buf->data + buf->len, data, len);
|
||||
buf->len += len;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static void bytes_buffer_free(bytes_buffer_t *buf) {
|
||||
if (buf == NULL) {
|
||||
return;
|
||||
}
|
||||
free(buf->data);
|
||||
buf->data = NULL;
|
||||
buf->len = 0;
|
||||
buf->cap = 0;
|
||||
}
|
||||
|
||||
static esp_err_t bytes_buffer_terminate(bytes_buffer_t *buf) {
|
||||
if (buf == NULL) {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
esp_err_t rc = bytes_buffer_reserve(buf, 1);
|
||||
if (rc != ESP_OK) {
|
||||
return rc;
|
||||
}
|
||||
buf->data[buf->len] = '\0';
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static const char *image_generation_api_key(void) {
|
||||
if (strlen(CONFIG_TQ_Z_IMAGE_API_KEY) > 0) {
|
||||
return CONFIG_TQ_Z_IMAGE_API_KEY;
|
||||
}
|
||||
if (strlen(CONFIG_TQ_VOICE_API_KEY) > 0) {
|
||||
return CONFIG_TQ_VOICE_API_KEY;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void prewarm_lock_init_if_needed(void) {
|
||||
if (s_prewarm_lock != NULL) {
|
||||
return;
|
||||
}
|
||||
s_prewarm_lock = xSemaphoreCreateMutex();
|
||||
}
|
||||
|
||||
static bool url_extract_origin(const char *url, char *out_origin, size_t out_origin_len) {
|
||||
if (url == NULL || out_origin == NULL || out_origin_len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *scheme = strstr(url, "://");
|
||||
if (scheme == NULL || scheme == url) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *host = scheme + 3;
|
||||
if (*host == '\0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *path = strchr(host, '/');
|
||||
size_t host_len = path != NULL ? (size_t)(path - host) : strlen(host);
|
||||
if (host_len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int n = snprintf(out_origin,
|
||||
out_origin_len,
|
||||
"%.*s://%.*s/",
|
||||
(int)(scheme - url),
|
||||
url,
|
||||
(int)host_len,
|
||||
host);
|
||||
return n > 0 && (size_t)n < out_origin_len;
|
||||
}
|
||||
|
||||
static void image_generation_remember_oss_origin(const char *image_url) {
|
||||
char origin[160] = {0};
|
||||
if (!url_extract_origin(image_url, origin, sizeof(origin))) {
|
||||
return;
|
||||
}
|
||||
|
||||
prewarm_lock_init_if_needed();
|
||||
if (s_prewarm_lock == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (xSemaphoreTake(s_prewarm_lock, pdMS_TO_TICKS(100)) != pdTRUE) {
|
||||
return;
|
||||
}
|
||||
strlcpy(s_last_oss_origin, origin, sizeof(s_last_oss_origin));
|
||||
xSemaphoreGive(s_prewarm_lock);
|
||||
}
|
||||
|
||||
static void image_generation_copy_oss_origin(char *out_origin, size_t out_origin_len) {
|
||||
if (out_origin == NULL || out_origin_len == 0) {
|
||||
return;
|
||||
}
|
||||
out_origin[0] = '\0';
|
||||
|
||||
prewarm_lock_init_if_needed();
|
||||
if (s_prewarm_lock == NULL) {
|
||||
strlcpy(out_origin, s_last_oss_origin, out_origin_len);
|
||||
return;
|
||||
}
|
||||
|
||||
if (xSemaphoreTake(s_prewarm_lock, pdMS_TO_TICKS(100)) != pdTRUE) {
|
||||
strlcpy(out_origin, s_last_oss_origin, out_origin_len);
|
||||
return;
|
||||
}
|
||||
strlcpy(out_origin, s_last_oss_origin, out_origin_len);
|
||||
xSemaphoreGive(s_prewarm_lock);
|
||||
}
|
||||
|
||||
static esp_err_t image_generation_http_prewarm_head(const char *url,
|
||||
const char *auth_header,
|
||||
uint32_t timeout_ms) {
|
||||
if (url == NULL || url[0] == '\0') {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_http_client_config_t cfg = {
|
||||
.url = url,
|
||||
.method = HTTP_METHOD_HEAD,
|
||||
.timeout_ms = (int)timeout_ms,
|
||||
.keep_alive_enable = true,
|
||||
.buffer_size = 1024,
|
||||
.buffer_size_tx = 1024,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
};
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(&cfg);
|
||||
if (client == NULL) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (auth_header != NULL && auth_header[0] != '\0') {
|
||||
esp_http_client_set_header(client, "Authorization", auth_header);
|
||||
}
|
||||
|
||||
esp_err_t rc = esp_http_client_open(client, 0);
|
||||
if (rc == ESP_OK) {
|
||||
(void)esp_http_client_fetch_headers(client);
|
||||
esp_http_client_close(client);
|
||||
}
|
||||
esp_http_client_cleanup(client);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static void image_generation_prewarm_task(void *arg) {
|
||||
image_prewarm_task_arg_t *task_arg = (image_prewarm_task_arg_t *)arg;
|
||||
char oss_origin[160] = {0};
|
||||
if (task_arg != NULL) {
|
||||
strlcpy(oss_origin, task_arg->oss_origin, sizeof(oss_origin));
|
||||
free(task_arg);
|
||||
}
|
||||
|
||||
int64_t start_ms = esp_timer_get_time() / 1000;
|
||||
const char *api_key = image_generation_api_key();
|
||||
char auth_header[192] = {0};
|
||||
const char *auth = NULL;
|
||||
if (api_key != NULL) {
|
||||
snprintf(auth_header, sizeof(auth_header), "Bearer %s", api_key);
|
||||
auth = auth_header;
|
||||
}
|
||||
|
||||
esp_err_t gen_rc = image_generation_http_prewarm_head(CONFIG_TQ_Z_IMAGE_API_ENDPOINT,
|
||||
auth,
|
||||
IMAGE_PREWARM_TIMEOUT_MS);
|
||||
esp_err_t oss_rc = image_generation_http_prewarm_head(oss_origin,
|
||||
NULL,
|
||||
IMAGE_PREWARM_TIMEOUT_MS);
|
||||
int64_t done_ms = esp_timer_get_time() / 1000;
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"https prewarm done, gen_rc=0x%x, oss_rc=0x%x, cost_ms=%lld",
|
||||
(unsigned)gen_rc,
|
||||
(unsigned)oss_rc,
|
||||
(long long)(done_ms - start_ms));
|
||||
|
||||
prewarm_lock_init_if_needed();
|
||||
if (s_prewarm_lock != NULL && xSemaphoreTake(s_prewarm_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
s_prewarm_running = false;
|
||||
xSemaphoreGive(s_prewarm_lock);
|
||||
} else {
|
||||
s_prewarm_running = false;
|
||||
}
|
||||
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void image_generation_schedule_prewarm(void) {
|
||||
prewarm_lock_init_if_needed();
|
||||
if (s_prewarm_lock == NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (xSemaphoreTake(s_prewarm_lock, pdMS_TO_TICKS(100)) != pdTRUE) {
|
||||
return;
|
||||
}
|
||||
if (s_prewarm_running) {
|
||||
xSemaphoreGive(s_prewarm_lock);
|
||||
return;
|
||||
}
|
||||
s_prewarm_running = true;
|
||||
xSemaphoreGive(s_prewarm_lock);
|
||||
|
||||
image_prewarm_task_arg_t *arg = (image_prewarm_task_arg_t *)malloc_prefer_psram(sizeof(*arg));
|
||||
if (arg == NULL) {
|
||||
if (xSemaphoreTake(s_prewarm_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
s_prewarm_running = false;
|
||||
xSemaphoreGive(s_prewarm_lock);
|
||||
} else {
|
||||
s_prewarm_running = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
memset(arg, 0, sizeof(*arg));
|
||||
image_generation_copy_oss_origin(arg->oss_origin, sizeof(arg->oss_origin));
|
||||
|
||||
BaseType_t ok = xTaskCreatePinnedToCore(image_generation_prewarm_task,
|
||||
"img_prewarm",
|
||||
IMAGE_PREWARM_STACK,
|
||||
arg,
|
||||
4,
|
||||
NULL,
|
||||
IMAGE_WORKER_CORE_ID);
|
||||
if (ok != pdPASS) {
|
||||
free(arg);
|
||||
if (xSemaphoreTake(s_prewarm_lock, pdMS_TO_TICKS(100)) == pdTRUE) {
|
||||
s_prewarm_running = false;
|
||||
xSemaphoreGive(s_prewarm_lock);
|
||||
} else {
|
||||
s_prewarm_running = false;
|
||||
}
|
||||
ESP_LOGW(TAG, "failed to create prewarm task");
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t http_read_full_response(esp_http_client_handle_t client,
|
||||
bytes_buffer_t *out_body,
|
||||
char *err,
|
||||
size_t err_len) {
|
||||
if (client == NULL || out_body == NULL) {
|
||||
image_generation_fill_err(err, err_len, "invalid http read args");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
char *tmp = (char *)malloc_prefer_psram(IMAGE_HTTP_READ_CHUNK_BYTES);
|
||||
if (tmp == NULL) {
|
||||
image_generation_fill_err(err, err_len, "no memory for http read buffer");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
int n = esp_http_client_read(client, tmp, IMAGE_HTTP_READ_CHUNK_BYTES);
|
||||
if (n < 0) {
|
||||
free(tmp);
|
||||
image_generation_fill_err(err, err_len, "http read failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
if (n == 0) {
|
||||
free(tmp);
|
||||
if (esp_http_client_is_complete_data_received(client)) {
|
||||
return ESP_OK;
|
||||
}
|
||||
image_generation_fill_err(err, err_len, "http response incomplete");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
esp_err_t append_rc = bytes_buffer_append(out_body, tmp, (size_t)n);
|
||||
if (append_rc != ESP_OK) {
|
||||
free(tmp);
|
||||
image_generation_fill_err(err, err_len, "http response too large");
|
||||
return append_rc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static esp_err_t http_request_collect(const esp_http_client_config_t *config,
|
||||
const char *auth_header,
|
||||
const char *body,
|
||||
int *out_status_code,
|
||||
bytes_buffer_t *out_body,
|
||||
char *err,
|
||||
size_t err_len) {
|
||||
if (config == NULL || out_body == NULL) {
|
||||
image_generation_fill_err(err, err_len, "invalid http request args");
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
esp_http_client_handle_t client = esp_http_client_init(config);
|
||||
if (client == NULL) {
|
||||
image_generation_fill_err(err, err_len, "http client init failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (auth_header != NULL) {
|
||||
esp_http_client_set_header(client, "Authorization", auth_header);
|
||||
}
|
||||
if (body != NULL) {
|
||||
esp_http_client_set_header(client, "Content-Type", "application/json");
|
||||
}
|
||||
|
||||
size_t body_len = body != NULL ? strlen(body) : 0;
|
||||
esp_err_t open_rc = esp_http_client_open(client, (int)body_len);
|
||||
if (open_rc != ESP_OK) {
|
||||
esp_http_client_cleanup(client);
|
||||
image_generation_fill_err(err, err_len, "http open failed");
|
||||
return open_rc;
|
||||
}
|
||||
|
||||
if (body != NULL && body_len > 0) {
|
||||
int written = esp_http_client_write(client, body, (int)body_len);
|
||||
if (written < 0 || (size_t)written != body_len) {
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
image_generation_fill_err(err, err_len, "http request write failed");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
int64_t content_len = esp_http_client_fetch_headers(client);
|
||||
if (content_len > 0 && (size_t)content_len > out_body->max_len) {
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
image_generation_fill_err(err, err_len, "http response too large");
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
if (content_len > 0) {
|
||||
esp_err_t reserve_rc = bytes_buffer_reserve(out_body, (size_t)content_len);
|
||||
if (reserve_rc != ESP_OK) {
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
image_generation_fill_err(err, err_len, "no memory for http response");
|
||||
return reserve_rc;
|
||||
}
|
||||
}
|
||||
if (out_status_code != NULL) {
|
||||
*out_status_code = esp_http_client_get_status_code(client);
|
||||
}
|
||||
|
||||
esp_err_t read_rc = http_read_full_response(client, out_body, err, err_len);
|
||||
esp_http_client_close(client);
|
||||
esp_http_client_cleanup(client);
|
||||
return read_rc;
|
||||
}
|
||||
|
||||
static const char *json_get_string(cJSON *obj, const char *name) {
|
||||
if (obj == NULL || name == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
cJSON *item = cJSON_GetObjectItemCaseSensitive(obj, name);
|
||||
if (cJSON_IsString(item) && item->valuestring != NULL) {
|
||||
return item->valuestring;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool json_extract_image_url(cJSON *root,
|
||||
char *out_url,
|
||||
size_t out_url_len,
|
||||
char *out_prompt,
|
||||
size_t out_prompt_len) {
|
||||
if (root == NULL || out_url == NULL || out_url_len == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON *output = cJSON_GetObjectItemCaseSensitive(root, "output");
|
||||
cJSON *choices = cJSON_GetObjectItemCaseSensitive(output, "choices");
|
||||
if (!cJSON_IsArray(choices)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON *choice = cJSON_GetArrayItem(choices, 0);
|
||||
if (!cJSON_IsObject(choice)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cJSON *message = cJSON_GetObjectItemCaseSensitive(choice, "message");
|
||||
cJSON *content = cJSON_GetObjectItemCaseSensitive(message, "content");
|
||||
if (!cJSON_IsArray(content)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
out_url[0] = '\0';
|
||||
if (out_prompt != NULL && out_prompt_len > 0) {
|
||||
out_prompt[0] = '\0';
|
||||
}
|
||||
|
||||
int count = cJSON_GetArraySize(content);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
cJSON *item = cJSON_GetArrayItem(content, i);
|
||||
if (!cJSON_IsObject(item)) {
|
||||
continue;
|
||||
}
|
||||
const char *image = json_get_string(item, "image");
|
||||
if (image != NULL && out_url[0] == '\0') {
|
||||
strlcpy(out_url, image, out_url_len);
|
||||
}
|
||||
|
||||
if (out_prompt != NULL && out_prompt_len > 0 && out_prompt[0] == '\0') {
|
||||
const char *text = json_get_string(item, "text");
|
||||
if (text != NULL) {
|
||||
strlcpy(out_prompt, text, out_prompt_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return out_url[0] != '\0';
|
||||
}
|
||||
|
||||
static void extract_remote_error_message(const char *json_text,
|
||||
int status_code,
|
||||
char *err,
|
||||
size_t err_len) {
|
||||
if (json_text == NULL) {
|
||||
image_generation_fill_err(err, err_len, "upstream request failed");
|
||||
return;
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_Parse(json_text);
|
||||
if (root == NULL) {
|
||||
if (status_code > 0) {
|
||||
snprintf(err, err_len, "upstream request failed, http_status=%d", status_code);
|
||||
} else {
|
||||
image_generation_fill_err(err, err_len, "upstream request failed");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const char *code = json_get_string(root, "code");
|
||||
const char *message = json_get_string(root, "message");
|
||||
|
||||
if (code != NULL && message != NULL) {
|
||||
snprintf(err, err_len, "%s: %s", code, message);
|
||||
} else if (message != NULL) {
|
||||
image_generation_fill_err(err, err_len, message);
|
||||
} else if (status_code > 0) {
|
||||
snprintf(err, err_len, "upstream request failed, http_status=%d", status_code);
|
||||
} else {
|
||||
image_generation_fill_err(err, err_len, "upstream request failed");
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
}
|
||||
|
||||
static char *build_generation_request_json(const image_generation_request_t *req) {
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
if (root == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON_AddStringToObject(root, "model", CONFIG_TQ_Z_IMAGE_MODEL);
|
||||
|
||||
cJSON *input = cJSON_AddObjectToObject(root, "input");
|
||||
cJSON *messages = cJSON_AddArrayToObject(input, "messages");
|
||||
cJSON *message = cJSON_CreateObject();
|
||||
cJSON_AddItemToArray(messages, message);
|
||||
cJSON_AddStringToObject(message, "role", "user");
|
||||
cJSON *content = cJSON_AddArrayToObject(message, "content");
|
||||
cJSON *content_item = cJSON_CreateObject();
|
||||
cJSON_AddItemToArray(content, content_item);
|
||||
cJSON_AddStringToObject(content_item, "text", req->prompt);
|
||||
|
||||
cJSON *parameters = cJSON_AddObjectToObject(root, "parameters");
|
||||
cJSON_AddBoolToObject(parameters, "prompt_extend", req->prompt_extend);
|
||||
|
||||
const char *size = (req->size != NULL && req->size[0] != '\0')
|
||||
? req->size
|
||||
: CONFIG_TQ_Z_IMAGE_DEFAULT_SIZE;
|
||||
cJSON_AddStringToObject(parameters, "size", size);
|
||||
if (req->has_seed) {
|
||||
cJSON_AddNumberToObject(parameters, "seed", (double)req->seed);
|
||||
}
|
||||
|
||||
char *body = cJSON_PrintUnformatted(root);
|
||||
cJSON_Delete(root);
|
||||
return body;
|
||||
}
|
||||
|
||||
static esp_err_t image_generation_invoke_model(const image_generation_request_t *req,
|
||||
image_generation_result_t *out_result,
|
||||
char *err,
|
||||
size_t err_len) {
|
||||
char *req_body = build_generation_request_json(req);
|
||||
if (req_body == NULL) {
|
||||
image_generation_fill_err(err, err_len, "build request json failed");
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
|
||||
const char *api_key = image_generation_api_key();
|
||||
if (api_key == NULL) {
|
||||
cJSON_free(req_body);
|
||||
image_generation_fill_err(err, err_len, "z-image api key not configured");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
|
||||
char auth_header[192] = {0};
|
||||
snprintf(auth_header, sizeof(auth_header), "Bearer %s", api_key);
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"z-image request start, timeout_ms=%u",
|
||||
(unsigned)req->generation_timeout_ms);
|
||||
|
||||
esp_http_client_config_t config = {
|
||||
.url = CONFIG_TQ_Z_IMAGE_API_ENDPOINT,
|
||||
.method = HTTP_METHOD_POST,
|
||||
.timeout_ms = (int)req->generation_timeout_ms,
|
||||
.keep_alive_enable = true,
|
||||
.buffer_size = IMAGE_HTTP_BUFFER_BYTES,
|
||||
.buffer_size_tx = IMAGE_HTTP_BUFFER_BYTES,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
};
|
||||
|
||||
bytes_buffer_t resp = {
|
||||
.data = NULL,
|
||||
.len = 0,
|
||||
.cap = 0,
|
||||
.max_len = CONFIG_TQ_Z_IMAGE_MAX_RESPONSE_BYTES,
|
||||
};
|
||||
int http_status = 0;
|
||||
esp_err_t rc = http_request_collect(&config,
|
||||
auth_header,
|
||||
req_body,
|
||||
&http_status,
|
||||
&resp,
|
||||
err,
|
||||
err_len);
|
||||
cJSON_free(req_body);
|
||||
if (rc != ESP_OK) {
|
||||
ESP_LOGW(TAG,
|
||||
"z-image request failed, rc=0x%x, msg=%s",
|
||||
(unsigned)rc,
|
||||
(err != NULL && err[0] != '\0') ? err : "http request failed");
|
||||
bytes_buffer_free(&resp);
|
||||
return rc;
|
||||
}
|
||||
|
||||
rc = bytes_buffer_terminate(&resp);
|
||||
if (rc != ESP_OK) {
|
||||
bytes_buffer_free(&resp);
|
||||
image_generation_fill_err(err, err_len, "generation response too large");
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (http_status < 200 || http_status >= 300) {
|
||||
extract_remote_error_message((const char *)resp.data, http_status, err, err_len);
|
||||
ESP_LOGW(TAG,
|
||||
"z-image http status=%d, msg=%s",
|
||||
http_status,
|
||||
(err != NULL && err[0] != '\0') ? err : "upstream request failed");
|
||||
bytes_buffer_free(&resp);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
cJSON *root = cJSON_Parse((const char *)resp.data);
|
||||
bytes_buffer_free(&resp);
|
||||
if (root == NULL) {
|
||||
image_generation_fill_err(err, err_len, "invalid generation response");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
const char *request_id = json_get_string(root, "request_id");
|
||||
if (request_id != NULL) {
|
||||
strlcpy(out_result->request_id, request_id, sizeof(out_result->request_id));
|
||||
}
|
||||
|
||||
cJSON *usage = cJSON_GetObjectItemCaseSensitive(root, "usage");
|
||||
cJSON *usage_w = cJSON_GetObjectItemCaseSensitive(usage, "width");
|
||||
cJSON *usage_h = cJSON_GetObjectItemCaseSensitive(usage, "height");
|
||||
if (cJSON_IsNumber(usage_w) && usage_w->valuedouble >= 1 && usage_w->valuedouble <= UINT16_MAX) {
|
||||
out_result->width = (uint16_t)usage_w->valuedouble;
|
||||
}
|
||||
if (cJSON_IsNumber(usage_h) && usage_h->valuedouble >= 1 && usage_h->valuedouble <= UINT16_MAX) {
|
||||
out_result->height = (uint16_t)usage_h->valuedouble;
|
||||
}
|
||||
|
||||
bool has_url = json_extract_image_url(root,
|
||||
out_result->image_url,
|
||||
sizeof(out_result->image_url),
|
||||
out_result->output_prompt,
|
||||
sizeof(out_result->output_prompt));
|
||||
if (!has_url) {
|
||||
const char *code = json_get_string(root, "code");
|
||||
const char *message = json_get_string(root, "message");
|
||||
if (code != NULL && message != NULL) {
|
||||
snprintf(err, err_len, "%s: %s", code, message);
|
||||
} else {
|
||||
image_generation_fill_err(err, err_len, "generation response has no image url");
|
||||
}
|
||||
ESP_LOGW(TAG,
|
||||
"z-image response parse failed, msg=%s",
|
||||
(err != NULL && err[0] != '\0') ? err : "generation response has no image url");
|
||||
cJSON_Delete(root);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
image_generation_remember_oss_origin(out_result->image_url);
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"z-image response ready, request_id=%s, model_size=%ux%u, image_url_len=%u",
|
||||
out_result->request_id[0] != '\0' ? out_result->request_id : "-",
|
||||
(unsigned)out_result->width,
|
||||
(unsigned)out_result->height,
|
||||
(unsigned)strlen(out_result->image_url));
|
||||
|
||||
cJSON_Delete(root);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t image_generation_download_png(image_generation_result_t *out_result,
|
||||
uint32_t timeout_ms,
|
||||
char *err,
|
||||
size_t err_len) {
|
||||
ESP_LOGI(TAG, "generated image download start, timeout_ms=%u", (unsigned)timeout_ms);
|
||||
bool coex_boosted = image_generation_boost_coex_for_download();
|
||||
|
||||
esp_http_client_config_t config = {
|
||||
.url = out_result->image_url,
|
||||
.method = HTTP_METHOD_GET,
|
||||
.timeout_ms = (int)timeout_ms,
|
||||
.keep_alive_enable = true,
|
||||
.buffer_size = IMAGE_HTTP_BUFFER_BYTES,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
};
|
||||
|
||||
bytes_buffer_t resp = {
|
||||
.data = NULL,
|
||||
.len = 0,
|
||||
.cap = 0,
|
||||
.max_len = CONFIG_TQ_Z_IMAGE_MAX_PNG_BYTES,
|
||||
};
|
||||
int http_status = 0;
|
||||
esp_err_t rc = http_request_collect(&config,
|
||||
NULL,
|
||||
NULL,
|
||||
&http_status,
|
||||
&resp,
|
||||
err,
|
||||
err_len);
|
||||
if (rc != ESP_OK) {
|
||||
ESP_LOGW(TAG,
|
||||
"generated image download failed, rc=0x%x, msg=%s",
|
||||
(unsigned)rc,
|
||||
(err != NULL && err[0] != '\0') ? err : "download failed");
|
||||
bytes_buffer_free(&resp);
|
||||
image_generation_restore_coex_after_download(coex_boosted);
|
||||
return rc;
|
||||
}
|
||||
|
||||
if (http_status < 200 || http_status >= 300) {
|
||||
rc = bytes_buffer_terminate(&resp);
|
||||
if (rc != ESP_OK) {
|
||||
bytes_buffer_free(&resp);
|
||||
image_generation_fill_err(err, err_len, "image download failed");
|
||||
image_generation_restore_coex_after_download(coex_boosted);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
extract_remote_error_message((const char *)resp.data, http_status, err, err_len);
|
||||
ESP_LOGW(TAG,
|
||||
"generated image download http status=%d, msg=%s",
|
||||
http_status,
|
||||
(err != NULL && err[0] != '\0') ? err : "image download failed");
|
||||
bytes_buffer_free(&resp);
|
||||
image_generation_restore_coex_after_download(coex_boosted);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
static const uint8_t png_header[8] = {
|
||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||
};
|
||||
if (resp.len < sizeof(png_header) || memcmp(resp.data, png_header, sizeof(png_header)) != 0) {
|
||||
bytes_buffer_free(&resp);
|
||||
image_generation_fill_err(err, err_len, "downloaded image is not png");
|
||||
ESP_LOGW(TAG, "generated image download invalid png signature");
|
||||
image_generation_restore_coex_after_download(coex_boosted);
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
out_result->png = resp.data;
|
||||
out_result->png_len = resp.len;
|
||||
ESP_LOGI(TAG, "generated image download done, png_bytes=%u", (unsigned)out_result->png_len);
|
||||
image_generation_restore_coex_after_download(coex_boosted);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void image_generation_result_reset(image_generation_result_t *result) {
|
||||
if (result == NULL) {
|
||||
return;
|
||||
}
|
||||
memset(result, 0, sizeof(*result));
|
||||
}
|
||||
|
||||
void image_generation_result_free(image_generation_result_t *result) {
|
||||
if (result == NULL) {
|
||||
return;
|
||||
}
|
||||
free(result->png);
|
||||
result->png = NULL;
|
||||
result->png_len = 0;
|
||||
}
|
||||
|
||||
esp_err_t image_generation_generate_png(const image_generation_request_t *req,
|
||||
image_generation_result_t *out_result,
|
||||
char *err,
|
||||
size_t err_len) {
|
||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_IMAGE_GENERATE_ATTEMPT, 1);
|
||||
|
||||
if (req == NULL || out_result == NULL || req->prompt == NULL || req->prompt[0] == '\0') {
|
||||
image_generation_fill_err(err, err_len, "prompt is required");
|
||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_IMAGE_GENERATE_FAILED, 1);
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
|
||||
image_generation_result_reset(out_result);
|
||||
|
||||
image_generation_request_t actual = *req;
|
||||
if (actual.generation_timeout_ms == 0) {
|
||||
actual.generation_timeout_ms = runtime_policy_image_generation_timeout_default_ms();
|
||||
}
|
||||
if (actual.download_timeout_ms == 0) {
|
||||
actual.download_timeout_ms = runtime_policy_image_download_timeout_default_ms();
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"image generate start, prompt_len=%u, size=%s, prompt_extend=%d, seed=%s, gen_timeout_ms=%u, fetch_timeout_ms=%u",
|
||||
(unsigned)strlen(actual.prompt),
|
||||
(actual.size != NULL && actual.size[0] != '\0') ? actual.size : "default",
|
||||
actual.prompt_extend,
|
||||
actual.has_seed ? "set" : "auto",
|
||||
(unsigned)actual.generation_timeout_ms,
|
||||
(unsigned)actual.download_timeout_ms);
|
||||
|
||||
int64_t t0_ms = esp_timer_get_time() / 1000;
|
||||
esp_err_t gen_rc = image_generation_invoke_model(&actual, out_result, err, err_len);
|
||||
if (gen_rc != ESP_OK) {
|
||||
ESP_LOGW(TAG,
|
||||
"image generate stage failed, rc=0x%x, msg=%s",
|
||||
(unsigned)gen_rc,
|
||||
(err != NULL && err[0] != '\0') ? err : "model invoke failed");
|
||||
if (gen_rc == ESP_ERR_TIMEOUT) {
|
||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_IMAGE_GENERATE_TIMEOUT, 1);
|
||||
} else {
|
||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_IMAGE_GENERATE_FAILED, 1);
|
||||
}
|
||||
runtime_diag_record_error("image_generate", gen_rc, err != NULL ? err : "model invoke failed");
|
||||
image_generation_result_free(out_result);
|
||||
return gen_rc;
|
||||
}
|
||||
|
||||
int64_t t_gen_done_ms = esp_timer_get_time() / 1000;
|
||||
ESP_LOGI(TAG,
|
||||
"image generate stage done, request_id=%s, gen_cost_ms=%lld",
|
||||
out_result->request_id[0] != '\0' ? out_result->request_id : "-",
|
||||
(long long)(t_gen_done_ms - t0_ms));
|
||||
esp_err_t dl_rc = image_generation_download_png(out_result, actual.download_timeout_ms, err, err_len);
|
||||
if (dl_rc != ESP_OK) {
|
||||
ESP_LOGW(TAG,
|
||||
"image download stage failed, rc=0x%x, msg=%s",
|
||||
(unsigned)dl_rc,
|
||||
(err != NULL && err[0] != '\0') ? err : "download failed");
|
||||
if (dl_rc == ESP_ERR_TIMEOUT) {
|
||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_IMAGE_GENERATE_TIMEOUT, 1);
|
||||
} else {
|
||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_IMAGE_GENERATE_FAILED, 1);
|
||||
}
|
||||
runtime_diag_record_error("image_download", dl_rc, err != NULL ? err : "download failed");
|
||||
image_generation_result_free(out_result);
|
||||
return dl_rc;
|
||||
}
|
||||
|
||||
int64_t t_dl_done_ms = esp_timer_get_time() / 1000;
|
||||
|
||||
ESP_LOGI(TAG,
|
||||
"image generation pipeline done, request_id=%s, png_bytes=%u, gen_ms=%lld, download_ms=%lld, total_ms=%lld",
|
||||
out_result->request_id[0] != '\0' ? out_result->request_id : "-",
|
||||
(unsigned)out_result->png_len,
|
||||
(long long)(t_gen_done_ms - t0_ms),
|
||||
(long long)(t_dl_done_ms - t_gen_done_ms),
|
||||
(long long)(t_dl_done_ms - t0_ms));
|
||||
runtime_diag_counter_add(RUNTIME_DIAG_COUNTER_IMAGE_GENERATE_SUCCESS, 1);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
Reference in New Issue
Block a user