486CHD commited on
Commit
f8b8869
·
verified ·
1 Parent(s): e5e7589

Upload 13 files

Browse files
Files changed (3) hide show
  1. index.html +26 -15
  2. package.json +2 -1
  3. server.js +126 -12
index.html CHANGED
@@ -440,7 +440,11 @@
440
  }
441
  const stored = localStorage.getItem(IMAGE_RESPONSE_STORAGE_KEY);
442
  const fallback = Device.isMobile ? 'url' : 'base64';
443
- return normalizeResponseMode(paramMode || stored, fallback);
 
 
 
 
444
  };
445
 
446
  const RESPONSE_MODE = resolveResponseMode();
@@ -869,10 +873,11 @@
869
  const fullUrl = this.getFullImageUrl(item);
870
  if (Device.isMobile) {
871
  if (item.displayUrl) return item.displayUrl;
872
- const source = item.image || fullUrl;
873
  if (source && source.startsWith('data:image')) {
874
  if (source.length > 1200000) {
875
- return item.thumb || fullUrl || PLACEHOLDER_IMAGE;
 
876
  }
877
  const objectUrl = this.dataUrlToObjectUrl(source);
878
  if (objectUrl) {
@@ -881,7 +886,7 @@
881
  }
882
  }
883
  }
884
- return item.image || fullUrl || '';
885
  },
886
 
