@goodandready/dsh-image-gen 0.11.2 → 0.11.4

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.
@@ -81,10 +81,15 @@ export async function saveAndAttachResult(ctx, exec, cfg, {
81
81
  })
82
82
  }
83
83
 
84
+ /** Raster media types safe for multimodal LLM context (DeepSeek, OpenAI, Claude, Gemini). */
85
+ const SAFE_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
86
+
84
87
  export function renderToolOutput(value) {
85
88
  const text = (value && value.summary) || (typeof value === "string" ? value : JSON.stringify(value || ""))
86
89
  const blocks = [{ type: "text", text }]
87
- if (value && value.attachment) {
90
+ if (value && value.attachment
91
+ && value.attachment.attachmentId
92
+ && SAFE_IMAGE_TYPES.has(value.attachment.mediaType)) {
88
93
  blocks.push({ type: "image", attachment: value.attachment })
89
94
  }
90
95
  return blocks
package/lib/index.js CHANGED
@@ -409,57 +409,43 @@ function migrateLegacySettings(sctx, scope) {
409
409
  }
410
410
  }
411
411
 
412
- /** Collect text chunks from llm.stream iterator. */
413
- export async function collectText(iterable) {
414
- let out = ''
415
- let sawDelta = false
416
- for await (const chunk of iterable) {
417
- if (chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string') {
418
- out += chunk.text
419
- sawDelta = true
420
- } else if (
421
- !sawDelta && chunk && chunk.type === 'block-end'
422
- && chunk.block && chunk.block.type === 'text' && typeof chunk.block.text === 'string'
423
- ) {
424
- out += chunk.block.text
412
+ /** Collect text chunks from llm.stream iterator.
413
+ * Canonical implementation lives in prompt-enhancer.js; re-exported for backwards compat. */
414
+ export { collectText } from './prompt-enhancer.js'
415
+
416
+
417
+ /** Expand short prompt via chat model; return original prompt on error.
418
+ * Canonical implementation lives in prompt-enhancer.js; re-exported for backwards compat. */
419
+ export { buildEnhancePromptSystemMessage } from './prompt-enhancer.js'
420
+
421
+
422
+ /** Expand short prompt via chat model; return original prompt on error.
423
+ * Canonical implementation lives in prompt-enhancer.js; re-exported for backwards compat. */
424
+ export { enhancePrompt } from './prompt-enhancer.js'
425
+
426
+
427
+ function sanitizeRaw(raw) {
428
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {}
429
+ const cleaned = {}
430
+ for (const [k, v] of Object.entries(raw)) {
431
+ if (v && typeof v === "object" && !Array.isArray(v) && typeof v.get !== "function" && Object.keys(v).length === 0) {
432
+ continue
425
433
  }
434
+ cleaned[k] = v
426
435
  }
427
- return out.trim()
436
+ return cleaned
428
437
  }
429
438
 
430
- /** Expand short prompt via chat model; return original prompt on error. */
431
- export function buildEnhancePromptSystemMessage(provider, model) {
432
- const isFlux = String(model || '').toLowerCase().includes('flux') || provider === 'fal'
433
- if (isFlux) {
434
- return 'You are an expert prompt engineer for FLUX image models. Expand the user prompt into a rich, natural descriptive English paragraph describing subject details, composition, lighting, camera angle, and atmosphere. Reply with ONLY the expanded prompt, no commentary, no markdown quotes.'
435
- }
436
- return 'You are an expert prompt engineer for Stable Diffusion models. Expand the user prompt into detailed comma-separated descriptive visual tags including subject, composition, studio lighting, materials, and artistic medium. Reply with ONLY the expanded prompt, no commentary.'
439
+ export function ensureConfig(raw) {
440
+ const cleaned = sanitizeRaw(raw)
441
+ const plain = plainConfig(cleaned)
442
+ return Config(plain)
437
443
  }
438
444
 
439
- /** Expand short prompt via chat model; return original prompt on error. */
440
- export async function enhancePrompt(ctx, cfg, prompt, signal, provider) {
441
- if (!cfg.enhancePrompt) return { prompt, enhanced: false }
442
- if (String(prompt).length >= (cfg.enhanceBelowChars || 200)) return { prompt, enhanced: false }
443
- try {
444
- const sysMsg = buildEnhancePromptSystemMessage(provider || cfg.provider, cfg.enhanceModel || cfg.model)
445
- const chunks = ctx.llm.stream({
446
- ...(signal ? { signal } : {}),
447
- ...(cfg.enhanceModel ? { model: cfg.enhanceModel } : {}),
448
- messages: [
449
- { role: 'system', content: sysMsg },
450
- { role: 'user', content: prompt },
451
- ],
452
- })
453
- const text = await collectText(chunks)
454
- if (!text) return { prompt, enhanced: false }
455
- return { prompt: text, enhanced: true }
456
- } catch (_) {
457
- return { prompt, enhanced: false }
458
- }
459
- }
460
445
 
461
446
  export function apply(ctx, rawConfig) {
462
- const config = Config(rawConfig ?? {})
447
+ const config = ensureConfig(rawConfig)
448
+ const baseConfig = plainConfig(config)
463
449
  if (ctx.systemPrompt && typeof ctx.systemPrompt.section === 'function') {
464
450
  ctx.systemPrompt.section({
465
451
  name: 'tool:image-generation',
@@ -474,7 +460,7 @@ export function apply(ctx, rawConfig) {
474
460
  let settingsApi
475
461
  let currentLiveConfig = null
476
462
  let getConfig = () => currentLiveConfig || config
477
- const live = () => Config(structuredClone(plainConfig(getConfig() ?? {}))) ?? config
463
+ const live = () => plainConfig(getConfig() ?? {})
478
464
 
479
465
  const createSettingsAdapter = (svc) => {
480
466
  if (!svc) return undefined
@@ -691,7 +677,7 @@ export function apply(ctx, rawConfig) {
691
677
 
692
678
  // Tool registrations live in register-tools.js; each tool is a labeled ctx.effect (#216).
693
679
  registerAllTools(ctx, {
694
- config,
680
+ config: baseConfig,
695
681
  live,
696
682
  saveAndAttachResult,
697
683
  resolveSource,
@@ -0,0 +1,51 @@
1
+ // prompt-enhancer.js — Prompt enhancement via LLM (#4 fix)
2
+ // Extracted from index.js to avoid circular dependency with tools/generation.js
3
+
4
+ /** Collect text chunks from llm.stream iterator. */
5
+ export async function collectText(iterable) {
6
+ let out = ''
7
+ let sawDelta = false
8
+ for await (const chunk of iterable) {
9
+ if (chunk && chunk.type === 'text-delta' && typeof chunk.text === 'string') {
10
+ out += chunk.text
11
+ sawDelta = true
12
+ } else if (
13
+ !sawDelta && chunk && chunk.type === 'block-end'
14
+ && chunk.block && chunk.block.type === 'text' && typeof chunk.block.text === 'string'
15
+ ) {
16
+ out += chunk.block.text
17
+ }
18
+ }
19
+ return out.trim()
20
+ }
21
+
22
+ /** Build system message for prompt enhancement. */
23
+ export function buildEnhancePromptSystemMessage(provider, model) {
24
+ const isFlux = String(model || '').toLowerCase().includes('flux') || provider === 'fal'
25
+ if (isFlux) {
26
+ return 'You are an expert prompt engineer for FLUX image models. Expand the user prompt into a rich, natural descriptive English paragraph describing subject details, composition, lighting, camera angle, and atmosphere. Reply with ONLY the expanded prompt, no commentary, no markdown quotes.'
27
+ }
28
+ return 'You are an expert prompt engineer for Stable Diffusion models. Expand the user prompt into detailed comma-separated descriptive visual tags including subject, composition, studio lighting, materials, and artistic medium. Reply with ONLY the expanded prompt, no commentary.'
29
+ }
30
+
31
+ /** Expand short prompt via chat model; return original prompt on error. */
32
+ export async function enhancePrompt(ctx, cfg, prompt, signal, provider) {
33
+ if (!cfg.enhancePrompt) return { prompt, enhanced: false }
34
+ if (String(prompt).length >= (cfg.enhanceBelowChars || 200)) return { prompt, enhanced: false }
35
+ try {
36
+ const sysMsg = buildEnhancePromptSystemMessage(provider || cfg.provider, cfg.enhanceModel || cfg.model)
37
+ const chunks = ctx.llm.stream({
38
+ ...(signal ? { signal } : {}),
39
+ ...(cfg.enhanceModel ? { model: cfg.enhanceModel } : {}),
40
+ messages: [
41
+ { role: 'system', content: sysMsg },
42
+ { role: 'user', content: prompt },
43
+ ],
44
+ })
45
+ const text = await collectText(chunks)
46
+ if (!text) return { prompt, enhanced: false }
47
+ return { prompt: text, enhanced: true }
48
+ } catch (_) {
49
+ return { prompt, enhanced: false }
50
+ }
51
+ }
package/lib/providers.js CHANGED
@@ -128,9 +128,16 @@ export async function pixelDiff(a, b) {
128
128
 
129
129
  /** Extract detailed ComfyUI node error messages from /history/{pid} response. */
130
130
 
131
- /** Safe attachment persistence with fallback when ctx.attachments is unavailable. */
131
+ /** Raster media types that DSH attachment store (Sharp) can process. */
132
+ const RASTER_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/webp', 'image/gif'])
133
+
134
+ /** Safe attachment persistence with fallback when ctx.attachments is unavailable.
135
+ * SVG and other non-raster types are never sent to saveImage (Sharp rejects them)
136
+ * and never emitted as image content blocks (LLMs reject them). */
132
137
  export async function saveAttachmentSafe(ctx, { bytes, mediaType, name }) {
133
- if (ctx && ctx.attachments && typeof ctx.attachments.saveImage === 'function') {
138
+ // Only attempt saveImage for raster formats that Sharp/LLMs can handle
139
+ if (RASTER_IMAGE_TYPES.has(mediaType)
140
+ && ctx && ctx.attachments && typeof ctx.attachments.saveImage === 'function') {
134
141
  try {
135
142
  const att = await ctx.attachments.saveImage({
136
143
  data: new Uint8Array(bytes),
@@ -198,16 +198,12 @@ export function registerProcessingTools(ctx, deps) {
198
198
  const filePath = path.join(outDir, name)
199
199
  await writeFile(filePath, res.bytes)
200
200
 
201
- let attachment = null
202
- let localUrl = ''
203
- if (ctx.attachments?.saveImage) {
204
- attachment = await ctx.attachments.saveImage({
205
- data: new Uint8Array(res.bytes),
206
- mediaType: 'image/svg+xml',
207
- name,
208
- })
209
- localUrl = '/dsh-image-gen/image?id=' + encodeURIComponent(attachment.attachmentId)
210
- }
201
+ // SVG is not a raster format — do not create image attachment (#5)
202
+ // saveAttachmentSafe already guards against SVG, but we skip entirely
203
+ // to avoid even a fallback stub that could confuse renderToolOutput
204
+ const attachment = null
205
+ const localUrl = ''
206
+
211
207
 
212
208
  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`
213
209
  return toLosslessJson({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
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",