Spaces:
Running
Running
Upload 13 files
Browse files- index.html +26 -15
- package.json +2 -1
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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 |
-
|
| 3080 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 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()
|