Changeset 3490444
- Timestamp:
- 03/25/2026 01:31:10 AM (3 days ago)
- Location:
- clipcloud-image-generation
- Files:
-
- 33 added
- 6 edited
-
tags/1.2.0 (added)
-
tags/1.2.0/assets (added)
-
tags/1.2.0/assets/css (added)
-
tags/1.2.0/assets/css/admin.css (added)
-
tags/1.2.0/assets/css/frontend.css (added)
-
tags/1.2.0/assets/css/list.css (added)
-
tags/1.2.0/assets/js (added)
-
tags/1.2.0/assets/js/admin.js (added)
-
tags/1.2.0/assets/js/notice.js (added)
-
tags/1.2.0/assets/js/status.js (added)
-
tags/1.2.0/clipcloud-image-generation.php (added)
-
tags/1.2.0/languages (added)
-
tags/1.2.0/languages/clipcloud-image-generation-da_DK.po (added)
-
tags/1.2.0/languages/clipcloud-image-generation-de_DE.po (added)
-
tags/1.2.0/languages/clipcloud-image-generation-es_ES.po (added)
-
tags/1.2.0/languages/clipcloud-image-generation-fr_FR.po (added)
-
tags/1.2.0/languages/clipcloud-image-generation-it_IT.po (added)
-
tags/1.2.0/languages/clipcloud-image-generation-ja.po (added)
-
tags/1.2.0/languages/clipcloud-image-generation-nl_NL.po (added)
-
tags/1.2.0/languages/clipcloud-image-generation-pl_PL.po (added)
-
tags/1.2.0/languages/clipcloud-image-generation-pt_BR.po (added)
-
tags/1.2.0/languages/clipcloud-image-generation-ru_RU.mo (added)
-
tags/1.2.0/languages/clipcloud-image-generation-ru_RU.po (added)
-
tags/1.2.0/languages/readme-da_DK.po (added)
-
tags/1.2.0/languages/readme-de_DE.po (added)
-
tags/1.2.0/languages/readme-es_ES.po (added)
-
tags/1.2.0/languages/readme-fr_FR.po (added)
-
tags/1.2.0/languages/readme-it_IT.po (added)
-
tags/1.2.0/languages/readme-ja.po (added)
-
tags/1.2.0/languages/readme-nl_NL.po (added)
-
tags/1.2.0/languages/readme-pl_PL.po (added)
-
tags/1.2.0/languages/readme-pt_BR.po (added)
-
tags/1.2.0/readme.txt (added)
-
trunk/assets/js/admin.js (modified) (1 diff)
-
trunk/assets/js/status.js (modified) (2 diffs)
-
trunk/clipcloud-image-generation.php (modified) (68 diffs)
-
trunk/languages/readme-da_DK.po (modified) (1 diff)
-
trunk/languages/readme-fr_FR.po (modified) (1 diff)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
clipcloud-image-generation/trunk/assets/js/admin.js
r3418668 r3490444 69 69 } 70 70 71 function setBalance( bal, free ) { 72 if ( ! balanceEl ) { 73 return; 74 } 75 if ( null === bal && null === free ) { 76 balanceEl.innerHTML = ''; 77 return; 78 } 79 const b = null !== bal && undefined !== bal ? bal : '—'; 80 const f = null !== free && undefined !== free ? free : '—'; 81 balanceEl.innerHTML = L.balance + ' <span class="value">' + b + '</span>🖼️ (+' + f + ' ' + L.freeToday + ')'; 82 } 83 84 function setFreeModeHint( bal ) { 85 if ( ! freeHintEl ) { 86 return; 87 } 88 if ( null !== bal && undefined !== bal && ! isNaN( bal ) && Number( bal ) < 20 ) { 89 freeHintEl.innerHTML = L.freeModeLabel + ' <span class="ccfi-tooltip"><span class="ccfi-helpicon">?</span><span class="ccfi-tooltip-box">' + ( L.freeModeTooltip || '' ).replace( /\n/g, '<br>' ) + '</span></span>'; 90 } else { 91 freeHintEl.innerHTML = ''; 92 } 93 } 71 function setBalance( bal, free ) { 72 if ( ! balanceEl ) { 73 return; 74 } 75 if ( null === bal && null === free ) { 76 balanceEl.textContent = ''; 77 return; 78 } 79 const b = null !== bal && undefined !== bal ? bal : '—'; 80 const f = null !== free && undefined !== free ? free : '—'; 81 82 while ( balanceEl.firstChild ) { 83 balanceEl.removeChild( balanceEl.firstChild ); 84 } 85 86 balanceEl.appendChild( document.createTextNode( ( L.balance || '' ) + ' ' ) ); 87 88 const valSpan = document.createElement( 'span' ); 89 valSpan.className = 'value'; 90 valSpan.textContent = String( b ); 91 balanceEl.appendChild( valSpan ); 92 93 balanceEl.appendChild( document.createTextNode( '🖼️ (+' + String( f ) + ' ' + ( L.freeToday || '' ) + ')' ) ); 94 } 95 96 function setFreeModeHint( bal ) { 97 if ( ! freeHintEl ) { 98 return; 99 } 100 while ( freeHintEl.firstChild ) { 101 freeHintEl.removeChild( freeHintEl.firstChild ); 102 } 103 if ( null !== bal && undefined !== bal && ! isNaN( bal ) && Number( bal ) < 20 ) { 104 freeHintEl.appendChild( document.createTextNode( String( L.freeModeLabel || '' ) + ' ' ) ); 105 106 const tooltip = document.createElement( 'span' ); 107 tooltip.className = 'ccfi-tooltip'; 108 109 const helpIcon = document.createElement( 'span' ); 110 helpIcon.className = 'ccfi-helpicon'; 111 helpIcon.textContent = '?'; 112 113 const tooltipBox = document.createElement( 'span' ); 114 tooltipBox.className = 'ccfi-tooltip-box'; 115 tooltipBox.textContent = String( L.freeModeTooltip || '' ); 116 117 tooltip.appendChild( helpIcon ); 118 tooltip.appendChild( tooltipBox ); 119 freeHintEl.appendChild( tooltip ); 120 } 121 } 94 122 95 123 function rebuildStyles( names, labelsMap ) { -
clipcloud-image-generation/trunk/assets/js/status.js
r3418668 r3490444 29 29 const pollActive = parseInt( vars.pollInterval, 10 ) || 15000; 30 30 const pollIdle = parseInt( vars.pollIntervalIdle, 10 ) || 60000; 31 const cronInterval = parseInt( vars.cronInterval, 10 ) || 30000;31 const cronInterval = parseInt( vars.cronInterval, 10 ) || 20000; 32 32 33 33 const labelsPost = vars.i18n && vars.i18n.post ? vars.i18n.post : {}; … … 257 257 if ( hasPendingDom() ) { 258 258 lastPending = true; 259 doPoll();260 }261 } );262 })();259 } 260 doPoll(); 261 } ); 262 })(); -
clipcloud-image-generation/trunk/clipcloud-image-generation.php
r3460288 r3490444 3 3 * Plugin Name: ClipCloud - Image Generation 4 4 * Description: Generates images using AI for your posts. 5 * Version: 1. 1.25 * Version: 1.2.0 6 6 * Author: ClipAI 7 7 * Text Domain: clipcloud-image-generation … … 36 36 // Polling. 37 37 const POLL_INTERVAL_SEC = 25; 38 const MAX_ATTEMPTS = 20; // 20 minutes total.38 const MAX_ATTEMPTS = 40; // Per-slot result polling attempts cap. 39 39 const HTTP_TIMEOUT_SEC = 25; 40 40 const MAX_SLOTS_PER_START = 2; // Hard cap on slots per single run. 41 41 const MAX_CREATION_ATTEMPTS = 3; 42 const RUNTIME_START_COST_SEC = 15; 43 const RUNTIME_START_COST_AUTO_ENHANCE_SEC = 30; 44 const RUNTIME_POLL_COST_SEC = 20; 42 45 43 46 // Meta / options. … … 55 58 const META_SLOT_CREATION_ATTEMPTS = '_ccfi_slot_creation_attempts'; // slot => attempts to start job. 56 59 const META_APPLY_PENDING = '_ccfi_apply_pending'; 60 const META_APPLY_ATTEMPTS = '_ccfi_apply_attempts'; 61 const META_SLOT_PROMPT_CONTEXT = '_ccfi_slot_prompt_context'; // slot => [prompt, enhanced]. 62 const MAX_APPLY_ATTEMPTS = 12; 57 63 58 64 // Cron. … … 60 66 const CRON_HOOK_START = 'clipcloud_fi_start_generation'; 61 67 const CRON_HOOK_WATCHDOG = 'clipcloud_fi_watchdog'; 68 const CRON_HOOK_VALIDATE_KEY = 'clipcloud_fi_validate_saved_key'; 69 const CRON_HOOK_REFRESH_ROUTE = 'clipcloud_fi_refresh_api_route'; 62 70 const WATCHDOG_BATCH_LIMIT = 20; 63 71 … … 91 99 add_action( self::CRON_HOOK_START, [ $this, 'cron_start_generation' ], 10, 2 ); 92 100 add_action( self::CRON_HOOK_WATCHDOG, [ $this, 'cron_watchdog' ] ); 101 add_action( self::CRON_HOOK_VALIDATE_KEY, [ $this, 'cron_validate_saved_key' ] ); 102 add_action( self::CRON_HOOK_REFRESH_ROUTE, [ $this, 'cron_refresh_api_route' ] ); 93 103 94 104 add_action( 'wp_ajax_' . self::AJAX_ACTION_VALIDATE, [ $this, 'ajax_validate_key' ] ); … … 117 127 wp_clear_scheduled_hook( self::CRON_HOOK_START ); 118 128 wp_clear_scheduled_hook( self::CRON_HOOK_WATCHDOG ); 129 wp_clear_scheduled_hook( self::CRON_HOOK_VALIDATE_KEY ); 130 wp_clear_scheduled_hook( self::CRON_HOOK_REFRESH_ROUTE ); 119 131 } 120 132 … … 148 160 self::META_SLOTS_HASH, 149 161 self::META_APPLY_PENDING, 150 ]; 162 self::META_APPLY_ATTEMPTS, 163 self::META_SLOT_PROMPT_CONTEXT, 164 ]; 151 165 152 166 foreach ( $meta_keys as $meta_key ) { … … 164 178 wp_clear_scheduled_hook( self::CRON_HOOK_START ); 165 179 wp_clear_scheduled_hook( self::CRON_HOOK_WATCHDOG ); 180 wp_clear_scheduled_hook( self::CRON_HOOK_VALIDATE_KEY ); 181 wp_clear_scheduled_hook( self::CRON_HOOK_REFRESH_ROUTE ); 166 182 } 167 183 … … 252 268 } 253 269 254 private function refresh_api_route_on_settings_save( $opts ) {255 return self::refresh_api_route( $opts );256 }257 258 270 private function get_api_server_status_message( $opts = null ) { 259 271 $o = is_array( $opts ) ? $opts : $this->get_opts(); … … 261 273 262 274 if ( 'backup' === $state ) { 263 return '🟡 Backup ClipCloud server is in use.';275 return __( '🟡 Backup ClipCloud server is in use.', 'clipcloud-image-generation' ); 264 276 } 265 277 if ( 'down' === $state ) { 266 return '⚠️ 🔴 ClipCloud servers are unavailable. Please contact your hosting provider and ask: "Why is there no access to clipcloud.clipai.pro?"';278 return __( '⚠️ 🔴 ClipCloud servers are unavailable. Please contact your hosting provider and ask: "Why is there no access to clipcloud.clipai.pro?"', 'clipcloud-image-generation' ); 267 279 } 268 280 269 281 // Also used before the first check on a fresh install. 270 return '🟢 Primary ClipCloud server is in use.'; 282 return __( '🟢 Primary ClipCloud server is in use.', 'clipcloud-image-generation' ); 283 } 284 285 private function schedule_api_key_validation() { 286 if ( ! wp_next_scheduled( self::CRON_HOOK_VALIDATE_KEY ) ) { 287 wp_schedule_single_event( time() + 1, self::CRON_HOOK_VALIDATE_KEY, [] ); 288 } 289 } 290 291 private function schedule_api_route_refresh() { 292 if ( ! wp_next_scheduled( self::CRON_HOOK_REFRESH_ROUTE ) ) { 293 wp_schedule_single_event( time() + 1, self::CRON_HOOK_REFRESH_ROUTE, [] ); 294 } 295 } 296 297 private function build_validated_key_opts( array $opts ) { 298 $out = $opts; 299 $api_key = isset( $out['api_key'] ) ? trim( (string) $out['api_key'] ) : ''; 300 301 $out['api_key_valid'] = false; 302 $out['styles_cache'] = []; 303 $out['styles_cache_ts'] = 0; 304 $out['styles_labels'] = []; 305 $out['styles_labels_ts'] = 0; 306 307 if ( '' === $api_key || false === strpos( $api_key, 'clip-' ) ) { 308 return $out; 309 } 310 311 $res = wp_remote_request( 312 $this->build_url( 313 self::EP_STYLES, 314 $this->get_effective_api_base( $out ) 315 ), 316 [ 317 'method' => 'GET', 318 'headers' => $this->http_headers( $api_key ), 319 'timeout' => 10, 320 ] 321 ); 322 323 $code = is_wp_error( $res ) ? 0 : (int) wp_remote_retrieve_response_code( $res ); 324 if ( is_wp_error( $res ) || 200 !== $code ) { 325 return $out; 326 } 327 328 $b = json_decode( wp_remote_retrieve_body( $res ), true ); 329 if ( ! is_array( $b ) || empty( $b['styles'] ) ) { 330 return $out; 331 } 332 333 $names = []; 334 foreach ( (array) $b['styles'] as $item ) { 335 if ( is_array( $item ) && isset( $item['name'] ) ) { 336 $names[] = (string) $item['name']; 337 } elseif ( is_string( $item ) ) { 338 $names[] = $item; 339 } 340 } 341 $names = array_values( array_filter( array_unique( $names ) ) ); 342 if ( empty( $names ) ) { 343 return $out; 344 } 345 346 $out['api_key_valid'] = true; 347 $out['styles_cache'] = $names; 348 $out['styles_cache_ts'] = time(); 349 350 $labels = $this->fetch_style_labels_for_locale(); 351 if ( ! empty( $labels ) ) { 352 $out['styles_labels'] = $labels; 353 $out['styles_labels_ts'] = time(); 354 } 355 356 return $out; 357 } 358 359 public function cron_validate_saved_key() { 360 $lock_key = 'ccfi_validate_key_lock'; 361 if ( get_transient( $lock_key ) ) { 362 return; 363 } 364 set_transient( $lock_key, 1, MINUTE_IN_SECONDS ); 365 366 $opts = $this->get_opts(); 367 $opts = self::refresh_api_route( $opts ); 368 $opts = $this->build_validated_key_opts( $opts ); 369 $this->save_opts( $opts ); 370 371 delete_transient( $lock_key ); 372 } 373 374 public function cron_refresh_api_route() { 375 $lock_key = 'ccfi_refresh_route_lock'; 376 if ( get_transient( $lock_key ) ) { 377 return; 378 } 379 set_transient( $lock_key, 1, MINUTE_IN_SECONDS ); 380 381 $opts = $this->get_opts(); 382 $opts = self::refresh_api_route( $opts ); 383 $this->save_opts( $opts ); 384 385 delete_transient( $lock_key ); 271 386 } 272 387 … … 478 593 479 594 $out['post_types'] = $post_types_clean; 480 $out = $this->refresh_api_route_on_settings_save( $out );481 595 482 596 // Reset validation/cache by default to avoid stale "valid" flags. … … 492 606 $api_key_changed = ( $out['api_key'] !== $prev_key ); 493 607 494 if ( '' === $out['api_key'] ) { 495 $out['api_key_valid'] = false; 496 } elseif ( ! $api_key_changed && $prev_key_was_valid ) { 497 $out['api_key_valid'] = true; 498 $out['styles_cache'] = isset( $prev_opts['styles_cache'] ) && is_array( $prev_opts['styles_cache'] ) ? $prev_opts['styles_cache'] : []; 499 $out['styles_cache_ts'] = isset( $prev_opts['styles_cache_ts'] ) ? (int) $prev_opts['styles_cache_ts'] : 0; 500 $out['styles_labels'] = isset( $prev_opts['styles_labels'] ) && is_array( $prev_opts['styles_labels'] ) ? $prev_opts['styles_labels'] : []; 501 $out['styles_labels_ts'] = isset( $prev_opts['styles_labels_ts'] ) ? (int) $prev_opts['styles_labels_ts'] : 0; 502 } else { 503 $res = wp_remote_request( 504 $this->build_url( 505 self::EP_STYLES, 506 $this->get_effective_api_base( $out ) 507 ), 508 [ 509 'method' => 'GET', 510 'headers' => $this->http_headers( $out['api_key'] ), 511 'timeout' => 10, 512 ] 513 ); 514 $code = is_wp_error( $res ) ? 0 : (int) wp_remote_retrieve_response_code( $res ); 515 516 if ( ! is_wp_error( $res ) && 200 === $code ) { 517 $b = json_decode( wp_remote_retrieve_body( $res ), true ); 518 if ( is_array( $b ) && ! empty( $b['styles'] ) ) { 519 $names = []; 520 foreach ( (array) $b['styles'] as $item ) { 521 if ( is_array( $item ) && isset( $item['name'] ) ) { 522 $names[] = (string) $item['name']; 523 } elseif ( is_string( $item ) ) { 524 $names[] = $item; 525 } 526 } 527 $names = array_values( array_filter( array_unique( $names ) ) ); 528 if ( ! empty( $names ) ) { 529 $out['api_key_valid'] = true; 530 $out['styles_cache'] = $names; 531 $out['styles_cache_ts'] = time(); 532 533 $labels = $this->fetch_style_labels_for_locale(); 534 if ( ! empty( $labels ) ) { 535 $out['styles_labels'] = $labels; 536 $out['styles_labels_ts'] = time(); 537 } 538 } 539 } 608 if ( '' === $out['api_key'] ) { 609 $out['api_key_valid'] = false; 610 } elseif ( ! $api_key_changed && $prev_key_was_valid ) { 611 $out['api_key_valid'] = true; 612 $out['styles_cache'] = isset( $prev_opts['styles_cache'] ) && is_array( $prev_opts['styles_cache'] ) ? $prev_opts['styles_cache'] : []; 613 $out['styles_cache_ts'] = isset( $prev_opts['styles_cache_ts'] ) ? (int) $prev_opts['styles_cache_ts'] : 0; 614 $out['styles_labels'] = isset( $prev_opts['styles_labels'] ) && is_array( $prev_opts['styles_labels'] ) ? $prev_opts['styles_labels'] : []; 615 $out['styles_labels_ts'] = isset( $prev_opts['styles_labels_ts'] ) ? (int) $prev_opts['styles_labels_ts'] : 0; 616 } elseif ( false !== strpos( $out['api_key'], 'clip-' ) ) { 617 // Validate asynchronously to avoid blocking/sometimes interrupted settings save. 618 $this->schedule_api_key_validation(); 540 619 } 541 } 542 543 return $out; 544 }, 545 ] 546 ); 620 621 // Refresh API route on every settings save, even if API key was not changed. 622 $this->schedule_api_route_refresh(); 623 624 return $out; 625 }, 626 ] 627 ); 547 628 548 629 add_settings_section( 'clipcloud_fi_main', __( 'Main settings', 'clipcloud-image-generation' ), '__return_false', 'clipcloud_fi' ); … … 681 762 'pollInterval' => 15000, 682 763 'pollIntervalIdle' => 60000, 683 'cronInterval' => 30000,764 'cronInterval' => 20000, 684 765 'i18n' => [ 685 766 'post' => $labels_post, … … 1196 1277 $post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0; 1197 1278 // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- sanitized and verified below. 1198 $slot = isset( $_GET['slot'] ) ? sanitize_key( wp_unslash( (string) $_GET['slot']) ) : '';1199 $slot_safe = ( 'featured' === $slot || preg_match( '/^h[2-4]:[a-z0-9\\-]+$/i', $slot ) );1279 $slot = isset( $_GET['slot'] ) ? strtolower( trim( sanitize_text_field( wp_unslash( (string) $_GET['slot'] ) ) ) ) : ''; 1280 $slot_safe = ( 'featured' === $slot || 1 === preg_match( '/^h[2-4]:[a-z0-9\\-]+$/', $slot ) ); 1200 1281 1201 1282 if ( $post_id <= 0 || '' === $slot || ! $slot_safe ) { … … 1711 1792 } 1712 1793 1713 $payload = [ 1714 'model' => $r['model'], 1715 'messages' => [ 1716 [ 1717 'role' => 'system', 1718 'content' => $system_text, 1794 $payload = [ 1795 'model' => $r['model'], 1796 'messages' => [ 1797 [ 1798 'role' => 'system', 1799 'content' => $system_text, 1800 ], 1801 [ 1802 'role' => 'user', 1803 'content' => $q, 1804 ], 1719 1805 ], 1720 [1721 ' role' => 'user',1722 'c ontent' => $q,1806 'chat_template_kwargs' => [ 1807 'enable_thinking' => false, 1808 'clear_thinking' => true, 1723 1809 ], 1724 ], 1725 'temperature' => 0.25, 1726 'top_p' => 0.85, 1727 'stream' => false, 1728 ]; 1810 'temperature' => 0.25, 1811 'top_p' => 0.85, 1812 'stream' => false, 1813 ]; 1729 1814 if ( '' !== $r['rk'] && '' !== $r['rv'] ) { 1730 1815 $payload[ $r['rk'] ] = $r['rv']; … … 1808 1893 } 1809 1894 1810 return [ $final_prompt, $final_style, '' ];1895 return [ $final_prompt, $final_style, '', false ]; 1811 1896 } 1812 1897 … … 1822 1907 $final_style = $is_auto_style ? ( $p_style ?: 'HD-HQ' ) : $sel; 1823 1908 $final_negative_prompt = $p_negative_prompt ? $p_negative_prompt : ''; 1824 return [ $final_prompt, $final_style, $final_negative_prompt ];1909 return [ $final_prompt, $final_style, $final_negative_prompt, true ]; 1825 1910 } 1826 1911 … … 1832 1917 $final_style = $is_auto_style ? 'HD-HQ' : $sel; 1833 1918 1834 return [ $final_prompt, $final_style, '' ];1919 return [ $final_prompt, $final_style, '', false ]; 1835 1920 } 1836 1921 … … 1962 2047 1963 2048 /** 2049 * Persist slot runtime maps immediately to survive interrupted cron runs. 2050 * 2051 * @param int $post_id Post ID. 2052 * @param array $states Slot states map. 2053 * @param array $jobs Slot jobs map. 2054 * @param array $attempts Slot attempts map. 2055 */ 2056 private function persist_slot_runtime_maps( $post_id, array $states, array $jobs, array $attempts ) { 2057 $this->save_slot_states_map( $post_id, $states ); 2058 2059 if ( empty( $jobs ) ) { 2060 delete_post_meta( $post_id, self::META_SLOT_JOBS ); 2061 } else { 2062 update_post_meta( $post_id, self::META_SLOT_JOBS, $jobs ); 2063 } 2064 2065 if ( empty( $attempts ) ) { 2066 delete_post_meta( $post_id, self::META_SLOT_ATTEMPTS ); 2067 } else { 2068 update_post_meta( $post_id, self::META_SLOT_ATTEMPTS, $attempts ); 2069 } 2070 } 2071 2072 /** 2073 * Read PHP max_execution_time. 2074 * 2075 * @return int 0 means "no limit". 2076 */ 2077 private function get_max_execution_time_seconds() { 2078 $raw_exec = function_exists( 'ini_get' ) ? ini_get( 'max_execution_time' ) : ''; 2079 $max_exec = is_numeric( $raw_exec ) ? (int) $raw_exec : 0; 2080 return max( 0, $max_exec ); 2081 } 2082 2083 /** 2084 * Remaining runtime for the current watchdog cycle. 2085 * 2086 * @param float $watchdog_started_at Watchdog start timestamp (microtime(true)). 2087 * @param int $safety_margin Seconds to keep as reserve. 2088 * @return int Remaining seconds, or PHP_INT_MAX when max_execution_time is unlimited. 2089 */ 2090 private function get_runtime_remaining_seconds( $watchdog_started_at = 0.0, $safety_margin = 0 ) { 2091 $max_exec = $this->get_max_execution_time_seconds(); 2092 if ( $max_exec <= 0 ) { 2093 return PHP_INT_MAX; 2094 } 2095 2096 $elapsed = 0.0; 2097 if ( $watchdog_started_at > 0 ) { 2098 $elapsed = max( 0.0, microtime( true ) - (float) $watchdog_started_at ); 2099 } 2100 2101 $remaining = (int) floor( $max_exec - $elapsed - max( 0, (int) $safety_margin ) ); 2102 return max( 0, $remaining ); 2103 } 2104 2105 /** 2106 * Whether there is enough runtime left to safely start one more remote request. 2107 * 2108 * @param float $watchdog_started_at Watchdog start timestamp (microtime(true)). 2109 * @param int $required_seconds Required seconds to allow one more request. 2110 * @return bool 2111 */ 2112 private function has_runtime_for_next_request( $watchdog_started_at = 0.0, $required_seconds = 15 ) { 2113 $required = max( 1, (int) $required_seconds ); 2114 $remaining = $this->get_runtime_remaining_seconds( $watchdog_started_at, 0 ); 2115 2116 if ( PHP_INT_MAX === $remaining ) { 2117 return true; 2118 } 2119 2120 return $remaining >= $required; 2121 } 2122 2123 /** 2124 * Compute a safe per-run slot budget based on PHP execution time limits. 2125 * 2126 * @param float $watchdog_started_at Watchdog start timestamp (microtime(true)); 0 for legacy/static mode. 2127 * @param int $per_slot_cost Estimated cost per one remote request in seconds. 2128 * @param int $default Upper cap. 2129 * @return int 2130 */ 2131 private function get_runtime_slot_budget( $watchdog_started_at = 0.0, $per_slot_cost = 0, $default = 10 ) { 2132 $default = max( 1, (int) $default ); 2133 $per_slot_cost = (int) $per_slot_cost; 2134 if ( $per_slot_cost <= 0 ) { 2135 $per_slot_cost = self::RUNTIME_START_COST_SEC; 2136 } 2137 2138 // Legacy/static mode used outside watchdog-aware flow. 2139 if ( $watchdog_started_at <= 0 ) { 2140 $max_exec = $this->get_max_execution_time_seconds(); 2141 if ( $max_exec <= 0 ) { 2142 return $default; 2143 } 2144 $available = max( 1, $max_exec - 5 ); 2145 $budget = (int) floor( $available / max( 1, $per_slot_cost ) ); 2146 return max( 1, min( $default, $budget ) ); 2147 } 2148 2149 $remaining = $this->get_runtime_remaining_seconds( $watchdog_started_at, 0 ); 2150 if ( PHP_INT_MAX === $remaining ) { 2151 return $default; 2152 } 2153 2154 $budget = (int) floor( $remaining / max( 1, $per_slot_cost ) ); 2155 return max( 0, min( $default, $budget ) ); 2156 } 2157 2158 /** 2159 * Compute a safe cap for how many slots can be started in one run. 2160 * 2161 * @param array|null $opts Options array to check auto_enhance mode. 2162 * @param float $watchdog_started_at Watchdog start timestamp (microtime(true)); 0 for legacy/static mode. 2163 * @return int 2164 */ 2165 private function get_runtime_start_budget( $opts = null, $watchdog_started_at = 0.0 ) { 2166 $default = self::MAX_SLOTS_PER_START; 2167 $o = is_array( $opts ) ? $opts : $this->get_opts(); 2168 $auto_enhance = ! empty( $o['auto_enhance'] ); 2169 $per_slot_cost = $auto_enhance ? self::RUNTIME_START_COST_AUTO_ENHANCE_SEC : self::RUNTIME_START_COST_SEC; 2170 2171 return $this->get_runtime_slot_budget( (float) $watchdog_started_at, $per_slot_cost, $default ); 2172 } 2173 2174 /** 1964 2175 * Collect all H2/H3/H4 headings from post content and return slots. 1965 2176 * … … 2158 2369 } 2159 2370 2371 // Prompt context. 2372 $slot_prompt_context = $this->get_slot_prompt_context_map( $post_id ); 2373 $slot_prompt_context_changed = false; 2374 foreach ( $slot_prompt_context as $slot => $context ) { 2375 if ( $this->is_heading_slot_key( $slot ) && ! isset( $allowed_map[ $slot ] ) ) { 2376 unset( $slot_prompt_context[ $slot ] ); 2377 $slot_prompt_context_changed = true; 2378 } 2379 } 2380 if ( $slot_prompt_context_changed ) { 2381 $this->save_slot_prompt_context_map( $post_id, $slot_prompt_context ); 2382 } 2383 2160 2384 // Generated attachments per slot. 2161 2385 if ( ! empty( $slots_to_purge ) ) { … … 2196 2420 delete_post_meta( $post_id, self::META_GENERATED_ATTACHMENTS ); 2197 2421 delete_post_meta( $post_id, self::META_SLOTS_HASH ); 2422 delete_post_meta( $post_id, self::META_APPLY_ATTEMPTS ); 2423 delete_post_meta( $post_id, self::META_SLOT_PROMPT_CONTEXT ); 2198 2424 2199 2425 $this->strip_plugin_figures_from_content( $post_id, $this->get_opts() ); … … 2249 2475 update_post_meta( $post_id, self::META_SLOT_CREATION_ATTEMPTS, $creation_attempts ); 2250 2476 } 2477 } 2478 2479 $slot_prompt_context = $this->get_slot_prompt_context_map( $post_id ); 2480 if ( isset( $slot_prompt_context[ $slot ] ) ) { 2481 unset( $slot_prompt_context[ $slot ] ); 2482 $this->save_slot_prompt_context_map( $post_id, $slot_prompt_context ); 2251 2483 } 2252 2484 } … … 2381 2613 * @param bool $force Force regeneration flag. 2382 2614 * @param int $max_slots Maximum slots to start in this run (0 = no extra cap). 2615 * @param float $watchdog_started_at Watchdog start timestamp (microtime(true)); 0 when not called from watchdog. 2383 2616 */ 2384 private function start_generation_for_post( $post_id, $force = false, $max_slots = 0 ) { 2385 $post_id = (int) $post_id; 2386 $max_slots = max( 0, (int) $max_slots ); 2617 private function start_generation_for_post( $post_id, $force = false, $max_slots = 0, $watchdog_started_at = 0.0 ) { 2618 $post_id = (int) $post_id; 2619 $max_slots = max( 0, (int) $max_slots ); 2620 $watchdog_started_at = (float) $watchdog_started_at; 2387 2621 if ( $post_id <= 0 || ! get_post( $post_id ) ) { 2388 2622 return 0; … … 2409 2643 $creation_attempts = is_array( $creation_attempts ) ? $creation_attempts : []; 2410 2644 $creation_attempts_changed = false; 2645 $slot_prompt_context = $this->get_slot_prompt_context_map( $post_id ); 2646 $slot_prompt_context_changed = false; 2411 2647 2412 2648 $slots_to_start = []; … … 2495 2731 $had_slots_to_start = ! empty( $slots_to_start ); 2496 2732 2497 // Limit number of slots per run. 2498 if ( ! empty( $slots_to_start ) && count( $slots_to_start ) > self::MAX_SLOTS_PER_START ) { 2499 $slots_to_start = array_slice( $slots_to_start, 0, self::MAX_SLOTS_PER_START ); 2500 $pending_retry = true; 2733 // Legacy/static mode: cap upfront by runtime estimate. 2734 // Watchdog-aware mode decides continuation only after each remote request. 2735 if ( $watchdog_started_at <= 0 ) { 2736 $runtime_start_limit = $this->get_runtime_start_budget( $o, 0 ); 2737 if ( ! empty( $slots_to_start ) && count( $slots_to_start ) > $runtime_start_limit ) { 2738 $slots_to_start = array_slice( $slots_to_start, 0, $runtime_start_limit ); 2739 $pending_retry = true; 2740 } 2501 2741 } 2502 2742 if ( $max_slots > 0 && ! empty( $slots_to_start ) && count( $slots_to_start ) > $max_slots ) { … … 2507 2747 $started = false; 2508 2748 $attempts_dummy = []; 2749 $slot_request_cost = ! empty( $o['auto_enhance'] ) ? self::RUNTIME_START_COST_AUTO_ENHANCE_SEC : self::RUNTIME_START_COST_SEC; 2750 $stop_due_runtime = false; 2509 2751 2510 2752 foreach ( $slots_to_start as $slot_info ) { … … 2513 2755 break; 2514 2756 } 2515 $slot = $slot_info['slot']; 2516 $heading_text = $slot_info['heading_text']; 2757 if ( $stop_due_runtime ) { 2758 $pending_retry = true; 2759 break; 2760 } 2761 2762 $slot = $slot_info['slot']; 2763 $heading_text = $slot_info['heading_text']; 2517 2764 $start_attempts = isset( $creation_attempts[ $slot ] ) ? (int) $creation_attempts[ $slot ] : 0; 2518 2765 2519 list( $final_prompt, $final_style, $final_negative_prompt ) = $this->prepare_prompt_and_style( $post_id, $o, $heading_text );2766 list( $final_prompt, $final_style, $final_negative_prompt, $enhanced_prompt_used ) = $this->prepare_prompt_and_style( $post_id, $o, $heading_text ); 2520 2767 2521 2768 $payload = [ … … 2537 2784 ] 2538 2785 ); 2786 $stop_due_runtime = ( $watchdog_started_at > 0 && ! $this->has_runtime_for_next_request( $watchdog_started_at, $slot_request_cost ) ); 2539 2787 2540 2788 if ( is_wp_error( $res ) ) { … … 2546 2794 } else { 2547 2795 $creation_attempts[ $slot ] = $start_attempts; 2548 $pending_retry = true;2796 $pending_retry = true; 2549 2797 } 2550 2798 $creation_attempts_changed = true; 2799 if ( $stop_due_runtime ) { 2800 $pending_retry = true; 2801 } 2551 2802 continue; 2552 2803 } … … 2555 2806 $body = json_decode( wp_remote_retrieve_body( $res ), true ); 2556 2807 2557 if ( $code >= 200 && $code < 300 && is_array( $body ) ) {2558 $job_id_raw = isset( $body[ self::FIELD_CREATION_ID ] ) ? (string) $body[ self::FIELD_CREATION_ID ] : '';2559 if ( '' !== $job_id_raw ) {2808 if ( $code >= 200 && $code < 300 && is_array( $body ) ) { 2809 $job_id_raw = isset( $body[ self::FIELD_CREATION_ID ] ) ? (string) $body[ self::FIELD_CREATION_ID ] : ''; 2810 if ( '' !== $job_id_raw ) { 2560 2811 $jobs[ $slot ] = $job_id_raw; 2561 2812 $states[ $slot ] = 'generating'; 2562 $started = true; 2813 $slot_prompt_context[ $slot ] = [ 2814 'prompt' => trim( wp_strip_all_tags( (string) $final_prompt ) ), 2815 'enhanced' => $enhanced_prompt_used ? 1 : 0, 2816 ]; 2817 $slot_prompt_context_changed = true; 2818 $started = true; 2563 2819 $started_slots++; 2564 2820 if ( isset( $creation_attempts[ $slot ] ) ) { … … 2566 2822 $creation_attempts_changed = true; 2567 2823 } 2824 if ( $stop_due_runtime ) { 2825 $pending_retry = true; 2826 } 2568 2827 continue; 2569 } 2570 } 2571 2572 // Some API nodes may reject optional negative_prompt; retry once without it. 2573 if ( isset( $payload['negative_prompt'] ) ) { 2574 $retry_payload = $payload; 2575 unset( $retry_payload['negative_prompt'] ); 2576 2577 $retry_res = wp_remote_request( 2578 $this->build_url( self::EP_CREATION ), 2579 [ 2580 'method' => 'POST', 2581 'headers' => $this->http_headers( $o['api_key'], true ), 2582 'timeout' => self::HTTP_TIMEOUT_SEC, 2583 'body' => wp_json_encode( $retry_payload, JSON_UNESCAPED_UNICODE ), 2584 ] 2585 ); 2586 2587 if ( ! is_wp_error( $retry_res ) ) { 2588 $retry_code = wp_remote_retrieve_response_code( $retry_res ); 2589 $retry_body = json_decode( wp_remote_retrieve_body( $retry_res ), true ); 2590 2591 if ( $retry_code >= 200 && $retry_code < 300 && is_array( $retry_body ) ) { 2592 $job_id_raw = isset( $retry_body[ self::FIELD_CREATION_ID ] ) ? (string) $retry_body[ self::FIELD_CREATION_ID ] : ''; 2593 if ( '' !== $job_id_raw ) { 2594 $jobs[ $slot ] = $job_id_raw; 2595 $states[ $slot ] = 'generating'; 2596 $started = true; 2597 $started_slots++; 2598 if ( isset( $creation_attempts[ $slot ] ) ) { 2599 unset( $creation_attempts[ $slot ] ); 2600 $creation_attempts_changed = true; 2601 } 2602 continue; 2828 } 2829 } 2830 2831 // Some API nodes may reject optional negative_prompt; retry once without it. 2832 if ( isset( $payload['negative_prompt'] ) ) { 2833 if ( $stop_due_runtime ) { 2834 $pending_retry = true; 2835 continue; 2836 } 2837 2838 $retry_payload = $payload; 2839 unset( $retry_payload['negative_prompt'] ); 2840 2841 $retry_res = wp_remote_request( 2842 $this->build_url( self::EP_CREATION ), 2843 [ 2844 'method' => 'POST', 2845 'headers' => $this->http_headers( $o['api_key'], true ), 2846 'timeout' => self::HTTP_TIMEOUT_SEC, 2847 'body' => wp_json_encode( $retry_payload, JSON_UNESCAPED_UNICODE ), 2848 ] 2849 ); 2850 $stop_due_runtime = ( $watchdog_started_at > 0 && ! $this->has_runtime_for_next_request( $watchdog_started_at, $slot_request_cost ) ); 2851 2852 if ( ! is_wp_error( $retry_res ) ) { 2853 $retry_code = wp_remote_retrieve_response_code( $retry_res ); 2854 $retry_body = json_decode( wp_remote_retrieve_body( $retry_res ), true ); 2855 2856 if ( $retry_code >= 200 && $retry_code < 300 && is_array( $retry_body ) ) { 2857 $job_id_raw = isset( $retry_body[ self::FIELD_CREATION_ID ] ) ? (string) $retry_body[ self::FIELD_CREATION_ID ] : ''; 2858 if ( '' !== $job_id_raw ) { 2859 $jobs[ $slot ] = $job_id_raw; 2860 $states[ $slot ] = 'generating'; 2861 $slot_prompt_context[ $slot ] = [ 2862 'prompt' => trim( wp_strip_all_tags( (string) $final_prompt ) ), 2863 'enhanced' => $enhanced_prompt_used ? 1 : 0, 2864 ]; 2865 $slot_prompt_context_changed = true; 2866 $started = true; 2867 $started_slots++; 2868 if ( isset( $creation_attempts[ $slot ] ) ) { 2869 unset( $creation_attempts[ $slot ] ); 2870 $creation_attempts_changed = true; 2603 2871 } 2872 if ( $stop_due_runtime ) { 2873 $pending_retry = true; 2874 } 2875 continue; 2604 2876 } 2605 2877 } 2606 2878 } 2607 2608 // If we reach here the slot failed. 2609 $start_attempts++; 2879 } 2880 2881 // If we reach here the slot failed. 2882 $start_attempts++; 2610 2883 if ( $start_attempts >= self::MAX_CREATION_ATTEMPTS ) { 2611 2884 $this->handle_slot_failure( $slot, $states, $jobs, $attempts_dummy ); … … 2613 2886 } else { 2614 2887 $creation_attempts[ $slot ] = $start_attempts; 2615 $pending_retry = true;2888 $pending_retry = true; 2616 2889 } 2617 2890 $creation_attempts_changed = true; 2891 if ( $stop_due_runtime ) { 2892 $pending_retry = true; 2893 } 2618 2894 } 2619 2895 … … 2634 2910 } 2635 2911 2912 if ( $slot_prompt_context_changed ) { 2913 $this->save_slot_prompt_context_map( $post_id, $slot_prompt_context ); 2914 } 2915 2636 2916 // If we actually started something, set status to generating and schedule polling. 2637 2917 if ( $started ) { … … 2647 2927 wp_schedule_single_event( time(), self::CRON_HOOK_WATCHDOG, [] ); 2648 2928 } 2929 } else { 2930 // Nothing to start or everything is finished: treat as completed. 2931 $has_done = in_array( 'done', $states, true ); 2932 $has_failed = in_array( 'failed', $states, true ); 2933 2934 if ( ! $had_slots_to_start && ! $has_done && ! $has_failed ) { 2935 delete_post_meta( $post_id, self::META_STATUS ); 2649 2936 } else { 2650 // Nothing to start or everything is finished: treat as completed. 2651 $has_done = in_array( 'done', $states, true ); 2652 $has_failed = in_array( 'failed', $states, true ); 2653 2654 if ( ! $had_slots_to_start && ! $has_done && ! $has_failed ) { 2655 delete_post_meta( $post_id, self::META_STATUS ); 2656 } else { 2657 update_post_meta( $post_id, self::META_STATUS, $has_failed ? 'failed' : 'done' ); 2658 } 2659 delete_post_meta( $post_id, self::META_FORCE ); 2660 } 2937 update_post_meta( $post_id, self::META_STATUS, $has_failed ? 'failed' : 'done' ); 2938 } 2939 delete_post_meta( $post_id, self::META_FORCE ); 2940 } 2661 2941 2662 2942 delete_transient( $lock_key ); … … 2673 2953 * @param int $post_id Post ID. 2674 2954 * @param int $max_slots Maximum slots/jobs to poll in this run (0 = no limit). 2955 * @param float $watchdog_started_at Watchdog start timestamp (microtime(true)); 0 when not called from watchdog. 2675 2956 */ 2676 public function cron_check_result( $post_id, $max_slots = 0 ) { 2677 $post_id = (int) $post_id; 2678 $max_slots = max( 0, (int) $max_slots ); 2957 public function cron_check_result( $post_id, $max_slots = 0, $watchdog_started_at = 0.0 ) { 2958 $post_id = (int) $post_id; 2959 $max_slots = max( 0, (int) $max_slots ); 2960 $watchdog_started_at = (float) $watchdog_started_at; 2679 2961 if ( $post_id <= 0 || ! get_post( $post_id ) ) { 2680 2962 return 0; … … 2700 2982 $images = $this->get_heading_images_map( $post_id ); 2701 2983 2984 // Heal stale/partial state so existing jobs are not skipped forever. 2985 $runtime_maps_changed = false; 2986 foreach ( $jobs as $slot => $remote_id_raw ) { 2987 $slot = (string) $slot; 2988 $remote_id = trim( (string) $remote_id_raw ); 2989 2990 if ( '' === $slot || '' === $remote_id ) { 2991 unset( $jobs[ $slot ], $attempts[ $slot ] ); 2992 $runtime_maps_changed = true; 2993 continue; 2994 } 2995 2996 if ( $remote_id !== (string) $remote_id_raw ) { 2997 $jobs[ $slot ] = $remote_id; 2998 $runtime_maps_changed = true; 2999 } 3000 3001 $slot_state = isset( $states[ $slot ] ) ? (string) $states[ $slot ] : ''; 3002 if ( in_array( $slot_state, [ 'done', 'failed' ], true ) ) { 3003 unset( $jobs[ $slot ], $attempts[ $slot ] ); 3004 $runtime_maps_changed = true; 3005 continue; 3006 } 3007 3008 if ( 'generating' !== $slot_state ) { 3009 $states[ $slot ] = 'generating'; 3010 $runtime_maps_changed = true; 3011 } 3012 } 3013 3014 if ( $runtime_maps_changed ) { 3015 $this->persist_slot_runtime_maps( $post_id, $states, $jobs, $attempts ); 3016 } 3017 if ( empty( $jobs ) ) { 3018 return 0; 3019 } 3020 2702 3021 $should_apply_content = false; 2703 3022 $processed_slots = 0; 3023 $stop_due_runtime = false; 2704 3024 2705 3025 foreach ( $jobs as $slot => $remote_id ) { … … 2707 3027 break; 2708 3028 } 3029 if ( $stop_due_runtime ) { 3030 break; 3031 } 2709 3032 $processed_slots++; 2710 $slot = (string) $slot; 2711 $remote_id = (string) $remote_id; 2712 $slot_state = isset( $states[ $slot ] ) ? $states[ $slot ] : ''; 2713 2714 if ( 'generating' !== $slot_state || '' === $remote_id ) { 3033 $slot = (string) $slot; 3034 $remote_id = (string) $remote_id; 3035 3036 if ( '' === $remote_id ) { 2715 3037 continue; 2716 3038 } … … 2719 3041 if ( $slot_attempts >= self::MAX_ATTEMPTS ) { 2720 3042 $this->handle_slot_failure( $slot, $states, $jobs, $attempts ); 3043 $this->persist_slot_runtime_maps( $post_id, $states, $jobs, $attempts ); 2721 3044 continue; 2722 3045 } … … 2724 3047 $slot_attempts++; 2725 3048 $attempts[ $slot ] = $slot_attempts; 3049 $this->persist_slot_runtime_maps( $post_id, $states, $jobs, $attempts ); 2726 3050 2727 3051 $path = str_replace( '{creation_id}', rawurlencode( $remote_id ), self::EP_CREATION_BY_ID ); … … 2735 3059 ] 2736 3060 ); 3061 $stop_due_runtime = ( $watchdog_started_at > 0 && ! $this->has_runtime_for_next_request( $watchdog_started_at, self::RUNTIME_POLL_COST_SEC ) ); 2737 3062 2738 3063 if ( is_wp_error( $res ) ) { … … 2744 3069 if ( 401 === $code || 404 === $code ) { 2745 3070 $this->handle_slot_failure( $slot, $states, $jobs, $attempts ); 3071 $this->persist_slot_runtime_maps( $post_id, $states, $jobs, $attempts ); 2746 3072 continue; 2747 3073 } … … 2759 3085 if ( array_key_exists( 'success', $body ) && false === $body['success'] ) { 2760 3086 $this->handle_slot_failure( $slot, $states, $jobs, $attempts ); 3087 $this->persist_slot_runtime_maps( $post_id, $states, $jobs, $attempts ); 2761 3088 continue; 2762 3089 } … … 2771 3098 if ( ! $first_url ) { 2772 3099 $this->handle_slot_failure( $slot, $states, $jobs, $attempts ); 3100 $this->persist_slot_runtime_maps( $post_id, $states, $jobs, $attempts ); 2773 3101 continue; 2774 3102 } … … 2778 3106 if ( ! $attach_id ) { 2779 3107 $this->handle_slot_failure( $slot, $states, $jobs, $attempts ); 3108 $this->persist_slot_runtime_maps( $post_id, $states, $jobs, $attempts ); 2780 3109 continue; 2781 3110 } … … 2804 3133 // Drop job and attempts for this slot. 2805 3134 unset( $jobs[ $slot ], $attempts[ $slot ] ); 3135 $this->persist_slot_runtime_maps( $post_id, $states, $jobs, $attempts ); 2806 3136 2807 3137 $should_apply_content = true; … … 2833 3163 $more_slots = $this->has_more_slots_to_start( $post_id, $force, $states, $images, $jobs, $o ); 2834 3164 2835 if ( $more_slots ) { 2836 $this->start_generation_for_post( $post_id, $force ); 3165 if ( $more_slots ) { 3166 $start_budget = 0; 3167 if ( $max_slots > 0 ) { 3168 $start_budget = max( 0, $max_slots - $processed_slots ); 3169 } 3170 3171 if ( 0 === $max_slots || $start_budget > 0 ) { 3172 $this->start_generation_for_post( $post_id, $force, $start_budget, $watchdog_started_at ); 3173 } 2837 3174 2838 3175 // Refresh state after attempting to start more slots. … … 2904 3241 } 2905 3242 3243 private function get_slot_prompt_context_map( $post_id ) { 3244 $raw = get_post_meta( $post_id, self::META_SLOT_PROMPT_CONTEXT, true ); 3245 if ( ! is_array( $raw ) ) { 3246 return []; 3247 } 3248 3249 $map = []; 3250 foreach ( $raw as $slot => $context ) { 3251 $key = (string) $slot; 3252 if ( '' === $key || ! is_array( $context ) ) { 3253 continue; 3254 } 3255 3256 $prompt = isset( $context['prompt'] ) && is_string( $context['prompt'] ) ? trim( $context['prompt'] ) : ''; 3257 $map[ $key ] = [ 3258 'prompt' => $prompt, 3259 'enhanced' => ! empty( $context['enhanced'] ) ? 1 : 0, 3260 ]; 3261 } 3262 3263 return $map; 3264 } 3265 3266 private function save_slot_prompt_context_map( $post_id, array $map ) { 3267 $clean = []; 3268 foreach ( $map as $slot => $context ) { 3269 $key = (string) $slot; 3270 if ( '' === $key || ! is_array( $context ) ) { 3271 continue; 3272 } 3273 3274 $prompt = isset( $context['prompt'] ) && is_string( $context['prompt'] ) ? trim( $context['prompt'] ) : ''; 3275 $clean[ $key ] = [ 3276 'prompt' => $prompt, 3277 'enhanced' => ! empty( $context['enhanced'] ) ? 1 : 0, 3278 ]; 3279 } 3280 3281 if ( empty( $clean ) ) { 3282 delete_post_meta( $post_id, self::META_SLOT_PROMPT_CONTEXT ); 3283 return; 3284 } 3285 3286 update_post_meta( $post_id, self::META_SLOT_PROMPT_CONTEXT, $clean ); 3287 } 3288 3289 private function get_slot_prompt_context( $post_id, $slot ) { 3290 $slot = (string) $slot; 3291 if ( '' === $slot ) { 3292 $slot = 'featured'; 3293 } 3294 3295 $map = $this->get_slot_prompt_context_map( $post_id ); 3296 if ( isset( $map[ $slot ] ) && is_array( $map[ $slot ] ) ) { 3297 return $map[ $slot ]; 3298 } 3299 3300 return [ 3301 'prompt' => '', 3302 'enhanced' => 0, 3303 ]; 3304 } 3305 3306 private function get_heading_text_by_slot( $post_id, $slot ) { 3307 $slot = (string) $slot; 3308 if ( '' === $slot || ! $this->is_heading_slot_key( $slot ) ) { 3309 return ''; 3310 } 3311 3312 $headings = $this->collect_heading_slots( $post_id ); 3313 foreach ( $headings as $heading ) { 3314 $heading_slot = isset( $heading['slot'] ) ? (string) $heading['slot'] : ''; 3315 if ( $heading_slot !== $slot ) { 3316 continue; 3317 } 3318 3319 $text = isset( $heading['text'] ) && is_string( $heading['text'] ) ? trim( wp_strip_all_tags( $heading['text'] ) ) : ''; 3320 return $text; 3321 } 3322 3323 return ''; 3324 } 3325 3326 private function build_attachment_title_for_slot( $post_id, $slot ) { 3327 $slot = (string) $slot; 3328 $post_title = get_the_title( $post_id ); 3329 $post_title = is_string( $post_title ) ? trim( wp_strip_all_tags( $post_title ) ) : ''; 3330 3331 if ( '' === $slot || 'featured' === $slot ) { 3332 return $post_title; 3333 } 3334 3335 $heading_title = $this->get_heading_text_by_slot( $post_id, $slot ); 3336 if ( '' !== $heading_title && '' !== $post_title ) { 3337 return $heading_title . ' — ' . $post_title; 3338 } 3339 if ( '' !== $heading_title ) { 3340 return $heading_title; 3341 } 3342 3343 return $post_title; 3344 } 3345 3346 private function apply_generated_attachment_texts( $post_id, $slot, $attach_id ) { 3347 $post_id = (int) $post_id; 3348 $slot = (string) $slot; 3349 $attach_id = (int) $attach_id; 3350 3351 if ( $post_id <= 0 || $attach_id <= 0 ) { 3352 return; 3353 } 3354 3355 if ( '' === $slot ) { 3356 $slot = 'featured'; 3357 } 3358 3359 $attachment_title = $this->build_attachment_title_for_slot( $post_id, $slot ); 3360 $attachment_title = trim( wp_strip_all_tags( (string) $attachment_title ) ); 3361 if ( '' === $attachment_title ) { 3362 $attachment_title = __( 'ClipCloud generated image', 'clipcloud-image-generation' ); 3363 } 3364 3365 $alt_text = $attachment_title; 3366 $context = $this->get_slot_prompt_context( $post_id, $slot ); 3367 if ( ! empty( $context['enhanced'] ) ) { 3368 $prompt = isset( $context['prompt'] ) && is_string( $context['prompt'] ) ? trim( wp_strip_all_tags( $context['prompt'] ) ) : ''; 3369 if ( '' !== $prompt ) { 3370 $alt_text = $prompt; 3371 } 3372 } 3373 3374 if ( '' === $alt_text ) { 3375 $alt_text = $attachment_title; 3376 } 3377 3378 wp_update_post( 3379 [ 3380 'ID' => $attach_id, 3381 'post_title' => $attachment_title, 3382 ] 3383 ); 3384 3385 update_post_meta( $attach_id, '_wp_attachment_image_alt', $alt_text ); 3386 } 3387 2906 3388 private function save_generated_attachments_map( $post_id, array $map ) { 2907 3389 $clean = []; … … 2952 3434 private function download_and_attach( $url, $post_id, $slot ) { 2953 3435 $slot = (string) $slot; 3436 $attachment_title = $this->build_attachment_title_for_slot( $post_id, $slot ); 3437 $attachment_title = trim( wp_strip_all_tags( (string) $attachment_title ) ); 3438 if ( '' === $attachment_title ) { 3439 $attachment_title = __( 'ClipCloud generated image', 'clipcloud-image-generation' ); 3440 } 2954 3441 2955 3442 require_once ABSPATH . 'wp-admin/includes/file.php'; … … 3034 3521 wp_delete_file( $tmp ); 3035 3522 $aid = (int) $recent[0]; 3523 $this->apply_generated_attachment_texts( $post_id, $slot, $aid ); 3036 3524 $this->track_generated_attachment( $post_id, $slot, $aid ); 3037 3525 return $aid; … … 3043 3531 ]; 3044 3532 3045 $attach_id = media_handle_sideload( $file_array, $post_id, __( 'ClipCloud generated image', 'clipcloud-image-generation' ));3533 $attach_id = media_handle_sideload( $file_array, $post_id, $attachment_title ); 3046 3534 if ( is_wp_error( $attach_id ) ) { 3047 3535 wp_delete_file( $file_array['tmp_name'] ); … … 3049 3537 } 3050 3538 update_post_meta( $attach_id, '_ccfi_source_name', $filename ); 3539 $this->apply_generated_attachment_texts( $post_id, $slot, $attach_id ); 3051 3540 $this->track_generated_attachment( $post_id, $slot, $attach_id ); 3052 3541 return $attach_id; … … 3396 3885 $has_jobs = ! empty( $jobs ); 3397 3886 3398 $is_pending = $has_active_status || $has_jobs || $has_generating_state; 3399 if ( ! $is_pending && $apply_pending ) { 3400 // Apply pending is left for cron to handle; it should not keep browser polling. 3401 $apply_pending = false; 3402 } 3887 $is_pending = $has_active_status || $has_jobs || $has_generating_state || $apply_pending; 3403 3888 3404 3889 if ( $is_pending ) { … … 3774 4259 $gen_h3 = ! empty( $opts['gen_h3'] ); 3775 4260 $gen_h4 = ! empty( $opts['gen_h4'] ); 3776 3777 if ( ! $gen_h2 && ! $gen_h3 && ! $gen_h4 ) { 4261 $enabled_levels = []; 4262 if ( $gen_h2 ) { 4263 $enabled_levels[] = 'h2'; 4264 } 4265 if ( $gen_h3 ) { 4266 $enabled_levels[] = 'h3'; 4267 } 4268 if ( $gen_h4 ) { 4269 $enabled_levels[] = 'h4'; 4270 } 4271 4272 if ( empty( $enabled_levels ) ) { 3778 4273 return $result; 3779 4274 } … … 3786 4281 3787 4282 if ( $replace_existing ) { 4283 $level_pattern = implode( '|', array_map( 'preg_quote', $enabled_levels ) ); 3788 4284 $patterns = [ 3789 '~<!--\s*wp:image[^>]*ccfi-heading-figure [^>]*-->.*?<!--\s*/wp:image\s*-->~is',3790 '~<figure[^>]*class="[^"]*ccfi-heading-figure [^"]*"[^>]*>.*?</figure>~is',4285 '~<!--\s*wp:image[^>]*ccfi-heading-figure-(?:' . $level_pattern . ')[^>]*-->.*?<!--\s*/wp:image\s*-->~is', 4286 '~<figure[^>]*class="[^"]*ccfi-heading-figure-(?:' . $level_pattern . ')[^"]*"[^>]*>.*?</figure>~is', 3791 4287 ]; 3792 4288 foreach ( $patterns as $pattern ) { … … 3814 4310 $cursor = $start + strlen( $full ); 3815 4311 4312 if ( '' === $text ) { 4313 $output .= $full; 4314 continue; 4315 } 4316 4317 $slot = $this->build_slot_id( $level, $text, $counters ); 4318 4319 // Keep slot counters in sync with collection logic even for disabled levels. 3816 4320 if ( ( 2 === $level && ! $gen_h2 ) || ( 3 === $level && ! $gen_h3 ) || ( 4 === $level && ! $gen_h4 ) ) { 3817 4321 $output .= $full; 3818 4322 continue; 3819 4323 } 3820 3821 if ( '' === $text ) {3822 $output .= $full;3823 continue;3824 }3825 3826 $slot = $this->build_slot_id( $level, $text, $counters );3827 4324 3828 4325 if ( empty( $map[ $slot ] ) ) { … … 3872 4369 } 3873 4370 3874 private function is_plugin_image_block( $block ) {4371 private function is_plugin_image_block( $block, array $enabled_levels = [] ) { 3875 4372 $haystack = ''; 3876 4373 … … 3885 4382 } 3886 4383 3887 return ( false !== strpos( $haystack, 'ccfi-heading-figure' ) ); 4384 if ( empty( $enabled_levels ) ) { 4385 return ( false !== strpos( $haystack, 'ccfi-heading-figure' ) ); 4386 } 4387 4388 $level_pattern = implode( '|', array_map( 'preg_quote', $enabled_levels ) ); 4389 if ( '' === $level_pattern ) { 4390 return ( false !== strpos( $haystack, 'ccfi-heading-figure' ) ); 4391 } 4392 4393 return (bool) preg_match( '~ccfi-heading-figure-(?:' . $level_pattern . ')~i', $haystack ); 3888 4394 } 3889 4395 … … 3971 4477 private function process_blocks_for_injection( array $blocks, $opts, $map, array &$counters, $position, &$modified ) { 3972 4478 $output = []; 4479 $gen_h2 = ! empty( $opts['gen_h2'] ); 4480 $gen_h3 = ! empty( $opts['gen_h3'] ); 4481 $gen_h4 = ! empty( $opts['gen_h4'] ); 4482 $enabled_levels = []; 4483 if ( $gen_h2 ) { 4484 $enabled_levels[] = 'h2'; 4485 } 4486 if ( $gen_h3 ) { 4487 $enabled_levels[] = 'h3'; 4488 } 4489 if ( $gen_h4 ) { 4490 $enabled_levels[] = 'h4'; 4491 } 3973 4492 3974 4493 foreach ( $blocks as $block ) { … … 3976 4495 3977 4496 // Drop previously inserted plugin figures to avoid duplicates. 3978 if ( 'core/image' === $name && $this->is_plugin_image_block( $block ) ) {4497 if ( 'core/image' === $name && $this->is_plugin_image_block( $block, $enabled_levels ) ) { 3979 4498 $modified = true; 3980 4499 continue; … … 3997 4516 $slot = $this->build_slot_id( $level, $text, $counters ); 3998 4517 4518 // Keep slot counters in sync with collection logic even for disabled levels. 4519 if ( ( 2 === $level && ! $gen_h2 ) || ( 3 === $level && ! $gen_h3 ) || ( 4 === $level && ! $gen_h4 ) ) { 4520 $output[] = $block; 4521 continue; 4522 } 4523 3999 4524 $image_block = null; 4000 4525 if ( ! empty( $map[ $slot ] ) ) { … … 4046 4571 4047 4572 private function apply_heading_images_to_content( $post_id, $opts, $map ) { 4573 $post_id = (int) $post_id; 4574 $apply_attempts = (int) get_post_meta( $post_id, self::META_APPLY_ATTEMPTS, true ); 4575 4048 4576 if ( empty( $map ) ) { 4049 4577 delete_post_meta( $post_id, self::META_APPLY_PENDING ); 4578 delete_post_meta( $post_id, self::META_APPLY_ATTEMPTS ); 4050 4579 return; 4051 4580 } … … 4061 4590 if ( ! $post ) { 4062 4591 delete_post_meta( $post_id, self::META_APPLY_PENDING ); 4592 delete_post_meta( $post_id, self::META_APPLY_ATTEMPTS ); 4593 if ( isset( $lock_set ) && $lock_set ) { 4594 delete_transient( $lock_key ); 4595 } 4596 return; 4597 } 4598 4599 $gen_h2 = ! empty( $opts['gen_h2'] ); 4600 $gen_h3 = ! empty( $opts['gen_h3'] ); 4601 $gen_h4 = ! empty( $opts['gen_h4'] ); 4602 if ( ! $gen_h2 && ! $gen_h3 && ! $gen_h4 ) { 4603 delete_post_meta( $post_id, self::META_APPLY_PENDING ); 4604 delete_post_meta( $post_id, self::META_APPLY_ATTEMPTS ); 4605 if ( isset( $lock_set ) && $lock_set ) { 4606 delete_transient( $lock_key ); 4607 } 4608 return; 4609 } 4610 4611 $heading_map = []; 4612 foreach ( $map as $slot_key => $attachment_id ) { 4613 $slot_key = (string) $slot_key; 4614 $attachment_id = (int) $attachment_id; 4615 if ( $attachment_id <= 0 ) { 4616 continue; 4617 } 4618 if ( $gen_h2 && 0 === strpos( $slot_key, 'h2:' ) ) { 4619 $heading_map[ $slot_key ] = $attachment_id; 4620 } elseif ( $gen_h3 && 0 === strpos( $slot_key, 'h3:' ) ) { 4621 $heading_map[ $slot_key ] = $attachment_id; 4622 } elseif ( $gen_h4 && 0 === strpos( $slot_key, 'h4:' ) ) { 4623 $heading_map[ $slot_key ] = $attachment_id; 4624 } 4625 } 4626 4627 if ( empty( $heading_map ) ) { 4628 delete_post_meta( $post_id, self::META_APPLY_PENDING ); 4629 delete_post_meta( $post_id, self::META_APPLY_ATTEMPTS ); 4063 4630 if ( isset( $lock_set ) && $lock_set ) { 4064 4631 delete_transient( $lock_key ); … … 4087 4654 $is_block = function_exists( 'has_blocks' ) && has_blocks( $post ); 4088 4655 4089 $has_heading_image = false;4090 foreach ( $map as $slot_key => $attachment_id ) {4091 if ( 0 === strpos( (string) $slot_key, 'h2:' ) || 0 === strpos( (string) $slot_key, 'h3:' ) || 0 === strpos( (string) $slot_key, 'h4:' ) ) {4092 $has_heading_image = true;4093 break;4094 }4095 }4096 4097 if ( ! $has_heading_image ) {4098 delete_post_meta( $post_id, self::META_APPLY_PENDING );4099 if ( isset( $lock_set ) && $lock_set ) {4100 delete_transient( $lock_key );4101 }4102 return;4103 }4104 4105 4656 $current = (string) $post->post_content; 4106 $render = $is_block ? $this->inject_images_into_blocks( $current, $opts, $map ) : $this->render_heading_images_markup( $current, $opts, $map, true );4657 $render = $is_block ? $this->inject_images_into_blocks( $current, $opts, $heading_map ) : $this->render_heading_images_markup( $current, $opts, $heading_map, true ); 4107 4658 4108 4659 // Fallback: if block injection did not change content, try HTML-based injection. 4109 4660 if ( $is_block && ( ! $render['modified'] || $render['content'] === $current ) ) { 4110 $render = $this->render_heading_images_markup( $current, $opts, $ map, true );4661 $render = $this->render_heading_images_markup( $current, $opts, $heading_map, true ); 4111 4662 } 4112 4663 4113 4664 if ( ! $render['modified'] || $render['content'] === $current ) { 4114 // Keep apply pending and reschedule to try again later. 4115 update_post_meta( $post_id, self::META_APPLY_PENDING, 1 ); 4116 if ( ! wp_next_scheduled( self::CRON_HOOK_WATCHDOG ) ) { 4117 wp_schedule_single_event( time() + self::POLL_INTERVAL_SEC, self::CRON_HOOK_WATCHDOG, [] ); 4665 $apply_attempts++; 4666 if ( $apply_attempts >= self::MAX_APPLY_ATTEMPTS ) { 4667 // Stop infinite retries when content cannot be updated automatically. 4668 delete_post_meta( $post_id, self::META_APPLY_PENDING ); 4669 delete_post_meta( $post_id, self::META_APPLY_ATTEMPTS ); 4670 } else { 4671 update_post_meta( $post_id, self::META_APPLY_PENDING, 1 ); 4672 update_post_meta( $post_id, self::META_APPLY_ATTEMPTS, $apply_attempts ); 4673 if ( ! wp_next_scheduled( self::CRON_HOOK_WATCHDOG ) ) { 4674 wp_schedule_single_event( time() + self::POLL_INTERVAL_SEC, self::CRON_HOOK_WATCHDOG, [] ); 4675 } 4118 4676 } 4119 4677 if ( isset( $lock_set ) && $lock_set ) { … … 4130 4688 ); 4131 4689 delete_post_meta( $post_id, self::META_APPLY_PENDING ); 4690 delete_post_meta( $post_id, self::META_APPLY_ATTEMPTS ); 4132 4691 if ( isset( $lock_set ) && $lock_set ) { 4133 4692 delete_transient( $lock_key ); 4134 4693 } 4694 4135 4695 } 4136 4696 … … 4151 4711 $start_time = microtime( true ); 4152 4712 $should_reschedule = false; 4153 $remaining_slots = 10;4154 4713 4155 4714 $posts = get_posts( … … 4192 4751 if ( ! empty( $posts ) ) { 4193 4752 foreach ( $posts as $post_id ) { 4194 if ( $remaining_slots <= 0 ) {4195 $should_reschedule = true;4196 break;4197 }4198 4753 $post_id = (int) $post_id; 4199 4754 if ( $post_id <= 0 ) { … … 4219 4774 if ( 'queued' === $status ) { 4220 4775 $force = (bool) get_post_meta( $post_id, self::META_FORCE, true ); 4221 $started = $this->start_generation_for_post( $post_id, $force, $remaining_slots ); 4222 $remaining_slots = max( 0, $remaining_slots - $started ); 4776 $this->start_generation_for_post( $post_id, $force, 0, $start_time ); 4223 4777 } elseif ( 'generating' === $status ) { 4224 $processed = $this->cron_check_result( $post_id, $remaining_slots ); 4225 $remaining_slots = max( 0, $remaining_slots - $processed ); 4778 $this->cron_check_result( $post_id, 0, $start_time ); 4226 4779 } 4227 4780 4228 4781 $status_after = get_post_meta( $post_id, self::META_STATUS, true ); 4229 if ( $apply_pending ) { 4782 $apply_pending_after = (bool) get_post_meta( $post_id, self::META_APPLY_PENDING, true ); 4783 if ( $apply_pending_after ) { 4230 4784 $should_reschedule = true; 4231 4785 } elseif ( in_array( $status_after, [ 'queued', 'generating' ], true ) ) { … … 4233 4787 } 4234 4788 4235 if ( microtime( true ) - $start_time > 15 ) {4236 $should_reschedule = true;4237 break;4238 }4239 4789 } 4240 4790 } -
clipcloud-image-generation/trunk/languages/readme-da_DK.po
r3455057 r3490444 29 29 #: readme.txt:15 30 30 msgid "* Don’t download images from other sites because you want to avoid copyright claims?" 31 msgstr "* Downloader du ikke billeder fra andre sider for at undgå ophavsretskrav? **"31 msgstr "* Downloader du ikke billeder fra andre sider for at undgå ophavsretskrav?" 32 32 33 33 #: readme.txt:16 34 34 msgid "* Tired of generating images manually via ChatGPT or similar services: coming up with prompts, waiting, then uploading images by hand?" 35 msgstr "* Træt af at generere billeder manuelt via ChatGPT eller lignende: finde på prompts, vente, og så uploade billederne i hånden? *"35 msgstr "* Træt af at generere billeder manuelt via ChatGPT eller lignende: finde på prompts, vente, og så uploade billederne i hånden?" 36 36 37 37 #: readme.txt:17 38 38 msgid "* Other auto-generation plugins are too expensive or have confusing pricing?" 39 msgstr "* Er andre auto-genereringsplugins for dyre eller har uklare priser? *"39 msgstr "* Er andre auto-genereringsplugins for dyre eller har uklare priser?" 40 40 41 41 #: readme.txt:19 -
clipcloud-image-generation/trunk/languages/readme-fr_FR.po
r3455057 r3490444 4 4 msgstr "" 5 5 "PO-Revision-Date: 2025-12-15 00:00+0100\n" 6 "Last-Translator: \n" 7 "Language-Team: \n" 6 8 "MIME-Version: 1.0\n" 7 9 "Content-Type: text/plain; charset=UTF-8\n" -
clipcloud-image-generation/trunk/readme.txt
r3460288 r3490444 3 3 Tags: AI, midjourney, featured image, image generation, thumbnail 4 4 Requires at least: 6.0 5 Tested up to: 6.95 Tested up to: 7.0 6 6 Requires PHP: 7.0 7 Stable tag: 1. 1.27 Stable tag: 1.2.0 8 8 License: GPL-2.0-or-later 9 9 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 90 90 91 91 == Changelog == 92 = 1.2.0 = 93 * The image title and alt tag now contain much more useful information. This is useful for website SEO. 94 * Image generation has been significantly accelerated. 95 * Fixed a very rare issue where one image might not be generated. 96 * Fixed API key saving in some WordPress installations. 97 * Other minor fixes and improvements. 98 92 99 = 1.1.2 = 93 100 * Another improvement to automatic prompt enhancement.
Note: See TracChangeset
for help on using the changeset viewer.