@dickpy/dsh-imagegen 1.5.6 → 1.5.8
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 +7 -5
- package/lib/client.js +36396 -847
- package/lib/client.js.map +1 -1
- package/lib/index.js +183 -7
- package/package.json +2 -1
- package/src/canvas-store.ts +3 -2
- package/src/client/CanvasWorkspace.tsx +417 -77
- package/src/client/ImageGenPanel.tsx +2823 -2807
- package/src/client/SettingsCard.tsx +1128 -1128
- package/src/client/canvas-workspace.module.css +157 -33
- package/src/client/locales.ts +1524 -1509
- package/src/client/panel.module.css +7 -0
- package/src/engine.ts +998 -845
- package/src/gallery-store.ts +311 -311
- package/src/history-store.ts +275 -275
- package/src/image-storage-path.ts +12 -12
- package/src/index.ts +430 -429
- package/src/model-catalog.ts +149 -124
- package/src/presets.ts +95 -86
- package/src/prompt-enhancer.ts +151 -137
- package/src/protocol.ts +2 -2
package/src/prompt-enhancer.ts
CHANGED
|
@@ -1,137 +1,151 @@
|
|
|
1
|
-
/** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
|
|
2
|
-
|
|
3
|
-
import { isLikelyImageModelId } from './model-catalog.ts'
|
|
4
|
-
|
|
5
|
-
export interface PromptModelConfig {
|
|
6
|
-
apiUrl: string
|
|
7
|
-
apiKey: string
|
|
8
|
-
model: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Credentials shared by OpenAI-compatible `/models` discovery. */
|
|
12
|
-
export interface ModelListConfig {
|
|
13
|
-
apiUrl: string
|
|
14
|
-
apiKey: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function endpoint(base: string, suffix: string): string {
|
|
18
|
-
return `${base.replace(/\/+$/, '')}${suffix}`
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function headers(apiKey: string): HeadersInit {
|
|
22
|
-
return {
|
|
23
|
-
'content-type': 'application/json',
|
|
24
|
-
...apiKey.trim() === '' ? {} : { authorization: `Bearer ${apiKey.trim()}` },
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async function responseJson(response: Response): Promise<Record<string, unknown>> {
|
|
29
|
-
const body: unknown = await response.json().catch(() => undefined)
|
|
30
|
-
if (!response.ok || body === undefined || body === null || typeof body !== 'object') {
|
|
31
|
-
const message = body !== null && typeof body === 'object' && typeof (body as { error?: { message?: unknown } }).error?.message === 'string'
|
|
32
|
-
? (body as { error: { message: string } }).error.message
|
|
33
|
-
: `HTTP ${response.status}`
|
|
34
|
-
throw new Error(message)
|
|
35
|
-
}
|
|
36
|
-
return body as Record<string, unknown>
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
type ModelRecord = Record<string, unknown> & { id: string }
|
|
40
|
-
|
|
41
|
-
async function listModelRecords(config: ModelListConfig): Promise<ModelRecord[]> {
|
|
42
|
-
if (config.apiUrl.trim() === '') throw new Error('API URL is required')
|
|
43
|
-
const response = await fetch(endpoint(config.apiUrl, '/models'), { headers: headers(config.apiKey) })
|
|
44
|
-
const body = await responseJson(response)
|
|
45
|
-
const data = Array.isArray(body.data) ? body.data : []
|
|
46
|
-
return data.flatMap(item => {
|
|
47
|
-
if (item === null || typeof item !== 'object' || typeof (item as { id?: unknown }).id !== 'string') return []
|
|
48
|
-
const id = (item as { id: string }).id.trim()
|
|
49
|
-
return id === '' ? [] : [{ ...(item as Record<string, unknown>), id }]
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function textOf(value: unknown): string[] {
|
|
54
|
-
if (typeof value === 'string') return [value]
|
|
55
|
-
if (!Array.isArray(value)) return []
|
|
56
|
-
return value.filter((item): item is string => typeof item === 'string')
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function hasImageGenerationCapability(record: ModelRecord): boolean | undefined {
|
|
60
|
-
const capability = record.capabilities
|
|
61
|
-
if (capability !== null && typeof capability === 'object') {
|
|
62
|
-
const values = capability as Record<string, unknown>
|
|
63
|
-
for (const key of ['image_generation', 'imageGeneration', 'text_to_image', 'textToImage', 'image_gen']) {
|
|
64
|
-
if (typeof values[key] === 'boolean') return values[key]
|
|
65
|
-
}
|
|
66
|
-
const serialized = JSON.stringify(values).toLowerCase()
|
|
67
|
-
if (/image[ _-]?generation|text[ _-]?to[ _-]?image/.test(serialized)) return true
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const taskText = [
|
|
71
|
-
...textOf(record.task),
|
|
72
|
-
...textOf(record.task_type),
|
|
73
|
-
...textOf(record.taskType),
|
|
74
|
-
...textOf(record.type),
|
|
75
|
-
...textOf(record.model_type),
|
|
76
|
-
...textOf(record.modelType),
|
|
77
|
-
...textOf(record.tasks),
|
|
78
|
-
...textOf(record.description),
|
|
79
|
-
].join(' ').toLowerCase()
|
|
80
|
-
if (/image[ _-]?generation|text[ _-]?to[ _-]?image|image[ _-]?gen/.test(taskText)) return true
|
|
81
|
-
if (/^image(?:[ _-]?generation)?$/.test(taskText.trim())) return true
|
|
82
|
-
if (/embedding|rerank|moderation|transcri|speech|audio|video|chat[ _-]?completion/.test(taskText)) return false
|
|
83
|
-
|
|
84
|
-
for (const key of ['output_modalities', 'outputModalities', 'supported_output_modalities']) {
|
|
85
|
-
const modalities = textOf(record[key]).map(value => value.toLowerCase())
|
|
86
|
-
if (modalities.length > 0) return modalities.includes('image')
|
|
87
|
-
}
|
|
88
|
-
return undefined
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function isImageModelRecord(record: ModelRecord): boolean {
|
|
92
|
-
return hasImageGenerationCapability(record) ?? isLikelyImageModelId(record.id)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** List candidates exposed by an OpenAI-compatible endpoint. */
|
|
96
|
-
export async function listOpenAIModels(config: ModelListConfig): Promise<string[]> {
|
|
97
|
-
return [...new Set((await listModelRecords(config)).map(record => record.id))]
|
|
98
|
-
.sort((a, b) => a.localeCompare(b))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** List only models that advertise or conventionally represent image generation. */
|
|
102
|
-
export async function listImageModels(config: ModelListConfig): Promise<string[]> {
|
|
103
|
-
return [...new Set((await listModelRecords(config)).filter(isImageModelRecord).map(record => record.id))]
|
|
104
|
-
.sort((a, b) => a.localeCompare(b))
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** List chat models exposed by an OpenAI-compatible endpoint. */
|
|
108
|
-
export async function listPromptModels(config: PromptModelConfig): Promise<string[]> {
|
|
109
|
-
return listOpenAIModels(config)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
1
|
+
/** OpenAI-compatible chat helpers used by the optional prompt-enhancement UI. */
|
|
2
|
+
|
|
3
|
+
import { isLikelyImageModelId } from './model-catalog.ts'
|
|
4
|
+
|
|
5
|
+
export interface PromptModelConfig {
|
|
6
|
+
apiUrl: string
|
|
7
|
+
apiKey: string
|
|
8
|
+
model: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Credentials shared by OpenAI-compatible `/models` discovery. */
|
|
12
|
+
export interface ModelListConfig {
|
|
13
|
+
apiUrl: string
|
|
14
|
+
apiKey: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function endpoint(base: string, suffix: string): string {
|
|
18
|
+
return `${base.replace(/\/+$/, '')}${suffix}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function headers(apiKey: string): HeadersInit {
|
|
22
|
+
return {
|
|
23
|
+
'content-type': 'application/json',
|
|
24
|
+
...apiKey.trim() === '' ? {} : { authorization: `Bearer ${apiKey.trim()}` },
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function responseJson(response: Response): Promise<Record<string, unknown>> {
|
|
29
|
+
const body: unknown = await response.json().catch(() => undefined)
|
|
30
|
+
if (!response.ok || body === undefined || body === null || typeof body !== 'object') {
|
|
31
|
+
const message = body !== null && typeof body === 'object' && typeof (body as { error?: { message?: unknown } }).error?.message === 'string'
|
|
32
|
+
? (body as { error: { message: string } }).error.message
|
|
33
|
+
: `HTTP ${response.status}`
|
|
34
|
+
throw new Error(message)
|
|
35
|
+
}
|
|
36
|
+
return body as Record<string, unknown>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type ModelRecord = Record<string, unknown> & { id: string }
|
|
40
|
+
|
|
41
|
+
async function listModelRecords(config: ModelListConfig): Promise<ModelRecord[]> {
|
|
42
|
+
if (config.apiUrl.trim() === '') throw new Error('API URL is required')
|
|
43
|
+
const response = await fetch(endpoint(config.apiUrl, '/models'), { headers: headers(config.apiKey) })
|
|
44
|
+
const body = await responseJson(response)
|
|
45
|
+
const data = Array.isArray(body.data) ? body.data : []
|
|
46
|
+
return data.flatMap(item => {
|
|
47
|
+
if (item === null || typeof item !== 'object' || typeof (item as { id?: unknown }).id !== 'string') return []
|
|
48
|
+
const id = (item as { id: string }).id.trim()
|
|
49
|
+
return id === '' ? [] : [{ ...(item as Record<string, unknown>), id }]
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function textOf(value: unknown): string[] {
|
|
54
|
+
if (typeof value === 'string') return [value]
|
|
55
|
+
if (!Array.isArray(value)) return []
|
|
56
|
+
return value.filter((item): item is string => typeof item === 'string')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function hasImageGenerationCapability(record: ModelRecord): boolean | undefined {
|
|
60
|
+
const capability = record.capabilities
|
|
61
|
+
if (capability !== null && typeof capability === 'object') {
|
|
62
|
+
const values = capability as Record<string, unknown>
|
|
63
|
+
for (const key of ['image_generation', 'imageGeneration', 'text_to_image', 'textToImage', 'image_gen']) {
|
|
64
|
+
if (typeof values[key] === 'boolean') return values[key]
|
|
65
|
+
}
|
|
66
|
+
const serialized = JSON.stringify(values).toLowerCase()
|
|
67
|
+
if (/image[ _-]?generation|text[ _-]?to[ _-]?image/.test(serialized)) return true
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const taskText = [
|
|
71
|
+
...textOf(record.task),
|
|
72
|
+
...textOf(record.task_type),
|
|
73
|
+
...textOf(record.taskType),
|
|
74
|
+
...textOf(record.type),
|
|
75
|
+
...textOf(record.model_type),
|
|
76
|
+
...textOf(record.modelType),
|
|
77
|
+
...textOf(record.tasks),
|
|
78
|
+
...textOf(record.description),
|
|
79
|
+
].join(' ').toLowerCase()
|
|
80
|
+
if (/image[ _-]?generation|text[ _-]?to[ _-]?image|image[ _-]?gen/.test(taskText)) return true
|
|
81
|
+
if (/^image(?:[ _-]?generation)?$/.test(taskText.trim())) return true
|
|
82
|
+
if (/embedding|rerank|moderation|transcri|speech|audio|video|chat[ _-]?completion/.test(taskText)) return false
|
|
83
|
+
|
|
84
|
+
for (const key of ['output_modalities', 'outputModalities', 'supported_output_modalities']) {
|
|
85
|
+
const modalities = textOf(record[key]).map(value => value.toLowerCase())
|
|
86
|
+
if (modalities.length > 0) return modalities.includes('image')
|
|
87
|
+
}
|
|
88
|
+
return undefined
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isImageModelRecord(record: ModelRecord): boolean {
|
|
92
|
+
return hasImageGenerationCapability(record) ?? isLikelyImageModelId(record.id)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** List candidates exposed by an OpenAI-compatible endpoint. */
|
|
96
|
+
export async function listOpenAIModels(config: ModelListConfig): Promise<string[]> {
|
|
97
|
+
return [...new Set((await listModelRecords(config)).map(record => record.id))]
|
|
98
|
+
.sort((a, b) => a.localeCompare(b))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** List only models that advertise or conventionally represent image generation. */
|
|
102
|
+
export async function listImageModels(config: ModelListConfig): Promise<string[]> {
|
|
103
|
+
return [...new Set((await listModelRecords(config)).filter(isImageModelRecord).map(record => record.id))]
|
|
104
|
+
.sort((a, b) => a.localeCompare(b))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** List chat models exposed by an OpenAI-compatible endpoint. */
|
|
108
|
+
export async function listPromptModels(config: PromptModelConfig): Promise<string[]> {
|
|
109
|
+
return listOpenAIModels(config)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Remove reasoning-model artifacts from a chat model's visible content:
|
|
113
|
+
* complete `<think>…</think>` blocks first, then anything from a dangling
|
|
114
|
+
* unclosed `<think>` to the end of the text. Reasoning models served through
|
|
115
|
+
* OpenAI-compatible endpoints (MiniMax M3, DeepSeek R1, Qwen QVQ, …) inline
|
|
116
|
+
* these blocks in `message.content`; leaking them into the prompt box both
|
|
117
|
+
* pollutes the prompt and can push it past image models' length limits. */
|
|
118
|
+
function stripReasoning(text: string): string {
|
|
119
|
+
const withoutClosed = text.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
120
|
+
const dangling = /<think>/i.exec(withoutClosed)
|
|
121
|
+
return (dangling === null ? withoutClosed : withoutClosed.slice(0, dangling.index)).trim()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Expand a concise image request into a production-ready image prompt. */
|
|
125
|
+
export async function enhancePrompt(config: PromptModelConfig, prompt: string): Promise<string> {
|
|
126
|
+
if (config.apiUrl.trim() === '' || config.model.trim() === '') throw new Error('prompt enhancement model is not configured')
|
|
127
|
+
const response = await fetch(endpoint(config.apiUrl, '/chat/completions'), {
|
|
128
|
+
method: 'POST',
|
|
129
|
+
headers: headers(config.apiKey),
|
|
130
|
+
body: JSON.stringify({
|
|
131
|
+
model: config.model.trim(),
|
|
132
|
+
temperature: 0.7,
|
|
133
|
+
messages: [
|
|
134
|
+
{
|
|
135
|
+
role: 'system',
|
|
136
|
+
content: 'You are an expert image-prompt editor. Expand the user request into one vivid, specific image-generation prompt. Preserve intent and language. Add only useful visual detail: subject, composition, lighting, materials, color, camera/style and quality. Return only the finished prompt, with no preface or markdown.',
|
|
137
|
+
},
|
|
138
|
+
{ role: 'user', content: prompt },
|
|
139
|
+
],
|
|
140
|
+
}),
|
|
141
|
+
})
|
|
142
|
+
const body = await responseJson(response)
|
|
143
|
+
const choices = Array.isArray(body.choices) ? body.choices : []
|
|
144
|
+
const content = choices[0] !== null && typeof choices[0] === 'object'
|
|
145
|
+
? (choices[0] as { message?: { content?: unknown } }).message?.content
|
|
146
|
+
: undefined
|
|
147
|
+
if (typeof content !== 'string' || content.trim() === '') throw new Error('chat model returned an empty prompt')
|
|
148
|
+
const enhanced = stripReasoning(content)
|
|
149
|
+
if (enhanced === '') throw new Error('chat model returned only reasoning content (empty <think> payload)')
|
|
150
|
+
return enhanced
|
|
151
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
export const IMAGEGEN_SETTINGS_NAMESPACE = 'dsh-imagegen'
|
|
9
9
|
|
|
10
10
|
/** Published package version shared by the host updater and the client UI. */
|
|
11
|
-
export const PLUGIN_VERSION = '1.5.
|
|
11
|
+
export const PLUGIN_VERSION = '1.5.8'
|
|
12
12
|
|
|
13
13
|
/** Same-origin route family (loopback-only, mirroring the dsh-ssh fence). */
|
|
14
14
|
export const SETTINGS_API = {
|
|
@@ -324,7 +324,7 @@ export interface CanvasDocument {
|
|
|
324
324
|
title: string
|
|
325
325
|
revision: number
|
|
326
326
|
viewport: CanvasViewport
|
|
327
|
-
background: 'dots' | 'lines' | 'diagonal' | 'checker' | 'blank' | 'image'
|
|
327
|
+
background: 'dots' | 'lines' | 'diagonal' | 'checker' | 'blank' | 'image' | 'flow' | 'aurora'
|
|
328
328
|
/** Custom background image URL (a canvas asset) when background is 'image'. */
|
|
329
329
|
backgroundImage?: string
|
|
330
330
|
nodes: CanvasNode[]
|