@goodandready/dsh-image-gen 0.10.23 → 0.10.25
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 +15 -0
- package/README.ru.md +15 -0
- package/README.zh.md +15 -0
- package/lib/client.js +161 -33
- package/lib/cost-meter.js +4 -2
- package/lib/generation-cache.js +6 -3
- package/lib/index.js +3 -1
- package/lib/loop-guard.js +2 -0
- package/lib/pattern-helpers.js +87 -0
- package/lib/prompt-polisher.js +0 -7
- package/lib/provider-utils.js +302 -0
- package/lib/providers/backends/custom.js +93 -0
- package/lib/providers/backends/fal.js +67 -0
- package/lib/providers/backends/gemini.js +37 -0
- package/lib/providers/backends/local.js +136 -0
- package/lib/providers/backends/replicate.js +83 -0
- package/lib/providers/backends/seedream.js +49 -0
- package/lib/providers/backends/subscription.js +58 -0
- package/lib/providers/shared-helpers.js +164 -0
- package/lib/providers.js +95 -847
- package/lib/register-tools.js +11 -1
- package/lib/responsive-helpers.js +94 -0
- package/lib/spritesheet-helpers.js +65 -0
- package/lib/tools/editing.js +2 -0
- package/lib/tools/pattern.js +125 -0
- package/lib/tools/{processing.js → processing-advanced.js} +11 -270
- package/lib/tools/processing-basic.js +327 -0
- package/lib/tools/responsive.js +267 -0
- package/lib/tools/sketch.js +167 -0
- package/lib/tools/spritesheet.js +151 -0
- package/package.json +3 -2
package/lib/register-tools.js
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
// Each tool lives in lib/tools/* and is registered inside a labeled ctx.effect.
|
|
3
3
|
|
|
4
4
|
import { registerGenerationTools } from './tools/generation.js'
|
|
5
|
-
import { registerProcessingTools } from './tools/processing.js'
|
|
5
|
+
import { registerProcessingTools } from './tools/processing-basic.js'
|
|
6
|
+
import { registerProcessingAdvancedTools } from './tools/processing-advanced.js'
|
|
6
7
|
import { registerEditingTools } from './tools/editing.js'
|
|
7
8
|
import { registerInspectTools } from './tools/inspect.js'
|
|
8
9
|
import { registerFrontendTools } from './tools/frontend.js'
|
|
10
|
+
import { registerSketchTools } from './tools/sketch.js'
|
|
11
|
+
import { registerSpritesheetTools } from './tools/spritesheet.js'
|
|
12
|
+
import { registerPatternTools } from './tools/pattern.js'
|
|
13
|
+
import { registerResponsiveTools } from './tools/responsive.js'
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* Registers every image-gen tool on the host tools service.
|
|
@@ -16,7 +21,12 @@ import { registerFrontendTools } from './tools/frontend.js'
|
|
|
16
21
|
export function registerAllTools(ctx, deps) {
|
|
17
22
|
registerGenerationTools(ctx, deps)
|
|
18
23
|
registerProcessingTools(ctx, deps)
|
|
24
|
+
registerProcessingAdvancedTools(ctx, deps)
|
|
19
25
|
registerEditingTools(ctx, deps)
|
|
20
26
|
registerInspectTools(ctx, deps)
|
|
21
27
|
registerFrontendTools(ctx, deps)
|
|
28
|
+
registerSketchTools(ctx, deps)
|
|
29
|
+
registerSpritesheetTools(ctx, deps)
|
|
30
|
+
registerPatternTools(ctx, deps)
|
|
31
|
+
registerResponsiveTools(ctx, deps)
|
|
22
32
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Pure helpers for generate_responsive_mockups (#173) — no cordis imports.
|
|
2
|
+
|
|
3
|
+
export const DEVICE_ORDER = ['mobile', 'tablet', 'desktop']
|
|
4
|
+
|
|
5
|
+
export const DEVICE_PRESETS = {
|
|
6
|
+
mobile: {
|
|
7
|
+
id: 'mobile',
|
|
8
|
+
label: 'Mobile',
|
|
9
|
+
aspect: '9:16',
|
|
10
|
+
size: 'portrait_16_9',
|
|
11
|
+
fileStem: 'mockup-mobile',
|
|
12
|
+
viewport: 'iPhone-class mobile viewport 375×812, portrait 9:16',
|
|
13
|
+
layoutHint:
|
|
14
|
+
'Single-column mobile UI mockup: compact header, stacked content, thumb-reachable bottom actions. '
|
|
15
|
+
+ 'Preserve the same product, brand colors, typography family and visual language as the desktop and tablet versions.',
|
|
16
|
+
},
|
|
17
|
+
tablet: {
|
|
18
|
+
id: 'tablet',
|
|
19
|
+
label: 'Tablet',
|
|
20
|
+
aspect: '3:4',
|
|
21
|
+
size: 'portrait_4_3',
|
|
22
|
+
fileStem: 'mockup-tablet',
|
|
23
|
+
viewport: 'iPad-class tablet viewport 768×1024, portrait 3:4',
|
|
24
|
+
layoutHint:
|
|
25
|
+
'Tablet UI mockup with two-column or wide single-column layout, roomier spacing than mobile, '
|
|
26
|
+
+ 'same product screens, brand colors, and component language as mobile and desktop versions.',
|
|
27
|
+
},
|
|
28
|
+
desktop: {
|
|
29
|
+
id: 'desktop',
|
|
30
|
+
label: 'Desktop',
|
|
31
|
+
aspect: '16:9',
|
|
32
|
+
size: 'landscape_16_9',
|
|
33
|
+
fileStem: 'mockup-desktop',
|
|
34
|
+
viewport: 'desktop viewport 1440×900 / 1920×1080, landscape 16:10–16:9',
|
|
35
|
+
layoutHint:
|
|
36
|
+
'Desktop UI mockup with multi-column layout, full navigation chrome and wide content area. '
|
|
37
|
+
+ 'Same product, brand palette and component language as mobile and tablet versions.',
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function normalizeDevices(input) {
|
|
42
|
+
if (input == null) return [...DEVICE_ORDER]
|
|
43
|
+
const list = Array.isArray(input) ? input : [input]
|
|
44
|
+
const cleaned = list
|
|
45
|
+
.map((d) => String(d || '').trim().toLowerCase())
|
|
46
|
+
.filter((d) => DEVICE_ORDER.includes(d))
|
|
47
|
+
const unique = []
|
|
48
|
+
for (const d of cleaned) {
|
|
49
|
+
if (!unique.includes(d)) unique.push(d)
|
|
50
|
+
}
|
|
51
|
+
return unique.length ? unique : [...DEVICE_ORDER]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveDevicePreset(device) {
|
|
55
|
+
return DEVICE_PRESETS[String(device || '').toLowerCase()] || DEVICE_PRESETS.mobile
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Build a shared semantic base prompt so the three viewports stay visually coherent.
|
|
60
|
+
* Device-specific lines only restate composition constraints, not the product idea.
|
|
61
|
+
*/
|
|
62
|
+
export function buildResponsivePrompt(basePrompt, { device, stylePreset, paletteColors } = {}) {
|
|
63
|
+
const preset = resolveDevicePreset(device)
|
|
64
|
+
const core = String(basePrompt || '').trim()
|
|
65
|
+
const style = stylePreset ? ` Style preset: ${stylePreset}.` : ''
|
|
66
|
+
const palette = Array.isArray(paletteColors) && paletteColors.length
|
|
67
|
+
? ` Dominant brand colors must remain consistent across all responsive mockups: ${paletteColors.join(', ')}.`
|
|
68
|
+
: ''
|
|
69
|
+
return `Responsive UI product mockup of: ${core}.${style}${palette} `
|
|
70
|
+
+ `Render as a polished ${preset.viewport}. ${preset.layoutHint} `
|
|
71
|
+
+ 'Keep the same information architecture, iconography language, spacing rhythm and color palette across mobile, tablet and desktop variants. '
|
|
72
|
+
+ 'High-fidelity interface design, clean UI, realistic device framing only if needed, no lorem-ipsum walls of unreadable text, no watermark.'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function mockupFileName(device, { outputName } = {}) {
|
|
76
|
+
const preset = resolveDevicePreset(device)
|
|
77
|
+
const stem = String(outputName || '').trim()
|
|
78
|
+
? String(outputName).trim().replace(/\.(png|jpe?g|webp)$/i, '')
|
|
79
|
+
: preset.fileStem
|
|
80
|
+
// Issue requires mockup-mobile.png style names when default stem is used.
|
|
81
|
+
if (!outputName) return `${preset.fileStem}.png`
|
|
82
|
+
return `${stem}-${preset.id}.png`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function deviceMeta(device) {
|
|
86
|
+
const preset = resolveDevicePreset(device)
|
|
87
|
+
return {
|
|
88
|
+
device: preset.id,
|
|
89
|
+
label: preset.label,
|
|
90
|
+
aspect: preset.aspect,
|
|
91
|
+
size: preset.size,
|
|
92
|
+
viewport: preset.viewport,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Pure helpers for generate_spritesheet (#177) — no cordis/dsh-tools imports.
|
|
2
|
+
|
|
3
|
+
export const ANIMATION_PRESETS = {
|
|
4
|
+
idle: ['neutral standing pose', 'subtle breathing chest rise', 'slight weight shift', 'return to neutral'],
|
|
5
|
+
walk: ['contact pose left foot forward', 'down pose', 'passing pose', 'up pose', 'contact pose right foot forward', 'down pose', 'passing pose', 'up pose'],
|
|
6
|
+
attack: ['wind-up anticipation', 'strike extension', 'impact frame', 'recoil follow-through'],
|
|
7
|
+
pulse: ['scale 1.0 rest', 'scale 1.08 glow peak', 'scale 1.0 settle', 'scale 0.96 micro-bounce'],
|
|
8
|
+
hover: ['lift 0px', 'lift -6px', 'lift 0px', 'lift 2px press'],
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function clampFrames(n, animation = 'idle') {
|
|
12
|
+
const v = Number(n)
|
|
13
|
+
const anim = String(animation || 'idle').toLowerCase()
|
|
14
|
+
// walk preset has a natural 8-phase cycle; 12 would stutter at loop point
|
|
15
|
+
if (anim === 'walk') return v === 4 ? 4 : 8
|
|
16
|
+
if (v === 4 || v === 8 || v === 12) return v
|
|
17
|
+
return 8
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildSpritesheetCss({ frameCount, frameW, frameH, durationSec, name }) {
|
|
21
|
+
const anim = name || 'play'
|
|
22
|
+
const sheetW = frameW * frameCount
|
|
23
|
+
return `/* dsh-image-gen spritesheet */
|
|
24
|
+
.${anim} {
|
|
25
|
+
width: ${frameW}px;
|
|
26
|
+
height: ${frameH}px;
|
|
27
|
+
background-image: url('./${anim}-sheet.svg');
|
|
28
|
+
background-repeat: no-repeat;
|
|
29
|
+
background-size: ${sheetW}px ${frameH}px;
|
|
30
|
+
animation: ${anim} ${durationSec}s steps(${frameCount}) infinite;
|
|
31
|
+
}
|
|
32
|
+
@keyframes ${anim} {
|
|
33
|
+
from { background-position: 0 0; }
|
|
34
|
+
to { background-position: -${sheetW}px 0; }
|
|
35
|
+
}
|
|
36
|
+
`
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildSpritesheetSvg(frames, { frameW, frameH }) {
|
|
40
|
+
const count = frames.length
|
|
41
|
+
const totalW = frameW * count
|
|
42
|
+
const totalH = frameH
|
|
43
|
+
let images = ''
|
|
44
|
+
frames.forEach((src, idx) => {
|
|
45
|
+
const b64 = Buffer.isBuffer(src.bytes) ? src.bytes.toString('base64') : Buffer.from(src.bytes).toString('base64')
|
|
46
|
+
const mime = src.mediaType || 'image/png'
|
|
47
|
+
const x = idx * frameW
|
|
48
|
+
images += `<image href="data:${mime};base64,${b64}" x="${x}" y="0" width="${frameW}" height="${frameH}" preserveAspectRatio="xMidYMid slice"/>`
|
|
49
|
+
})
|
|
50
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
51
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${totalH}" viewBox="0 0 ${totalW} ${totalH}">
|
|
52
|
+
${images}
|
|
53
|
+
</svg>`
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function buildFramePrompts(subject, animation, frameCount) {
|
|
57
|
+
const preset = ANIMATION_PRESETS[animation] || ANIMATION_PRESETS.idle
|
|
58
|
+
const phases = []
|
|
59
|
+
for (let i = 0; i < frameCount; i++) {
|
|
60
|
+
phases.push(preset[i % preset.length])
|
|
61
|
+
}
|
|
62
|
+
return phases.map((phase, i) =>
|
|
63
|
+
`2D game sprite frame ${i + 1} of ${frameCount}: ${subject}. Animation phase: ${phase}. `
|
|
64
|
+
+ 'Transparent or solid flat background, consistent character scale and camera, same character design across frames, clean silhouette, even lighting.')
|
|
65
|
+
}
|
package/lib/tools/editing.js
CHANGED
|
@@ -116,6 +116,7 @@ export function registerEditingTools(ctx, deps) {
|
|
|
116
116
|
},
|
|
117
117
|
async execute(args, exec) {
|
|
118
118
|
try {
|
|
119
|
+
const cfg = live()
|
|
119
120
|
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
120
121
|
const estimatedCostEdit = calculateGenerationCost({
|
|
121
122
|
provider: cfg.provider || 'fal',
|
|
@@ -249,6 +250,7 @@ export function registerEditingTools(ctx, deps) {
|
|
|
249
250
|
},
|
|
250
251
|
async execute(args, exec) {
|
|
251
252
|
try {
|
|
253
|
+
const cfg = live()
|
|
252
254
|
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
253
255
|
const count = normalizeCount(args.count || 1)
|
|
254
256
|
const estimatedCostVary = calculateGenerationCost({
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// pattern — generate_seamless_pattern tool (#178)
|
|
2
|
+
|
|
3
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
6
|
+
import { makeProviders, toLosslessJson, saveAttachmentSafe, sizeToPixels } from '../providers.js'
|
|
7
|
+
import { trackAndAssertLoopGuard } from '../loop-guard.js'
|
|
8
|
+
import { sanitizeErrorAndLogs } from '../security.js'
|
|
9
|
+
import {
|
|
10
|
+
buildSeamlessPrompt,
|
|
11
|
+
buildPatternCss,
|
|
12
|
+
normalizeDensity,
|
|
13
|
+
} from '../pattern-helpers.js'
|
|
14
|
+
|
|
15
|
+
export function registerPatternTools(ctx, deps) {
|
|
16
|
+
const { live, slugify, resolveApiKey } = deps
|
|
17
|
+
|
|
18
|
+
ctx.effect(() => {
|
|
19
|
+
ctx.tools.register(
|
|
20
|
+
defineTool({
|
|
21
|
+
name: 'generate_seamless_pattern',
|
|
22
|
+
description:
|
|
23
|
+
'Generate a seamless tileable repeating pattern for backgrounds (#178). '
|
|
24
|
+
+ 'Describe the motif and optional style; returns a square pattern image plus CSS background-repeat snippet.',
|
|
25
|
+
parameters: {
|
|
26
|
+
motif: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
required: true,
|
|
29
|
+
description: 'What to repeat (e.g. "terrazzo chips", "sakura blossoms", "circuit traces").',
|
|
30
|
+
},
|
|
31
|
+
style: {
|
|
32
|
+
type: 'string',
|
|
33
|
+
description: 'Visual style (e.g. "watercolor", "flat vector", "isometric").',
|
|
34
|
+
},
|
|
35
|
+
density: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
description: 'Motif density: low, medium, or high (default medium).',
|
|
38
|
+
},
|
|
39
|
+
size: {
|
|
40
|
+
type: 'string',
|
|
41
|
+
description: 'Named image size (default square).',
|
|
42
|
+
},
|
|
43
|
+
output_name: {
|
|
44
|
+
type: 'string',
|
|
45
|
+
description: 'File name stem / CSS class (default seamless-pattern).',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
output: {
|
|
49
|
+
schema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
additionalProperties: true,
|
|
52
|
+
properties: {
|
|
53
|
+
summary: { type: 'string' },
|
|
54
|
+
path: { type: 'string' },
|
|
55
|
+
url: { type: 'string' },
|
|
56
|
+
css: { type: 'string' },
|
|
57
|
+
cssPath: { type: 'string' },
|
|
58
|
+
format: { type: 'string' },
|
|
59
|
+
attachment: { type: 'object', additionalProperties: true },
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
async execute(args, exec) {
|
|
64
|
+
try {
|
|
65
|
+
const cfg = live()
|
|
66
|
+
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
67
|
+
const density = normalizeDensity(args.density)
|
|
68
|
+
const prompt = buildSeamlessPrompt(args.motif, args.style, density)
|
|
69
|
+
const provider = cfg.provider || 'fal'
|
|
70
|
+
const format = cfg.defaultFormat || 'png'
|
|
71
|
+
const size = args.size || 'square'
|
|
72
|
+
const seed = Math.floor(Math.random() * 100000)
|
|
73
|
+
|
|
74
|
+
const pdeps = {
|
|
75
|
+
fetchImpl: fetch,
|
|
76
|
+
resolveKey: (ref) => resolveApiKey(ctx, ref),
|
|
77
|
+
cfg,
|
|
78
|
+
}
|
|
79
|
+
const job = { prompt, size, format, seed, provider }
|
|
80
|
+
const providers = makeProviders(pdeps, job)
|
|
81
|
+
const fn = providers[provider] || providers.fal || providers.custom
|
|
82
|
+
const gen = await fn(seed, prompt)
|
|
83
|
+
|
|
84
|
+
const stem = slugify(args.output_name || 'seamless-pattern')
|
|
85
|
+
const name = `${stem}.${format}`
|
|
86
|
+
const { attachment, localUrl } = await saveAttachmentSafe(ctx, {
|
|
87
|
+
bytes: gen.bytes,
|
|
88
|
+
mediaType: gen.mediaType || 'image/png',
|
|
89
|
+
name,
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
93
|
+
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
94
|
+
await mkdir(outDir, { recursive: true })
|
|
95
|
+
const filePath = path.join(outDir, name)
|
|
96
|
+
await writeFile(filePath, gen.bytes)
|
|
97
|
+
|
|
98
|
+
const [tilePx] = sizeToPixels(size)
|
|
99
|
+
const css = buildPatternCss({ className: stem, sizePx: tilePx, imagePath: `./${name}` })
|
|
100
|
+
const cssPath = path.join(outDir, `${stem}.css`)
|
|
101
|
+
await writeFile(cssPath, css, 'utf8')
|
|
102
|
+
|
|
103
|
+
const summary = `### Seamless pattern (${density})\n`
|
|
104
|
+
+ `- **Motif**: ${args.motif}\n`
|
|
105
|
+
+ `- **File**: \`${filePath}\`\n`
|
|
106
|
+
+ `- **CSS**: \`${cssPath}\`\n\n`
|
|
107
|
+
+ ``
|
|
108
|
+
|
|
109
|
+
return toLosslessJson({
|
|
110
|
+
summary,
|
|
111
|
+
path: filePath,
|
|
112
|
+
url: localUrl,
|
|
113
|
+
css,
|
|
114
|
+
cssPath,
|
|
115
|
+
format,
|
|
116
|
+
attachment,
|
|
117
|
+
})
|
|
118
|
+
} catch (err) {
|
|
119
|
+
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
}),
|
|
123
|
+
)
|
|
124
|
+
}, 'dsh-image-gen: tool generate_seamless_pattern')
|
|
125
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// processing-basic — core image-gen tools (remove_background, upscale_image, vectorize_image, blend_images). Split from processing.js (#239).
|
|
2
|
+
|
|
1
3
|
// processing — image-gen tools (remove_background, upscale_image, vectorize_image, blend_images). Extracted from apply() (#216).
|
|
2
4
|
|
|
3
5
|
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
@@ -44,7 +46,8 @@ import {
|
|
|
44
46
|
} from '../frontend-assets.js'
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
|
|
50
|
+
export function registerProcessingAdvancedTools(ctx, deps) {
|
|
48
51
|
const {
|
|
49
52
|
config,
|
|
50
53
|
live,
|
|
@@ -53,274 +56,6 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
53
56
|
slugify,
|
|
54
57
|
resolveApiKey,
|
|
55
58
|
} = deps
|
|
56
|
-
ctx.effect(() => {
|
|
57
|
-
ctx.tools.register(
|
|
58
|
-
defineTool({
|
|
59
|
-
name: 'remove_background',
|
|
60
|
-
description: 'Remove the background of an image (path or attachment id) and return a transparent PNG.',
|
|
61
|
-
parameters: {
|
|
62
|
-
image: { type: 'string', required: true, description: 'Path or attachment id of the source image.' },
|
|
63
|
-
model: { type: 'string', description: 'FAL model for background removal (default: fal-ai/birefnet).' },
|
|
64
|
-
output_name: { type: 'string', description: 'Custom output file name without extension.' },
|
|
65
|
-
},
|
|
66
|
-
output: {
|
|
67
|
-
schema: {
|
|
68
|
-
type: 'object',
|
|
69
|
-
additionalProperties: true,
|
|
70
|
-
properties: {
|
|
71
|
-
path: { type: 'string' },
|
|
72
|
-
url: { type: 'string' },
|
|
73
|
-
width: { type: 'number' },
|
|
74
|
-
height: { type: 'number' },
|
|
75
|
-
format: { type: 'string' },
|
|
76
|
-
},
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
async execute(args, exec) {
|
|
80
|
-
try {
|
|
81
|
-
const source = await resolveSource(ctx, exec, args.image)
|
|
82
|
-
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
83
|
-
const cfg = live()
|
|
84
|
-
const res = await removeBackgroundFal(
|
|
85
|
-
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
86
|
-
{ imageBytes: source.bytes, mediaType: source.mediaType, model: args.model, signal: exec.signal }
|
|
87
|
-
)
|
|
88
|
-
const stem = `${slugify(args.output_name || 'nobg')}-${Date.now().toString(36)}`
|
|
89
|
-
const name = `${stem}.png`
|
|
90
|
-
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
91
|
-
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
92
|
-
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
93
|
-
await mkdir(outDir, { recursive: true })
|
|
94
|
-
const filePath = path.join(outDir, name)
|
|
95
|
-
await writeFile(filePath, res.bytes)
|
|
96
|
-
const summary = buildDualOutputMarkdown({
|
|
97
|
-
action: 'background removed',
|
|
98
|
-
filePath,
|
|
99
|
-
width: res.width || attachment.width,
|
|
100
|
-
height: res.height || attachment.height,
|
|
101
|
-
mediaType: 'image/png',
|
|
102
|
-
provider: 'fal',
|
|
103
|
-
model: args.model || 'fal-ai/birefnet',
|
|
104
|
-
attachmentId: attachment.attachmentId,
|
|
105
|
-
})
|
|
106
|
-
return toLosslessJson({
|
|
107
|
-
summary,
|
|
108
|
-
path: filePath,
|
|
109
|
-
url: localUrl,
|
|
110
|
-
width: res.width || attachment.width,
|
|
111
|
-
height: res.height || attachment.height,
|
|
112
|
-
format: 'png',
|
|
113
|
-
attachment,
|
|
114
|
-
})
|
|
115
|
-
} catch (err) {
|
|
116
|
-
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
}),
|
|
120
|
-
)
|
|
121
|
-
}, 'dsh-image-gen: tool remove_background')
|
|
122
|
-
ctx.effect(() => {
|
|
123
|
-
ctx.tools.register(
|
|
124
|
-
defineTool({
|
|
125
|
-
name: 'upscale_image',
|
|
126
|
-
description: 'Upscale an image 2x or 4x with enhanced clarity and detail.',
|
|
127
|
-
parameters: {
|
|
128
|
-
image: { type: 'string', required: true, description: 'Path or attachment id of the source image.' },
|
|
129
|
-
scale: { type: 'number', description: 'Upscale factor: 2 or 4 (default: 2).' },
|
|
130
|
-
prompt: { type: 'string', description: 'Optional guiding prompt for detail reconstruction.' },
|
|
131
|
-
creativity: { type: 'number', description: 'Creativity / hallucination level from 0 to 1.' },
|
|
132
|
-
output_name: { type: 'string', description: 'Custom output file name.' },
|
|
133
|
-
},
|
|
134
|
-
output: {
|
|
135
|
-
schema: {
|
|
136
|
-
type: 'object',
|
|
137
|
-
additionalProperties: true,
|
|
138
|
-
properties: {
|
|
139
|
-
path: { type: 'string' },
|
|
140
|
-
url: { type: 'string' },
|
|
141
|
-
scale: { type: 'number' },
|
|
142
|
-
width: { type: 'number' },
|
|
143
|
-
height: { type: 'number' },
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
},
|
|
147
|
-
async execute(args, exec) {
|
|
148
|
-
try {
|
|
149
|
-
const source = await resolveSource(ctx, exec, args.image)
|
|
150
|
-
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
151
|
-
const cfg = live()
|
|
152
|
-
const res = await upscaleImageFal(
|
|
153
|
-
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
154
|
-
{ imageBytes: source.bytes, mediaType: source.mediaType, scale: args.scale, prompt: args.prompt, creativity: args.creativity, signal: exec.signal }
|
|
155
|
-
)
|
|
156
|
-
const stem = `${slugify(args.output_name || 'upscaled')}-${Date.now().toString(36)}`
|
|
157
|
-
const name = `${stem}.png`
|
|
158
|
-
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
159
|
-
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
160
|
-
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
161
|
-
await mkdir(outDir, { recursive: true })
|
|
162
|
-
const filePath = path.join(outDir, name)
|
|
163
|
-
await writeFile(filePath, res.bytes)
|
|
164
|
-
const summary = buildDualOutputMarkdown({
|
|
165
|
-
action: `upscaled (${args.scale || 2}x)`,
|
|
166
|
-
filePath,
|
|
167
|
-
width: res.width || attachment.width,
|
|
168
|
-
height: res.height || attachment.height,
|
|
169
|
-
mediaType: 'image/png',
|
|
170
|
-
provider: 'fal',
|
|
171
|
-
model: 'fal-ai/clarity-upscaler',
|
|
172
|
-
attachmentId: attachment.attachmentId,
|
|
173
|
-
})
|
|
174
|
-
return toLosslessJson({
|
|
175
|
-
summary,
|
|
176
|
-
path: filePath,
|
|
177
|
-
url: localUrl,
|
|
178
|
-
scale: args.scale || 2,
|
|
179
|
-
width: res.width || attachment.width,
|
|
180
|
-
height: res.height || attachment.height,
|
|
181
|
-
attachment,
|
|
182
|
-
})
|
|
183
|
-
} catch (err) {
|
|
184
|
-
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
185
|
-
}
|
|
186
|
-
},
|
|
187
|
-
}),
|
|
188
|
-
)
|
|
189
|
-
}, 'dsh-image-gen: tool upscale_image')
|
|
190
|
-
ctx.effect(() => {
|
|
191
|
-
ctx.tools.register(
|
|
192
|
-
defineTool({
|
|
193
|
-
name: 'vectorize_image',
|
|
194
|
-
description: 'Convert a raster image or icon to SVG vector format.',
|
|
195
|
-
parameters: {
|
|
196
|
-
image: { type: 'string', required: true, description: 'Path or attachment id of the source image.' },
|
|
197
|
-
color_mode: { type: 'string', description: '"color" or "binary" (default: "color").' },
|
|
198
|
-
output_name: { type: 'string', description: 'Custom output file name.' },
|
|
199
|
-
},
|
|
200
|
-
output: {
|
|
201
|
-
schema: {
|
|
202
|
-
type: 'object',
|
|
203
|
-
additionalProperties: true,
|
|
204
|
-
properties: {
|
|
205
|
-
path: { type: 'string' },
|
|
206
|
-
svg: { type: 'string' },
|
|
207
|
-
format: { type: 'string' },
|
|
208
|
-
},
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
async execute(args, exec) {
|
|
212
|
-
try {
|
|
213
|
-
const source = await resolveSource(ctx, exec, args.image)
|
|
214
|
-
if (!source || !source.bytes) throw new Error('Source image not found: ' + args.image)
|
|
215
|
-
const cfg = live()
|
|
216
|
-
const res = traceToSvg(source.bytes, { colorMode: args.color_mode })
|
|
217
|
-
const stem = `${slugify(args.output_name || 'vector')}-${Date.now().toString(36)}`
|
|
218
|
-
const name = `${stem}.svg`
|
|
219
|
-
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
220
|
-
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
221
|
-
await mkdir(outDir, { recursive: true })
|
|
222
|
-
const filePath = path.join(outDir, name)
|
|
223
|
-
await writeFile(filePath, res.bytes)
|
|
224
|
-
|
|
225
|
-
let attachment = null
|
|
226
|
-
let localUrl = ''
|
|
227
|
-
if (ctx.attachments?.saveImage) {
|
|
228
|
-
attachment = await ctx.attachments.saveImage({
|
|
229
|
-
data: new Uint8Array(res.bytes),
|
|
230
|
-
mediaType: 'image/svg+xml',
|
|
231
|
-
name,
|
|
232
|
-
})
|
|
233
|
-
localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const summary = `### Vectorized SVG Export\n- **File**: ${filePath}\n- **Format**: SVG (XML vector)\n- **Color mode**: ${args.color_mode || 'color'}\n- **Palette size**: ${res.palette ? res.palette.length : 16} colors\n`
|
|
237
|
-
return toLosslessJson({
|
|
238
|
-
summary,
|
|
239
|
-
path: filePath,
|
|
240
|
-
url: localUrl,
|
|
241
|
-
svg: res.svg,
|
|
242
|
-
format: 'svg',
|
|
243
|
-
attachment,
|
|
244
|
-
})
|
|
245
|
-
} catch (err) {
|
|
246
|
-
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
|
-
}),
|
|
250
|
-
)
|
|
251
|
-
}, 'dsh-image-gen: tool vectorize_image')
|
|
252
|
-
ctx.effect(() => {
|
|
253
|
-
ctx.tools.register(
|
|
254
|
-
defineTool({
|
|
255
|
-
name: 'blend_images',
|
|
256
|
-
description: 'Blend multiple images (paths or attachment ids) into a coherent new composition.',
|
|
257
|
-
parameters: {
|
|
258
|
-
images: { type: 'array', items: { type: 'string' }, required: true, description: 'List of image paths or attachment ids to blend.' },
|
|
259
|
-
weights: { type: 'array', items: { type: 'number' }, description: 'Weights for each image (optional).' },
|
|
260
|
-
prompt: { type: 'string', description: 'Guiding prompt for the blend.' },
|
|
261
|
-
output_name: { type: 'string', description: 'Custom output file name.' },
|
|
262
|
-
},
|
|
263
|
-
output: {
|
|
264
|
-
schema: {
|
|
265
|
-
type: 'object',
|
|
266
|
-
additionalProperties: true,
|
|
267
|
-
properties: {
|
|
268
|
-
path: { type: 'string' },
|
|
269
|
-
url: { type: 'string' },
|
|
270
|
-
width: { type: 'number' },
|
|
271
|
-
height: { type: 'number' },
|
|
272
|
-
},
|
|
273
|
-
},
|
|
274
|
-
},
|
|
275
|
-
async execute(args, exec) {
|
|
276
|
-
try {
|
|
277
|
-
const cfg = live()
|
|
278
|
-
trackAndAssertLoopGuard(exec?.agent?.session?.id || 'default', { limit: cfg.loopGuardLimit })
|
|
279
|
-
assertBudgetAvailable(0.05, cfg.dailyBudgetUsd)
|
|
280
|
-
const refs = Array.isArray(args.images) ? args.images : [args.images].filter(Boolean)
|
|
281
|
-
const sources = []
|
|
282
|
-
for (const r of refs) {
|
|
283
|
-
const s = await resolveSource(ctx, exec, r)
|
|
284
|
-
if (s && s.bytes) sources.push(s)
|
|
285
|
-
}
|
|
286
|
-
if (sources.length === 0) throw new Error('No valid images found to blend')
|
|
287
|
-
const res = await blendImagesFal(
|
|
288
|
-
{ fetchImpl: fetch, resolveKey: (ref) => resolveApiKey(ctx, ref), cfg },
|
|
289
|
-
{ images: sources, weights: args.weights, prompt: args.prompt, signal: exec.signal }
|
|
290
|
-
)
|
|
291
|
-
const stem = `${slugify(args.output_name || 'blended')}-${Date.now().toString(36)}`
|
|
292
|
-
const name = `${stem}.png`
|
|
293
|
-
const { attachment, localUrl } = await saveAttachmentSafe(ctx, { bytes: res.bytes, mediaType: 'image/png', name })
|
|
294
|
-
const sessionCwd = exec.agent?.session?.header?.cwd
|
|
295
|
-
const outDir = path.resolve(sessionCwd || process.cwd(), cfg.outputDir || 'generated/images')
|
|
296
|
-
await mkdir(outDir, { recursive: true })
|
|
297
|
-
const filePath = path.join(outDir, name)
|
|
298
|
-
await writeFile(filePath, res.bytes)
|
|
299
|
-
const summary = buildDualOutputMarkdown({
|
|
300
|
-
action: 'blended composition',
|
|
301
|
-
filePath,
|
|
302
|
-
width: res.width || attachment.width,
|
|
303
|
-
height: res.height || attachment.height,
|
|
304
|
-
mediaType: 'image/png',
|
|
305
|
-
provider: 'fal',
|
|
306
|
-
model: 'fal-ai/image-blend',
|
|
307
|
-
attachmentId: attachment.attachmentId,
|
|
308
|
-
})
|
|
309
|
-
return toLosslessJson({
|
|
310
|
-
summary,
|
|
311
|
-
path: filePath,
|
|
312
|
-
url: localUrl,
|
|
313
|
-
width: res.width || attachment.width,
|
|
314
|
-
height: res.height || attachment.height,
|
|
315
|
-
attachment,
|
|
316
|
-
})
|
|
317
|
-
} catch (err) {
|
|
318
|
-
throw new Error(sanitizeErrorAndLogs(err.message || err))
|
|
319
|
-
}
|
|
320
|
-
},
|
|
321
|
-
}),
|
|
322
|
-
)
|
|
323
|
-
}, 'dsh-image-gen: tool blend_images')
|
|
324
59
|
|
|
325
60
|
ctx.effect(() => {
|
|
326
61
|
ctx.tools.register(
|
|
@@ -723,7 +458,13 @@ export function registerProcessingTools(ctx, deps) {
|
|
|
723
458
|
const b64 = Buffer.isBuffer(src.bytes) ? src.bytes.toString('base64') : Buffer.from(src.bytes).toString('base64')
|
|
724
459
|
logoDataUri = `data:${src.mediaType || 'image/png'};base64,${b64}`
|
|
725
460
|
}
|
|
726
|
-
} catch (
|
|
461
|
+
} catch (err) {
|
|
462
|
+
// Optional logo embed: brand pack still exports without it.
|
|
463
|
+
const msg = err?.message || String(err)
|
|
464
|
+
if (typeof ctx?.logger?.debug === 'function') {
|
|
465
|
+
ctx.logger.debug(`dsh-image-gen: brand logo embed skipped: ${msg}`)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
727
468
|
}
|
|
728
469
|
|
|
729
470
|
const exportedFiles = []
|