Spaces:
Sleeping
Sleeping
Commit
·
354cde7
1
Parent(s):
7f5e77e
fixed hunyuan size. fixed saving. fixed image preview size. added large preview.
Browse files- public/index.html +198 -12
- server.js +78 -71
public/index.html
CHANGED
|
@@ -339,9 +339,9 @@
|
|
| 339 |
|
| 340 |
.card img {
|
| 341 |
width: 100%;
|
|
|
|
| 342 |
display: block;
|
| 343 |
-
|
| 344 |
-
object-fit: cover;
|
| 345 |
}
|
| 346 |
|
| 347 |
.card .meta {
|
|
@@ -627,6 +627,44 @@
|
|
| 627 |
width: 100%;
|
| 628 |
}
|
| 629 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 630 |
</style>
|
| 631 |
</head>
|
| 632 |
<body>
|
|
@@ -923,6 +961,12 @@
|
|
| 923 |
const img = document.createElement('img');
|
| 924 |
img.src = dataUrl;
|
| 925 |
img.alt = params.prompt || 'image';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 926 |
|
| 927 |
const meta = document.createElement('div');
|
| 928 |
meta.className = 'meta';
|
|
@@ -955,10 +999,19 @@
|
|
| 955 |
<div style="margin-bottom: 4px;">尺寸: ${params.width}x${params.height}</div>
|
| 956 |
<div style="margin-bottom: 4px;">步数: ${params.num_inference_steps} | 引导: ${params.guidance_scale}</div>
|
| 957 |
<div style="margin-bottom: 8px; font-size: 12px; line-height: 1.3; word-break: break-all;">${params.prompt}</div>
|
| 958 |
-
<div class="row">
|
| 959 |
-
<button class="secondary" onclick="downloadImageMobile('${filename}', '${dataUrl}')">下载</button>
|
| 960 |
-
</div>
|
| 961 |
`;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 962 |
|
| 963 |
meta.appendChild(metaHeader);
|
| 964 |
meta.appendChild(metaContent);
|
|
@@ -980,6 +1033,115 @@
|
|
| 980 |
}
|
| 981 |
}
|
| 982 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
// 拖拽功能
|
| 984 |
let dragState = {
|
| 985 |
isDragging: false,
|
|
@@ -1172,6 +1334,9 @@
|
|
| 1172 |
async function saveToChosenFolder(filename, dataUrl) {
|
| 1173 |
if (!state.folderHandle) return false;
|
| 1174 |
try {
|
|
|
|
|
|
|
|
|
|
| 1175 |
const fileHandle = await state.folderHandle.getFileHandle(filename, { create: true });
|
| 1176 |
const writable = await fileHandle.createWritable();
|
| 1177 |
await writable.write(dataURLtoBlob(dataUrl));
|
|
@@ -1200,9 +1365,18 @@
|
|
| 1200 |
return;
|
| 1201 |
}
|
| 1202 |
const handle = await window.showDirectoryPicker();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1203 |
state.folderHandle = handle;
|
| 1204 |
-
|
| 1205 |
-
qs('#
|
|
|
|
| 1206 |
} catch(e) {
|
| 1207 |
qs('#folderStatus').textContent = '未选择';
|
| 1208 |
qs('#folderStatusMobile').textContent = '未选择';
|
|
@@ -1347,8 +1521,16 @@
|
|
| 1347 |
const fname = `image_${Date.now()}_${Math.random().toString(16).slice(2)}.jpg`;
|
| 1348 |
updateCardWithImage(placeholder, dataUrl, p, fname);
|
| 1349 |
|
|
|
|
| 1350 |
if (state.folderHandle) {
|
| 1351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1352 |
}
|
| 1353 |
|
| 1354 |
if (state.sound) {
|
|
@@ -1458,10 +1640,14 @@
|
|
| 1458 |
// 键盘事件
|
| 1459 |
document.addEventListener('keydown', (e) => {
|
| 1460 |
if (e.key === 'Enter' && !state.sidebarOpen) generate();
|
| 1461 |
-
if (e.key === 'Escape'
|
| 1462 |
-
|
| 1463 |
-
|
| 1464 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1465 |
window.addEventListener('resize', checkMobile);
|
| 1466 |
checkMobile();
|
| 1467 |
|
|
|
|
| 339 |
|
| 340 |
.card img {
|
| 341 |
width: 100%;
|
| 342 |
+
height: auto;
|
| 343 |
display: block;
|
| 344 |
+
object-fit: contain;
|
|
|
|
| 345 |
}
|
| 346 |
|
| 347 |
.card .meta {
|
|
|
|
| 627 |
width: 100%;
|
| 628 |
}
|
| 629 |
}
|
| 630 |
+
|
| 631 |
+
/* Image Viewer (Lightbox) */
|
| 632 |
+
.image-viewer-overlay {
|
| 633 |
+
position: fixed;
|
| 634 |
+
inset: 0;
|
| 635 |
+
background: rgba(0, 0, 0, 0.7);
|
| 636 |
+
display: none;
|
| 637 |
+
align-items: center;
|
| 638 |
+
justify-content: center;
|
| 639 |
+
z-index: 1000;
|
| 640 |
+
}
|
| 641 |
+
.image-viewer-overlay.show { display: flex; }
|
| 642 |
+
.image-viewer-content {
|
| 643 |
+
position: relative;
|
| 644 |
+
max-width: 90vw;
|
| 645 |
+
max-height: 90vh;
|
| 646 |
+
}
|
| 647 |
+
.image-viewer-content img {
|
| 648 |
+
max-width: 90vw;
|
| 649 |
+
max-height: 90vh;
|
| 650 |
+
width: auto;
|
| 651 |
+
height: auto;
|
| 652 |
+
object-fit: contain;
|
| 653 |
+
box-shadow: var(--shadow-lg);
|
| 654 |
+
border-radius: 8px;
|
| 655 |
+
}
|
| 656 |
+
.image-viewer-close {
|
| 657 |
+
position: absolute;
|
| 658 |
+
right: 8px;
|
| 659 |
+
top: 8px;
|
| 660 |
+
background: var(--bg);
|
| 661 |
+
color: var(--fg);
|
| 662 |
+
border: 1px solid var(--border);
|
| 663 |
+
border-radius: 6px;
|
| 664 |
+
padding: 6px 10px;
|
| 665 |
+
cursor: pointer;
|
| 666 |
+
min-height: auto;
|
| 667 |
+
}
|
| 668 |
</style>
|
| 669 |
</head>
|
| 670 |
<body>
|
|
|
|
| 961 |
const img = document.createElement('img');
|
| 962 |
img.src = dataUrl;
|
| 963 |
img.alt = params.prompt || 'image';
|
| 964 |
+
img.style.cursor = 'zoom-in';
|
| 965 |
+
img.addEventListener('click', function(e) {
|
| 966 |
+
e.preventDefault();
|
| 967 |
+
e.stopPropagation();
|
| 968 |
+
openImageViewer(dataUrl, params);
|
| 969 |
+
});
|
| 970 |
|
| 971 |
const meta = document.createElement('div');
|
| 972 |
meta.className = 'meta';
|
|
|
|
| 999 |
<div style="margin-bottom: 4px;">尺寸: ${params.width}x${params.height}</div>
|
| 1000 |
<div style="margin-bottom: 4px;">步数: ${params.num_inference_steps} | 引导: ${params.guidance_scale}</div>
|
| 1001 |
<div style="margin-bottom: 8px; font-size: 12px; line-height: 1.3; word-break: break-all;">${params.prompt}</div>
|
|
|
|
|
|
|
|
|
|
| 1002 |
`;
|
| 1003 |
+
const row = document.createElement('div');
|
| 1004 |
+
row.className = 'row';
|
| 1005 |
+
const dlBtn = document.createElement('button');
|
| 1006 |
+
dlBtn.className = 'secondary';
|
| 1007 |
+
dlBtn.textContent = '下载';
|
| 1008 |
+
dlBtn.addEventListener('click', function(e) {
|
| 1009 |
+
e.preventDefault();
|
| 1010 |
+
e.stopPropagation();
|
| 1011 |
+
downloadImageMobile(filename, dataUrl);
|
| 1012 |
+
});
|
| 1013 |
+
row.appendChild(dlBtn);
|
| 1014 |
+
metaContent.appendChild(row);
|
| 1015 |
|
| 1016 |
meta.appendChild(metaHeader);
|
| 1017 |
meta.appendChild(metaContent);
|
|
|
|
| 1033 |
}
|
| 1034 |
}
|
| 1035 |
|
| 1036 |
+
// Image Viewer (Lightbox)
|
| 1037 |
+
function ensureImageViewer() {
|
| 1038 |
+
let overlay = document.getElementById('imageViewer');
|
| 1039 |
+
if (!overlay) {
|
| 1040 |
+
overlay = document.createElement('div');
|
| 1041 |
+
overlay.id = 'imageViewer';
|
| 1042 |
+
overlay.className = 'image-viewer-overlay';
|
| 1043 |
+
const content = document.createElement('div');
|
| 1044 |
+
content.className = 'image-viewer-content';
|
| 1045 |
+
const img = document.createElement('img');
|
| 1046 |
+
img.alt = 'preview';
|
| 1047 |
+
const closeBtn = document.createElement('button');
|
| 1048 |
+
closeBtn.className = 'image-viewer-close';
|
| 1049 |
+
closeBtn.textContent = '关闭';
|
| 1050 |
+
closeBtn.addEventListener('click', function(e) {
|
| 1051 |
+
e.preventDefault();
|
| 1052 |
+
e.stopPropagation();
|
| 1053 |
+
closeImageViewer();
|
| 1054 |
+
});
|
| 1055 |
+
// click outside content area closes viewer
|
| 1056 |
+
overlay.addEventListener('click', function(e) {
|
| 1057 |
+
if (e.target === overlay) {
|
| 1058 |
+
closeImageViewer();
|
| 1059 |
+
}
|
| 1060 |
+
});
|
| 1061 |
+
content.appendChild(img);
|
| 1062 |
+
content.appendChild(closeBtn);
|
| 1063 |
+
overlay.appendChild(content);
|
| 1064 |
+
document.body.appendChild(overlay);
|
| 1065 |
+
}
|
| 1066 |
+
return overlay;
|
| 1067 |
+
}
|
| 1068 |
+
|
| 1069 |
+
function openImageViewer(dataUrl, params) {
|
| 1070 |
+
const overlay = ensureImageViewer();
|
| 1071 |
+
const img = overlay.querySelector('.image-viewer-content img');
|
| 1072 |
+
img.src = dataUrl;
|
| 1073 |
+
img.alt = (params && params.prompt) ? params.prompt : 'image';
|
| 1074 |
+
overlay.classList.add('show');
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
function closeImageViewer() {
|
| 1078 |
+
const overlay = document.getElementById('imageViewer');
|
| 1079 |
+
if (overlay) {
|
| 1080 |
+
overlay.classList.remove('show');
|
| 1081 |
+
const img = overlay.querySelector('.image-viewer-content img');
|
| 1082 |
+
if (img) img.src = '';
|
| 1083 |
+
}
|
| 1084 |
+
}
|
| 1085 |
+
|
| 1086 |
+
function isViewerOpen() {
|
| 1087 |
+
const overlay = document.getElementById('imageViewer');
|
| 1088 |
+
return !!(overlay && overlay.classList.contains('show'));
|
| 1089 |
+
}
|
| 1090 |
+
// Image Viewer (Lightbox)
|
| 1091 |
+
function ensureImageViewer() {
|
| 1092 |
+
let overlay = document.getElementById('imageViewer');
|
| 1093 |
+
if (!overlay) {
|
| 1094 |
+
overlay = document.createElement('div');
|
| 1095 |
+
overlay.id = 'imageViewer';
|
| 1096 |
+
overlay.className = 'image-viewer-overlay';
|
| 1097 |
+
const content = document.createElement('div');
|
| 1098 |
+
content.className = 'image-viewer-content';
|
| 1099 |
+
const img = document.createElement('img');
|
| 1100 |
+
img.alt = 'preview';
|
| 1101 |
+
const closeBtn = document.createElement('button');
|
| 1102 |
+
closeBtn.className = 'image-viewer-close';
|
| 1103 |
+
closeBtn.textContent = '关闭';
|
| 1104 |
+
closeBtn.addEventListener('click', function(e) {
|
| 1105 |
+
e.preventDefault();
|
| 1106 |
+
e.stopPropagation();
|
| 1107 |
+
closeImageViewer();
|
| 1108 |
+
});
|
| 1109 |
+
// click outside content area closes viewer
|
| 1110 |
+
overlay.addEventListener('click', function(e) {
|
| 1111 |
+
if (e.target === overlay) {
|
| 1112 |
+
closeImageViewer();
|
| 1113 |
+
}
|
| 1114 |
+
});
|
| 1115 |
+
content.appendChild(img);
|
| 1116 |
+
content.appendChild(closeBtn);
|
| 1117 |
+
overlay.appendChild(content);
|
| 1118 |
+
document.body.appendChild(overlay);
|
| 1119 |
+
}
|
| 1120 |
+
return overlay;
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
function openImageViewer(dataUrl, params) {
|
| 1124 |
+
const overlay = ensureImageViewer();
|
| 1125 |
+
const img = overlay.querySelector('.image-viewer-content img');
|
| 1126 |
+
img.src = dataUrl;
|
| 1127 |
+
img.alt = (params && params.prompt) ? params.prompt : 'image';
|
| 1128 |
+
overlay.classList.add('show');
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
function closeImageViewer() {
|
| 1132 |
+
const overlay = document.getElementById('imageViewer');
|
| 1133 |
+
if (overlay) {
|
| 1134 |
+
overlay.classList.remove('show');
|
| 1135 |
+
const img = overlay.querySelector('.image-viewer-content img');
|
| 1136 |
+
if (img) img.src = '';
|
| 1137 |
+
}
|
| 1138 |
+
}
|
| 1139 |
+
|
| 1140 |
+
function isViewerOpen() {
|
| 1141 |
+
const overlay = document.getElementById('imageViewer');
|
| 1142 |
+
return !!(overlay && overlay.classList.contains('show'));
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
// 拖拽功能
|
| 1146 |
let dragState = {
|
| 1147 |
isDragging: false,
|
|
|
|
| 1334 |
async function saveToChosenFolder(filename, dataUrl) {
|
| 1335 |
if (!state.folderHandle) return false;
|
| 1336 |
try {
|
| 1337 |
+
// 如果无写入权限,避免在非用户激活上下文触发授权提示
|
| 1338 |
+
const perm = await state.folderHandle.queryPermission({ mode: 'readwrite' });
|
| 1339 |
+
if (perm !== 'granted') return false;
|
| 1340 |
const fileHandle = await state.folderHandle.getFileHandle(filename, { create: true });
|
| 1341 |
const writable = await fileHandle.createWritable();
|
| 1342 |
await writable.write(dataURLtoBlob(dataUrl));
|
|
|
|
| 1365 |
return;
|
| 1366 |
}
|
| 1367 |
const handle = await window.showDirectoryPicker();
|
| 1368 |
+
// 在用户点击的上下文中请求写入权限
|
| 1369 |
+
let perm = 'prompt';
|
| 1370 |
+
if ('queryPermission' in handle && 'requestPermission' in handle) {
|
| 1371 |
+
perm = await handle.queryPermission({ mode: 'readwrite' });
|
| 1372 |
+
if (perm !== 'granted') {
|
| 1373 |
+
perm = await handle.requestPermission({ mode: 'readwrite' });
|
| 1374 |
+
}
|
| 1375 |
+
}
|
| 1376 |
state.folderHandle = handle;
|
| 1377 |
+
const ok = perm === 'granted';
|
| 1378 |
+
qs('#folderStatus').textContent = ok ? '已选择(可写)' : '已选择(只读)';
|
| 1379 |
+
qs('#folderStatusMobile').textContent = ok ? '已选择(可写)' : '已选择(只读)';
|
| 1380 |
} catch(e) {
|
| 1381 |
qs('#folderStatus').textContent = '未选择';
|
| 1382 |
qs('#folderStatusMobile').textContent = '未选择';
|
|
|
|
| 1521 |
const fname = `image_${Date.now()}_${Math.random().toString(16).slice(2)}.jpg`;
|
| 1522 |
updateCardWithImage(placeholder, dataUrl, p, fname);
|
| 1523 |
|
| 1524 |
+
let saved = false;
|
| 1525 |
if (state.folderHandle) {
|
| 1526 |
+
try {
|
| 1527 |
+
saved = await saveToChosenFolder(fname, dataUrl);
|
| 1528 |
+
} catch(e) {
|
| 1529 |
+
saved = false;
|
| 1530 |
+
}
|
| 1531 |
+
}
|
| 1532 |
+
if (!saved) {
|
| 1533 |
+
downloadImageMobile(fname, dataUrl);
|
| 1534 |
}
|
| 1535 |
|
| 1536 |
if (state.sound) {
|
|
|
|
| 1640 |
// 键盘事件
|
| 1641 |
document.addEventListener('keydown', (e) => {
|
| 1642 |
if (e.key === 'Enter' && !state.sidebarOpen) generate();
|
| 1643 |
+
if (e.key === 'Escape') {
|
| 1644 |
+
if (isViewerOpen()) {
|
| 1645 |
+
closeImageViewer();
|
| 1646 |
+
} else if (state.sidebarOpen) {
|
| 1647 |
+
closeSidebar();
|
| 1648 |
+
}
|
| 1649 |
+
}
|
| 1650 |
+
});// 响应式检测
|
| 1651 |
window.addEventListener('resize', checkMobile);
|
| 1652 |
checkMobile();
|
| 1653 |
|
server.js
CHANGED
|
@@ -62,11 +62,17 @@ app.use(helmet.contentSecurityPolicy({
|
|
| 62 |
useDefaults: true,
|
| 63 |
directives: {
|
| 64 |
"default-src": ["'self'"],
|
|
|
|
| 65 |
"script-src": ["'self'", "'unsafe-inline'"],
|
|
|
|
|
|
|
| 66 |
"style-src": ["'self'", "'unsafe-inline'"],
|
|
|
|
| 67 |
"img-src": ["'self'", "data:", "blob:"],
|
| 68 |
-
|
| 69 |
-
"
|
|
|
|
|
|
|
| 70 |
"media-src": ["'self'", "data:", "blob:"],
|
| 71 |
"frame-ancestors": ["'self'", "https://huggingface.co", "https://*.huggingface.co"]
|
| 72 |
}
|
|
@@ -369,12 +375,10 @@ app.post('/api/generate', async (req, res) => {
|
|
| 369 |
// Minimal payload (some models reject extended fields). Include size/steps/seed for hunyuan-image-3 compatibility
|
| 370 |
const variantCMinimal = {
|
| 371 |
model: targetModel,
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
...(flat.seed !== null ? { seed: flat.seed } : {})
|
| 377 |
-
}
|
| 378 |
};
|
| 379 |
|
| 380 |
// duplicate removed
|
|
@@ -457,78 +461,81 @@ app.post('/api/generate', async (req, res) => {
|
|
| 457 |
result = await tryCall(variantCMinimal, 'minimal', generateUrl);
|
| 458 |
} catch (e0) { /* continue */ }
|
| 459 |
}
|
| 460 |
-
|
| 461 |
-
try {
|
| 462 |
-
result = await tryCall(variantA, 'nested', generateUrl);
|
| 463 |
-
} catch (e1) {
|
| 464 |
try {
|
| 465 |
-
result = await tryCall(
|
| 466 |
-
} catch (
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
const
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
try {
|
| 490 |
-
result = await tryCall(
|
| 491 |
-
} catch (
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
}
|
| 502 |
-
// success with fallback
|
| 503 |
-
return res.json({
|
| 504 |
-
ok: true,
|
| 505 |
-
image: `data:${result.contentType};base64,${result.imageBase64}`,
|
| 506 |
-
contentType: result.contentType,
|
| 507 |
-
meta: {
|
| 508 |
-
model: flat.model,
|
| 509 |
-
upstream_model: targetModel,
|
| 510 |
-
fallback_used: true,
|
| 511 |
-
fallback_model: fallbackModel,
|
| 512 |
-
width: flat.width,
|
| 513 |
-
height: flat.height,
|
| 514 |
-
guidance_scale: flat.guidance_scale,
|
| 515 |
-
num_inference_steps: flat.num_inference_steps,
|
| 516 |
-
seed: flat.seed
|
| 517 |
-
},
|
| 518 |
-
tried: result.tried
|
| 519 |
-
});
|
| 520 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
}
|
| 522 |
-
const status = e2.status || 502;
|
| 523 |
-
return res.status(status).json({
|
| 524 |
-
ok: false,
|
| 525 |
-
error: e2.hint || e2.message || 'Upstream error',
|
| 526 |
-
code: e2.code || 'UPSTREAM_ERROR',
|
| 527 |
-
upstream_model: targetModel
|
| 528 |
-
});
|
| 529 |
}
|
| 530 |
}
|
| 531 |
|
|
|
|
|
|
|
| 532 |
return res.json({
|
| 533 |
ok: true,
|
| 534 |
image: `data:${result.contentType};base64,${result.imageBase64}`,
|
|
|
|
| 62 |
useDefaults: true,
|
| 63 |
directives: {
|
| 64 |
"default-src": ["'self'"],
|
| 65 |
+
// Allow SPA inline scripts but forbid inline event attributes per CSP3
|
| 66 |
"script-src": ["'self'", "'unsafe-inline'"],
|
| 67 |
+
"script-src-attr": ["'none'"],
|
| 68 |
+
// Permit external stylesheet from Baomitu CDN via style-src-elem
|
| 69 |
"style-src": ["'self'", "'unsafe-inline'"],
|
| 70 |
+
"style-src-elem": ["'self'", "'unsafe-inline'", "https://lib.baomitu.com"],
|
| 71 |
"img-src": ["'self'", "data:", "blob:"],
|
| 72 |
+
// Allow Baomitu icon fonts
|
| 73 |
+
"font-src": ["'self'", "data:", "https://lib.baomitu.com"],
|
| 74 |
+
// Allow CSS sourcemap requests to Baomitu CDN
|
| 75 |
+
"connect-src": ["'self'", "https://image.chutes.ai", "https://lib.baomitu.com"],
|
| 76 |
"media-src": ["'self'", "data:", "blob:"],
|
| 77 |
"frame-ancestors": ["'self'", "https://huggingface.co", "https://*.huggingface.co"]
|
| 78 |
}
|
|
|
|
| 375 |
// Minimal payload (some models reject extended fields). Include size/steps/seed for hunyuan-image-3 compatibility
|
| 376 |
const variantCMinimal = {
|
| 377 |
model: targetModel,
|
| 378 |
+
prompt: flat.prompt,
|
| 379 |
+
size: `${flat.width}x${flat.height}`,
|
| 380 |
+
steps: flat.num_inference_steps,
|
| 381 |
+
...(flat.seed !== null ? { seed: flat.seed } : {})
|
|
|
|
|
|
|
| 382 |
};
|
| 383 |
|
| 384 |
// duplicate removed
|
|
|
|
| 461 |
result = await tryCall(variantCMinimal, 'minimal', generateUrl);
|
| 462 |
} catch (e0) { /* continue */ }
|
| 463 |
}
|
| 464 |
+
else {
|
|
|
|
|
|
|
|
|
|
| 465 |
try {
|
| 466 |
+
result = await tryCall(variantA, 'nested', generateUrl);
|
| 467 |
+
} catch (e1) {
|
| 468 |
+
try {
|
| 469 |
+
result = await tryCall(variantB, 'flat', generateUrl);
|
| 470 |
+
} catch (e2) {
|
| 471 |
+
// Auto fallback to another model when capacity/infrastructure errors (disabled when NO_FALLBACK=true)
|
| 472 |
+
const capacityCodes = ['UPSTREAM_CAPACITY_EXHAUSTED','UPSTREAM_NO_INSTANCES','UPSTREAM_INFRASTRUCTURE','UPSTREAM_BAD_GATEWAY'];
|
| 473 |
+
if (AUTO_FALLBACK && !NO_FALLBACK && capacityCodes.includes(e2.code || '') ) {
|
| 474 |
+
// choose fallback model (prefer free and different from current)
|
| 475 |
+
function chooseFallback(currentId, list) {
|
| 476 |
+
const key = (currentId || '').toLowerCase();
|
| 477 |
+
const free = list.filter(m => m.free && (m.id || m.name || '').toLowerCase() !== key);
|
| 478 |
+
if (free.length) return (free[0].upstream_id || free[0].id || free[0].name);
|
| 479 |
+
const any = list.find(m => (m.id || m.name || '').toLowerCase() !== key);
|
| 480 |
+
if (any) return (any.upstream_id || any.id || any.name);
|
| 481 |
+
return null;
|
| 482 |
+
}
|
| 483 |
+
const fallbackModel = chooseFallback(targetModel, localList);
|
| 484 |
+
if (fallbackModel && fallbackModel !== targetModel) {
|
| 485 |
+
const fbA = { ...variantA, model: fallbackModel };
|
| 486 |
+
const fbB = { ...variantB, model: fallbackModel };
|
| 487 |
+
// Choose URL for fallback model
|
| 488 |
+
const fbCfg = getModelConfig(localList, fallbackModel);
|
| 489 |
+
const fbUrl = (fbCfg && fbCfg.upstream_url) ? fbCfg.upstream_url : GENERATE_API_URL;
|
| 490 |
try {
|
| 491 |
+
result = await tryCall(fbA, 'nested-fallback', fbUrl);
|
| 492 |
+
} catch (e3) {
|
| 493 |
+
try {
|
| 494 |
+
result = await tryCall(fbB, 'flat-fallback', fbUrl);
|
| 495 |
+
} catch (e4) {
|
| 496 |
+
const status = e4.status || 502;
|
| 497 |
+
return res.status(status).json({
|
| 498 |
+
ok: false,
|
| 499 |
+
error: e4.hint || e4.message || 'Upstream error',
|
| 500 |
+
code: e4.code || 'UPSTREAM_ERROR',
|
| 501 |
+
upstream_model: targetModel,
|
| 502 |
+
fallback_model: fallbackModel
|
| 503 |
+
});
|
| 504 |
+
}
|
| 505 |
}
|
| 506 |
+
// success with fallback
|
| 507 |
+
return res.json({
|
| 508 |
+
ok: true,
|
| 509 |
+
image: `data:${result.contentType};base64,${result.imageBase64}`,
|
| 510 |
+
contentType: result.contentType,
|
| 511 |
+
meta: {
|
| 512 |
+
model: flat.model,
|
| 513 |
+
upstream_model: targetModel,
|
| 514 |
+
fallback_used: true,
|
| 515 |
+
fallback_model: fallbackModel,
|
| 516 |
+
width: flat.width,
|
| 517 |
+
height: flat.height,
|
| 518 |
+
guidance_scale: flat.guidance_scale,
|
| 519 |
+
num_inference_steps: flat.num_inference_steps,
|
| 520 |
+
seed: flat.seed
|
| 521 |
+
},
|
| 522 |
+
tried: result.tried
|
| 523 |
+
});
|
| 524 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
}
|
| 526 |
+
const status = e2.status || 502;
|
| 527 |
+
return res.status(status).json({
|
| 528 |
+
ok: false,
|
| 529 |
+
error: e2.hint || e2.message || 'Upstream error',
|
| 530 |
+
code: e2.code || 'UPSTREAM_ERROR',
|
| 531 |
+
upstream_model: targetModel
|
| 532 |
+
});
|
| 533 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 534 |
}
|
| 535 |
}
|
| 536 |
|
| 537 |
+
|
| 538 |
+
|
| 539 |
return res.json({
|
| 540 |
ok: true,
|
| 541 |
image: `data:${result.contentType};base64,${result.imageBase64}`,
|