visual-prompt-kit 0.1.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/README.md +254 -0
- package/bin/visual-prompt.js +5 -0
- package/docs/keyword-authoring.md +207 -0
- package/package.json +43 -0
- package/skills/image-prompt-composer/SKILL.md +40 -0
- package/skills/image-prompt-composer/agents/openai.yaml +4 -0
- package/src/cli.js +261 -0
- package/src/define.js +78 -0
- package/src/generator/export.js +20 -0
- package/src/generator/generateTags.js +64 -0
- package/src/generator/index.js +8 -0
- package/src/generator/locks.js +40 -0
- package/src/generator/pick.js +27 -0
- package/src/generator/seed.js +28 -0
- package/src/index.js +23 -0
- package/src/prompt/buildPromptRequest.js +63 -0
- package/src/prompt/index.js +7 -0
- package/src/prompt/serializePromptRequest.js +21 -0
- package/src/themes/index.js +43 -0
- package/src/themes/sweet-girl/dimensions/camera.js +42 -0
- package/src/themes/sweet-girl/dimensions/expression.js +42 -0
- package/src/themes/sweet-girl/dimensions/facial.js +34 -0
- package/src/themes/sweet-girl/dimensions/lighting.js +34 -0
- package/src/themes/sweet-girl/dimensions/outfit.js +44 -0
- package/src/themes/sweet-girl/dimensions/pose.js +66 -0
- package/src/themes/sweet-girl/dimensions/scene.js +102 -0
- package/src/themes/sweet-girl/dimensions/style.js +32 -0
- package/src/themes/sweet-girl/dimensions/subject.js +38 -0
- package/src/themes/sweet-girl/dimensions/vibe.js +34 -0
- package/src/themes/sweet-girl/index.js +1 -0
- package/src/themes/sweet-girl/manifest.js +50 -0
- package/src/themes/sweet-girl/presets/index.js +26 -0
- package/src/validate/index.js +5 -0
- package/src/validate/rules.js +5 -0
- package/src/validate/theme.js +79 -0
package/src/cli.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
const {
|
|
2
|
+
listThemes,
|
|
3
|
+
getTheme,
|
|
4
|
+
listDimensions,
|
|
5
|
+
validateTheme,
|
|
6
|
+
generateTags,
|
|
7
|
+
serializeSelection,
|
|
8
|
+
buildPromptRequest,
|
|
9
|
+
serializePromptRequest
|
|
10
|
+
} = require("./index");
|
|
11
|
+
|
|
12
|
+
function writeLine(stream, message = "") {
|
|
13
|
+
stream.write(`${message}\n`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseKeyValue(raw, optionName) {
|
|
17
|
+
const separator = raw.indexOf("=");
|
|
18
|
+
if (separator <= 0 || separator === raw.length - 1) {
|
|
19
|
+
throw new Error(`Option ${optionName} expects KEY=VALUE, received "${raw}".`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
key: raw.slice(0, separator),
|
|
24
|
+
value: raw.slice(separator + 1)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ensureNextValue(argv, index, optionName) {
|
|
29
|
+
const value = argv[index + 1];
|
|
30
|
+
if (!value || value.startsWith("-")) {
|
|
31
|
+
throw new Error(`Option ${optionName} requires a value.`);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseArgv(argv) {
|
|
37
|
+
const args = [...argv];
|
|
38
|
+
const first = args[0];
|
|
39
|
+
const command = !first || first.startsWith("-") ? "help" : args.shift();
|
|
40
|
+
const options = {
|
|
41
|
+
theme: "sweet-girl",
|
|
42
|
+
format: undefined,
|
|
43
|
+
seed: undefined,
|
|
44
|
+
preset: undefined,
|
|
45
|
+
locks: {},
|
|
46
|
+
counts: {}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
50
|
+
const token = args[index];
|
|
51
|
+
|
|
52
|
+
switch (token) {
|
|
53
|
+
case "--theme":
|
|
54
|
+
case "-t": {
|
|
55
|
+
options.theme = ensureNextValue(args, index, token);
|
|
56
|
+
index += 1;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case "--seed":
|
|
60
|
+
case "-s": {
|
|
61
|
+
options.seed = ensureNextValue(args, index, token);
|
|
62
|
+
index += 1;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
case "--preset":
|
|
66
|
+
case "-p": {
|
|
67
|
+
options.preset = ensureNextValue(args, index, token);
|
|
68
|
+
index += 1;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
case "--format":
|
|
72
|
+
case "-f": {
|
|
73
|
+
options.format = ensureNextValue(args, index, token);
|
|
74
|
+
index += 1;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case "--lock":
|
|
78
|
+
case "-l": {
|
|
79
|
+
const parsed = parseKeyValue(ensureNextValue(args, index, token), token);
|
|
80
|
+
options.locks[parsed.key] ||= [];
|
|
81
|
+
options.locks[parsed.key].push(parsed.value);
|
|
82
|
+
index += 1;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case "--count":
|
|
86
|
+
case "-c": {
|
|
87
|
+
const parsed = parseKeyValue(ensureNextValue(args, index, token), token);
|
|
88
|
+
const count = Number(parsed.value);
|
|
89
|
+
if (!Number.isInteger(count) || count < 1) {
|
|
90
|
+
throw new Error(`Option ${token} requires a positive integer count.`);
|
|
91
|
+
}
|
|
92
|
+
options.counts[parsed.key] = count;
|
|
93
|
+
index += 1;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "--help":
|
|
97
|
+
case "-h": {
|
|
98
|
+
options.help = true;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
default:
|
|
102
|
+
throw new Error(`Unknown option "${token}".`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
command,
|
|
108
|
+
options
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatList(items, format) {
|
|
113
|
+
if (format === "json") {
|
|
114
|
+
return JSON.stringify(items, null, 2);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (format === "txt" || format === "text") {
|
|
118
|
+
return items.join("\n");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
throw new Error(`Unsupported list format "${format}".`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function printHelp(stdout) {
|
|
125
|
+
writeLine(stdout, "visual-prompt <command> [options]");
|
|
126
|
+
writeLine(stdout);
|
|
127
|
+
writeLine(stdout, "Commands:");
|
|
128
|
+
writeLine(stdout, " themes List theme ids");
|
|
129
|
+
writeLine(stdout, " dimensions [--theme <id>] List dimension keys for a theme");
|
|
130
|
+
writeLine(stdout, " presets [--theme <id>] List preset names for a theme");
|
|
131
|
+
writeLine(stdout, " validate [--theme <id>] Validate a theme");
|
|
132
|
+
writeLine(stdout, " generate [options] Generate prompt tags");
|
|
133
|
+
writeLine(stdout, " prompt [options] Build an LLM prompt from generated tags");
|
|
134
|
+
writeLine(stdout);
|
|
135
|
+
writeLine(stdout, "Shared options for generate / prompt:");
|
|
136
|
+
writeLine(stdout, " --theme, -t <id> Theme id, default sweet-girl");
|
|
137
|
+
writeLine(stdout, " --seed, -s <value> Stable random seed");
|
|
138
|
+
writeLine(stdout, " --preset, -p <name> Apply a preset");
|
|
139
|
+
writeLine(stdout, " --format, -f <json|txt> Output format");
|
|
140
|
+
writeLine(stdout, " --lock, -l <dimension=text> Repeatable lock option");
|
|
141
|
+
writeLine(stdout, " --count, -c <dimension=n> Override pick count");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveFormat(command, format) {
|
|
145
|
+
if (format) {
|
|
146
|
+
return format;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (command === "generate") {
|
|
150
|
+
return "json";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (command === "prompt") {
|
|
154
|
+
return "txt";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return "txt";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function runCli(argv, io = {}) {
|
|
161
|
+
const stdout = io.stdout || process.stdout;
|
|
162
|
+
const stderr = io.stderr || process.stderr;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const { command, options } = parseArgv(argv);
|
|
166
|
+
const format = resolveFormat(command, options.format);
|
|
167
|
+
|
|
168
|
+
if (options.help || command === "help") {
|
|
169
|
+
printHelp(stdout);
|
|
170
|
+
return 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (command === "themes") {
|
|
174
|
+
writeLine(stdout, formatList(listThemes().map((theme) => theme.id), format));
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const theme = getTheme(options.theme);
|
|
179
|
+
|
|
180
|
+
if (command === "dimensions") {
|
|
181
|
+
writeLine(
|
|
182
|
+
stdout,
|
|
183
|
+
formatList(listDimensions(theme).map((dimension) => dimension.key), format)
|
|
184
|
+
);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (command === "presets") {
|
|
189
|
+
writeLine(stdout, formatList(Object.keys(theme.presets), format));
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (command === "validate") {
|
|
194
|
+
const result = validateTheme(theme);
|
|
195
|
+
if (format === "json") {
|
|
196
|
+
writeLine(
|
|
197
|
+
stdout,
|
|
198
|
+
JSON.stringify(
|
|
199
|
+
{
|
|
200
|
+
theme: theme.id,
|
|
201
|
+
valid: result.valid,
|
|
202
|
+
warnings: result.warnings,
|
|
203
|
+
errors: result.errors
|
|
204
|
+
},
|
|
205
|
+
null,
|
|
206
|
+
2
|
|
207
|
+
)
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
for (const warning of result.warnings) {
|
|
211
|
+
writeLine(stderr, `WARN: ${warning}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!result.valid) {
|
|
215
|
+
for (const error of result.errors) {
|
|
216
|
+
writeLine(stderr, `ERROR: ${error}`);
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
writeLine(stdout, `Theme "${theme.id}" passed validation with ${theme.dimensions.length} dimensions.`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return result.valid ? 0 : 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (command === "generate") {
|
|
227
|
+
const result = generateTags({
|
|
228
|
+
theme,
|
|
229
|
+
seed: options.seed,
|
|
230
|
+
preset: options.preset,
|
|
231
|
+
locks: options.locks,
|
|
232
|
+
counts: options.counts
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
writeLine(stdout, serializeSelection(result, format));
|
|
236
|
+
return 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (command === "prompt") {
|
|
240
|
+
const request = buildPromptRequest({
|
|
241
|
+
theme,
|
|
242
|
+
seed: options.seed,
|
|
243
|
+
preset: options.preset,
|
|
244
|
+
locks: options.locks,
|
|
245
|
+
counts: options.counts
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
writeLine(stdout, serializePromptRequest(request, format));
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
throw new Error(`Unknown command "${command}".`);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
writeLine(stderr, error.message);
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
module.exports = {
|
|
260
|
+
runCli
|
|
261
|
+
};
|
package/src/define.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
function normalizeItem(key, item, index) {
|
|
2
|
+
if (typeof item === "string") {
|
|
3
|
+
return {
|
|
4
|
+
id: `${key}-${String(index + 1).padStart(3, "0")}`,
|
|
5
|
+
text: item,
|
|
6
|
+
weight: 1
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
id: item.id || `${key}-${String(index + 1).padStart(3, "0")}`,
|
|
12
|
+
text: item.text,
|
|
13
|
+
weight: item.weight || 1,
|
|
14
|
+
tags: item.tags || [],
|
|
15
|
+
bannedWith: item.bannedWith || []
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defineDimension(definition) {
|
|
20
|
+
if (!definition || !definition.key) {
|
|
21
|
+
throw new Error("Dimension definition requires a key.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!Array.isArray(definition.items) || definition.items.length === 0) {
|
|
25
|
+
throw new Error(`Dimension "${definition.key}" must define a non-empty items array.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const items = definition.items.map((item, index) => normalizeItem(definition.key, item, index));
|
|
29
|
+
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
key: definition.key,
|
|
32
|
+
zhName: definition.zhName,
|
|
33
|
+
description: definition.description || "",
|
|
34
|
+
pick: Object.freeze({
|
|
35
|
+
min: definition.pick?.min ?? 1,
|
|
36
|
+
max: definition.pick?.max ?? 1,
|
|
37
|
+
recommended: definition.pick?.recommended ?? definition.pick?.min ?? 1
|
|
38
|
+
}),
|
|
39
|
+
rules: Object.freeze([...(definition.rules || [])]),
|
|
40
|
+
minimumItems: definition.minimumItems ?? 20,
|
|
41
|
+
forbiddenTerms: Object.freeze([...(definition.forbiddenTerms || [])]),
|
|
42
|
+
items: Object.freeze(items)
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function defineTheme(definition) {
|
|
47
|
+
if (!definition || !definition.id) {
|
|
48
|
+
throw new Error("Theme definition requires an id.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!Array.isArray(definition.dimensions) || definition.dimensions.length === 0) {
|
|
52
|
+
throw new Error(`Theme "${definition.id}" must define dimensions.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const seen = new Set();
|
|
56
|
+
for (const dimension of definition.dimensions) {
|
|
57
|
+
if (seen.has(dimension.key)) {
|
|
58
|
+
throw new Error(`Duplicate dimension key "${dimension.key}" in theme "${definition.id}".`);
|
|
59
|
+
}
|
|
60
|
+
seen.add(dimension.key);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
id: definition.id,
|
|
65
|
+
version: definition.version || "0.1.0",
|
|
66
|
+
locale: definition.locale || "zh-CN",
|
|
67
|
+
target: definition.target,
|
|
68
|
+
description: definition.description || "",
|
|
69
|
+
bannedLexicon: Object.freeze([...(definition.bannedLexicon || [])]),
|
|
70
|
+
presets: Object.freeze({ ...(definition.presets || {}) }),
|
|
71
|
+
dimensions: Object.freeze([...definition.dimensions])
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
defineDimension,
|
|
77
|
+
defineTheme
|
|
78
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function flattenSelection(tags) {
|
|
2
|
+
return Object.values(tags).flat();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function serializeSelection(result, format = "json") {
|
|
6
|
+
if (format === "txt") {
|
|
7
|
+
return result.flat.join(",");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (format === "json") {
|
|
11
|
+
return JSON.stringify(result, null, 2);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
throw new Error(`Unsupported export format "${format}".`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = {
|
|
18
|
+
flattenSelection,
|
|
19
|
+
serializeSelection
|
|
20
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { createSeededRandom } = require("./seed");
|
|
2
|
+
const { pickWeightedUnique } = require("./pick");
|
|
3
|
+
const { mergeLocks } = require("./locks");
|
|
4
|
+
const { flattenSelection } = require("./export");
|
|
5
|
+
|
|
6
|
+
function findItemsByText(dimension, texts) {
|
|
7
|
+
return texts.map((text) => {
|
|
8
|
+
const item = dimension.items.find((entry) => entry.text === text);
|
|
9
|
+
if (!item) {
|
|
10
|
+
throw new Error(`Unknown keyword "${text}" in dimension "${dimension.key}".`);
|
|
11
|
+
}
|
|
12
|
+
return item;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function generateTags(options) {
|
|
17
|
+
const theme = options.theme;
|
|
18
|
+
const seed = options.seed ?? `seed-${Date.now()}`;
|
|
19
|
+
const preset = options.preset ?? null;
|
|
20
|
+
const counts = options.counts || {};
|
|
21
|
+
const mergedLocks = mergeLocks(theme, preset, options.locks || {});
|
|
22
|
+
const rng = createSeededRandom(seed);
|
|
23
|
+
const tags = {};
|
|
24
|
+
const detailed = {};
|
|
25
|
+
|
|
26
|
+
for (const dimension of theme.dimensions) {
|
|
27
|
+
const lockedTexts = mergedLocks[dimension.key] || [];
|
|
28
|
+
const lockedItems = findItemsByText(dimension, lockedTexts);
|
|
29
|
+
if (lockedItems.length > dimension.pick.max) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Locked keyword count for "${dimension.key}" exceeds max pick count ${dimension.pick.max}.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
const lockedIds = new Set(lockedItems.map((item) => item.id));
|
|
35
|
+
const requestedCount = counts[dimension.key] ?? dimension.pick.recommended;
|
|
36
|
+
const finalCount = Math.max(
|
|
37
|
+
dimension.pick.min,
|
|
38
|
+
Math.min(dimension.pick.max, Math.max(requestedCount, lockedItems.length))
|
|
39
|
+
);
|
|
40
|
+
const picks = pickWeightedUnique(
|
|
41
|
+
dimension.items,
|
|
42
|
+
finalCount - lockedItems.length,
|
|
43
|
+
rng,
|
|
44
|
+
lockedIds
|
|
45
|
+
);
|
|
46
|
+
const selected = [...lockedItems, ...picks];
|
|
47
|
+
|
|
48
|
+
tags[dimension.key] = selected.map((item) => item.text);
|
|
49
|
+
detailed[dimension.key] = selected;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
theme: theme.id,
|
|
54
|
+
seed,
|
|
55
|
+
preset,
|
|
56
|
+
tags,
|
|
57
|
+
detailed,
|
|
58
|
+
flat: flattenSelection(tags)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
generateTags
|
|
64
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function dedupe(values) {
|
|
2
|
+
return [...new Set(values)];
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function resolvePreset(theme, presetName) {
|
|
6
|
+
if (!presetName) {
|
|
7
|
+
return {};
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const preset = theme.presets[presetName];
|
|
11
|
+
if (!preset) {
|
|
12
|
+
throw new Error(`Unknown preset "${presetName}" for theme "${theme.id}".`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return preset;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function mergeLocks(theme, presetName, explicitLocks = {}) {
|
|
19
|
+
const presetLocks = resolvePreset(theme, presetName);
|
|
20
|
+
const keys = new Set([
|
|
21
|
+
...Object.keys(presetLocks),
|
|
22
|
+
...Object.keys(explicitLocks)
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const merged = {};
|
|
26
|
+
|
|
27
|
+
for (const key of keys) {
|
|
28
|
+
merged[key] = dedupe([
|
|
29
|
+
...(presetLocks[key] || []),
|
|
30
|
+
...(explicitLocks[key] || [])
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return merged;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
mergeLocks,
|
|
39
|
+
resolvePreset
|
|
40
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function pickWeightedUnique(items, count, rng, excludedIds = new Set()) {
|
|
2
|
+
const pool = items.filter((item) => !excludedIds.has(item.id));
|
|
3
|
+
const selected = [];
|
|
4
|
+
|
|
5
|
+
while (selected.length < count && pool.length > 0) {
|
|
6
|
+
const totalWeight = pool.reduce((sum, item) => sum + (item.weight || 1), 0);
|
|
7
|
+
let threshold = rng() * totalWeight;
|
|
8
|
+
let pickedIndex = 0;
|
|
9
|
+
|
|
10
|
+
for (let index = 0; index < pool.length; index += 1) {
|
|
11
|
+
threshold -= pool[index].weight || 1;
|
|
12
|
+
if (threshold <= 0) {
|
|
13
|
+
pickedIndex = index;
|
|
14
|
+
break;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
selected.push(pool[pickedIndex]);
|
|
19
|
+
pool.splice(pickedIndex, 1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return selected;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
pickWeightedUnique
|
|
27
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function hashSeed(input) {
|
|
2
|
+
const text = String(input);
|
|
3
|
+
let hash = 2166136261;
|
|
4
|
+
|
|
5
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
6
|
+
hash ^= text.charCodeAt(index);
|
|
7
|
+
hash = Math.imul(hash, 16777619);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return hash >>> 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createSeededRandom(seed) {
|
|
14
|
+
let state = hashSeed(seed);
|
|
15
|
+
|
|
16
|
+
return function next() {
|
|
17
|
+
state += 0x6D2B79F5;
|
|
18
|
+
let value = state;
|
|
19
|
+
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
20
|
+
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
21
|
+
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
hashSeed,
|
|
27
|
+
createSeededRandom
|
|
28
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { defineDimension, defineTheme } = require("./define");
|
|
2
|
+
const { themes, listThemes, getTheme, listDimensions, getDimension } = require("./themes");
|
|
3
|
+
const sweetGirl = require("./themes/sweet-girl");
|
|
4
|
+
const { validateTheme } = require("./validate");
|
|
5
|
+
const { generateTags, flattenSelection, serializeSelection } = require("./generator");
|
|
6
|
+
const { buildPromptRequest, serializePromptRequest } = require("./prompt");
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
defineDimension,
|
|
10
|
+
defineTheme,
|
|
11
|
+
themes,
|
|
12
|
+
sweetGirl,
|
|
13
|
+
listThemes,
|
|
14
|
+
getTheme,
|
|
15
|
+
listDimensions,
|
|
16
|
+
getDimension,
|
|
17
|
+
validateTheme,
|
|
18
|
+
generateTags,
|
|
19
|
+
flattenSelection,
|
|
20
|
+
serializeSelection,
|
|
21
|
+
buildPromptRequest,
|
|
22
|
+
serializePromptRequest
|
|
23
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const { generateTags } = require("../generator");
|
|
2
|
+
|
|
3
|
+
const DEFAULT_SYSTEM_PROMPT = [
|
|
4
|
+
"你是通用生图模型的人像提示词编辑器。",
|
|
5
|
+
"把离散视觉标签重组为一段自然完整的中文生图提示词,适用于 Google、豆包等常见生图模型。",
|
|
6
|
+
"你必须围绕甜美少女写真,吸收全部标签并重组为具体、连贯、有画面感的描述。",
|
|
7
|
+
"禁止罗列标签,禁止按原顺序复述,禁止使用“采用、呈现、营造”等机械拼接句。"
|
|
8
|
+
].join("");
|
|
9
|
+
|
|
10
|
+
function formatDimensionTags(tags) {
|
|
11
|
+
return Object.entries(tags)
|
|
12
|
+
.map(([key, values]) => `- ${key}: ${values.join(" / ")}`)
|
|
13
|
+
.join("\n");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildPromptRequest(options) {
|
|
17
|
+
const theme = options.theme;
|
|
18
|
+
const selection =
|
|
19
|
+
options.selection ||
|
|
20
|
+
generateTags({
|
|
21
|
+
theme,
|
|
22
|
+
seed: options.seed,
|
|
23
|
+
preset: options.preset,
|
|
24
|
+
locks: options.locks,
|
|
25
|
+
counts: options.counts
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const system = options.systemPrompt || DEFAULT_SYSTEM_PROMPT;
|
|
29
|
+
const user = [
|
|
30
|
+
"请把下面这些视觉标签无损重组为最终生图提示词。",
|
|
31
|
+
"",
|
|
32
|
+
"要求:",
|
|
33
|
+
"1. 必须吸收全部标签,但允许等价改写,不要逐词照抄。",
|
|
34
|
+
"2. 先建立主体、场景与核心氛围,再自然融入姿态、神情、光线、镜头和质感,形成完整画面。",
|
|
35
|
+
"3. 不要按输入顺序机械串联,不要写成标签清单扩写;如果读起来像顺序拼接,视为失败。",
|
|
36
|
+
"4. 可以补足少量合理细节,如发丝、肤感、空气感、空间层次,但不得新增冲突设定。",
|
|
37
|
+
"5. 语言应简洁、具体、可视化,适合常见通用生图模型理解。",
|
|
38
|
+
"6. 只输出一段中文提示词,建议 80 到 140 字,不要解释,不要分点,不要标题。",
|
|
39
|
+
"",
|
|
40
|
+
"按维度整理的视觉标签:",
|
|
41
|
+
formatDimensionTags(selection.tags),
|
|
42
|
+
"",
|
|
43
|
+
"请直接输出最终提示词。"
|
|
44
|
+
].join("\n");
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
theme: theme.id,
|
|
48
|
+
seed: selection.seed,
|
|
49
|
+
preset: selection.preset,
|
|
50
|
+
system,
|
|
51
|
+
user,
|
|
52
|
+
tags: selection.tags,
|
|
53
|
+
flat: selection.flat,
|
|
54
|
+
messages: [
|
|
55
|
+
{ role: "system", content: system },
|
|
56
|
+
{ role: "user", content: user }
|
|
57
|
+
]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
buildPromptRequest
|
|
63
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
function serializePromptRequest(request, format = "txt") {
|
|
2
|
+
if (format === "json") {
|
|
3
|
+
return JSON.stringify(request, null, 2);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (format === "txt" || format === "text") {
|
|
7
|
+
return [
|
|
8
|
+
"[System Prompt]",
|
|
9
|
+
request.system,
|
|
10
|
+
"",
|
|
11
|
+
"[User Prompt]",
|
|
12
|
+
request.user
|
|
13
|
+
].join("\n");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
throw new Error(`Unsupported prompt format "${format}".`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
serializePromptRequest
|
|
21
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const sweetGirl = require("./sweet-girl");
|
|
2
|
+
|
|
3
|
+
const themes = {
|
|
4
|
+
[sweetGirl.id]: sweetGirl
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
function listThemes() {
|
|
8
|
+
return Object.values(themes);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getTheme(themeId) {
|
|
12
|
+
const theme = themes[themeId];
|
|
13
|
+
|
|
14
|
+
if (!theme) {
|
|
15
|
+
throw new Error(`Unknown theme "${themeId}".`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return theme;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function listDimensions(themeOrId) {
|
|
22
|
+
const theme = typeof themeOrId === "string" ? getTheme(themeOrId) : themeOrId;
|
|
23
|
+
return [...theme.dimensions];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getDimension(themeOrId, dimensionKey) {
|
|
27
|
+
const theme = typeof themeOrId === "string" ? getTheme(themeOrId) : themeOrId;
|
|
28
|
+
const dimension = theme.dimensions.find((entry) => entry.key === dimensionKey);
|
|
29
|
+
|
|
30
|
+
if (!dimension) {
|
|
31
|
+
throw new Error(`Unknown dimension "${dimensionKey}" in theme "${theme.id}".`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return dimension;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
themes,
|
|
39
|
+
listThemes,
|
|
40
|
+
getTheme,
|
|
41
|
+
listDimensions,
|
|
42
|
+
getDimension
|
|
43
|
+
};
|