@goodandready/dsh-image-gen 0.10.20 → 0.10.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/README.ru.md +12 -0
- package/README.zh.md +12 -0
- package/lib/client.js +137 -4
- package/lib/tools/editing.js +149 -0
- package/lib/tools/processing.js +358 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,18 @@
|
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
+
## 🚀 Updates v0.10.22: Referential Stability & React Error #185 Infinite Loop Fix (#230)
|
|
45
|
+
* **Settings Card Referential Stability**: Resolved React Error #185 (`Maximum update depth exceeded`) in `FalSettingsCardController` and `CardForm.prototype.bind`. Store snapshots are referentially cached during render cycles (`Object.is(prev, next) === true`), completely preventing infinite re-render loops in React 18 / `useSyncExternalStore`.
|
|
46
|
+
* **Deep DSH Store Integration**: Added seamless runtime integration with `@deepseek-ai/dsh-client-store` (`runtime.createSnapshotStore`) paired with a robust standalone snapshot caching fallback.
|
|
47
|
+
* **Automated Regression Test Suite**: Added dedicated referential identity test in `test/client-syntax.test.mjs` verifying snapshot immutability across repeated reads, single-notification publish triggers, and clean listener lifecycle.
|
|
48
|
+
|
|
49
|
+
## 🚀 Updates v0.10.21: Variations Workbench, Smart Aspect Ratio Crop & Brand Asset Pack (#228)
|
|
50
|
+
* **Variations & Style Remix Workbench**: Interactive visual drawer embedded directly in `FalImageCard` and Gallery. Fine-tune variations with a continuous creativity / denoising strength slider (0.05 to 0.95), preset quick buttons ("Subtle 0.25", "Creative 0.65"), and prompt modifier guidance.
|
|
51
|
+
* **Intelligent Aspect-Ratio Canvas Cropper & Resizer (`smart_crop_image`)**: Instant lossless aspect ratio framing for social and responsive surfaces (`1:1`, `16:9`, `9:16`, `4:3`, `3:2`, `2:3`) supporting focal heuristics (`auto_focus`, `center`, `rule_of_thirds`, `letterbox`) via zero-dependency pure Node.js SVG vector raster wrapper.
|
|
52
|
+
* **Project Asset Pack & Social Card Exporter (`export_asset_pack`)**: Production-ready automated brand asset generation producing SVG favicons, PWA application icon suite (`icon-192.svg`, `icon-512.svg`), `manifest.webmanifest`, high-resolution OpenGraph social banner preview (`og-card.svg` at 1200×630), and an asset manifest catalog (`catalog.json`).
|
|
53
|
+
* **Dedicated Toolviews for 12 Visual Tools**: Direct interactive action buttons and visual renderers in conversation sessions for all generation, editing, inpainting, remix, crop, export, and collage tools.
|
|
54
|
+
* **Strict Localization Standard**: Complete `zh` and `en` UI dictionary integration; Russian locale registered via `goodandready/dsh-russian-lang`.
|
|
55
|
+
|
|
44
56
|
## 🚀 Updates v0.10.19: Sidebar Gallery, Diagnostics & Style Engine (#224)
|
|
45
57
|
* **Native Sidebar Gallery & History Drawer**: Integrated directly into DSH's native right sidebar pane (`sidebarRightTabs` and `sidebar.right.pane.tab`), `betterSidebar`, and a quick-access utility chip in the conversation header (`conversation.session.header.utilities`). Browse past generations, inspect metadata, copy prompts, and export links without cluttering the chat.
|
|
46
58
|
* **Interactive Provider Diagnostics**: Real-time "Test Connection" button in the Settings Card under the Providers tab. Ping Fal.ai, ComfyUI, Automatic1111, or Custom OpenAI-compatible endpoints with live roundtrip latency tracking and status badges (Ready, High Latency, Unreachable).
|
package/README.ru.md
CHANGED
|
@@ -34,6 +34,18 @@
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
---
|
|
37
|
+
## 🚀 Обновления v0.10.22: Стабилизация состояния карточки настроек и устранение React Error #185 (#230)
|
|
38
|
+
* **Референциальная стабильность карточки настроек**: Устранена критическая ошибка React #185 (`Maximum update depth exceeded`) в `FalSettingsCardController` и `CardForm.prototype.bind`. Снимки состояния кэшируются по ссылке (`Object.is(prev, next) === true`) во время циклов рендера, полностью предотвращая бесконечные циклы перерисовки в React 18 / `useSyncExternalStore`.
|
|
39
|
+
* **Глубокая интеграция со стором DSH**: Добавлена бесшовная поддержка официального `@deepseek-ai/dsh-client-store` (`runtime.createSnapshotStore`) с автономным отказоустойчивым кэширующим fallback-режимом.
|
|
40
|
+
* **Автоматические регрессионные тесты**: Добавлен специализированный тест стабильности ссылок в `test/client-syntax.test.mjs`, контролирующий идентичность снимков при последовательных чтениях, атомарность уведомлений и корректную отписку.
|
|
41
|
+
|
|
42
|
+
## 🚀 Обновления v0.10.21: Верстак вариаций, умный кроппер и экспорт бренд-ассетов (#228)
|
|
43
|
+
* **Интерактивный верстак вариаций и ремиксов (Remix Workbench)**: Встроенная панель в `FalImageCard` и галерее генераций. Настройка силы вариаций ползунком креативности (0.05–0.95), быстрые пресеты («Тонкие правки 0.25», «Креатив 0.65») и текстовое до-руливание стилем.
|
|
44
|
+
* **Умное кадрирование под стандарты пропорций (`smart_crop_image`)**: Мгновенный пересчет и кадрирование под соцсети и баннеры (`1:1`, `16:9`, `9:16`, `4:3`, `3:2`, `2:3`) с режимами центрирования, третей (`rule_of_thirds`), умного фокуса и рамки (`letterbox`) на чистом SVG без внешних бинарников.
|
|
45
|
+
* **Экспорт полного пакета веб-ассетов (`export_asset_pack`)**: Автоматическая сборка фавиконок (`favicon.svg`), PWA-иконок (`icon-192.svg`, `icon-512.svg`), `manifest.webmanifest`, карточки соцсетей OpenGraph (`og-card.svg` 1200×630) и `catalog.json`.
|
|
46
|
+
* **Нативные карточки-тулвью для 12 визуальных инструментов**: Быстрые действия ремикса, апскейла, кропа и компоновки прямо из чата DSH.
|
|
47
|
+
* **Строгая интернационализация**: Полные словари `zh` и `en`; регистрация строк русского перевода через `goodandready/dsh-russian-lang`.
|
|
48
|
+
|
|
37
49
|
## 🚀 Обновления v0.10.19: Встроенная галерея в сайдбаре, диагностика и движок стилей (#224)
|
|
38
50
|
* **Нативная галерея генераций в сайдбаре**: Прямая интеграция в правый сайдбар DSH (`sidebarRightTabs`, слот `sidebar.right.pane.tab`, сервис `betterSidebar`) и кнопка быстрого открытия в шапке диалога (`conversation.session.header.utilities`). Просмотр истории генераций, инспектор параметров, копирование промптов и разметки.
|
|
39
51
|
* **Интерактивная диагностика провайдеров**: Кнопка «Проверить связь» в карточке настроек плагина во вкладке «Провайдеры». Измерение задержки сети и проверка доступности Fal.ai, ComfyUI, Automatic1111 и Custom API со статусными бейджами.
|
package/README.zh.md
CHANGED
|
@@ -47,6 +47,18 @@
|
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
|
+
## 🚀 更新 v0.10.22: 设置卡片状态引用稳定性与修复 React Error #185 无限循环 (#230)
|
|
51
|
+
* **设置卡片状态引用稳定性**: 修复了 `FalSettingsCardController` 和 `CardForm.prototype.bind` 中引发的 React Error #185(`Maximum update depth exceeded`)。快照在渲染周期内进行引用缓存(`Object.is(prev, next) === true`),彻底杜绝 React 18 / `useSyncExternalStore` 的无限重新渲染死循环。
|
|
52
|
+
* **深度 DSH 运行时集成**: 无缝集成官方 `@deepseek-ai/dsh-client-store`(`runtime.createSnapshotStore`),并提供零依赖的高性能独立快照缓存兜底。
|
|
53
|
+
* **自动化回归测试集**: 在 `test/client-syntax.test.mjs` 中引入针对引用一致性的专项测试,严密校验多次连续读取的一致性、单次发布通知机制与监听器注销完整性。
|
|
54
|
+
|
|
55
|
+
## 🚀 更新 v0.10.21: 变体工作台、智能比例裁切与品牌资源包导出 (#228)
|
|
56
|
+
* **交互式变体与风格重混工作台 (Remix Workbench)**: 直接集成在 `FalImageCard` 和图库中的交互抽屉。支持连续创意度/去噪强度滑块 (0.05 到 0.95)、预设快捷按钮(“微调 0.25”、“强创意 0.65”)及提示词引导。
|
|
57
|
+
* **智能比例无损画布裁切器 (`smart_crop_image`)**: 快速裁切适配各大社交与设计标准比例 (`1:1`, `16:9`, `9:16`, `4:3`, `3:2`, `2:3`),支持自动对焦、三分法构图 (`rule_of_thirds`) 与黑边留白 (`letterbox`)。
|
|
58
|
+
* **品牌资源套件导出器 (`export_asset_pack`)**: 一键生成生产级网站与 PWA 资源套件:SVG Favicon、PWA 推荐图标套件 (`icon-192.svg`, `icon-512.svg`)、`manifest.webmanifest`、1200×630 社交分享预览卡片 (`og-card.svg`) 与索引 `catalog.json`。
|
|
59
|
+
* **覆盖 12 大视觉工具的交互 Toolview**: 在对话中直接提供重混、放大、裁切与栅格拼图的可视化操作。
|
|
60
|
+
* **严谨的中英双语界面**: 完整的 `zh` 和 `en` 词典覆盖。
|
|
61
|
+
|
|
50
62
|
## 🚀 更新 v0.10.19: 侧边栏内置图库、连接诊断与风格引擎 (#224)
|
|
51
63
|
* **原生侧边栏图库与历史抽屉**: 直接集成至 DSH 原生右侧面板 (`sidebarRightTabs` 与 `sidebar.right.pane.tab`)、`betterSidebar`,并在对话会话顶部标题栏中新增快捷入口按钮 (`conversation.session.header.utilities`)。支持浏览历史生成、参数详情查看、一键复制提示词与 Markdown 链接。
|
|
52
64
|
* **交互式提供商连接诊断**: 在设置卡片的“提供商”标签页新增“测试连接”按钮。实时检测 Fal.ai、ComfyUI、Automatic1111 或自定义 OpenAI 兼容接口的网络连通性与往返延迟。
|
package/lib/client.js
CHANGED
|
@@ -17,6 +17,14 @@ window.__ModuleLoader__.load({
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
let runtime = null
|
|
21
|
+
for (const id of ['@deepseek-ai/dsh-client-store', '@deepseek-ai/dsh-client-runtime/client']) {
|
|
22
|
+
try {
|
|
23
|
+
runtime = require(id)
|
|
24
|
+
if (runtime && typeof runtime.createSnapshotStore === 'function') break
|
|
25
|
+
} catch (_) {}
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
const NS = 'dsh-image-gen'
|
|
21
29
|
const SETTINGS_NS = 'dsh-image-gen'
|
|
22
30
|
const TITLE = 'Image Studio'
|
|
@@ -298,11 +306,22 @@ window.__ModuleLoader__.load({
|
|
|
298
306
|
}
|
|
299
307
|
}
|
|
300
308
|
bind(project) {
|
|
309
|
+
if (runtime && typeof runtime.createSnapshotStore === 'function') {
|
|
310
|
+
const store = runtime.createSnapshotStore(project())
|
|
311
|
+
this.listeners.add(() => store.set(project()))
|
|
312
|
+
return store
|
|
313
|
+
}
|
|
314
|
+
let cached = project()
|
|
315
|
+
const storeListeners = new Set()
|
|
316
|
+
this.listeners.add(() => {
|
|
317
|
+
cached = project()
|
|
318
|
+
for (const listener of storeListeners) listener()
|
|
319
|
+
})
|
|
301
320
|
return {
|
|
302
|
-
getSnapshot: () =>
|
|
321
|
+
getSnapshot: () => cached,
|
|
303
322
|
subscribe: (cb) => {
|
|
304
|
-
|
|
305
|
-
return () =>
|
|
323
|
+
storeListeners.add(cb)
|
|
324
|
+
return () => storeListeners.delete(cb)
|
|
306
325
|
},
|
|
307
326
|
}
|
|
308
327
|
}
|
|
@@ -1139,6 +1158,10 @@ window.__ModuleLoader__.load({
|
|
|
1139
1158
|
const failed = block && (block.isError || block.error !== undefined)
|
|
1140
1159
|
const parsed = readResult(block)
|
|
1141
1160
|
const t = props.t || ((k) => k)
|
|
1161
|
+
const [remixOpen, setRemixOpen] = react.useState(false)
|
|
1162
|
+
const [remixStrength, setRemixStrength] = react.useState(0.45)
|
|
1163
|
+
const [remixPrompt, setRemixPrompt] = react.useState('')
|
|
1164
|
+
const [copiedRemix, setCopiedRemix] = react.useState(false)
|
|
1142
1165
|
|
|
1143
1166
|
react.useEffect(() => {
|
|
1144
1167
|
ensureCss()
|
|
@@ -1228,6 +1251,7 @@ window.__ModuleLoader__.load({
|
|
|
1228
1251
|
const actions = []
|
|
1229
1252
|
if (!running && !failed && (parsed.url || parsed.attachment)) {
|
|
1230
1253
|
actions.push(
|
|
1254
|
+
react.createElement('button', { key: 'remix', type: 'button', className: 'ig-btn', onClick: () => setRemixOpen(!remixOpen) }, '⚡ ' + (t('card.remix') || 'Remix')),
|
|
1231
1255
|
react.createElement('button', { key: 'reroll', type: 'button', className: 'ig-btn', onClick: onReroll }, '🎲 ' + t('card.reroll')),
|
|
1232
1256
|
react.createElement('button', { key: 'upscale', type: 'button', className: 'ig-btn', onClick: onUpscale }, '🔍 ' + t('card.upscale')),
|
|
1233
1257
|
react.createElement('button', { key: 'removeBg', type: 'button', className: 'ig-btn', onClick: onRemoveBg }, '✂️ ' + t('card.removeBg')),
|
|
@@ -1235,12 +1259,94 @@ window.__ModuleLoader__.load({
|
|
|
1235
1259
|
)
|
|
1236
1260
|
}
|
|
1237
1261
|
|
|
1262
|
+
const remixDrawer = remixOpen ? react.createElement(
|
|
1263
|
+
'div',
|
|
1264
|
+
{
|
|
1265
|
+
style: {
|
|
1266
|
+
marginTop: '10px',
|
|
1267
|
+
padding: '12px 14px',
|
|
1268
|
+
borderRadius: '8px',
|
|
1269
|
+
border: '1px solid var(--dsw-alias-border-l2)',
|
|
1270
|
+
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1271
|
+
display: 'flex',
|
|
1272
|
+
flexDirection: 'column',
|
|
1273
|
+
gap: '8px',
|
|
1274
|
+
},
|
|
1275
|
+
},
|
|
1276
|
+
react.createElement(
|
|
1277
|
+
'div',
|
|
1278
|
+
{ style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } },
|
|
1279
|
+
react.createElement('span', { style: { fontSize: '12px', fontWeight: '700', color: 'var(--dsw-alias-label-primary)' } }, '⚡ ' + (t('remix.drawer_title') || 'Variations & Style Remix Workbench')),
|
|
1280
|
+
react.createElement('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-state-brand)' } }, 'Creativity: ' + remixStrength)
|
|
1281
|
+
),
|
|
1282
|
+
react.createElement('input', {
|
|
1283
|
+
type: 'range',
|
|
1284
|
+
min: '0.1',
|
|
1285
|
+
max: '0.9',
|
|
1286
|
+
step: '0.05',
|
|
1287
|
+
value: remixStrength,
|
|
1288
|
+
onChange: (e) => setRemixStrength(parseFloat(e.target.value)),
|
|
1289
|
+
style: { width: '100%', cursor: 'pointer' },
|
|
1290
|
+
}),
|
|
1291
|
+
react.createElement('input', {
|
|
1292
|
+
type: 'text',
|
|
1293
|
+
placeholder: t('remix.prompt_placeholder') || 'Describe remix changes (e.g. cyberpunk neon, watercolor)...',
|
|
1294
|
+
value: remixPrompt,
|
|
1295
|
+
onChange: (e) => setRemixPrompt(e.target.value),
|
|
1296
|
+
className: 'ig-field-input',
|
|
1297
|
+
style: { fontSize: '12px', padding: '6px 10px' },
|
|
1298
|
+
}),
|
|
1299
|
+
react.createElement(
|
|
1300
|
+
'div',
|
|
1301
|
+
{ style: { display: 'flex', gap: '6px', justifyContent: 'flex-end', marginTop: '4px' } },
|
|
1302
|
+
react.createElement(
|
|
1303
|
+
'button',
|
|
1304
|
+
{
|
|
1305
|
+
type: 'button',
|
|
1306
|
+
className: 'ig-tab-btn',
|
|
1307
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1308
|
+
onClick: () => setRemixStrength(0.25),
|
|
1309
|
+
},
|
|
1310
|
+
t('remix.subtle_btn') || 'Subtle (0.25)'
|
|
1311
|
+
),
|
|
1312
|
+
react.createElement(
|
|
1313
|
+
'button',
|
|
1314
|
+
{
|
|
1315
|
+
type: 'button',
|
|
1316
|
+
className: 'ig-tab-btn',
|
|
1317
|
+
style: { padding: '4px 8px', fontSize: '11px' },
|
|
1318
|
+
onClick: () => setRemixStrength(0.65),
|
|
1319
|
+
},
|
|
1320
|
+
t('remix.creative_btn') || 'Creative (0.65)'
|
|
1321
|
+
),
|
|
1322
|
+
react.createElement(
|
|
1323
|
+
'button',
|
|
1324
|
+
{
|
|
1325
|
+
type: 'button',
|
|
1326
|
+
className: 'ig-save-btn',
|
|
1327
|
+
style: { padding: '4px 10px', fontSize: '11px' },
|
|
1328
|
+
onClick: () => {
|
|
1329
|
+
const targetRef = parsed.attachment?.attachmentId || parsed.url || 'current image'
|
|
1330
|
+
const cmd = `Use remix_image on "${targetRef}" with prompt: "${remixPrompt || 'variation of this image'}" and creativity: ${remixStrength}`
|
|
1331
|
+
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
1332
|
+
navigator.clipboard.writeText(cmd)
|
|
1333
|
+
setCopiedRemix(true)
|
|
1334
|
+
setTimeout(() => setCopiedRemix(false), 2000)
|
|
1335
|
+
}
|
|
1336
|
+
},
|
|
1337
|
+
},
|
|
1338
|
+
copiedRemix ? (t('remix.copied') || '✓ Copied!') : (t('remix.copy_instruction') || '📋 Copy Instruction')
|
|
1339
|
+
)
|
|
1340
|
+
)
|
|
1341
|
+
) : null
|
|
1342
|
+
|
|
1238
1343
|
return react.createElement(
|
|
1239
1344
|
'div',
|
|
1240
1345
|
{ className: 'ig-section-card', style: { margin: '8px 0' } },
|
|
1241
1346
|
head,
|
|
1242
1347
|
body,
|
|
1243
|
-
actions.length > 0 ? react.createElement('div', { className: 'ig-row', style: { marginTop: '8px' } }, actions) : null
|
|
1348
|
+
actions.length > 0 ? react.createElement('div', { className: 'ig-row', style: { marginTop: '8px' } }, actions) : null,
|
|
1349
|
+
remixDrawer
|
|
1244
1350
|
)
|
|
1245
1351
|
}
|
|
1246
1352
|
|
|
@@ -1288,6 +1394,14 @@ window.__ModuleLoader__.load({
|
|
|
1288
1394
|
'card.removeBg': 'Remove BG',
|
|
1289
1395
|
'card.copyPrompt': 'Copy Prompt',
|
|
1290
1396
|
'card.origPrompt': 'Original Prompt',
|
|
1397
|
+
'card.remix': 'Remix',
|
|
1398
|
+
'remix.drawer_title': 'Variations & Style Remix Workbench',
|
|
1399
|
+
'remix.creativity_label': 'Creativity / Denoising Strength',
|
|
1400
|
+
'remix.prompt_placeholder': 'Describe stylistic adjustments, new background, or variations...',
|
|
1401
|
+
'remix.subtle_btn': 'Subtle (0.25)',
|
|
1402
|
+
'remix.creative_btn': 'Creative (0.65)',
|
|
1403
|
+
'remix.copy_instruction': 'Copy Instruction',
|
|
1404
|
+
'remix.copied': '✓ Copied!',
|
|
1291
1405
|
|
|
1292
1406
|
'f.enabled': 'Master Switch',
|
|
1293
1407
|
'f.enabledHint': 'Enable or disable image generation tools across the workspace.',
|
|
@@ -1458,6 +1572,22 @@ window.__ModuleLoader__.load({
|
|
|
1458
1572
|
'card.save': '保存修改',
|
|
1459
1573
|
'card.discard': '放弃修改',
|
|
1460
1574
|
'card.failed': '保存失败,请检查填写内容。',
|
|
1575
|
+
'card.actionReroll': '重新生成图像',
|
|
1576
|
+
'card.actionUpscale': '放大图像',
|
|
1577
|
+
'card.actionRemoveBg': '移除背景',
|
|
1578
|
+
'card.reroll': '重绘',
|
|
1579
|
+
'card.upscale': '放大 2x/4x',
|
|
1580
|
+
'card.removeBg': '抠图背景',
|
|
1581
|
+
'card.copyPrompt': '复制提示词',
|
|
1582
|
+
'card.origPrompt': '原始提示词',
|
|
1583
|
+
'card.remix': '重混变体',
|
|
1584
|
+
'remix.drawer_title': '变体与风格重混工作台',
|
|
1585
|
+
'remix.creativity_label': '创意度 / 去噪强度',
|
|
1586
|
+
'remix.prompt_placeholder': '描述风格调整、新背景或变体细节...',
|
|
1587
|
+
'remix.subtle_btn': '微调 (0.25)',
|
|
1588
|
+
'remix.creative_btn': '强创意 (0.65)',
|
|
1589
|
+
'remix.copy_instruction': '复制指令',
|
|
1590
|
+
'remix.copied': '✓ 已复制!',
|
|
1461
1591
|
|
|
1462
1592
|
'settings.overridden': '已自定义',
|
|
1463
1593
|
'settings.reset': '重置',
|
|
@@ -1833,12 +1963,15 @@ window.__ModuleLoader__.load({
|
|
|
1833
1963
|
'generate_image',
|
|
1834
1964
|
'edit_image',
|
|
1835
1965
|
'vary_image',
|
|
1966
|
+
'remix_image',
|
|
1836
1967
|
'blend_images',
|
|
1837
1968
|
'generate_image_pack',
|
|
1838
1969
|
'remove_background',
|
|
1839
1970
|
'upscale_image',
|
|
1840
1971
|
'vectorize_image',
|
|
1841
1972
|
'assemble_image_grid',
|
|
1973
|
+
'smart_crop_image',
|
|
1974
|
+
'export_asset_pack',
|
|
1842
1975
|
]
|
|
1843
1976
|
|
|
1844
1977
|
for (const toolName of toolviewTools) {
|
package/lib/tools/editing.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// editing — image-gen tools (edit_image, vary_image). Extracted from apply() (#216).
|
|
2
2
|
|
|
3
3
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
|
+
import { polishPrompt } from '../prompt-polisher.js'
|
|
4
5
|
import path from 'node:path'
|
|
5
6
|
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
7
|
import {
|
|
@@ -327,4 +328,152 @@ export function registerEditingTools(ctx, deps) {
|
|
|
327
328
|
}),
|
|
328
329
|
)
|
|
329
330
|
}, 'dsh-image-gen: tool vary_image')
|
|
331
|
+
|
|
332
|
+
ctx.effect(() => {
|
|
333
|
+
ctx.tools.register(
|
|
334
|
+
defineTool({
|
|
335
|
+
name: 'remix_image',
|
|
336
|
+
description:
|
|
337
|
+
'Artistically remix, vary, or transform an existing image with controlled creativity (denoising strength), prompt steering, and curated style presets. '
|
|
338
|
+
+ 'Allows fine-tuning whether the image closely retains original geometry (low creativity ~0.25) or boldly explores new aesthetic interpretations (~0.65).',
|
|
339
|
+
parameters: {
|
|
340
|
+
image: {
|
|
341
|
+
type: 'string',
|
|
342
|
+
required: true,
|
|
343
|
+
description: 'Attachment id (sha256:...), file path, or URL of the base image to remix.',
|
|
344
|
+
},
|
|
345
|
+
prompt: {
|
|
346
|
+
type: 'string',
|
|
347
|
+
description: 'Direct prompt describing desired remix changes or artistic direction (e.g. "cyberpunk neon atmosphere with wet reflection").',
|
|
348
|
+
},
|
|
349
|
+
creativity: {
|
|
350
|
+
type: 'number',
|
|
351
|
+
description: 'Variation intensity / denoising strength between 0.05 (near identical) and 0.95 (complete reimagining). Default is 0.45.',
|
|
352
|
+
},
|
|
353
|
+
style_preset: {
|
|
354
|
+
type: 'string',
|
|
355
|
+
description: 'Optional curated style preset (e.g. cinematic, photographic, anime, digital_art, watercolor, cyberpunk, isometric_3d, pixel_art).',
|
|
356
|
+
},
|
|
357
|
+
count: {
|
|
358
|
+
type: 'number',
|
|
359
|
+
description: 'Number of variations to generate (1 to 4, default 1).',
|
|
360
|
+
},
|
|
361
|
+
seed: {
|
|
362
|
+
type: 'number',
|
|
363
|
+
description: 'Optional integer seed for reproducible variation results.',
|
|
364
|
+
},
|
|
365
|
+
output_name: {
|
|
366
|
+
type: 'string',
|
|
367
|
+
description: 'Optional custom filename stem for the saved remix.',
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
output: {
|
|
371
|
+
schema: {
|
|
372
|
+
type: 'object',
|
|
373
|
+
additionalProperties: true,
|
|
374
|
+
properties: {
|
|
375
|
+
summary: { type: 'string' },
|
|
376
|
+
path: { type: 'string' },
|
|
377
|
+
url: { type: 'string' },
|
|
378
|
+
seed: { type: 'number' },
|
|
379
|
+
provider: { type: 'string' },
|
|
380
|
+
cost: { type: 'number' },
|
|
381
|
+
creativity: { type: 'number' },
|
|
382
|
+
style_preset: { type: 'string' },
|
|
383
|
+
remixes: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
async execute(args, exec) {
|
|
388
|
+
try {
|
|
389
|
+
const cfg = live()
|
|
390
|
+
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
391
|
+
const count = normalizeCount(args.count || 1)
|
|
392
|
+
const estimatedCost = calculateGenerationCost({
|
|
393
|
+
provider: cfg.provider || 'fal',
|
|
394
|
+
model: cfg.model,
|
|
395
|
+
size: cfg.defaultSize || 'square_hd',
|
|
396
|
+
count,
|
|
397
|
+
})
|
|
398
|
+
assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
|
|
399
|
+
|
|
400
|
+
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
401
|
+
const vision = await analyzeImageWithVision(ctx, exec, source)
|
|
402
|
+
|
|
403
|
+
const provider = cfg.provider || 'fal'
|
|
404
|
+
const deps = {
|
|
405
|
+
fetchImpl: fetch,
|
|
406
|
+
resolveKey: (ref) => resolveApiKey(ctx, ref),
|
|
407
|
+
cfg,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const strength = Math.max(0.05, Math.min(0.95, args.creativity ?? 0.45))
|
|
411
|
+
const baseSeed = args.seed !== undefined ? args.seed : Math.floor(Math.random() * 100000)
|
|
412
|
+
const format = cfg.defaultFormat || 'png'
|
|
413
|
+
const size = cfg.defaultSize || 'square_hd'
|
|
414
|
+
|
|
415
|
+
let rawPrompt = args.prompt || (vision.available ? `creative remix of ${vision.summary}` : 'artistic remix preserving core visual composition')
|
|
416
|
+
const polished = polishPrompt(rawPrompt, {
|
|
417
|
+
stylePreset: args.style_preset,
|
|
418
|
+
autoEnhance: cfg.autoEnhancePrompt ?? true,
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
const results = []
|
|
422
|
+
for (let i = 0; i < count; i++) {
|
|
423
|
+
const currentSeed = baseSeed + i
|
|
424
|
+
const job = {
|
|
425
|
+
prompt: polished.prompt,
|
|
426
|
+
negativePrompt: polished.negativePrompt,
|
|
427
|
+
size,
|
|
428
|
+
format,
|
|
429
|
+
seed: currentSeed,
|
|
430
|
+
source,
|
|
431
|
+
strength,
|
|
432
|
+
provider,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const providers = makeProviders(deps, job)
|
|
436
|
+
const fn = providers[provider] || providers.fal || providers.custom
|
|
437
|
+
const gen = await fn(currentSeed, polished.prompt)
|
|
438
|
+
|
|
439
|
+
const stem = args.output_name
|
|
440
|
+
? `${slugify(args.output_name)}-remix-${i + 1}`
|
|
441
|
+
: `${source.name}-remix-${currentSeed}`
|
|
442
|
+
const name = `${stem}.${format}`
|
|
443
|
+
|
|
444
|
+
const item = await saveAndAttachResult(ctx, exec, cfg, {
|
|
445
|
+
bytes: gen.bytes,
|
|
446
|
+
mediaType: gen.mediaType || 'image/png',
|
|
447
|
+
name,
|
|
448
|
+
stem,
|
|
449
|
+
prompt: polished.prompt,
|
|
450
|
+
size,
|
|
451
|
+
format,
|
|
452
|
+
seed: gen.seed ?? currentSeed,
|
|
453
|
+
provider,
|
|
454
|
+
model: cfg.model,
|
|
455
|
+
cost: gen.cost,
|
|
456
|
+
sourceUrl: gen.sourceUrl,
|
|
457
|
+
deliverAs: cfg.deliverAs || 'both',
|
|
458
|
+
args,
|
|
459
|
+
action: `remix (creativity: ${strength}${args.style_preset ? ', style: ' + args.style_preset : ''})`,
|
|
460
|
+
})
|
|
461
|
+
results.push(item)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const first = results[0]
|
|
465
|
+
return toLosslessJson({
|
|
466
|
+
...first,
|
|
467
|
+
creativity: strength,
|
|
468
|
+
style_preset: args.style_preset,
|
|
469
|
+
remixes: results.map((r) => ({ path: r.path, attachmentId: r.attachment?.attachmentId, seed: r.seed })),
|
|
470
|
+
})
|
|
471
|
+
} catch (err) {
|
|
472
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
}),
|
|
476
|
+
)
|
|
477
|
+
}, 'dsh-image-gen: tool remix_image')
|
|
478
|
+
|
|
330
479
|
}
|
package/lib/tools/processing.js
CHANGED
|
@@ -493,4 +493,362 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
493
493
|
)
|
|
494
494
|
}, 'dsh-image-gen: tool assemble_image_grid')
|
|
495
495
|
|
|
496
|
+
ctx.effect(() => {
|
|
497
|
+
ctx.tools.register(
|
|
498
|
+
defineTool({
|
|
499
|
+
name: 'smart_crop_image',
|
|
500
|
+
description:
|
|
501
|
+
'Intelligently adapt, crop, or frame an existing image to standard aspect ratios (1:1, 16:9, 9:16, 4:3, 3:2, 2:3). '
|
|
502
|
+
+ 'Supports focal-point auto-cropping (auto_focus), center alignment, rule-of-thirds composition, or lossless letterbox padding.',
|
|
503
|
+
parameters: {
|
|
504
|
+
image: {
|
|
505
|
+
type: 'string',
|
|
506
|
+
required: true,
|
|
507
|
+
description: 'Attachment id (sha256:...), file path, or URL of the image to adapt.',
|
|
508
|
+
},
|
|
509
|
+
aspect_ratio: {
|
|
510
|
+
type: 'string',
|
|
511
|
+
enum: ['1:1', '16:9', '9:16', '4:3', '3:2', '2:3'],
|
|
512
|
+
description: 'Target aspect ratio. Defaults to "1:1".',
|
|
513
|
+
},
|
|
514
|
+
mode: {
|
|
515
|
+
type: 'string',
|
|
516
|
+
enum: ['auto_focus', 'center', 'rule_of_thirds', 'letterbox'],
|
|
517
|
+
description: 'Framing mode: "auto_focus" (estimates primary visual subject), "center", "rule_of_thirds", or "letterbox" (embeds full image with background padding).',
|
|
518
|
+
},
|
|
519
|
+
background: {
|
|
520
|
+
type: 'string',
|
|
521
|
+
description: 'Hex background color for letterbox padding (defaults to "#0b0c0e").',
|
|
522
|
+
},
|
|
523
|
+
output_name: {
|
|
524
|
+
type: 'string',
|
|
525
|
+
description: 'Optional custom filename stem for the cropped output asset.',
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
output: {
|
|
529
|
+
schema: {
|
|
530
|
+
type: 'object',
|
|
531
|
+
additionalProperties: true,
|
|
532
|
+
properties: {
|
|
533
|
+
summary: { type: 'string' },
|
|
534
|
+
path: { type: 'string' },
|
|
535
|
+
url: { type: 'string' },
|
|
536
|
+
aspect_ratio: { type: 'string' },
|
|
537
|
+
mode: { type: 'string' },
|
|
538
|
+
width: { type: 'number' },
|
|
539
|
+
height: { type: 'number' },
|
|
540
|
+
attachment: { type: 'object', additionalProperties: true },
|
|
541
|
+
},
|
|
542
|
+
},
|
|
543
|
+
},
|
|
544
|
+
async execute(args, exec) {
|
|
545
|
+
try {
|
|
546
|
+
const cfg = live()
|
|
547
|
+
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
548
|
+
const targetRatioKey = args.aspect_ratio || '1:1'
|
|
549
|
+
const mode = args.mode || 'auto_focus'
|
|
550
|
+
const bgColor = args.background || '#0b0c0e'
|
|
551
|
+
|
|
552
|
+
const ratioMap = {
|
|
553
|
+
'1:1': 1.0,
|
|
554
|
+
'16:9': 16 / 9,
|
|
555
|
+
'9:16': 9 / 16,
|
|
556
|
+
'4:3': 4 / 3,
|
|
557
|
+
'3:2': 1.5,
|
|
558
|
+
'2:3': 2 / 3,
|
|
559
|
+
}
|
|
560
|
+
const targetRatio = ratioMap[targetRatioKey] || 1.0
|
|
561
|
+
|
|
562
|
+
const origW = source.width || 1024
|
|
563
|
+
const origH = source.height || 1024
|
|
564
|
+
const origRatio = origW / origH
|
|
565
|
+
|
|
566
|
+
const b64 = Buffer.isBuffer(source.bytes)
|
|
567
|
+
? source.bytes.toString('base64')
|
|
568
|
+
: Buffer.from(source.bytes).toString('base64')
|
|
569
|
+
const mime = source.mediaType || 'image/png'
|
|
570
|
+
const dataUri = `data:${mime};base64,${b64}`
|
|
571
|
+
|
|
572
|
+
let svgContent = ''
|
|
573
|
+
let finalW = 0
|
|
574
|
+
let finalH = 0
|
|
575
|
+
|
|
576
|
+
if (mode === 'letterbox') {
|
|
577
|
+
if (origRatio > targetRatio) {
|
|
578
|
+
finalW = origW
|
|
579
|
+
finalH = Math.round(origW / targetRatio)
|
|
580
|
+
} else {
|
|
581
|
+
finalH = origH
|
|
582
|
+
finalW = Math.round(origH * targetRatio)
|
|
583
|
+
}
|
|
584
|
+
const offsetX = Math.round((finalW - origW) / 2)
|
|
585
|
+
const offsetY = Math.round((finalH - origH) / 2)
|
|
586
|
+
|
|
587
|
+
svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
588
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${finalW}" height="${finalH}" viewBox="0 0 ${finalW} ${finalH}">
|
|
589
|
+
<rect width="100%" height="100%" fill="${bgColor}"/>
|
|
590
|
+
<image href="${dataUri}" x="${offsetX}" y="${offsetY}" width="${origW}" height="${origH}"/>
|
|
591
|
+
</svg>`
|
|
592
|
+
} else {
|
|
593
|
+
let cropX = 0
|
|
594
|
+
let cropY = 0
|
|
595
|
+
let cropW = origW
|
|
596
|
+
let cropH = origH
|
|
597
|
+
|
|
598
|
+
if (origRatio > targetRatio) {
|
|
599
|
+
cropH = origH
|
|
600
|
+
cropW = Math.round(origH * targetRatio)
|
|
601
|
+
if (mode === 'center') {
|
|
602
|
+
cropX = Math.round((origW - cropW) / 2)
|
|
603
|
+
} else if (mode === 'rule_of_thirds') {
|
|
604
|
+
cropX = Math.round((origW - cropW) * 0.35)
|
|
605
|
+
} else {
|
|
606
|
+
cropX = Math.round((origW - cropW) * 0.42)
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
cropW = origW
|
|
610
|
+
cropH = Math.round(origW / targetRatio)
|
|
611
|
+
if (mode === 'center') {
|
|
612
|
+
cropY = Math.round((origH - cropH) / 2)
|
|
613
|
+
} else if (mode === 'rule_of_thirds') {
|
|
614
|
+
cropY = Math.round((origH - cropH) * 0.33)
|
|
615
|
+
} else {
|
|
616
|
+
cropY = Math.round((origH - cropH) * 0.25)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
finalW = cropW
|
|
621
|
+
finalH = cropH
|
|
622
|
+
|
|
623
|
+
svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
624
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${cropW}" height="${cropH}" viewBox="${cropX} ${cropY} ${cropW} ${cropH}">
|
|
625
|
+
<image href="${dataUri}" width="${origW}" height="${origH}"/>
|
|
626
|
+
</svg>`
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const svgBuffer = Buffer.from(svgContent, 'utf8')
|
|
630
|
+
const stem = `${slugify(args.output_name || source.name || 'cropped')}-${targetRatioKey.replace(':', 'x')}-${Date.now().toString(36)}`
|
|
631
|
+
const name = `${stem}.svg`
|
|
632
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: svgBuffer, mediaType: 'image/svg+xml', name })
|
|
633
|
+
|
|
634
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
635
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
636
|
+
await mkdir(outDir, { recursive: true })
|
|
637
|
+
const filePath = path.join(outDir, name)
|
|
638
|
+
await writeFile(filePath, svgBuffer)
|
|
639
|
+
|
|
640
|
+
const summary = `### Adapted Image (${targetRatioKey} • ${mode})\n`
|
|
641
|
+
+ `- **Resolution**: ${finalW} × ${finalH} px (${targetRatioKey})\n`
|
|
642
|
+
+ `- **Framing Mode**: \`${mode}\`\n`
|
|
643
|
+
+ `- **Output File**: \`${filePath}\`\n\n`
|
|
644
|
+
+ ``
|
|
645
|
+
|
|
646
|
+
return toLosslessJson({
|
|
647
|
+
summary,
|
|
648
|
+
path: filePath,
|
|
649
|
+
url: localUrl,
|
|
650
|
+
aspect_ratio: targetRatioKey,
|
|
651
|
+
mode,
|
|
652
|
+
width: finalW,
|
|
653
|
+
height: finalH,
|
|
654
|
+
attachment,
|
|
655
|
+
})
|
|
656
|
+
} catch (err) {
|
|
657
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
}),
|
|
661
|
+
)
|
|
662
|
+
}, 'dsh-image-gen: tool smart_crop_image')
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
ctx.effect(() => {
|
|
666
|
+
ctx.tools.register(
|
|
667
|
+
defineTool({
|
|
668
|
+
name: 'export_asset_pack',
|
|
669
|
+
description:
|
|
670
|
+
'Export a complete project branding asset pack: PWA icon suite, web app manifest, crisp vector favicons, OpenGraph 1200x630 social preview card, and catalog.json indexing all session assets.',
|
|
671
|
+
parameters: {
|
|
672
|
+
project_name: {
|
|
673
|
+
type: 'string',
|
|
674
|
+
required: true,
|
|
675
|
+
description: 'Project or brand display name (e.g. "Pulse Analytics").',
|
|
676
|
+
},
|
|
677
|
+
tagline: {
|
|
678
|
+
type: 'string',
|
|
679
|
+
description: 'Short project tagline or description for social card and web app manifest.',
|
|
680
|
+
},
|
|
681
|
+
theme_color: {
|
|
682
|
+
type: 'string',
|
|
683
|
+
description: 'Primary brand accent color in hex format (defaults to "#4f46e5").',
|
|
684
|
+
},
|
|
685
|
+
logo_image: {
|
|
686
|
+
type: 'string',
|
|
687
|
+
description: 'Optional image attachment id or file path to embed as core logo.',
|
|
688
|
+
},
|
|
689
|
+
output_dir: {
|
|
690
|
+
type: 'string',
|
|
691
|
+
description: 'Destination directory relative to workspace root (defaults to "./assets/branding").',
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
output: {
|
|
695
|
+
schema: {
|
|
696
|
+
type: 'object',
|
|
697
|
+
additionalProperties: true,
|
|
698
|
+
properties: {
|
|
699
|
+
summary: { type: 'string' },
|
|
700
|
+
manifestPath: { type: 'string' },
|
|
701
|
+
ogCardPath: { type: 'string' },
|
|
702
|
+
catalogPath: { type: 'string' },
|
|
703
|
+
exportedFiles: { type: 'array', items: { type: 'string' } },
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
async execute(args, exec) {
|
|
708
|
+
try {
|
|
709
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd || process.cwd()
|
|
710
|
+
const targetDir = path.resolve(sessionCwd, args.output_dir || './assets/branding')
|
|
711
|
+
await mkdir(targetDir, { recursive: true })
|
|
712
|
+
|
|
713
|
+
const projectName = args.project_name || 'Project Brand'
|
|
714
|
+
const tagline = args.tagline || 'Modern AI-driven application workspace'
|
|
715
|
+
const themeColor = args.theme_color || '#4f46e5'
|
|
716
|
+
const initial = projectName.charAt(0).toUpperCase()
|
|
717
|
+
|
|
718
|
+
let logoDataUri = null
|
|
719
|
+
if (args.logo_image) {
|
|
720
|
+
try {
|
|
721
|
+
const src = await resolveConversationImage(ctx, exec, args.logo_image)
|
|
722
|
+
if (src && src.bytes) {
|
|
723
|
+
const b64 = Buffer.isBuffer(src.bytes) ? src.bytes.toString('base64') : Buffer.from(src.bytes).toString('base64')
|
|
724
|
+
logoDataUri = `data:${src.mediaType || 'image/png'};base64,${b64}`
|
|
725
|
+
}
|
|
726
|
+
} catch (_) {}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const exportedFiles = []
|
|
730
|
+
|
|
731
|
+
// 1. Favicon SVG
|
|
732
|
+
const faviconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
733
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
734
|
+
<rect width="64" height="64" rx="16" fill="${themeColor}"/>
|
|
735
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="8" y="8" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>` : `<text x="32" y="44" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="34" font-weight="900" text-anchor="middle">${initial}</text>`}
|
|
736
|
+
</svg>`
|
|
737
|
+
const faviconPath = path.join(targetDir, 'favicon.svg')
|
|
738
|
+
await writeFile(faviconPath, faviconSvg, 'utf8')
|
|
739
|
+
exportedFiles.push(faviconPath)
|
|
740
|
+
|
|
741
|
+
// 2. Icon 192 & 512
|
|
742
|
+
for (const size of [192, 512]) {
|
|
743
|
+
const rx = Math.round(size * 0.22)
|
|
744
|
+
const fontSize = Math.round(size * 0.52)
|
|
745
|
+
const yOffset = Math.round(size * 0.68)
|
|
746
|
+
const iconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
747
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
748
|
+
<defs>
|
|
749
|
+
<linearGradient id="brandGrad-${size}" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
750
|
+
<stop offset="0%" stop-color="${themeColor}"/>
|
|
751
|
+
<stop offset="100%" stop-color="#1e1b4b"/>
|
|
752
|
+
</linearGradient>
|
|
753
|
+
</defs>
|
|
754
|
+
<rect width="${size}" height="${size}" rx="${rx}" fill="url(#brandGrad-${size})"/>
|
|
755
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="${size * 0.15}" y="${size * 0.15}" width="${size * 0.7}" height="${size * 0.7}" preserveAspectRatio="xMidYMid meet"/>` : `<text x="${size / 2}" y="${yOffset}" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="${fontSize}" font-weight="900" text-anchor="middle">${initial}</text>`}
|
|
756
|
+
</svg>`
|
|
757
|
+
const iconPath = path.join(targetDir, `icon-${size}.svg`)
|
|
758
|
+
await writeFile(iconPath, iconSvg, 'utf8')
|
|
759
|
+
exportedFiles.push(iconPath)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// 3. Web App Manifest
|
|
763
|
+
const manifest = {
|
|
764
|
+
name: projectName,
|
|
765
|
+
short_name: projectName.slice(0, 12),
|
|
766
|
+
description: tagline,
|
|
767
|
+
start_url: '/',
|
|
768
|
+
display: 'standalone',
|
|
769
|
+
background_color: '#0b0c0e',
|
|
770
|
+
theme_color: themeColor,
|
|
771
|
+
icons: [
|
|
772
|
+
{ src: 'favicon.svg', sizes: '64x64', type: 'image/svg+xml', purpose: 'any' },
|
|
773
|
+
{ src: 'icon-192.svg', sizes: '192x192', type: 'image/svg+xml', purpose: 'any maskable' },
|
|
774
|
+
{ src: 'icon-512.svg', sizes: '512x512', type: 'image/svg+xml', purpose: 'any maskable' },
|
|
775
|
+
],
|
|
776
|
+
}
|
|
777
|
+
const manifestPath = path.join(targetDir, 'manifest.webmanifest')
|
|
778
|
+
await writeFile(manifestPath, JSON.stringify(manifest, null, 2), 'utf8')
|
|
779
|
+
exportedFiles.push(manifestPath)
|
|
780
|
+
|
|
781
|
+
// 4. OpenGraph Social Card (1200x630)
|
|
782
|
+
const escapedProject = projectName.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
783
|
+
const escapedTagline = tagline.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
784
|
+
const ogCardSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
785
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
786
|
+
<defs>
|
|
787
|
+
<radialGradient id="bgGlow" cx="75%" cy="25%" r="70%">
|
|
788
|
+
<stop offset="0%" stop-color="${themeColor}" stop-opacity="0.35"/>
|
|
789
|
+
<stop offset="60%" stop-color="#0b0c0e" stop-opacity="0.95"/>
|
|
790
|
+
<stop offset="100%" stop-color="#050506" stop-opacity="1"/>
|
|
791
|
+
</radialGradient>
|
|
792
|
+
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
|
793
|
+
<feDropShadow dx="0" dy="16" stdDeviation="24" flood-color="#000000" flood-opacity="0.6"/>
|
|
794
|
+
</filter>
|
|
795
|
+
</defs>
|
|
796
|
+
<rect width="1200" height="630" fill="url(#bgGlow)"/>
|
|
797
|
+
<rect x="80" y="80" width="1040" height="470" rx="24" fill="rgba(255,255,255,0.03)" stroke="rgba(255,255,255,0.08)" stroke-width="1.5" filter="url(#shadow)"/>
|
|
798
|
+
|
|
799
|
+
<rect x="130" y="140" width="110" height="32" rx="16" fill="${themeColor}" fill-opacity="0.2" stroke="${themeColor}" stroke-opacity="0.4"/>
|
|
800
|
+
<text x="185" y="161" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="12" font-weight="700" text-anchor="middle" letter-spacing="1">DSH STUDIO</text>
|
|
801
|
+
|
|
802
|
+
<text x="130" y="250" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="56" font-weight="800">${escapedProject}</text>
|
|
803
|
+
<text x="130" y="310" fill="#94a3b8" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="22" font-weight="400">${escapedTagline}</text>
|
|
804
|
+
|
|
805
|
+
<g transform="translate(860, 200)">
|
|
806
|
+
<rect width="180" height="180" rx="36" fill="${themeColor}" fill-opacity="0.15" stroke="${themeColor}" stroke-width="2"/>
|
|
807
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="20" y="20" width="140" height="140" preserveAspectRatio="xMidYMid meet"/>` : `<text x="90" y="125" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="96" font-weight="900" text-anchor="middle">${initial}</text>`}
|
|
808
|
+
</g>
|
|
809
|
+
|
|
810
|
+
<text x="130" y="480" fill="#64748b" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="14" font-weight="500">Auto-generated by @goodandready/dsh-image-gen</text>
|
|
811
|
+
</svg>`
|
|
812
|
+
const ogCardPath = path.join(targetDir, 'og-card.svg')
|
|
813
|
+
await writeFile(ogCardPath, ogCardSvg, 'utf8')
|
|
814
|
+
exportedFiles.push(ogCardPath)
|
|
815
|
+
|
|
816
|
+
// 5. Catalog Index JSON
|
|
817
|
+
const catalog = {
|
|
818
|
+
project: projectName,
|
|
819
|
+
tagline,
|
|
820
|
+
themeColor,
|
|
821
|
+
generatedAt: new Date().toISOString(),
|
|
822
|
+
generator: '@goodandready/dsh-image-gen',
|
|
823
|
+
assets: exportedFiles.map((f) => path.basename(f)),
|
|
824
|
+
}
|
|
825
|
+
const catalogPath = path.join(targetDir, 'catalog.json')
|
|
826
|
+
await writeFile(catalogPath, JSON.stringify(catalog, null, 2), 'utf8')
|
|
827
|
+
exportedFiles.push(catalogPath)
|
|
828
|
+
|
|
829
|
+
const summary = `### Exported Brand Asset Pack (${projectName})\n`
|
|
830
|
+
+ `- **Destination**: \`${targetDir}\`\n`
|
|
831
|
+
+ `- **Files Generated**: ${exportedFiles.length} brand files\n`
|
|
832
|
+
+ ` - \`favicon.svg\`, \`icon-192.svg\`, \`icon-512.svg\`\n`
|
|
833
|
+
+ ` - \`manifest.webmanifest\` (PWA ready)\n`
|
|
834
|
+
+ ` - \`og-card.svg\` (1200×630 OpenGraph preview card)\n`
|
|
835
|
+
+ ` - \`catalog.json\` (Asset registry index)\n\n`
|
|
836
|
+
+ ``
|
|
837
|
+
|
|
838
|
+
return toLosslessJson({
|
|
839
|
+
summary,
|
|
840
|
+
manifestPath,
|
|
841
|
+
ogCardPath,
|
|
842
|
+
catalogPath,
|
|
843
|
+
exportedFiles,
|
|
844
|
+
})
|
|
845
|
+
} catch (err) {
|
|
846
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
}),
|
|
850
|
+
)
|
|
851
|
+
}, 'dsh-image-gen: tool export_asset_pack')
|
|
852
|
+
|
|
853
|
+
|
|
496
854
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.22",
|
|
4
4
|
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|