@goodandready/dsh-image-gen 0.11.11 → 0.11.12

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 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
 
@@ -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 0.65
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' ? 'character' : 'style'
39
- const strength = normalizeAnchorStrength(data.strength)
40
- const label = String(data.label || (mode === 'character' ? 'Character Anchor' : 'Style Anchor')).trim()
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
- if (isRunning && !src && draftSrc) {
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
- }// 105-inpaint-canvas.js — Interactive in-chat canvas drawing overlay for inpainting (#285, #301).
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: "' + targetRef + '", mask: "' + maskDataUrl + '"'
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: '1/1',
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
- imgUrl ? react.createElement('img', {
2018
- src: imgUrl,
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: 512,
2034
- height: 512,
2100
+ width: canvasDims.width,
2101
+ height: canvasDims.height,
2035
2102
  style: {
2036
2103
  position: 'absolute',
2037
2104
  top: 0,
@@ -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
+ }
@@ -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
+ }
@@ -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: "character" preserves facial/physical identity; "style" preserves artistic rendering, lighting, and palette. Default: style.',
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',