assetmax
Version:
Manifest-driven asset management system with contract-based generation
296 lines • 12.4 kB
JavaScript
;
/**
* TOML Manifest Auto-Generator
* Scans existing assets and generates manifest.toml automatically
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateManifest = exports.ManifestGenerator = void 0;
const fs_1 = require("fs");
const path = __importStar(require("path"));
const toml = __importStar(require("@iarna/toml"));
class ManifestGenerator {
supportedFormats = new Set([
'.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg',
'.mp4', '.webm', '.mov'
]);
categoryMapping = {
'icons': 'icons',
'avatars': 'illustrations',
'photos': 'photos',
'videos': 'videos',
'illustrations': 'illustrations',
'hero': 'photos',
'onboarding': 'illustrations'
};
async generateFromDirectory(assetsDir, config) {
console.log(`🔍 Scanning assets in: ${assetsDir}`);
const assets = await this.scanAssets(assetsDir);
const groupedAssets = this.groupAssets(assets);
const manifest = this.buildManifest(groupedAssets, config);
return toml.stringify(manifest);
}
async generateFromExistingAssets(assetsFilePath, config) {
console.log(`📖 Reading existing assets from: ${assetsFilePath}`);
const assetsContent = await fs_1.promises.readFile(assetsFilePath, 'utf-8');
const assets = await this.parseAssetsFile(assetsContent);
const manifest = this.buildManifestFromAssets(assets, config);
return toml.stringify(manifest);
}
async scanAssets(directory) {
const assets = [];
const scan = async (dir, relativePath = '') => {
const entries = await fs_1.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const relPath = path.join(relativePath, entry.name);
if (entry.isDirectory()) {
await scan(fullPath, relPath);
}
else if (entry.isFile()) {
const ext = path.extname(entry.name).toLowerCase();
if (this.supportedFormats.has(ext)) {
assets.push(this.createAssetInfo(entry.name, relPath));
}
}
}
};
await scan(directory);
return assets;
}
createAssetInfo(filename, relativePath) {
const name = path.basename(filename, path.extname(filename));
const pathParts = relativePath.split(path.sep);
const category = this.inferCategory(pathParts);
const subcategory = this.inferSubcategory(pathParts);
const assetInfo = {
name: this.sanitizeName(name),
path: relativePath,
extension: path.extname(filename).slice(1),
category,
estimatedPrompt: this.generatePrompt(name, category, subcategory),
altText: this.generateAltText(name, category)
};
if (subcategory !== undefined) {
assetInfo.subcategory = subcategory;
}
return assetInfo;
}
inferCategory(pathParts) {
for (const part of pathParts) {
const normalized = part.toLowerCase();
if (this.categoryMapping[normalized]) {
return this.categoryMapping[normalized] || 'illustrations';
}
}
// Default categorization based on common patterns
const lastPart = pathParts[pathParts.length - 2]?.toLowerCase() || '';
return this.categoryMapping[lastPart] || 'illustrations';
}
inferSubcategory(pathParts) {
if (pathParts.length > 2) {
return pathParts[pathParts.length - 2];
}
return undefined;
}
generatePrompt(name, category, subcategory) {
const cleanName = name.replace(/[_-]/g, ' ');
const context = subcategory ? `${subcategory} ` : '';
switch (category) {
case 'icons':
return `Simple ${context}icon: ${cleanName} in modern flat design style. Clean minimalist vector with rounded edges, perfect for UI navigation.`;
case 'illustrations':
return `Duolingo-style ${context}illustration: ${cleanName} with friendly cartoon aesthetic, bright colors, rounded shapes, optimistic mood.`;
case 'photos':
return `Professional lifestyle photography: ${cleanName} scene with natural lighting, authentic expressions, high quality composition.`;
case 'videos':
return `Professional video content: ${cleanName} with smooth cinematography, engaging visuals, appropriate pacing for user experience.`;
default:
return `High-quality ${category} asset: ${cleanName} designed for modern user interface with attention to accessibility and user experience.`;
}
}
generateAltText(name, category) {
const cleanName = name.replace(/[_-]/g, ' ');
return `${cleanName} ${category === 'icons' ? 'icon' : category === 'photos' ? 'photo' : 'illustration'}`;
}
sanitizeName(name) {
return name
.replace(/[^a-zA-Z0-9_-]/g, '_')
.replace(/_{2,}/g, '_')
.replace(/^_|_$/g, '');
}
groupAssets(assets) {
const groups = {};
for (const asset of assets) {
const groupKey = asset.subcategory
? `${asset.category}_${asset.subcategory}`
: asset.category;
if (!groups[groupKey]) {
groups[groupKey] = [];
}
groups[groupKey].push(asset);
}
return groups;
}
buildManifest(groupedAssets, config) {
const manifest = {
meta: {
name: config.projectName,
version: '1.0.0',
description: `Asset manifest for ${config.projectName}`,
base_url: config.baseUrl,
generated_at: new Date().toISOString()
},
categories: {
illustrations: { path: 'illustrations', type: 'image', formats: ['png', 'jpg', 'webp'] },
photos: { path: 'photos', type: 'image', formats: ['webp', 'jpg'] },
videos: { path: 'videos', type: 'video', formats: ['mp4', 'webm'] },
icons: { path: 'icons', type: 'icon', formats: ['png', 'svg'] }
},
assets: {},
build: {
output_dir: 'src/lib',
output_file: 'assets.ts',
type_definitions: true,
fallback_image: `${config.baseUrl}/stubs/placeholder.png`
},
cli: {
models: { images: 'flux-kontext', videos: 'veo-3-fast' },
pricing: { 'flux-kontext': 0.015, 'veo-3-fast': 0.050 },
output_dir: config.outputDir
},
generation: {
skip_existing: true,
convert_formats: true,
verify_output: true,
max_retries: 3
}
};
// Add asset groups
for (const [groupName, assets] of Object.entries(groupedAssets)) {
const firstAsset = assets[0];
if (!firstAsset) {
throw new Error(`No assets found for group: ${groupName}`);
}
const groupConfig = {
description: `${groupName.replace(/_/g, ' ')} assets`,
category: firstAsset.category,
format: this.getDefaultFormat(firstAsset.extension),
generation_model: firstAsset.category === 'videos' ? 'veo-3-fast' : 'flux-kontext'
};
if (firstAsset.subcategory) {
groupConfig.subcategory = firstAsset.subcategory;
}
// Add individual assets
for (const asset of assets) {
groupConfig[asset.name] = {
prompt: asset.estimatedPrompt,
alt: asset.altText,
aspect_ratio: this.inferAspectRatio(asset.name, asset.category)
};
}
manifest.assets[groupName] = groupConfig;
}
return manifest;
}
async parseAssetsFile(content) {
// Parse TypeScript assets file and extract asset definitions
const assets = [];
// Extract asset definitions using regex (simplified - could be improved with AST parsing)
const assetMatches = content.matchAll(/(\w+):\s*\{[^}]+src:\s*`[^`]+\/([^`]+)`[^}]+alt:\s*'([^']+)'[^}]*\}/g);
for (const match of assetMatches) {
const [, name, filename, alt] = match;
if (!name || !filename || !alt)
continue;
const ext = path.extname(filename).slice(1);
const pathParts = filename.split('/');
const assetInfo = {
name,
path: filename,
extension: ext,
category: this.inferCategory(pathParts),
estimatedPrompt: `Generated prompt for ${name}`,
altText: alt
};
const subcategory = this.inferSubcategory(pathParts);
if (subcategory !== undefined) {
assetInfo.subcategory = subcategory;
}
assets.push(assetInfo);
}
return assets;
}
buildManifestFromAssets(assets, config) {
const groupedAssets = this.groupAssets(assets);
return this.buildManifest(groupedAssets, config);
}
getDefaultFormat(extension) {
const formatMap = {
'jpg': 'png', 'jpeg': 'png', 'png': 'png', 'webp': 'webp',
'svg': 'png', 'gif': 'png',
'mp4': 'mp4', 'webm': 'mp4', 'mov': 'mp4'
};
return formatMap[extension.toLowerCase()] || 'png';
}
inferAspectRatio(name, category) {
const nameLower = name.toLowerCase();
if (nameLower.includes('hero') || nameLower.includes('onboarding')) {
return '9:16';
}
if (category === 'icons' || nameLower.includes('avatar')) {
return '1:1';
}
if (category === 'videos') {
return '16:9';
}
return '1:1';
}
}
exports.ManifestGenerator = ManifestGenerator;
// CLI interface
async function generateManifest(options) {
const generator = new ManifestGenerator();
const config = {
projectName: options.projectName,
baseUrl: options.baseUrl || '/assets',
outputDir: options.outputDir || 'public/assets'
};
let manifestContent;
if (options.assetsDir) {
manifestContent = await generator.generateFromDirectory(options.assetsDir, config);
}
else if (options.assetsFile) {
manifestContent = await generator.generateFromExistingAssets(options.assetsFile, config);
}
else {
throw new Error('Either assetsDir or assetsFile must be provided');
}
const outputPath = options.outputFile || 'asset-manifest.toml';
await fs_1.promises.writeFile(outputPath, manifestContent, 'utf-8');
console.log(`✅ Generated manifest: ${outputPath}`);
}
exports.generateManifest = generateManifest;
//# sourceMappingURL=manifest-generator.js.map