save changes

This commit is contained in:
2025-08-29 11:21:46 +02:00
parent 153e50a356
commit 62e22e4965
3 changed files with 11058 additions and 54 deletions

View File

@ -0,0 +1,531 @@
import puppeteer from 'puppeteer';
import type { Page } from 'puppeteer';
import dotenv from 'dotenv';
import * as fs from 'fs/promises';
import path from 'path';
dotenv.config();
/**
* Script: generate_pinterest_keywords.ts
*
* For each genre/subgenre pair (provided below) this script:
* - searches Pinterest for "genre subgenre"
* - scrolls the results page several times
* - collects up to 20 unique pin IDs from the page
* - outputs a JSON array to the console:
* [{ genre: "...", subGenre: "...", pinIds: ["id1","id2", ...] }, ...]
*
* Usage:
* - npx ts-node src/generate_pinterest_keywords.ts
* - or compile with tsc and run with node
*
* Notes:
* - Puppeteer will run headless by default. If you need to debug visually set headless: false.
* - Adjust SCROLL_ITERATIONS and MAX_PIN_IDS_PER_TERM if you want different behavior.
*/
const SCROLL_ITERATIONS = 8; // number of times to scroll (adjust if you want more)
const MAX_PIN_IDS_PER_TERM = 20; // target number of pin ids per genre/subgenre
const SCROLL_DELAY_MS = 900; // delay between scrolls
const searchList: { genre: string; subGenre: string }[] = [
{ genre: "abstract", subGenre: "3D Renderings" },
{ genre: "abstract", subGenre: "Abstract Portraits" },
{ genre: "abstract", subGenre: "Collage" },
{ genre: "abstract", subGenre: "Color Explosions" },
{ genre: "abstract", subGenre: "cubic" },
{ genre: "abstract", subGenre: "Cubism" },
{ genre: "abstract", subGenre: "Digital Glitch" },
{ genre: "abstract", subGenre: "Fluid Paints" },
{ genre: "abstract", subGenre: "Fractals" },
{ genre: "abstract", subGenre: "Geometric Shapes" },
{ genre: "abstract", subGenre: "Graffiti" },
{ genre: "abstract", subGenre: "Impressionism" },
{ genre: "abstract", subGenre: "Kaleidoscopes" },
{ genre: "abstract", subGenre: "Light Art" },
{ genre: "abstract", subGenre: "Mandalas" },
{ genre: "abstract", subGenre: "Minimalism" },
{ genre: "abstract", subGenre: "Optical Illusions" },
{ genre: "abstract", subGenre: "particle" },
{ genre: "abstract", subGenre: "Pop Art" },
{ genre: "abstract", subGenre: "science" },
{ genre: "abstract", subGenre: "space" },
{ genre: "abstract", subGenre: "sphere" },
{ genre: "abstract", subGenre: "Street Art" },
{ genre: "abstract", subGenre: "Surrealism" },
{ genre: "abstract", subGenre: "Typography Art" },
{ genre: "abstruct", subGenre: "art" },
{ genre: "abstruct", subGenre: "colors" },
{ genre: "abstruct", subGenre: "particle" },
{ genre: "animals", subGenre: "Bats" },
{ genre: "animals", subGenre: "Bears" },
{ genre: "animals", subGenre: "Butterflies" },
{ genre: "animals", subGenre: "Camels" },
{ genre: "animals", subGenre: "Crocodiles" },
{ genre: "animals", subGenre: "Dolphins" },
{ genre: "animals", subGenre: "Eagles" },
{ genre: "animals", subGenre: "Elephants" },
{ genre: "animals", subGenre: "Foxes" },
{ genre: "animals", subGenre: "Giraffes" },
{ genre: "animals", subGenre: "Horses" },
{ genre: "animals", subGenre: "Lions" },
{ genre: "animals", subGenre: "Owls" },
{ genre: "animals", subGenre: "Pandas" },
{ genre: "animals", subGenre: "Penguins" },
{ genre: "animals", subGenre: "Seals" },
{ genre: "animals", subGenre: "Sharks" },
{ genre: "animals", subGenre: "Tigers" },
{ genre: "animals", subGenre: "Whales" },
{ genre: "animals", subGenre: "Wolves" },
{ genre: "architecture", subGenre: "luxury room" },
{ genre: "childhood", subGenre: "Adoption" },
{ genre: "childhood", subGenre: "Babies" },
{ genre: "childhood", subGenre: "Bedtime Stories" },
{ genre: "childhood", subGenre: "Birthday Parties" },
{ genre: "childhood", subGenre: "Children Playing" },
{ genre: "childhood", subGenre: "Family Dinners" },
{ genre: "childhood", subGenre: "Family Portraits" },
{ genre: "childhood", subGenre: "Graduations" },
{ genre: "childhood", subGenre: "Grandparents" },
{ genre: "childhood", subGenre: "Holiday Celebrations" },
{ genre: "childhood", subGenre: "Learning to Walk" },
{ genre: "childhood", subGenre: "Nursery" },
{ genre: "childhood", subGenre: "Parent-Teacher Meetings" },
{ genre: "childhood", subGenre: "Picnics" },
{ genre: "childhood", subGenre: "Pregnancy" },
{ genre: "childhood", subGenre: "School Activities" },
{ genre: "childhood", subGenre: "Siblings" },
{ genre: "childhood", subGenre: "Sports with Kids" },
{ genre: "childhood", subGenre: "Toys" },
{ genre: "childhood", subGenre: "Vacations" },
{ genre: "cinematic", subGenre: "Action Movies" },
{ genre: "cinematic", subGenre: "Animations" },
{ genre: "cinematic", subGenre: "Documentaries" },
{ genre: "cinematic", subGenre: "Experimental Cinema" },
{ genre: "cinematic", subGenre: "Fantasy Epics" },
{ genre: "cinematic", subGenre: "Historical Dramas" },
{ genre: "cinematic", subGenre: "Hollywood Blockbusters" },
{ genre: "cinematic", subGenre: "Horror Films" },
{ genre: "cinematic", subGenre: "Indie Films" },
{ genre: "cinematic", subGenre: "Mockumentaries" },
{ genre: "cinematic", subGenre: "Musicals" },
{ genre: "cinematic", subGenre: "Nature Documentaries" },
{ genre: "cinematic", subGenre: "Noir" },
{ genre: "cinematic", subGenre: "Romantic Comedies" },
{ genre: "cinematic", subGenre: "Sci-Fi Thrillers" },
{ genre: "cinematic", subGenre: "Short Films" },
{ genre: "cinematic", subGenre: "Silent Films" },
{ genre: "cinematic", subGenre: "Stop Motion" },
{ genre: "cinematic", subGenre: "Superhero Films" },
{ genre: "cinematic", subGenre: "Westerns" },
{ genre: "city", subGenre: "Bridges" },
{ genre: "city", subGenre: "Castles" },
{ genre: "city", subGenre: "Cathedrals" },
{ genre: "city", subGenre: "Factories" },
{ genre: "city", subGenre: "Futuristic Cities" },
{ genre: "city", subGenre: "Historic Towns" },
{ genre: "city", subGenre: "Libraries" },
{ genre: "city", subGenre: "Markets" },
{ genre: "city", subGenre: "Modern Plazas" },
{ genre: "city", subGenre: "Museums" },
{ genre: "city", subGenre: "Palaces" },
{ genre: "city", subGenre: "Residential Blocks" },
{ genre: "city", subGenre: "Skylines" },
{ genre: "city", subGenre: "Skyscrapers" },
{ genre: "city", subGenre: "Slums" },
{ genre: "city", subGenre: "Stadiums" },
{ genre: "city", subGenre: "Street Cafes" },
{ genre: "city", subGenre: "Urban Parks" },
{ genre: "fantasy", subGenre: "academia" },
{ genre: "fantasy", subGenre: "aesthetic" },
{ genre: "fantasy", subGenre: "art" },
{ genre: "fantasy", subGenre: "Crystal Caves" },
{ genre: "fantasy", subGenre: "Dark Castles" },
{ genre: "fantasy", subGenre: "dark ethereal" },
{ genre: "fantasy", subGenre: "darkacademia" },
{ genre: "fantasy", subGenre: "ddreamy room" },
{ genre: "fantasy", subGenre: "Dragon Realms" },
{ genre: "fantasy", subGenre: "dreamy room" },
{ genre: "fantasy", subGenre: "Elven Cities" },
{ genre: "fantasy", subGenre: "Enchanted Rivers" },
{ genre: "fantasy", subGenre: "Epic Battles" },
{ genre: "fantasy", subGenre: "ethereal" },
{ genre: "fantasy", subGenre: "Fairy Villages" },
{ genre: "fantasy", subGenre: "Floating Islands" },
{ genre: "fantasy", subGenre: "Ghostly Spirits" },
{ genre: "fantasy", subGenre: "illumication" },
{ genre: "fantasy", subGenre: "Knights" },
{ genre: "fantasy", subGenre: "landscape" },
{ genre: "fantasy", subGenre: "Magic Forests" },
{ genre: "fantasy", subGenre: "Magical Beasts" },
{ genre: "fantasy", subGenre: "Mystic Portals" },
{ genre: "fantasy", subGenre: "Mythical Weapons" },
{ genre: "fantasy", subGenre: "Queens and Kings" },
{ genre: "fantasy", subGenre: "Runes and Symbols" },
{ genre: "fantasy", subGenre: "Sacred Temples" },
{ genre: "fantasy", subGenre: "Shape-shifters" },
{ genre: "fantasy", subGenre: "Talking Animals" },
{ genre: "fantasy", subGenre: "Wizards" },
{ genre: "fashion", subGenre: "Accessories" },
{ genre: "fashion", subGenre: "Boho Style" },
{ genre: "fashion", subGenre: "Bridal Wear" },
{ genre: "fashion", subGenre: "Business Suits" },
{ genre: "fashion", subGenre: "Casual Wear" },
{ genre: "fashion", subGenre: "Cocktail Dresses" },
{ genre: "fashion", subGenre: "Cosplay" },
{ genre: "fashion", subGenre: "Evening Gowns" },
{ genre: "fashion", subGenre: "Hair Styling" },
{ genre: "fashion", subGenre: "Haute Couture" },
{ genre: "fashion", subGenre: "Makeup Styles" },
{ genre: "fashion", subGenre: "Pajamas" },
{ genre: "fashion", subGenre: "Runway Shows" },
{ genre: "fashion", subGenre: "School Uniforms" },
{ genre: "fashion", subGenre: "Shoes" },
{ genre: "fashion", subGenre: "Sportswear" },
{ genre: "fashion", subGenre: "Streetwear" },
{ genre: "fashion", subGenre: "Swimwear" },
{ genre: "fashion", subGenre: "Traditional Costumes" },
{ genre: "fashion", subGenre: "Vintage Outfits" },
{ genre: "food", subGenre: "Bakeries" },
{ genre: "food", subGenre: "BBQ" },
{ genre: "food", subGenre: "Breakfasts" },
{ genre: "food", subGenre: "Chocolate Making" },
{ genre: "food", subGenre: "Cocktails" },
{ genre: "food", subGenre: "Coffee Culture" },
{ genre: "food", subGenre: "Desserts" },
{ genre: "food", subGenre: "Farmers Market" },
{ genre: "food", subGenre: "Fine Dining" },
{ genre: "food", subGenre: "Food Trucks" },
{ genre: "food", subGenre: "Fruit Platters" },
{ genre: "food", subGenre: "Pasta Dishes" },
{ genre: "food", subGenre: "Pizza" },
{ genre: "food", subGenre: "Seafood" },
{ genre: "food", subGenre: "Spices and Herbs" },
{ genre: "food", subGenre: "Street Food" },
{ genre: "food", subGenre: "Sushi" },
{ genre: "food", subGenre: "Tea Ceremonies" },
{ genre: "food", subGenre: "Vegetarian Meals" },
{ genre: "food", subGenre: "Wine Tasting" },
{ genre: "history", subGenre: "African Tribes" },
{ genre: "history", subGenre: "Ancient Egypt" },
{ genre: "history", subGenre: "Celtic Legends" },
{ genre: "history", subGenre: "Chinese Dynasties" },
{ genre: "history", subGenre: "Greek Temples" },
{ genre: "history", subGenre: "Indian Kingdoms" },
{ genre: "history", subGenre: "Industrial Revolution" },
{ genre: "history", subGenre: "Mayan Ruins" },
{ genre: "history", subGenre: "Medieval Europe" },
{ genre: "history", subGenre: "Native American" },
{ genre: "history", subGenre: "Nomadic Life" },
{ genre: "history", subGenre: "Ottoman Empire" },
{ genre: "history", subGenre: "Renaissance" },
{ genre: "history", subGenre: "Samurai Japan" },
{ genre: "history", subGenre: "Traditional Festivals" },
{ genre: "history", subGenre: "Victorian Era" },
{ genre: "history", subGenre: "Viking Culture" },
{ genre: "history", subGenre: "World War Eras" },
{ genre: "horror", subGenre: "Abandoned Hospitals" },
{ genre: "horror", subGenre: "Ancient Tombs" },
{ genre: "horror", subGenre: "Blood Moons" },
{ genre: "horror", subGenre: "Creepy Dolls" },
{ genre: "horror", subGenre: "Curses" },
{ genre: "horror", subGenre: "Dark Alleys" },
{ genre: "horror", subGenre: "Demons" },
{ genre: "horror", subGenre: "Foggy Forests" },
{ genre: "horror", subGenre: "Ghosts" },
{ genre: "horror", subGenre: "Graveyards" },
{ genre: "horror", subGenre: "Haunted Houses" },
{ genre: "horror", subGenre: "Monsters" },
{ genre: "horror", subGenre: "Occult Rituals" },
{ genre: "horror", subGenre: "Possessions" },
{ genre: "horror", subGenre: "Scary Clowns" },
{ genre: "horror", subGenre: "Shadows" },
{ genre: "horror", subGenre: "Silent Villages" },
{ genre: "horror", subGenre: "Vampires" },
{ genre: "horror", subGenre: "Werewolves" },
{ genre: "horror", subGenre: "Witches" },
{ genre: "music", subGenre: "Ballet" },
{ genre: "music", subGenre: "Breakdance" },
{ genre: "music", subGenre: "Choirs" },
{ genre: "music", subGenre: "Classical Concerts" },
{ genre: "music", subGenre: "Dance Battles" },
{ genre: "music", subGenre: "DJ Performances" },
{ genre: "music", subGenre: "Drumming" },
{ genre: "music", subGenre: "Electronic Music" },
{ genre: "music", subGenre: "Flamenco" },
{ genre: "music", subGenre: "Folk Dance" },
{ genre: "music", subGenre: "Hip-Hop Dance" },
{ genre: "music", subGenre: "Jazz Clubs" },
{ genre: "music", subGenre: "K-Pop" },
{ genre: "music", subGenre: "Opera" },
{ genre: "music", subGenre: "Orchestras" },
{ genre: "music", subGenre: "Rock Bands" },
{ genre: "music", subGenre: "Salsa" },
{ genre: "music", subGenre: "Singing Solo" },
{ genre: "music", subGenre: "Street Dance" },
{ genre: "music", subGenre: "Tap Dance" },
{ genre: "nature", subGenre: "Aurora Skies" },
{ genre: "nature", subGenre: "Canyons" },
{ genre: "nature", subGenre: "Caves" },
{ genre: "nature", subGenre: "Cliffs" },
{ genre: "nature", subGenre: "Coral Reefs" },
{ genre: "nature", subGenre: "Deserts" },
{ genre: "nature", subGenre: "Forests" },
{ genre: "nature", subGenre: "Glaciers" },
{ genre: "nature", subGenre: "Lakes" },
{ genre: "nature", subGenre: "Meadows" },
{ genre: "nature", subGenre: "Mountains" },
{ genre: "nature", subGenre: "night sky" },
{ genre: "nature", subGenre: "Oceans" },
{ genre: "nature", subGenre: "Rainforest" },
{ genre: "nature", subGenre: "Rivers" },
{ genre: "nature", subGenre: "Savannah" },
{ genre: "nature", subGenre: "Storms" },
{ genre: "nature", subGenre: "Sunsets" },
{ genre: "nature", subGenre: "Volcanoes" },
{ genre: "nature", subGenre: "Waterfalls" },
{ genre: "nature", subGenre: "Wetlands" },
{ genre: "people", subGenre: "Artists" },
{ genre: "people", subGenre: "Celebrations" },
{ genre: "people", subGenre: "Children" },
{ genre: "people", subGenre: "Commuting" },
{ genre: "people", subGenre: "Craftsmen" },
{ genre: "people", subGenre: "Daily Life" },
{ genre: "people", subGenre: "Elderly" },
{ genre: "people", subGenre: "Family Moments" },
{ genre: "people", subGenre: "Farm Life" },
{ genre: "people", subGenre: "Festivals" },
{ genre: "people", subGenre: "Fishing Villages" },
{ genre: "people", subGenre: "Friendship" },
{ genre: "people", subGenre: "Markets" },
{ genre: "people", subGenre: "Relaxing" },
{ genre: "people", subGenre: "Romantic Dates" },
{ genre: "people", subGenre: "School Days" },
{ genre: "people", subGenre: "Shopping" },
{ genre: "people", subGenre: "Street Life" },
{ genre: "people", subGenre: "Travelers" },
{ genre: "people", subGenre: "Workplaces" },
{ genre: "romance", subGenre: "Anger" },
{ genre: "romance", subGenre: "Anniversaries" },
{ genre: "romance", subGenre: "Arguments" },
{ genre: "romance", subGenre: "Comforting" },
{ genre: "romance", subGenre: "Confessions" },
{ genre: "romance", subGenre: "Family Love" },
{ genre: "romance", subGenre: "Farewells" },
{ genre: "romance", subGenre: "First Dates" },
{ genre: "romance", subGenre: "Friendship" },
{ genre: "romance", subGenre: "Heartbreak" },
{ genre: "romance", subGenre: "Hugs" },
{ genre: "romance", subGenre: "Joy" },
{ genre: "romance", subGenre: "Kisses" },
{ genre: "romance", subGenre: "Laughter" },
{ genre: "romance", subGenre: "Loneliness" },
{ genre: "romance", subGenre: "Reunions" },
{ genre: "romance", subGenre: "Sadness" },
{ genre: "romance", subGenre: "Surprise" },
{ genre: "romance", subGenre: "wedding" },
{ genre: "romance", subGenre: "Weddings" },
{ genre: "sci-fi", subGenre: "AI Entities" },
{ genre: "sci-fi", subGenre: "Aliens" },
{ genre: "sci-fi", subGenre: "Androids" },
{ genre: "sci-fi", subGenre: "Clones" },
{ genre: "sci-fi", subGenre: "Colonies on Mars" },
{ genre: "sci-fi", subGenre: "Cyberpunk Cities" },
{ genre: "sci-fi", subGenre: "Exosuits" },
{ genre: "sci-fi", subGenre: "Futuristic Weapons" },
{ genre: "sci-fi", subGenre: "Galactic Battles" },
{ genre: "sci-fi", subGenre: "Genetic Labs" },
{ genre: "sci-fi", subGenre: "Holograms" },
{ genre: "sci-fi", subGenre: "Hover Cars" },
{ genre: "sci-fi", subGenre: "Nanotechnology" },
{ genre: "sci-fi", subGenre: "Post-Apocalypse" },
{ genre: "sci-fi", subGenre: "Robots" },
{ genre: "sci-fi", subGenre: "Space Stations" },
{ genre: "sci-fi", subGenre: "Time Travel" },
{ genre: "sci-fi", subGenre: "Virtual Reality" },
{ genre: "sci-fi", subGenre: "Wormholes" },
{ genre: "space", subGenre: "Asteroids" },
{ genre: "space", subGenre: "Aurora" },
{ genre: "space", subGenre: "Black Holes" },
{ genre: "space", subGenre: "Comets" },
{ genre: "space", subGenre: "Cosmic Dust" },
{ genre: "space", subGenre: "Deep Space" },
{ genre: "space", subGenre: "Eclipses" },
{ genre: "space", subGenre: "Exoplanets" },
{ genre: "space", subGenre: "Galaxies" },
{ genre: "space", subGenre: "Meteor Showers" },
{ genre: "space", subGenre: "Moons" },
{ genre: "space", subGenre: "Nebulae" },
{ genre: "space", subGenre: "Observatories" },
{ genre: "space", subGenre: "Planets" },
{ genre: "space", subGenre: "Rocket Launches" },
{ genre: "space", subGenre: "Satellites" },
{ genre: "space", subGenre: "Spacewalks" },
{ genre: "space", subGenre: "Stars" },
{ genre: "space", subGenre: "Supernovas" },
{ genre: "space", subGenre: "Telescopes" },
{ genre: "sports", subGenre: "Archery" },
{ genre: "sports", subGenre: "Baseball" },
{ genre: "sports", subGenre: "Basketball" },
{ genre: "sports", subGenre: "Boxing" },
{ genre: "sports", subGenre: "Climbing" },
{ genre: "sports", subGenre: "Cycling" },
{ genre: "sports", subGenre: "Fencing" },
{ genre: "sports", subGenre: "Gymnastics" },
{ genre: "sports", subGenre: "Horse Riding" },
{ genre: "sports", subGenre: "Martial Arts" },
{ genre: "sports", subGenre: "Rowing" },
{ genre: "sports", subGenre: "Running" },
{ genre: "sports", subGenre: "Skateboarding" },
{ genre: "sports", subGenre: "Skiing" },
{ genre: "sports", subGenre: "Snowboarding" },
{ genre: "sports", subGenre: "Soccer" },
{ genre: "sports", subGenre: "Surfing" },
{ genre: "sports", subGenre: "Swimming" },
{ genre: "sports", subGenre: "Tennis" },
{ genre: "sports", subGenre: "Yoga" },
{ genre: "technology", subGenre: "3D Printing" },
{ genre: "technology", subGenre: "Artificial Intelligence" },
{ genre: "technology", subGenre: "Augmented Reality" },
{ genre: "technology", subGenre: "Autonomous Cars" },
{ genre: "technology", subGenre: "Biotech" },
{ genre: "technology", subGenre: "Cyber Security" },
{ genre: "technology", subGenre: "Data Centers" },
{ genre: "technology", subGenre: "Digital Currency" },
{ genre: "technology", subGenre: "Drones" },
{ genre: "technology", subGenre: "Futuristic Homes" },
{ genre: "technology", subGenre: "Green Tech" },
{ genre: "technology", subGenre: "Nanobots" },
{ genre: "technology", subGenre: "Quantum Computing" },
{ genre: "technology", subGenre: "Robotics" },
{ genre: "technology", subGenre: "Smart Cities" },
{ genre: "technology", subGenre: "Smart Farms" },
{ genre: "technology", subGenre: "Space Elevators" },
{ genre: "technology", subGenre: "Surveillance Systems" },
{ genre: "technology", subGenre: "VR Worlds" },
{ genre: "technology", subGenre: "Wearables" },
{ genre: "travel", subGenre: "Backpacking" },
{ genre: "travel", subGenre: "Camping" },
{ genre: "travel", subGenre: "City Tours" },
{ genre: "travel", subGenre: "Cruises" },
{ genre: "travel", subGenre: "Cultural Trips" },
{ genre: "travel", subGenre: "Desert Journeys" },
{ genre: "travel", subGenre: "Diving Trips" },
{ genre: "travel", subGenre: "Exploring Ruins" },
{ genre: "travel", subGenre: "Festivals Abroad" },
{ genre: "travel", subGenre: "Glamping" },
{ genre: "travel", subGenre: "Hiking" },
{ genre: "travel", subGenre: "Hot Air Balloons" },
{ genre: "travel", subGenre: "Island Hopping" },
{ genre: "travel", subGenre: "Jungle Treks" },
{ genre: "travel", subGenre: "Motorbike Tours" },
{ genre: "travel", subGenre: "Mountain Climbing" },
{ genre: "travel", subGenre: "Polar Expeditions" },
{ genre: "travel", subGenre: "Road Trips" },
{ genre: "travel", subGenre: "Safari" },
{ genre: "travel", subGenre: "Train Journeys" },
{ genre: "work", subGenre: "Actors" },
{ genre: "work", subGenre: "Artists" },
{ genre: "work", subGenre: "Athletes" },
{ genre: "work", subGenre: "Chefs" },
{ genre: "work", subGenre: "Craftsmen" },
];
function extractPinIdFromHref(href: string): string | null {
// hrefs look like https://www.pinterest.com/pin/123456789012345678/...
const m = href.match(/\/pin\/([^\/\?]+)/);
return m ? m[1] : null;
}
async function collectPinIdsForSearch(page: Page, query: string): Promise<string[]> {
const pinIds = new Set<string>();
const searchUrl = `https://www.pinterest.com/search/pins/?q=${encodeURIComponent(query)}`;
await page.goto(searchUrl, { waitUntil: 'networkidle2', timeout: 60000 });
// initial collect
const collect = async () => {
const hrefs = await page.$$eval('a', anchors => (anchors as any[]).map(a => (a as any).href)) as string[];
for (const href of hrefs) {
if (!href) continue;
if (href.includes('/pin/')) {
const id = href.match(/\/pin\/([^\/\?]+)/);
if (id && id[1]) pinIds.add(id[1]);
if (pinIds.size >= MAX_PIN_IDS_PER_TERM) break;
}
}
};
await collect();
// Scroll a few times to load more results
for (let i = 0; i < SCROLL_ITERATIONS && pinIds.size < MAX_PIN_IDS_PER_TERM; i++) {
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await new Promise(resolve => setTimeout(resolve, SCROLL_DELAY_MS + Math.floor(Math.random() * 800)));
await collect();
}
return Array.from(pinIds).slice(0, MAX_PIN_IDS_PER_TERM);
}
(async () => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
const results: { genre: string; subGenre: string; pinIds: string[] }[] = [];
for (const item of searchList) {
const term = `${item.genre} ${item.subGenre}`;
try {
console.log(`Searching Pinterest for: "${term}"`);
const pinIds = await collectPinIdsForSearch(page, term);
console.log(` -> Found ${pinIds.length} pinIds for "${term}"`);
results.push({ genre: item.genre, subGenre: item.subGenre, pinIds });
// small delay between queries to avoid being throttled
await new Promise(resolve => setTimeout(resolve, 600 + Math.floor(Math.random() * 1000)));
} catch (err) {
console.error(`Error collecting pins for "${term}":`, err);
results.push({ genre: item.genre, subGenre: item.subGenre, pinIds: [] });
}
}
await browser.close();
// Output JSON to file (generated/pinterest_keywords.json)
const outDir = path.join(process.cwd(), 'generated');
try {
await fs.mkdir(outDir, { recursive: true });
const outPath = path.join(outDir, 'pinterest_keywords.json');
await fs.writeFile(outPath, JSON.stringify(results, null, 2), 'utf-8');
console.log(`Saved ${results.length} entries to ${outPath}`);
} catch (err) {
console.error('Failed to write output file:', err);
// Fallback to printing JSON to stdout
console.log(JSON.stringify(results, null, 2));
}
})();

