@playcraft/cli 0.1.39-prod → 0.1.41-prod
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.
|
@@ -3,9 +3,9 @@ import { writeFileSync, mkdirSync } from 'fs';
|
|
|
3
3
|
import { dirname, parse, join } from 'path';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
import { resolveImageBackgroundRequirement, validateImageDeliveryOptions } from '@playcraft/common/prefab';
|
|
6
|
-
import { readImageAssetContext } from '../utils/image-asset-context.js';
|
|
6
|
+
import { assertImageAssetOutputPaths, readImageAssetContext } from '../utils/image-asset-context.js';
|
|
7
7
|
import { AgentApiClient } from '../utils/agent-api-client.js';
|
|
8
|
-
import { MAX_REFERENCE_IMAGES, collectReferenceImagePaths, collectReferenceImagePayloads, sniffImageExtension, extensionForImageMime, resolveImageOutputPath, handleError, raceWithTimeout, } from './tools-utils.js';
|
|
8
|
+
import { MAX_REFERENCE_IMAGES, collectReferenceImagePaths, parseImageTargetFieldPath, collectReferenceImagePayloads, sniffImageExtension, extensionForImageMime, resolveImageOutputPath, handleError, raceWithTimeout, } from './tools-utils.js';
|
|
9
9
|
function isMasterCompositeStripRequest(opts) {
|
|
10
10
|
if (opts.aspectRatio === '45:16')
|
|
11
11
|
return true;
|
|
@@ -154,6 +154,7 @@ export function registerGenerationCommands(tools) {
|
|
|
154
154
|
.option('--aspect-ratio <ratio>', '宽高比 (1:1|45:16|16:9|9:16|3:4|4:3|2:3|3:2|21:9)', '1:1')
|
|
155
155
|
.requiredOption('--output <path>', '保存路径(--count>1 时自动追加 _1/_2 等后缀)')
|
|
156
156
|
.option('--target-asset <path>', 'Existing asset to replace; read its schema references and delivery constraints')
|
|
157
|
+
.option('--target-field-path <path>', 'Target image field as an array of keys, e.g. ["endUI","playButton"]', parseImageTargetFieldPath)
|
|
157
158
|
.option('--no-target-reference', 'Generate without the existing asset reference only when explicitly requested')
|
|
158
159
|
.option('--output-format <format>', 'Final format: png, webp or jpeg')
|
|
159
160
|
.option('--fit-mode <mode>', 'Canvas fitting: contain, cover or fill')
|
|
@@ -176,9 +177,16 @@ export function registerGenerationCommands(tools) {
|
|
|
176
177
|
throw new Error('Conflicting background options. Use --background transparent or --background opaque; no image request was sent.');
|
|
177
178
|
}
|
|
178
179
|
opts = { ...opts, removeBackground: explicitBackground };
|
|
180
|
+
if (opts.targetFieldPath && !opts.targetAsset) {
|
|
181
|
+
throw new Error('--target-field-path requires --target-asset; no image request was sent.');
|
|
182
|
+
}
|
|
179
183
|
const explicitOutputFormat = opts.outputFormat;
|
|
184
|
+
const { dir: outputDir, name: outputName, ext: outputExt } = parse(opts.output);
|
|
185
|
+
const outputCandidates = [opts.output, ...Array.from({ length: opts.count > 1 ? Math.min(opts.count, 10) : 0 }, (_, i) => join(outputDir, `${outputName}_${i + 1}${outputExt}`))];
|
|
180
186
|
const assetContext = opts.targetAsset ? await readImageAssetContext(opts.targetAsset, process.cwd(), {
|
|
181
187
|
referenceImagePaths: opts.referenceImage, useTargetReference: opts.targetReference,
|
|
188
|
+
...(opts.targetFieldPath ? { targetFieldPath: opts.targetFieldPath } : {}),
|
|
189
|
+
outputPaths: outputCandidates,
|
|
182
190
|
prompt: opts.prompt, removeBackground: opts.removeBackground,
|
|
183
191
|
}) : undefined;
|
|
184
192
|
opts = { ...opts,
|
|
@@ -343,6 +351,8 @@ export function registerGenerationCommands(tools) {
|
|
|
343
351
|
console.log(`↑ Letterbox ${generatedWidth}×${generatedHeight} → ${stripTarget.width}×${stripTarget.height}(等比 + 白边,不拉伸)`);
|
|
344
352
|
}
|
|
345
353
|
}
|
|
354
|
+
if (assetContext)
|
|
355
|
+
assertImageAssetOutputPaths(assetContext, [resolvedPath]);
|
|
346
356
|
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
347
357
|
writeFileSync(resolvedPath, imgBuf);
|
|
348
358
|
let actualWidth = null;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { readImageAssetContext } from '../utils/image-asset-context.js';
|
|
2
|
-
import { collectReferenceImagePaths, handleError } from './tools-utils.js';
|
|
2
|
+
import { collectReferenceImagePaths, handleError, parseImageTargetFieldPath } from './tools-utils.js';
|
|
3
3
|
export function registerImageContextCommand(tools) {
|
|
4
4
|
tools.command('image-context')
|
|
5
5
|
.description('Read image asset reference and delivery constraints locally; does not generate an image')
|
|
6
6
|
.requiredOption('--asset-path <path>', 'Existing image asset path relative to the project')
|
|
7
|
+
.option('--target-field-path <path>', 'Target image field as an array of keys, e.g. ["endUI","playButton"]', parseImageTargetFieldPath)
|
|
8
|
+
.option('--output-path <path>', 'Intended output path; checks shared asset protection before generation', collectReferenceImagePaths, [])
|
|
9
|
+
.option('--allowed-output-root <path>', 'Additional output directory authorized by the caller; does not allow external references', collectReferenceImagePaths, [])
|
|
7
10
|
.option('--reference-image <path>', 'Explicit local reference; may be repeated', collectReferenceImagePaths, [])
|
|
8
11
|
.option('--no-target-reference', 'Do not use the target schema or current asset as a reference')
|
|
9
12
|
.option('--prompt <text>', 'Generation request used to resolve background intent')
|
|
@@ -13,6 +16,9 @@ export function registerImageContextCommand(tools) {
|
|
|
13
16
|
try {
|
|
14
17
|
console.log(JSON.stringify(await readImageAssetContext(opts.assetPath, process.cwd(), {
|
|
15
18
|
referenceImagePaths: opts.referenceImage, useTargetReference: opts.targetReference,
|
|
19
|
+
...(opts.targetFieldPath ? { targetFieldPath: opts.targetFieldPath } : {}),
|
|
20
|
+
outputPaths: opts.outputPath,
|
|
21
|
+
allowedOutputRoots: opts.allowedOutputRoot,
|
|
16
22
|
prompt: opts.prompt, removeBackground: opts.removeBackground,
|
|
17
23
|
})));
|
|
18
24
|
}
|
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync, readFileSync } from 'fs';
|
|
2
2
|
import { join, parse } from 'path';
|
|
3
3
|
import { tmpdir } from 'os';
|
|
4
|
+
import { InvalidArgumentError } from 'commander';
|
|
4
5
|
export const TMP_DIR = join(tmpdir(), 'playcraft');
|
|
5
6
|
export const MAX_REFERENCE_IMAGES = 8;
|
|
7
|
+
export function parseImageTargetFieldPath(value) {
|
|
8
|
+
let parsed;
|
|
9
|
+
try {
|
|
10
|
+
parsed = JSON.parse(value);
|
|
11
|
+
}
|
|
12
|
+
catch { /* Report the expected shape below. */ }
|
|
13
|
+
if (!Array.isArray(parsed) || !parsed.length || parsed.some(key => typeof key !== 'string' || !key.length)) {
|
|
14
|
+
throw new InvalidArgumentError('Expected an array of field names, e.g. ["endUI","playButton"].');
|
|
15
|
+
}
|
|
16
|
+
return parsed;
|
|
17
|
+
}
|
|
6
18
|
export function ensureTmpDir() {
|
|
7
19
|
mkdirSync(TMP_DIR, { recursive: true });
|
|
8
20
|
}
|
|
@@ -1,27 +1,87 @@
|
|
|
1
|
-
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import JSON5 from 'json5';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
import { extractAssetFields, getByPath, resolveAiImageTargetConstraints, resolveImageBackgroundRequirement, THEME_SCHEMA_PATH, themeDataPath, } from '@playcraft/common/prefab';
|
|
6
|
+
function isWithinDirectory(root, file) {
|
|
7
|
+
const relative = path.relative(root, file);
|
|
8
|
+
return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
9
|
+
}
|
|
10
|
+
/** Resolve existing ancestors so a new workspace need not have been created yet. */
|
|
11
|
+
function realPathWithParents(file) {
|
|
12
|
+
let ancestor = file;
|
|
13
|
+
while (!existsSync(ancestor)) {
|
|
14
|
+
const parent = path.dirname(ancestor);
|
|
15
|
+
if (parent === ancestor)
|
|
16
|
+
throw new Error(`Image path has no existing ancestor: ${file}`);
|
|
17
|
+
ancestor = parent;
|
|
18
|
+
}
|
|
19
|
+
return path.resolve(realpathSync(ancestor), path.relative(ancestor, file));
|
|
20
|
+
}
|
|
21
|
+
/** Resolve aliases and existing parent symlinks, including for a new output file. */
|
|
22
|
+
function canonicalProjectPath(root, input) {
|
|
23
|
+
const mapped = input.startsWith('/project/') ? input.slice('/project/'.length) : input;
|
|
24
|
+
const file = path.resolve(root, mapped);
|
|
25
|
+
if (!isWithinDirectory(root, file)) {
|
|
26
|
+
throw new Error(`Image asset path is outside the project: ${input}`);
|
|
27
|
+
}
|
|
28
|
+
const real = realPathWithParents(file);
|
|
29
|
+
if (!isWithinDirectory(realPathWithParents(root), real)) {
|
|
30
|
+
throw new Error(`Image asset symlink is outside the project: ${input}`);
|
|
31
|
+
}
|
|
32
|
+
return real;
|
|
33
|
+
}
|
|
34
|
+
/** Check every extension a writer may choose before sending a generation request. */
|
|
35
|
+
export function assertImageAssetOutputPaths(context, outputPaths, projectDir = process.cwd(), allowedOutputRoots = []) {
|
|
36
|
+
if (!context.requiresSeparateOutput)
|
|
37
|
+
return;
|
|
38
|
+
const root = path.resolve(projectDir);
|
|
39
|
+
const target = canonicalProjectPath(root, context.targetAssetPath);
|
|
40
|
+
const targetStat = statSync(target);
|
|
41
|
+
const outputRoots = [root, ...allowedOutputRoots.map(outputRoot => path.resolve(root, outputRoot))];
|
|
42
|
+
for (const output of outputPaths) {
|
|
43
|
+
const mapped = output.startsWith('/project/') ? output.slice('/project/'.length) : output;
|
|
44
|
+
const { dir, name } = path.parse(mapped);
|
|
45
|
+
// The response encoding can change the requested extension.
|
|
46
|
+
const candidates = [mapped, ...['.png', '.webp', '.jpg', '.gif', '.bmp'].map(ext => path.join(dir, `${name}${ext}`))];
|
|
47
|
+
for (const candidate of candidates) {
|
|
48
|
+
const candidatePath = path.resolve(root, candidate);
|
|
49
|
+
const outputRoot = outputRoots.find(allowed => isWithinDirectory(allowed, candidatePath));
|
|
50
|
+
if (!outputRoot)
|
|
51
|
+
throw new Error(`Image output path is outside the project and allowed output roots: ${output}`);
|
|
52
|
+
const file = canonicalProjectPath(outputRoot, candidatePath);
|
|
53
|
+
const fileStat = existsSync(file) ? statSync(file) : undefined;
|
|
54
|
+
if (!path.relative(target, file) || (targetStat.ino !== 0 && fileStat?.ino === targetStat.ino && fileStat.dev === targetStat.dev)) {
|
|
55
|
+
throw new Error(`Cannot overwrite shared image asset ${context.targetAssetPath}: other fields have different requirements. Use a separate outputPath, then explicitly update the selected field reference; no shared asset was changed.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
6
60
|
/** Resolve only real files in this project, including legacy /project aliases. */
|
|
7
61
|
function projectFile(root, input) {
|
|
8
62
|
const mapped = input.startsWith('/project/') ? input.slice('/project/'.length) : input;
|
|
9
63
|
const file = path.resolve(root, mapped);
|
|
10
|
-
|
|
11
|
-
if (!file.startsWith(prefix))
|
|
64
|
+
if (!isWithinDirectory(root, file))
|
|
12
65
|
throw new Error(`Image asset path is outside the project: ${input}`);
|
|
13
66
|
const real = realpathSync(file);
|
|
14
|
-
if (!
|
|
67
|
+
if (!isWithinDirectory(realpathSync(root), real))
|
|
15
68
|
throw new Error(`Image asset symlink is outside the project: ${input}`);
|
|
16
69
|
return file;
|
|
17
70
|
}
|
|
18
71
|
/** Read the same per-field constraints used by the asset image dialog. */
|
|
19
72
|
export async function readImageAssetContext(assetPath, projectDir = process.cwd(), options = {}) {
|
|
73
|
+
const targetFieldPath = options.targetFieldPath;
|
|
74
|
+
if (targetFieldPath !== undefined && (!Array.isArray(targetFieldPath)
|
|
75
|
+
|| !targetFieldPath.length || targetFieldPath.some(key => typeof key !== 'string' || !key.length))) {
|
|
76
|
+
throw new Error('targetFieldPath must be a non-empty array of non-empty field names.');
|
|
77
|
+
}
|
|
20
78
|
const root = path.resolve(projectDir);
|
|
21
79
|
const target = projectFile(root, assetPath);
|
|
80
|
+
const realTarget = realpathSync(target);
|
|
81
|
+
const targetStat = statSync(target);
|
|
22
82
|
const relativeTarget = path.relative(root, target).replace(/\\/g, '/');
|
|
23
83
|
const themeMatch = relativeTarget.match(/^src\/theme\/([^/]+)\/assets\//);
|
|
24
|
-
let
|
|
84
|
+
let fields = [];
|
|
25
85
|
const schemaFile = path.join(root, THEME_SCHEMA_PATH);
|
|
26
86
|
if (themeMatch && existsSync(schemaFile)) {
|
|
27
87
|
const schema = JSON5.parse(readFileSync(projectFile(root, THEME_SCHEMA_PATH), 'utf8'));
|
|
@@ -37,70 +97,132 @@ export async function readImageAssetContext(assetPath, projectDir = process.cwd(
|
|
|
37
97
|
const value = typeof configured === 'string' ? configured : candidate.schema.default;
|
|
38
98
|
if (typeof value !== 'string' || /^https?:\/\//i.test(value))
|
|
39
99
|
return false;
|
|
40
|
-
|
|
100
|
+
const configuredPath = value.startsWith('/project/') ? path.resolve(root, value.slice('/project/'.length))
|
|
101
|
+
: path.resolve(themeRoot, value);
|
|
102
|
+
if (configuredPath === target)
|
|
103
|
+
return true;
|
|
104
|
+
try {
|
|
105
|
+
const candidateFile = projectFile(root, configuredPath);
|
|
106
|
+
if (!path.relative(realTarget, realpathSync(candidateFile)))
|
|
107
|
+
return true;
|
|
108
|
+
const candidateStat = statSync(candidateFile);
|
|
109
|
+
return targetStat.ino !== 0 && candidateStat.ino === targetStat.ino && candidateStat.dev === targetStat.dev;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Other fields may reference missing files or paths outside this project.
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
41
115
|
});
|
|
42
|
-
if (matches.length
|
|
43
|
-
throw new Error('Multiple image fields use this asset; specify an unambiguous target asset.');
|
|
44
|
-
field = matches[0];
|
|
45
|
-
if (!field)
|
|
116
|
+
if (!matches.length)
|
|
46
117
|
throw new Error(`No image schema field matches ${relativeTarget}; check theme.data.json5 before generating.`);
|
|
118
|
+
fields = matches;
|
|
119
|
+
if (targetFieldPath && !fields.some(candidate => candidate.path.length === targetFieldPath.length
|
|
120
|
+
&& candidate.path.every((key, index) => key === targetFieldPath[index]))) {
|
|
121
|
+
throw new Error(`targetFieldPath ${JSON.stringify(targetFieldPath)} does not reference ${relativeTarget}. Matching fields: ${matches.map(candidate => JSON.stringify(candidate.path)).join(', ')}.`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (targetFieldPath) {
|
|
125
|
+
throw new Error('targetFieldPath requires an asset with a matching theme schema.');
|
|
47
126
|
}
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
127
|
+
const targetMeta = await sharp(target).metadata();
|
|
128
|
+
let targetHasTransparency;
|
|
129
|
+
const contextForField = async (field) => {
|
|
130
|
+
const xp = field?.schema.xplatform;
|
|
131
|
+
const ext = xp?.ext;
|
|
132
|
+
// Schema references follow the dialog's root/theme conventions; explicit tool paths do not change.
|
|
133
|
+
const schemaReferences = Array.isArray(ext?.reference_image)
|
|
134
|
+
? ext.reference_image.filter(ref => typeof ref === 'string' && ref.trim()).map(ref => {
|
|
135
|
+
const raw = ref.trim().replace(/\\/g, '/');
|
|
136
|
+
const normalized = raw.replace(/^\.\/+/, '').replace(/^\/+/, '').replace(/\/{2,}/g, '/');
|
|
137
|
+
if (/^\/?(?:assets|src|public)\//.test(raw) || normalized.startsWith('src/'))
|
|
138
|
+
return normalized;
|
|
139
|
+
if (!themeMatch || path.isAbsolute(raw) || /^https?:\/\//i.test(raw))
|
|
140
|
+
return raw;
|
|
141
|
+
return path.posix.join('src/theme', themeMatch[1], normalized);
|
|
142
|
+
}) : [];
|
|
143
|
+
const references = options.referenceImagePaths?.length ? options.referenceImagePaths
|
|
144
|
+
: options.useTargetReference === false ? [] : schemaReferences.length ? schemaReferences : [relativeTarget];
|
|
145
|
+
const referenceImagePaths = references.map(ref => /^https?:\/\//i.test(ref) ? ref
|
|
146
|
+
: path.relative(realpathSync(root), canonicalProjectPath(root, ref)).replace(/\\/g, '/'));
|
|
147
|
+
const hasSchemaBackground = typeof ext?.remove_background === 'boolean';
|
|
148
|
+
if (!hasSchemaBackground && targetHasTransparency === undefined) {
|
|
149
|
+
targetHasTransparency = !!targetMeta.hasAlpha && (await sharp(target).stats()).channels.at(-1).min < 250;
|
|
150
|
+
}
|
|
151
|
+
const removeBackground = resolveImageBackgroundRequirement({
|
|
152
|
+
prompt: options.prompt ?? '',
|
|
153
|
+
schemaRemoveBackground: hasSchemaBackground ? ext.remove_background : undefined,
|
|
154
|
+
removeBackground: options.removeBackground,
|
|
155
|
+
fallbackRemoveBackground: targetHasTransparency ?? false,
|
|
156
|
+
});
|
|
157
|
+
const delivery = resolveAiImageTargetConstraints({
|
|
158
|
+
schemaResolution: ext?.resolution,
|
|
159
|
+
schemaMimeType: xp?.mime_type,
|
|
160
|
+
schemaFitMode: xp?.fit_mode ?? ext?.fit_mode,
|
|
161
|
+
schemaRemoveBackground: removeBackground,
|
|
162
|
+
schemaPaddingColor: ext?.padding_color,
|
|
163
|
+
assetPath: relativeTarget,
|
|
164
|
+
referenceWidth: targetMeta.width,
|
|
165
|
+
referenceHeight: targetMeta.height,
|
|
166
|
+
referenceFormat: targetMeta.format,
|
|
167
|
+
});
|
|
168
|
+
if (!['png', 'webp', 'jpeg'].includes(delivery.format))
|
|
169
|
+
throw new Error('Unsupported image delivery format; use a dedicated animation or vector workflow.');
|
|
170
|
+
return {
|
|
171
|
+
targetAssetPath: relativeTarget,
|
|
172
|
+
fieldPath: field?.path,
|
|
173
|
+
referenceImagePaths,
|
|
174
|
+
width: delivery.width, height: delivery.height,
|
|
175
|
+
aspectRatio: typeof ext?.ratio === 'string' ? ext.ratio : `${delivery.width}:${delivery.height}`,
|
|
176
|
+
outputFormat: (removeBackground && delivery.format === 'jpeg' && !delivery.fromSchemaMimeType
|
|
177
|
+
? 'png' : delivery.format),
|
|
178
|
+
outputFormatFromSchema: delivery.fromSchemaMimeType,
|
|
179
|
+
fitMode: delivery.fitMode, removeBackground: delivery.removeBackground, paddingColor: delivery.paddingColor,
|
|
180
|
+
removeBackgroundFromSchema: hasSchemaBackground,
|
|
181
|
+
imageModelRef: typeof ext?.imageModelRef === 'string' ? ext.imageModelRef : undefined,
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
const contexts = [];
|
|
185
|
+
let unresolvedSharedField = false;
|
|
186
|
+
for (const field of fields.length ? fields : [undefined]) {
|
|
187
|
+
try {
|
|
188
|
+
contexts.push(await contextForField(field));
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
const selected = field?.path.length === targetFieldPath?.length
|
|
192
|
+
&& field?.path.every((key, index) => key === targetFieldPath?.[index]);
|
|
193
|
+
if (!targetFieldPath || selected)
|
|
194
|
+
throw error;
|
|
195
|
+
// Invalid sibling requirements still protect the original shared file.
|
|
196
|
+
unresolvedSharedField = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const context = targetFieldPath ? contexts.find(candidate => candidate.fieldPath?.length === targetFieldPath.length
|
|
200
|
+
&& candidate.fieldPath.every((key, index) => key === targetFieldPath[index])) : contexts[0];
|
|
201
|
+
const deliveryKey = ({ fieldPath: _fieldPath, outputFormatFromSchema: _formatSource, removeBackgroundFromSchema: _backgroundSource, aspectRatio, ...delivery }) => {
|
|
202
|
+
const ratio = aspectRatio.match(/^\s*(\d+(?:\.\d+)?)\s*:\s*(\d+(?:\.\d+)?)\s*$/);
|
|
203
|
+
return JSON.stringify({ ...delivery, aspectRatio: ratio && Number(ratio[2]) > 0 ? Number(ratio[1]) / Number(ratio[2]) : aspectRatio });
|
|
204
|
+
};
|
|
205
|
+
const conflicting = unresolvedSharedField || contexts.some(candidate => deliveryKey(candidate) !== deliveryKey(context));
|
|
206
|
+
if (conflicting && !targetFieldPath) {
|
|
207
|
+
throw new Error(`Multiple image fields use ${relativeTarget} with different references or delivery constraints. Set targetFieldPath (or --target-field-path) to one of: ${fields.map(field => JSON.stringify(field.path)).join(', ')}.`);
|
|
208
|
+
}
|
|
209
|
+
if (targetFieldPath) {
|
|
210
|
+
// A separate output retains exactly the selected field's requirements.
|
|
211
|
+
if (conflicting || contexts.some(candidate => (candidate.outputFormatFromSchema && !context.outputFormatFromSchema)
|
|
212
|
+
|| (candidate.removeBackgroundFromSchema && !context.removeBackgroundFromSchema)))
|
|
213
|
+
context.requiresSeparateOutput = true;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
context.outputFormatFromSchema = contexts.some(candidate => candidate.outputFormatFromSchema);
|
|
217
|
+
context.removeBackgroundFromSchema = contexts.some(candidate => candidate.removeBackgroundFromSchema);
|
|
218
|
+
}
|
|
219
|
+
assertImageAssetOutputPaths(context, options.outputPaths ?? [], root, options.allowedOutputRoots);
|
|
220
|
+
if (context.referenceImagePaths.length > 8)
|
|
64
221
|
throw new Error('At most 8 reference images are allowed.');
|
|
65
|
-
for (const ref of referenceImagePaths) {
|
|
222
|
+
for (const ref of context.referenceImagePaths) {
|
|
66
223
|
if (/^https?:\/\//i.test(ref))
|
|
67
224
|
throw new Error('Download the schema reference into the project before generating.');
|
|
68
225
|
await sharp(projectFile(root, ref)).metadata();
|
|
69
226
|
}
|
|
70
|
-
|
|
71
|
-
const hasSchemaBackground = typeof ext?.remove_background === 'boolean';
|
|
72
|
-
const targetStats = !hasSchemaBackground && targetMeta.hasAlpha ? await sharp(target).stats() : undefined;
|
|
73
|
-
const targetHasTransparency = !!targetStats && targetStats.channels.at(-1).min < 250;
|
|
74
|
-
const removeBackground = resolveImageBackgroundRequirement({
|
|
75
|
-
prompt: options.prompt ?? '',
|
|
76
|
-
schemaRemoveBackground: hasSchemaBackground ? ext.remove_background : undefined,
|
|
77
|
-
removeBackground: options.removeBackground,
|
|
78
|
-
fallbackRemoveBackground: targetHasTransparency,
|
|
79
|
-
});
|
|
80
|
-
const delivery = resolveAiImageTargetConstraints({
|
|
81
|
-
schemaResolution: ext?.resolution,
|
|
82
|
-
schemaMimeType: xp?.mime_type,
|
|
83
|
-
schemaFitMode: xp?.fit_mode ?? ext?.fit_mode,
|
|
84
|
-
schemaRemoveBackground: removeBackground,
|
|
85
|
-
schemaPaddingColor: ext?.padding_color,
|
|
86
|
-
assetPath: relativeTarget,
|
|
87
|
-
referenceWidth: targetMeta.width,
|
|
88
|
-
referenceHeight: targetMeta.height,
|
|
89
|
-
referenceFormat: targetMeta.format,
|
|
90
|
-
});
|
|
91
|
-
if (!['png', 'webp', 'jpeg'].includes(delivery.format))
|
|
92
|
-
throw new Error('Unsupported image delivery format; use a dedicated animation or vector workflow.');
|
|
93
|
-
return {
|
|
94
|
-
targetAssetPath: relativeTarget,
|
|
95
|
-
fieldPath: field?.path,
|
|
96
|
-
referenceImagePaths,
|
|
97
|
-
width: delivery.width, height: delivery.height,
|
|
98
|
-
aspectRatio: typeof ext?.ratio === 'string' ? ext.ratio : `${delivery.width}:${delivery.height}`,
|
|
99
|
-
outputFormat: (removeBackground && delivery.format === 'jpeg' && !delivery.fromSchemaMimeType
|
|
100
|
-
? 'png' : delivery.format),
|
|
101
|
-
outputFormatFromSchema: delivery.fromSchemaMimeType,
|
|
102
|
-
fitMode: delivery.fitMode, removeBackground: delivery.removeBackground, paddingColor: delivery.paddingColor,
|
|
103
|
-
removeBackgroundFromSchema: hasSchemaBackground,
|
|
104
|
-
imageModelRef: typeof ext?.imageModelRef === 'string' ? ext.imageModelRef : undefined,
|
|
105
|
-
};
|
|
227
|
+
return context;
|
|
106
228
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@playcraft/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.41-prod",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"@gltf-transform/core": "^4.3.0",
|
|
46
46
|
"@gltf-transform/extensions": "^4.3.0",
|
|
47
47
|
"@gltf-transform/functions": "^4.3.0",
|
|
48
|
-
"@playcraft/build": "^0.1.
|
|
49
|
-
"@playcraft/common": "^0.1.
|
|
48
|
+
"@playcraft/build": "^0.1.40-beta",
|
|
49
|
+
"@playcraft/common": "^0.1.40-beta",
|
|
50
50
|
"chokidar": "^4.0.3",
|
|
51
51
|
"commander": "^13.1.0",
|
|
52
52
|
"cors": "^2.8.6",
|