@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.
@@ -0,0 +1,183 @@
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: (_args, value) => renderToolOutput(value),
93
+ },
94
+ async execute(args, execCtx) {
95
+ const cfg = live.value
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 sheetPlan = buildCharacterSheetPrompt({
100
+ prompt: args.prompt,
101
+ layout: args.layout,
102
+ style: args.style,
103
+ negativePrompt: args.negativePrompt,
104
+ })
105
+
106
+ const providers = makeProviders({
107
+ fetchImpl: fetch,
108
+ resolveKey: (id) => resolveApiKey(cfg, id),
109
+ cfg,
110
+ })
111
+
112
+ const fallbackChain = resolveFallbackChain(activeProvider, cfg)
113
+
114
+ const aspectPixels = ASPECT_RATIOS[sheetPlan.aspectRatio] || ASPECT_RATIOS['16:9']
115
+
116
+ const jobPayload = {
117
+ prompt: sheetPlan.prompt,
118
+ negativePrompt: sheetPlan.negativePrompt,
119
+ aspectRatio: sheetPlan.aspectRatio,
120
+ aspectPixels,
121
+ format: 'png',
122
+ seed,
123
+ quality: 'high',
124
+ }
125
+
126
+ const { result: genResult, provider: usedProvider } = await executeWithFallback(
127
+ fallbackChain,
128
+ providers,
129
+ jobPayload,
130
+ { signal: execCtx?.signal }
131
+ )
132
+
133
+ const timestamp = Date.now()
134
+ const safeSlug = slugify(args.prompt.slice(0, 30)) || 'character-sheet'
135
+ const filename = `character-sheet-${sheetPlan.layout}-${safeSlug}-${timestamp}.png`
136
+
137
+ let attachment = null
138
+ if (genResult.bytes) {
139
+ const outDir = path.resolve(process.cwd(), cfg.outputDir || 'output')
140
+ await mkdir(outDir, { recursive: true })
141
+ const diskPath = path.join(outDir, filename)
142
+ await writeFile(diskPath, Buffer.from(genResult.bytes))
143
+
144
+ attachment = await saveAttachmentSafe(
145
+ ctx,
146
+ Buffer.from(genResult.bytes),
147
+ 'image/png',
148
+ filename,
149
+ diskPath
150
+ )
151
+ } else if (genResult.url) {
152
+ attachment = {
153
+ url: genResult.url,
154
+ mediaType: 'image/png',
155
+ filename,
156
+ }
157
+ }
158
+
159
+ const response = {
160
+ prompt: sheetPlan.prompt,
161
+ characterDescription: args.prompt,
162
+ layout: sheetPlan.layout,
163
+ style: sheetPlan.style,
164
+ aspectRatio: sheetPlan.aspectRatio,
165
+ views: sheetPlan.views,
166
+ seed,
167
+ provider: usedProvider,
168
+ attachment,
169
+ }
170
+
171
+ if (attachment?.attachmentId) {
172
+ response.attachmentId = attachment.attachmentId
173
+ }
174
+ if (attachment?.url) {
175
+ response.url = attachment.url
176
+ }
177
+
178
+ return response
179
+ },
180
+ })
181
+ )
182
+ }, 'dsh-image-gen: tool generate_character_sheet')
183
+ }
@@ -0,0 +1,168 @@
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: (_args, value) => renderToolOutput(value),
89
+ },
90
+ async execute(args, execCtx) {
91
+ const cfg = live?.value || (typeof live === 'function' ? live() : {})
92
+ const activeProvider = args.provider || cfg.activeProvider || 'fal'
93
+ const seed = typeof args.seed === 'number' ? args.seed : Math.floor(Math.random() * 2147483647)
94
+
95
+ const plan = buildDiagramIllustrationPrompt({
96
+ diagram: args.diagram,
97
+ style: args.style,
98
+ title: args.title,
99
+ aspectRatio: args.aspect_ratio || '16:9',
100
+ })
101
+
102
+ const providers = makeProviders({
103
+ fetchImpl: fetch,
104
+ resolveKey: (id) => resolveApiKey(cfg, id),
105
+ cfg,
106
+ })
107
+ const fallbackChain = resolveFallbackChain(activeProvider, cfg)
108
+ const aspectPixels = ASPECT_RATIOS[plan.aspectRatio] || ASPECT_RATIOS['16:9']
109
+
110
+ const jobPayload = {
111
+ prompt: plan.prompt,
112
+ negativePrompt: plan.negativePrompt,
113
+ aspectRatio: plan.aspectRatio,
114
+ aspectPixels,
115
+ format: 'png',
116
+ seed,
117
+ quality: 'high',
118
+ }
119
+
120
+ const { result: genResult, provider: usedProvider } = await executeWithFallback(
121
+ fallbackChain,
122
+ providers,
123
+ jobPayload,
124
+ { signal: execCtx?.signal }
125
+ )
126
+
127
+ const outDir = path.resolve(process.cwd(), cfg.outputDir || 'output')
128
+ await mkdir(outDir, { recursive: true })
129
+ const timestamp = Date.now()
130
+ const safeSlug = slugify(args.title || 'architecture-diagram') || 'diagram'
131
+ const filename = `diagram-3d-${safeSlug}-${timestamp}.png`
132
+
133
+ let attachment = null
134
+ if (genResult.bytes) {
135
+ const diskPath = path.join(outDir, filename)
136
+ await writeFile(diskPath, Buffer.from(genResult.bytes))
137
+ attachment = await saveAttachmentSafe(
138
+ ctx,
139
+ Buffer.from(genResult.bytes),
140
+ 'image/png',
141
+ filename,
142
+ diskPath
143
+ )
144
+ } else if (genResult.url) {
145
+ attachment = {
146
+ url: genResult.url,
147
+ mediaType: 'image/png',
148
+ filename,
149
+ }
150
+ }
151
+
152
+ return {
153
+ title: args.title || 'System Architecture',
154
+ style: plan.style,
155
+ aspectRatio: plan.aspectRatio,
156
+ detectedNodes: plan.parsedNodes,
157
+ prompt: plan.prompt,
158
+ seed,
159
+ provider: usedProvider,
160
+ attachment,
161
+ url: attachment?.url,
162
+ attachmentId: attachment?.attachmentId,
163
+ }
164
+ },
165
+ })
166
+ )
167
+ }, 'dsh-image-gen: tool beautify_diagram')
168
+ }
@@ -1,22 +1,4 @@
1
1
  import { registerGenerationPackTool } from './generation-pack.js'
