@goodandready/dsh-image-gen 0.10.35 → 0.11.1

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.
@@ -4,6 +4,11 @@ import {
4
4
  calculateBackoff,
5
5
  extractComfyNodeErrors,
6
6
  } from '../shared-helpers.js'
7
+ import {
8
+ validateComfyWorkflow,
9
+ interpolateComfyWorkflow,
10
+ buildDefaultComfyWorkflow,
11
+ } from '../../comfy-workflow-helpers.js'
7
12
 
8
13
  /**
9
14
  * @param {fetchImpl: Function, resolveKey: Function, cfg: object} deps
@@ -65,17 +70,34 @@ export function createLocalGenerator(deps, job) {
65
70
 
66
71
  // ComfyUI: submit via /prompt, poll /history/{prompt_id} until completed.
67
72
  const promptId = `dsh-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`
68
- const workflow = {
69
- prompt: {
70
- '3': { class_type: 'KSampler', inputs: { seed: seedArg ?? 0, steps: cfg.localSteps ?? 20, cfg: cfg.localCfg ?? 7, sampler_name: 'euler', scheduler: 'normal', denoise: 1, model: ['4', 0], positive: ['6', 0], negative: ['7', 0], latent_image: ['5', 0] } },
71
- '4': { class_type: 'CheckpointLoaderSimple', inputs: { ckpt_name: cfg.localModel || 'v1-5-pruned-emaonly.safetensors' } },
72
- '5': { class_type: 'EmptyLatentImage', inputs: { width, height, batch_size: 1 } },
73
- '6': { class_type: 'CLIPTextEncode', inputs: { text: promptArg, clip: ['4', 1] } },
74
- '7': { class_type: 'CLIPTextEncode', inputs: { text: negativePrompt || '', clip: ['4', 1] } },
75
- '8': { class_type: 'VAEDecode', inputs: { samples: ['3', 0], vae: ['4', 2] } },
76
- '9': { class_type: 'SaveImage', inputs: { filename_prefix: 'dsh', images: ['8', 0] } },
77
- },
73
+ let workflowGraph
74
+ if (cfg.comfyWorkflowJson) {
75
+ const template = validateComfyWorkflow(cfg.comfyWorkflowJson)
76
+ workflowGraph = interpolateComfyWorkflow(template, {
77
+ prompt: promptArg,
78
+ negativePrompt: negativePrompt || '',
79
+ seed: seedArg ?? 0,
80
+ width,
81
+ height,
82
+ steps: cfg.localSteps ?? 20,
83
+ cfg: cfg.localCfg ?? 7,
84
+ model: cfg.localModel,
85
+ image: source && source.bytes ? Buffer.from(source.bytes).toString('base64') : '',
86
+ })
87
+ } else {
88
+ workflowGraph = buildDefaultComfyWorkflow({
89
+ prompt: promptArg,
90
+ negativePrompt,
91
+ seed: seedArg,
92
+ width,
93
+ height,
94
+ model: cfg.localModel,
95
+ steps: cfg.localSteps,
96
+ cfg: cfg.localCfg,
97
+ })
78
98
  }
99
+
100
+ const workflow = { prompt: workflowGraph }
79
101
  const submit = await fetchImpl(`${base}/prompt`, {
80
102
  method: 'POST',
81
103
  headers: { 'Content-Type': 'application/json' },
@@ -133,4 +155,4 @@ export function createLocalGenerator(deps, job) {
133
155
  }
134
156
  }
135
157
  return local
136
- }
158
+ }
package/lib/providers.js CHANGED
@@ -4,28 +4,18 @@
4
4
 
5
5
  // Utilities and constants extracted to provider-utils.js (#239)
6
6
  import {
7
- resolveApiKeyCandidates,
8
- PROVIDER_MAX_COUNTS,
9
- clampProviderCount,
10
- buildEndpointUrl,
11
- createAbortError,
12
- estimateSharpnessAndVariance,
13
- quantizePalette,
14
- formatA1111Parameters,
15
- computeGenerationHash,
16
- PROVIDER_KEYS,
17
- fallbackOrder,
18
- calculateBackoff,
19
7
  isFatalClientError,
20
8
  formatErrorMessage,
21
- resolveSubscriptionSize,
22
- SUBSCRIPTION_SIZES,
23
- IMAGE_SIZES,
24
- OUTPUT_FORMATS,
25
- SIZE_PIXELS,
26
- normalizeCount,
9
+ formatA1111Parameters,
10
+ quantizePalette,
27
11
  } from './provider-utils.js'
28
12
 
13
+ import {
14
+ falAuthHeader,
15
+ submitJob,
16
+ pollStatus,
17
+ } from './providers/shared-helpers.js'
18
+
29
19
  // Per-backend factories (#218)
30
20
  import { createFalGenerator } from './providers/backends/fal.js'
31
21
  import { createCustomGenerator } from './providers/backends/custom.js'
@@ -49,18 +39,7 @@ export {
49
39
  extractComfyNodeErrors,
50
40
  } from './providers/shared-helpers.js'
51
41
 
52
- import {
53
- falAuthHeader,
54
- normalizeMediaType,
55
- submitJob,
56
- pollStatus,
57
- buildEditForm,
58
- estimateCost,
59
- pxSize,
60
- snapToMultipleOf64,
61
- snapDimensions,
62
- extractComfyNodeErrors,
63
- } from './providers/shared-helpers.js'
42
+
64
43
 
65
44
  // Re-export utilities so existing imports from './providers.js' continue to work
66
45
  export {
@@ -14,6 +14,7 @@ import { registerResponsiveTools } from './tools/responsive.js'
14
14
  import { registerAnchorTools } from './tools/anchor.js'
15
15
  import { registerUiAssetTools } from './tools/ui-asset.js'
16
16
  import { registerStyleMatrixTools } from './tools/style-matrix.js'
17
+ import { registerThemePairTools } from './tools/theme-pair.js'
17
18
 
18
19
  /**
19
20
  * Registers every image-gen tool on the host tools service.
@@ -35,4 +36,5 @@ export function registerAllTools(ctx, deps) {
35
36
  registerAnchorTools(ctx, deps)
36
37
  registerUiAssetTools(ctx, deps)
37
38
  registerStyleMatrixTools(ctx, deps)
38
- }
39
+ registerThemePairTools(ctx, deps)
40
+ }
@@ -0,0 +1,112 @@
1
+ // lib/settings-route.js
2
+ // REST endpoints for plugin settings (/dsh-image-gen/config, /dsh-image-gen/status) (#295, #300).
3
+ // Provides HTTP read/write fallback when DSH core is accessed over network.
4
+
5
+ import { isTrustedLocalRequest } from './security.js'
6
+ import { publicConfig, plainConfig, Config } from './index.js'
7
+
8
+ export function registerSettingsRoutes(ctx, { live, getSettingsApi, setLiveConfig }) {
9
+ // 1. GET /dsh-image-gen/status
10
+ ctx.effect(() => ctx.webServer.register({
11
+ kind: 'exact',
12
+ path: '/dsh-image-gen/status',
13
+ handler: async (req, res) => {
14
+ if (req.method !== 'GET') {
15
+ res.writeHead(405, { 'Content-Type': 'application/json' })
16
+ res.end(JSON.stringify({ error: 'GET only' }))
17
+ return
18
+ }
19
+ if (!isTrustedLocalRequest(req)) {
20
+ res.writeHead(403, { 'Content-Type': 'application/json' })
21
+ res.end(JSON.stringify({ error: 'forbidden' }))
22
+ return
23
+ }
24
+ const cfg = publicConfig(live())
25
+ res.writeHead(200, { 'Content-Type': 'application/json' })
26
+ res.end(JSON.stringify({
27
+ ok: true,
28
+ provider: cfg.provider,
29
+ model: cfg.model,
30
+ enabled: cfg.enabled,
31
+ config: cfg,
32
+ }))
33
+ },
34
+ }), 'dsh-image-gen: status route')
35
+
36
+ // 2. GET & PUT /dsh-image-gen/config
37
+ ctx.effect(() => ctx.webServer.register({
38
+ kind: 'exact',
39
+ path: '/dsh-image-gen/config',
40
+ handler: async (req, res) => {
41
+ if (req.method === 'GET') {
42
+ if (!isTrustedLocalRequest(req)) {
43
+ res.writeHead(403, { 'Content-Type': 'application/json' })
44
+ res.end(JSON.stringify({ error: 'forbidden' }))
45
+ return
46
+ }
47
+ res.writeHead(200, { 'Content-Type': 'application/json' })
48
+ res.end(JSON.stringify({ ok: true, config: publicConfig(live()) }))
49
+ return
50
+ }
51
+
52
+ if (req.method !== 'PUT') {
53
+ res.writeHead(405, { 'Content-Type': 'application/json' })
54
+ res.end(JSON.stringify({ error: 'GET or PUT only' }))
55
+ return
56
+ }
57
+
58
+ if (!isTrustedLocalRequest(req)) {
59
+ res.writeHead(403, { 'Content-Type': 'application/json' })
60
+ res.end(JSON.stringify({ error: 'forbidden' }))
61
+ return
62
+ }
63
+
64
+ const chunks = []
65
+ req.on('data', (c) => chunks.push(c))
66
+ req.on('end', async () => {
67
+ let payload
68
+ try {
69
+ payload = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')
70
+ } catch {
71
+ res.writeHead(400, { 'Content-Type': 'application/json' })
72
+ res.end(JSON.stringify({ ok: false, error: 'invalid JSON' }))
73
+ return
74
+ }
75
+
76
+ const raw = payload && typeof payload.config === 'object' && payload.config !== null ? payload.config : payload
77
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
78
+ res.writeHead(400, { 'Content-Type': 'application/json' })
79
+ res.end(JSON.stringify({ ok: false, error: 'body must be a JSON object' }))
80
+ return
81
+ }
82
+
83
+ const allowedKeys = new Set(Object.keys(Config.dict || {}))
84
+ for (const k of Object.keys(raw)) {
85
+ if (!allowedKeys.has(k)) {
86
+ res.writeHead(400, { 'Content-Type': 'application/json' })
87
+ res.end(JSON.stringify({ ok: false, error: `unknown config field: ${k}` }))
88
+ return
89
+ }
90
+ }
91
+
92
+ try {
93
+ const base = plainConfig(live()) || {}
94
+ const merged = Config({ ...base, ...raw })
95
+ const settingsApi = getSettingsApi ? getSettingsApi() : null
96
+ if (settingsApi && typeof settingsApi.replace === 'function') {
97
+ await settingsApi.replace(merged)
98
+ } else if (settingsApi && typeof settingsApi.update === 'function') {
99
+ await settingsApi.update(raw)
100
+ } else if (typeof setLiveConfig === 'function') {
101
+ setLiveConfig(merged)
102
+ }
103
+ res.writeHead(200, { 'Content-Type': 'application/json' })
104
+ res.end(JSON.stringify({ ok: true, config: publicConfig(merged) }))
105
+ } catch (err) {
106
+ res.writeHead(400, { 'Content-Type': 'application/json' })
107
+ res.end(JSON.stringify({ ok: false, error: String(err?.message || err) }))
108
+ }
109
+ })
110
+ },
111
+ }), 'dsh-image-gen: config route')
112
+ }
@@ -0,0 +1,83 @@
1
+ // theme-pair-helpers.js — helper functions for generate_theme_pair (#189)
2
+
3
+ export const THEME_STYLES = [
4
+ 'minimalist',
5
+ 'isometric',
6
+ 'flat',
7
+ '3d_render',
8
+ 'lineart',
9
+ 'cyberpunk',
10
+ 'claymorphism',
11
+ 'glassmorphism',
12
+ ]
13
+
14
+ /**
15
+ * Synthesizes prompt directives for light and dark theme variants.
16
+ * @param {object} opts
17
+ * @param {string} opts.prompt - Core user prompt
18
+ * @param {string} [opts.style] - Aesthetic style
19
+ * @param {number} [opts.darkContrastBoost=1.2] - Contrast boost multiplier for dark theme
20
+ * @returns {{ lightPrompt: string, darkPrompt: string }}
21
+ */
22
+ export function buildThemePairPrompts({ prompt, style, darkContrastBoost = 1.2 }) {
23
+ const cleanPrompt = String(prompt || '').trim()
24
+ const styleDirective = style ? `, in ${style} aesthetic style` : ''
25
+
26
+ const lightDirectives = [
27
+ 'light mode color palette',
28
+ 'clean pure white background (#ffffff)',
29
+ 'soft ambient occlusion shadows',
30
+ 'balanced crisp natural daylight illumination',
31
+ 'subtle pastel secondary accents',
32
+ 'high daytime readability',
33
+ ].join(', ')
34
+
35
+ const contrastNotes = (darkContrastBoost && darkContrastBoost > 1.0)
36
+ ? `enhanced ${Math.round(darkContrastBoost * 100)}% dynamic range highlights, luminous neon edge glow`
37
+ : 'luminous subtle edge glow'
38
+
39
+ const darkDirectives = [
40
+ 'dark mode color palette',
41
+ 'deep dark slate or obsidian background (#0f172a, #18181b)',
42
+ contrastNotes,
43
+ 'vibrant high-contrast focal points',
44
+ 'sleek night UI aesthetics',
45
+ 'eye-comfort calibrated dark surface tones',
46
+ ].join(', ')
47
+
48
+ const lightPrompt = `${cleanPrompt}${styleDirective}, ${lightDirectives}`
49
+ const darkPrompt = `${cleanPrompt}${styleDirective}, ${darkDirectives}`
50
+
51
+ return { lightPrompt, darkPrompt }
52
+ }
53
+
54
+ /**
55
+ * Builds responsive HTML/CSS snippets for adaptive theme switching.
56
+ * @param {object} opts
57
+ * @param {string} opts.lightSrc - Light image path or url
58
+ * @param {string} opts.darkSrc - Dark image path or url
59
+ * @param {string} [opts.alt] - Alt text
60
+ * @returns {{ html: string, css: string }}
61
+ */
62
+ export function buildThemePairSnippet({ lightSrc, darkSrc, alt = 'Theme adaptive graphic' }) {
63
+ const html = `<picture class="dsh-theme-pair">
64
+ <source srcset="${darkSrc}" media="(prefers-color-scheme: dark)">
65
+ <img src="${lightSrc}" alt="${alt}" loading="lazy">
66
+ </picture>`
67
+
68
+ const css = `/* CSS media-query & class-based dark theme switching */
69
+ .dsh-theme-pair img {
70
+ max-width: 100%;
71
+ height: auto;
72
+ display: block;
73
+ border-radius: 8px;
74
+ transition: opacity 0.3s ease;
75
+ }
76
+
77
+ /* Optional manual class override: <body class="dark"> or <html class="dark"> */
78
+ :is(.dark, [data-theme="dark"]) .dsh-theme-pair img {
79
+ content: url("${darkSrc}");
80
+ }`
81
+
82
+ return { html, css }
83
+ }
@@ -4,7 +4,6 @@
4
4
  import { defineTool } from '@deepseek-ai/dsh-tools'
