Fix duplicate image, remove hardcoded model references
Three fixes in one: 1. Duplicate image: prepend_caption_to_post() was embedding the full image + caption as a <figure> block, but the theme already displays the featured image. Now only the caption text is prepended — no second copy of the image. 2. Hardcoded model names: removed all FLUX.1/FLUX.2 references from model-agnostic code (pipeline, prompt builder, sideloader, OpenRouter provider, interface, admin labels). The Cloudflare provider still references FLUX since it specifically wraps that model family. 3. Test alignment: OpenRouterImagePricingTest was asserting against removed models (flux-2-pro, seedream). Updated to match current COST_PER_IMAGE entries (Gemini Flash, GPT-5 Image). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -181,8 +181,8 @@ class PRAutoBlogger_Settings_Fields_Extended {
|
|||||||
'section' => 'prautoblogger_images',
|
'section' => 'prautoblogger_images',
|
||||||
'default' => 'openrouter',
|
'default' => 'openrouter',
|
||||||
'options' => [
|
'options' => [
|
||||||
'openrouter' => __( 'OpenRouter (FLUX.2 Pro, Seedream, GPT-Image)', 'prautoblogger' ),
|
'openrouter' => __( 'OpenRouter (multiple models available)', 'prautoblogger' ),
|
||||||
'cloudflare' => __( 'Cloudflare Workers AI (FLUX.1 schnell)', 'prautoblogger' ),
|
'cloudflare' => __( 'Cloudflare Workers AI', 'prautoblogger' ),
|
||||||
],
|
],
|
||||||
'description' => __( 'OpenRouter reuses your existing API key and offers higher-quality models. Cloudflare is cheaper but lower quality.', 'prautoblogger' ),
|
'description' => __( 'OpenRouter reuses your existing API key and offers higher-quality models. Cloudflare is cheaper but lower quality.', 'prautoblogger' ),
|
||||||
],
|
],
|
||||||
@@ -193,7 +193,7 @@ class PRAutoBlogger_Settings_Fields_Extended {
|
|||||||
'section' => 'prautoblogger_images',
|
'section' => 'prautoblogger_images',
|
||||||
'default' => PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL,
|
'default' => PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL,
|
||||||
'capability' => 'image_generation',
|
'capability' => 'image_generation',
|
||||||
'description' => __( 'Pick a model from your selected provider. FLUX.2 Pro recommended for quality.', 'prautoblogger' ),
|
'description' => __( 'Pick a model from your selected provider.', 'prautoblogger' ),
|
||||||
'badge' => __( 'Quality', 'prautoblogger' ),
|
'badge' => __( 'Quality', 'prautoblogger' ),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ class PRAutoBlogger_Image_Media_Sideloader {
|
|||||||
gmdate( 'Y-m-d_H-i-s' )
|
gmdate( 'Y-m-d_H-i-s' )
|
||||||
);
|
);
|
||||||
|
|
||||||
return $base . '.png'; // Default to PNG since FLUX.1 outputs PNG.
|
return $base . '.png'; // Default to PNG — most image models output PNG.
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ class PRAutoBlogger_Image_Pipeline {
|
|||||||
/**
|
/**
|
||||||
* Default image dimensions (landscape).
|
* Default image dimensions (landscape).
|
||||||
*/
|
*/
|
||||||
// FLUX.1 requires both dimensions to be divisible by 8.
|
// Dimensions must be divisible by 8 (required by some providers).
|
||||||
// 1200×632 is the closest 8-aligned pair to the standard OG image size (1200×630).
|
// 1200×632 is the closest 8-aligned pair to the standard OG image size (1200×630).
|
||||||
private const DEFAULT_WIDTH = 1200;
|
private const DEFAULT_WIDTH = 1200;
|
||||||
private const DEFAULT_HEIGHT = 632;
|
private const DEFAULT_HEIGHT = 632;
|
||||||
|
|
||||||
/** @var PRAutoBlogger_Image_Provider_Interface Image gen provider (FLUX.1 etc). */
|
/** @var PRAutoBlogger_Image_Provider_Interface Image gen provider. */
|
||||||
private PRAutoBlogger_Image_Provider_Interface $provider;
|
private PRAutoBlogger_Image_Provider_Interface $provider;
|
||||||
|
|
||||||
/** @var PRAutoBlogger_Image_Prompt_Builder Builds scene + caption from article data. */
|
/** @var PRAutoBlogger_Image_Prompt_Builder Builds scene + caption from article data. */
|
||||||
@@ -248,15 +248,14 @@ class PRAutoBlogger_Image_Pipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepend a comic caption as a styled figcaption block to post content.
|
* Prepend the comic caption as styled text at the top of the post content.
|
||||||
*
|
*
|
||||||
* Inserts a <figure> block at the top of the post containing the featured
|
* Only inserts the caption — NOT the image. The theme already displays the
|
||||||
* image and a <figcaption> with the comic punchline. Uses inline styles
|
* featured image, so embedding it again would cause a duplicate.
|
||||||
* for portability across themes.
|
|
||||||
*
|
*
|
||||||
* @param int $post_id Post ID.
|
* @param int $post_id Post ID.
|
||||||
* @param int $attachment_id Featured image attachment ID.
|
* @param int $attachment_id Featured image attachment ID (for meta storage only).
|
||||||
* @param string $caption Caption text (the comic punchline).
|
* @param string $caption Caption text (the comic punchline).
|
||||||
*/
|
*/
|
||||||
private function prepend_caption_to_post( int $post_id, int $attachment_id, string $caption ): void {
|
private function prepend_caption_to_post( int $post_id, int $attachment_id, string $caption ): void {
|
||||||
$post = get_post( $post_id );
|
$post = get_post( $post_id );
|
||||||
@@ -264,23 +263,15 @@ class PRAutoBlogger_Image_Pipeline {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$img_url = wp_get_attachment_url( $attachment_id );
|
// Caption-only block — the theme handles the featured image display.
|
||||||
$alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
|
$caption_html = sprintf(
|
||||||
|
'<p class="prautoblogger-comic-caption" style="text-align:center;font-style:italic;color:#555;font-size:1.1em;margin:0 0 2em 0;">— "%s"</p>',
|
||||||
// Build a self-contained figure block with the comic image and caption.
|
|
||||||
$figure = sprintf(
|
|
||||||
'<figure class="prautoblogger-comic" style="text-align:center;margin:0 0 2em 0;">'
|
|
||||||
. '<img src="%s" alt="%s" style="max-width:100%%;height:auto;border:2px solid #333;border-radius:4px;" />'
|
|
||||||
. '<figcaption style="font-style:italic;color:#555;margin-top:0.5em;font-size:1.1em;">— "%s"</figcaption>'
|
|
||||||
. '</figure>',
|
|
||||||
esc_url( $img_url ),
|
|
||||||
esc_attr( $alt ),
|
|
||||||
esc_html( $caption )
|
esc_html( $caption )
|
||||||
);
|
);
|
||||||
|
|
||||||
wp_update_post( [
|
wp_update_post( [
|
||||||
'ID' => $post_id,
|
'ID' => $post_id,
|
||||||
'post_content' => $figure . "\n\n" . $post->post_content,
|
'post_content' => $caption_html . "\n\n" . $post->post_content,
|
||||||
] );
|
] );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
* Builds image prompts from article content or source data.
|
* Builds image prompts from article content or source data.
|
||||||
*
|
*
|
||||||
* Uses a cheap LLM (via OpenRouter) to distill article content into concise
|
* Uses a cheap LLM (via OpenRouter) to distill article content into concise
|
||||||
* visual scene descriptions optimised for FLUX.1. Falls back to rule-based
|
* visual scene descriptions optimised for image generation. Falls back to rule-based
|
||||||
* synthesis if the LLM call fails, so image generation never blocks on a
|
* synthesis if the LLM call fails, so image generation never blocks on a
|
||||||
* prompt-rewriting outage.
|
* prompt-rewriting outage.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* OpenRouter image provider — FLUX.2, Seedream, GPT-Image, Gemini Flash.
|
* OpenRouter image provider — model-agnostic, works with any image model.
|
||||||
*
|
*
|
||||||
* Calls the standard OpenRouter chat/completions endpoint with
|
* Calls the standard OpenRouter chat/completions endpoint with
|
||||||
* `modalities: ["image"]`. Reuses the same API key and optional
|
* `modalities: ["image"]`. Reuses the same API key and optional
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ declare(strict_types=1);
|
|||||||
* content generation run, after editorial review, before publish.
|
* content generation run, after editorial review, before publish.
|
||||||
* Dependencies: None at interface level — implementations wrap a specific HTTP API.
|
* Dependencies: None at interface level — implementations wrap a specific HTTP API.
|
||||||
*
|
*
|
||||||
* @see class-cloudflare-image-provider.php — Default implementation (FLUX.1 via Workers AI).
|
* @see class-cloudflare-image-provider.php — Cloudflare Workers AI implementation.
|
||||||
* @see interface-llm-provider.php — Parallel pattern for LLM providers.
|
* @see interface-llm-provider.php — Parallel pattern for LLM providers.
|
||||||
* @see ARCHITECTURE.md — External API Integrations table.
|
* @see ARCHITECTURE.md — External API Integrations table.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|||||||
* Contract for a provider-specific AI model registry.
|
* Contract for a provider-specific AI model registry.
|
||||||
*
|
*
|
||||||
* Phase 1 (v1): single implementation for OpenRouter (`class-open-router-model-registry.php`).
|
* Phase 1 (v1): single implementation for OpenRouter (`class-open-router-model-registry.php`).
|
||||||
* Phase 3: a second implementation wraps Cloudflare Workers AI FLUX model data. A
|
* Phase 3: a second implementation wraps Cloudflare Workers AI model data. A
|
||||||
* higher-level dispatcher iterates registered registries, calls each with the same
|
* higher-level dispatcher iterates registered registries, calls each with the same
|
||||||
* capability, and unions the results — the per-registry interface stays unchanged.
|
* capability, and unions the results — the per-registry interface stays unchanged.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ define( 'PRAUTOBLOGGER_DEFAULT_WRITING_MODEL', 'google/gemini-2.5-flash-lite' );
|
|||||||
define( 'PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL', 'google/gemini-2.5-flash-lite' );
|
define( 'PRAUTOBLOGGER_DEFAULT_EDITOR_MODEL', 'google/gemini-2.5-flash-lite' );
|
||||||
|
|
||||||
// Image generation defaults.
|
// Image generation defaults.
|
||||||
// Default provider is OpenRouter (higher quality); Cloudflare is the fallback.
|
// Default provider is OpenRouter; Cloudflare is the fallback.
|
||||||
// Default model is FLUX.2 Pro via OpenRouter (~$0.03/image, good quality).
|
// Default model is Gemini 2.5 Flash Image ($0.005/image, good for comics).
|
||||||
|
// User can override via Settings → Images → Image Model.
|
||||||
define( 'PRAUTOBLOGGER_DEFAULT_IMAGE_PROVIDER', 'openrouter' );
|
define( 'PRAUTOBLOGGER_DEFAULT_IMAGE_PROVIDER', 'openrouter' );
|
||||||
define( 'PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL', 'google/gemini-2.5-flash-image' );
|
define( 'PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL', 'google/gemini-2.5-flash-image' );
|
||||||
|
|
||||||
|
|||||||
@@ -22,61 +22,47 @@ class OpenRouterImagePricingTest extends BaseTestCase {
|
|||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Default model constant is FLUX.2 Pro. */
|
/** Empty hint resolves to the PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL constant. */
|
||||||
public function test_default_model_constant(): void {
|
|
||||||
$this->assertSame(
|
|
||||||
'black-forest-labs/flux-2-pro',
|
|
||||||
\PRAutoBlogger_OpenRouter_Image_Pricing::DEFAULT_MODEL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Empty hint resolves to default model. */
|
|
||||||
public function test_resolve_model_empty_hint_uses_default(): void {
|
public function test_resolve_model_empty_hint_uses_default(): void {
|
||||||
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
||||||
$model = $pricing->resolve_model( '' );
|
$model = $pricing->resolve_model( '' );
|
||||||
$this->assertSame( 'black-forest-labs/flux-2-pro', $model );
|
$this->assertSame( PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL, $model );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Valid hint resolves to itself. */
|
/** Explicit hint resolves to itself, regardless of what default is set. */
|
||||||
public function test_resolve_model_valid_hint(): void {
|
public function test_resolve_model_valid_hint(): void {
|
||||||
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
||||||
$model = $pricing->resolve_model( 'bytedance/seedream-3.0' );
|
$model = $pricing->resolve_model( 'openai/gpt-5-image' );
|
||||||
$this->assertSame( 'bytedance/seedream-3.0', $model );
|
$this->assertSame( 'openai/gpt-5-image', $model );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Unknown model falls back to default. */
|
/** Gemini Flash Image costs $0.005/image. */
|
||||||
public function test_resolve_model_unknown_falls_back_to_default(): void {
|
public function test_estimate_cost_gemini_flash(): void {
|
||||||
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
||||||
$model = $pricing->resolve_model( 'nonexistent/model-999' );
|
$cost = $pricing->estimate_cost( 1200, 632, 'google/gemini-2.5-flash-image' );
|
||||||
$this->assertSame( 'black-forest-labs/flux-2-pro', $model );
|
$this->assertEqualsWithDelta( 0.005, $cost, 0.000001 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** FLUX.2 Pro costs $0.03/image. */
|
/** GPT-5 Image costs $0.08/image. */
|
||||||
public function test_estimate_cost_flux2_pro(): void {
|
public function test_estimate_cost_gpt5_image(): void {
|
||||||
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
||||||
$cost = $pricing->estimate_cost( 1200, 632, 'black-forest-labs/flux-2-pro' );
|
$cost = $pricing->estimate_cost( 1200, 632, 'openai/gpt-5-image' );
|
||||||
$this->assertEqualsWithDelta( 0.03, $cost, 0.000001 );
|
$this->assertEqualsWithDelta( 0.08, $cost, 0.000001 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Seedream costs $0.003/image. */
|
/** Unknown model falls back to conservative $0.05 estimate. */
|
||||||
public function test_estimate_cost_seedream(): void {
|
|
||||||
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
|
||||||
$cost = $pricing->estimate_cost( 1200, 632, 'bytedance/seedream-3.0' );
|
|
||||||
$this->assertEqualsWithDelta( 0.003, $cost, 0.000001 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Unknown model in estimate_cost falls back to default pricing. */
|
|
||||||
public function test_estimate_cost_unknown_model(): void {
|
public function test_estimate_cost_unknown_model(): void {
|
||||||
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
$pricing = new \PRAutoBlogger_OpenRouter_Image_Pricing();
|
||||||
$cost = $pricing->estimate_cost( 1200, 632, 'unknown/model' );
|
$cost = $pricing->estimate_cost( 1200, 632, 'unknown/model' );
|
||||||
$this->assertEqualsWithDelta( 0.03, $cost, 0.000001 );
|
$this->assertEqualsWithDelta( 0.05, $cost, 0.000001 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/** get_model_costs returns a non-empty map. */
|
/** get_model_costs returns a non-empty map with known models. */
|
||||||
public function test_get_model_costs(): void {
|
public function test_get_model_costs(): void {
|
||||||
$costs = \PRAutoBlogger_OpenRouter_Image_Pricing::get_model_costs();
|
$costs = \PRAutoBlogger_OpenRouter_Image_Pricing::get_model_costs();
|
||||||
$this->assertIsArray( $costs );
|
$this->assertIsArray( $costs );
|
||||||
$this->assertArrayHasKey( 'black-forest-labs/flux-2-pro', $costs );
|
$this->assertArrayHasKey( 'google/gemini-2.5-flash-image', $costs );
|
||||||
|
$this->assertArrayHasKey( 'openai/gpt-5-image', $costs );
|
||||||
$this->assertGreaterThan( 0, count( $costs ) );
|
$this->assertGreaterThan( 0, count( $costs ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user