@goodandready/dsh-image-gen 0.11.11 → 0.11.13
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 +8 -0
- package/lib/anchor-helpers.js +38 -9
- package/lib/character-sheet-helpers.js +115 -0
- package/lib/client.js +86 -13
- package/lib/diagram-helpers.js +105 -0
- package/lib/index.js +4 -1
- package/lib/providers.js +7 -3
- package/lib/register-tools.js +7 -1
- package/lib/security.js +5 -4
- package/lib/settings-route.js +1 -1
- package/lib/tools/anchor.js +2 -2
- package/lib/tools/character-sheet.js +183 -0
- package/lib/tools/diagram.js +168 -0
- package/lib/tools/generation.js +46 -22
- package/lib/tools/style-matrix.js +1 -1
- package/lib/tools/ui-asset.js +1 -1
- package/lib/tools/vision-ocr.js +277 -0
- package/lib/updater.js +1 -1
- package/lib/vision-ocr-helpers.js +119 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -167,6 +167,14 @@
|
|
|
167
167
|
* **Cache & History Consistency**: Ensured content-addressed disk cache hits synchronize seamlessly with generation history and sidecar metadata.
|
|
168
168
|
* **Test Suite Expansion**: Added `test/batch4-hardening.test.mjs`, expanding test coverage to **128 automated unit tests (100% pass)**.
|
|
169
169
|
|
|
170
|
+
### 🚀 What's New in v0.11.12
|
|
171
|
+
* **Character Sheet Generator (`generate_character_sheet`, #181)**: Synthesizes consistent character turnaround and model sheets (1x3, 2x2, emotions) preserving facial features and attire across anime, 3D, concept, and comic styles.
|
|
172
|
+
* **FaceID & Portrait Consistency (`face_reference`, #183)**: FaceID / IP-Adapter prompt directives and `face` anchor mode in `set_style_anchor` for persistent facial likeness across scenes.
|
|
173
|
+
* **OCR Localization & Text Replacement (`replace_image_text`, #180)**: Automated OCR text detection via `dsh-vision-bridge`, seamless background inpainting, and crisp typography replacement at original coordinates.
|
|
174
|
+
* **3D Isometric Diagram Beautifier (`beautify_diagram`, #185)**: Converts raw Mermaid/Graphviz architecture diagrams into stunning 3D isometric tech illustrations for documentation and hero headers.
|
|
175
|
+
* **Interactive Inpainting Canvas (#328)**: In-chat masking overlay with aspect ratio auto-detection, adjustable brush/eraser, and direct prompt dispatch.
|
|
176
|
+
* **Live Progress Stream (#329)**: Real-time progress bar, stage indicators, and denoising step counters in `FalImageCard`.
|
|
177
|
+
|
|
170
178
|
### 🚀 What's New in v0.11.9
|
|
171
179
|
* **ComfyUI Local Backend Payload Fix (#322, GitHub #7)**: Eliminated redundant nesting in `lib/providers/backends/local.js` that wrapped workflows as `{ prompt: { prompt: workflowGraph } }`, resolving `missing_node_type: Node 'ID #prompt' has no class_type` HTTP 400 errors during local ComfyUI generations. Added node shape validation before submission.
|
|
172
180
|
|
package/lib/anchor-helpers.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// anchor-helpers.js — Character & Style Reference Anchor manager (#283).
|
|
1
|
+
// anchor-helpers.js — Character & Style Reference Anchor manager (#283, #183).
|
|
2
2
|
// Maintains active visual identity and style reference across multi-turn sessions.
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* In-memory registry of active visual anchors keyed by scopeId (e.g. session id).
|
|
6
|
-
* Structure: Map<string, { image: string, label: string, mode: 'character'|'style', strength: number, updatedAt: string }>
|
|
6
|
+
* Structure: Map<string, { image: string, label: string, mode: 'character'|'style'|'face', strength: number, updatedAt: string }>
|
|
7
7
|
*/
|
|
8
8
|
const sessionAnchors = new Map()
|
|
9
9
|
|
|
@@ -11,10 +11,11 @@ const sessionAnchors = new Map()
|
|
|
11
11
|
* Normalizes anchor strength between 0.1 and 1.0.
|
|
12
12
|
*
|
|
13
13
|
* @param {number|undefined} val
|
|
14
|
+
* @param {number} [defaultVal=0.65]
|
|
14
15
|
* @returns {number}
|
|
15
16
|
*/
|
|
16
|
-
export function normalizeAnchorStrength(val) {
|
|
17
|
-
if (typeof val !== 'number' || Number.isNaN(val)) return
|
|
17
|
+
export function normalizeAnchorStrength(val, defaultVal = 0.65) {
|
|
18
|
+
if (typeof val !== 'number' || Number.isNaN(val)) return defaultVal
|
|
18
19
|
return Math.min(1.0, Math.max(0.1, Number(val.toFixed(2))))
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -25,7 +26,7 @@ export function normalizeAnchorStrength(val) {
|
|
|
25
26
|
* @param {object} data
|
|
26
27
|
* @param {string} data.image Image URL, attachment ID (sha256:...), or local path
|
|
27
28
|
* @param {string} [data.label] Human-readable description of character/style
|
|
28
|
-
* @param {'character'|'style'} [data.mode='style']
|
|
29
|
+
* @param {'character'|'style'|'face'} [data.mode='style']
|
|
29
30
|
* @param {number} [data.strength=0.65]
|
|
30
31
|
* @returns {object} The stored anchor record
|
|
31
32
|
*/
|
|
@@ -35,9 +36,11 @@ export function setSessionAnchor(scopeId, data = {}) {
|
|
|
35
36
|
throw new Error('An anchor image (URL, path, or attachment ID) is required.')
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
const mode = data.mode === 'character'
|
|
39
|
-
const
|
|
40
|
-
const
|
|
39
|
+
const mode = data.mode === 'character' || data.mode === 'face' ? data.mode : 'style'
|
|
40
|
+
const defaultStrength = mode === 'face' || mode === 'character' ? 0.75 : 0.65
|
|
41
|
+
const strength = normalizeAnchorStrength(data.strength, defaultStrength)
|
|
42
|
+
const defaultLabel = mode === 'face' ? 'Portrait Reference' : (mode === 'character' ? 'Character Anchor' : 'Style Anchor')
|
|
43
|
+
const label = String(data.label || defaultLabel).trim()
|
|
41
44
|
|
|
42
45
|
const anchor = {
|
|
43
46
|
image: String(data.image).trim(),
|
|
@@ -95,9 +98,35 @@ export function applyAnchorPromptHints(anchor, prompt) {
|
|
|
95
98
|
// Avoid duplicate injection if user already mentioned the anchor label
|
|
96
99
|
if (lower.includes(labelLower)) return prompt
|
|
97
100
|
|
|
98
|
-
if (anchor.mode === 'character') {
|
|
101
|
+
if (anchor.mode === 'face' || anchor.mode === 'character') {
|
|
99
102
|
return `${prompt}, visual character anchor: ${anchor.label}, maintaining consistent facial features, costume, and physical identity`
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
return `${prompt}, visual style anchor: ${anchor.label}, maintaining consistent aesthetic palette, rendering technique, and lighting`
|
|
103
106
|
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Builds prompt directives for FaceID / IP-Adapter portrait consistency (#183).
|
|
110
|
+
*
|
|
111
|
+
* @param {object} options
|
|
112
|
+
* @param {string} options.prompt Core scene description
|
|
113
|
+
* @param {number} [options.faceStrength=0.75] Identity preservation weight (0.1 - 1.0)
|
|
114
|
+
* @param {string} [options.characterName] Optional character identifier
|
|
115
|
+
* @returns {{ prompt: string, negativePrompt: string, faceStrength: number }}
|
|
116
|
+
*/
|
|
117
|
+
export function buildFaceConsistencyPrompt(options = {}) {
|
|
118
|
+
const basePrompt = String(options.prompt || '').trim()
|
|
119
|
+
const faceStrength = normalizeAnchorStrength(options.faceStrength, 0.75)
|
|
120
|
+
const nameSuffix = options.characterName ? ` of ${options.characterName}` : ''
|
|
121
|
+
|
|
122
|
+
const faceDirective = `FaceID facial identity match${nameSuffix} (strength: ${faceStrength}), preserving exact facial structure, eye shape, nose, lips, jawline and recognizable facial landmarks from portrait reference`
|
|
123
|
+
const combinedPrompt = basePrompt ? `${basePrompt}, ${faceDirective}` : faceDirective
|
|
124
|
+
|
|
125
|
+
const faceNegative = 'altered face, different face, distorted facial features, asymmetric eyes, morphed facial landmarks, low facial detail, blurry face'
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
prompt: combinedPrompt,
|
|
129
|
+
negativePrompt: faceNegative,
|
|
130
|
+
faceStrength,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// character-sheet-helpers.js — helper functions for character_sheet_generator tool (#181).
|
|
2
|
+
|
|
3
|
+
export const CHARACTER_SHEET_LAYOUTS = {
|
|
4
|
+
turnaround: {
|
|
5
|
+
name: 'turnaround',
|
|
6
|
+
label: 'Full Turnaround (4-view horizontal)',
|
|
7
|
+
aspectRatio: '16:9',
|
|
8
|
+
views: ['front view', 'side profile view', 'three-quarter view', 'back view'],
|
|
9
|
+
directive: 'full character turnaround sheet showing multiple sequential angles: front view, side profile view, three-quarter view, and back view arranged horizontally',
|
|
10
|
+
},
|
|
11
|
+
'1x3': {
|
|
12
|
+
name: '1x3',
|
|
13
|
+
label: 'Triptych (Front, Side, Back)',
|
|
14
|
+
aspectRatio: '16:9',
|
|
15
|
+
views: ['front view', 'side view', 'back view'],
|
|
16
|
+
directive: 'character model sheet with 3 full-body views: front view, side view, back view side-by-side',
|
|
17
|
+
},
|
|
18
|
+
'2x2': {
|
|
19
|
+
name: '2x2',
|
|
20
|
+
label: 'Quad Grid (2x2 Multi-Angle)',
|
|
21
|
+
aspectRatio: '1:1',
|
|
22
|
+
views: ['front view', 'three-quarter view', 'close-up portrait', 'back view'],
|
|
23
|
+
directive: '2x2 character reference sheet grid featuring: top-left front view, top-right three-quarter view, bottom-left close-up facial portrait, bottom-right back view',
|
|
24
|
+
},
|
|
25
|
+
emotions: {
|
|
26
|
+
name: 'emotions',
|
|
27
|
+
label: 'Facial Expression Sheet (4 Expressions)',
|
|
28
|
+
aspectRatio: '1:1',
|
|
29
|
+
views: ['neutral', 'happy / smiling', 'focused / intense', 'shocked / surprised'],
|
|
30
|
+
directive: 'character facial expression sheet displaying identical character with 4 distinct expressions: neutral, happy/smiling, focused/intense, and surprised',
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const CHARACTER_SHEET_STYLES = {
|
|
35
|
+
concept_art: {
|
|
36
|
+
name: 'concept_art',
|
|
37
|
+
label: 'Digital Concept Art',
|
|
38
|
+
directive: 'digital concept art, character model sheet, clean sharp silhouette, production reference, ArtStation featured',
|
|
39
|
+
},
|
|
40
|
+
anime: {
|
|
41
|
+
name: 'anime',
|
|
42
|
+
label: 'Anime & Manga',
|
|
43
|
+
directive: 'clean modern anime production sheet, precise lineart, cel-shaded coloring, animation design document',
|
|
44
|
+
},
|
|
45
|
+
'3d_animation': {
|
|
46
|
+
name: '3d_animation',
|
|
47
|
+
label: '3D Feature Film CGI',
|
|
48
|
+
directive: '3D animated feature film character model, subsurface scattering, Octane render, Pixar/Dreamworks aesthetic, cinematic lighting',
|
|
49
|
+
},
|
|
50
|
+
pixel_art: {
|
|
51
|
+
name: 'pixel_art',
|
|
52
|
+
label: '16-bit Pixel Art',
|
|
53
|
+
directive: 'crisp 16-bit pixel art character design sheet, game asset sprite reference, clean pixel clusters, retro aesthetic',
|
|
54
|
+
},
|
|
55
|
+
realistic: {
|
|
56
|
+
name: 'realistic',
|
|
57
|
+
label: 'Cinematic Photorealism',
|
|
58
|
+
directive: 'cinematic studio photography, detailed skin texture, 85mm lens, neutral gray studio backdrop, master lighting',
|
|
59
|
+
},
|
|
60
|
+
comic: {
|
|
61
|
+
name: 'comic',
|
|
62
|
+
label: 'Graphic Novel / Comic',
|
|
63
|
+
directive: 'graphic novel art style, dynamic ink lines, bold cross-hatching, rich comic book color palette',
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Builds the specialized prompt for generating consistent character sheets.
|
|
69
|
+
*
|
|
70
|
+
* @param {object} options
|
|
71
|
+
* @param {string} options.prompt Base character description
|
|
72
|
+
* @param {string} [options.layout] 'turnaround' | '1x3' | '2x2' | 'emotions'
|
|
73
|
+
* @param {string} [options.style] Style preset key
|
|
74
|
+
* @param {string} [options.negativePrompt] Optional negative directives
|
|
75
|
+
* @returns {{ prompt: string, negativePrompt: string, aspectRatio: string, views: string[] }}
|
|
76
|
+
*/
|
|
77
|
+
export function buildCharacterSheetPrompt(options = {}) {
|
|
78
|
+
const baseDesc = String(options.prompt || '').trim()
|
|
79
|
+
if (!baseDesc) {
|
|
80
|
+
throw new Error('Character description prompt is required for character sheet generation')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const layoutKey = options.layout && CHARACTER_SHEET_LAYOUTS[options.layout]
|
|
84
|
+
? options.layout
|
|
85
|
+
: 'turnaround'
|
|
86
|
+
const layoutDef = CHARACTER_SHEET_LAYOUTS[layoutKey]
|
|
87
|
+
|
|
88
|
+
const styleKey = options.style && CHARACTER_SHEET_STYLES[options.style]
|
|
89
|
+
? options.style
|
|
90
|
+
: 'concept_art'
|
|
91
|
+
const styleDef = CHARACTER_SHEET_STYLES[styleKey]
|
|
92
|
+
|
|
93
|
+
const consistencyAnchor = 'single unified character sheet, identical person across all angles, same facial features, same hairstyle, identical outfit and colors, consistent proportions, neutral neutral background'
|
|
94
|
+
|
|
95
|
+
const finalPrompt = [
|
|
96
|
+
layoutDef.directive,
|
|
97
|
+
`Character: ${baseDesc}`,
|
|
98
|
+
styleDef.directive,
|
|
99
|
+
consistencyAnchor,
|
|
100
|
+
].filter(Boolean).join(', ')
|
|
101
|
+
|
|
102
|
+
const defaultNegative = 'different characters, inconsistent face, mismatched clothing, cropped limbs, cluttered background, chaotic layout, watermark, low quality, distorted anatomy'
|
|
103
|
+
const finalNegative = options.negativePrompt
|
|
104
|
+
? `${defaultNegative}, ${options.negativePrompt}`
|
|
105
|
+
: defaultNegative
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
prompt: finalPrompt,
|
|
109
|
+
negativePrompt: finalNegative,
|
|
110
|
+
aspectRatio: layoutDef.aspectRatio,
|
|
111
|
+
views: layoutDef.views,
|
|
112
|
+
layout: layoutKey,
|
|
113
|
+
style: styleKey,
|
|
114
|
+
}
|
|
115
|
+
}
|
package/lib/client.js
CHANGED
|
@@ -140,6 +140,12 @@ window.__ModuleLoader__.load({
|
|
|
140
140
|
.ig-progressive-wrap{position:relative;width:100%;overflow:hidden;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);min-height:160px;display:flex;align-items:center;justify-content:center}
|
|
141
141
|
.ig-draft-img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;filter:blur(6px);opacity:0.75;transform:scale(1.02);transition:opacity 0.4s ease}
|
|
142
142
|
.ig-final-img{position:relative;width:100%;height:auto;display:block;opacity:1;transition:opacity 0.35s ease}
|
|
143
|
+
|
|
144
|
+
.ig-progress-container{display:flex;flex-direction:column;gap:5px;padding:8px 10px;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;margin-top:6px}
|
|
145
|
+
.ig-progress-meta{display:flex;justify-content:space-between;align-items:center;font-size:11px;color:var(--dsw-alias-label-secondary);font-weight:500}
|
|
146
|
+
.ig-progress-track{height:6px;width:100%;background:var(--dsw-alias-bg-layer-1);border-radius:3px;overflow:hidden;position:relative}
|
|
147
|
+
.ig-progress-fill{height:100%;background:var(--dsw-alias-state-brand);border-radius:3px;transition:width 0.25s ease-in-out}
|
|
148
|
+
|
|
143
149
|
.ig-draft-overlay{position:absolute;bottom:8px;left:8px;padding:3px 8px;border-radius:4px;background:color-mix(in srgb, var(--dsw-alias-bg-layer-1) 80%, transparent);font-size:11px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:6px;backdrop-filter:blur(4px)}
|
|
144
150
|
|
|
145
151
|
@media (prefers-reduced-motion: reduce){
|
|
@@ -1543,6 +1549,9 @@ window.__ModuleLoader__.load({
|
|
|
1543
1549
|
src: displaySrc,
|
|
1544
1550
|
draftSrc,
|
|
1545
1551
|
isRunning: running,
|
|
1552
|
+
progress: typeof parsed?.progress === 'number' ? parsed.progress : (typeof block?.progress === 'number' ? block.progress : undefined),
|
|
1553
|
+
step: parsed?.step || block?.step,
|
|
1554
|
+
stage: parsed?.stage || block?.stage,
|
|
1546
1555
|
alt: prompt || 'generated image',
|
|
1547
1556
|
t,
|
|
1548
1557
|
}))
|
|
@@ -1645,6 +1654,8 @@ window.__ModuleLoader__.load({
|
|
|
1645
1654
|
react,
|
|
1646
1655
|
t,
|
|
1647
1656
|
parsed,
|
|
1657
|
+
imgUrl: parsed.imageUrl || parsed.url || parsed.attachment?.url,
|
|
1658
|
+
targetRef: parsed.attachmentId || parsed.attachment?.attachmentId || parsed.imageUrl || parsed.url,
|
|
1648
1659
|
onClose: () => setInpaintOpen(false),
|
|
1649
1660
|
}) : null
|
|
1650
1661
|
|
|
@@ -1710,11 +1721,14 @@ window.__ModuleLoader__.load({
|
|
|
1710
1721
|
current.seed !== undefined ? `seed: ${current.seed}` : (current.timestamp ? new Date(current.timestamp).toLocaleTimeString() : '')
|
|
1711
1722
|
)
|
|
1712
1723
|
)
|
|
1713
|
-
} // -------------------------------------------------------------- Progressive Draft Preview (#147)
|
|
1724
|
+
} // -------------------------------------------------------------- Progressive Draft & Live Progress Preview (#147, #329)
|
|
1714
1725
|
function ProgressiveImagePreview(props) {
|
|
1715
1726
|
const src = props.src
|
|
1716
1727
|
const draftSrc = props.draftSrc
|
|
1717
1728
|
const isRunning = props.isRunning
|
|
1729
|
+
const progress = typeof props.progress === 'number' ? Math.min(100, Math.max(0, props.progress)) : undefined
|
|
1730
|
+
const step = props.step
|
|
1731
|
+
const stage = props.stage || (progress !== undefined && progress < 100 ? 'Rendering...' : 'Complete')
|
|
1718
1732
|
const alt = props.alt || 'Generated image'
|
|
1719
1733
|
const t = props.t || ((k) => k)
|
|
1720
1734
|
|
|
@@ -1724,7 +1738,28 @@ window.__ModuleLoader__.load({
|
|
|
1724
1738
|
setLoaded(false)
|
|
1725
1739
|
}, [src])
|
|
1726
1740
|
|
|
1727
|
-
|
|
1741
|
+
const showProgress = isRunning || (progress !== undefined && progress < 100)
|
|
1742
|
+
|
|
1743
|
+
const progressElement = showProgress ? react.createElement(
|
|
1744
|
+
'div',
|
|
1745
|
+
{ className: 'ig-progress-container' },
|
|
1746
|
+
react.createElement(
|
|
1747
|
+
'div',
|
|
1748
|
+
{ className: 'ig-progress-meta' },
|
|
1749
|
+
react.createElement('span', null, '⚡ ' + stage + (step ? ' (' + step + ')' : '')),
|
|
1750
|
+
progress !== undefined ? react.createElement('span', { style: { fontWeight: '700', color: 'var(--dsw-alias-state-brand)' } }, Math.round(progress) + '%') : null
|
|
1751
|
+
),
|
|
1752
|
+
react.createElement(
|
|
1753
|
+
'div',
|
|
1754
|
+
{ className: 'ig-progress-track' },
|
|
1755
|
+
react.createElement('div', {
|
|
1756
|
+
className: 'ig-progress-fill',
|
|
1757
|
+
style: { width: (progress !== undefined ? progress : 45) + '%' },
|
|
1758
|
+
})
|
|
1759
|
+
)
|
|
1760
|
+
) : null
|
|
1761
|
+
|
|
1762
|
+
if (showProgress && !src && draftSrc) {
|
|
1728
1763
|
return react.createElement(
|
|
1729
1764
|
'div',
|
|
1730
1765
|
{ className: 'ig-progressive-wrap' },
|
|
@@ -1737,10 +1772,15 @@ window.__ModuleLoader__.load({
|
|
|
1737
1772
|
'div',
|
|
1738
1773
|
{ className: 'ig-draft-overlay' },
|
|
1739
1774
|
'⚡ ' + (t('card.draftPreview') || 'Rendering draft...')
|
|
1740
|
-
)
|
|
1775
|
+
),
|
|
1776
|
+
progressElement
|
|
1741
1777
|
)
|
|
1742
1778
|
}
|
|
1743
1779
|
|
|
1780
|
+
if (!src && !draftSrc && showProgress) {
|
|
1781
|
+
return progressElement
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1744
1784
|
if (!src && !draftSrc) return null
|
|
1745
1785
|
|
|
1746
1786
|
return react.createElement(
|
|
@@ -1758,12 +1798,14 @@ window.__ModuleLoader__.load({
|
|
|
1758
1798
|
className: 'ig-final-img',
|
|
1759
1799
|
style: { opacity: loaded || !draftSrc ? 1 : 0.8 },
|
|
1760
1800
|
onLoad: () => setLoaded(true),
|
|
1761
|
-
}) : null
|
|
1801
|
+
}) : null,
|
|
1802
|
+
progressElement
|
|
1762
1803
|
)
|
|
1763
|
-
}
|
|
1804
|
+
}
|
|
1805
|
+
// 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285, #301, #328).
|
|
1764
1806
|
|
|
1765
1807
|
function InpaintCanvas(props) {
|
|
1766
|
-
const { imgUrl, targetRef, onClose, t } = props
|
|
1808
|
+
const { imgUrl, targetRef, parsed, onClose, t } = props
|
|
1767
1809
|
const canvasRef = react.useRef(null)
|
|
1768
1810
|
const isDrawingRef = react.useRef(false)
|
|
1769
1811
|
const undoStackRef = react.useRef([])
|
|
@@ -1772,6 +1814,10 @@ window.__ModuleLoader__.load({
|
|
|
1772
1814
|
const [hasStrokes, setHasStrokes] = react.useState(false)
|
|
1773
1815
|
const [copied, setCopied] = react.useState(false)
|
|
1774
1816
|
const [inpaintPrompt, setInpaintPrompt] = react.useState('')
|
|
1817
|
+
const [canvasDims, setCanvasDims] = react.useState({ width: 512, height: 512, ratio: '1/1' })
|
|
1818
|
+
|
|
1819
|
+
const effectiveImgUrl = imgUrl || parsed?.imageUrl || parsed?.url || parsed?.attachment?.url || ''
|
|
1820
|
+
const effectiveTargetRef = targetRef || parsed?.attachmentId || parsed?.attachment?.attachmentId || parsed?.imageUrl || parsed?.url || 'current image'
|
|
1775
1821
|
|
|
1776
1822
|
const initCanvas = react.useCallback(() => {
|
|
1777
1823
|
const canvas = canvasRef.current
|
|
@@ -1785,7 +1831,19 @@ window.__ModuleLoader__.load({
|
|
|
1785
1831
|
|
|
1786
1832
|
react.useEffect(() => {
|
|
1787
1833
|
initCanvas()
|
|
1788
|
-
}, [initCanvas])
|
|
1834
|
+
}, [initCanvas, canvasDims])
|
|
1835
|
+
|
|
1836
|
+
function handleImageLoaded(e) {
|
|
1837
|
+
if (e.target && e.target.naturalWidth && e.target.naturalHeight) {
|
|
1838
|
+
const nw = e.target.naturalWidth
|
|
1839
|
+
const nh = e.target.naturalHeight
|
|
1840
|
+
// Scale dimensions down to manageable working canvas size (max 768)
|
|
1841
|
+
const scale = Math.min(1, 768 / Math.max(nw, nh))
|
|
1842
|
+
const cw = Math.round(nw * scale)
|
|
1843
|
+
const ch = Math.round(nh * scale)
|
|
1844
|
+
setCanvasDims({ width: cw, height: ch, ratio: `${cw}/${ch}` })
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1789
1847
|
|
|
1790
1848
|
function getPos(e, canvas) {
|
|
1791
1849
|
const rect = canvas.getBoundingClientRect()
|
|
@@ -1902,13 +1960,21 @@ window.__ModuleLoader__.load({
|
|
|
1902
1960
|
if (!canvas) return
|
|
1903
1961
|
const maskDataUrl = canvas.toDataURL('image/png')
|
|
1904
1962
|
const promptText = inpaintPrompt.trim() || 'inpaint masked region'
|
|
1905
|
-
const instruction = 'Use edit_image with prompt: "' + promptText + '", source_image: "' +
|
|
1963
|
+
const instruction = 'Use edit_image with prompt: "' + promptText + '", source_image: "' + effectiveTargetRef + '", mask: "' + maskDataUrl + '"'
|
|
1906
1964
|
|
|
1907
1965
|
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
|
1908
1966
|
navigator.clipboard.writeText(instruction)
|
|
1909
1967
|
setCopied(true)
|
|
1910
1968
|
setTimeout(() => setCopied(false), 2500)
|
|
1911
1969
|
}
|
|
1970
|
+
|
|
1971
|
+
try {
|
|
1972
|
+
const textarea = document.querySelector('textarea[placeholder*="message"], textarea[class*="chat"], textarea')
|
|
1973
|
+
if (textarea) {
|
|
1974
|
+
textarea.value = instruction
|
|
1975
|
+
if (typeof window !== 'undefined' && typeof window.Event === 'function') textarea.dispatchEvent(new window.Event('input', { bubbles: true }))
|
|
1976
|
+
}
|
|
1977
|
+
} catch (_err) { /* non-fatal: optional textarea dispatch */ }
|
|
1912
1978
|
}
|
|
1913
1979
|
|
|
1914
1980
|
return react.createElement(
|
|
@@ -2004,7 +2070,7 @@ window.__ModuleLoader__.load({
|
|
|
2004
2070
|
position: 'relative',
|
|
2005
2071
|
width: '100%',
|
|
2006
2072
|
maxWidth: '512px',
|
|
2007
|
-
aspectRatio:
|
|
2073
|
+
aspectRatio: canvasDims.ratio,
|
|
2008
2074
|
borderRadius: '8px',
|
|
2009
2075
|
overflow: 'hidden',
|
|
2010
2076
|
border: '1px solid var(--dsw-alias-border-l2)',
|
|
@@ -2014,9 +2080,10 @@ window.__ModuleLoader__.load({
|
|
|
2014
2080
|
touchAction: 'none',
|
|
2015
2081
|
},
|
|
2016
2082
|
},
|
|
2017
|
-
|
|
2018
|
-
src:
|
|
2083
|
+
effectiveImgUrl ? react.createElement('img', {
|
|
2084
|
+
src: effectiveImgUrl,
|
|
2019
2085
|
alt: 'Source for Inpaint',
|
|
2086
|
+
onLoad: handleImageLoaded,
|
|
2020
2087
|
style: {
|
|
2021
2088
|
position: 'absolute',
|
|
2022
2089
|
top: 0,
|
|
@@ -2030,8 +2097,8 @@ window.__ModuleLoader__.load({
|
|
|
2030
2097
|
}) : null,
|
|
2031
2098
|
react.createElement('canvas', {
|
|
2032
2099
|
ref: canvasRef,
|
|
2033
|
-
width:
|
|
2034
|
-
height:
|
|
2100
|
+
width: canvasDims.width,
|
|
2101
|
+
height: canvasDims.height,
|
|
2035
2102
|
style: {
|
|
2036
2103
|
position: 'absolute',
|
|
2037
2104
|
top: 0,
|
|
@@ -3009,6 +3076,12 @@ window.__ModuleLoader__.load({
|
|
|
3009
3076
|
'smart_crop_image',
|
|
3010
3077
|
'export_asset_pack',
|
|
3011
3078
|
'generate_ui_asset',
|
|
3079
|
+
'generate_character_sheet',
|
|
3080
|
+
'replace_image_text',
|
|
3081
|
+
'beautify_diagram',
|
|
3082
|
+
'generate_seamless_pattern',
|
|
3083
|
+
'generate_spritesheet',
|
|
3084
|
+
'sketch_to_image',
|
|
3012
3085
|
]
|
|
3013
3086
|
|
|
3014
3087
|
for (const toolName of toolviewTools) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// diagram-helpers.js — helper functions for beautify_diagram tool (#185).
|
|
2
|
+
|
|
3
|
+
export const DIAGRAM_3D_STYLES = {
|
|
4
|
+
isometric_3d: {
|
|
5
|
+
name: 'isometric_3d',
|
|
6
|
+
label: 'Glossy 3D Isometric Architecture',
|
|
7
|
+
directive: 'ultra-detailed 3D isometric architecture visualization, floating frosted glassmorphism node panels, luminous glowing neon data pipelines, soft ambient occlusion, Octane 3D render, tech presentation hero graphic',
|
|
8
|
+
},
|
|
9
|
+
cyber_blueprint: {
|
|
10
|
+
name: 'cyber_blueprint',
|
|
11
|
+
label: 'Holographic Cyber Blueprint',
|
|
12
|
+
directive: 'holographic 3D engineering schematic, isometric grid perspective, glowing cyan and amber circuit traces, semi-transparent HUD modules, high-tech enterprise infrastructure',
|
|
13
|
+
},
|
|
14
|
+
clay_minimal: {
|
|
15
|
+
name: 'clay_minimal',
|
|
16
|
+
label: 'Minimalist Tactile Clay Render',
|
|
17
|
+
directive: 'minimalist 3D isometric clay render, smooth matte ceramic node blocks, soft directional studio lighting, elegant pastel palette on clean backdrop, tactile editorial illustration',
|
|
18
|
+
},
|
|
19
|
+
glossy_dark: {
|
|
20
|
+
name: 'glossy_dark',
|
|
21
|
+
label: 'Obsidian Glass Dark Mode',
|
|
22
|
+
directive: 'dark obsidian glassmorphic 3D diagram, luminous neon accent borders, dark slate background, glowing fiber-optic connectors, enterprise cloud architecture showcase',
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Extracts nodes and connections from raw Mermaid or Graphviz diagram text.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} sourceCode
|
|
30
|
+
* @returns {{ nodes: string[], connections: string[], summary: string }}
|
|
31
|
+
*/
|
|
32
|
+
export function parseMermaidStructure(sourceCode = '') {
|
|
33
|
+
const text = String(sourceCode).trim()
|
|
34
|
+
if (!text) {
|
|
35
|
+
return { nodes: [], connections: [], summary: 'generic multi-tiered system topology' }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const nodes = new Set()
|
|
39
|
+
const connections = []
|
|
40
|
+
|
|
41
|
+
// Extract node labels inside brackets: A[Client App], B(API Gateway), C[(Database)]
|
|
42
|
+
const bracketMatches = text.matchAll(/([A-Za-z0-9_]+)\s*(\[|\(|\{\[|\(\[)([\s\S]*?)(\]|\)|\}\]|\)\])/g)
|
|
43
|
+
for (const m of bracketMatches) {
|
|
44
|
+
const label = m[3]?.replace(/^[\(\[\{]+|[\)\]\}]+$/g, '').trim()
|
|
45
|
+
if (label && label.length < 50) {
|
|
46
|
+
nodes.add(label)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Extract connection arrows: -->, ==>, -.->
|
|
51
|
+
const arrowLines = text.split('\n').filter((l) => l.includes('-->') || l.includes('==>') || l.includes('-.->'))
|
|
52
|
+
for (const line of arrowLines.slice(0, 10)) {
|
|
53
|
+
const cleaned = line.replace(/\[.*?\]|\(.*?\)/g, '').trim()
|
|
54
|
+
if (cleaned) connections.push(cleaned)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const nodeList = Array.from(nodes)
|
|
58
|
+
let summary = ''
|
|
59
|
+
if (nodeList.length > 0) {
|
|
60
|
+
summary = `nodes representing ${nodeList.slice(0, 6).join(', ')} connected in structured data flow`
|
|
61
|
+
} else {
|
|
62
|
+
summary = 'structured multi-node architectural graph showing sequential service pipelines'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
nodes: nodeList,
|
|
67
|
+
connections,
|
|
68
|
+
summary,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Builds the prompt for beautify_diagram image synthesis.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} options
|
|
76
|
+
* @param {string} options.diagram Raw Mermaid code, SVG, or description
|
|
77
|
+
* @param {string} [options.style] Style preset key from DIAGRAM_3D_STYLES
|
|
78
|
+
* @param {string} [options.title] Optional main title
|
|
79
|
+
* @param {string} [options.aspectRatio='16:9']
|
|
80
|
+
* @returns {{ prompt: string, negativePrompt: string, style: string, parsedNodes: string[] }}
|
|
81
|
+
*/
|
|
82
|
+
export function buildDiagramIllustrationPrompt(options = {}) {
|
|
83
|
+
const { diagram, style = 'isometric_3d', title, aspectRatio = '16:9' } = options
|
|
84
|
+
const styleDef = DIAGRAM_3D_STYLES[style] || DIAGRAM_3D_STYLES.isometric_3d
|
|
85
|
+
|
|
86
|
+
const parsed = parseMermaidStructure(diagram)
|
|
87
|
+
const titlePart = title ? `titled "${title}", ` : ''
|
|
88
|
+
|
|
89
|
+
const corePrompt = [
|
|
90
|
+
`high-end 3D architectural illustration ${titlePart}`,
|
|
91
|
+
`depicting ${parsed.summary}`,
|
|
92
|
+
styleDef.directive,
|
|
93
|
+
'clean isometric angle, centered composition, pristine technical depth, 8k resolution, suitable for developer documentation and hero banner',
|
|
94
|
+
].filter(Boolean).join(', ')
|
|
95
|
+
|
|
96
|
+
const negativePrompt = 'messy handwriting, scribbles, low resolution, 2D flat wireframe, unreadable blurry text, broken geometry, chaotic distortion, watermark'
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
prompt: corePrompt,
|
|
100
|
+
negativePrompt,
|
|
101
|
+
style: styleDef.name,
|
|
102
|
+
parsedNodes: parsed.nodes,
|
|
103
|
+
aspectRatio,
|
|
104
|
+
}
|
|
105
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -312,7 +312,10 @@ export function apply(ctx, rawConfig) {
|
|
|
312
312
|
}
|
|
313
313
|
try {
|
|
314
314
|
const u = new URL(req.url, 'http://localhost')
|
|
315
|
-
const
|
|
315
|
+
const qProvider = u.searchParams.get('provider')
|
|
316
|
+
const provider = (typeof qProvider === 'string' && qProvider.trim())
|
|
317
|
+
|| (typeof config?.provider === 'string' && config.provider.trim())
|
|
318
|
+
|| 'fal'
|
|
316
319
|
const deps = {
|
|
317
320
|
fetchImpl: fetch,
|
|
318
321
|
resolveKey: (ref) => resolveApiKey(ctx, ref),
|
package/lib/providers.js
CHANGED
|
@@ -378,7 +378,7 @@ export async function blendImagesFal({ fetchImpl, resolveKey, cfg }, { images, w
|
|
|
378
378
|
/** @internal — direct provider call path for integration tests */
|
|
379
379
|
export async function editImageDirect(deps, job) {
|
|
380
380
|
const providers = makeProviders(deps, job)
|
|
381
|
-
const providerKey = job.provider || deps.cfg.defaultProvider || 'fal'
|
|
381
|
+
const providerKey = (typeof job?.provider === 'string' && job.provider.trim()) || (typeof deps?.cfg?.provider === 'string' && deps.cfg.provider.trim()) || (typeof deps?.cfg?.defaultProvider === 'string' && deps.cfg.defaultProvider.trim()) || 'fal'
|
|
382
382
|
const fn = providers[providerKey] || providers.fal || providers.custom
|
|
383
383
|
if (!fn) throw new Error(`Provider "${providerKey}" does not support image editing`)
|
|
384
384
|
return fn(job.seed, job.prompt)
|
|
@@ -391,7 +391,7 @@ export async function varyImageDirect(deps, job) {
|
|
|
391
391
|
...job,
|
|
392
392
|
strength: job.variationStrength ?? job.strength ?? 0.35,
|
|
393
393
|
})
|
|
394
|
-
const providerKey = job.provider || deps.cfg.defaultProvider || 'fal'
|
|
394
|
+
const providerKey = (typeof job?.provider === 'string' && job.provider.trim()) || (typeof deps?.cfg?.provider === 'string' && deps.cfg.provider.trim()) || (typeof deps?.cfg?.defaultProvider === 'string' && deps.cfg.defaultProvider.trim()) || 'fal'
|
|
395
395
|
const fn = providers[providerKey] || providers.fal || providers.custom
|
|
396
396
|
if (!fn) throw new Error(`Provider "${providerKey}" does not support image variations`)
|
|
397
397
|
return fn(job.seed, job.prompt)
|
|
@@ -428,7 +428,11 @@ export function toLosslessJson(val) {
|
|
|
428
428
|
export async function testProviderConnection(deps, providerId) {
|
|
429
429
|
const { fetchImpl = fetch, resolveKey, cfg } = deps
|
|
430
430
|
const start = Date.now()
|
|
431
|
-
const
|
|
431
|
+
const rawProvider = (typeof providerId === 'string' && providerId.trim())
|
|
432
|
+
|| (typeof cfg?.provider === 'string' && cfg.provider.trim())
|
|
433
|
+
|| (typeof cfg?.defaultProvider === 'string' && cfg.defaultProvider.trim())
|
|
434
|
+
|| 'fal'
|
|
435
|
+
const p = rawProvider.toLowerCase()
|
|
432
436
|
|
|
433
437
|
try {
|
|
434
438
|
if (p === 'fal') {
|
package/lib/register-tools.js
CHANGED
|
@@ -15,6 +15,9 @@ import { registerAnchorTools } from './tools/anchor.js'
|
|
|
15
15
|
import { registerUiAssetTools } from './tools/ui-asset.js'
|
|
16
16
|
import { registerStyleMatrixTools } from './tools/style-matrix.js'
|
|
17
17
|
import { registerThemePairTools } from './tools/theme-pair.js'
|
|
18
|
+
import { registerCharacterSheetTools } from './tools/character-sheet.js'
|
|
19
|
+
import { registerVisionOcrTools } from './tools/vision-ocr.js'
|
|
20
|
+
import { registerDiagramTools } from './tools/diagram.js'
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* Registers every image-gen tool on the host tools service.
|
|
@@ -37,4 +40,7 @@ export function registerAllTools(ctx, deps) {
|
|
|
37
40
|
registerUiAssetTools(ctx, deps)
|
|
38
41
|
registerStyleMatrixTools(ctx, deps)
|
|
39
42
|
registerThemePairTools(ctx, deps)
|
|
40
|
-
|
|
43
|
+
registerCharacterSheetTools(ctx, deps)
|
|
44
|
+
registerVisionOcrTools(ctx, deps)
|
|
45
|
+
registerDiagramTools(ctx, deps)
|
|
46
|
+
}
|
package/lib/security.js
CHANGED
|
@@ -64,7 +64,8 @@ export function enforceSecurePermissions(filePath) {
|
|
|
64
64
|
try {
|
|
65
65
|
chmodSync(filePath, 0o600)
|
|
66
66
|
return true
|
|
67
|
-
} catch {
|
|
67
|
+
} catch (_err) {
|
|
68
|
+
/* non-fatal: permission change error */
|
|
68
69
|
return false
|
|
69
70
|
}
|
|
70
71
|
}
|
|
@@ -143,7 +144,7 @@ export function extractHostName(hostHeader) {
|
|
|
143
144
|
try {
|
|
144
145
|
const url = new URL(`http://${trimmed}`)
|
|
145
146
|
return url.hostname.replace(/^\[|\]$/g, '')
|
|
146
|
-
} catch {
|
|
147
|
+
} catch (_err) {
|
|
147
148
|
return trimmed.replace(/^\[|\]$/g, '').split(':')[0] || ''
|
|
148
149
|
}
|
|
149
150
|
}
|
|
@@ -210,7 +211,7 @@ export function isTrustedLocalRequest(req) {
|
|
|
210
211
|
if (url.host.toLowerCase() !== host.toLowerCase()) {
|
|
211
212
|
return false
|
|
212
213
|
}
|
|
213
|
-
} catch {
|
|
214
|
+
} catch (_err) {
|
|
214
215
|
return false
|
|
215
216
|
}
|
|
216
217
|
}
|
|
@@ -226,7 +227,7 @@ export function isTrustedLocalRequest(req) {
|
|
|
226
227
|
if (!isLoopbackAddress(refererHostName) && !isPrivateLanAddress(refererHostName)) {
|
|
227
228
|
return false
|
|
228
229
|
}
|
|
229
|
-
} catch {
|
|
230
|
+
} catch (_err) {
|
|
230
231
|
return false
|
|
231
232
|
}
|
|
232
233
|
}
|
package/lib/settings-route.js
CHANGED
|
@@ -79,7 +79,7 @@ export function registerSettingsRoutes(ctx, { live, getSettingsApi, setLiveConfi
|
|
|
79
79
|
let payload
|
|
80
80
|
try {
|
|
81
81
|
payload = JSON.parse(buf.toString('utf8') || '{}')
|
|
82
|
-
} catch {
|
|
82
|
+
} catch (_err) {
|
|
83
83
|
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
84
84
|
res.end(JSON.stringify({ ok: false, error: 'invalid JSON' }))
|
|
85
85
|
return
|
package/lib/tools/anchor.js
CHANGED
|
@@ -30,8 +30,8 @@ export function registerAnchorTools(ctx, deps) {
|
|
|
30
30
|
},
|
|
31
31
|
mode: {
|
|
32
32
|
type: 'string',
|
|
33
|
-
enum: ['style', 'character'],
|
|
34
|
-
description: 'Anchor mode: "
|
|
33
|
+
enum: ['style', 'character', 'face'],
|
|
34
|
+
description: 'Anchor mode: "face" preserves facial portrait identity via FaceID/IP-Adapter; "character" preserves overall character traits; "style" preserves aesthetic palette. Default: style.',
|
|
35
35
|
},
|
|
36
36
|
strength: {
|
|
37
37
|
type: 'number',
|