| Server IP : 3.147.158.171 / Your IP : 216.73.216.88 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/repo/api/ |
Upload File : |
"""
Article Generation Script
Creates articles using AI characters from Drupal on WordPress sites.
Supports featured images, secondary images, current events research, fact-checking, and series.
Usage:
python create_articles.py <site_id> [options]
Options:
--character <name> Use only the specified character (bypass selection)
--character-description-file <path> Load character description override from file
--featured-image Generate featured image for articles
--secondary-image Generate secondary image for articles
--fact-check Post fact-check comments on new articles
--corrections Post a corrections follow-up after the fact-check (requires --fact-check or the character's fact-check setting)
--current-events Research current events before generating articles
--no-previous-articles Exclude character's previous articles from context
--series <name> Create article as part of a series (auto-creates or continues)
--search-depth <basic|advanced> Override search depth (basic=1 credit, advanced=2 credits)
--chunks-per-source <1-3> Override chunks per source (only with advanced depth)
--seed-image-url <url> Reference image URL anchoring the featured image (single article or Part 1 only)
--seed-image-instructions <text> How the seed image should be used (e.g. "use this person as the main character")
--seed-image-instructions-file <path> Same as --seed-image-instructions but read from a file
Examples:
python create_articles.py mysite --featured-image
python create_articles.py mysite --character "John Doe" --fact-check
python create_articles.py mysite --character-description-file cron-character-description.txt --current-events
python create_articles.py mysite --no-previous-articles
python create_articles.py mysite --character "John Doe" --series "My Story" --featured-image
python create_articles.py mysite --current-events --search-depth advanced --chunks-per-source 2
python create_articles.py mysite --character "John Doe" --featured-image \\
--seed-image-url https://example.com/anchor.jpg \\
--seed-image-instructions "use this person as the main character"
"""
import asyncio
import logging
import sys
import time
# IMPORTANT: Import cron_common FIRST to load .env before any app imports
import cron_common
from app.appTypes import ArticleCreationResult
from app.services.article_creation_service import ArticleCreationService
from app.services import posting_history
from app.services.credits_guard import CreditsExhausted
# Parse all arguments (including article-specific flags)
(site_id, character_name, character_description_override,
generate_featured_image, generate_secondary_image,
run_fact_check, research_current_events,
include_previous_articles, series_name,
search_depth_override, chunks_per_source_override,
seed_image_url, seed_image_instructions,
run_corrections) = cron_common.parse_article_arguments(__doc__)
# Display configuration
print(f"site id: {site_id}")
if character_name:
print(f"SINGLE CHARACTER MODE: {character_name}")
if generate_featured_image:
print("FEATURED IMAGE GENERATION ENABLED")
if generate_secondary_image:
print("SECONDARY IMAGE GENERATION ENABLED")
if run_fact_check:
print("FACT-CHECKING ENABLED")
if research_current_events:
print("CURRENT EVENTS RESEARCH ENABLED")
if not include_previous_articles:
print("PREVIOUS ARTICLES EXCLUDED")
if series_name:
print(f"SERIES MODE: {series_name}")
if search_depth_override:
print(f"SEARCH DEPTH OVERRIDE: {search_depth_override}")
if chunks_per_source_override is not None:
print(f"CHUNKS PER SOURCE OVERRIDE: {chunks_per_source_override}")
if seed_image_url:
print(f"SEED IMAGE URL: {seed_image_url}")
if seed_image_instructions:
print(f"SEED IMAGE INSTRUCTIONS: {seed_image_instructions}")
# Check internet connectivity
cron_common.check_internet_connectivity()
# Setup logging
cron_common.setup_logging('api.log')
# Track script startup time
script_start = time.time()
# Initialize services
step_start = time.time()
wp_service, drupal_service, site_config_service, chat_service = cron_common.initialize_services()
logging.info(f"Services initialized ({time.time() - step_start:.2f}s)")
# Load site configuration
step_start = time.time()
site = cron_common.load_site_config(site_config_service, site_id)
logging.info(f"Site config loaded ({time.time() - step_start:.2f}s)")
# Get JWT token
step_start = time.time()
wp_token = cron_common.get_jwt_token(wp_service, site, site_id)
logging.info(f"JWT token obtained ({time.time() - step_start:.2f}s)")
# Fetch and filter characters
step_start = time.time()
filtered_characters = cron_common.fetch_and_filter_characters(
drupal_service, wp_service, site, site_id, wp_token
)
logging.info(f"Characters fetched and filtered ({time.time() - step_start:.2f}s)")
# Select characters for article generation
step_start = time.time()
characters = cron_common.select_characters(
filtered_characters, drupal_service, site, character_name,
field='articlesPerDay'
)
logging.info(f"Characters selected ({time.time() - step_start:.2f}s)")
# Filter out characters that already posted today (skip in single-character mode)
if not character_name:
step_start = time.time()
characters = cron_common.filter_already_posted_today(
characters, wp_service, site, wp_token
)
logging.info(f"Already-posted-today filter applied ({time.time() - step_start:.2f}s)")
if not characters:
logging.info('No characters remaining after today-filter.')
print('No characters remaining after today-filter.')
sys.exit(0)
# Load each character's existing articles
step_start = time.time()
cron_common.load_character_articles(characters, wp_service, site, wp_token)
logging.info(f"Character articles loaded ({time.time() - step_start:.2f}s)")
logging.info(f"Setup complete ({time.time() - script_start:.2f}s total)")
# Initialize article creation service
article_creation_service = ArticleCreationService()
# Generate articles with inline fact-checking
async def create_articles_for_characters():
"""Generate articles for all characters using ArticleCreationService."""
created_articles = []
for index, character in enumerate(characters):
try:
logging.info(f"Creating article for character: {character.name}")
# Per-character: use global CLI flag if set, otherwise use character's own setting
char_research = research_current_events or getattr(character, 'researchCurrentEvents', False)
char_featured_image = generate_featured_image or getattr(character, 'createFeaturedImage', False)
char_secondary_image = generate_secondary_image or getattr(character, 'createSecondaryImage', False)
char_fact_check = run_fact_check or getattr(character, 'enableFactChecking', False)
char_corrections = run_corrections or getattr(character, 'enableCorrections', False)
char_include_previous = include_previous_articles and getattr(character, 'includePreviousArticles', True)
# Apply CLI search parameter overrides to character object
if search_depth_override:
character.searchDepth = search_depth_override
if chunks_per_source_override is not None:
character.chunksPerSource = chunks_per_source_override
# Call ArticleCreationService directly (no progress callback for CLI)
result: ArticleCreationResult = await article_creation_service.create_article(
site=site,
character=character,
character_description_override=character_description_override,
generate_featured_image=char_featured_image,
generate_secondary_image=char_secondary_image,
research_current_events=char_research,
include_previous_articles=char_include_previous,
enable_fact_checking=char_fact_check,
enable_corrections=char_corrections,
series_name=series_name,
seed_image_url=seed_image_url,
seed_image_instructions=seed_image_instructions,
progress_callback=None # Silent operation for CLI
)
# Store article data
created_articles.append({
'character': character,
'article': result.article,
'metadata': {
'post_id': result.post_id,
'featured_image_url': result.featured_image_url,
'secondary_image_url': result.secondary_image_url,
'search_results': result.search_results
},
'actual_model': result.actual_model
})
# Log success
model_info = f"{result.actual_model.provider.value} {result.actual_model.name}"
logging.info(f"Article created for '{character.name}' using model: {model_info}")
# Record the posting so the schedule calendar can link to it
posting_history.record_post(
site,
kind='article',
character=character.name,
status='success',
character_slug=character.slug,
storage_id=character.storageId,
post_id=result.post_id,
url=result.permalink or f"{site.wp_host}/?p={result.post_id}",
title=result.article.title,
model=result.actual_model.name,
)
except CreditsExhausted as e:
# Out of credits is a site-level pause, not a per-character failure:
# record it cleanly (no scary traceback) and stop — every remaining
# character shares the same balance and would hit the cached verdict.
logging.warning(f"Article generation paused for '{site.slug}': {e}")
posting_history.record_post(
site,
kind='article',
character=character.name,
status='paused',
character_slug=character.slug,
storage_id=character.storageId,
error=f"out of credits ({e.reason})",
)
break
except Exception as e:
logging.error(f"Failed to create article for '{character.name}': {e}", exc_info=True)
posting_history.record_post(
site,
kind='article',
character=character.name,
status='failed',
character_slug=character.slug,
storage_id=character.storageId,
error=str(e),
)
# Continue with other characters even if one fails
continue
# Delay between characters (except for last one)
if index < len(characters) - 1:
logging.info("Sleeping 60 seconds before next character...")
time.sleep(60)
return created_articles
try:
created_articles = asyncio.run(create_articles_for_characters())
logging.info(f"Successfully generated articles for {len(characters)} character(s)")
if run_fact_check:
logging.info(f"Fact-checking was performed inline during article creation")
logging.info(f"Script completed ({time.time() - script_start:.2f}s total)")
except Exception as e:
logging.error(f"Failed to generate articles: {e}", exc_info=True)
print(f"Error generating articles: {e}")
sys.exit(1)
print(f"Article generation complete! Created {len(created_articles) if created_articles else 0} article(s)")