vaporwaver-ts
Version:
TypeScript version of the Vaporwaver Python library. Vaporwaver is a Python library for generating vaporwave image art.
254 lines (221 loc) • 9.55 kB
text/typescript
import { spawn } from 'child_process';
import { existsSync, promises as fs } from 'fs';
import path, { dirname, join } from 'path';
import type { PathLike } from 'fs';
import { DependencyChecker } from './utils/dependency-checker';
import { logger } from './utils/logger';
import { fileURLToPath } from 'url';
import { mkdir } from 'fs/promises';
export const validGradients = [
"none", "autumn", "bone", "jet", "winter", "rainbow", "ocean",
"summer", "spring", "cool", "hsv", "pink", "hot", "parula",
"magma", "inferno", "plasma", "viridis", "cividis", "deepgreen"
] as const;
export type GradientType = typeof validGradients[number];
export interface IFlag {
background?: string | PathLike;
misc?: string | PathLike;
miscPosX?: number;
miscPosY?: number;
miscScale?: number;
miscRotate?: number;
characterPath: string;
characterXPos?: number;
characterYPos?: number;
characterScale?: number;
characterRotate?: number;
characterGlitch?: number;
characterGlitchSeed?: number;
characterGradient?: GradientType;
crt?: boolean;
outputPath?: PathLike;
characterOnly?: boolean;
miscAboveCharacter?: boolean;
tmpDir?: string;
}
export class VaporwaverError extends Error {
constructor(message: string, public readonly details: any = {}) {
super(message);
this.name = 'VaporwaverError';
}
}
const isValidPngFile = async (filePath: string): Promise<boolean> => {
try {
const buffer = await fs.readFile(filePath, { flag: 'r' });
return buffer.length >= 8 &&
buffer[0] === 0x89 &&
buffer[1] === 0x50 &&
buffer[2] === 0x4E &&
buffer[3] === 0x47;
} catch {
return false;
}
};
export const getModulePath = (): string => {
try {
// En mode développement (src) ou production (dist)
const currentFilePath = fileURLToPath(import.meta.url);
return dirname(dirname(currentFilePath));
} catch {
return process.cwd();
}
};
export async function vaporwaver(flags: IFlag): Promise<void> {
logger.info('Starting vaporwaver process', { flags });
try {
// Check dependencies first
await DependencyChecker.checkPython();
await DependencyChecker.checkPythonDependencies();
const rootPath = getModulePath();
const pyScript = join(rootPath, 'vaporwaver.py');
const packageTmpDir = join(rootPath, 'tmp');
// S'assurer que le dossier existe
await mkdir(packageTmpDir, { recursive: true });
// Si un dossier tmp externe est fourni, l'utiliser en priorité
const tmpDir = flags.tmpDir || packageTmpDir;
process.env.VAPORWAVER_TMP = tmpDir;
logger.debug(`Using temporary directory: ${tmpDir}`);
if (flags.outputPath) {
const fileName = path.basename(flags.outputPath as string);
const outputDir = flags.tmpDir || packageTmpDir;
flags.outputPath = join(outputDir, fileName);
}
// Validate paths and files...
const cleanBackgroundName = typeof flags.background === 'string'
? flags.background.replace(/\.png$/, '')
: 'default';
const cleanMiscName = typeof flags.misc === 'string'
? flags.misc.replace(/\.png$/, '')
: 'none';
const bgPath = cleanBackgroundName.includes(path.sep)
? flags.background as string
: join(rootPath, 'picts', 'backgrounds', `${cleanBackgroundName}.png`);
const miscPath = cleanMiscName.includes(path.sep)
? flags.misc as string
: join(rootPath, 'picts', 'miscs', `${cleanMiscName}.png`);
const requiredFiles: Array<{ path: PathLike, name: string }> = [
{ path: flags.characterPath, name: 'Character' },
{ path: pyScript, name: 'Python script' }
];
if (!flags.characterOnly) {
requiredFiles.push({ path: bgPath, name: 'Background' });
if (miscPath && cleanMiscName !== 'none') {
requiredFiles.push({ path: miscPath, name: 'Misc' });
}
}
for (const file of requiredFiles) {
if (!existsSync(file.path)) {
throw new VaporwaverError(`${file.name} file not found: ${file.path}`);
}
if (file.path.toString().endsWith('.png') && !await isValidPngFile(file.path.toString())) {
throw new VaporwaverError(`Invalid PNG file: ${file.path}`);
}
}
const pyArgs = [pyScript];
pyArgs.push(
`-c=${flags.characterPath}`,
`-o=${flags.outputPath}`
);
if (flags.characterOnly) {
pyArgs.push(
`--character-only`,
`-cgd=${flags.characterGradient?.toLowerCase() ?? 'none'}`,
`-cg=${flags.characterGlitch ?? 0.1}`,
`-cgs=${flags.characterGlitchSeed ?? 0}`
);
} else {
if (flags.crt === true) {
pyArgs.push("-crt");
}
if (flags.miscAboveCharacter === true) {
pyArgs.push("--misc-above");
}
pyArgs.push(
`-b=${cleanBackgroundName}`,
`-m=${cleanMiscName}`,
`-cx=${flags.characterXPos ?? 0}`,
`-cy=${flags.characterYPos ?? 0}`,
`-cs=${flags.characterScale ?? 100}`,
`-cr=${flags.characterRotate ?? 0}`,
`-cg=${flags.characterGlitch ?? 0.1}`,
`-cgs=${flags.characterGlitchSeed ?? 0}`,
`-cgd=${flags.characterGradient?.toLowerCase() ?? 'none'}`,
`-mx=${flags.miscPosX ?? 0}`,
`-my=${flags.miscPosY ?? 0}`,
`-ms=${flags.miscScale ?? 100}`,
`-mr=${flags.miscRotate ?? 0}`
);
}
logger.debug('Executing Python script with args:', { pyArgs });
// Vérifier que le fichier character est toujours accessible juste avant l'exécution
try {
await fs.access(flags.characterPath, fs.constants.R_OK);
} catch (error) {
throw new VaporwaverError(`Character file is not accessible before Python execution: ${flags.characterPath}`, { originalError: error });
}
return new Promise((resolve, reject) => {
const pythonProcess = spawn('python', pyArgs, {
env: {
...process.env,
PYTHONPATH: rootPath,
VAPORWAVER_TMP: tmpDir
}
});
let stderrData = '';
let stdoutData = '';
pythonProcess.stdout.on('data', (data: Buffer) => {
stdoutData += data.toString();
logger.debug('Python stdout:', data.toString());
});
pythonProcess.stderr.on('data', (data: Buffer) => {
stderrData += data;
logger.error('Python stderr:', data.toString());
});
pythonProcess.on('close', (code) => {
if (code === 0) {
logger.info('Vaporwaver process completed successfully');
// Vérifier que le fichier de sortie existe
if (flags.outputPath && !existsSync(flags.outputPath)) {
const warning = `Warning: Output file not found at ${flags.outputPath} after successful execution`;
logger.warn(warning);
}
resolve();
} else {
const error = new VaporwaverError(
`Python process failed with code ${code}`,
{ stderr: stderrData, stdout: stdoutData }
);
logger.error('Vaporwaver process failed', error);
reject(error);
}
});
pythonProcess.on('error', (err) => {
const error = new VaporwaverError(
'Failed to start Python process',
{ originalError: err, stdout: stdoutData, stderr: stderrData }
);
logger.error('Process start failed', error);
reject(error);
});
// Ajouter un timeout de sécurité
const timeout = setTimeout(() => {
pythonProcess.kill();
const error = new VaporwaverError(
'Python process timed out after 30 seconds',
{ stdout: stdoutData, stderr: stderrData }
);
logger.error('Process timeout', error);
reject(error);
}, 30000); // 30 secondes
pythonProcess.on('close', () => {
clearTimeout(timeout);
});
});
} catch (error) {
logger.error('Vaporwaver error:', error);
throw error instanceof VaporwaverError ? error : new VaporwaverError(
'An unexpected error occurred',
{ originalError: error }
);
}
}