Replace broken DDG search with AI thumbnail generation fallback

DuckDuckGo's HTML search endpoint now requires vqd session tokens,
making server-side search-based URL resolution unreliable. Simplified
the thumbnail pipeline:

- OG scrape only for non-aggregator URLs (direct publisher pages)
- AI image generation via OpenRouter for aggregator articles (Google News)
- Added Image Model settings field in admin UI
- Added thumbnail_local column for AI-generated images
- Updated public/admin display to check thumbnail_local as fallback
- Removed all DuckDuckGo search code (find_article_url_via_search,
  extract_ddg_url, resolve_article_for_scraping)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Terence Ong
2026-04-06 20:29:32 +07:00
parent de35f2d684
commit cd8eee11b8
7 changed files with 118 additions and 228 deletions

View File

@@ -211,6 +211,7 @@ class Peptide_News_Admin {
$this->add_setting( 'openrouter_api_key', __( 'OpenRouter API Key', 'peptide-news' ), 'render_openrouter_key_field', 'peptide_news_llm_section' );
$this->add_setting( 'llm_keywords_model', __( 'Keywords Model', 'peptide-news' ), 'render_llm_keywords_model_field', 'peptide_news_llm_section' );
$this->add_setting( 'llm_summary_model', __( 'Summary Model', 'peptide-news' ), 'render_llm_summary_model_field', 'peptide_news_llm_section' );
$this->add_setting( 'llm_image_model', __( 'Image Generation Model', 'peptide-news' ), 'render_llm_image_model_field', 'peptide_news_llm_section' );
$this->add_setting( 'llm_max_articles', __( 'Max Articles per Cycle', 'peptide-news' ), 'render_llm_max_articles_field', 'peptide_news_llm_section' );
}
@@ -264,6 +265,7 @@ class Peptide_News_Admin {
'openrouter_api_key' => 'sanitize_text_field',
'llm_keywords_model' => array( $this, 'sanitize_model_id' ),
'llm_summary_model' => array( $this, 'sanitize_model_id' ),
'llm_image_model' => array( $this, 'sanitize_model_id' ),
'llm_max_articles' => 'absint',
'thumbnail_fallback' => 'esc_url_raw',
);
@@ -530,6 +532,15 @@ class Peptide_News_Admin {
echo '<p class="description">' . esc_html__( 'OpenRouter model ID for article summarization (e.g., google/gemini-2.0-flash-001, anthropic/claude-3.5-sonnet).', 'peptide-news' ) . '</p>';
}
public function render_llm_image_model_field() {
$value = get_option( 'peptide_news_llm_image_model', '' );
printf(
'<input type="text" name="peptide_news_llm_image_model" value="%s" class="regular-text" />',
esc_attr( $value )
);
echo '<p class="description">' . esc_html__( 'OpenRouter model ID for AI thumbnail generation (e.g., openai/dall-e-3, stabilityai/stable-diffusion-xl). Leave blank to disable AI thumbnails.', 'peptide-news' ) . '</p>';
}
public function render_llm_max_articles_field() {
$value = get_option( 'peptide_news_llm_max_articles', 10 );
printf(
@@ -609,8 +620,12 @@ class Peptide_News_Admin {
$btn.prop('disabled', false).text('<?php echo esc_js( __( 'Fix Article Thumbnails', 'peptide-news' ) ); ?>');
if (response.success) {
var d = response.data;
var msg = d.updated + ' of ' + d.total + ' thumbnails found.';
if (d.failed > 0) { msg += ' ' + d.failed + ' could not be resolved.'; }
var parts = [];
if (d.scraped > 0) { parts.push(d.scraped + ' scraped'); }
if (d.generated > 0) { parts.push(d.generated + ' AI-generated'); }
if (d.failed > 0) { parts.push(d.failed + ' remaining'); }
var msg = (d.scraped + d.generated) + ' of ' + d.total + ' thumbnails resolved';
if (parts.length) { msg += ' (' + parts.join(', ') + ')'; }
$result.text(msg);
if (d.samples && d.samples.length) { console.log('Thumbnail backfill:', JSON.stringify(d.samples, null, 2)); }
} else {

View File

@@ -73,8 +73,16 @@ $total_pages = ceil( $total / $per_page );
<td><?php echo esc_html( wp_date( 'M j, Y', strtotime( $article->published_at ) ) ); ?></td>
<td><?php echo esc_html( wp_date( 'M j, Y', strtotime( $article->fetched_at ) ) ); ?></td>
<td>
<?php if ( ! empty( $article->thumbnail_url ) ) : ?>
<img src="<?php echo esc_url( $article->thumbnail_url ); ?>" alt="" style="width:50px;height:50px;object-fit:cover;border-radius:4px;" />
<?php
$thumb_src = '';
if ( ! empty( $article->thumbnail_url ) && '_no_image' !== $article->thumbnail_url ) {
$thumb_src = $article->thumbnail_url;
} elseif ( ! empty( $article->thumbnail_local ) ) {
$upload_dir = wp_upload_dir();
$thumb_src = $upload_dir['baseurl'] . '/' . $article->thumbnail_local;
}
if ( $thumb_src ) : ?>
<img src="<?php echo esc_url( $thumb_src ); ?>" alt="" style="width:50px;height:50px;object-fit:cover;border-radius:4px;" />
<?php else : ?>
<span class="dashicons dashicons-format-image" style="color:#ccc;font-size:24px;"></span>
<?php endif; ?>