@goodandready/dsh-image-gen 0.11.39 → 0.11.41

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,134 @@
1
+ import {
2
+ enqueueBatch,
3
+ getBatchQueue,
4
+ getBatchProgress,
5
+ cancelBatch,
6
+ retryFailedBatchItems,
7
+ } from '../batch-queue.js'
8
+
9
+ /**
10
+ * Registers batch queue management tools (#437).
11
+ */
12
+ export function registerBatchQueueTools(ctx, deps = {}) {
13
+ ctx.tools.register({
14
+ name: 'enqueue_batch_jobs',
15
+ description: 'Enqueues a series of generation or processing jobs with budget pre-flight check and concurrency control.',
16
+ parameters: {
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ properties: {
20
+ name: {
21
+ type: 'string',
22
+ description: 'Descriptive title for the batch.',
23
+ },
24
+ items: {
25
+ type: 'array',
26
+ items: {
27
+ type: 'object',
28
+ additionalProperties: true,
29
+ properties: {
30
+ tool: { type: 'string' },
31
+ params: { type: 'object', additionalProperties: true },
32
+ estimatedCost: { type: 'number' },
33
+ },
34
+ },
35
+ description: 'Array of jobs to execute.',
36
+ },
37
+ concurrency: {
38
+ type: 'number',
39
+ description: 'Maximum concurrent active workers (default 2, max 10).',
40
+ },
41
+ maxBudgetUSD: {
42
+ type: 'number',
43
+ description: 'Maximum total allowed budget in USD for this entire batch.',
44
+ },
45
+ },
46
+ required: ['items'],
47
+ },
48
+ output: {
49
+ render(args, value) {
50
+ return [
51
+ {
52
+ type: 'text',
53
+ text: `Enqueued Batch: [${value?.batchId}] "${value?.name}"\n` +
54
+ `Total Jobs: ${value?.items?.length} | Estimated Cost: $${value?.totalEstimatedCost?.toFixed(3)}\n` +
55
+ `Status: ${value?.status} | Concurrency: ${value?.concurrency}`,
56
+ },
57
+ ]
58
+ },
59
+ },
60
+ async execute(args) {
61
+ const storagePath = deps.queueStoragePath || null
62
+ return enqueueBatch(
63
+ {
64
+ name: args.name,
65
+ items: args.items,
66
+ concurrency: args.concurrency || 2,
67
+ maxBudget: args.maxBudgetUSD || Infinity,
68
+ },
69
+ storagePath
70
+ )
71
+ },
72
+ })
73
+
74
+ ctx.tools.register({
75
+ name: 'manage_batch_queue',
76
+ description: 'Manages batch queues: status inquiry, cancellation, or retrying failed items.',
77
+ parameters: {
78
+ type: 'object',
79
+ additionalProperties: false,
80
+ properties: {
81
+ batchId: {
82
+ type: 'string',
83
+ description: 'Target batch queue identifier.',
84
+ },
85
+ action: {
86
+ type: 'string',
87
+ enum: ['status', 'cancel', 'retry_failed'],
88
+ description: 'Action to perform: check status, cancel, or retry failed items.',
89
+ },
90
+ itemId: {
91
+ type: 'string',
92
+ description: 'Optional single item ID to cancel.',
93
+ },
94
+ },
95
+ required: ['batchId', 'action'],
96
+ },
97
+ output: {
98
+ render(args, value) {
99
+ if (args.action === 'status') {
100
+ const p = value?.progress || {}
101
+ return [
102
+ {
103
+ type: 'text',
104
+ text: `Batch [${args.batchId}] Status: ${value?.status} (${p.percent}%)\n` +
105
+ `Total: ${p.total} | Completed: ${p.completed} | Failed: ${p.failed} | Queued: ${p.queued} | Running: ${p.running} | Interrupted: ${p.interrupted || 0}`,
106
+ },
107
+ ]
108
+ }
109
+ if (args.action === 'cancel') {
110
+ return [{ type: 'text', text: `Cancelled batch [${args.batchId}]. Queued jobs discarded.` }]
111
+ }
112
+ if (args.action === 'retry_failed') {
113
+ return [{ type: 'text', text: `Reset ${value?.resetCount || 0} failed items for retry in batch [${args.batchId}].` }]
114
+ }
115
+ return [{ type: 'text', text: JSON.stringify(value) }]
116
+ },
117
+ },
118
+ async execute(args) {
119
+ const storagePath = deps.queueStoragePath || null
120
+ if (args.action === 'status') {
121
+ const batch = getBatchQueue(args.batchId, storagePath)
122
+ if (!batch) return { ok: false, error: `Batch "${args.batchId}" not found` }
123
+ return { ok: true, batchId: batch.batchId, status: batch.status, progress: getBatchProgress(batch), items: batch.items }
124
+ }
125
+ if (args.action === 'cancel') {
126
+ return cancelBatch(args.batchId, args.itemId, storagePath)
127
+ }
128
+ if (args.action === 'retry_failed') {
129
+ return retryFailedBatchItems(args.batchId, args.itemId ? [args.itemId] : null, storagePath)
130
+ }
131
+ throw new Error(`Unknown action: ${args.action}`)
132
+ },
133
+ })
134
+ }
@@ -0,0 +1,139 @@
1
+ import {
2
+ getCapabilityRegistry,
3
+ recommendModelForTask,
4
+ validateModelCapability,
5
+ } from '../capabilities.js'
6
+
7
+ /**
8
+ * Registers capability-based model selection tools (#434).
9
+ */
10
+ export function registerCapabilityTools(ctx, deps = {}) {
11
+ ctx.tools.register({
12
+ name: 'select_image_model',
13
+ description: 'Recommends the best provider and model based on required task capabilities (text rendering, inpainting, face preservation, cost limits).',
14
+ parameters: {
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ properties: {
18
+ operations: {
19
+ type: 'array',
20
+ items: { type: 'string' },
21
+ description: 'Required operations, e.g. ["txt2img", "inpaint", "img2img"].',
22
+ },
23
+ textRendering: {
24
+ type: 'boolean',
25
+ description: 'Whether native high-precision text rendering is required.',
26
+ },
27
+ facePreservation: {
28
+ type: 'boolean',
29
+ description: 'Whether facial identity likeness preservation is required.',
30
+ },
31
+ maskInpainting: {
32
+ type: 'boolean',
33
+ description: 'Whether pixel-accurate inpainting mask support is required.',
34
+ },
35
+ referenceConditioning: {
36
+ type: 'boolean',
37
+ description: 'Whether reference image guidance is required.',
38
+ },
39
+ controlnet: {
40
+ type: 'boolean',
41
+ description: 'Whether structural ControlNet guidance is required.',
42
+ },
43
+ minResolution: {
44
+ type: 'number',
45
+ description: 'Minimum resolution in pixels (e.g. 1024 or 2048).',
46
+ },
47
+ maxCostUSD: {
48
+ type: 'number',
49
+ description: 'Maximum estimated cost in USD per generation.',
50
+ },
51
+ },
52
+ },
53
+ output: {
54
+ render(args, value) {
55
+ if (!value?.ok) {
56
+ return [{ type: 'text', text: `Capability selection: ${value?.message || 'No matching model found.'}` }]
57
+ }
58
+ const rec = value.recommended
59
+ return [
60
+ {
61
+ type: 'text',
62
+ text: `Recommended Model: ${rec.displayName} (${rec.provider}/${rec.modelId})\nReason: ${value.recommendationReason}\nEstimated Cost: $${rec.estimatedCostUSD} (Source: ${rec.pricingSource})`,
63
+ },
64
+ ]
65
+ },
66
+ },
67
+ async execute(args) {
68
+ const requirements = {
69
+ operations: args.operations || [],
70
+ textRendering: Boolean(args.textRendering),
71
+ facePreservation: Boolean(args.facePreservation),
72
+ maskInpainting: Boolean(args.maskInpainting),
73
+ referenceConditioning: Boolean(args.referenceConditioning),
74
+ controlnet: Boolean(args.controlnet),
75
+ minResolution: args.minResolution || 0,
76
+ maxCost: args.maxCostUSD || Infinity,
77
+ }
78
+
79
+ // Check configured provider list from deps or runtime
80
+ const configuredProviders = deps.cfg?.provider ? [deps.cfg.provider] : null
81
+ return recommendModelForTask(requirements, { configuredProviders })
82
+ },
83
+ })
84
+
85
+ ctx.tools.register({
86
+ name: 'validate_model_features',
87
+ description: 'Pre-flight check if a model or provider supports specific generation features before running the task.',
88
+ parameters: {
89
+ type: 'object',
90
+ additionalProperties: false,
91
+ properties: {
92
+ provider: {
93
+ type: 'string',
94
+ description: 'Provider name (fal, gemini, replicate, local, seedream).',
95
+ },
96
+ modelId: {
97
+ type: 'string',
98
+ description: 'Specific model identifier.',
99
+ },
100
+ operation: {
101
+ type: 'string',
102
+ description: 'Requested operation (txt2img, img2img, inpaint).',
103
+ },
104
+ requiresMask: {
105
+ type: 'boolean',
106
+ description: 'Whether operation requires inpainting mask.',
107
+ },
108
+ requiresReference: {
109
+ type: 'boolean',
110
+ description: 'Whether operation requires reference image.',
111
+ },
112
+ requiresFacePreservation: {
113
+ type: 'boolean',
114
+ description: 'Whether operation requires face preservation.',
115
+ },
116
+ requiresTextRender: {
117
+ type: 'boolean',
118
+ description: 'Whether operation requires text rendering.',
119
+ },
120
+ },
121
+ required: ['provider'],
122
+ },
123
+ output: {
124
+ render(args, value) {
125
+ return [
126
+ {
127
+ type: 'text',
128
+ text: value.valid
129
+ ? `Validation PASSED for ${args.provider}: ${value.model?.displayName}`
130
+ : `Validation FAILED (${value.status}): ${value.reason}`,
131
+ },
132
+ ]
133
+ },
134
+ },
135
+ async execute(args) {
136
+ return validateModelCapability(args.provider, args.modelId, args)
137
+ },
138
+ })
139
+ }
@@ -0,0 +1,116 @@
1
+ import {
2
+ createStagingSession,
3
+ reviewStagedEdit,
4
+ getStagingSession,
5
+ } from '../inpaint-staging.js'
6
+
7
+ /**
8
+ * Registers staging & review workflow tools (#436).
9
+ */
10
+ export function registerInpaintStagingTools(ctx, deps = {}) {
11
+ ctx.tools.register({
12
+ name: 'stage_image_edit',
13
+ description: 'Stages a localized inpaint or image modification in preview state for user review before committing.',
14
+ parameters: {
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ properties: {
18
+ image: {
19
+ type: 'string',
20
+ description: 'Original image path or URL to edit.',
21
+ },
22
+ prompt: {
23
+ type: 'string',
24
+ description: 'Edit instruction or prompt for the localized modification.',
25
+ },
26
+ mask: {
27
+ type: 'string',
28
+ description: 'Optional inpainting mask path, base64, or SVG string.',
29
+ },
30
+ bbox: {
31
+ type: 'array',
32
+ items: { type: 'number' },
33
+ description: 'Optional bounding box [x1, y1, x2, y2] (normalized 0-1000).',
34
+ },
35
+ previewImage: {
36
+ type: 'string',
37
+ description: 'Initial preview image path if already produced.',
38
+ },
39
+ },
40
+ required: ['image', 'prompt'],
41
+ },
42
+ output: {
43
+ render(args, value) {
44
+ return [
45
+ {
46
+ type: 'text',
47
+ text: `Staging Session Created: [${value?.stageId}]\n` +
48
+ `Status: ${value?.status} (preview only, original preserved)\n` +
49
+ `Instruction: "${value?.prompt}"\n` +
50
+ `Preview: ${value?.previewImage || 'Generating in background...'}`,
51
+ },
52
+ ]
53
+ },
54
+ },
55
+ async execute(args) {
56
+ const storagePath = deps.stagingStoragePath || null
57
+ return createStagingSession(
58
+ {
59
+ originalImage: args.image,
60
+ prompt: args.prompt,
61
+ mask: args.mask,
62
+ bbox: args.bbox,
63
+ previewImage: args.previewImage,
64
+ provider: deps.cfg?.provider || 'fal',
65
+ },
66
+ storagePath
67
+ )
68
+ },
69
+ })
70
+
71
+ ctx.tools.register({
72
+ name: 'review_staged_edit',
73
+ description: 'Reviews a staged image edit: accept (commits change), reject (restores original), retry (new prompt), or cancel.',
74
+ parameters: {
75
+ type: 'object',
76
+ additionalProperties: false,
77
+ properties: {
78
+ stageId: {
79
+ type: 'string',
80
+ description: 'ID of the staging session to review.',
81
+ },
82
+ action: {
83
+ type: 'string',
84
+ enum: ['accept', 'reject', 'retry', 'cancel'],
85
+ description: 'Review decision: accept, reject, retry, or cancel.',
86
+ },
87
+ prompt: {
88
+ type: 'string',
89
+ description: 'New instruction if retrying with updated prompt.',
90
+ },
91
+ reason: {
92
+ type: 'string',
93
+ description: 'Optional reason when rejecting or cancelling.',
94
+ },
95
+ },
96
+ required: ['stageId', 'action'],
97
+ },
98
+ output: {
99
+ render(args, value) {
100
+ if (!value?.ok) {
101
+ return [{ type: 'text', text: `Review failed: ${value?.error || 'Unknown error'}` }]
102
+ }
103
+ return [
104
+ {
105
+ type: 'text',
106
+ text: `Review Result: ${value.message}\nStatus: ${value.status}\nStage ID: ${args.stageId}`,
107
+ },
108
+ ]
109
+ },
110
+ },
111
+ async execute(args) {
112
+ const storagePath = deps.stagingStoragePath || null
113
+ return reviewStagedEdit(args.stageId, args.action, { prompt: args.prompt, reason: args.reason }, storagePath)
114
+ },
115
+ })
116
+ }
@@ -0,0 +1,139 @@
1
+ import {
2
+ getAllProjectProfiles,
3
+ getProjectProfileById,
4
+ saveCustomProfile,
5
+ deleteCustomProfile,
6
+ resolveEffectiveParams,
7
+ } from '../project-profiles.js'
8
+
9
+ /**
10
+ * Registers project visual profile management tools (#435).
11
+ */
12
+ export function registerProjectProfileTools(ctx, deps = {}) {
13
+ ctx.tools.register({
14
+ name: 'manage_project_profiles',
15
+ description: 'Manages visual project and series profiles (list, get, save, delete) for consistent styling across generation sets.',
16
+ parameters: {
17
+ type: 'object',
18
+ additionalProperties: false,
19
+ properties: {
20
+ action: {
21
+ type: 'string',
22
+ enum: ['list', 'get', 'save', 'delete'],
23
+ description: 'Action to perform: list all profiles, get by id, save (create/update), or delete.',
24
+ },
25
+ id: {
26
+ type: 'string',
27
+ description: 'Profile ID for get or delete actions.',
28
+ },
29
+ profile: {
30
+ type: 'object',
31
+ additionalProperties: true,
32
+ description: 'Profile specification object for save action (id, name, palette, stylePrompt, negativePrompt, format, rules).',
33
+ },
34
+ },
35
+ required: ['action'],
36
+ },
37
+ output: {
38
+ render(args, value) {
39
+ if (args.action === 'list') {
40
+ const list = Array.isArray(value) ? value : []
41
+ return [
42
+ {
43
+ type: 'text',
44
+ text: `Project Profiles (${list.length}):\n` +
45
+ list.map((p) => `- [${p.id}] ${p.name} (v${p.version || '1.0.0'}) ${p.builtin ? '(builtin)' : '(custom)'}`).join('\n'),
46
+ },
47
+ ]
48
+ }
49
+ if (args.action === 'get') {
50
+ return [
51
+ {
52
+ type: 'text',
53
+ text: value ? `Profile [${value.id}] ${value.name}:\nStyle: "${value.stylePrompt || ''}"\nPalette: ${(value.palette || []).join(', ')}` : 'Profile not found.',
54
+ },
55
+ ]
56
+ }
57
+ if (args.action === 'save') {
58
+ return [{ type: 'text', text: `Saved profile [${value?.id}] ${value?.name} successfully.` }]
59
+ }
60
+ if (args.action === 'delete') {
61
+ return [{ type: 'text', text: value ? `Deleted profile ${args.id}.` : `Profile ${args.id} not found or cannot be deleted.` }]
62
+ }
63
+ return [{ type: 'text', text: JSON.stringify(value) }]
64
+ },
65
+ },
66
+ async execute(args) {
67
+ const storagePath = deps.profilesPath || null
68
+ if (args.action === 'list') {
69
+ return getAllProjectProfiles(storagePath)
70
+ }
71
+ if (args.action === 'get') {
72
+ return getProjectProfileById(args.id, storagePath)
73
+ }
74
+ if (args.action === 'save') {
75
+ return saveCustomProfile(args.profile, storagePath)
76
+ }
77
+ if (args.action === 'delete') {
78
+ return deleteCustomProfile(args.id, storagePath)
79
+ }
80
+ throw new Error(`Unknown action: ${args.action}`)
81
+ },
82
+ })
83
+
84
+ ctx.tools.register({
85
+ name: 'resolve_project_profile_params',
86
+ description: 'Calculates effective generation parameters by cascading request overrides over a project visual profile and system defaults.',
87
+ parameters: {
88
+ type: 'object',
89
+ additionalProperties: false,
90
+ properties: {
91
+ profileId: {
92
+ type: 'string',
93
+ description: 'Identifier of the project visual profile.',
94
+ },
95
+ prompt: {
96
+ type: 'string',
97
+ description: 'Specific image request prompt.',
98
+ },
99
+ negativePrompt: {
100
+ type: 'string',
101
+ description: 'Specific negative prompt overrides.',
102
+ },
103
+ size: {
104
+ type: 'string',
105
+ description: 'Specific dimensions (e.g. 1024x1024 or 1280x720).',
106
+ },
107
+ format: {
108
+ type: 'string',
109
+ description: 'Output format (webp, png, jpeg).',
110
+ },
111
+ },
112
+ required: ['profileId'],
113
+ },
114
+ output: {
115
+ render(args, value) {
116
+ return [
117
+ {
118
+ type: 'text',
119
+ text: `Effective Parameters for Profile [${value?.appliedProfile?.name || args.profileId}]:\n` +
120
+ `Prompt: "${value?.effectiveParams?.prompt || ''}"\n` +
121
+ `Negative: "${value?.effectiveParams?.negativePrompt || ''}"\n` +
122
+ `Dimensions: ${value?.effectiveParams?.size} (${value?.effectiveParams?.format})\n` +
123
+ `Provenance: ${JSON.stringify(value?.provenance || {})}`,
124
+ },
125
+ ]
126
+ },
127
+ },
128
+ async execute(args) {
129
+ const storagePath = deps.profilesPath || null
130
+ const requestParams = {
131
+ prompt: args.prompt,
132
+ negativePrompt: args.negativePrompt,
133
+ size: args.size,
134
+ format: args.format,
135
+ }
136
+ return resolveEffectiveParams(requestParams, args.profileId, storagePath)
137
+ },
138
+ })
139
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.11.39",
3
+ "version": "0.11.41",
4
4
  "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
5
5
  "keywords": [
6
6
  "deepseek-harness",