372 lines
18 KiB
Python
372 lines
18 KiB
Python
# foodie_config.py
|
|
# Constants shared across all automator scripts
|
|
from dotenv import load_dotenv
|
|
import os
|
|
from typing import Dict, List, Optional, TypedDict, Union
|
|
from pathlib import Path
|
|
import logging
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
handlers=[
|
|
logging.FileHandler('foodie_automator.log'),
|
|
logging.StreamHandler()
|
|
]
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Load environment variables
|
|
load_dotenv()
|
|
|
|
# API Keys
|
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
|
|
PIXABAY_API_KEY = os.getenv("PIXABAY_API_KEY")
|
|
FLICKR_API_KEY = os.getenv("FLICKR_API_KEY")
|
|
FLICKR_API_SECRET = os.getenv("FLICKR_API_SECRET")
|
|
|
|
# Validate required API keys
|
|
def validate_api_keys() -> None:
|
|
"""Validate that all required API keys are present."""
|
|
required_keys = {
|
|
"OPENAI_API_KEY": OPENAI_API_KEY,
|
|
"PIXABAY_API_KEY": PIXABAY_API_KEY,
|
|
"FLICKR_API_KEY": FLICKR_API_KEY,
|
|
"FLICKR_API_SECRET": FLICKR_API_SECRET
|
|
}
|
|
|
|
missing_keys = [key for key, value in required_keys.items() if not value]
|
|
if missing_keys:
|
|
logger.error(f"Missing required API keys: {', '.join(missing_keys)}")
|
|
raise ValueError(f"Missing required API keys: {', '.join(missing_keys)}")
|
|
|
|
# Type definitions
|
|
class AuthorConfig(TypedDict):
|
|
url: str
|
|
username: str
|
|
password: str
|
|
persona: str
|
|
bio: str
|
|
dob: str
|
|
|
|
class XCredentials(TypedDict):
|
|
username: str
|
|
x_username: str
|
|
api_key: str
|
|
api_secret: str
|
|
access_token: str
|
|
access_token_secret: str
|
|
client_secret: str
|
|
|
|
class PersonaConfig(TypedDict):
|
|
description: str
|
|
tone: str
|
|
article_prompt: str
|
|
x_prompt: str
|
|
|
|
# Author configurations
|
|
AUTHORS: List[AuthorConfig] = [
|
|
{
|
|
"url": "https://insiderfoodie.com",
|
|
"username": "owenjohnson",
|
|
"password": os.getenv("OWENJOHNSON_PASSWORD"),
|
|
"persona": "Visionary Editor",
|
|
"bio": "I oversee worldwide dining shifts, obsessed with the big picture. My edits deliver precise takes—charting the future of food with confidence.",
|
|
"dob": "1990-04-26"
|
|
},
|
|
{
|
|
"url": "https://insiderfoodie.com",
|
|
"username": "javiermorales",
|
|
"password": os.getenv("JAVIERMORALES_PASSWORD"),
|
|
"persona": "Foodie Critic",
|
|
"bio": "I judge food scenes worldwide, wielding a fearless pen. My takes expose what shines and what flops—no compromise, just truth.",
|
|
"dob": "1996-07-08"
|
|
},
|
|
{
|
|
"url": "https://insiderfoodie.com",
|
|
"username": "aishapatel",
|
|
"password": os.getenv("AISHAPATEL_PASSWORD"),
|
|
"persona": "Trend Scout",
|
|
"bio": "I scout global food trends, obsessed with what's emerging. My sharp predictions map the industry's path—always one step ahead.",
|
|
"dob": "1999-03-15"
|
|
},
|
|
{
|
|
"url": "https://insiderfoodie.com",
|
|
"username": "trangnguyen",
|
|
"password": os.getenv("TRANGNGUYEN_PASSWORD"),
|
|
"persona": "Culture Connoisseur",
|
|
"bio": "I trace worldwide dining traditions, weaving past into present. My words uncover the soul of flavor—connecting cultures bite by bite.",
|
|
"dob": "2002-08-22"
|
|
},
|
|
{
|
|
"url": "https://insiderfoodie.com",
|
|
"username": "keishareid",
|
|
"password": os.getenv("KEISHAREID_PASSWORD"),
|
|
"persona": "African-American Soul Food Sage",
|
|
"bio": "I bring soul food's legacy to life, blending history with modern vibes. My stories celebrate flavor and resilience—dishing out culture with every bite.",
|
|
"dob": "1994-06-10"
|
|
},
|
|
{
|
|
"url": "https://insiderfoodie.com",
|
|
"username": "lilamoreau",
|
|
"password": os.getenv("LILAMOREAU_PASSWORD"),
|
|
"persona": "Global Street Food Nomad",
|
|
"bio": "I roam the globe chasing street eats, from stalls to trucks. My tales uncover bold flavors and gritty trends shaping food on the go.",
|
|
"dob": "1993-02-14"
|
|
}
|
|
]
|
|
|
|
# X (Twitter) API credentials
|
|
X_API_CREDENTIALS: List[XCredentials] = [
|
|
{
|
|
"username": "owenjohnson",
|
|
"x_username": "@insiderfoodieowen",
|
|
"api_key": os.getenv("OWENJOHNSON_X_API_KEY"),
|
|
"api_secret": os.getenv("OWENJOHNSON_X_API_SECRET"),
|
|
"access_token": os.getenv("OWENJOHNSON_X_ACCESS_TOKEN"),
|
|
"access_token_secret": os.getenv("OWENJOHNSON_X_ACCESS_TOKEN_SECRET"),
|
|
"client_secret": os.getenv("OWENJOHNSON_X_CLIENT_SECRET")
|
|
},
|
|
{
|
|
"username": "javiermorales",
|
|
"x_username": "@insiderfoodiejavier",
|
|
"api_key": os.getenv("JAVIERMORALES_X_API_KEY"),
|
|
"api_secret": os.getenv("JAVIERMORALES_X_API_SECRET"),
|
|
"access_token": os.getenv("JAVIERMORALES_X_ACCESS_TOKEN"),
|
|
"access_token_secret": os.getenv("JAVIERMORALES_X_ACCESS_TOKEN_SECRET"),
|
|
"client_secret": os.getenv("JAVIERMORALES_X_CLIENT_SECRET")
|
|
},
|
|
{
|
|
"username": "aishapatel",
|
|
"x_username": "@insiderfoodieaisha",
|
|
"api_key": os.getenv("AISHAPATEL_X_API_KEY"),
|
|
"api_secret": os.getenv("AISHAPATEL_X_API_SECRET"),
|
|
"access_token": os.getenv("AISHAPATEL_X_ACCESS_TOKEN"),
|
|
"access_token_secret": os.getenv("AISHAPATEL_X_ACCESS_TOKEN_SECRET"),
|
|
"client_secret": os.getenv("AISHAPATEL_X_CLIENT_SECRET")
|
|
},
|
|
{
|
|
"username": "trangnguyen",
|
|
"x_username": "@insiderfoodietrang",
|
|
"api_key": os.getenv("TRANGNGUYEN_X_API_KEY"),
|
|
"api_secret": os.getenv("TRANGNGUYEN_X_API_SECRET"),
|
|
"access_token": os.getenv("TRANGNGUYEN_X_ACCESS_TOKEN"),
|
|
"access_token_secret": os.getenv("TRANGNGUYEN_X_ACCESS_TOKEN_SECRET"),
|
|
"client_secret": os.getenv("TRANGNGUYEN_X_CLIENT_SECRET")
|
|
},
|
|
{
|
|
"username": "keishareid",
|
|
"x_username": "@insiderfoodiekeisha",
|
|
"api_key": os.getenv("KEISHAREID_X_API_KEY"),
|
|
"api_secret": os.getenv("KEISHAREID_X_API_SECRET"),
|
|
"access_token": os.getenv("KEISHAREID_X_ACCESS_TOKEN"),
|
|
"access_token_secret": os.getenv("KEISHAREID_X_ACCESS_TOKEN_SECRET"),
|
|
"client_secret": os.getenv("KEISHAREID_X_CLIENT_SECRET")
|
|
},
|
|
{
|
|
"username": "lilamoreau",
|
|
"x_username": "@insiderfoodielila",
|
|
"api_key": os.getenv("LILAMOREAU_X_API_KEY"),
|
|
"api_secret": os.getenv("LILAMOREAU_X_API_SECRET"),
|
|
"access_token": os.getenv("LILAMOREAU_X_ACCESS_TOKEN"),
|
|
"access_token_secret": os.getenv("LILAMOREAU_X_ACCESS_TOKEN_SECRET"),
|
|
"client_secret": os.getenv("LILAMOREAU_X_CLIENT_SECRET")
|
|
}
|
|
]
|
|
|
|
# Persona configurations
|
|
PERSONA_CONFIGS: Dict[str, PersonaConfig] = {
|
|
"Visionary Editor": {
|
|
"description": "a commanding food editor with a borderless view",
|
|
"tone": "a polished and insightful tone, like 'This redefines culinary excellence.'",
|
|
"article_prompt": (
|
|
"You're {description}. Summarize this article in {tone}. "
|
|
"Explore a wide range of food-related topics, skip recipes. Generate exactly {num_paragraphs} paragraphs, 60-80 words each, full thoughts, with a single \n break. "
|
|
"Write naturally in a refined yet engaging style, with a slight Upworthy/Buzzfeed flair, without mentioning the source name or URL directly in the text. "
|
|
"Add a bold take and end with a thought-provoking question like Neil Patel would do to boost engagement! Do not include emojis in the summary."
|
|
),
|
|
"x_prompt": (
|
|
"Craft a tweet as {description}. Keep it under 280 characters, using {tone}. "
|
|
"For article tweets, include the article title, a quirky hook, and the URL. "
|
|
"For engagement tweets, ask a question about food trends, foods, or articles to engage the public. "
|
|
"For personal tweets, reflect on your role at InsiderFoodie or background. "
|
|
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
|
|
)
|
|
},
|
|
"Foodie Critic": {
|
|
"description": "a seasoned foodie reviewer with a sharp eye",
|
|
"tone": "a professional yet engaging tone, like 'This dish is a revelation.'",
|
|
"article_prompt": (
|
|
"You're {description}. Summarize this article in {tone}. "
|
|
"Explore a wide range of food-related topics, skip recipes. Generate exactly {num_paragraphs} paragraphs, 60-80 words each, full thoughts, with a single \n break. "
|
|
"Write naturally in a refined yet engaging style, with a slight Upworthy/Buzzfeed flair, without mentioning the source name or URL directly in the text. "
|
|
"Add a subtle opinion and end with a thought-provoking question like Neil Patel would do to boost engagement! Do not include emojis in the summary."
|
|
),
|
|
"x_prompt": (
|
|
"Craft a tweet as {description}. Keep it under 280 characters, using {tone}. "
|
|
"For article tweets, include the article title, a quirky hook, and the URL. "
|
|
"For engagement tweets, ask a question about food trends, foods, or articles to engage the public. "
|
|
"For personal tweets, reflect on your role at InsiderFoodie or background. "
|
|
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
|
|
)
|
|
},
|
|
"Trend Scout": {
|
|
"description": "a forward-thinking editor obsessed with trends",
|
|
"tone": "an insightful and forward-looking tone, like 'This sets the stage for what's next.'",
|
|
"article_prompt": (
|
|
"You're {description}. Summarize this article in {tone}. "
|
|
"Explore a wide range of food-related topics, skip recipes. Generate exactly {num_paragraphs} paragraphs, 60-80 words each, full thoughts, with a single \n break. "
|
|
"Write naturally in a refined yet engaging style, with a slight Upworthy/Buzzfeed flair, without mentioning the source name or URL directly in the text. "
|
|
"Predict what's next and end with a thought-provoking question like Neil Patel would do to boost engagement! Do not include emojis in the summary."
|
|
),
|
|
"x_prompt": (
|
|
"Craft a tweet as {description}. Keep it under 280 characters, using {tone}. "
|
|
"For article tweets, include the article title, a quirky hook, and the URL. "
|
|
"For engagement tweets, ask a question about food trends, foods, or articles to engage the public. "
|
|
"For personal tweets, reflect on your role at InsiderFoodie or background. "
|
|
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
|
|
)
|
|
},
|
|
"Culture Connoisseur": {
|
|
"description": "a cultured food writer who loves storytelling",
|
|
"tone": "a warm and thoughtful tone, like 'This evokes a sense of tradition.'",
|
|
"article_prompt": (
|
|
"You're {description}. Summarize this article in {tone}. "
|
|
"Explore a wide range of food-related topics, skip recipes. Generate exactly {num_paragraphs} paragraphs, 60-80 words each, full thoughts, with a single \n break. "
|
|
"Write naturally in a refined yet engaging style, with a slight Upworthy/Buzzfeed flair, without mentioning the source name or URL directly in the text. "
|
|
"Add a thoughtful observation and end with a thought-provoking question like Neil Patel would do to boost engagement! Do not include emojis in the summary."
|
|
),
|
|
"x_prompt": (
|
|
"Craft a tweet as {description}. Keep it under 280 characters, using {tone}. "
|
|
"For article tweets, include the article title, a quirky hook, and the URL. "
|
|
"For engagement tweets, ask a question about food trends, foods, or articles to engage the public. "
|
|
"For personal tweets, reflect on your role at InsiderFoodie or background. "
|
|
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
|
|
)
|
|
},
|
|
"African-American Soul Food Sage": {
|
|
"description": "a vibrant storyteller rooted in African-American culinary heritage",
|
|
"tone": "a heartfelt and authentic tone, like 'This captures the essence of heritage.'",
|
|
"article_prompt": (
|
|
"You're {description}. Summarize this article in {tone}. "
|
|
"Explore a wide range of food-related topics, skip recipes. Generate exactly {num_paragraphs} paragraphs, 60-80 words each, full thoughts, with a single \n break. "
|
|
"Write naturally in a refined yet engaging style, with a slight Upworthy/Buzzfeed flair, without mentioning the source name or URL directly in the text. "
|
|
"Add a heritage twist and end with a thought-provoking question like Neil Patel would do to boost engagement! Do not include emojis in the summary."
|
|
),
|
|
"x_prompt": (
|
|
"Craft a tweet as {description}. Keep it under 280 characters, using {tone}. "
|
|
"For article tweets, include the article title, a quirky hook, and the URL. "
|
|
"For engagement tweets, ask a question about food trends, foods, or articles to engage the public. "
|
|
"For personal tweets, reflect on your role at InsiderFoodie or background. "
|
|
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
|
|
)
|
|
},
|
|
"Global Street Food Nomad": {
|
|
"description": "an adventurous explorer of global street food",
|
|
"tone": "a bold and adventurous tone, like 'This takes you on a global journey.'",
|
|
"article_prompt": (
|
|
"You're {description}. Summarize this article in {tone}. "
|
|
"Explore a wide range of food-related topics, skip recipes. Generate exactly {num_paragraphs} paragraphs, 60-80 words each, full thoughts, with a single \n break. "
|
|
"Write naturally in a refined yet engaging style, with a slight Upworthy/Buzzfeed flair, without mentioning the source name or URL directly in the text. "
|
|
"Drop a street-level insight and end with a thought-provoking question like Neil Patel would do to boost engagement! Do not include emojis in the summary."
|
|
),
|
|
"x_prompt": (
|
|
"Craft a tweet as {description}. Keep it under 280 characters, using {tone}. "
|
|
"For article tweets, include the article title, a quirky hook, and the URL. "
|
|
"For engagement tweets, ask a question about food trends, foods, or articles to engage the public. "
|
|
"For personal tweets, reflect on your role at InsiderFoodie or background. "
|
|
"Avoid emojis and clichés like 'game-changer'. Return only the tweet text."
|
|
)
|
|
}
|
|
}
|
|
|
|
# File paths
|
|
BASE_DIR = Path("/home/blade/insiderfoodie/foodie_automator")
|
|
FILE_PATHS = {
|
|
"posted_rss_titles": BASE_DIR / "posted_rss_titles.json",
|
|
"posted_google_titles": BASE_DIR / "posted_google_titles.json",
|
|
"posted_reddit_titles": BASE_DIR / "posted_reddit_titles.json",
|
|
"used_images": BASE_DIR / "used_images.json",
|
|
"author_backgrounds": BASE_DIR / "author_backgrounds.json",
|
|
"x_post_counts": BASE_DIR / "x_post_counts.json",
|
|
"recent_posts": BASE_DIR / "recent_posts.json"
|
|
}
|
|
|
|
# Expiration periods
|
|
EXPIRATION_DAYS = 3
|
|
IMAGE_EXPIRATION_DAYS = 7
|
|
|
|
# RSS feed configurations
|
|
RSS_FEEDS: List[str] = [
|
|
"https://www.eater.com/rss/full.xml",
|
|
"https://www.nrn.com/rss.xml",
|
|
"https://rss.nytimes.com/services/xml/rss/nyt/DiningandWine.xml",
|
|
"https://www.theguardian.com/food/rss"
|
|
]
|
|
|
|
RSS_FEED_NAMES: Dict[str, tuple[str, str]] = {
|
|
"https://www.eater.com/rss/full.xml": ("Eater", "https://www.eater.com/"),
|
|
"https://www.nrn.com/rss.xml": ("Nation's Restaurant News", "https://www.nrn.com/"),
|
|
"https://rss.nytimes.com/services/xml/rss/nyt/DiningandWine.xml": ("The New York Times", "https://www.nytimes.com/section/food"),
|
|
"https://www.theguardian.com/food/rss": ("The Guardian Food", "https://www.theguardian.com/food")
|
|
}
|
|
|
|
RECIPE_KEYWORDS = ["recipe", "cook", "bake", "baking", "cooking", "ingredient", "method", "mix", "stir", "preheat", "dinners", "make", "dish", "healthy"]
|
|
PROMO_KEYWORDS = ["we serve", "our guests", "event", "competition", "franchise", "off", "discount", "sale"]
|
|
HOME_KEYWORDS = ["home", "house", "household", "appliance", "kitchen", "gadget"]
|
|
PRODUCT_KEYWORDS = ["best", "buy", "storage", "organizer", "shop", "price", "container", "product", "deal", "sale", "discount"]
|
|
|
|
CATEGORIES = [
|
|
"People", "Trends", "Travel",
|
|
"Lifestyle", "Buzz", "Culture", "Health", "Drink", "Food", "Eats"
|
|
]
|
|
|
|
REDDIT_CLIENT_ID = os.getenv("REDDIT_CLIENT_ID")
|
|
REDDIT_CLIENT_SECRET = os.getenv("REDDIT_CLIENT_SECRET")
|
|
REDDIT_USER_AGENT = os.getenv("REDDIT_USER_AGENT")
|
|
REDDIT_SUBREDDITS = [
|
|
"food",
|
|
"FoodPorn",
|
|
"spicy"
|
|
]
|
|
FAST_FOOD_KEYWORDS = [
|
|
"mcdonald", "burger king", "wendy", "taco bell", "kfc",
|
|
"subway", "domino", "pizza hut", "chipotle", "dunkin",
|
|
"starbucks", "sonic", "arby", "jack in the box", "popeyes",
|
|
"fast food", "chain", "drive-thru"
|
|
]
|
|
|
|
SUMMARY_MODEL = "gpt-4o" # or "gpt-4.1-mini" for testing
|
|
LIGHT_TASK_MODEL = "gpt-4o-mini"
|
|
|
|
def get_clean_source_name(source_name: str) -> str:
|
|
"""Clean and standardize source names."""
|
|
try:
|
|
# Remove common prefixes and suffixes
|
|
clean_name = source_name.strip()
|
|
clean_name = clean_name.replace("The ", "").replace("the ", "")
|
|
clean_name = clean_name.replace("Food", "").replace("food", "")
|
|
clean_name = clean_name.replace("Dining", "").replace("dining", "")
|
|
clean_name = clean_name.replace("Restaurant", "").replace("restaurant", "")
|
|
|
|
# Remove any remaining whitespace
|
|
clean_name = " ".join(clean_name.split())
|
|
|
|
return clean_name if clean_name else source_name
|
|
except Exception as e:
|
|
logger.error(f"Error cleaning source name '{source_name}': {e}")
|
|
return source_name
|
|
|
|
# Validate configurations on import
|
|
validate_api_keys()
|
|
|
|
# Ensure all file paths exist
|
|
for path in FILE_PATHS.values():
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
if not path.exists():
|
|
path.touch()
|
|
logger.info(f"Created missing file: {path}")
|
|
|
|
# Log successful configuration
|
|
logger.info("Configuration loaded successfully") |