2
-
3
- /** Run an array of async task functions with a concurrency cap */
4
- async function asyncPool(tasks, concurrency = 3) {
5
- const results = new Array(tasks.length)
6
- let nextIdx = 0
7
- async function worker() {
8
- while (nextIdx < tasks.length) {
9
- const idx = nextIdx++
10
- results[idx] = await tasks[idx]()
11
- }
12
- }
13
- const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker())
14
- await Promise.all(workers)
15
- return results
16
- }
17
-
18
- // generation — image-gen tools (generate_image, generate_image_pack). Extracted from apply() (#216).
19
-
20
2
  import { defineTool } from '@deepseek-ai/dsh-tools'
21
3
  import path from 'node:path'
22
4
  import { mkdir, writeFile } from 'node:fs/promises'
@@ -41,7 +23,7 @@ import { calculateGenerationCost, recordSpend, assertBudgetAvailable } from '../
41
23
  import { trackAndAssertLoopGuard } from '../loop-guard.js'
42
24
  import { getCachedGeneration, setCachedGeneration } from '../generation-cache.js'
43
25
  import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
44
- import { getSessionAnchor, applyAnchorPromptHints } from '../anchor-helpers.js'
26
+ import { getSessionAnchor, applyAnchorPromptHints, buildFaceConsistencyPrompt } from '../anchor-helpers.js'
45
27
  import { enhancePrompt } from '../prompt-enhancer.js'