5
5
  import {
6
6
  setSessionAnchor,
7
- getSessionAnchor,
8
7
  clearSessionAnchor,
9
8
  normalizeAnchorStrength,
10
9
  } from '../anchor-helpers.js'
@@ -102,7 +101,7 @@ export function registerAnchorTools(ctx, deps) {
102
101
  if (resolveSource && (imageRef.startsWith('sha256:') || !imageRef.startsWith('http'))) {
103
102
  try {
104
103
  await resolveSource(ctx, exec, imageRef)
105
- } catch (err) {
104
+ } catch {
106
105
  // Best effort check; non-blocking if remote URL or future attachment
107
106
  }
108
107
  }
@@ -3,49 +3,17 @@ import { renderToolOutput } from "../attachment-helper.js"
3
3
 
4
4
  import { defineTool } from '@deepseek-ai/dsh-tools'
5
5
  import { polishPrompt } from '../prompt-polisher.js'
6
- import path from 'node:path'
7
- import { mkdir, writeFile } from 'node:fs/promises'
8
6
  import {
9
- computeGenerationHash,
10
- IMAGE_SIZES,
11
- OUTPUT_FORMATS,
12
- PROVIDER_KEYS,
13
- buildSidecar,
14
7
  makeProviders,
15
- ASPECT_RATIOS,
16
- pixelDiff,
17
8
  normalizeCount,
18
- tryGenerate,
19
- fallbackOrder,
20
- embedPngMetadata,
21
- removeBackgroundFal,
22
- upscaleImageFal,
23
- traceToSvg,
24
- estimateCost,
25
- estimateSharpnessAndVariance,
26
- applyStylePreset,
27
- resolveStylePreset,
28
- blendImagesFal,
29
9
  toLosslessJson,
30
- saveAttachmentSafe,
31
10
  editImageDirect,
32
11
  varyImageDirect,
33
12
  } from '../providers.js'
34
- import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from '../resolve-image.js'
35
- import { sanitizeNegativePrompt, supportsNegativePrompt } from '../negative-sanitizer.js'
36
- import { executeWithQualityGate } from '../quality-gate.js'
37
- import { calculateGenerationCost, recordSpend, assertBudgetAvailable } from '../cost-meter.js'
13
+ import { resolveConversationImage, analyzeImageWithVision } from '../resolve-image.js'
14
+ import { calculateGenerationCost, assertBudgetAvailable } from '../cost-meter.js'
38
15
  import { trackAndAssertLoopGuard } from '../loop-guard.js'
39
16
  import { sanitizeErrorAndLogs } from '../security.js'
40
- import { getCachedGeneration, setCachedGeneration } from '../generation-cache.js'
41
- import {
42
- extractDesignTokens,
43
- generateCssGradient,
44
- checkWcagContrast,
45
- optimizeSvgContent,
46
- generatePwaIconSuite,
47
- extractSampleColorsFromBuffer,
48
- } from '../frontend-assets.js'
49
17
 
50
18
 
51
19
  export function registerEditingTools(ctx, deps) {
@@ -2,51 +2,18 @@ import { renderToolOutput } from "../attachment-helper.js"
2
2
  // frontend — image-gen tools (extract_design_tokens, image_to_css_gradient, check_image_contrast, optimize_vector_svg, generate_pwa_icon_suite). Extracted from apply() (#216).
3
3
 
4
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
- computeGenerationHash,
9
- IMAGE_SIZES,
10
- tryGenerate,
11
- fallbackOrder,
12
- embedPngMetadata,
13
- removeBackgroundFal,
14
- upscaleImageFal,
15
- traceToSvg,
16
- estimateCost,
17
- estimateSharpnessAndVariance,
18
- applyStylePreset,
19
- resolveStylePreset,
20
- blendImagesFal,
21
- toLosslessJson,
22
- saveAttachmentSafe,
23
- } from '../providers.js'
24
- import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from '../resolve-image.js'
25
- import { sanitizeNegativePrompt, supportsNegativePrompt } from '../negative-sanitizer.js'
26
- import { executeWithQualityGate } from '../quality-gate.js'
27
- import { calculateGenerationCost, recordSpend, assertBudgetAvailable } from '../cost-meter.js'
28
- import { trackAndAssertLoopGuard } from '../loop-guard.js'
5
+ import { toLosslessJson } from '../providers.js'
29
6
  import { sanitizeErrorAndLogs } from '../security.js'
30
- import { getCachedGeneration, setCachedGeneration } from '../generation-cache.js'
31
7
  import {
32
8
  extractDesignTokens,
33
9
  generateCssGradient,
34
10
  checkWcagContrast,
35
11
  optimizeSvgContent,
36
12
  generatePwaIconSuite,
37
- extractSampleColorsFromBuffer,
38
13
  } from '../frontend-assets.js'
39
14
 
40
-
41
15
  export function registerFrontendTools(ctx, deps) {
42
- const {
43
- config,
44
- live,
45
- saveAndAttachResult,
46
- resolveSource,
47
- slugify,
48
- resolveApiKey,
49
- } = deps
16
+ const { resolveSource } = deps
50
17
  ctx.effect(() => {
51
18
  ctx.tools.register(
52
19
  defineTool({
@@ -4,8 +4,7 @@ import { toLosslessJson } from '../providers.js'
4
4
  import { slugify } from '../index.js'
5
5
 
6
6
  export function registerGenerationPackTool(ctx, deps) {
7
- const { live } = deps
8
-
7
+
9
8
  ctx.effect(() => {
10
9
  ctx.tools.register(
11
10
  defineTool({
@@ -30,8 +29,7 @@ export function registerGenerationPackTool(ctx, deps) {
30
29
  },
31
30
  async execute(args, exec) {
32
31
  const ratios = Array.isArray(args.aspect_ratios) && args.aspect_ratios.length ? args.aspect_ratios : ['1:1', '16:9', '9:16']
33
- const cfg = live()
34
- const generateTool = ctx.tools.get('generate_image')
32
+ const generateTool = ctx.tools.get('generate_image')
35
33
  const results = []
36
34
  const warnings = []
37
35
  const seedBase = Math.floor(Math.random() * 100000)
@@ -1,4 +1,3 @@
1
- import { polishPrompt } from '../prompt-polisher.js'
2
1
  import { registerGenerationPackTool } from './generation-pack.js'
3
2
 
4
3
  /** Run an array of async task functions with a concurrency cap */
@@ -29,46 +28,26 @@ import {
29
28
  buildSidecar,
30
29
  makeProviders,
31
30
  ASPECT_RATIOS,
32
- pixelDiff,
33
31
  normalizeCount,
34
- tryGenerate,
35
- fallbackOrder,
36
32
  embedPngMetadata,
37
- removeBackgroundFal,
38
- upscaleImageFal,
39
- traceToSvg,
40
- estimateCost,
41
- estimateSharpnessAndVariance,
42
- applyStylePreset,
43
33
  resolveStylePreset,
44
- blendImagesFal,
45
34
  toLosslessJson,
46
35
  saveAttachmentSafe,
47
36
  } from '../providers.js'
48
- import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from '../resolve-image.js'
37
+ import { buildDualOutputMarkdown } from '../resolve-image.js'
49
38
  import { sanitizeNegativePrompt, supportsNegativePrompt } from '../negative-sanitizer.js'
50
39
  import { executeWithQualityGate } from '../quality-gate.js'
51
40
  import { calculateGenerationCost, recordSpend, assertBudgetAvailable } from '../cost-meter.js'
52
41
  import { trackAndAssertLoopGuard } from '../loop-guard.js'
53
- import { sanitizeErrorAndLogs } from '../security.js'
54
42
  import { getCachedGeneration, setCachedGeneration } from '../generation-cache.js'
55
43
  import { resolveFallbackChain, executeWithFallback } from '../fallback-router.js'
56
44
  import { getSessionAnchor, applyAnchorPromptHints } from '../anchor-helpers.js'
57
- import {
58
- extractDesignTokens,
59
- generateCssGradient,
60
- checkWcagContrast,
61
- optimizeSvgContent,
62
- generatePwaIconSuite,
63
- extractSampleColorsFromBuffer,
64
- } from '../frontend-assets.js'
65
45
 
66
46
 
67
47
  export function registerGenerationTools(ctx, deps) {
68
48
  const {
69
49
  config,
70
50
  live,
71
- saveAndAttachResult,
72
51
  resolveSource,
73
52
  slugify,
74
53
  resolveApiKey,
@@ -269,7 +248,7 @@ export function registerGenerationTools(ctx, deps) {
269
248
  // Subscriptions service is optional: without it, subscription providers
270
249
  // gracefully decline while other providers continue functioning.
271
250
  let subscriptionImages
272
- try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch (noService) { subscriptionImages = undefined }
251
+ try { subscriptionImages = ctx.get && ctx.get('subscriptionImages') } catch { subscriptionImages = undefined }
273
252
 
274
253
  const source = args.source_image ? await resolveSource(ctx, exec, args.source_image) : undefined
275
254
  const mask = args.mask ? await resolveSource(ctx, exec, args.mask) : undefined
@@ -2,52 +2,13 @@ import { renderToolOutput } from "../attachment-helper.js"
2
2
  // inspect — image-gen tools (compare_images, inspect_image_quality). Extracted from apply() (#216).
3
3
 
4
4
  import { defineTool } from '@deepseek-ai/dsh-tools'
5
- import path from 'node:path'
6
- import { mkdir, writeFile } from 'node:fs/promises'
7
5
  import {
8
- computeGenerationHash,
9
- IMAGE_SIZES,
10
6
  pixelDiff,
11
- tryGenerate,
12
- fallbackOrder,
13
- embedPngMetadata,
14
- removeBackgroundFal,
15
- upscaleImageFal,
16
- traceToSvg,
17
- estimateCost,
18
7
  estimateSharpnessAndVariance,
19
- applyStylePreset,
20
- resolveStylePreset,
21
- blendImagesFal,
22
- toLosslessJson,
23
- saveAttachmentSafe,
24
8
  } from '../providers.js'
25
- import { resolveConversationImage, analyzeImageWithVision, buildDualOutputMarkdown } from '../resolve-image.js'
26
- import { sanitizeNegativePrompt, supportsNegativePrompt } from '../negative-sanitizer.js'
27
- import { executeWithQualityGate } from '../quality-gate.js'
28
- import { calculateGenerationCost, recordSpend, assertBudgetAvailable } from '../cost-meter.js'
29
- import { trackAndAssertLoopGuard } from '../loop-guard.js'
30
- import { sanitizeErrorAndLogs } from '../security.js'
31
- import { getCachedGeneration, setCachedGeneration } from '../generation-cache.js'
32
- import {
33
- extractDesignTokens,
34
- generateCssGradient,
35
- checkWcagContrast,
36
- optimizeSvgContent,
37
- generatePwaIconSuite,
38
- extractSampleColorsFromBuffer,
39
- } from '../frontend-assets.js'
40
-
41
9
 
42
10
  export function registerInspectTools(ctx, deps) {
43
- const {
44
- config,
45
- live,
46
- saveAndAttachResult,
47
- resolveSource,
48
- slugify,
49
- resolveApiKey,
50
- } = deps
11
+ const { resolveSource } = deps
51
12
  ctx.effect(() => {
52
13
  ctx.tools.register(
53
14
  defineTool({
@@ -58,8 +19,8 @@ export function registerInspectTools(ctx, deps) {
58
19
  image_b: { type: 'string', required: true, description: 'Path or attachment id of the second image.' },
59
20
  },
60
21
  output: {
61
- render: (_args, value) => renderToolOutput(value),
62
- schema: {
22
+ render: (_args, value) => renderToolOutput(value),
23
+ schema: {
63
24
  type: 'object',
64
25
  additionalProperties: false,
65
26
  properties: {
@@ -86,8 +47,8 @@ export function registerInspectTools(ctx, deps) {
86
47
  expected_elements: { type: 'string', description: 'What elements should be present and verified.' },
87
48
  },
88
49
  output: {
89
- render: (_args, value) => renderToolOutput(value),
90
- schema: {
50
+ render: (_args, value) => renderToolOutput(value),
51
+ schema: {
91
52
  type: 'object',
92
53
  additionalProperties: false,
93
54
  properties: {
@@ -109,6 +70,6 @@ export function registerInspectTools(ctx, deps) {
109
70
  }
110
71
  },
111
72
  }),
112
- )
73
+ )
113
74
  }, 'dsh-image-gen: tool inspect_image_quality')
114
75
  }
@@ -11,6 +11,8 @@ import {
11
11
  buildSeamlessPrompt,
12
12
  buildPatternCss,
13
13
  normalizeDensity,
14
+ scoreEdgeWrap,
15
+ tilePixels,
14
16
  } from '../pattern-helpers.js'
15
17
 
16
18
  export function registerPatternTools(ctx, deps) {
@@ -97,6 +99,14 @@ export function registerPatternTools(ctx, deps) {
97
99
  const filePath = path.join(outDir, name)
98
100
  await writeFile(filePath, gen.bytes)
99
101
 
102
+ if (gen.bytes && gen.width && gen.height) {
103
+ const seamScore = scoreEdgeWrap(new Uint8Array(gen.bytes), gen.width, gen.height)
104
+ const tiled = tilePixels(new Uint8Array(gen.bytes), gen.width, gen.height, 4, 2, 2)
105
+ if (exec?.logger?.debug) {
106
+ exec.logger.debug(`[dsh-image-gen] Pattern seam score: ${seamScore}, 2x2 width: ${tiled.width}`)
107
+ }
108
+ }
109
+
100
110
  const [tilePx] = sizeToPixels(size)
101
111
  const css = buildPatternCss({ className: stem, sizePx: tilePx, imagePath: `./${name}` })
102
112
  const cssPath = path.join(outDir, `${stem}.css`)