extract_first_paragraph( $content ); $concepts = $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 ); } /** * Build a visual prompt from source Reddit thread data. * * Tries LLM rewriting first; falls back to rule-based synthesis on failure. * Appends the style suffix from plugin options. * * @param array{ * title?: string, * selftext?: string, * comments?: string[], * } $source_data Reddit source data with title and comments. * * @return string Concise prompt with style suffix appended. */ public function build_source_prompt( array $source_data ): string { $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 ); $style_suffix = get_option( 'prautoblogger_image_style_suffix', PRAUTOBLOGGER_DEFAULT_IMAGE_STYLE_SUFFIX ); return trim( $concepts . ' ' . $style_suffix ); } /** * Use a cheap LLM to distill title + context into a visual scene. * * Falls back to rule-based synthesis if the LLM call fails for any * reason (network, auth, timeout, unexpected response shape). * * Side effects: one OpenRouter API call; one cost-tracker log entry. * * @param string $title Article or thread title. * @param string $context First paragraph or top comment. * @return string Visual scene description (without style suffix). */ private function rewrite_via_llm( string $title, string $context ): string { $title = trim( sanitize_text_field( $title ) ); $context = trim( sanitize_text_field( $context ) ); // Truncate context to keep prompt tokens low. if ( strlen( $context ) > 300 ) { $context = substr( $context, 0, 300 ) . '...'; } $user_message = "Article title: {$title}"; if ( '' !== $context ) { $user_message .= "\n\nSummary: {$context}"; } try { $llm = $this->get_llm_provider(); $model = get_option( 'prautoblogger_analysis_model', PRAUTOBLOGGER_DEFAULT_ANALYSIS_MODEL ); $result = $llm->send_chat_completion( [ [ 'role' => 'system', 'content' => self::REWRITER_SYSTEM_PROMPT ], [ 'role' => 'user', 'content' => $user_message ], ], $model, [ 'temperature' => 0.7, 'max_tokens' => self::REWRITER_MAX_TOKENS, ] ); $scene = trim( $result['content'] ?? '' ); // Log the rewrite cost so it shows in the analytics dashboard. ( new PRAutoBlogger_Cost_Tracker() )->log_api_call( null, 'image_prompt_rewrite', 'openrouter', $model, $result['prompt_tokens'] ?? 0, $result['completion_tokens'] ?? 0 ); $cost = $llm->estimate_cost( $model, $result['prompt_tokens'] ?? 0, $result['completion_tokens'] ?? 0 ); PRAutoBlogger_Logger::instance()->debug( sprintf( 'Image prompt rewritten (%d→%d chars, $%.6f): %s', strlen( $user_message ), strlen( $scene ), $cost, substr( $scene, 0, 120 ) ), 'image_prompt_builder' ); if ( '' !== $scene ) { return $scene; } } catch ( \Exception $e ) { // LLM failure is not fatal — fall back to rule-based synthesis. PRAutoBlogger_Logger::instance()->warning( 'Image prompt LLM rewrite failed, using fallback: ' . $e->getMessage(), 'image_prompt_builder' ); } return $this->synthesize_visual_concepts_fallback( $title, $context ); } /** * Extract the first paragraph from HTML content. * * Removes all tags and captures the text up to the first double-newline * or 200 characters, whichever comes first. * * @param string $html HTML content. * @return string Plain text of the first paragraph. */ private function extract_first_paragraph( string $html ): string { $text = wp_strip_all_tags( $html ); $paras = preg_split( '/\n\n+/', $text ); $para = isset( $paras[0] ) ? trim( $paras[0] ) : ''; if ( strlen( $para ) > 200 ) { $para = substr( $para, 0, 200 ) . '...'; } return $para; } /** * Rule-based fallback when LLM rewriting is unavailable. * * Kept deliberately simple — this only runs if OpenRouter is down. * * @param string $title Main heading / topic. * @param string $supporting_text Additional context. * @return string Synthesized visual prompt concept. */ private function synthesize_visual_concepts_fallback( string $title, string $supporting_text ): string { $prompt = "A visual representation of: {$title}"; if ( '' !== $supporting_text ) { $snippet = strlen( $supporting_text ) > 150 ? substr( $supporting_text, 0, 150 ) . '...' : $supporting_text; $prompt .= ". Context: {$snippet}"; } return trim( $prompt ); } /** * Lazy-load the OpenRouter provider. * * @return PRAutoBlogger_OpenRouter_Provider */ private function get_llm_provider(): PRAutoBlogger_OpenRouter_Provider { if ( null === $this->llm ) { $this->llm = new PRAutoBlogger_OpenRouter_Provider(); } return $this->llm; } }