46
28
  import {
47
29
  readHistory,
@@ -53,6 +35,22 @@ import {
53
35
  cachedResult,
54
36
  } from '../history.js'
55
37
 
38
+ // generation — image-gen tools (generate_image, generate_image_pack). Extracted from apply() (#216).
39
+
40
+ /** Run an array of async task functions with a concurrency cap */
41
+ async function asyncPool(tasks, concurrency = 3) {
42
+ const results = new Array(tasks.length)
43
+ let nextIdx = 0
44
+ async function worker() {
45
+ while (nextIdx < tasks.length) {
46
+ const idx = nextIdx++
47
+ results[idx] = await tasks[idx]()
48
+ }
49
+ }
50
+ const workers = Array.from({ length: Math.min(concurrency, tasks.length) }, () => worker())
51
+ await Promise.all(workers)
52
+ return results
53
+ }
56
54
 
57
55
  export function registerGenerationTools(ctx, deps) {
58
56
  const {
@@ -172,6 +170,14 @@ export function registerGenerationTools(ctx, deps) {
172
170
  type: 'number',
173
171
  description: 'Optional strength of reference anchor influence (0.1 to 1.0).',
174
172
  },
173
+ face_reference: {
174
+ type: 'string',
175
+ description: 'Portrait reference image (URL, local path, or attachment id) for facial identity preservation via FaceID / IP-Adapter (#183).',
176
+ },
177
+ face_strength: {
178
+ type: 'number',
179
+ description: 'Facial identity similarity weight between 0.1 and 1.0 (default: 0.75).',
180
+ },
175
181
  },
176
182
  output: {
177
183
  schema: {
@@ -336,7 +342,7 @@ export function registerGenerationTools(ctx, deps) {
336
342
  // Subscriptions service is optional: without it, subscription providers
337
343
  // gracefully decline while other providers continue functioning.
338
344
  let subscriptionImages
339
- try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch { subscriptionImages = undefined }
345
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
340
346
 
341
347
  const source = args.source_image ? await resolveSource(ctx, exec, args.source_image) : undefined
342
348
  const mask = args.mask ? await resolveSource(ctx, exec, args.mask) : undefined
@@ -345,7 +351,25 @@ export function registerGenerationTools(ctx, deps) {
345
351
  const styleInfo = resolveStylePreset(effectiveStyle, args.negative_prompt, args.guidance_scale)
346
352
  let effectivePrompt = styleInfo.promptSuffix ? `${enhanced.prompt}, ${styleInfo.promptSuffix}` : enhanced.prompt
347
353
  const activeAnchor = getSessionAnchor(sessionId)
348
- if (args.reference_image) {
354
+ let resolvedFaceRef
355
+ const faceRefInput = args.face_reference || (activeAnchor && (activeAnchor.mode === 'face' || activeAnchor.mode === 'character') ? activeAnchor.image : undefined)
356
+ if (faceRefInput) {
357
+ try {
358
+ resolvedFaceRef = await resolveSource(ctx, exec, faceRefInput)
359
+ } catch (_err) {
360
+ /* non-fatal: face_reference resolve fallback */
361
+ resolvedFaceRef = { url: faceRefInput }
362
+ }
363
+ const facePlan = buildFaceConsistencyPrompt({
364
+ prompt: effectivePrompt,
365
+ faceStrength: args.face_strength || activeAnchor?.strength,
366
+ characterName: activeAnchor?.label,
367
+ })
368
+ effectivePrompt = facePlan.prompt
369
+ if (facePlan.negativePrompt) {
370
+ effectiveNegative = effectiveNegative ? `${effectiveNegative}, ${facePlan.negativePrompt}` : facePlan.negativePrompt
371
+ }
372
+ } else if (args.reference_image) {
349
373
  effectivePrompt = `${effectivePrompt}, visual reference anchor`
350
374
  } else if (activeAnchor) {
351
375
  effectivePrompt = applyAnchorPromptHints(activeAnchor, effectivePrompt)
@@ -373,7 +397,7 @@ export function registerGenerationTools(ctx, deps) {
373
397
  assertBudgetAvailable(estimatedCost, cfg.dailyBudgetUsd)
374
398
  const providers = makeProviders(
375
399
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },
376
- { 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 },
400
+ { 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 },
377
401
  )
378
402
  // Subscription providers return {ok:false, reason} instead of throwing:
379
403
  // rejection reaches the model as text. Without this guard,
@@ -126,7 +126,7 @@ export function registerStyleMatrixTools(ctx, deps) {
126
126
 
127
127
  const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
128
128
  let subscriptionImages
129
- try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch { subscriptionImages = undefined }
129
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
130
130
 
131
131
  const chain = resolveFallbackChain(provider, cfg.fallbackProviders, PROVIDER_KEYS)
132
132
  const quadrantIds = ['A', 'B', 'C', 'D']
@@ -145,7 +145,7 @@ export function registerUiAssetTools(ctx, deps) {
145
145
 
146
146
  const provider = PROVIDER_KEYS.includes(cfg.provider) ? cfg.provider : 'fal'
147
147
  let subscriptionImages
148
- try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch { subscriptionImages = undefined }
148
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (_err) { subscriptionImages = undefined }
149
149
 
150
150
  const providers = makeProviders(
151
151
  { fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg, subscriptionImages },