@goodandready/dsh-image-gen 0.10.3 → 0.10.5
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 +12 -0
- package/README.zh.md +8 -0
- package/lib/client.js +57 -30
- package/lib/frontend-assets.js +436 -0
- package/lib/index.js +566 -3
- package/lib/providers.js +50 -2
- package/lib/resolve-image.js +243 -0
- package/package.json +5 -1
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { estimateSharpnessAndVariance } from './providers.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scan active session messages in reverse order to find the latest image attachment or file ref.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} exec - Tool execution context provided by DSH
|
|
9
|
+
* @returns {{ ref: string, name?: string } | undefined}
|
|
10
|
+
*/
|
|
11
|
+
export function findLastConversationImage(exec) {
|
|
12
|
+
if (!exec || !exec.agent || !exec.agent.session) return undefined
|
|
13
|
+
const session = exec.agent.session
|
|
14
|
+
const messages = typeof session.deriveMessages === 'function'
|
|
15
|
+
? session.deriveMessages()
|
|
16
|
+
: Array.isArray(session.messages) ? session.messages : []
|
|
17
|
+
|
|
18
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
19
|
+
const msg = messages[i]
|
|
20
|
+
if (!msg) continue
|
|
21
|
+
|
|
22
|
+
// 1. Check direct attachments array on message
|
|
23
|
+
if (Array.isArray(msg.attachments)) {
|
|
24
|
+
for (const att of msg.attachments) {
|
|
25
|
+
if (att && (att.mediaType?.startsWith('image/') || att.attachmentId?.startsWith('sha256:') || att.id?.startsWith('sha256:'))) {
|
|
26
|
+
return {
|
|
27
|
+
ref: att.attachmentId || att.id || att.ref,
|
|
28
|
+
name: att.name || att.filename || 'previous-image',
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 2. Check content blocks
|
|
35
|
+
if (Array.isArray(msg.content)) {
|
|
36
|
+
for (const block of msg.content) {
|
|
37
|
+
if (!block) continue
|
|
38
|
+
if (block.type === 'image' && block.source) {
|
|
39
|
+
const ref = block.source.attachmentId || block.source.id || block.source.data || block.source.url
|
|
40
|
+
if (ref) return { ref, name: 'conversation-image' }
|
|
41
|
+
}
|
|
42
|
+
if (block.type === 'tool_result' && block.content) {
|
|
43
|
+
// Look for file path or attachment reference in tool results
|
|
44
|
+
const str = typeof block.content === 'string' ? block.content : JSON.stringify(block.content)
|
|
45
|
+
const shaMatch = str.match(/sha256:[a-f0-9]{64}/i)
|
|
46
|
+
if (shaMatch) return { ref: shaMatch[0], name: 'tool-image' }
|
|
47
|
+
const fileMatch = str.match(/(?:File|Path|Saved to):\s*[`"']?([^\r\n`"']+\.(?:png|jpe?g|webp|svg))[`"']?/i)
|
|
48
|
+
if (fileMatch) return { ref: fileMatch[1].trim(), name: path.basename(fileMatch[1].trim()) }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 3. Check metadata fields
|
|
54
|
+
if (msg.metadata) {
|
|
55
|
+
const metaImg = msg.metadata.attachmentId || msg.metadata.image || msg.metadata.outputFile
|
|
56
|
+
if (metaImg && typeof metaImg === 'string') {
|
|
57
|
+
return { ref: metaImg, name: 'metadata-image' }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Determine MIME type by extension or signature.
|
|
67
|
+
*/
|
|
68
|
+
export function detectMediaType(filePath, buffer) {
|
|
69
|
+
if (buffer && buffer.length >= 8) {
|
|
70
|
+
// PNG signature: 89 50 4E 47 0D 0A 1A 0A
|
|
71
|
+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47) return 'image/png'
|
|
72
|
+
// JPEG signature: FF D8 FF
|
|
73
|
+
if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF) return 'image/jpeg'
|
|
74
|
+
// WebP signature: RIFF ... WEBP
|
|
75
|
+
if (buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46 &&
|
|
76
|
+
buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50) return 'image/webp'
|
|
77
|
+
// SVG xml/svg check
|
|
78
|
+
const head = buffer.slice(0, 100).toString('utf8').toLowerCase()
|
|
79
|
+
if (head.includes('<svg') || head.includes('<?xml')) return 'image/svg+xml'
|
|
80
|
+
}
|
|
81
|
+
const ext = path.extname(String(filePath || '')).toLowerCase()
|
|
82
|
+
if (ext === '.jpg' || ext === '.jpeg') return 'image/jpeg'
|
|
83
|
+
if (ext === '.webp') return 'image/webp'
|
|
84
|
+
if (ext === '.svg') return 'image/svg+xml'
|
|
85
|
+
return 'image/png'
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Resolve target image bytes and mediaType, automatically looking up session history if omitted.
|
|
90
|
+
*
|
|
91
|
+
* @param {object} ctx - Cordis context
|
|
92
|
+
* @param {object} exec - Tool execution context
|
|
93
|
+
* @param {string} [targetRef] - User provided path, attachment ID, or 'latest'
|
|
94
|
+
* @returns {Promise<{ bytes: Buffer, mediaType: string, ref: string, name: string }>}
|
|
95
|
+
*/
|
|
96
|
+
export async function resolveConversationImage(ctx, exec, targetRef) {
|
|
97
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd || process.cwd()
|
|
98
|
+
let effectiveRef = typeof targetRef === 'string' ? targetRef.trim() : ''
|
|
99
|
+
|
|
100
|
+
const isImplicit = !effectiveRef ||
|
|
101
|
+
effectiveRef.toLowerCase() === 'latest' ||
|
|
102
|
+
effectiveRef.toLowerCase() === 'last' ||
|
|
103
|
+
effectiveRef.toLowerCase() === 'previous' ||
|
|
104
|
+
effectiveRef.toLowerCase() === 'current'
|
|
105
|
+
|
|
106
|
+
if (isImplicit) {
|
|
107
|
+
const found = findLastConversationImage(exec)
|
|
108
|
+
if (!found || !found.ref) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
'No previous image found in the active conversation session. ' +
|
|
111
|
+
'Please provide an explicit image path or attachment ID.'
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
effectiveRef = found.ref
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 1. Attachment store resolution
|
|
118
|
+
if (effectiveRef.startsWith('sha256:')) {
|
|
119
|
+
if (!ctx?.attachments?.readImage) {
|
|
120
|
+
throw new Error(`Attachments service unavailable to read ${effectiveRef}`)
|
|
121
|
+
}
|
|
122
|
+
const stored = await ctx.attachments.readImage({
|
|
123
|
+
attachmentId: effectiveRef,
|
|
124
|
+
mediaType: 'image/png',
|
|
125
|
+
bytes: 0,
|
|
126
|
+
width: 0,
|
|
127
|
+
height: 0,
|
|
128
|
+
})
|
|
129
|
+
const bytes = Buffer.from(stored.data || stored.bytes || [])
|
|
130
|
+
return {
|
|
131
|
+
bytes,
|
|
132
|
+
mediaType: stored.ref?.mediaType || detectMediaType('', bytes),
|
|
133
|
+
ref: effectiveRef,
|
|
134
|
+
name: `attachment-${effectiveRef.slice(7, 15)}`,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 2. Data URI
|
|
139
|
+
if (effectiveRef.startsWith('data:')) {
|
|
140
|
+
const commaIndex = effectiveRef.indexOf(',')
|
|
141
|
+
if (commaIndex > 0) {
|
|
142
|
+
const meta = effectiveRef.slice(0, commaIndex)
|
|
143
|
+
const data = effectiveRef.slice(commaIndex + 1)
|
|
144
|
+
const mediaMatch = meta.match(/data:([^;]+)/)
|
|
145
|
+
const mediaType = mediaMatch ? mediaMatch[1] : 'image/png'
|
|
146
|
+
const bytes = Buffer.from(data, 'base64')
|
|
147
|
+
return {
|
|
148
|
+
bytes,
|
|
149
|
+
mediaType,
|
|
150
|
+
ref: effectiveRef.slice(0, 32) + '...',
|
|
151
|
+
name: 'inline-image',
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 3. Local filesystem resolution
|
|
157
|
+
const absPath = path.isAbsolute(effectiveRef)
|
|
158
|
+
? effectiveRef
|
|
159
|
+
: path.resolve(sessionCwd, effectiveRef)
|
|
160
|
+
|
|
161
|
+
const bytes = await readFile(absPath)
|
|
162
|
+
const mediaType = detectMediaType(absPath, bytes)
|
|
163
|
+
return {
|
|
164
|
+
bytes,
|
|
165
|
+
mediaType,
|
|
166
|
+
ref: effectiveRef,
|
|
167
|
+
name: path.basename(absPath, path.extname(absPath)),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Query @goodandready/dsh-vision-bridge or Cordis tools to extract structural scene cues.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} ctx - Cordis context
|
|
175
|
+
* @param {object} exec - Tool execution context
|
|
176
|
+
* @param {{ bytes: Buffer, mediaType: string, ref: string }} source - Source image
|
|
177
|
+
* @returns {Promise<{ available: boolean, summary: string, subjects?: string[], palette?: string }>}
|
|
178
|
+
*/
|
|
179
|
+
export async function analyzeImageWithVision(ctx, exec, source) {
|
|
180
|
+
if (!source || !source.bytes) {
|
|
181
|
+
return { available: false, summary: 'No image data to analyze' }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Check if dsh-vision-bridge tools exist in runtime
|
|
185
|
+
const inspectTool = ctx?.tools?.get?.('inspect_image') || ctx?.tools?.get?.('describe_image')
|
|
186
|
+
if (inspectTool && typeof inspectTool.execute === 'function') {
|
|
187
|
+
try {
|
|
188
|
+
const res = await inspectTool.execute(
|
|
189
|
+
{
|
|
190
|
+
source: source.ref,
|
|
191
|
+
attachmentIds: source.ref.startsWith('sha256:') ? [source.ref] : undefined,
|
|
192
|
+
paths: !source.ref.startsWith('sha256:') ? [source.ref] : undefined,
|
|
193
|
+
question: 'Summarize in 1 sentence: key subjects, visual style, and dominant colors for image editing.',
|
|
194
|
+
detail: 'low',
|
|
195
|
+
},
|
|
196
|
+
exec,
|
|
197
|
+
)
|
|
198
|
+
const text = res?.description || res?.summary || (typeof res === 'string' ? res : '')
|
|
199
|
+
if (text && text.trim()) {
|
|
200
|
+
return {
|
|
201
|
+
available: true,
|
|
202
|
+
summary: text.trim(),
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
} catch {
|
|
206
|
+
// Vision bridge execution failed or timed out; proceed with local fallback
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Local fallback: analyze sharpness and dimensions without external model call
|
|
211
|
+
const analysis = estimateSharpnessAndVariance(source.bytes)
|
|
212
|
+
const fallbackSummary = analysis.isBlank
|
|
213
|
+
? 'Low-contrast or uniform canvas'
|
|
214
|
+
: `Detected visual asset (${source.mediaType}, sharpness: ${analysis.score})`
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
available: false,
|
|
218
|
+
summary: fallbackSummary,
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Форматирует структурированный текстовый отчёт для безопасного возврата в LLM (#150 Dual-Output).
|
|
223
|
+
*/
|
|
224
|
+
export function buildDualOutputMarkdown({
|
|
225
|
+
action = 'generated',
|
|
226
|
+
filePath,
|
|
227
|
+
width,
|
|
228
|
+
height,
|
|
229
|
+
mediaType,
|
|
230
|
+
seed,
|
|
231
|
+
provider,
|
|
232
|
+
model,
|
|
233
|
+
cost,
|
|
234
|
+
attachmentId,
|
|
235
|
+
}) {
|
|
236
|
+
return `✅ **Image ${action} successfully**\n`
|
|
237
|
+
+ `- **File:** \`${filePath}\`\n`
|
|
238
|
+
+ `- **Dimensions:** ${width}x${height} (${mediaType || 'image/png'})\n`
|
|
239
|
+
+ `- **Seed:** ${seed ?? 'N/A'} | **Provider:** ${provider} (${model})\n`
|
|
240
|
+
+ (cost !== undefined ? `- **Cost:** $${Number(cost).toFixed(4)}\n` : '')
|
|
241
|
+
+ `- **Attachment ID:** \`${attachmentId}\`\n\n`
|
|
242
|
+
+ `*(Interactive visual card rendered in conversation UI)*`
|
|
243
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.5",
|
|
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",
|
|
@@ -59,6 +59,10 @@
|
|
|
59
59
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
60
60
|
"@deepseek-ai/dsh-attachment": "^0.1.0-rc.6",
|
|
61
61
|
"@deepseek-ai/dsh-credentials": "^0.1.0-rc.6",
|
|
62
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
63
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
64
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
65
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
62
66
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
63
67
|
},
|
|
64
68
|
"scripts": {
|