@goodandready/dsh-image-gen 0.10.34 → 0.11.0
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/lib/client.js +1020 -39
- package/lib/comfy-workflow-helpers.js +161 -0
- package/lib/index.js +4 -0
- package/lib/providers/backends/local.js +33 -11
- package/lib/register-tools.js +3 -1
- package/lib/theme-pair-helpers.js +83 -0
- package/lib/tools/theme-pair.js +251 -0
- package/lib/vault.js +183 -0
- package/package.json +1 -1
package/lib/vault.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { existsSync, unlinkSync } from 'node:fs'
|
|
2
|
+
import { readHistory, writeHistory } from './history.js'
|
|
3
|
+
import { isTrustedLocalRequest } from './security.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Match image dimensions against an aspect ratio label or ratio value.
|
|
7
|
+
*/
|
|
8
|
+
export function matchAspectRatio(width, height, targetAspect) {
|
|
9
|
+
if (!targetAspect || targetAspect === 'any' || targetAspect === 'all') return true
|
|
10
|
+
if (!width || !height) return false
|
|
11
|
+
const ratio = width / height
|
|
12
|
+
|
|
13
|
+
switch (targetAspect) {
|
|
14
|
+
case '1:1':
|
|
15
|
+
case 'square':
|
|
16
|
+
return Math.abs(ratio - 1.0) < 0.1
|
|
17
|
+
case '16:9':
|
|
18
|
+
case 'landscape':
|
|
19
|
+
return Math.abs(ratio - (16 / 9)) < 0.15
|
|
20
|
+
case '9:16':
|
|
21
|
+
case 'portrait':
|
|
22
|
+
return Math.abs(ratio - (9 / 16)) < 0.15
|
|
23
|
+
case '4:3':
|
|
24
|
+
return Math.abs(ratio - (4 / 3)) < 0.15
|
|
25
|
+
case '3:4':
|
|
26
|
+
return Math.abs(ratio - (3 / 4)) < 0.15
|
|
27
|
+
case '21:9':
|
|
28
|
+
case 'ultrawide':
|
|
29
|
+
return Math.abs(ratio - (21 / 9)) < 0.2
|
|
30
|
+
default:
|
|
31
|
+
return true
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Filter, sort, and paginate vault entries with path sanitization.
|
|
37
|
+
*/
|
|
38
|
+
export function filterVaultEntries(entries, options = {}, existsFn = existsSync) {
|
|
39
|
+
const {
|
|
40
|
+
q = '',
|
|
41
|
+
provider = '',
|
|
42
|
+
aspect = '',
|
|
43
|
+
sort = 'newest',
|
|
44
|
+
offset = 0,
|
|
45
|
+
limit = 30,
|
|
46
|
+
} = options
|
|
47
|
+
|
|
48
|
+
const safeOffset = Math.max(0, Number.parseInt(offset, 10) || 0)
|
|
49
|
+
const safeLimit = Math.min(100, Math.max(1, Number.parseInt(limit, 10) || 30))
|
|
50
|
+
const cleanQ = String(q).trim().toLowerCase()
|
|
51
|
+
const cleanProvider = String(provider).trim().toLowerCase()
|
|
52
|
+
|
|
53
|
+
const existing = (Array.isArray(entries) ? entries : []).filter((e) => e && e.path && existsFn(e.path))
|
|
54
|
+
|
|
55
|
+
const filtered = existing.filter((e) => {
|
|
56
|
+
if (cleanQ && !String(e.prompt || '').toLowerCase().includes(cleanQ)) {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
59
|
+
if (cleanProvider && String(e.provider || '').toLowerCase() !== cleanProvider) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
if (aspect && !matchAspectRatio(e.width, e.height, aspect)) {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
return true
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
filtered.sort((a, b) => {
|
|
69
|
+
const timeA = a.createdAt ? Date.parse(a.createdAt) : 0
|
|
70
|
+
const timeB = b.createdAt ? Date.parse(b.createdAt) : 0
|
|
71
|
+
if (sort === 'oldest') {
|
|
72
|
+
return timeA - timeB
|
|
73
|
+
}
|
|
74
|
+
return timeB - timeA
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const total = filtered.length
|
|
78
|
+
const sliced = filtered.slice(safeOffset, safeOffset + safeLimit)
|
|
79
|
+
|
|
80
|
+
const items = sliced.map((e) => {
|
|
81
|
+
const { path: _discardPath, ...safeEntry } = e
|
|
82
|
+
return {
|
|
83
|
+
id: e.id || e.attachmentId || String(e.createdAt || Math.random()),
|
|
84
|
+
...safeEntry,
|
|
85
|
+
thumbnailUrl: e.thumbnailUrl || (e.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(e.attachmentId)}` : ''),
|
|
86
|
+
url: e.attachmentId ? `/dsh-image-gen/image?id=${encodeURIComponent(e.attachmentId)}` : '',
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
items,
|
|
92
|
+
total,
|
|
93
|
+
offset: safeOffset,
|
|
94
|
+
limit: safeLimit,
|
|
95
|
+
hasMore: safeOffset + safeLimit < total,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Delete entry and associated disk files (image + sidecar json).
|
|
101
|
+
*/
|
|
102
|
+
export async function deleteVaultEntry(id) {
|
|
103
|
+
if (!id) return { success: false, error: 'Missing entry id' }
|
|
104
|
+
const entries = await readHistory()
|
|
105
|
+
const idx = entries.findIndex((e) => e.id === id || e.attachmentId === id)
|
|
106
|
+
if (idx === -1) {
|
|
107
|
+
return { success: false, error: 'Vault entry not found' }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const [entry] = entries.splice(idx, 1)
|
|
111
|
+
if (entry && entry.path) {
|
|
112
|
+
try {
|
|
113
|
+
unlinkSync(entry.path)
|
|
114
|
+
} catch (_) { /* ignore missing file */ }
|
|
115
|
+
try {
|
|
116
|
+
unlinkSync(entry.path.replace(/\.[^.]+$/, '.json'))
|
|
117
|
+
} catch (_) { /* ignore missing sidecar */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await writeHistory(entries)
|
|
121
|
+
return { success: true, id }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Register vault HTTP routes on DSH webServer.
|
|
126
|
+
*/
|
|
127
|
+
export function registerVaultRoutes(ctx) {
|
|
128
|
+
ctx.effect(() => ctx.webServer.register({
|
|
129
|
+
kind: 'exact',
|
|
130
|
+
path: '/dsh-image-gen/vault',
|
|
131
|
+
handler: async (req, res) => {
|
|
132
|
+
if (!isTrustedLocalRequest(req)) {
|
|
133
|
+
res.writeHead(403, { 'Content-Type': 'application/json' })
|
|
134
|
+
res.end(JSON.stringify({ error: 'forbidden' }))
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (req.method === 'GET') {
|
|
139
|
+
const u = new URL(req.url, 'http://localhost')
|
|
140
|
+
const q = u.searchParams.get('q') || ''
|
|
141
|
+
const provider = u.searchParams.get('provider') || ''
|
|
142
|
+
const aspect = u.searchParams.get('aspect') || ''
|
|
143
|
+
const sort = u.searchParams.get('sort') || 'newest'
|
|
144
|
+
const offset = u.searchParams.get('offset') || '0'
|
|
145
|
+
const limit = u.searchParams.get('limit') || '30'
|
|
146
|
+
|
|
147
|
+
const entries = await readHistory()
|
|
148
|
+
const result = filterVaultEntries(entries, { q, provider, aspect, sort, offset, limit })
|
|
149
|
+
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
150
|
+
res.end(JSON.stringify({ ok: true, ...result }))
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (req.method === 'DELETE') {
|
|
155
|
+
const u = new URL(req.url, 'http://localhost')
|
|
156
|
+
let id = u.searchParams.get('id')
|
|
157
|
+
|
|
158
|
+
if (!id) {
|
|
159
|
+
try {
|
|
160
|
+
const chunks = []
|
|
161
|
+
for await (const chunk of req) chunks.push(chunk)
|
|
162
|
+
const body = JSON.parse(Buffer.concat(chunks).toString('utf-8'))
|
|
163
|
+
id = body.id
|
|
164
|
+
} catch (_) { /* body parse error */ }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!id) {
|
|
168
|
+
res.writeHead(400, { 'Content-Type': 'application/json' })
|
|
169
|
+
res.end(JSON.stringify({ ok: false, error: 'Missing id parameter' }))
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = await deleteVaultEntry(id)
|
|
174
|
+
res.writeHead(result.success ? 200 : 404, { 'Content-Type': 'application/json' })
|
|
175
|
+
res.end(JSON.stringify({ ok: result.success, ...result }))
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
res.writeHead(405, { 'Content-Type': 'application/json' })
|
|
180
|
+
res.end(JSON.stringify({ error: 'Method not allowed' }))
|
|
181
|
+
},
|
|
182
|
+
}), 'dsh-image-gen: vault route')
|
|
183
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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",
|