@goodandready/dsh-image-gen 0.11.10 → 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.
@@ -0,0 +1,187 @@
1
+ // character-sheet.js — generate_character_sheet tool (#181).
2
+ // Generates multi-angle character model sheets and turnarounds with visual consistency.
3
+
4
+ import { defineTool } from '@deepseek-ai/dsh-tools'
5
+ import path from 'node:path'
6
+ import { mkdir, writeFile } from 'node:fs/promises'
7
+ import {
8
+ ASPECT_RATIOS,
9
+ buildSidecar,
10
+ makeProviders,
11
+ toLosslessJson,
12
+ saveAttachmentSafe,
13
+ } from '../providers.js'
14
+ import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
15
+ import { renderToolOutput } from '../attachment-helper.js'
16
+ import {
17
+ CHARACTER_SHEET_LAYOUTS,
18
+ CHARACTER_SHEET_STYLES,
19
+ buildCharacterSheetPrompt,
20
+ } from '../character-sheet-helpers.js'
21
+
22
+ export function registerCharacterSheetTools(ctx, deps) {
23
+ const {
24
+ live,
25
+ slugify,
26
+ resolveApiKey,
27
+ } = deps
28
+
29
+ ctx.effect(() => {
30
+ ctx.tools.register(
31
+ defineTool({
32
+ name: 'generate_character_sheet',
33
+ description:
34
+ 'Generate a multi-angle character model sheet or turnaround reference with high visual identity consistency. '
35
+ + 'Supports horizontal turnarounds (front, profile, 3/4, back), 1x3 triptychs, 2x2 grids, and facial emotion sheets across concept, anime, 3D, and comic styles.',
36
+ parameters: {
37
+ prompt: {
38
+ type: 'string',
39
+ description: 'Core character description (e.g. "female cyborg detective with neon violet hair, yellow trenchcoat, cybernetic arm").',
40
+ },
41
+ layout: {
42
+ type: 'string',
43
+ enum: ['turnaround', '1x3', '2x2', 'emotions'],
44
+ description: 'Layout arrangement: "turnaround" (4 sequential horizontal views), "1x3" (front, side, back), "2x2" (quad angles), or "emotions" (4 facial expressions). Default: turnaround.',
45
+ },
46
+ style: {
47
+ type: 'string',
48
+ enum: ['concept_art', 'anime', '3d_animation', 'pixel_art', 'realistic', 'comic'],
49
+ description: 'Artistic rendering style. Default: concept_art.',
50
+ },
51
+ negativePrompt: {
52
+ type: 'string',
53
+ description: 'Negative prompt directives to suppress unwanted elements.',
54
+ },
55
+ seed: {
56
+ type: 'integer',
57
+ description: 'RNG seed for reproducible character generation across sessions.',
58
+ },
59
+ provider: {
60
+ type: 'string',
61
+ description: 'Optional provider override (e.g. fal, replicate, local). Defaults to active provider.',
62
+ },
63
+ },
64
+ output: {
65
+ schema: {
66
+ type: 'object',
67
+ additionalProperties: true,
68
+ properties: {
69
+ prompt: { type: 'string' },
70
+ layout: { type: 'string' },
71
+ style: { type: 'string' },
72
+ aspectRatio: { type: 'string' },
73
+ views: {
74
+ type: 'array',
75
+ items: { type: 'string' },
76
+ },
77
+ seed: { type: 'integer' },
78
+ provider: { type: 'string' },
79
+ attachment: {
80
+ type: 'object',
81
+ additionalProperties: true,
82
+ properties: {
83
+ attachmentId: { type: 'string' },
84
+ url: { type: 'string' },
85
+ mediaType: { type: 'string' },
86
+ filename: { type: 'string' },
87
+ byteSize: { type: 'integer' },
88
+ },
89
+ },
90
+ },
91
+ },
92
+ render: (result, renderCtx) =>
93
+ renderToolOutput(result, renderCtx, {
94
+ title: (res) => `Character Sheet (${res.layout || 'turnaround'}) — ${res.style || 'concept_art'}`,
95
+ subtitle: (res) => (res.views ? `Views: ${res.views.join(' | ')}` : ''),
96
+ }),
97
+ },
98
+ async execute(args, execCtx) {
99
+ const cfg = live.value
100
+ const activeProvider = args.provider || cfg.activeProvider || 'fal'
101
+ const seed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 2147483647)
102
+
103
+ const sheetPlan = buildCharacterSheetPrompt({
104
+ prompt: args.prompt,
105
+ layout: args.layout,
106
+ style: args.style,
107
+ negativePrompt: args.negativePrompt,
108
+ })
109
+
110
+ const providers = makeProviders({
111
+ fetchImpl: fetch,
112
+ resolveKey: (id) => resolveApiKey(cfg, id),
113
+ cfg,
114
+ })
115
+
116
+ const fallbackChain = resolveFallbackChain(activeProvider, cfg)
117
+
118
+ const aspectPixels = ASPECT_RATIOS[sheetPlan.aspectRatio] || ASPECT_RATIOS['16:9']
119
+
120
+ const jobPayload = {
121
+ prompt: sheetPlan.prompt,
122
+ negativePrompt: sheetPlan.negativePrompt,
123
+ aspectRatio: sheetPlan.aspectRatio,
124
+ aspectPixels,
125
+ format: 'png',
126
+ seed,
127
+ quality: 'high',
128
+ }
129
+
130
+ const { result: genResult, provider: usedProvider } = await executeWithFallback(
131
+ fallbackChain,
132
+ providers,
133
+ jobPayload,
134
+ { signal: execCtx?.signal }
135
+ )
136
+
137
+ const timestamp = Date.now()
138
+ const safeSlug = slugify(args.prompt.slice(0, 30)) || 'character-sheet'
139
+ const filename = `character-sheet-${sheetPlan.layout}-${safeSlug}-${timestamp}.png`
140
+
141
+ let attachment = null
142
+ if (genResult.bytes) {
143
+ const outDir = path.resolve(process.cwd(), cfg.outputDir || 'output')
144
+ await mkdir(outDir, { recursive: true })
145
+ const diskPath = path.join(outDir, filename)
146
+ await writeFile(diskPath, Buffer.from(genResult.bytes))
147
+
148
+ attachment = await saveAttachmentSafe(
149
+ ctx,
150
+ Buffer.from(genResult.bytes),
151
+ 'image/png',
152
+ filename,
153
+ diskPath
154
+ )
155
+ } else if (genResult.url) {
156
+ attachment = {
157
+ url: genResult.url,
158
+ mediaType: 'image/png',
159
+ filename,
160
+ }
161
+ }
162
+
163
+ const response = {
164
+ prompt: sheetPlan.prompt,
165
+ characterDescription: args.prompt,
166
+ layout: sheetPlan.layout,
167
+ style: sheetPlan.style,
168
+ aspectRatio: sheetPlan.aspectRatio,
169
+ views: sheetPlan.views,
170
+ seed,
171
+ provider: usedProvider,
172
+ attachment,
173
+ }
174
+
175
+ if (attachment?.attachmentId) {
176
+ response.attachmentId = attachment.attachmentId
177
+ }
178
+ if (attachment?.url) {
179
+ response.url = attachment.url
180
+ }
181
+
182
+ return response
183
+ },
184
+ })
185
+ )
186
+ }, 'dsh-image-gen: tool generate_character_sheet')
187
+ }
@@ -0,0 +1,172 @@
1
+ // diagram.js — beautify_diagram tool (#185).
2
+ // Transforms raw Mermaid / Graphviz diagrams into high-end 3D isometric architectural illustrations.
3
+
4
+ import { defineTool } from '@deepseek-ai/dsh-tools'
5
+ import path from 'node:path'
6
+ import { mkdir, writeFile } from 'node:fs/promises'
7
+ import {
8
+ ASPECT_RATIOS,
9
+ makeProviders,
10
+ saveAttachmentSafe,
11
+ } from '../providers.js'
12
+ import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
13
+ import { renderToolOutput } from '../attachment-helper.js'
14
+ import {
15
+ DIAGRAM_3D_STYLES,
16
+ buildDiagramIllustrationPrompt,
17
+ } from '../diagram-helpers.js'
18
+
19
+ export function registerDiagramTools(ctx, deps) {
20
+ const {
21
+ live,
22
+ slugify,
23
+ resolveApiKey,
24
+ } = deps
25
+
26
+ ctx.effect(() => {
27
+ ctx.tools.register(
28
+ defineTool({
29
+ name: 'beautify_diagram',
30
+ description:
31
+ 'Transform raw Mermaid or Graphviz diagrams into stunning 3D isometric architectural illustrations '
32
+ + 'with glowing data pipes, glassmorphism cards, and pristine technical lighting for documentation and hero blocks.',
33
+ parameters: {
34
+ diagram: {
35
+ type: 'string',
36
+ description: 'Raw Mermaid diagram source code, Graphviz DOT text, or image path/attachment.',
37
+ },
38
+ style: {
39
+ type: 'string',
40
+ enum: ['isometric_3d', 'cyber_blueprint', 'clay_minimal', 'glossy_dark'],
41
+ description: '3D rendering aesthetic style. Default: isometric_3d.',
42
+ },
43
+ title: {
44
+ type: 'string',
45
+ description: 'Optional architecture title or headline to feature in the composition.',
46
+ },
47
+ aspect_ratio: {
48
+ type: 'string',
49
+ enum: ['16:9', '1:1', '4:3', '21:9'],
50
+ description: 'Aspect ratio for documentation header or hero banner (default: 16:9).',
51
+ },
52
+ seed: {
53
+ type: 'integer',
54
+ description: 'Optional seed for reproducible diagram generation.',
55
+ },
56
+ provider: {
57
+ type: 'string',
58
+ description: 'Optional provider override. Defaults to active provider.',
59
+ },
60
+ },
61
+ output: {
62
+ schema: {
63
+ type: 'object',
64
+ additionalProperties: true,
65
+ properties: {
66
+ title: { type: 'string' },
67
+ style: { type: 'string' },
68
+ aspectRatio: { type: 'string' },
69
+ detectedNodes: {
70
+ type: 'array',
71
+ items: { type: 'string' },
72
+ },
73
+ prompt: { type: 'string' },
74
+ seed: { type: 'integer' },
75
+ provider: { type: 'string' },
76
+ attachment: {
77
+ type: 'object',
78
+ additionalProperties: true,
79
+ properties: {
80
+ attachmentId: { type: 'string' },
81
+ url: { type: 'string' },
82
+ mediaType: { type: 'string' },
83
+ filename: { type: 'string' },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ render: (result, renderCtx) =>
89
+ renderToolOutput(result, renderCtx, {
90
+ title: (res) => `3D Architecture Diagram — ${res.title || res.style || 'Isometric'}`,
91
+ subtitle: (res) => (res.detectedNodes?.length ? `Nodes: ${res.detectedNodes.slice(0, 4).join(', ')}` : ''),
92
+ }),
93
+ },
94
+ async execute(args, execCtx) {
95
+ const cfg = live?.value || (typeof live === 'function' ? live() : {})
96
+ const activeProvider = args.provider || cfg.activeProvider || 'fal'
97
+ const seed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 2147483647)
98
+
99
+ const plan = buildDiagramIllustrationPrompt({
100
+ diagram: args.diagram,
101
+ style: args.style,
102
+ title: args.title,
103
+ aspectRatio: args.aspect_ratio || '16:9',
104
+ })
105
+
106
+ const providers = makeProviders({
107
+ fetchImpl: fetch,
108
+ resolveKey: (id) => resolveApiKey(cfg, id),
109
+ cfg,
110
+ })
111
+ const fallbackChain = resolveFallbackChain(activeProvider, cfg)
112
+ const aspectPixels = ASPECT_RATIOS[plan.aspectRatio] || ASPECT_RATIOS['16:9']
113
+
114
+ const jobPayload = {
115
+ prompt: plan.prompt,
116
+ negativePrompt: plan.negativePrompt,
117
+ aspectRatio: plan.aspectRatio,
118
+ aspectPixels,
119
+ format: 'png',
120
+ seed,
121
+ quality: 'high',
122
+ }
123
+
124
+ const { result: genResult, provider: usedProvider } = await executeWithFallback(
125
+ fallbackChain,
126
+ providers,
127
+ jobPayload,
128
+ { signal: execCtx?.signal }
129
+ )
130
+
131
+ const outDir = path.resolve(process.cwd(), cfg.outputDir || 'output')
132
+ await mkdir(outDir, { recursive: true })
133
+ const timestamp = Date.now()
134
+ const safeSlug = slugify(args.title || 'architecture-diagram') || 'diagram'
135
+ const filename = `diagram-3d-${safeSlug}-${timestamp}.png`
136
+
137
+ let attachment = null
138
+ if (genResult.bytes) {
139
+ const diskPath = path.join(outDir, filename)
140
+ await writeFile(diskPath, Buffer.from(genResult.bytes))
141
+ attachment = await saveAttachmentSafe(
142
+ ctx,
143
+ Buffer.from(genResult.bytes),
144
+ 'image/png',
145
+ filename,
146
+ diskPath
147
+ )
148
+ } else if (genResult.url) {
149
+ attachment = {
150
+ url: genResult.url,
151
+ mediaType: 'image/png',
152
+ filename,
153
+ }
154
+ }
155
+
156
+ return {
157
+ title: args.title || 'System Architecture',
158
+ style: plan.style,
159
+ aspectRatio: plan.aspectRatio,
160
+ detectedNodes: plan.parsedNodes,
161
+ prompt: plan.prompt,
162
+ seed,
163
+ provider: usedProvider,
164
+ attachment,
165
+ url: attachment?.url,
166
+ attachmentId: attachment?.attachmentId,
167
+ }
168
+ },
169
+ })
170
+ )
171
+ }, 'dsh-image-gen: tool beautify_diagram')
172
+ }
@@ -41,7 +41,7 @@ import { calculateGenerationCost, recordSpend, assertBudgetAvailable } from '../
41
41
  import { trackAndAssertLoopGuard } from '../loop-guard.js'
42
42
  import { getCachedGeneration, setCachedGeneration } from '../generation-cache.js'
43
43
  import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
44
- import { getSessionAnchor, applyAnchorPromptHints } from '../anchor-helpers.js'
44
+ import { getSessionAnchor, applyAnchorPromptHints, buildFaceConsistencyPrompt } from '../anchor-helpers.js'
45
45
  import { enhancePrompt } from '../prompt-enhancer.js'
46
46
  import {
47
47
  readHistory,
@@ -172,6 +172,14 @@ export function registerGenerationTools(ctx, deps) {
172
172
  type: 'number',
173
173
  description: 'Optional strength of reference anchor influence (0.1 to 1.0).',
174
174
  },
175
+ face_reference: {
176
+ type: 'string',
177
+ description: 'Portrait reference image (URL, local path, or attachment id) for facial identity preservation via FaceID / IP-Adapter (#183).',
178
+ },
179
+ face_strength: {
180
+ type: 'number',
181
+ description: 'Facial identity similarity weight between 0.1 and 1.0 (default: 0.75).',
182
+ },
175
183
  },
176
184
  output: {
177
185
  schema: {
@@ -182,7 +190,28 @@ export function registerGenerationTools(ctx, deps) {
182
190
  channel: { type: 'string' },
183
191
  provider: { type: 'string' },
184
192
  model: { type: 'string' },
185
- _fallback: { type: 'string' },
193
+ _fallback: {
194
+ type: 'object',
195
+ additionalProperties: true,
196
+ properties: {
197
+ triggered: { type: 'boolean' },
198
+ primaryProvider: { type: 'string' },
199
+ providerUsed: { type: 'string' },
200
+ attempts: {
201
+ type: 'array',
202
+ items: {
203
+ type: 'object',
204
+ additionalProperties: true,
205
+ properties: {
206
+ provider: { type: 'string' },
207
+ durationMs: { type: 'number' },
208
+ success: { type: 'boolean' },
209
+ error: { type: 'string' },
210
+ },
211
+ },
212
+ },
213
+ },
214
+ },
186
215
  path: { type: 'string' },
187
216
  url: { type: 'string' },
188
217
  width: { type: 'integer' },
@@ -231,7 +260,28 @@ export function registerGenerationTools(ctx, deps) {
231
260
  cost: { type: 'number' },
232
261
  fromCache: { type: 'boolean' },
233
262
  qualityReport: { type: 'object', additionalProperties: true },
234
- _fallback: { type: 'string' },
263
+ _fallback: {
264
+ type: 'object',
265
+ additionalProperties: true,
266
+ properties: {
267
+ triggered: { type: 'boolean' },
268
+ primaryProvider: { type: 'string' },
269
+ providerUsed: { type: 'string' },
270
+ attempts: {
271
+ type: 'array',
272
+ items: {
273
+ type: 'object',
274
+ additionalProperties: true,
275
+ properties: {
276
+ provider: { type: 'string' },
277
+ durationMs: { type: 'number' },
278
+ success: { type: 'boolean' },
279
+ error: { type: 'string' },
280
+ },
281
+ },
282
+ },
283
+ },
284
+ },
235
285
  provider: { type: 'string' },
236
286
  model: { type: 'string' },
237
287
  attachment: {
@@ -303,7 +353,24 @@ export function registerGenerationTools(ctx, deps) {
303
353
  const styleInfo = resolveStylePreset(effectiveStyle, args.negative_prompt, args.guidance_scale)
304
354
  let effectivePrompt = styleInfo.promptSuffix ? `${enhanced.prompt}, ${styleInfo.promptSuffix}` : enhanced.prompt
305
355
  const activeAnchor = getSessionAnchor(sessionId)
306
- if (args.reference_image) {
356
+ let resolvedFaceRef
357
+ const faceRefInput = args.face_reference || (activeAnchor && (activeAnchor.mode === 'face' || activeAnchor.mode === 'character') ? activeAnchor.image : undefined)
358
+ if (faceRefInput) {
359
+ try {
360
+ resolvedFaceRef = await resolveSource(ctx, exec, faceRefInput)
361
+ } catch {
362
+ resolvedFaceRef = { url: faceRefInput }
363
+ }
364
+ const facePlan = buildFaceConsistencyPrompt({
365
+ prompt: effectivePrompt,
366
+ faceStrength: args.face_strength || activeAnchor?.strength,
367
+ characterName: activeAnchor?.label,
368
+ })
369
+ effectivePrompt = facePlan.prompt
370
+ if (facePlan.negativePrompt) {
371
+ effectiveNegative = effectiveNegative ? `${effectiveNegative}, ${facePlan.negativePrompt}` : facePlan.negativePrompt
372
+ }
373
+ } else if (args.reference_image) {
307
374
  effectivePrompt = `${effectivePrompt}, visual reference anchor`
308
375
  } else if (activeAnchor) {
309
376
  effectivePrompt = applyAnchorPromptHints(activeAnchor, effectivePrompt)
@@ -331,7 +398,7 @@ export function registerGenerationTools(ctx, deps) {
331
398
  assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
332
399
  const providers = makeProviders(
333
400
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
334
- { prompt: effectivePrompt, size, format, seed: args.seed, signal: exec.signal, negativePrompt: effectiveNegative, guidanceScale: effectiveGuidance, source, mask, strength: args.strength, quality: args.quality, style: args.style, aspectPixels, aspectRatio: args.aspect_ratio },
401
+ { prompt: effectivePrompt, size, format, seed: args.seed, signal: exec.signal, negativePrompt: effectiveNegative, guidanceScale: effectiveGuidance, source, mask, strength: args.strength, quality: args.quality, style: args.style, aspectPixels, aspectRatio: args.aspect_ratio, faceReference: resolvedFaceRef },
335
402
  )
336
403
  // Subscription providers return {ok:false, reason} instead of throwing:
337
404
  // rejection reaches the model as text. Without this guard,
@@ -91,6 +91,28 @@ export function registerUiAssetTools(ctx, deps) {
91
91
  type: 'object',
92
92
  additionalProperties: true,
93
93
  },
94
+ _fallback: {
95
+ type: 'object',
96
+ additionalProperties: true,
97
+ properties: {
98
+ triggered: { type: 'boolean' },
99
+ primaryProvider: { type: 'string' },
100
+ providerUsed: { type: 'string' },
101
+ attempts: {
102
+ type: 'array',
103
+ items: {
104
+ type: 'object',
105
+ additionalProperties: true,
106
+ properties: {
107
+ provider: { type: 'string' },
108
+ durationMs: { type: 'number' },
109
+ success: { type: 'boolean' },
110
+ error: { type: 'string' },
111
+ },
112
+ },
113
+ },
114
+ },
115
+ },
94
116
  },
95
117
  },
96
118
  render(args, value) {