fix: update CF AI image gen to use multipart for FLUX.2 models
All checks were successful
Release / Test backend (push) Successful in 42s
Release / Check ui (push) Successful in 1m41s
Release / Docker (push) Successful in 5m39s
Release / Gitea Release (push) Successful in 30s

Cloudflare Workers AI changed the API for flux-2-dev, flux-2-klein-4b,
and flux-2-klein-9b to require multipart/form-data (instead of JSON) and
now returns {"image":"<base64>"} instead of raw PNG bytes.

- Add requiresMultipart() helper for the three FLUX.2 models
- callImageAPI builds multipart body for those models, JSON for others
- Parse {"image":"<base64>"} JSON response; fall back to raw bytes for legacy models
- Use "steps" field name (not "num_steps") in multipart forms per CF docs
- Book page: capture and display actual backend error message instead of blank 'Error'
This commit is contained in:
root
2026-04-08 22:28:36 +05:00
parent 04e63414a3
commit a904ff4e21
2 changed files with 179 additions and 72 deletions

View File

@@ -164,6 +164,7 @@
let coverPreview = $state<string | null>(null);
let coverSaving = $state(false);
let coverResult = $state<'saved' | 'error' | ''>('');
let coverErrorMsg = $state('');
let coverPromptOpen = $state(false);
function buildCoverPrompt(): string {
@@ -186,6 +187,7 @@
coverGenerating = true;
coverPreview = null;
coverResult = '';
coverErrorMsg = '';
const promptToUse = coverPrompt.trim() || buildCoverPrompt();
try {
let res: Response;
@@ -210,22 +212,25 @@
} else {
res = await fetch('/api/admin/image-gen', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, type: 'cover', prompt: promptToUse })
});
}
if (res.ok) {
const d = await res.json();
coverPreview = d.image_b64 ? `data:${d.content_type ?? 'image/png'};base64,${d.image_b64}` : null;
} else {
coverResult = 'error';
}
} catch {
coverResult = 'error';
} finally {
coverGenerating = false;
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slug, type: 'cover', prompt: promptToUse })
});
}
if (res.ok) {
const d = await res.json();
coverPreview = d.image_b64 ? `data:${d.content_type ?? 'image/png'};base64,${d.image_b64}` : null;
} else {
const d = await res.json().catch(() => ({}));
coverErrorMsg = (d as any).error ?? '';
coverResult = 'error';
}
} catch (e: any) {
coverErrorMsg = e?.message ?? '';
coverResult = 'error';
} finally {
coverGenerating = false;
}
}
async function saveCover() {
const slug = data.book?.slug;
@@ -258,6 +263,7 @@
let chapterCoverGenerating = $state(false);
let chapterCoverPreview = $state<string | null>(null);
let chapterCoverResult = $state<'saved' | 'error' | ''>('');
let chapterCoverErrorMsg = $state('');
let chapterCoverSaving = $state(false);
let chapterCoverPrompt = $state('');
@@ -269,6 +275,7 @@
chapterCoverGenerating = true;
chapterCoverPreview = null;
chapterCoverResult = '';
chapterCoverErrorMsg = '';
const promptToUse = chapterCoverPrompt.trim() || `Chapter ${n} illustration for "${data.book?.title ?? slug}". Dramatic scene, vivid colors, detailed art, cinematic lighting.`;
try {
const res = await fetch('/api/admin/image-gen', {
@@ -280,9 +287,12 @@
const d = await res.json();
chapterCoverPreview = d.image_b64 ? `data:${d.content_type ?? 'image/png'};base64,${d.image_b64}` : null;
} else {
const d = await res.json().catch(() => ({}));
chapterCoverErrorMsg = (d as any).error ?? '';
chapterCoverResult = 'error';
}
} catch {
} catch (e: any) {
chapterCoverErrorMsg = e?.message ?? '';
chapterCoverResult = 'error';
} finally {
chapterCoverGenerating = false;
@@ -1101,7 +1111,7 @@
{m.book_detail_admin_generate()}{coverUseAsRef ? ' (img2img)' : ''}
</button>
{#if coverResult === 'error'}
<span class="text-xs text-(--color-danger)">{m.common_error()}</span>
<span class="text-xs text-(--color-danger)">{coverErrorMsg || m.common_error()}</span>
{:else if coverResult === 'saved'}
<span class="text-xs text-green-400">{m.book_detail_admin_saved()}</span>
{/if}
@@ -1159,7 +1169,7 @@
{m.book_detail_admin_generate()}
</button>
{#if chapterCoverResult === 'error'}
<span class="text-xs text-(--color-danger)">{m.common_error()}</span>
<span class="text-xs text-(--color-danger)">{chapterCoverErrorMsg || m.common_error()}</span>
{:else if chapterCoverResult === 'saved'}
<span class="text-xs text-green-400">{m.book_detail_admin_saved()}</span>
{/if}