@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.
- package/README.md +8 -0
- package/lib/anchor-helpers.js +38 -9
- package/lib/character-sheet-helpers.js +115 -0
- package/lib/client.js +80 -13
- package/lib/diagram-helpers.js +105 -0
- package/lib/register-tools.js +7 -1
- package/lib/tools/anchor.js +2 -2
- package/lib/tools/character-sheet.js +187 -0
- package/lib/tools/diagram.js +172 -0
- package/lib/tools/generation.js +72 -5
- package/lib/tools/ui-asset.js +22 -0
- package/lib/tools/vision-ocr.js +281 -0
- package/lib/vision-ocr-helpers.js +119 -0
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/lib/tools/generation.js
CHANGED
|
@@ -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: {
|
|
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: {
|
|
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
|
-
|
|
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,
|
package/lib/tools/ui-asset.js
CHANGED
|
@@ -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) {
|