Separate comic captions from generated images
Captions were being baked into the image by the FLUX model, causing
text to get cut off at the edges. Now the LLM generates scene and
caption separately — the scene drives image gen (with explicit
"no text in the image" instruction), and the caption is inserted
as a styled <figure>/<figcaption> HTML block at the top of the post.
Changes:
- rewrite_via_llm() returns array{scene, caption} instead of string
- build_article_prompt() and build_source_prompt() return {prompt, caption}
- Style suffix v4: removes "caption text" instruction, adds "no text" directive
- Image pipeline destructures prompt/caption, stores caption in attachment meta
- prepend_caption_to_post() inserts figure+figcaption HTML into post content
- Deduplicated generate_image_a/b into shared generate_single_image()
- One-time v4 migration auto-updates existing style suffix on deploy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -159,6 +159,17 @@ class PRAutoBlogger {
|
||||
update_option( 'prautoblogger_migrated_style_suffix_v3', '1' );
|
||||
}
|
||||
|
||||
// One-time migration (v4): remove caption-in-image instruction from style suffix.
|
||||
// Captions are now inserted as HTML below the image, not baked into the image.
|
||||
if ( ! get_option( 'prautoblogger_migrated_style_suffix_v4' ) ) {
|
||||
$current = get_option( 'prautoblogger_image_style_suffix', '' );
|
||||
// Detect v3 style suffix (contains "caption text" instruction).
|
||||
if ( false !== strpos( $current, 'caption text in a clean sans-serif font below the panel' ) ) {
|
||||
update_option( 'prautoblogger_image_style_suffix', PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX );
|
||||
}
|
||||
update_option( 'prautoblogger_migrated_style_suffix_v4', '1' );
|
||||
}
|
||||
|
||||
// One-time migration: re-wrap existing encrypted values with "enc:" prefix.
|
||||
if ( ! get_option( 'prautoblogger_migrated_enc_prefix' ) ) {
|
||||
$enc_options = [ 'prautoblogger_openrouter_api_key', 'prautoblogger_ga4_credentials_json' ];
|
||||
|
||||
@@ -4,23 +4,15 @@ declare(strict_types=1);
|
||||
/**
|
||||
* Orchestrates image generation for published articles.
|
||||
*
|
||||
* Generates two images per article (A/B experiment):
|
||||
* - Image A: Article-driven prompt → featured image
|
||||
* - Image B: Source-driven prompt → post meta (_prautoblogger_image_b_id)
|
||||
*
|
||||
* Each image is independently fallible — if A fails, still try B. If both fail,
|
||||
* the article is published without images (graceful degradation).
|
||||
* Generates two images per article (A/B): Image A (article-driven) as featured
|
||||
* image, Image B (source-driven) in post meta. Each is independently fallible.
|
||||
*
|
||||
* Triggered by: PRAutoBlogger_Pipeline_Runner after publisher creates the post.
|
||||
* Dependencies: PRAutoBlogger_Image_Provider_Interface (concrete provider),
|
||||
* PRAutoBlogger_Image_Prompt_Builder, PRAutoBlogger_Image_Media_Sideloader,
|
||||
* PRAutoBlogger_Cost_Tracker, PRAutoBlogger_Logger.
|
||||
* Dependencies: Image provider, Prompt builder, Sideloader, Cost tracker, Logger.
|
||||
*
|
||||
* @see core/class-pipeline-runner.php — Calls this after publisher.
|
||||
* @see core/class-publisher.php — Sets featured image + post meta.
|
||||
* @see core/class-image-prompt-builder.php — Prompt generation.
|
||||
* @see core/class-image-prompt-builder.php — Prompt generation (scene + caption).
|
||||
* @see core/class-image-media-sideloader.php — Media library integration.
|
||||
* @see ARCHITECTURE.md — Image pipeline data flow.
|
||||
*/
|
||||
class PRAutoBlogger_Image_Pipeline {
|
||||
|
||||
@@ -32,32 +24,16 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
private const DEFAULT_WIDTH = 1200;
|
||||
private const DEFAULT_HEIGHT = 632;
|
||||
|
||||
/**
|
||||
* The image provider (FLUX.1 via Cloudflare or another).
|
||||
*
|
||||
* @var PRAutoBlogger_Image_Provider_Interface
|
||||
*/
|
||||
/** @var PRAutoBlogger_Image_Provider_Interface Image gen provider (FLUX.1 etc). */
|
||||
private PRAutoBlogger_Image_Provider_Interface $provider;
|
||||
|
||||
/**
|
||||
* Prompt builder.
|
||||
*
|
||||
* @var PRAutoBlogger_Image_Prompt_Builder
|
||||
*/
|
||||
/** @var PRAutoBlogger_Image_Prompt_Builder Builds scene + caption from article data. */
|
||||
private PRAutoBlogger_Image_Prompt_Builder $prompt_builder;
|
||||
|
||||
/**
|
||||
* Media library sideloader.
|
||||
*
|
||||
* @var PRAutoBlogger_Image_Media_Sideloader
|
||||
*/
|
||||
/** @var PRAutoBlogger_Image_Media_Sideloader Downloads images into WP media library. */
|
||||
private PRAutoBlogger_Image_Media_Sideloader $sideloader;
|
||||
|
||||
/**
|
||||
* Cost tracker for logging spend.
|
||||
*
|
||||
* @var PRAutoBlogger_Cost_Tracker
|
||||
*/
|
||||
/** @var PRAutoBlogger_Cost_Tracker Logs image generation spend. */
|
||||
private PRAutoBlogger_Cost_Tracker $cost_tracker;
|
||||
|
||||
/**
|
||||
@@ -134,8 +110,11 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Build the article-driven prompt (returns scene + caption).
|
||||
$article_prompt = $this->prompt_builder->build_article_prompt( $article_data );
|
||||
|
||||
// Generate Image A (article-driven prompt).
|
||||
$image_a_result = $this->generate_image_a( $post_id, $article_data );
|
||||
$image_a_result = $this->generate_image_a( $post_id, $article_prompt['prompt'] );
|
||||
if ( ! is_wp_error( $image_a_result ) && isset( $image_a_result['attachment_id'] ) ) {
|
||||
$result['image_a_id'] = $image_a_result['attachment_id'];
|
||||
|
||||
@@ -143,6 +122,13 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
// process times out before Image B finishes or before the
|
||||
// caller gets to handle the return value.
|
||||
set_post_thumbnail( $post_id, $image_a_result['attachment_id'] );
|
||||
|
||||
// Store the caption as attachment meta for theme/display use.
|
||||
if ( '' !== $article_prompt['caption'] ) {
|
||||
update_post_meta( $image_a_result['attachment_id'], '_prautoblogger_image_caption', $article_prompt['caption'] );
|
||||
$this->prepend_caption_to_post( $post_id, $image_a_result['attachment_id'], $article_prompt['caption'] );
|
||||
}
|
||||
|
||||
PRAutoBlogger_Logger::instance()->info(
|
||||
sprintf( 'Set featured image (attachment %d) for post %d', $image_a_result['attachment_id'], $post_id ),
|
||||
'image_pipeline'
|
||||
@@ -158,13 +144,20 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
|
||||
// Generate Image B (source-driven prompt) if source data is available.
|
||||
if ( null !== $source_data && ! empty( $source_data ) ) {
|
||||
$image_b_result = $this->generate_image_b( $post_id, $source_data );
|
||||
$source_prompt = $this->prompt_builder->build_source_prompt( $source_data );
|
||||
$image_b_result = $this->generate_image_b( $post_id, $source_prompt['prompt'] );
|
||||
if ( ! is_wp_error( $image_b_result ) && isset( $image_b_result['attachment_id'] ) ) {
|
||||
$result['image_b_id'] = $image_b_result['attachment_id'];
|
||||
|
||||
// Store Image B reference immediately for the same
|
||||
// timeout-resilience reason as Image A above.
|
||||
update_post_meta( $post_id, '_prautoblogger_image_b_id', $image_b_result['attachment_id'] );
|
||||
|
||||
// Store the caption as attachment meta.
|
||||
if ( '' !== $source_prompt['caption'] ) {
|
||||
update_post_meta( $image_b_result['attachment_id'], '_prautoblogger_image_caption', $source_prompt['caption'] );
|
||||
}
|
||||
|
||||
PRAutoBlogger_Logger::instance()->info(
|
||||
sprintf( 'Stored Image B (attachment %d) for post %d', $image_b_result['attachment_id'], $post_id ),
|
||||
'image_pipeline'
|
||||
@@ -187,55 +180,56 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Image A from article content.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $article_data Article data.
|
||||
*
|
||||
* @return array{
|
||||
* attachment_id?: int,
|
||||
* cost_usd: float,
|
||||
* }|\WP_Error
|
||||
*/
|
||||
private function generate_image_a( int $post_id, array $article_data ) {
|
||||
try {
|
||||
// Build the article-driven prompt.
|
||||
$prompt = $this->prompt_builder->build_article_prompt( $article_data );
|
||||
/** @see generate_single_image() — Image A wrapper. */
|
||||
private function generate_image_a( int $post_id, string $prompt ) {
|
||||
return $this->generate_single_image( $post_id, $prompt, 'image_a', 'Image A' );
|
||||
}
|
||||
|
||||
// Estimate cost before generating.
|
||||
/** @see generate_single_image() — Image B wrapper. */
|
||||
private function generate_image_b( int $post_id, string $prompt ) {
|
||||
return $this->generate_single_image( $post_id, $prompt, 'image_b', 'Image B' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single image: budget check → provider call → sideload → log.
|
||||
*
|
||||
* Shared implementation for Image A and Image B to avoid duplication.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param string $prompt Image generation prompt.
|
||||
* @param string $slot Cost-tracking slot ('image_a' or 'image_b').
|
||||
* @param string $label Human-readable label for log messages.
|
||||
*
|
||||
* @return array{attachment_id?: int, cost_usd: float}|\WP_Error
|
||||
*/
|
||||
private function generate_single_image( int $post_id, string $prompt, string $slot, string $label ) {
|
||||
try {
|
||||
$estimated_cost = $this->provider->estimate_cost( self::DEFAULT_WIDTH, self::DEFAULT_HEIGHT );
|
||||
if ( $this->cost_tracker->would_exceed_budget( $estimated_cost ) ) {
|
||||
return new \WP_Error(
|
||||
'budget_exceeded',
|
||||
'Image generation would exceed monthly budget.'
|
||||
);
|
||||
return new \WP_Error( 'budget_exceeded', 'Image generation would exceed monthly budget.' );
|
||||
}
|
||||
|
||||
// Generate the image.
|
||||
$image_data = $this->provider->generate_image( $prompt, self::DEFAULT_WIDTH, self::DEFAULT_HEIGHT );
|
||||
|
||||
// Sideload the image into media library.
|
||||
$attachment_id = $this->sideloader->sideload_image(
|
||||
$image_data,
|
||||
$post_id,
|
||||
substr( $prompt, 0, 100 ) // Use first 100 chars of prompt as alt text.
|
||||
substr( $prompt, 0, 100 )
|
||||
);
|
||||
|
||||
if ( is_wp_error( $attachment_id ) ) {
|
||||
return $attachment_id;
|
||||
}
|
||||
|
||||
// Log the cost.
|
||||
$this->cost_tracker->log_image_generation(
|
||||
$image_data['cost_usd'],
|
||||
$image_data['model'] ?? 'unknown',
|
||||
$post_id,
|
||||
'image_a'
|
||||
$slot
|
||||
);
|
||||
|
||||
PRAutoBlogger_Logger::instance()->info(
|
||||
sprintf( 'Image A generated for post %d (attachment %d, cost $%.4f)', $post_id, $attachment_id, $image_data['cost_usd'] ),
|
||||
sprintf( '%s generated for post %d (attachment %d, cost $%.4f)', $label, $post_id, $attachment_id, $image_data['cost_usd'] ),
|
||||
'image_pipeline'
|
||||
);
|
||||
|
||||
@@ -245,7 +239,7 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
];
|
||||
} catch ( \Throwable $e ) {
|
||||
PRAutoBlogger_Logger::instance()->error(
|
||||
sprintf( 'Image A %s: %s', get_class( $e ), $e->getMessage() ),
|
||||
sprintf( '%s %s: %s', $label, get_class( $e ), $e->getMessage() ),
|
||||
'image_pipeline'
|
||||
);
|
||||
|
||||
@@ -254,68 +248,39 @@ class PRAutoBlogger_Image_Pipeline {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Image B from source data.
|
||||
* Prepend a comic caption as a styled figcaption block to post content.
|
||||
*
|
||||
* @param int $post_id Post ID.
|
||||
* @param array $source_data Source Reddit data.
|
||||
* Inserts a <figure> block at the top of the post containing the featured
|
||||
* image and a <figcaption> with the comic punchline. Uses inline styles
|
||||
* for portability across themes.
|
||||
*
|
||||
* @return array{
|
||||
* attachment_id?: int,
|
||||
* cost_usd: float,
|
||||
* }|\WP_Error
|
||||
* @param int $post_id Post ID.
|
||||
* @param int $attachment_id Featured image attachment ID.
|
||||
* @param string $caption Caption text (the comic punchline).
|
||||
*/
|
||||
private function generate_image_b( int $post_id, array $source_data ) {
|
||||
try {
|
||||
// Build the source-driven prompt.
|
||||
$prompt = $this->prompt_builder->build_source_prompt( $source_data );
|
||||
|
||||
// Estimate cost before generating.
|
||||
$estimated_cost = $this->provider->estimate_cost( self::DEFAULT_WIDTH, self::DEFAULT_HEIGHT );
|
||||
if ( $this->cost_tracker->would_exceed_budget( $estimated_cost ) ) {
|
||||
return new \WP_Error(
|
||||
'budget_exceeded',
|
||||
'Image generation would exceed monthly budget.'
|
||||
);
|
||||
}
|
||||
|
||||
// Generate the image.
|
||||
$image_data = $this->provider->generate_image( $prompt, self::DEFAULT_WIDTH, self::DEFAULT_HEIGHT );
|
||||
|
||||
// Sideload the image into media library.
|
||||
$attachment_id = $this->sideloader->sideload_image(
|
||||
$image_data,
|
||||
$post_id,
|
||||
substr( $prompt, 0, 100 ) // Use first 100 chars of prompt as alt text.
|
||||
);
|
||||
|
||||
if ( is_wp_error( $attachment_id ) ) {
|
||||
return $attachment_id;
|
||||
}
|
||||
|
||||
// Log the cost.
|
||||
$this->cost_tracker->log_image_generation(
|
||||
$image_data['cost_usd'],
|
||||
$image_data['model'] ?? 'unknown',
|
||||
$post_id,
|
||||
'image_b'
|
||||
);
|
||||
|
||||
PRAutoBlogger_Logger::instance()->info(
|
||||
sprintf( 'Image B generated for post %d (attachment %d, cost $%.4f)', $post_id, $attachment_id, $image_data['cost_usd'] ),
|
||||
'image_pipeline'
|
||||
);
|
||||
|
||||
return [
|
||||
'attachment_id' => $attachment_id,
|
||||
'cost_usd' => $image_data['cost_usd'],
|
||||
];
|
||||
} catch ( \Throwable $e ) {
|
||||
PRAutoBlogger_Logger::instance()->error(
|
||||
sprintf( 'Image B %s: %s', get_class( $e ), $e->getMessage() ),
|
||||
'image_pipeline'
|
||||
);
|
||||
|
||||
return new \WP_Error( 'image_generation_failed', $e->getMessage() );
|
||||
private function prepend_caption_to_post( int $post_id, int $attachment_id, string $caption ): void {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$img_url = wp_get_attachment_url( $attachment_id );
|
||||
$alt = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true );
|
||||
|
||||
// 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 )
|
||||
);
|
||||
|
||||
wp_update_post( [
|
||||
'ID' => $post_id,
|
||||
'post_content' => $figure . "\n\n" . $post->post_content,
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,9 @@ PROMPT;
|
||||
* Build a visual prompt from finished article content.
|
||||
*
|
||||
* Tries LLM rewriting first; falls back to rule-based synthesis on failure.
|
||||
* Appends the style suffix from plugin options.
|
||||
* Separates the scene description (for image gen) from the caption text
|
||||
* (for insertion below the image as HTML). Appends style suffix to the
|
||||
* scene-only prompt so no text is rendered inside the image.
|
||||
*
|
||||
* @param array{
|
||||
* post_title?: string,
|
||||
@@ -75,18 +77,21 @@ PROMPT;
|
||||
* suggested_title?: string,
|
||||
* } $article_data Article data with title and HTML content.
|
||||
*
|
||||
* @return string Concise prompt with style suffix appended.
|
||||
* @return array{prompt: string, caption: string} Image prompt (scene + style) and caption text.
|
||||
*/
|
||||
public function build_article_prompt( array $article_data ): string {
|
||||
public function build_article_prompt( array $article_data ): array {
|
||||
$title = $article_data['post_title'] ?? $article_data['suggested_title'] ?? 'Product';
|
||||
$content = $article_data['post_content'] ?? '';
|
||||
$first_para = $this->extract_first_paragraph( $content );
|
||||
|
||||
$concepts = $this->rewrite_via_llm( $title, $first_para );
|
||||
$parsed = $this->rewrite_via_llm( $title, $first_para );
|
||||
|
||||
$style_suffix = get_option( 'prautoblogger_image_style_suffix', PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX );
|
||||
|
||||
return trim( $concepts . ' ' . $style_suffix );
|
||||
return [
|
||||
'prompt' => trim( $parsed['scene'] . ' ' . $style_suffix ),
|
||||
'caption' => $parsed['caption'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,22 +106,29 @@ PROMPT;
|
||||
* comments?: string[],
|
||||
* } $source_data Reddit source data with title and comments.
|
||||
*
|
||||
* @return string Concise prompt with style suffix appended.
|
||||
* @return array{prompt: string, caption: string} Image prompt (scene + style) and caption text.
|
||||
*/
|
||||
public function build_source_prompt( array $source_data ): string {
|
||||
public function build_source_prompt( array $source_data ): array {
|
||||
$title = $source_data['title'] ?? 'Reddit Discussion';
|
||||
$comments = $source_data['comments'] ?? [];
|
||||
$context = is_array( $comments ) && ! empty( $comments ) ? $comments[0] : '';
|
||||
|
||||
$concepts = $this->rewrite_via_llm( $title, $context );
|
||||
$parsed = $this->rewrite_via_llm( $title, $context );
|
||||
|
||||
$style_suffix = get_option( 'prautoblogger_image_style_suffix', PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX );
|
||||
|
||||
return trim( $concepts . ' ' . $style_suffix );
|
||||
return [
|
||||
'prompt' => trim( $parsed['scene'] . ' ' . $style_suffix ),
|
||||
'caption' => $parsed['caption'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Use a cheap LLM to distill title + context into a visual scene.
|
||||
* Use a cheap LLM to distill title + context into a visual scene + caption.
|
||||
*
|
||||
* The LLM response contains a SCENE line and a CAPTION line separated by a
|
||||
* blank line. This method parses them apart so the scene drives image gen
|
||||
* (no text baked in) and the caption is inserted as HTML below the image.
|
||||
*
|
||||
* Falls back to rule-based synthesis if the LLM call fails for any
|
||||
* reason (network, auth, timeout, unexpected response shape).
|
||||
@@ -125,9 +137,9 @@ PROMPT;
|
||||
*
|
||||
* @param string $title Article or thread title.
|
||||
* @param string $context First paragraph or top comment.
|
||||
* @return string Visual scene description (without style suffix).
|
||||
* @return array{scene: string, caption: string} Scene for image gen, caption for HTML.
|
||||
*/
|
||||
private function rewrite_via_llm( string $title, string $context ): string {
|
||||
private function rewrite_via_llm( string $title, string $context ): array {
|
||||
$title = trim( sanitize_text_field( $title ) );
|
||||
$context = trim( sanitize_text_field( $context ) );
|
||||
|
||||
@@ -157,7 +169,7 @@ PROMPT;
|
||||
]
|
||||
);
|
||||
|
||||
$scene = trim( $result['content'] ?? '' );
|
||||
$raw = trim( $result['content'] ?? '' );
|
||||
|
||||
// Log the rewrite cost so it shows in the analytics dashboard.
|
||||
( new PRAutoBlogger_Cost_Tracker() )->log_api_call(
|
||||
@@ -176,12 +188,12 @@ PROMPT;
|
||||
);
|
||||
|
||||
PRAutoBlogger_Logger::instance()->debug(
|
||||
sprintf( 'Image prompt rewritten (%d→%d chars, $%.6f): %s', strlen( $user_message ), strlen( $scene ), $cost, substr( $scene, 0, 120 ) ),
|
||||
sprintf( 'Image prompt rewritten (%d→%d chars, $%.6f): %s', strlen( $user_message ), strlen( $raw ), $cost, substr( $raw, 0, 120 ) ),
|
||||
'image_prompt_builder'
|
||||
);
|
||||
|
||||
if ( '' !== $scene ) {
|
||||
return $scene;
|
||||
if ( '' !== $raw ) {
|
||||
return $this->parse_scene_and_caption( $raw );
|
||||
}
|
||||
} catch ( \Throwable $e ) {
|
||||
// LLM failure is not fatal — fall back to rule-based synthesis.
|
||||
@@ -194,6 +206,36 @@ PROMPT;
|
||||
return $this->synthesize_visual_concepts_fallback( $title, $context );
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the LLM response into separate scene and caption parts.
|
||||
*
|
||||
* Expected format from the LLM:
|
||||
* Scene description text here.
|
||||
*
|
||||
* "Caption punchline here."
|
||||
*
|
||||
* Falls back gracefully: if no blank-line separator is found, the entire
|
||||
* response becomes the scene with an empty caption.
|
||||
*
|
||||
* @param string $raw Raw LLM response text.
|
||||
* @return array{scene: string, caption: string}
|
||||
*/
|
||||
private function parse_scene_and_caption( string $raw ): array {
|
||||
// Split on blank line (the format the system prompt requests).
|
||||
$parts = preg_split( '/\n\s*\n/', $raw, 2 );
|
||||
|
||||
if ( count( $parts ) >= 2 ) {
|
||||
$scene = trim( $parts[0] );
|
||||
$caption = trim( $parts[1] );
|
||||
// Strip surrounding quotes from caption — the LLM wraps it in quotes.
|
||||
$caption = trim( $caption, '"\'' );
|
||||
return [ 'scene' => $scene, 'caption' => $caption ];
|
||||
}
|
||||
|
||||
// No blank-line separator found — treat entire response as scene.
|
||||
return [ 'scene' => trim( $raw ), 'caption' => '' ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the first paragraph from HTML content.
|
||||
*
|
||||
@@ -222,16 +264,19 @@ PROMPT;
|
||||
*
|
||||
* @param string $title Main heading / topic.
|
||||
* @param string $supporting_text Additional context.
|
||||
* @return string Synthesized visual prompt concept.
|
||||
* @return array{scene: string, caption: string} Scene for image gen, caption for HTML.
|
||||
*/
|
||||
private function synthesize_visual_concepts_fallback( string $title, string $supporting_text ): string {
|
||||
private function synthesize_visual_concepts_fallback( string $title, string $supporting_text ): array {
|
||||
// Fallback produces a simple comic concept when the LLM is unavailable.
|
||||
$prompt = sprintf(
|
||||
'A cartoon scientist in a lab coat looking bewildered while examining something related to: %s. Caption below reads: "Science is full of surprises."',
|
||||
$scene = sprintf(
|
||||
'A cartoon scientist in a lab coat looking bewildered while examining something related to: %s.',
|
||||
$title
|
||||
);
|
||||
|
||||
return trim( $prompt );
|
||||
return [
|
||||
'scene' => trim( $scene ),
|
||||
'caption' => 'Science is full of surprises.',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -60,11 +60,12 @@ define( 'PRAUTOBLOGGER_DEFAULT_IMAGE_PROVIDER', 'openrouter' );
|
||||
define( 'PRAUTOBLOGGER_DEFAULT_IMAGE_MODEL', 'google/gemini-2.5-flash-image' );
|
||||
|
||||
// Default style suffix appended to every image-gen prompt. Updated
|
||||
// 2026-04-18: single-panel newspaper comic style. Users may override
|
||||
// via Settings → Images → Style Suffix.
|
||||
// 2026-04-18 v4: single-panel newspaper comic style. Caption is now
|
||||
// inserted as HTML text below the image, NOT rendered inside it.
|
||||
// Users may override via Settings → Images → Style Suffix.
|
||||
define(
|
||||
'PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX',
|
||||
'Style: a single-panel newspaper comic in classic black ink on white paper, simple clean linework like The Far Side or B.C., slightly yellowed newsprint texture, hand-drawn crosshatching for shading, bold panel border, caption text in a clean sans-serif font below the panel, warm and humorous tone, visible in a newspaper comics section.'
|
||||
'Style: a single-panel newspaper comic in classic black ink on white paper, simple clean linework like The Far Side or B.C., slightly yellowed newsprint texture, hand-drawn crosshatching for shading, bold panel border, warm and humorous tone, visible in a newspaper comics section. IMPORTANT: Do not render any text, captions, speech bubbles, or words inside the image.'
|
||||
);
|
||||
|
||||
/*
|
||||
|
||||
@@ -35,8 +35,16 @@ class ImagePipelineTest extends BaseTestCase {
|
||||
return trim( strip_tags( $str ) );
|
||||
} );
|
||||
Functions\when( 'update_post_meta' )->justReturn( true );
|
||||
Functions\when( 'set_post_thumbnail' )->justReturn( true );
|
||||
Functions\when( 'get_post_meta' )->justReturn( '' );
|
||||
Functions\when( 'get_post' )->justReturn( (object) [ 'ID' => 1, 'post_content' => '<p>Test content</p>' ] );
|
||||
Functions\when( 'wp_update_post' )->justReturn( 1 );
|
||||
Functions\when( 'wp_get_attachment_url' )->justReturn( 'https://example.com/img.png' );
|
||||
Functions\when( 'sanitize_title' )->returnArg();
|
||||
Functions\when( 'esc_html__' )->returnArg();
|
||||
Functions\when( 'esc_url' )->returnArg();
|
||||
Functions\when( 'esc_attr' )->returnArg();
|
||||
Functions\when( 'esc_html' )->returnArg();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user