diff --git a/.gitignore b/.gitignore index 0020ddc..8f55f40 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ images/processed images/processed_backup images/background_removed images/unprocessed +images/liveries_background_removed +images/liveries_unprocessed +images/liveries_processed node_modules \ No newline at end of file diff --git a/process-images.ts b/process-images.ts index b9dce5f..58b51ac 100644 --- a/process-images.ts +++ b/process-images.ts @@ -5,9 +5,35 @@ const inputDirectory = "images/unprocessed"; const outputDirectory = "images/processed"; const backgroundRemovedDirectory = "images/background_removed"; const watermarkImage = "images/Watermark.png"; + +const liveryInputDirectory = "images/liveries_unprocessed" +const liveryOutputDirectory = "images/liveries_processed" +const liveryBackgroundRemovedDirectory = "images/liveries_background_removed" + + import Replicate from "replicate"; import sharp from "sharp"; +const convertToPng = async () => { + const pngFiles = fs + .readdirSync(outputDirectory) + .filter((file) => file.toLowerCase().endsWith(".png")); + + for (const file of pngFiles) { + const filePath = path.join(outputDirectory, file); + const fileBuffer = fs.readFileSync(filePath); + const metadata = await sharp(fileBuffer).metadata(); + + if (metadata.format !== 'png') { + console.log(`Converting to PNG: ${filePath}`); + const pngData = await sharp(fileBuffer).png().toBuffer(); + fs.writeFileSync(filePath, pngData); + console.log(`Converted: ${filePath}`); + } + } +}; + + const removeWatermark = async () => { const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN }); @@ -23,24 +49,9 @@ const removeWatermark = async () => { const filePath = path.join(inputDirectory, file); const outputPath = path.join(outputDirectory, file); - // Ensure the output directory exists before every write fs.mkdirSync(path.dirname(outputPath), { recursive: true }); if (fs.existsSync(outputPath)) { - const fileBuffer = fs.readFileSync(outputPath); - const metadata = await sharp(fileBuffer).metadata(); - - if (metadata.format !== 'png') { - console.log(`About to write to: ${outputPath}`); - console.log(`Dir exists: ${fs.existsSync(path.dirname(outputPath))}`); - console.log(`Dir writable:`, (() => { try { fs.accessSync(path.dirname(outputPath), fs.constants.W_OK); return true; } catch { return false; } })()); - console.log(`File writable:`, (() => { try { fs.accessSync(outputPath, fs.constants.W_OK); return true; } catch { return false; } })()); - const pngData = await sharp(fileBuffer).png().toBuffer(); - console.log(`PNG buffer size: ${pngData.length}`); - fs.writeFileSync(outputPath, pngData); - console.log(`Write succeeded`); - } - continue; } @@ -104,9 +115,71 @@ const removeBackground = async () => { } }; +const generateLiveries = async () => { + const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN }); + + const prompt = "Generate a 2D, side-view digital technical illustration of the identical plane pictured - with no wheels (MAKE SURE TO REMOVE ANY WHEELS). (correct the perspective of the plane so it is side view) (in Norebbo's style, preserving the livery markings exactly), with a plain white background (no shadows) and with some mild lighting and sheen effects, correcting perspective if the plane is not side-on. Make sure to remove any wheels if they are visible. DO NOT ADD ANYTHING TO THE LIVERY THAT IS NOT IN THE ORIGINAL PHOTO"; + + fs.mkdirSync(liveryOutputDirectory, { recursive: true }); + + const imageFiles = fs + .readdirSync(liveryInputDirectory) + .filter((file) => /\.(png|jpg|jpeg|webp)$/i.test(file)); + + for (const file of imageFiles) { + const filePath = path.join(liveryInputDirectory, file); + const ext = path.extname(file).toLowerCase(); + const outputFile = ext !== ".png" ? file.replace(/\.[^.]+$/, ".png") : file; + const outputPath = path.join(liveryOutputDirectory, outputFile); + + if (fs.existsSync(outputPath)) { + continue; + } + + console.log(`Processing: ${filePath}`); + + const mimeType = ext === ".jpg" || ext === ".jpeg" ? "image/jpeg" + : ext === ".webp" ? "image/webp" + : "image/png"; + + const fileBuffer = fs.readFileSync(filePath); + const blob = new Blob([fileBuffer], { type: mimeType }); + const uploadedFile = await replicate.files.create(blob, { + filename: file, + contentType: mimeType, + }); + + console.log(`Uploaded: ${uploadedFile.urls.get}`); + + const output = await replicate.run("google/gemini-2.5-flash-image", { + input: { + prompt, + image_input: [uploadedFile.urls.get], + aspect_ratio: "16:9", + output_format: "png", + }, + }) as ReadableStream; + + const reader = output.getReader(); + const chunks: Uint8Array[] = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + const resultBuffer = Buffer.concat(chunks.map(c => Buffer.from(c))); + fs.writeFileSync(outputPath, resultBuffer); + + console.log(`Saved: ${outputPath}`); + } +}; + + const commands: Record Promise> = { removeWatermark, removeBackground, + convertToPng, + generateLiveries, }; const command = process.argv[2];