save changes
This commit is contained in:
531
src/generate_pinterest_keywords.ts
Normal file
531
src/generate_pinterest_keywords.ts
Normal 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
10375
src/pinterest_keywords.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -43,7 +43,7 @@ async function getPromptsForImage(imagePaths: string[], pinUrl: string, genre: s
|
||||
const timestamp = new Date().getTime();
|
||||
const imageFileName = `${pinId}_${timestamp}.png`;
|
||||
|
||||
const renamedImagePaths = [];
|
||||
const renamedImagePaths: string[] = [];
|
||||
for (let i = 0; i < imagePaths.length; i++) {
|
||||
const renamedPath = path.join(path.dirname(imagePaths[i]), `${pinId}_${timestamp}_${i}.png`);
|
||||
await fs.rename(imagePaths[i], renamedPath);
|
||||
@ -106,10 +106,14 @@ async function generateImageForTask(task: GenerationTask, server: { baseUrl: str
|
||||
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(
|
||||
imagePrompt,
|
||||
sourceFileNames[0],
|
||||
sourceFileNames[1],
|
||||
srcA,
|
||||
srcB,
|
||||
imageFileName,
|
||||
baseUrl,
|
||||
outputDir,
|
||||
@ -173,16 +177,107 @@ async function getPinUrlFromPinterest(keyword: string): Promise<string | null> {
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const keywords = [
|
||||
{ genre: "fantasy", subGenre: "ethereal", pinId: "311311393008071543" },
|
||||
{ genre: "fantasy", subGenre: "ethereal", pinId: "1688918605889326" },
|
||||
{ genre: "fantasy", subGenre: "ethereal", pinId: "4925880837133615" },
|
||||
{ genre: "fantasy", subGenre: "ethereal", pinId: "985231163711528" },
|
||||
{ genre: "fantasy", subGenre: "dark ethereal", pinId: "768497123954670531" },
|
||||
{ genre: "fantasy", subGenre: "dark ethereal", pinId: "5840674510388768" },
|
||||
{ genre: "fantasy", subGenre: "dark ethereal", pinId: "281123201734741654" },
|
||||
{ genre: "fantasy", subGenre: "dark ethereal", pinId: "41306521578186951" },
|
||||
];
|
||||
// Load pinterest keywords JSON, pick up to 20 subGenres and choose 1 pinId per subGenre
|
||||
const keywordsFilePath = path.resolve(process.cwd(), 'src', 'pinterest_keywords.json');
|
||||
let allKeywords: { genre: string; subGenre: string; pinIds?: string[]; pinId?: string[] }[] = [];
|
||||
try {
|
||||
const raw = await fs.readFile(keywordsFilePath, 'utf-8');
|
||||
allKeywords = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
logger.error('Failed to read pinterest keywords JSON:', err);
|
||||
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) {
|
||||
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 pinId = (genreSubGenre as any).pinId;
|
||||
let pin: string | null = null;
|
||||
|
||||
if (pinId) {
|
||||
pin = `https://www.pinterest.com/pin/${pinId}/`;
|
||||
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;
|
||||
}
|
||||
|
||||
logger.info(`--- Starting processing for pin: ${pin} ---`);
|
||||
const downloadedImagePaths = await downloadImagesFromPinterestPin(pin);
|
||||
if (downloadedImagePaths.length === 0) {
|
||||
logger.warn(`No images were downloaded for pin ${pin}. Skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const selectedImages = downloadedImagePaths.sort(() => 0.5 - Math.random()).slice(0, 2);
|
||||
logger.info(`--- Randomly selected ${selectedImages.length} images for processing ---`);
|
||||
|
||||
if (selectedImages.length === 2) {
|
||||
const pinId = path.basename(selectedImages[0], path.extname(selectedImages[0])).split('_related_')[0];
|
||||
const pinUrl = `https://www.pinterest.com/pin/${pinId}/`;
|
||||
const task = await getPromptsForImage(selectedImages, pinUrl, genre, subGenre);
|
||||
if (task) {
|
||||
generationTasks.push(task);
|
||||
for (let i = 0; i < 10; i++) {
|
||||
// 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}`);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Skipping pin ${pin} as it did not have at least 2 images.`);
|
||||
for (const imagePath of selectedImages) {
|
||||
await fs.unlink(imagePath);
|
||||
|
||||
if (!selectedPinId) {
|
||||
logger.warn(`No pinId available for ${genre}/${subGenre}. Skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const pin = `https://www.pinterest.com/pin/${selectedPinId}/`;
|
||||
logger.info(`--- Starting processing for pin: ${pin} ---`);
|
||||
|
||||
// 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.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const selectedImages = downloadedImagePaths.sort(() => 0.5 - Math.random()).slice(0, 2);
|
||||
logger.info(`--- Downloaded ${selectedImages.length} image(s) for processing ---`);
|
||||
|
||||
// proceed if we have at least one image
|
||||
if (selectedImages.length >= 1) {
|
||||
const task = await getPromptsForImage(selectedImages, pin, genre, subGenre);
|
||||
if (task) {
|
||||
generationTasks.push(task);
|
||||
}
|
||||
} else {
|
||||
logger.warn(`Skipping pin ${pin} as it did not yield images.`);
|
||||
for (const imagePath of selectedImages) {
|
||||
try {
|
||||
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,
|
||||
});
|
||||
logger.info(`Renamed files and updated database record for video ID: ${videoId}`);
|
||||
|
||||
await fs.unlink(newImagePath);
|
||||
logger.info(`Deleted paired image: ${newImagePath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('An error occurred during video generation or database operations:', error);
|
||||
|
||||
Reference in New Issue
Block a user