Skip to content

Commit afc53ee

Browse files
committed
fix(image): normalize local image rendering for url/base64/b64_json
1 parent 3448431 commit afc53ee

3 files changed

Lines changed: 54 additions & 12 deletions

File tree

app/services/grok/imagine_experimental.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -239,12 +239,15 @@ async def convert_urls(self, token: str, urls: Iterable[str], response_format: s
239239
continue
240240
if mode == "url":
241241
path = _normalize_asset_path(raw)
242+
if path in {"", "/"}:
243+
continue
244+
await dl.download(path, token, "image")
242245
app_url = str(get_config("app.app_url", "") or "").strip()
246+
local_path = f"/v1/files/image{path}"
243247
if app_url:
244-
await dl.download(path, token, "image")
245-
out.append(f"{app_url.rstrip('/')}/v1/files/image{path}")
248+
out.append(f"{app_url.rstrip('/')}{local_path}")
246249
else:
247-
out.append(f"{ASSET_API}{path}")
250+
out.append(local_path)
248251
continue
249252

250253
data_uri = await dl.to_base64(raw, token, "image")
@@ -383,4 +386,3 @@ async def _stream_response():
383386
"IMAGE_METHODS",
384387
"resolve_image_generation_method",
385388
]
386-

app/services/grok/processor.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,19 @@ async def process_url(self, path: str, media_type: str = "image") -> str:
6868

6969
if not path.startswith("/"):
7070
path = f"/{path}"
71-
71+
72+
# Invalid root path is not a displayable image URL.
73+
if path in {"", "/"}:
74+
return ""
75+
76+
# Always materialize to local cache endpoint so callers don't rely on
77+
# direct assets.grok.com access (often blocked without upstream cookies).
78+
dl_service = self._get_dl()
79+
await dl_service.download(path, self.token, media_type)
80+
local_path = f"/v1/files/{media_type}{path}"
7281
if self.app_url:
73-
dl_service = self._get_dl()
74-
await dl_service.download(path, self.token, media_type)
75-
return f"{self.app_url.rstrip('/')}/v1/files/{media_type}{path}"
76-
else:
77-
return f"{ASSET_URL.rstrip('/')}{path}"
82+
return f"{self.app_url.rstrip('/')}{local_path}"
83+
return local_path
7884

7985
def _sse(self, content: str = "", role: str = None, finish: str = None) -> str:
8086
"""构建 SSE 响应 (StreamProcessor 通用)"""

app/static/chat/chat.js

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,37 @@ function toAbsoluteUrl(url) {
4343
}
4444
}
4545

46+
function detectBase64ImageMime(base64Text) {
47+
const s = String(base64Text || '').trim().replace(/\s+/g, '');
48+
if (!s) return 'image/png';
49+
if (s.startsWith('/9j/')) return 'image/jpeg';
50+
if (s.startsWith('iVBORw0KGgo')) return 'image/png';
51+
if (s.startsWith('UklGR')) return 'image/webp';
52+
if (s.startsWith('R0lGOD')) return 'image/gif';
53+
if (s.startsWith('Qk')) return 'image/bmp';
54+
return 'image/png';
55+
}
56+
57+
function toImageDataUrl(raw) {
58+
const value = String(raw || '').trim();
59+
if (!value) return '';
60+
if (value.startsWith('data:image/')) return value;
61+
const mime = detectBase64ImageMime(value);
62+
return `data:${mime};base64,${value}`;
63+
}
64+
65+
function pickImageSrc(item) {
66+
const rawUrl = String(item?.url || '').trim();
67+
if (rawUrl && rawUrl !== 'https://assets.grok.com/' && rawUrl !== 'https://assets.grok.com') {
68+
return toAbsoluteUrl(rawUrl);
69+
}
70+
const b64json = String(item?.b64_json || '').trim();
71+
if (b64json) return toImageDataUrl(b64json);
72+
const base64 = String(item?.base64 || '').trim();
73+
if (base64) return toImageDataUrl(base64);
74+
return '';
75+
}
76+
4677
function showUserMsg(role, content) {
4778
const wrap = document.createElement('div');
4879
wrap.className = 'msg';
@@ -418,22 +449,25 @@ async function generateImage() {
418449
const res = await fetch('/v1/images/generations', {
419450
method: 'POST',
420451
headers,
421-
body: JSON.stringify({ prompt, model, n, response_format: 'url' }),
452+
body: JSON.stringify({ prompt, model, n }),
422453
});
423454
const data = await res.json().catch(() => ({}));
424455
if (!res.ok) throw new Error(data?.error?.message || data?.detail || `HTTP ${res.status}`);
425456

426457
const items = Array.isArray(data?.data) ? data.data : [];
427458
if (!items.length) throw new Error('没有生成结果');
428459

460+
let rendered = 0;
429461
items.forEach((it) => {
430-
const url = it.url ? String(it.url) : it.b64_json ? `data:image/png;base64,${String(it.b64_json)}` : '';
462+
const url = pickImageSrc(it);
431463
if (!url) return;
464+
rendered += 1;
432465
const card = document.createElement('div');
433466
card.className = 'result-card';
434467
card.innerHTML = `<img src="${escapeHtml(url)}" alt="image" />`;
435468
q('image-results').appendChild(card);
436469
});
470+
if (!rendered) throw new Error('图片返回为空或格式不支持');
437471
} catch (e) {
438472
showToast('生图失败: ' + (e?.message || e), 'error');
439473
}

0 commit comments

Comments
 (0)