887
  revokeDisplayUrl(item) {
@@ -1571,7 +1576,8 @@
1571
  img.loading = isPriority ? 'eager' : 'lazy';
1572
  img.decoding = 'async';
1573
  img.fetchPriority = isPriority ? 'high' : 'low';
1574
- const displaySrc = item.thumb || ImageHandler.getDisplayImage(item) || ImageHandler.getFullImageUrl(item) || item.imageUrl;
 
1575
  LazyImageLoader.observe(img, displaySrc);
1576
  img.alt = `作品 ${item.id}`;
1577
  img.onload = () => {
@@ -1681,7 +1687,8 @@
1681
  if (!this.container || !item) return;
1682
  const img = this.container.querySelector(`[data-id="${item.id}"] img`);
1683
  if (!img) return;
1684
- const nextSrc = item.thumb || ImageHandler.getDisplayImage(item) || ImageHandler.getFullImageUrl(item) || item.imageUrl;
 
1685
  if (nextSrc) {
1686
  img.src = nextSrc;
1687
  img.removeAttribute('data-src');
@@ -1698,7 +1705,7 @@
1698
  }
1699
  const candidates = Array.isArray(items) ? items : this.getVisibleItems();
1700
  const queue = candidates.filter((item) => {
1701
- if (item.thumb) return false;
1702
  const source = item.image || item.imageUrl;
1703
  if (!source || typeof source !== 'string') return false;
1704
  return source.startsWith('data:image')
@@ -1721,7 +1728,7 @@
1721
  }
1722
  return;
1723
  }
1724
- const source = item.image || item.imageUrl;
1725
  const thumb = await ImageHandler.createThumbnail(source, maxWidth, quality);
1726
  if (thumb && thumb !== source) {
1727
  item.thumb = thumb;
@@ -1832,7 +1839,8 @@
1832
 
1833
  // 设置主图 - 直接赋�??
1834
  const fullUrl = ImageHandler.getFullImageUrl(item);
1835
- const initialSrc = item.thumb || ImageHandler.getDisplayImage(item) || fullUrl || '';
 
1836
  this.imgEl.src = initialSrc;
1837
  this.imgEl.onerror = () => {
1838
  if (fullUrl && this.imgEl.src !== fullUrl) {
@@ -2446,7 +2454,7 @@
2446
  }
2447
  const candidates = Array.isArray(items) ? items : this.getVisibleItems();
2448
  const queue = candidates.filter((item) => {
2449
- if (item.thumb) return false;
2450
  const source = item.image || item.imageUrl;
2451
  if (!source || typeof source !== 'string') return false;
2452
  return source.startsWith('data:image')
@@ -2469,7 +2477,7 @@
2469
  }
2470
  return;
2471
  }
2472
- const source = item.image || item.imageUrl;
2473
  const thumb = await ImageHandler.createThumbnail(source, maxWidth, quality);
2474
  if (thumb && thumb !== source) {
2475
  item.thumb = thumb;
@@ -2502,7 +2510,8 @@
2502
  img.loading = 'lazy';
2503
  img.decoding = 'async';
2504
  img.fetchPriority = 'low';
2505
- const imageSrc = item.thumb || ImageHandler.getDisplayImage(item) || ImageHandler.getFullImageUrl(item) || item.imageUrl;
 
2506
  LazyImageLoader.observe(img, imageSrc);
2507
  img.alt = item.prompt || '创意作品';
2508
  card.appendChild(img);
@@ -2582,7 +2591,8 @@
2582
  if (!this.container || !item) return;
2583
  const img = this.container.querySelector(`[data-id="${item.id}"] img`);
2584
  if (!img) return;
2585
- const nextSrc = item.thumb || ImageHandler.getDisplayImage(item) || ImageHandler.getFullImageUrl(item) || item.imageUrl;
 
2586
  if (nextSrc) {
2587
  img.src = nextSrc;
2588
  img.removeAttribute('data-src');
@@ -3076,8 +3086,9 @@
3076
  const imageUrl = rawImageUrl || (previewImage && !previewImage.startsWith('data:image') ? previewImage : null);
3077
  const thumbWidth = 768;
3078
  const thumbQuality = 0.76;
3079
- let thumb = null;
3080
- if (!Device.isMobile) {
 
3081
  thumb = await ImageHandler.createThumbnail(previewImage, thumbWidth, thumbQuality);
3082
  }
3083
 
 
440
  }
441
  const stored = localStorage.getItem(IMAGE_RESPONSE_STORAGE_KEY);
442
  const fallback = Device.isMobile ? 'url' : 'base64';
443
+ const resolved = normalizeResponseMode(paramMode || stored, fallback);
444
+ if (Device.isMobile && !paramMode && resolved === 'base64') {
445
+ return fallback;
446
+ }
447
+ return resolved;
448
  };
449
 
450
  const RESPONSE_MODE = resolveResponseMode();
 
873
  const fullUrl = this.getFullImageUrl(item);
874
  if (Device.isMobile) {
875
  if (item.displayUrl) return item.displayUrl;
876
+ const source = resolveAssetUrl(item.image) || fullUrl;
877
  if (source && source.startsWith('data:image')) {
878
  if (source.length > 1200000) {
879
+ const thumbSrc = resolveAssetUrl(item.thumb || item.thumbUrl);
880
+ return thumbSrc || fullUrl || PLACEHOLDER_IMAGE;
881
  }
882
  const objectUrl = this.dataUrlToObjectUrl(source);
883
  if (objectUrl) {
 
886
  }
887
  }
888
  }
889
+ return resolveAssetUrl(item.image) || fullUrl || '';
890
  },
891
 
892
  revokeDisplayUrl(item) {
 
1576
  img.loading = isPriority ? 'eager' : 'lazy';
1577
  img.decoding = 'async';
1578
  img.fetchPriority = isPriority ? 'high' : 'low';
1579
+ const thumbSrc = resolveAssetUrl(item.thumb || item.thumbUrl);
1580
+ const displaySrc = thumbSrc || ImageHandler.getDisplayImage(item) || ImageHandler.getFullImageUrl(item) || resolveAssetUrl(item.imageUrl);
1581
  LazyImageLoader.observe(img, displaySrc);
1582
  img.alt = `作品 ${item.id}`;
1583
  img.onload = () => {
 
1687
  if (!this.container || !item) return;
1688
  const img = this.container.querySelector(`[data-id="${item.id}"] img`);
1689
  if (!img) return;
1690
+ const thumbSrc = resolveAssetUrl(item.thumb || item.thumbUrl);
1691
+ const nextSrc = thumbSrc || ImageHandler.getDisplayImage(item) || ImageHandler.getFullImageUrl(item) || resolveAssetUrl(item.imageUrl);
1692
  if (nextSrc) {
1693
  img.src = nextSrc;
1694
  img.removeAttribute('data-src');
 
1705
  }
1706
  const candidates = Array.isArray(items) ? items : this.getVisibleItems();
1707
  const queue = candidates.filter((item) => {
1708
+ if (item.thumb || item.thumbUrl) return false;
1709
  const source = item.image || item.imageUrl;
1710
  if (!source || typeof source !== 'string') return false;
1711
  return source.startsWith('data:image')
 
1728
  }
1729
  return;
1730
  }
1731
+ const source = resolveAssetUrl(item.image || item.imageUrl);
1732
  const thumb = await ImageHandler.createThumbnail(source, maxWidth, quality);
1733
  if (thumb && thumb !== source) {
1734
  item.thumb = thumb;
 
1839
 
1840
  // 设置主图 - 直接赋�??
1841
  const fullUrl = ImageHandler.getFullImageUrl(item);
1842
+ const thumbSrc = resolveAssetUrl(item.thumb || item.thumbUrl);
1843
+ const initialSrc = thumbSrc || ImageHandler.getDisplayImage(item) || fullUrl || '';
1844
  this.imgEl.src = initialSrc;
1845
  this.imgEl.onerror = () => {
1846
  if (fullUrl && this.imgEl.src !== fullUrl) {
 
2454
  }
2455
  const candidates = Array.isArray(items) ? items : this.getVisibleItems();
2456
  const queue = candidates.filter((item) => {
2457
+ if (item.thumb || item.thumbUrl) return false;
2458
  const source = item.image || item.imageUrl;
2459
  if (!source || typeof source !== 'string') return false;
2460
  return source.startsWith('data:image')
 
2477
  }
2478
  return;
2479
  }
2480
+ const source = resolveAssetUrl(item.image || item.imageUrl);
2481
  const thumb = await ImageHandler.createThumbnail(source, maxWidth, quality);
2482
  if (thumb && thumb !== source) {
2483
  item.thumb = thumb;
 
2510
  img.loading = 'lazy';
2511
  img.decoding = 'async';
2512
  img.fetchPriority = 'low';
2513
+ const thumbSrc = resolveAssetUrl(item.thumb || item.thumbUrl);
2514
+ const imageSrc = thumbSrc || ImageHandler.getDisplayImage(item) || ImageHandler.getFullImageUrl(item) || resolveAssetUrl(item.imageUrl);
2515
  LazyImageLoader.observe(img, imageSrc);
2516
  img.alt = item.prompt || '创意作品';
2517
  card.appendChild(img);
 
2591
  if (!this.container || !item) return;
2592
  const img = this.container.querySelector(`[data-id="${item.id}"] img`);
2593
  if (!img) return;
2594
+ const thumbSrc = resolveAssetUrl(item.thumb || item.thumbUrl);
2595
+ const nextSrc = thumbSrc || ImageHandler.getDisplayImage(item) || ImageHandler.getFullImageUrl(item) || resolveAssetUrl(item.imageUrl);
2596
  if (nextSrc) {
2597
  img.src = nextSrc;
2598
  img.removeAttribute('data-src');
 
3086
  const imageUrl = rawImageUrl || (previewImage && !previewImage.startsWith('data:image') ? previewImage : null);
3087
  const thumbWidth = 768;
3088
  const thumbQuality = 0.76;
3089
+ const serverThumb = typeof data.thumbUrl === 'string' ? data.thumbUrl : null;
3090
+ let thumb = serverThumb;
3091
+ if (!thumb && !Device.isMobile) {
3092
  thumb = await ImageHandler.createThumbnail(previewImage, thumbWidth, thumbQuality);
3093
  }
3094
 
package.json CHANGED
@@ -8,6 +8,7 @@
8
  "dependencies": {
9
  "cookie-parser": "^1.4.6",
10
  "dotenv": "^16.3.1",
11
- "express": "^4.18.2"
 
12
  }
13
  }
 
8
  "dependencies": {
9
  "cookie-parser": "^1.4.6",
10
  "dotenv": "^16.3.1",
11
+ "express": "^4.18.2",
12
+ "sharp": "^0.33.5"
13
  }
14
  }
server.js CHANGED
@@ -10,7 +10,20 @@ import dotenv from 'dotenv';
10
  import path from 'path';
11
  import { fileURLToPath } from 'url';
12
  import fs from 'fs/promises';
13
- import crypto from 'crypto';
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
  // ============================================
16
  // 配置初始化模块
@@ -56,6 +69,9 @@ const DATA_DIR = path.join(__dirname, 'data');
56
  const PUBLIC_GALLERY_FILE = path.join(DATA_DIR, 'public-gallery.json');
57
  const STATS_FILE = path.join(DATA_DIR, 'usage-stats.json');
58
  const GENERATED_DIR = path.join(DATA_DIR, 'generated');
 
 
 
59
 
60
  // ============================================
61
  // Express 应用初始化
@@ -67,6 +83,7 @@ app.use(express.urlencoded({ extended: true, limit: '200mb' }));
67
  app.use(cookieParser());
68
  app.use(express.static(__dirname));
69
  app.use('/generated', express.static(GENERATED_DIR));
 
70
 
71
  // ============================================
72
  // 认证中间件
@@ -356,6 +373,65 @@ const GeneratedImageStore = {
356
  await fs.mkdir(GENERATED_DIR, { recursive: true });
357
  },
358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  async saveDataUrl(dataUrl) {
360
  if (!dataUrl || typeof dataUrl !== 'string') return null;
361
  const match = dataUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/);
@@ -369,12 +445,15 @@ const GeneratedImageStore = {
369
  const filePath = path.join(GENERATED_DIR, filename);
370
 
371
  await this.ensureDir();
372
- await fs.writeFile(filePath, Buffer.from(base64Data, 'base64'));
 
 
373
 
374
  return {
375
  id: filename,
376
  url: `/generated/${filename}`,
377
- mimeType
 
378
  };
379
  },
380
 
@@ -720,23 +799,31 @@ app.post('/api/generate', authMiddleware, async (req, res) => {
720
  const wantsBase64 = returnBase64 !== false;
721
  let imageUrl = null;
722
  let imageId = null;
 
723
 
724
  if (typeof imageData === 'string' && imageData.startsWith('data:image/')) {
725
- if (wantsUrl) {
726
- try {
727
- const saved = await GeneratedImageStore.saveDataUrl(imageData);
728
- if (saved) {
729
- imageUrl = saved.url;
730
- imageId = saved.id;
731
- }
732
- } catch (saveError) {
733
- console.error(`[${new Date().toISOString()}] Save generated image failed:`, saveError);
734
  }
 
 
735
  }
736
  } else if (typeof imageData === 'string') {
737
  imageUrl = imageData;
738
  }
739
 
 
 
 
 
 
 
 
 
740
  console.log(`[${new Date().toISOString()}] Image generated`);
741
 
742
  recordOnce(true);
@@ -748,6 +835,7 @@ app.post('/api/generate', authMiddleware, async (req, res) => {
748
  success: true,
749
  imageUrl,
750
  imageId,
 
751
  prompt: trimmedPrompt,
752
  inputImages: uploadedImages,
753
  timestamp: new Date().toISOString()
@@ -800,9 +888,11 @@ app.get('/api/public-gallery', async (req, res) => {
800
  if (saved) {
801
  item.imageUrl = saved.url;
802
  item.imageId = saved.id;
 
803
  delete item.image;
804
  rest.imageUrl = saved.url;
805
  rest.imageId = saved.id;
 
806
  delete rest.image;
807
  updated = true;
808
  }
@@ -811,6 +901,19 @@ app.get('/api/public-gallery', async (req, res) => {
811
  }
812
  }
813
 
 
 
 
 
 
 
 
 
 
 
 
 
 
814
  if (lite) {
815
  if (Array.isArray(rest.inputImages)) {
816
  rest.inputCount = rest.inputImages.length;
@@ -867,6 +970,7 @@ app.post('/api/public-gallery', authMiddleware, async (req, res) => {
867
  let finalImage = null;
868
  let finalImageUrl = null;
869
  let finalImageId = null;
 
870
 
871
  if (image && typeof image === 'string' && ImageParser.isValidBase64Image(image)) {
872
  finalImage = image;
@@ -888,12 +992,21 @@ app.post('/api/public-gallery', authMiddleware, async (req, res) => {
888
  if (saved) {
889
  finalImageUrl = saved.url;
890
  finalImageId = saved.id;
 
891
  finalImage = null;
892
  }
893
  } catch (error) {
894
  console.error(`[${new Date().toISOString()}] Public gallery save image failed:`, error);
895
  }
896
  }
 
 
 
 
 
 
 
 
897
 
898
  const sanitizedRefs = Array.isArray(inputImages)
899
  ? inputImages
@@ -907,6 +1020,7 @@ app.post('/api/public-gallery', authMiddleware, async (req, res) => {
907
  image: finalImage,
908
  imageUrl: finalImageUrl,
909
  imageId: finalImageId,
 
910
  inputImages: sanitizedRefs,
911
  timestamp: new Date().toISOString(),
912
  deleteToken: generateDeleteToken()
 
10
  import path from 'path';
11
  import { fileURLToPath } from 'url';
12
  import fs from 'fs/promises';
13
+ import crypto from 'crypto';
14
+
15
+ let sharpPromise = null;
16
+ const loadSharp = async () => {
17
+ if (!sharpPromise) {
18
+ sharpPromise = import('sharp')
19
+ .then((mod) => mod.default || mod)
20
+ .catch((error) => {
21
+ console.warn('[WARN] sharp not available, thumbnail generation disabled:', error.message);
22
+ return null;
23
+ });
24
+ }
25
+ return sharpPromise;
26
+ };
27
 
28
  // ============================================
29
  // 配置初始化模块
 
69
  const PUBLIC_GALLERY_FILE = path.join(DATA_DIR, 'public-gallery.json');
70
  const STATS_FILE = path.join(DATA_DIR, 'usage-stats.json');
71
  const GENERATED_DIR = path.join(DATA_DIR, 'generated');
72
+ const THUMB_DIR = path.join(GENERATED_DIR, 'thumbs');
73
+ const THUMB_WIDTH = parsePositiveInt(process.env.THUMB_WIDTH, 1024);
74
+ const THUMB_QUALITY = parsePositiveInt(process.env.THUMB_QUALITY, 82);
75
 
76
  // ============================================
77
  // Express 应用初始化
 
83
  app.use(cookieParser());
84
  app.use(express.static(__dirname));
85
  app.use('/generated', express.static(GENERATED_DIR));
86
+ app.use('/generated/thumbs', express.static(THUMB_DIR));
87
 
88
  // ============================================
89
  // 认证中间件
 
373
  await fs.mkdir(GENERATED_DIR, { recursive: true });
374
  },
375
 
376
+ async ensureThumbDir() {
377
+ await fs.mkdir(THUMB_DIR, { recursive: true });
378
+ },
379
+
380
+ buildThumbName(filename) {
381
+ const baseName = path.basename(filename, path.extname(filename));
382
+ return `${baseName}-thumb.jpg`;
383
+ },
384
+
385
+ async createThumbnailFromBuffer(buffer, filename) {
386
+ const sharp = await loadSharp();
387
+ if (!sharp) return null;
388
+ const thumbName = this.buildThumbName(filename);
389
+ const thumbPath = path.join(THUMB_DIR, thumbName);
390
+ try {
391
+ await this.ensureThumbDir();
392
+ await sharp(buffer)
393
+ .rotate()
394
+ .resize({ width: THUMB_WIDTH })
395
+ .jpeg({ quality: THUMB_QUALITY })
396
+ .toFile(thumbPath);
397
+ return `/generated/thumbs/${thumbName}`;
398
+ } catch (error) {
399
+ console.error(`[${new Date().toISOString()}] Generate thumbnail failed:`, error);
400
+ return null;
401
+ }
402
+ },
403
+
404
+ async ensureThumbnailFromUrl(imageUrl) {
405
+ if (!imageUrl || typeof imageUrl !== 'string') return null;
406
+ if (!imageUrl.startsWith('/generated/')) return null;
407
+ const filename = path.basename(imageUrl);
408
+ const filePath = path.join(GENERATED_DIR, filename);
409
+ const thumbName = this.buildThumbName(filename);
410
+ const thumbPath = path.join(THUMB_DIR, thumbName);
411
+ try {
412
+ await fs.access(thumbPath);
413
+ return `/generated/thumbs/${thumbName}`;
414
+ } catch {
415
+ // continue to generate
416
+ }
417
+
418
+ const sharp = await loadSharp();
419
+ if (!sharp) return null;
420
+ try {
421
+ await fs.access(filePath);
422
+ await this.ensureThumbDir();
423
+ await sharp(filePath)
424
+ .rotate()
425
+ .resize({ width: THUMB_WIDTH })
426
+ .jpeg({ quality: THUMB_QUALITY })
427
+ .toFile(thumbPath);
428
+ return `/generated/thumbs/${thumbName}`;
429
+ } catch (error) {
430
+ console.error(`[${new Date().toISOString()}] Ensure thumbnail failed:`, error);
431
+ return null;
432
+ }
433
+ },
434
+
435
  async saveDataUrl(dataUrl) {
436
  if (!dataUrl || typeof dataUrl !== 'string') return null;
437
  const match = dataUrl.match(/^data:(image\/[a-zA-Z0-9.+-]+);base64,(.+)$/);
 
445
  const filePath = path.join(GENERATED_DIR, filename);
446
 
447
  await this.ensureDir();
448
+ const buffer = Buffer.from(base64Data, 'base64');
449
+ await fs.writeFile(filePath, buffer);
450
+ const thumbUrl = await this.createThumbnailFromBuffer(buffer, filename);
451
 
452
  return {
453
  id: filename,
454
  url: `/generated/${filename}`,
455
+ mimeType,
456
+ thumbUrl
457
  };
458
  },
459
 
 
799
  const wantsBase64 = returnBase64 !== false;
800
  let imageUrl = null;
801
  let imageId = null;
802
+ let thumbUrl = null;
803
 
804
  if (typeof imageData === 'string' && imageData.startsWith('data:image/')) {
805
+ try {
806
+ const saved = await GeneratedImageStore.saveDataUrl(imageData);
807
+ if (saved) {
808
+ imageUrl = saved.url;
809
+ imageId = saved.id;
810
+ thumbUrl = saved.thumbUrl || null;
 
 
 
811
  }
812
+ } catch (saveError) {
813
+ console.error(`[${new Date().toISOString()}] Save generated image failed:`, saveError);
814
  }
815
  } else if (typeof imageData === 'string') {
816
  imageUrl = imageData;
817
  }
818
 
819
+ if (!thumbUrl && imageUrl) {
820
+ try {
821
+ thumbUrl = await GeneratedImageStore.ensureThumbnailFromUrl(imageUrl);
822
+ } catch (thumbError) {
823
+ console.error(`[${new Date().toISOString()}] Ensure thumbnail failed:`, thumbError);
824
+ }
825
+ }
826
+
827
  console.log(`[${new Date().toISOString()}] Image generated`);
828
 
829
  recordOnce(true);
 
835
  success: true,
836
  imageUrl,
837
  imageId,
838
+ thumbUrl,
839
  prompt: trimmedPrompt,
840
  inputImages: uploadedImages,
841
  timestamp: new Date().toISOString()
 
888
  if (saved) {
889
  item.imageUrl = saved.url;
890
  item.imageId = saved.id;
891
+ item.thumbUrl = saved.thumbUrl || null;
892
  delete item.image;
893
  rest.imageUrl = saved.url;
894
  rest.imageId = saved.id;
895
+ rest.thumbUrl = saved.thumbUrl || null;
896
  delete rest.image;
897
  updated = true;
898
  }
 
901
  }
902
  }
903
 
904
+ if (!rest.thumbUrl && rest.imageUrl) {
905
+ try {
906
+ const ensuredThumb = await GeneratedImageStore.ensureThumbnailFromUrl(rest.imageUrl);
907
+ if (ensuredThumb) {
908
+ item.thumbUrl = ensuredThumb;
909
+ rest.thumbUrl = ensuredThumb;
910
+ updated = true;
911
+ }
912
+ } catch (error) {
913
+ console.error(`[${new Date().toISOString()}] Public gallery ensure thumbnail failed:`, error);
914
+ }
915
+ }
916
+
917
  if (lite) {
918
  if (Array.isArray(rest.inputImages)) {
919
  rest.inputCount = rest.inputImages.length;
 
970
  let finalImage = null;
971
  let finalImageUrl = null;
972
  let finalImageId = null;
973
+ let finalThumbUrl = null;
974
 
975
  if (image && typeof image === 'string' && ImageParser.isValidBase64Image(image)) {
976
  finalImage = image;
 
992
  if (saved) {
993
  finalImageUrl = saved.url;
994
  finalImageId = saved.id;
995
+ finalThumbUrl = saved.thumbUrl || null;
996
  finalImage = null;
997
  }
998
  } catch (error) {
999
  console.error(`[${new Date().toISOString()}] Public gallery save image failed:`, error);
1000
  }
1001
  }
1002
+
1003
+ if (!finalThumbUrl && finalImageUrl) {
1004
+ try {
1005
+ finalThumbUrl = await GeneratedImageStore.ensureThumbnailFromUrl(finalImageUrl);
1006
+ } catch (error) {
1007
+ console.error(`[${new Date().toISOString()}] Public gallery ensure thumbnail failed:`, error);
1008
+ }
1009
+ }
1010
 
1011
  const sanitizedRefs = Array.isArray(inputImages)
1012
  ? inputImages
 
1020
  image: finalImage,
1021
  imageUrl: finalImageUrl,
1022
  imageId: finalImageId,
1023
+ thumbUrl: finalThumbUrl,
1024
  inputImages: sanitizedRefs,
1025
  timestamp: new Date().toISOString(),
1026
  deleteToken: generateDeleteToken()