173 lines
5.7 KiB
TypeScript
173 lines
5.7 KiB
TypeScript
import dotenv from 'dotenv';
|
||
import * as fs from 'fs/promises';
|
||
import * as path from 'path';
|
||
import { logger } from '../lib/logger';
|
||
import { getPinUrlFromPinterest, downloadImageFromPin } from '../lib/pinterest';
|
||
import { convertImage } from '../lib/image-converter';
|
||
|
||
dotenv.config();
|
||
|
||
const KEYWORDS = [
|
||
'teen girl portrait',
|
||
'woman portrait',
|
||
'woman face close',
|
||
'teen face close',
|
||
'beautiful woman face closeup',
|
||
'beautiful teen',
|
||
'russian teen',
|
||
'skandinavian teen',
|
||
'uk teen',
|
||
'asian teen',
|
||
'east european teen',
|
||
'cute teen',
|
||
'beautiful woman',
|
||
'russian woman',
|
||
'skandinavian woman',
|
||
'uk woman',
|
||
'asian woman',
|
||
'east european woman',
|
||
'cute woman',];
|
||
const TARGET_COUNT = Number(process.env.IMAGE_COUNT || 20);
|
||
const PROMPT =
|
||
`change camera angle to closeup face from image1,
|
||
change background to light gray with faing gradient,
|
||
change face angle to look at directry look at camera˛
|
||
change lighting to soft light,
|
||
change face expression to neautral expression,
|
||
change age to 20 years old,
|
||
`;
|
||
|
||
type ServerCfg = { baseUrl: string; outputDir: string; inputDir: string };
|
||
|
||
function getServerConfig(): ServerCfg {
|
||
const candidates = [
|
||
{ baseUrl: process.env.SERVER1_COMFY_BASE_URL, outputDir: process.env.SERVER1_COMFY_OUTPUT_DIR },
|
||
//{ baseUrl: process.env.SERVER2_COMFY_BASE_URL, outputDir: process.env.SERVER2_COMFY_OUTPUT_DIR },
|
||
].filter((s): s is { baseUrl: string; outputDir: string } => !!s.baseUrl && !!s.outputDir);
|
||
|
||
if (candidates.length === 0) {
|
||
throw new Error(
|
||
'No ComfyUI server configured. Please set SERVER1_COMFY_BASE_URL/OUTPUT_DIR or SERVER2_COMFY_BASE_URL/OUTPUT_DIR in .env'
|
||
);
|
||
}
|
||
const chosen = candidates[0];
|
||
const inputDir = chosen.outputDir.replace('output', 'input');
|
||
return { ...chosen, inputDir };
|
||
}
|
||
|
||
function sleep(ms: number) {
|
||
return new Promise((res) => setTimeout(res, ms));
|
||
}
|
||
|
||
async function ensureDir(p: string) {
|
||
try {
|
||
await fs.mkdir(p, { recursive: true });
|
||
} catch {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
async function collectImages(keyword: string, total: number): Promise<string[]> {
|
||
const results: string[] = [];
|
||
// ensure local download dir exists (pinterest.ts also ensures it, but harmless here)
|
||
await ensureDir(path.join(process.cwd(), 'download'));
|
||
|
||
while (results.length < total) {
|
||
try {
|
||
const pinUrl = await getPinUrlFromPinterest(keyword);
|
||
if (!pinUrl) {
|
||
logger.warn('No pin URL found, retrying...');
|
||
await sleep(1500);
|
||
continue;
|
||
}
|
||
const remaining = total - results.length;
|
||
// attempt to grab up to 5 per pin to reduce churn
|
||
const batchTarget = Math.min(5, remaining);
|
||
const imgs = await downloadImageFromPin(pinUrl, batchTarget);
|
||
if (imgs && imgs.length > 0) {
|
||
results.push(...imgs);
|
||
logger.info(`Downloaded ${imgs.length} image(s) from ${pinUrl}.Progress: ${results.length}/${total}`);
|
||
} else {
|
||
logger.warn(`Pin yielded no downloadable images: ${pinUrl}`);
|
||
}
|
||
await sleep(1000 + Math.random() * 1000);
|
||
} catch (err) {
|
||
logger.error('Error while collecting images:', err);
|
||
await sleep(2000);
|
||
}
|
||
}
|
||
return results.slice(0, total);
|
||
}
|
||
|
||
async function processImages(imagePaths: string[], server: ServerCfg) {
|
||
await ensureDir(server.inputDir);
|
||
|
||
for (const localImagePath of imagePaths) {
|
||
const baseName = path.basename(localImagePath);
|
||
const serverInputPath = path.join(server.inputDir, baseName);
|
||
|
||
try {
|
||
// copy source image into ComfyUI input dir so workflow node can find it by filename
|
||
await fs.copyFile(localImagePath, serverInputPath);
|
||
logger.info(`Copied ${localImagePath} -> ${serverInputPath}`);
|
||
|
||
// Run conversion (sequential to avoid output race conditions)
|
||
const generatedPath = await convertImage(
|
||
PROMPT,
|
||
baseName,
|
||
server.baseUrl,
|
||
server.outputDir,
|
||
{ width: 720, height: 1280 } // portrait
|
||
);
|
||
logger.info(`Generated image: ${generatedPath}`);
|
||
} catch (err) {
|
||
logger.error(`Failed to convert ${localImagePath}:`, err);
|
||
} finally {
|
||
// cleanup both server input copy and local downloaded file
|
||
try {
|
||
await fs.unlink(serverInputPath);
|
||
} catch { }
|
||
try {
|
||
await fs.unlink(localImagePath);
|
||
} catch { }
|
||
await sleep(500);
|
||
}
|
||
}
|
||
}
|
||
|
||
async function main() {
|
||
const server = getServerConfig();
|
||
|
||
// Infinite loop as requested
|
||
while (true) {
|
||
|
||
for (const KEYWORD of KEYWORDS) {
|
||
logger.info(`Starting Pinterest image conversion loop for keyword: "${KEYWORD}" (target ${TARGET_COUNT})`);
|
||
try {
|
||
const images = await collectImages(KEYWORD, TARGET_COUNT);
|
||
logger.info(`Collected ${images.length} image(s). Starting conversion...`);
|
||
await processImages(images, server);
|
||
logger.info('Iteration completed. Sleeping before next cycle...');
|
||
} catch (err) {
|
||
logger.error('Iteration failed:', err);
|
||
}
|
||
|
||
// brief pause between iterations
|
||
await sleep(10000);
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
process.on('unhandledRejection', (reason) => {
|
||
logger.error('UnhandledRejection:', reason);
|
||
});
|
||
|
||
process.on('uncaughtException', (err) => {
|
||
logger.error('UncaughtException:', err);
|
||
});
|
||
|
||
main().catch((e) => {
|
||
logger.error('Fatal error:', e);
|
||
});
|