restyle-sprites 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/dist/cli.js ADDED
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { config as loadEnv } from 'dotenv';
5
+ import inquirer from 'inquirer';
6
+ import { AssetPackWriter } from './AssetPackWriter.js';
7
+ import { BatchGenerator } from './BatchGenerator.js';
8
+ import { loadConfig } from './config.js';
9
+ import { ImageProcessor } from './ImageProcessor.js';
10
+ import { OpenAIImageClient } from './OpenAIImageClient.js';
11
+ import { PixelArtPostProcessor } from './PixelArtPostProcessor.js';
12
+ import { StyleExplorer } from './StyleExplorer.js';
13
+ function readArg(name) {
14
+ const args = process.argv.slice(2);
15
+ const direct = args.find((arg) => arg.startsWith(`--${name}=`));
16
+ if (direct) {
17
+ return direct.slice(name.length + 3);
18
+ }
19
+ const index = args.indexOf(`--${name}`);
20
+ if (index >= 0 && args[index + 1]) {
21
+ return args[index + 1];
22
+ }
23
+ return undefined;
24
+ }
25
+ function requireArg(name) {
26
+ const value = readArg(name)?.trim();
27
+ if (!value) {
28
+ throw new Error(`Missing required argument --${name}`);
29
+ }
30
+ return value;
31
+ }
32
+ function loadLocalEnvironment(configDir) {
33
+ const envPath = path.join(configDir, '.env');
34
+ loadEnv({ path: envPath, quiet: true });
35
+ }
36
+ async function resolvePackName() {
37
+ const fromArg = readArg('name');
38
+ if (fromArg && fromArg.trim().length > 0) {
39
+ return fromArg.trim();
40
+ }
41
+ const answer = await inquirer.prompt([
42
+ {
43
+ type: 'input',
44
+ name: 'packName',
45
+ message: 'Asset pack name',
46
+ validate: (value) => (value.trim().length > 0 ? true : 'Pack name is required.'),
47
+ },
48
+ ]);
49
+ return answer.packName.trim();
50
+ }
51
+ async function ensurePackDirectories(outputRootDir, packName) {
52
+ const packDir = path.join(outputRootDir, packName);
53
+ const spritesDir = path.join(packDir, 'sprites');
54
+ await fs.mkdir(spritesDir, { recursive: true });
55
+ return { packDir, spritesDir };
56
+ }
57
+ async function exploreStyle(packName, workspaceRoot, outputRootDir, sampleSprites) {
58
+ const { packDir } = await ensurePackDirectories(outputRootDir, packName);
59
+ const imageProcessor = new ImageProcessor();
60
+ const styleExplorer = new StyleExplorer(new OpenAIImageClient(), {
61
+ workspaceRoot,
62
+ sampleSprites,
63
+ imageProcessor,
64
+ });
65
+ const result = await styleExplorer.runInteractive(packDir);
66
+ const writer = new AssetPackWriter();
67
+ await writer.writeManifest({
68
+ packDir,
69
+ packName,
70
+ description: `Style exploration draft for ${packName}. Style prompt: ${result.prompt}`,
71
+ styleReferenceImage: path.basename(result.styleReferencePath),
72
+ assets: [],
73
+ });
74
+ await writer.updatePackIndex({ packsRootDir: outputRootDir, activePack: 'default' });
75
+ console.log(`Style reference stored at ${result.styleReferencePath}`);
76
+ }
77
+ async function exploreStyleOnce(packName, styleDirection, workspaceRoot, outputRootDir, sampleSprites) {
78
+ const { packDir } = await ensurePackDirectories(outputRootDir, packName);
79
+ const client = new OpenAIImageClient();
80
+ const imageProcessor = new ImageProcessor();
81
+ const sampleSheet = await StyleExplorer.buildSampleSheetFromSources(workspaceRoot, imageProcessor, sampleSprites);
82
+ const sampleSheetPath = path.join(packDir, 'style-source-sample.png');
83
+ await fs.writeFile(sampleSheetPath, sampleSheet);
84
+ const stylePrompt = StyleExplorer.buildStyleReferencePrompt(styleDirection);
85
+ const styleResult = await client.generateStyleReference(stylePrompt, sampleSheetPath);
86
+ const writer = new AssetPackWriter();
87
+ const styleReferencePath = path.join(packDir, 'style-reference.png');
88
+ await fs.writeFile(styleReferencePath, styleResult.image);
89
+ await writer.writeManifest({
90
+ packDir,
91
+ packName,
92
+ description: `Style exploration draft for ${packName}. Style direction: ${styleDirection}`,
93
+ styleReferenceImage: path.basename(styleReferencePath),
94
+ assets: [],
95
+ });
96
+ await writer.updatePackIndex({ packsRootDir: outputRootDir, activePack: 'default' });
97
+ console.log(`Style reference stored at ${styleReferencePath}`);
98
+ }
99
+ async function readStylePromptFromManifest(packDir) {
100
+ const manifestPath = path.join(packDir, 'manifest.json');
101
+ try {
102
+ const raw = await fs.readFile(manifestPath, 'utf8');
103
+ const parsed = JSON.parse(raw);
104
+ if (!parsed.description) {
105
+ return null;
106
+ }
107
+ const match = parsed.description.match(/Style (?:direction|prompt):\s*(.+)$/i);
108
+ return match?.[1]?.trim() ?? null;
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ }
114
+ async function generatePack(params) {
115
+ const { packDir } = await ensurePackDirectories(params.outputRootDir, params.packName);
116
+ const styleReferencePath = path.join(packDir, 'style-reference.png');
117
+ await fs.access(styleReferencePath);
118
+ const styleDirectionFromManifest = await readStylePromptFromManifest(packDir);
119
+ const styleDirection = params.stylePromptArg?.trim() ??
120
+ styleDirectionFromManifest ??
121
+ 'Stylized 2D game sprites, cohesive palette, clean edges, high contrast, tiny-sprite readability';
122
+ const batchGenerator = new BatchGenerator(new OpenAIImageClient(), new ImageProcessor(), new PixelArtPostProcessor());
123
+ const assets = await batchGenerator.generatePackAssets({
124
+ workspaceRoot: params.workspaceRoot,
125
+ assets: params.assets,
126
+ styleReferencePath,
127
+ outputPackDir: packDir,
128
+ stylePrompt: styleDirection,
129
+ });
130
+ const writer = new AssetPackWriter();
131
+ await writer.writeManifest({
132
+ packDir,
133
+ packName: params.packName,
134
+ description: `AI restyled asset pack: ${params.packName}`,
135
+ styleReferenceImage: 'style-reference.png',
136
+ assets,
137
+ });
138
+ await writer.updatePackIndex({ packsRootDir: params.outputRootDir, activePack: params.packName });
139
+ console.log(`Asset pack generated: ${packDir}`);
140
+ }
141
+ async function initDefaultPack(workspaceRoot, outputRootDir, assetsConfig) {
142
+ const imageProcessor = new ImageProcessor();
143
+ const { packDir } = await ensurePackDirectories(outputRootDir, 'default');
144
+ const assets = [];
145
+ for (const asset of assetsConfig) {
146
+ const sourcePath = path.join(workspaceRoot, asset.sourceFile);
147
+ const outputPath = path.join(packDir, asset.outputFile);
148
+ await imageProcessor.convertToPng(sourcePath, outputPath);
149
+ assets.push({
150
+ id: asset.id,
151
+ metadata: asset.metadata,
152
+ file: asset.outputFile,
153
+ kind: asset.kind,
154
+ width: asset.width,
155
+ height: asset.height,
156
+ frameWidth: asset.kind === 'spritesheet' ? asset.frameWidth : undefined,
157
+ frameHeight: asset.kind === 'spritesheet' ? asset.frameHeight : undefined,
158
+ frameCount: asset.kind === 'spritesheet' ? asset.frameCount : undefined,
159
+ frameDirection: asset.kind === 'spritesheet' ? asset.frameDirection : undefined,
160
+ });
161
+ }
162
+ const writer = new AssetPackWriter();
163
+ await writer.writeManifest({
164
+ packDir,
165
+ packName: 'default',
166
+ description: 'Baseline asset pack converted from source assets',
167
+ assets,
168
+ });
169
+ await writer.updatePackIndex({ packsRootDir: outputRootDir, activePack: 'default' });
170
+ console.log('Default asset pack generated.');
171
+ }
172
+ function printHelp() {
173
+ console.log([
174
+ 'restyle-sprites',
175
+ '',
176
+ 'Commands:',
177
+ ' explore --name <pack> --config <path>',
178
+ ' explore-once --name <pack> --config <path> --prompt "<style direction>"',
179
+ ' generate --name <pack> --config <path> [--style "<style direction>"]',
180
+ ' init-default --config <path>',
181
+ '',
182
+ 'Notes:',
183
+ ' - --config supports .json, .yaml, .yml',
184
+ ' - all source/output paths are resolved relative to the config file directory',
185
+ ].join('\n'));
186
+ }
187
+ async function main() {
188
+ const command = process.argv[2];
189
+ if (!command || command === '--help' || command === '-h' || command === 'help') {
190
+ printHelp();
191
+ return;
192
+ }
193
+ const configArg = readArg('config') ?? 'restyle.config.json';
194
+ const { configDir, config } = await loadConfig(configArg);
195
+ loadLocalEnvironment(configDir);
196
+ const workspaceRoot = configDir;
197
+ const outputRootDir = path.resolve(configDir, config.outputDir);
198
+ await fs.mkdir(outputRootDir, { recursive: true });
199
+ if (command === 'explore') {
200
+ const packName = await resolvePackName();
201
+ await exploreStyle(packName, workspaceRoot, outputRootDir, config.sampleSprites);
202
+ return;
203
+ }
204
+ if (command === 'explore-once') {
205
+ const packName = await resolvePackName();
206
+ const styleDirection = requireArg('prompt');
207
+ await exploreStyleOnce(packName, styleDirection, workspaceRoot, outputRootDir, config.sampleSprites);
208
+ return;
209
+ }
210
+ if (command === 'generate') {
211
+ const packName = await resolvePackName();
212
+ await generatePack({
213
+ packName,
214
+ workspaceRoot,
215
+ outputRootDir,
216
+ stylePromptArg: readArg('style'),
217
+ assets: config.assets,
218
+ });
219
+ return;
220
+ }
221
+ if (command === 'init-default') {
222
+ await initDefaultPack(workspaceRoot, outputRootDir, config.assets);
223
+ return;
224
+ }
225
+ console.error('Unknown command. Use one of: explore, explore-once, generate, init-default');
226
+ process.exitCode = 1;
227
+ }
228
+ void main();
@@ -0,0 +1,8 @@
1
+ import { RestyleSpritesConfig } from './types.js';
2
+ interface LoadedConfig {
3
+ configPath: string;
4
+ configDir: string;
5
+ config: RestyleSpritesConfig;
6
+ }
7
+ export declare function loadConfig(configPathArg: string): Promise<LoadedConfig>;
8
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,91 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import yaml from 'js-yaml';
4
+ function isObject(value) {
5
+ return typeof value === 'object' && value !== null;
6
+ }
7
+ function assertString(value, field) {
8
+ if (typeof value !== 'string' || value.trim().length === 0) {
9
+ throw new Error(`Invalid config field "${field}": expected non-empty string.`);
10
+ }
11
+ return value.trim();
12
+ }
13
+ function assertNumber(value, field) {
14
+ if (typeof value !== 'number' || Number.isNaN(value)) {
15
+ throw new Error(`Invalid config field "${field}": expected number.`);
16
+ }
17
+ return value;
18
+ }
19
+ function parseAssetDefinition(value, index) {
20
+ if (!isObject(value)) {
21
+ throw new Error(`Invalid config field "assets[${index}]": expected object.`);
22
+ }
23
+ const kind = assertString(value.kind, `assets[${index}].kind`);
24
+ const base = {
25
+ id: assertString(value.id, `assets[${index}].id`),
26
+ sourceFile: assertString(value.sourceFile, `assets[${index}].sourceFile`),
27
+ outputFile: assertString(value.outputFile, `assets[${index}].outputFile`),
28
+ kind,
29
+ category: value.category,
30
+ width: assertNumber(value.width, `assets[${index}].width`),
31
+ height: assertNumber(value.height, `assets[${index}].height`),
32
+ promptHint: assertString(value.promptHint, `assets[${index}].promptHint`),
33
+ metadata: isObject(value.metadata) ? value.metadata : undefined,
34
+ };
35
+ if (kind === 'image') {
36
+ return {
37
+ ...base,
38
+ kind: 'image',
39
+ };
40
+ }
41
+ if (kind === 'spritesheet') {
42
+ return {
43
+ ...base,
44
+ kind: 'spritesheet',
45
+ frameWidth: assertNumber(value.frameWidth, `assets[${index}].frameWidth`),
46
+ frameHeight: assertNumber(value.frameHeight, `assets[${index}].frameHeight`),
47
+ frameCount: assertNumber(value.frameCount, `assets[${index}].frameCount`),
48
+ frameDirection: assertString(value.frameDirection, `assets[${index}].frameDirection`),
49
+ };
50
+ }
51
+ throw new Error(`Invalid config field "assets[${index}].kind": expected "image" or "spritesheet".`);
52
+ }
53
+ function validateConfig(data) {
54
+ if (!isObject(data)) {
55
+ throw new Error('Invalid config: expected object at root.');
56
+ }
57
+ const assetsRaw = data.assets;
58
+ if (!Array.isArray(assetsRaw) || assetsRaw.length === 0) {
59
+ throw new Error('Invalid config field "assets": expected non-empty array.');
60
+ }
61
+ const sampleSpritesRaw = data.sampleSprites;
62
+ if (!Array.isArray(sampleSpritesRaw) || sampleSpritesRaw.length === 0) {
63
+ throw new Error('Invalid config field "sampleSprites": expected non-empty array.');
64
+ }
65
+ return {
66
+ outputDir: assertString(data.outputDir, 'outputDir'),
67
+ assets: assetsRaw.map((asset, index) => parseAssetDefinition(asset, index)),
68
+ sampleSprites: sampleSpritesRaw.map((sample, index) => assertString(sample, `sampleSprites[${index}]`)),
69
+ defaultActivePack: typeof data.defaultActivePack === 'string' && data.defaultActivePack.trim().length > 0
70
+ ? data.defaultActivePack.trim()
71
+ : undefined,
72
+ };
73
+ }
74
+ export async function loadConfig(configPathArg) {
75
+ const configPath = path.resolve(configPathArg);
76
+ const configDir = path.dirname(configPath);
77
+ const raw = await fs.readFile(configPath, 'utf8');
78
+ const ext = path.extname(configPath).toLowerCase();
79
+ const parsed = ext === '.yaml' || ext === '.yml'
80
+ ? yaml.load(raw)
81
+ : ext === '.json'
82
+ ? JSON.parse(raw)
83
+ : (() => {
84
+ throw new Error(`Unsupported config extension "${ext}". Use .json, .yaml, or .yml`);
85
+ })();
86
+ return {
87
+ configPath,
88
+ configDir,
89
+ config: validateConfig(parsed),
90
+ };
91
+ }
@@ -0,0 +1,8 @@
1
+ export * from './types.js';
2
+ export * from './config.js';
3
+ export * from './AssetPackWriter.js';
4
+ export * from './BatchGenerator.js';
5
+ export * from './ImageProcessor.js';
6
+ export * from './OpenAIImageClient.js';
7
+ export * from './PixelArtPostProcessor.js';
8
+ export * from './StyleExplorer.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export * from './types.js';
2
+ export * from './config.js';
3
+ export * from './AssetPackWriter.js';
4
+ export * from './BatchGenerator.js';
5
+ export * from './ImageProcessor.js';
6
+ export * from './OpenAIImageClient.js';
7
+ export * from './PixelArtPostProcessor.js';
8
+ export * from './StyleExplorer.js';
@@ -0,0 +1,54 @@
1
+ export type AssetKind = 'image' | 'spritesheet';
2
+ export type AssetCategory = 'character' | 'resource' | 'effect' | 'prop' | 'icon' | 'scene' | 'font';
3
+ export interface AssetDefinitionBase {
4
+ id: string;
5
+ sourceFile: string;
6
+ outputFile: string;
7
+ kind: AssetKind;
8
+ category?: AssetCategory;
9
+ width: number;
10
+ height: number;
11
+ promptHint: string;
12
+ metadata?: Record<string, unknown>;
13
+ }
14
+ export interface ImageAssetDefinition extends AssetDefinitionBase {
15
+ kind: 'image';
16
+ }
17
+ export interface SpriteSheetAssetDefinition extends AssetDefinitionBase {
18
+ kind: 'spritesheet';
19
+ frameWidth: number;
20
+ frameHeight: number;
21
+ frameCount: number;
22
+ frameDirection: 'vertical' | 'horizontal';
23
+ }
24
+ export type AssetDefinition = ImageAssetDefinition | SpriteSheetAssetDefinition;
25
+ export interface StyleReference {
26
+ packName: string;
27
+ prompt: string;
28
+ imagePath: string;
29
+ }
30
+ export interface AssetPackManifestEntry {
31
+ id: string;
32
+ file: string;
33
+ kind: AssetKind;
34
+ width: number;
35
+ height: number;
36
+ frameWidth?: number;
37
+ frameHeight?: number;
38
+ frameCount?: number;
39
+ frameDirection?: 'vertical' | 'horizontal';
40
+ metadata?: Record<string, unknown>;
41
+ }
42
+ export interface AssetPackManifest {
43
+ name: string;
44
+ description: string;
45
+ createdAt: string;
46
+ styleReferenceImage?: string;
47
+ assets: AssetPackManifestEntry[];
48
+ }
49
+ export interface RestyleSpritesConfig {
50
+ outputDir: string;
51
+ assets: AssetDefinition[];
52
+ sampleSprites: string[];
53
+ defaultActivePack?: string;
54
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "restyle-sprites",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered sprite restyling pipeline for consistent game asset packs",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/lx-0/restyle-sprites.git"
10
+ },
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "bin": {
14
+ "restyle-sprites": "./dist/cli.js"
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "default": "./dist/index.js"
20
+ }
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE",
26
+ ".env.example"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "provenance": true,
31
+ "registry": "https://registry.npmjs.org/"
32
+ },
33
+ "dependencies": {
34
+ "dotenv": "^17.3.1",
35
+ "image-q": "^4.0.0",
36
+ "inquirer": "^13.3.0",
37
+ "js-yaml": "^4.1.0",
38
+ "openai": "^6.25.0",
39
+ "sharp": "^0.34.5"
40
+ },
41
+ "devDependencies": {
42
+ "@changesets/cli": "^2.29.5",
43
+ "@types/js-yaml": "^4.0.9",
44
+ "@types/node": "^25.3.2",
45
+ "tsx": "^4.21.0",
46
+ "typescript": "^5.9.2",
47
+ "vitest": "^2.1.4"
48
+ },
49
+ "scripts": {
50
+ "build": "tsc -p tsconfig.json",
51
+ "dev": "tsx src/cli.ts",
52
+ "typecheck": "tsc -p tsconfig.json --noEmit",
53
+ "test": "vitest run",
54
+ "changeset": "changeset",
55
+ "version-packages": "changeset version",
56
+ "release": "changeset publish"
57
+ }
58
+ }