z-image
This commit is contained in:
38
README.md
38
README.md
@@ -10,7 +10,7 @@ ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic:
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Wi-Fi STA-only mode (configured SSID/password; no SoftAP fallback)
|
- Wi-Fi STA-only mode (configured SSID/password; no SoftAP fallback)
|
||||||
- Built-in web UI at `/` for connect/status, text print, and image upload print
|
- Built-in web UI at `/` for connect/status, text print, and Z-Image prompt print
|
||||||
- BLE central client auto-scan/connect to printer name
|
- BLE central client auto-scan/connect to printer name
|
||||||
- Async print queue with jobs (`queued/running/success/failed/canceled`)
|
- Async print queue with jobs (`queued/running/success/failed/canceled`)
|
||||||
- Printer precheck (paper / battery / temperature)
|
- Printer precheck (paper / battery / temperature)
|
||||||
@@ -33,7 +33,7 @@ ESP32-S3 works as a Wi-Fi REST controller and replaces Android App logic:
|
|||||||
- `GET /v1/printer/status`
|
- `GET /v1/printer/status`
|
||||||
- Print:
|
- Print:
|
||||||
- `POST /v1/print/raster`
|
- `POST /v1/print/raster`
|
||||||
- `POST /v1/print/image` (direct PNG binary upload, no base64)
|
- `POST /v1/print/image` (JSON prompt -> generate image -> print, also compatible with direct PNG binary upload)
|
||||||
- `POST /v1/print/qr`
|
- `POST /v1/print/qr`
|
||||||
- `POST /v1/print/text`
|
- `POST /v1/print/text`
|
||||||
- `POST /v1/print/receipt`
|
- `POST /v1/print/receipt`
|
||||||
@@ -81,6 +81,13 @@ In `menuconfig -> TQ Controller Config`:
|
|||||||
- `TQ_WIFI_PASSWORD`
|
- `TQ_WIFI_PASSWORD`
|
||||||
- `TQ_HTTP_PORT`
|
- `TQ_HTTP_PORT`
|
||||||
- `TQ_API_KEY` (optional)
|
- `TQ_API_KEY` (optional)
|
||||||
|
- Z-Image HTTP auth and model fields:
|
||||||
|
- `TQ_Z_IMAGE_API_KEY` (optional, empty means fallback to `TQ_VOICE_API_KEY`)
|
||||||
|
- `TQ_Z_IMAGE_API_ENDPOINT`
|
||||||
|
- `TQ_Z_IMAGE_MODEL`
|
||||||
|
- `TQ_Z_IMAGE_DEFAULT_SIZE`
|
||||||
|
- `TQ_Z_IMAGE_TIMEOUT_MS`
|
||||||
|
- `TQ_Z_IMAGE_DOWNLOAD_TIMEOUT_MS`
|
||||||
- Voice WebSocket auth and app fields:
|
- Voice WebSocket auth and app fields:
|
||||||
- `TQ_VOICE_API_KEY`
|
- `TQ_VOICE_API_KEY`
|
||||||
- `TQ_VOICE_WORKSPACE_ID`
|
- `TQ_VOICE_WORKSPACE_ID`
|
||||||
@@ -121,7 +128,7 @@ The page provides:
|
|||||||
- health/status check
|
- health/status check
|
||||||
- connect/disconnect printer
|
- connect/disconnect printer
|
||||||
- submit a simple text print job
|
- submit a simple text print job
|
||||||
- upload a PNG image (binary payload) and let ESP32-S3 decode/threshold/scale/raster conversion
|
- input an image prompt, then let ESP32-S3 call DashScope Z-Image, download PNG, decode/threshold/scale/raster conversion, and print
|
||||||
- view jobs list
|
- view jobs list
|
||||||
|
|
||||||
If API key is enabled, input it in the page before invoking actions.
|
If API key is enabled, input it in the page before invoking actions.
|
||||||
@@ -163,7 +170,25 @@ curl -X POST http://<esp-ip>/v1/print/raster \
|
|||||||
}'
|
}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### Image print (direct PNG upload, no base64)
|
### Image print (generate with prompt, then print)
|
||||||
|
```bash
|
||||||
|
curl -X POST http://<esp-ip>/v1/print/image \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"prompt":"一只坐在窗边的橘猫,午后阳光,胶片质感,写实风格。",
|
||||||
|
"size":"1120*1440",
|
||||||
|
"prompt_extend":false,
|
||||||
|
"threshold":160,
|
||||||
|
"max_height":2200,
|
||||||
|
"scale_to_width":true,
|
||||||
|
"invert":false,
|
||||||
|
"density":"中等",
|
||||||
|
"timeout_ms":45000,
|
||||||
|
"fetch_timeout_ms":15000
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image print (direct PNG upload, backward-compatible)
|
||||||
```bash
|
```bash
|
||||||
curl -X POST 'http://<esp-ip>/v1/print/image?scale_to_width=1&threshold=160&invert=0&max_height=2200&density=medium' \
|
curl -X POST 'http://<esp-ip>/v1/print/image?scale_to_width=1&threshold=160&invert=0&max_height=2200&density=medium' \
|
||||||
-H 'Content-Type: image/png' \
|
-H 'Content-Type: image/png' \
|
||||||
@@ -172,7 +197,7 @@ curl -X POST 'http://<esp-ip>/v1/print/image?scale_to_width=1&threshold=160&inve
|
|||||||
|
|
||||||
Request constraints:
|
Request constraints:
|
||||||
- Request body limit: 4 MB
|
- Request body limit: 4 MB
|
||||||
- Suggested image size: <= 3 MB when using built-in Web UI uploader
|
- Suggested image size: <= 3 MB when using direct PNG upload compatibility mode
|
||||||
|
|
||||||
### QR print
|
### QR print
|
||||||
```bash
|
```bash
|
||||||
@@ -305,8 +330,9 @@ curl -X POST http://<esp-ip>/v1/voice/session/stop
|
|||||||
|
|
||||||
- BLE side is central/client role, not printer peripheral role.
|
- BLE side is central/client role, not printer peripheral role.
|
||||||
- `base64_msb_1bpp` uses Android-compatible bit order (MSB first).
|
- `base64_msb_1bpp` uses Android-compatible bit order (MSB first).
|
||||||
- `/v1/print/image` accepts raw `image/png` bytes directly (no base64 wrapper).
|
- `/v1/print/image` supports two modes: JSON prompt generation (DashScope Z-Image) and raw `image/png` upload (no base64 wrapper).
|
||||||
- Large buffers for image upload/decode prefer PSRAM first, then fallback to internal RAM.
|
- Large buffers for image upload/decode prefer PSRAM first, then fallback to internal RAM.
|
||||||
|
- Partition table uses `partitions.csv` with a 4MB `factory` app partition on 16MB flash modules.
|
||||||
- `/v1/print/text` and `/v1/print/receipt` now support UTF-8 Chinese via embedded 16x16 GB2312 glyphs.
|
- `/v1/print/text` and `/v1/print/receipt` now support UTF-8 Chinese via embedded 16x16 GB2312 glyphs.
|
||||||
- Characters outside embedded glyph set are rendered as square fallback boxes.
|
- Characters outside embedded glyph set are rendered as square fallback boxes.
|
||||||
- QR encoding uses embedded `qrcodegen` (Project Nayuki C implementation).
|
- QR encoding uses embedded `qrcodegen` (Project Nayuki C implementation).
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ idf_component_register(
|
|||||||
"domain/src/printer_protocol_commands.c"
|
"domain/src/printer_protocol_commands.c"
|
||||||
"domain/src/raster_tools.c"
|
"domain/src/raster_tools.c"
|
||||||
"domain/src/raster_tools_image_qr.c"
|
"domain/src/raster_tools_image_qr.c"
|
||||||
|
"domain/src/image_generation.c"
|
||||||
"domain/src/system_runtime.c"
|
"domain/src/system_runtime.c"
|
||||||
"domain/src/voice_interaction.c"
|
"domain/src/voice_interaction.c"
|
||||||
"platform/src/platform_bootstrap.c"
|
"platform/src/platform_bootstrap.c"
|
||||||
@@ -36,6 +37,7 @@ idf_component_register(
|
|||||||
esp_netif
|
esp_netif
|
||||||
esp_event
|
esp_event
|
||||||
esp_http_server
|
esp_http_server
|
||||||
|
esp_http_client
|
||||||
nvs_flash
|
nvs_flash
|
||||||
json
|
json
|
||||||
mbedtls
|
mbedtls
|
||||||
|
|||||||
@@ -20,9 +20,45 @@ config TQ_API_KEY
|
|||||||
string "REST API Key (optional, empty means disabled)"
|
string "REST API Key (optional, empty means disabled)"
|
||||||
default ""
|
default ""
|
||||||
|
|
||||||
|
config TQ_Z_IMAGE_API_KEY
|
||||||
|
string "Z-Image API Key for DashScope HTTP (optional, fallback to Voice API key)"
|
||||||
|
default "sk-7a50eca6856d4afb968ac3bf512f6d1b"
|
||||||
|
|
||||||
|
config TQ_Z_IMAGE_API_ENDPOINT
|
||||||
|
string "Z-Image HTTP endpoint"
|
||||||
|
default "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
|
||||||
|
|
||||||
|
config TQ_Z_IMAGE_MODEL
|
||||||
|
string "Z-Image model"
|
||||||
|
default "z-image-turbo"
|
||||||
|
|
||||||
|
config TQ_Z_IMAGE_DEFAULT_SIZE
|
||||||
|
string "Z-Image default output size"
|
||||||
|
default "1024*1536"
|
||||||
|
|
||||||
|
config TQ_Z_IMAGE_TIMEOUT_MS
|
||||||
|
int "Z-Image generation timeout (ms)"
|
||||||
|
range 5000 180000
|
||||||
|
default 45000
|
||||||
|
|
||||||
|
config TQ_Z_IMAGE_DOWNLOAD_TIMEOUT_MS
|
||||||
|
int "Z-Image image download timeout (ms)"
|
||||||
|
range 2000 120000
|
||||||
|
default 15000
|
||||||
|
|
||||||
|
config TQ_Z_IMAGE_MAX_RESPONSE_BYTES
|
||||||
|
int "Z-Image max response body bytes"
|
||||||
|
range 1024 262144
|
||||||
|
default 65536
|
||||||
|
|
||||||
|
config TQ_Z_IMAGE_MAX_PNG_BYTES
|
||||||
|
int "Z-Image max downloaded PNG bytes"
|
||||||
|
range 65536 8388608
|
||||||
|
default 4194304
|
||||||
|
|
||||||
config TQ_VOICE_API_KEY
|
config TQ_VOICE_API_KEY
|
||||||
string "Voice API Key for DashScope WebSocket (optional)"
|
string "Voice API Key for DashScope WebSocket (optional)"
|
||||||
default ""
|
default "sk-7a50eca6856d4afb968ac3bf512f6d1b"
|
||||||
|
|
||||||
config TQ_VOICE_WS_URI
|
config TQ_VOICE_WS_URI
|
||||||
string "Voice WebSocket URI"
|
string "Voice WebSocket URI"
|
||||||
@@ -30,11 +66,11 @@ config TQ_VOICE_WS_URI
|
|||||||
|
|
||||||
config TQ_VOICE_WORKSPACE_ID
|
config TQ_VOICE_WORKSPACE_ID
|
||||||
string "Voice workspace_id"
|
string "Voice workspace_id"
|
||||||
default ""
|
default "llm-igyrxzdvbwja5gyt"
|
||||||
|
|
||||||
config TQ_VOICE_APP_ID
|
config TQ_VOICE_APP_ID
|
||||||
string "Voice app_id"
|
string "Voice app_id"
|
||||||
default ""
|
default "fe83b756f6634dcc842c6e7f356c0cf8"
|
||||||
|
|
||||||
config TQ_VOICE_USER_ID
|
config TQ_VOICE_USER_ID
|
||||||
string "Voice client user_id"
|
string "Voice client user_id"
|
||||||
@@ -46,7 +82,7 @@ config TQ_VOICE_DEVICE_UUID
|
|||||||
|
|
||||||
config TQ_VOICE_TTS_VOICE
|
config TQ_VOICE_TTS_VOICE
|
||||||
string "Voice TTS voice (optional)"
|
string "Voice TTS voice (optional)"
|
||||||
default ""
|
default "longling_v3"
|
||||||
|
|
||||||
config TQ_VOICE_SAMPLE_RATE
|
config TQ_VOICE_SAMPLE_RATE
|
||||||
int "Voice sample rate"
|
int "Voice sample rate"
|
||||||
|
|||||||
@@ -63,8 +63,18 @@ static const char *s_web_index =
|
|||||||
" <button id='btnPrintText'>Print Text</button>\n"
|
" <button id='btnPrintText'>Print Text</button>\n"
|
||||||
" </div>\n"
|
" </div>\n"
|
||||||
"\n"
|
"\n"
|
||||||
" <label>Image Upload</label>\n"
|
" <label>Z-Image Prompt</label>\n"
|
||||||
" <input id='imageFile' type='file' accept='.png,image/png'>\n"
|
" <textarea id='imagePrompt'>一只坐在窗边的橘猫,午后阳光,胶片质感,写实风格。</textarea>\n"
|
||||||
|
" <div class='row'>\n"
|
||||||
|
" <select id='imgSize' style='max-width:180px'>\n"
|
||||||
|
" <option value='1024*1024'>1024*1024</option>\n"
|
||||||
|
" <option value='1120*1440' selected>1120*1440</option>\n"
|
||||||
|
" <option value='1536*864'>1536*864</option>\n"
|
||||||
|
" <option value='1536*1536'>1536*1536</option>\n"
|
||||||
|
" </select>\n"
|
||||||
|
" <input id='imgTimeout' type='number' min='5000' max='180000' value='45000' placeholder='Gen timeout ms' style='max-width:160px'>\n"
|
||||||
|
" <input id='imgFetchTimeout' type='number' min='2000' max='120000' value='15000' placeholder='Fetch timeout ms' style='max-width:170px'>\n"
|
||||||
|
" </div>\n"
|
||||||
" <div class='row'>\n"
|
" <div class='row'>\n"
|
||||||
" <input id='imgThreshold' type='number' min='0' max='255' value='160' placeholder='Threshold 0-255' style='max-width:160px'>\n"
|
" <input id='imgThreshold' type='number' min='0' max='255' value='160' placeholder='Threshold 0-255' style='max-width:160px'>\n"
|
||||||
" <input id='imgMaxHeight' type='number' min='64' max='3000' value='2200' placeholder='Max height' style='max-width:160px'>\n"
|
" <input id='imgMaxHeight' type='number' min='64' max='3000' value='2200' placeholder='Max height' style='max-width:160px'>\n"
|
||||||
@@ -84,9 +94,13 @@ static const char *s_web_index =
|
|||||||
" <input id='imgInvert' type='checkbox' style='width:auto;'>\n"
|
" <input id='imgInvert' type='checkbox' style='width:auto;'>\n"
|
||||||
" Invert\n"
|
" Invert\n"
|
||||||
" </label>\n"
|
" </label>\n"
|
||||||
" <button id='btnPrintImage'>Print Image</button>\n"
|
" <label style='margin:0;display:flex;align-items:center;gap:6px;'>\n"
|
||||||
|
" <input id='imgPromptExtend' type='checkbox' style='width:auto;'>\n"
|
||||||
|
" Prompt Extend\n"
|
||||||
|
" </label>\n"
|
||||||
|
" <button id='btnPrintImage'>Generate & Print</button>\n"
|
||||||
" </div>\n"
|
" </div>\n"
|
||||||
" <div class='small'>Direct PNG upload: browser sends binary PNG; ESP32-S3 decodes/scales/thresholds then prints.</div>\n"
|
" <div class='small'>Submit prompt only: ESP32-S3 calls DashScope Z-Image, downloads PNG, rasterizes and prints.</div>\n"
|
||||||
"\n"
|
"\n"
|
||||||
" <div class='small'>Tip: connect printer first, then submit print jobs.</div>\n"
|
" <div class='small'>Tip: connect printer first, then submit print jobs.</div>\n"
|
||||||
" <pre id='out'>Ready.</pre>\n"
|
" <pre id='out'>Ready.</pre>\n"
|
||||||
@@ -97,12 +111,16 @@ static const char *s_web_index =
|
|||||||
" const apiKeyEl = document.getElementById('apiKey');\n"
|
" const apiKeyEl = document.getElementById('apiKey');\n"
|
||||||
" const printerNameEl = document.getElementById('printerName');\n"
|
" const printerNameEl = document.getElementById('printerName');\n"
|
||||||
" const printTextEl = document.getElementById('printText');\n"
|
" const printTextEl = document.getElementById('printText');\n"
|
||||||
" const imageFileEl = document.getElementById('imageFile');\n"
|
" const imagePromptEl = document.getElementById('imagePrompt');\n"
|
||||||
|
" const imgSizeEl = document.getElementById('imgSize');\n"
|
||||||
|
" const imgTimeoutEl = document.getElementById('imgTimeout');\n"
|
||||||
|
" const imgFetchTimeoutEl = document.getElementById('imgFetchTimeout');\n"
|
||||||
" const imgThresholdEl = document.getElementById('imgThreshold');\n"
|
" const imgThresholdEl = document.getElementById('imgThreshold');\n"
|
||||||
" const imgMaxHeightEl = document.getElementById('imgMaxHeight');\n"
|
" const imgMaxHeightEl = document.getElementById('imgMaxHeight');\n"
|
||||||
" const imgDensityEl = document.getElementById('imgDensity');\n"
|
" const imgDensityEl = document.getElementById('imgDensity');\n"
|
||||||
" const imgScaleToWidthEl = document.getElementById('imgScaleToWidth');\n"
|
" const imgScaleToWidthEl = document.getElementById('imgScaleToWidth');\n"
|
||||||
" const imgInvertEl = document.getElementById('imgInvert');\n"
|
" const imgInvertEl = document.getElementById('imgInvert');\n"
|
||||||
|
" const imgPromptExtendEl = document.getElementById('imgPromptExtend');\n"
|
||||||
"\n"
|
"\n"
|
||||||
" function log(obj) {\n"
|
" function log(obj) {\n"
|
||||||
" if (typeof obj === 'string') {\n"
|
" if (typeof obj === 'string') {\n"
|
||||||
@@ -128,41 +146,23 @@ static const char *s_web_index =
|
|||||||
" return n;\n"
|
" return n;\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
"\n"
|
"\n"
|
||||||
" function buildImageUploadRequest(file) {\n"
|
" function buildImagePromptRequest() {\n"
|
||||||
" if (!file) {\n"
|
" const prompt = imagePromptEl.value.trim();\n"
|
||||||
" throw new Error('please select a png image first');\n"
|
" if (!prompt) {\n"
|
||||||
|
" throw new Error('please input image prompt');\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
" const name = (file.name || '').toLowerCase();\n"
|
|
||||||
" const type = (file.type || '').toLowerCase();\n"
|
|
||||||
" const isPng = type.includes('png') || name.endsWith('.png');\n"
|
|
||||||
" if (!isPng) {\n"
|
|
||||||
" throw new Error('only .png is supported in direct upload mode');\n"
|
|
||||||
" }\n"
|
|
||||||
" if (file.size <= 0) {\n"
|
|
||||||
" throw new Error('empty image file');\n"
|
|
||||||
" }\n"
|
|
||||||
" if (file.size > 3 * 1024 * 1024) {\n"
|
|
||||||
" throw new Error('png too large (>3MB), backend limit is 4MB');\n"
|
|
||||||
" }\n"
|
|
||||||
"\n"
|
|
||||||
" const densityMap = {\n"
|
|
||||||
" '较淡': 'light',\n"
|
|
||||||
" '中等': 'medium',\n"
|
|
||||||
" '较浓': 'dark',\n"
|
|
||||||
" '最深': 'max'\n"
|
|
||||||
" };\n"
|
|
||||||
"\n"
|
|
||||||
" const q = new URLSearchParams();\n"
|
|
||||||
" q.set('scale_to_width', imgScaleToWidthEl.checked ? '1' : '0');\n"
|
|
||||||
" q.set('threshold', String(toInt(imgThresholdEl.value, 160, 0, 255)));\n"
|
|
||||||
" q.set('invert', imgInvertEl.checked ? '1' : '0');\n"
|
|
||||||
" q.set('max_height', String(toInt(imgMaxHeightEl.value, 2200, 64, 3000)));\n"
|
|
||||||
" q.set('density', densityMap[imgDensityEl.value] || 'medium');\n"
|
|
||||||
"\n"
|
"\n"
|
||||||
" return {\n"
|
" return {\n"
|
||||||
" path: '/v1/print/image?' + q.toString(),\n"
|
" prompt,\n"
|
||||||
" body: file,\n"
|
" size: imgSizeEl.value || '1120*1440',\n"
|
||||||
" contentType: file.type || 'image/png'\n"
|
" prompt_extend: imgPromptExtendEl.checked,\n"
|
||||||
|
" threshold: toInt(imgThresholdEl.value, 160, 0, 255),\n"
|
||||||
|
" max_height: toInt(imgMaxHeightEl.value, 2200, 64, 3000),\n"
|
||||||
|
" density: imgDensityEl.value || '中等',\n"
|
||||||
|
" scale_to_width: imgScaleToWidthEl.checked,\n"
|
||||||
|
" invert: imgInvertEl.checked,\n"
|
||||||
|
" timeout_ms: toInt(imgTimeoutEl.value, 45000, 5000, 180000),\n"
|
||||||
|
" fetch_timeout_ms: toInt(imgFetchTimeoutEl.value, 15000, 2000, 120000)\n"
|
||||||
" };\n"
|
" };\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
"\n"
|
"\n"
|
||||||
@@ -179,19 +179,6 @@ static const char *s_web_index =
|
|||||||
" return data;\n"
|
" return data;\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
"\n"
|
"\n"
|
||||||
" async function callBinaryApi(path, body, contentType) {\n"
|
|
||||||
" const h = headers(false);\n"
|
|
||||||
" h['Content-Type'] = contentType || 'application/octet-stream';\n"
|
|
||||||
" const r = await fetch(path, { method: 'POST', headers: h, body });\n"
|
|
||||||
" const text = await r.text();\n"
|
|
||||||
" let data = text;\n"
|
|
||||||
" try { data = JSON.parse(text); } catch (_) {}\n"
|
|
||||||
" if (!r.ok) {\n"
|
|
||||||
" throw { status: r.status, data };\n"
|
|
||||||
" }\n"
|
|
||||||
" return data;\n"
|
|
||||||
" }\n"
|
|
||||||
"\n"
|
|
||||||
" async function run(fn) {\n"
|
" async function run(fn) {\n"
|
||||||
" try {\n"
|
" try {\n"
|
||||||
" const data = await fn();\n"
|
" const data = await fn();\n"
|
||||||
@@ -226,11 +213,8 @@ static const char *s_web_index =
|
|||||||
" }));\n"
|
" }));\n"
|
||||||
"\n"
|
"\n"
|
||||||
" document.getElementById('btnPrintImage').onclick = () => run(async () => {\n"
|
" document.getElementById('btnPrintImage').onclick = () => run(async () => {\n"
|
||||||
" const file = imageFileEl.files && imageFileEl.files[0] ? imageFileEl.files[0] : null;\n"
|
" const req = buildImagePromptRequest();\n"
|
||||||
" const req = buildImageUploadRequest(file);\n"
|
" return callApi('/v1/print/image', 'POST', req);\n"
|
||||||
" const result = await callBinaryApi(req.path, req.body, req.contentType);\n"
|
|
||||||
" result.client_note = 'uploaded png bytes: ' + file.size;\n"
|
|
||||||
" return result;\n"
|
|
||||||
" });\n"
|
" });\n"
|
||||||
" </script>\n"
|
" </script>\n"
|
||||||
"</body>\n"
|
"</body>\n"
|
||||||
@@ -252,6 +236,7 @@ esp_err_t rest_server_start(void) {
|
|||||||
config.server_port = CONFIG_TQ_HTTP_PORT;
|
config.server_port = CONFIG_TQ_HTTP_PORT;
|
||||||
config.uri_match_fn = httpd_uri_match_wildcard;
|
config.uri_match_fn = httpd_uri_match_wildcard;
|
||||||
config.max_uri_handlers = 32;
|
config.max_uri_handlers = 32;
|
||||||
|
config.stack_size = 10240;
|
||||||
|
|
||||||
esp_err_t err = httpd_start(&s_server, &config);
|
esp_err_t err = httpd_start(&s_server, &config);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
|
|
||||||
|
#include "image_generation.h"
|
||||||
#include "printer_protocol.h"
|
#include "printer_protocol.h"
|
||||||
#include "raster_tools.h"
|
#include "raster_tools.h"
|
||||||
static esp_err_t submit_raster_job_and_reply(httpd_req_t *req,
|
static esp_err_t submit_raster_job_and_reply(httpd_req_t *req,
|
||||||
@@ -154,6 +155,194 @@ static void parse_image_upload_options(httpd_req_t *req,
|
|||||||
free(query);
|
free(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool scale_to_width;
|
||||||
|
uint8_t threshold;
|
||||||
|
bool invert;
|
||||||
|
uint16_t max_height;
|
||||||
|
const char *density;
|
||||||
|
} image_print_options_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char prompt[801];
|
||||||
|
char size[24];
|
||||||
|
bool has_size;
|
||||||
|
bool prompt_extend;
|
||||||
|
bool has_seed;
|
||||||
|
uint32_t seed;
|
||||||
|
uint32_t timeout_ms;
|
||||||
|
uint32_t fetch_timeout_ms;
|
||||||
|
} image_generate_options_t;
|
||||||
|
|
||||||
|
static void image_print_options_set_defaults(image_print_options_t *out) {
|
||||||
|
if (out == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out->scale_to_width = true;
|
||||||
|
out->threshold = 160;
|
||||||
|
out->invert = false;
|
||||||
|
out->max_height = 2200;
|
||||||
|
out->density = "中等";
|
||||||
|
}
|
||||||
|
|
||||||
|
static void parse_image_print_options_from_json(cJSON *json, image_print_options_t *out) {
|
||||||
|
if (json == NULL || out == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *jscale = cJSON_GetObjectItemCaseSensitive(json, "scale_to_width");
|
||||||
|
cJSON *jthreshold = cJSON_GetObjectItemCaseSensitive(json, "threshold");
|
||||||
|
cJSON *jinvert = cJSON_GetObjectItemCaseSensitive(json, "invert");
|
||||||
|
cJSON *jmaxh = cJSON_GetObjectItemCaseSensitive(json, "max_height");
|
||||||
|
cJSON *jdensity = cJSON_GetObjectItemCaseSensitive(json, "density");
|
||||||
|
|
||||||
|
out->scale_to_width = rest_server_json_bool_with_default(jscale, out->scale_to_width);
|
||||||
|
out->invert = rest_server_json_bool_with_default(jinvert, out->invert);
|
||||||
|
if (cJSON_IsNumber(jthreshold) && jthreshold->valuedouble >= 0 && jthreshold->valuedouble <= 255) {
|
||||||
|
out->threshold = (uint8_t)jthreshold->valuedouble;
|
||||||
|
}
|
||||||
|
if (cJSON_IsNumber(jmaxh) && jmaxh->valuedouble >= 64 && jmaxh->valuedouble <= 3000) {
|
||||||
|
out->max_height = (uint16_t)jmaxh->valuedouble;
|
||||||
|
}
|
||||||
|
if (cJSON_IsString(jdensity) && jdensity->valuestring != NULL) {
|
||||||
|
out->density = map_density_text(jdensity->valuestring);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parse_image_size_text(const char *text, uint32_t *out_w, uint32_t *out_h) {
|
||||||
|
if (text == NULL || out_w == NULL || out_h == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *sep = strchr(text, '*');
|
||||||
|
if (sep == NULL || sep == text || *(sep + 1) == '\0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char wbuf[12] = {0};
|
||||||
|
char hbuf[12] = {0};
|
||||||
|
size_t wlen = (size_t)(sep - text);
|
||||||
|
size_t hlen = strlen(sep + 1);
|
||||||
|
if (wlen == 0 || hlen == 0 || wlen >= sizeof(wbuf) || hlen >= sizeof(hbuf)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(wbuf, text, wlen);
|
||||||
|
memcpy(hbuf, sep + 1, hlen);
|
||||||
|
wbuf[wlen] = '\0';
|
||||||
|
hbuf[hlen] = '\0';
|
||||||
|
|
||||||
|
char *wend = NULL;
|
||||||
|
char *hend = NULL;
|
||||||
|
long w = strtol(wbuf, &wend, 10);
|
||||||
|
long h = strtol(hbuf, &hend, 10);
|
||||||
|
if (wend == wbuf || hend == hbuf || *wend != '\0' || *hend != '\0') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (w <= 0 || h <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out_w = (uint32_t)w;
|
||||||
|
*out_h = (uint32_t)h;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool parse_image_generation_options(cJSON *json,
|
||||||
|
image_generate_options_t *out,
|
||||||
|
char *err,
|
||||||
|
size_t err_len) {
|
||||||
|
if (json == NULL || out == NULL) {
|
||||||
|
if (err != NULL && err_len > 0) {
|
||||||
|
snprintf(err, err_len, "invalid args");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
out->timeout_ms = CONFIG_TQ_Z_IMAGE_TIMEOUT_MS;
|
||||||
|
out->fetch_timeout_ms = CONFIG_TQ_Z_IMAGE_DOWNLOAD_TIMEOUT_MS;
|
||||||
|
|
||||||
|
cJSON *jprompt = cJSON_GetObjectItemCaseSensitive(json, "prompt");
|
||||||
|
if (!cJSON_IsString(jprompt) || jprompt->valuestring == NULL || jprompt->valuestring[0] == '\0') {
|
||||||
|
if (err != NULL && err_len > 0) {
|
||||||
|
snprintf(err, err_len, "prompt is required");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t prompt_len = strlen(jprompt->valuestring);
|
||||||
|
if (prompt_len > 800) {
|
||||||
|
if (err != NULL && err_len > 0) {
|
||||||
|
snprintf(err, err_len, "prompt too long, max 800 chars");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
strlcpy(out->prompt, jprompt->valuestring, sizeof(out->prompt));
|
||||||
|
|
||||||
|
cJSON *jsize = cJSON_GetObjectItemCaseSensitive(json, "size");
|
||||||
|
if (cJSON_IsString(jsize) && jsize->valuestring != NULL && jsize->valuestring[0] != '\0') {
|
||||||
|
uint32_t w = 0;
|
||||||
|
uint32_t h = 0;
|
||||||
|
if (!parse_image_size_text(jsize->valuestring, &w, &h)) {
|
||||||
|
if (err != NULL && err_len > 0) {
|
||||||
|
snprintf(err, err_len, "size must be like 1120*1440");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint64_t pixels = (uint64_t)w * (uint64_t)h;
|
||||||
|
if (pixels < (512ULL * 512ULL) || pixels > (2048ULL * 2048ULL)) {
|
||||||
|
if (err != NULL && err_len > 0) {
|
||||||
|
snprintf(err, err_len, "size pixels must be in [512*512, 2048*2048]");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
out->has_size = true;
|
||||||
|
strlcpy(out->size, jsize->valuestring, sizeof(out->size));
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *jextend = cJSON_GetObjectItemCaseSensitive(json, "prompt_extend");
|
||||||
|
out->prompt_extend = rest_server_json_bool_with_default(jextend, false);
|
||||||
|
|
||||||
|
cJSON *jseed = cJSON_GetObjectItemCaseSensitive(json, "seed");
|
||||||
|
if (cJSON_IsNumber(jseed)) {
|
||||||
|
if (jseed->valuedouble < 0 || jseed->valuedouble > 2147483647.0) {
|
||||||
|
if (err != NULL && err_len > 0) {
|
||||||
|
snprintf(err, err_len, "seed must be in [0, 2147483647]");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out->has_seed = true;
|
||||||
|
out->seed = (uint32_t)jseed->valuedouble;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *jtimeout = cJSON_GetObjectItemCaseSensitive(json, "timeout_ms");
|
||||||
|
if (cJSON_IsNumber(jtimeout)) {
|
||||||
|
if (jtimeout->valuedouble < 5000 || jtimeout->valuedouble > 180000) {
|
||||||
|
if (err != NULL && err_len > 0) {
|
||||||
|
snprintf(err, err_len, "timeout_ms must be 5000..180000");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out->timeout_ms = (uint32_t)jtimeout->valuedouble;
|
||||||
|
}
|
||||||
|
|
||||||
|
cJSON *jfetch_timeout = cJSON_GetObjectItemCaseSensitive(json, "fetch_timeout_ms");
|
||||||
|
if (cJSON_IsNumber(jfetch_timeout)) {
|
||||||
|
if (jfetch_timeout->valuedouble < 2000 || jfetch_timeout->valuedouble > 120000) {
|
||||||
|
if (err != NULL && err_len > 0) {
|
||||||
|
snprintf(err, err_len, "fetch_timeout_ms must be 2000..120000");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out->fetch_timeout_ms = (uint32_t)jfetch_timeout->valuedouble;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static esp_err_t decode_image_json_to_raster(cJSON *json,
|
static esp_err_t decode_image_json_to_raster(cJSON *json,
|
||||||
uint16_t *out_width,
|
uint16_t *out_width,
|
||||||
uint16_t *out_height,
|
uint16_t *out_height,
|
||||||
@@ -341,21 +530,26 @@ esp_err_t rest_server_print_raster_post(httpd_req_t *req) {
|
|||||||
return submit_rc;
|
return submit_rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t rest_server_print_image_post(httpd_req_t *req) {
|
static esp_err_t rest_server_send_image_generation_error(httpd_req_t *req,
|
||||||
if (!rest_server_auth_ok(req)) {
|
esp_err_t rc,
|
||||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
const char *msg) {
|
||||||
}
|
const char *message = (msg != NULL && msg[0] != '\0') ? msg : "image generation failed";
|
||||||
|
if (rc == ESP_ERR_INVALID_ARG) {
|
||||||
char content_type[96] = {0};
|
return rest_server_send_error(req, "400 Bad Request", message);
|
||||||
if (httpd_req_get_hdr_value_str(req, "Content-Type", content_type, sizeof(content_type)) == ESP_OK) {
|
}
|
||||||
if (strstr(content_type, "application/json") != NULL) {
|
if (rc == ESP_ERR_INVALID_STATE) {
|
||||||
return rest_server_send_error(req, "400 Bad Request", "use binary png body, not json");
|
return rest_server_send_error(req, "503 Service Unavailable", message);
|
||||||
}
|
}
|
||||||
if (strstr(content_type, "image/png") == NULL) {
|
if (rc == ESP_ERR_TIMEOUT) {
|
||||||
return rest_server_send_error(req, "415 Unsupported Media Type", "Content-Type must be image/png");
|
return rest_server_send_error(req, "504 Gateway Timeout", message);
|
||||||
}
|
}
|
||||||
|
if (rc == ESP_ERR_NO_MEM) {
|
||||||
|
return rest_server_send_error(req, "500 Internal Server Error", message);
|
||||||
|
}
|
||||||
|
return rest_server_send_error(req, "502 Bad Gateway", message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static esp_err_t rest_server_print_image_binary(httpd_req_t *req) {
|
||||||
bool scale_to_width = true;
|
bool scale_to_width = true;
|
||||||
uint8_t threshold = 160;
|
uint8_t threshold = 160;
|
||||||
bool invert = false;
|
bool invert = false;
|
||||||
@@ -409,6 +603,143 @@ esp_err_t rest_server_print_image_post(httpd_req_t *req) {
|
|||||||
return submit_rc;
|
return submit_rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static esp_err_t rest_server_print_image_generate(httpd_req_t *req) {
|
||||||
|
char *body = NULL;
|
||||||
|
cJSON *json = NULL;
|
||||||
|
image_generation_result_t *gen_result = NULL;
|
||||||
|
uint8_t *raster = NULL;
|
||||||
|
char request_id[80] = {0};
|
||||||
|
esp_err_t ret = ESP_FAIL;
|
||||||
|
|
||||||
|
esp_err_t body_err = rest_server_read_body(req, &body);
|
||||||
|
if (body_err != ESP_OK) {
|
||||||
|
return rest_server_send_error(req, "400 Bad Request", "invalid request body");
|
||||||
|
}
|
||||||
|
|
||||||
|
json = cJSON_Parse(body);
|
||||||
|
free(body);
|
||||||
|
body = NULL;
|
||||||
|
if (json == NULL) {
|
||||||
|
return rest_server_send_error(req, "400 Bad Request", "invalid json");
|
||||||
|
}
|
||||||
|
|
||||||
|
image_generate_options_t gen_opt = {0};
|
||||||
|
char parse_err[128] = {0};
|
||||||
|
if (!parse_image_generation_options(json, &gen_opt, parse_err, sizeof(parse_err))) {
|
||||||
|
ret = rest_server_send_error(req,
|
||||||
|
"400 Bad Request",
|
||||||
|
parse_err[0] != '\0' ? parse_err : "invalid image options");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
image_print_options_t print_opt = {0};
|
||||||
|
image_print_options_set_defaults(&print_opt);
|
||||||
|
parse_image_print_options_from_json(json, &print_opt);
|
||||||
|
cJSON_Delete(json);
|
||||||
|
json = NULL;
|
||||||
|
|
||||||
|
image_generation_request_t gen_req = {
|
||||||
|
.prompt = gen_opt.prompt,
|
||||||
|
.size = gen_opt.has_size ? gen_opt.size : NULL,
|
||||||
|
.prompt_extend = gen_opt.prompt_extend,
|
||||||
|
.has_seed = gen_opt.has_seed,
|
||||||
|
.seed = gen_opt.seed,
|
||||||
|
.generation_timeout_ms = gen_opt.timeout_ms,
|
||||||
|
.download_timeout_ms = gen_opt.fetch_timeout_ms,
|
||||||
|
};
|
||||||
|
|
||||||
|
gen_result = (image_generation_result_t *)calloc(1, sizeof(*gen_result));
|
||||||
|
if (gen_result == NULL) {
|
||||||
|
ret = rest_server_send_error(req, "500 Internal Server Error", "no memory");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
image_generation_result_reset(gen_result);
|
||||||
|
|
||||||
|
char model_err[160] = {0};
|
||||||
|
esp_err_t gen_rc = image_generation_generate_png(&gen_req,
|
||||||
|
gen_result,
|
||||||
|
model_err,
|
||||||
|
sizeof(model_err));
|
||||||
|
if (gen_rc != ESP_OK) {
|
||||||
|
ret = rest_server_send_image_generation_error(req, gen_rc, model_err);
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gen_result->request_id[0] != '\0') {
|
||||||
|
strlcpy(request_id, gen_result->request_id, sizeof(request_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t width = 0;
|
||||||
|
uint16_t height = 0;
|
||||||
|
size_t raster_len = 0;
|
||||||
|
char decode_err[128] = {0};
|
||||||
|
esp_err_t decode_rc = raster_tools_convert_png_to_raster_384(gen_result->png,
|
||||||
|
gen_result->png_len,
|
||||||
|
print_opt.scale_to_width,
|
||||||
|
print_opt.threshold,
|
||||||
|
print_opt.invert,
|
||||||
|
print_opt.max_height,
|
||||||
|
&width,
|
||||||
|
&height,
|
||||||
|
&raster,
|
||||||
|
&raster_len,
|
||||||
|
decode_err,
|
||||||
|
sizeof(decode_err));
|
||||||
|
if (decode_rc != ESP_OK) {
|
||||||
|
ret = rest_server_send_error(req,
|
||||||
|
"400 Bad Request",
|
||||||
|
decode_err[0] != '\0' ? decode_err : "decode generated image failed");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
|
||||||
|
char warning[160] = {0};
|
||||||
|
if (request_id[0] != '\0') {
|
||||||
|
snprintf(warning, sizeof(warning), "z-image request_id=%s", request_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = submit_raster_job_and_reply(req,
|
||||||
|
raster,
|
||||||
|
raster_len,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
print_opt.density,
|
||||||
|
warning);
|
||||||
|
|
||||||
|
cleanup:
|
||||||
|
if (body != NULL) {
|
||||||
|
free(body);
|
||||||
|
}
|
||||||
|
if (json != NULL) {
|
||||||
|
cJSON_Delete(json);
|
||||||
|
}
|
||||||
|
if (raster != NULL) {
|
||||||
|
free(raster);
|
||||||
|
}
|
||||||
|
if (gen_result != NULL) {
|
||||||
|
image_generation_result_free(gen_result);
|
||||||
|
free(gen_result);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t rest_server_print_image_post(httpd_req_t *req) {
|
||||||
|
if (!rest_server_auth_ok(req)) {
|
||||||
|
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
char content_type[96] = {0};
|
||||||
|
if (httpd_req_get_hdr_value_str(req, "Content-Type", content_type, sizeof(content_type)) == ESP_OK) {
|
||||||
|
if (strstr(content_type, "application/json") != NULL) {
|
||||||
|
return rest_server_print_image_generate(req);
|
||||||
|
}
|
||||||
|
if (strstr(content_type, "image/png") == NULL) {
|
||||||
|
return rest_server_send_error(req, "415 Unsupported Media Type", "Content-Type must be application/json or image/png");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rest_server_print_image_binary(req);
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t rest_server_print_qr_post(httpd_req_t *req) {
|
esp_err_t rest_server_print_qr_post(httpd_req_t *req) {
|
||||||
if (!rest_server_auth_ok(req)) {
|
if (!rest_server_auth_ok(req)) {
|
||||||
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
return rest_server_send_error(req, "401 Unauthorized", "unauthorized");
|
||||||
|
|||||||
35
main/domain/include/image_generation.h
Normal file
35
main/domain/include/image_generation.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
const char *prompt;
|
||||||
|
const char *size;
|
||||||
|
bool prompt_extend;
|
||||||
|
bool has_seed;
|
||||||
|
uint32_t seed;
|
||||||
|
uint32_t generation_timeout_ms;
|
||||||
|
uint32_t download_timeout_ms;
|
||||||
|
} image_generation_request_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *png;
|
||||||
|
size_t png_len;
|
||||||
|
char image_url[1024];
|
||||||
|
char output_prompt[256];
|
||||||
|
char request_id[80];
|
||||||
|
uint16_t width;
|
||||||
|
uint16_t height;
|
||||||
|
} image_generation_result_t;
|
||||||
|
|
||||||
|
void image_generation_result_reset(image_generation_result_t *result);
|
||||||
|
void image_generation_result_free(image_generation_result_t *result);
|
||||||
|
|
||||||
|
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);
|
||||||
591
main/domain/src/image_generation.c
Normal file
591
main/domain/src/image_generation.c
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
#include "image_generation.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "cJSON.h"
|
||||||
|
#include "esp_crt_bundle.h"
|
||||||
|
#include "esp_heap_caps.h"
|
||||||
|
#include "esp_http_client.h"
|
||||||
|
#include "esp_log.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 "1024*1536"
|
||||||
|
#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
|
||||||
|
|
||||||
|
static const char *TAG = "image_generation";
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t *data;
|
||||||
|
size_t len;
|
||||||
|
size_t cap;
|
||||||
|
size_t max_len;
|
||||||
|
} bytes_buffer_t;
|
||||||
|
|
||||||
|
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 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[1024];
|
||||||
|
while (true) {
|
||||||
|
int n = esp_http_client_read(client, tmp, sizeof(tmp));
|
||||||
|
if (n < 0) {
|
||||||
|
image_generation_fill_err(err, err_len, "http read failed");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
if (n == 0) {
|
||||||
|
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) {
|
||||||
|
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 (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_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 = false,
|
||||||
|
.buffer_size = 2048,
|
||||||
|
.buffer_size_tx = 2048,
|
||||||
|
.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) {
|
||||||
|
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);
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
cJSON_Delete(root);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
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_http_client_config_t config = {
|
||||||
|
.url = out_result->image_url,
|
||||||
|
.method = HTTP_METHOD_GET,
|
||||||
|
.timeout_ms = (int)timeout_ms,
|
||||||
|
.keep_alive_enable = false,
|
||||||
|
.buffer_size = 2048,
|
||||||
|
.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) {
|
||||||
|
bytes_buffer_free(&resp);
|
||||||
|
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");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
extract_remote_error_message((const char *)resp.data, http_status, err, err_len);
|
||||||
|
bytes_buffer_free(&resp);
|
||||||
|
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");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
out_result->png = resp.data;
|
||||||
|
out_result->png_len = resp.len;
|
||||||
|
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) {
|
||||||
|
if (req == NULL || out_result == NULL || req->prompt == NULL || req->prompt[0] == '\0') {
|
||||||
|
image_generation_fill_err(err, err_len, "prompt is required");
|
||||||
|
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 = CONFIG_TQ_Z_IMAGE_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
if (actual.download_timeout_ms == 0) {
|
||||||
|
actual.download_timeout_ms = CONFIG_TQ_Z_IMAGE_DOWNLOAD_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "invoke z-image, prompt_len=%u", (unsigned)strlen(actual.prompt));
|
||||||
|
esp_err_t gen_rc = image_generation_invoke_model(&actual, out_result, err, err_len);
|
||||||
|
if (gen_rc != ESP_OK) {
|
||||||
|
image_generation_result_free(out_result);
|
||||||
|
return gen_rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "download image from model output");
|
||||||
|
esp_err_t dl_rc = image_generation_download_png(out_result, actual.download_timeout_ms, err, err_len);
|
||||||
|
if (dl_rc != ESP_OK) {
|
||||||
|
image_generation_result_free(out_result);
|
||||||
|
return dl_rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
4
partitions.csv
Normal file
4
partitions.csv
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, , 0x6000,
|
||||||
|
phy_init, data, phy, , 0x1000,
|
||||||
|
factory, app, factory, , 4M,
|
||||||
|
@@ -8,7 +8,9 @@ CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=247
|
|||||||
CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n
|
CONFIG_ESP_WIFI_STA_DISCONNECTED_PM_ENABLE=n
|
||||||
CONFIG_TQ_WIFI_SSID="TalkingQ"
|
CONFIG_TQ_WIFI_SSID="TalkingQ"
|
||||||
CONFIG_TQ_WIFI_PASSWORD="TalkingQ123"
|
CONFIG_TQ_WIFI_PASSWORD="TalkingQ123"
|
||||||
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
|
# CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE is not set
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM=y
|
||||||
|
CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv"
|
||||||
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
|
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
|
||||||
CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
|
CONFIG_ESPTOOLPY_FLASHSIZE="16MB"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user