10375
src/pinterest_keywords.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@ async function getPromptsForImage(imagePaths: string[], pinUrl: string, genre: s
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
const imageFileName = `${pinId}_${timestamp}.png`; const imageFileName = `${pinId}_${timestamp}.png`;
const renamedImagePaths = []; const renamedImagePaths: string[] = [];
for (let i = 0; i < imagePaths.length; i++) { for (let i = 0; i < imagePaths.length; i++) {
const renamedPath = path.join(path.dirname(imagePaths[i]), `${pinId}_${timestamp}_${i}.png`); const renamedPath = path.join(path.dirname(imagePaths[i]), `${pinId}_${timestamp}_${i}.png`);
await fs.rename(imagePaths[i], renamedPath); await fs.rename(imagePaths[i], renamedPath);
@ -106,10 +106,14 @@ async function generateImageForTask(task: GenerationTask, server: { baseUrl: str
logger.info(`Copied ${sourcePath} to ${destPath}`); logger.info(`Copied ${sourcePath} to ${destPath}`);
} }
// generateImage expects two source files; if we only have one, pass the same one twice
const srcA = sourceFileNames[0];
const srcB = sourceFileNames[1] || sourceFileNames[0];
const generatedImagePath = await generateImage( const generatedImagePath = await generateImage(
imagePrompt, imagePrompt,
sourceFileNames[0], srcA,
sourceFileNames[1], srcB,
imageFileName, imageFileName,
baseUrl, baseUrl,
outputDir, outputDir,
@ -173,16 +177,107 @@ async function getPinUrlFromPinterest(keyword: string): Promise<string | null> {
} }
(async () => { (async () => {
const keywords = [ // Load pinterest keywords JSON, pick up to 20 subGenres and choose 1 pinId per subGenre
{ genre: "fantasy", subGenre: "ethereal", pinId: "311311393008071543" }, const keywordsFilePath = path.resolve(process.cwd(), 'src', 'pinterest_keywords.json');
{ genre: "fantasy", subGenre: "ethereal", pinId: "1688918605889326" }, let allKeywords: { genre: string; subGenre: string; pinIds?: string[]; pinId?: string[] }[] = [];
{ genre: "fantasy", subGenre: "ethereal", pinId: "4925880837133615" }, try {
{ genre: "fantasy", subGenre: "ethereal", pinId: "985231163711528" }, const raw = await fs.readFile(keywordsFilePath, 'utf-8');
{ genre: "fantasy", subGenre: "dark ethereal", pinId: "768497123954670531" }, allKeywords = JSON.parse(raw);
{ genre: "fantasy", subGenre: "dark ethereal", pinId: "5840674510388768" }, } catch (err) {
{ genre: "fantasy", subGenre: "dark ethereal", pinId: "281123201734741654" }, logger.error('Failed to read pinterest keywords JSON:', err);
{ genre: "fantasy", subGenre: "dark ethereal", pinId: "41306521578186951" }, return;
]; }
allKeywords = allKeywords.filter(a => {
return (a.genre == "food" && a.subGenre == "imagination")
});
function shuffle<T>(arr: T[]): T[] {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
const selectedEntries = shuffle(allKeywords.slice()).slice(0, Math.min(20, allKeywords.length));
// Download up to `count` images from a pin URL by opening the pin page and scrolling up to 5 times to trigger lazy loading
// Returns an array of saved image paths (may be empty)
async function downloadOneImageFromPin(pinUrl: string, count: number = 1): Promise<string[]> {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36');
await page.setViewport({ width: 1920, height: 1080 });
try {
await page.goto(pinUrl, { waitUntil: 'networkidle2', timeout: 30000 });
for (let i = 0; i < 5; i++) {
await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
await new Promise((r) => setTimeout(r, 700 + Math.random() * 800));
}
const imgs: string[] = await page.$$eval('img', imgs =>
imgs.map(img => (img as HTMLImageElement).src)
.filter(src => !!src && (src.includes('pinimg') || /\.(jpe?g|png|webp)$/i.test(src)))
);
if (!imgs || imgs.length === 0) {
logger.warn(`No image src found on pin page ${pinUrl}`);
return [];
}
// shuffle and pick up to `count` unique images
const shuffled = imgs.slice().sort(() => 0.5 - Math.random());
const chosen = shuffled.slice(0, Math.min(count, shuffled.length));
const outDir = path.join(process.cwd(), 'download');
await fs.mkdir(outDir, { recursive: true });
const results: string[] = [];
for (let i = 0; i < chosen.length; i++) {
const src = chosen[i];
try {
const imgPage = await browser.newPage();
const resp = await imgPage.goto(src, { timeout: 30000, waitUntil: 'networkidle2' });
if (!resp) {
logger.warn(`Failed to fetch image ${src} from ${pinUrl}`);
await imgPage.close();
continue;
}
const buffer = await resp.buffer();
const pinId = pinUrl.split('/').filter(Boolean).pop() || `pin_${Date.now()}`;
const timestamp = Date.now();
const outPath = path.join(outDir, `${pinId}_${timestamp}_${i}.png`);
await fs.writeFile(outPath, buffer);
results.push(outPath);
await imgPage.close();
} catch (err) {
logger.error(`Failed to download image ${src} from ${pinUrl}:`, err);
}
}
return results;
} catch (err) {
logger.error(`Failed to download images from ${pinUrl}:`, err);
return [];
} finally {
await browser.close();
}
}
// Build keywords list with single chosen pinId per selected subGenre
const keywords: { genre: string; subGenre: string; pinId: string[] }[] = [];
for (const entry of selectedEntries) {
const pinIds = (entry.pinIds || entry.pinId) as string[] | undefined;
if (!Array.isArray(pinIds) || pinIds.length === 0) continue;
const chosenPinId = pinIds[Math.floor(Math.random() * pinIds.length)];
keywords.push({ genre: entry.genre, subGenre: entry.subGenre, pinId: [chosenPinId] });
}
if (keywords.length === 0) {
logger.error("No keywords/pinIds available from pinterest_keywords.json. Exiting.");
return;
}
if (servers.length === 0) { if (servers.length === 0) {
logger.error("No servers configured. Please check your .env file."); logger.error("No servers configured. Please check your .env file.");
@ -196,45 +291,51 @@ async function getPinUrlFromPinterest(keyword: string): Promise<string | null> {
const { genre, subGenre } = genreSubGenre; const { genre, subGenre } = genreSubGenre;
const pinId = (genreSubGenre as any).pinId; for (let i = 0; i < 10; i++) {
let pin: string | null = null; // pinId is now an array with a single chosen id. Pick the first element.
const pinIdField = (genreSubGenre as any).pinId;
let selectedPinId: string | undefined;
if (Array.isArray(pinIdField) && pinIdField.length > 0) {
selectedPinId = pinIdField[0];
logger.info(`Selected chosen pinId ${selectedPinId} for ${genre} / ${subGenre}`);
} else if (typeof pinIdField === 'string' && pinIdField) {
selectedPinId = pinIdField;
logger.info(`Using single pinId ${selectedPinId} for ${genre} / ${subGenre}`);
}
if (pinId) { if (!selectedPinId) {
pin = `https://www.pinterest.com/pin/${pinId}/`; logger.warn(`No pinId available for ${genre}/${subGenre}. Skipping.`);
logger.info(`Using direct pin URL for pinId: ${pinId}`);
} /* else {
// Uncomment the block below to enable searching by term instead of using pinId.
// const searchTerm = (genreSubGenre as any).search || `${genre} ${subGenre}`;
// logger.info(`Searching for a pin with search term: ${searchTerm}`);
// pin = await getPinUrlFromPinterest(searchTerm);
} */
if (!pin) {
logger.warn(`No pinId provided and search is disabled for genre: ${genre}, subGenre: ${subGenre}. Skipping.`);
continue; continue;
} }
const pin = `https://www.pinterest.com/pin/${selectedPinId}/`;
logger.info(`--- Starting processing for pin: ${pin} ---`); logger.info(`--- Starting processing for pin: ${pin} ---`);
const downloadedImagePaths = await downloadImagesFromPinterestPin(pin);
if (downloadedImagePaths.length === 0) { // download images from the pin page (pass desired count as second arg)
const downloadedImagePaths = await downloadOneImageFromPin(pin, 20);
if (!downloadedImagePaths || downloadedImagePaths.length === 0) {
logger.warn(`No images were downloaded for pin ${pin}. Skipping.`); logger.warn(`No images were downloaded for pin ${pin}. Skipping.`);
continue; continue;
} }
const selectedImages = downloadedImagePaths.sort(() => 0.5 - Math.random()).slice(0, 2); const selectedImages = downloadedImagePaths.sort(() => 0.5 - Math.random()).slice(0, 2);
logger.info(`--- Randomly selected ${selectedImages.length} images for processing ---`); logger.info(`--- Downloaded ${selectedImages.length} image(s) for processing ---`);
if (selectedImages.length === 2) { // proceed if we have at least one image
const pinId = path.basename(selectedImages[0], path.extname(selectedImages[0])).split('_related_')[0]; if (selectedImages.length >= 1) {
const pinUrl = `https://www.pinterest.com/pin/${pinId}/`; const task = await getPromptsForImage(selectedImages, pin, genre, subGenre);
const task = await getPromptsForImage(selectedImages, pinUrl, genre, subGenre);
if (task) { if (task) {
generationTasks.push(task); generationTasks.push(task);
} }
} else { } else {
logger.warn(`Skipping pin ${pin} as it did not have at least 2 images.`); logger.warn(`Skipping pin ${pin} as it did not yield images.`);
for (const imagePath of selectedImages) { for (const imagePath of selectedImages) {
try {
await fs.unlink(imagePath); await fs.unlink(imagePath);
} catch (error) {
logger.error(`Failed to delete image ${imagePath}:`, error);
}
}
} }
} }
} }
@ -306,9 +407,6 @@ async function getPinUrlFromPinterest(keyword: string): Promise<string | null> {
video_path: newVideoPath, video_path: newVideoPath,
}); });
logger.info(`Renamed files and updated database record for video ID: ${videoId}`); logger.info(`Renamed files and updated database record for video ID: ${videoId}`);
await fs.unlink(newImagePath);
logger.info(`Deleted paired image: ${newImagePath}`);
} }
} catch (error) { } catch (error) {
logger.error('An error occurred during video generation or database operations:', error); logger.error('An error occurred during video generation or database operations:', error);