Changeset 3402727
- Timestamp:
- 11/25/2025 05:25:05 PM (3 months ago)
- Location:
- alttext-ai/trunk
- Files:
-
- 7 edited
-
README.txt (modified) (2 diffs)
-
admin/class-atai-settings.php (modified) (1 diff)
-
admin/js/admin.js (modified) (1 diff)
-
admin/partials/settings.php (modified) (1 diff)
-
atai.php (modified) (2 diffs)
-
changelog.txt (modified) (1 diff)
-
includes/class-atai-attachment.php (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
alttext-ai/trunk/README.txt
r3373984 r3402727 6 6 Requires at least: 4.7 7 7 Tested up to: 6.8 8 Stable tag: 1.10.1 48 Stable tag: 1.10.15 9 9 WC requires at least: 3.3 10 10 WC tested up to: 10.1 … … 70 70 == Changelog == 71 71 72 = 1.10.1 4 - 2025-10-06=73 * Fixed: ChatGPT prompts with special characters (β, →, ₂) now save correctly74 * Improved: Real-time error messages when ChatGPT prompts are invalid75 * Improved: Live character counter shows remaining space for ChatGPT prompts72 = 1.10.15 - 2025-11-25 = 73 * Improved: WPML translations now process automatically when generating alt text for primary images 74 * Improved: Bulk generation prevents double-processing of WPML translated images 75 * Improved: Better error handling and tracking for multilingual alt text generation 76 76 77 77 = 1.10.13 - 2025-10-06 = -
alttext-ai/trunk/admin/class-atai-settings.php
r3373984 r3402727 558 558 * @param string $input The text of the GPT prompt. 559 559 * 560 * @return string Returns the prompt string if valid, otherwise returns the old value with an error message.560 * @return string Returns the prompt string if valid, otherwise an empty string. 561 561 */ 562 562 public function sanitize_gpt_prompt( $input ) { 563 // Use mb_strlen to count characters, not bytes, for UTF-8 consistency with HTML maxlength 564 // Empty strings are allowed (user wants to clear the prompt) 565 if ( empty( trim( $input ) ) ) { 563 if ( strlen($input) > 512 || strpos($input, "{{AltText}}") === false ) { 566 564 return ''; 567 565 } 568 569 // Get old value to preserve it if validation fails 570 $old_value = get_option( 'atai_gpt_prompt', '' ); 571 572 // Check if prompt is too long (character count, not bytes) 573 $char_count = mb_strlen( $input, 'UTF-8' ); 574 if ( $char_count > 512 ) { 575 add_settings_error( 576 'atai_gpt_prompt', 577 'atai_gpt_prompt_too_long', 578 sprintf( 579 __( 'ChatGPT prompt is too long (%d characters). Maximum is 512 characters.', 'alttext-ai' ), 580 $char_count 581 ), 582 'error' 583 ); 584 return $old_value; 585 } 586 587 // Check if prompt contains the required {{AltText}} macro 588 if ( strpos( $input, '{{AltText}}' ) === false ) { 589 add_settings_error( 590 'atai_gpt_prompt', 591 'atai_gpt_prompt_missing_macro', 592 __( 'ChatGPT prompt must include the {{AltText}} macro, which will be replaced with the generated alt text.', 'alttext-ai' ), 593 'error' 594 ); 595 return $old_value; 596 } 597 598 return sanitize_textarea_field( $input ); 566 else { 567 return sanitize_textarea_field($input); 568 } 599 569 } 600 570 -
alttext-ai/trunk/admin/js/admin.js
r3373984 r3402727 1882 1882 setTimeout(extendMediaTemplate, 500); 1883 1883 }); 1884 1885 /**1886 * Real-time ChatGPT prompt validation1887 */1888 document.addEventListener('DOMContentLoaded', () => {1889 const promptTextarea = document.getElementById('atai_gpt_prompt');1890 const errorsContainer = document.getElementById('atai_gpt_prompt_errors');1891 const missingMacroError = document.getElementById('atai_gpt_prompt_error_missing_macro');1892 const tooLongError = document.getElementById('atai_gpt_prompt_error_too_long');1893 const charCountSpan = document.getElementById('atai_gpt_prompt_char_count');1894 const charCounterSpan = document.getElementById('atai_gpt_prompt_char_counter');1895 1896 if (!promptTextarea || !errorsContainer) {1897 return; // Not on settings page1898 }1899 1900 function validatePrompt() {1901 const value = promptTextarea.value;1902 const charCount = value.length; // JavaScript .length counts UTF-16 code units, similar to mb_strlen1903 const hasMacro = value.includes('{{AltText}}');1904 const isTooLong = charCount > 512;1905 const isEmpty = value.trim() === '';1906 1907 // Update character counter1908 if (charCounterSpan) {1909 charCounterSpan.textContent = `${charCount}/512`;1910 1911 // Update counter color based on length1912 if (isTooLong) {1913 charCounterSpan.classList.remove('text-gray-400');1914 charCounterSpan.classList.add('text-red-500', 'font-medium');1915 } else if (charCount > 400) {1916 charCounterSpan.classList.remove('text-gray-400', 'text-red-500');1917 charCounterSpan.classList.add('text-amber-500', 'font-medium');1918 } else {1919 charCounterSpan.classList.remove('text-red-500', 'text-amber-500', 'font-medium');1920 charCounterSpan.classList.add('text-gray-400');1921 }1922 }1923 1924 // Reset state1925 let hasErrors = false;1926 missingMacroError.classList.add('hidden');1927 tooLongError.classList.add('hidden');1928 1929 // Check for missing macro (only if not empty)1930 if (!isEmpty && !hasMacro) {1931 missingMacroError.classList.remove('hidden');1932 hasErrors = true;1933 }1934 1935 // Check for length1936 if (isTooLong) {1937 charCountSpan.textContent = charCount;1938 tooLongError.classList.remove('hidden');1939 hasErrors = true;1940 }1941 1942 // Show/hide errors container1943 if (hasErrors) {1944 errorsContainer.classList.remove('hidden');1945 } else {1946 errorsContainer.classList.add('hidden');1947 }1948 1949 // Update textarea appearance1950 if (hasErrors) {1951 promptTextarea.classList.remove('ring-gray-300', 'focus:ring-primary-600');1952 promptTextarea.classList.add('ring-red-300', 'focus:ring-red-600');1953 } else {1954 promptTextarea.classList.remove('ring-red-300', 'focus:ring-red-600');1955 promptTextarea.classList.add('ring-gray-300', 'focus:ring-primary-600');1956 }1957 }1958 1959 // Validate on input (real-time)1960 promptTextarea.addEventListener('input', validatePrompt);1961 1962 // Validate on paste1963 promptTextarea.addEventListener('paste', () => {1964 // Use setTimeout to validate after paste content is inserted1965 setTimeout(validatePrompt, 0);1966 });1967 1968 // Initial validation on page load1969 validatePrompt();1970 });1971 1884 })(); -
alttext-ai/trunk/admin/partials/settings.php
r3373984 r3402727 503 503 placeholder="example: Rewrite the following text in the style of Shakespeare: {{AltText}}" 504 504 ><?php echo esc_html ( get_option( 'atai_gpt_prompt' ) ); ?></textarea> 505 506 <!-- Real-time validation error container -->507 <div id="atai_gpt_prompt_errors" class="mt-2 hidden">508 <div id="atai_gpt_prompt_error_missing_macro" class="p-3 bg-red-50 border border-red-200 rounded-md text-red-700 text-sm hidden">509 <strong><?php esc_html_e( 'Missing required macro:', 'alttext-ai' ); ?></strong>510 <?php esc_html_e( 'Your prompt must include {{AltText}} which will be replaced with the generated alt text.', 'alttext-ai' ); ?>511 </div>512 <div id="atai_gpt_prompt_error_too_long" class="p-3 bg-red-50 border border-red-200 rounded-md text-red-700 text-sm hidden">513 <strong><?php esc_html_e( 'Prompt too long:', 'alttext-ai' ); ?></strong>514 <span id="atai_gpt_prompt_char_count">0</span> <?php esc_html_e( 'characters. Maximum is 512 characters.', 'alttext-ai' ); ?>515 </div>516 </div>517 505 </div> 518 506 <p class="mt-1 text-gray-500"> 519 507 <?php esc_html_e( 'Your prompt MUST include the macro {{AltText}}, which will be substituted with the generated alt text, then sent to ChatGPT.', 'alttext-ai' ); ?> 520 <span id="atai_gpt_prompt_char_counter" class="float-right text-xs text-gray-400">0/512</span>521 508 </p> 522 509 </div> -
alttext-ai/trunk/atai.php
r3373984 r3402727 16 16 * Plugin URI: https://alttext.ai/product 17 17 * Description: Automatically generate image alt text with AltText.ai. 18 * Version: 1.10.1 418 * Version: 1.10.15 19 19 * Author: AltText.ai 20 20 * Author URI: https://alttext.ai … … 34 34 * Current plugin version. 35 35 */ 36 define( 'ATAI_VERSION', '1.10.1 4' );36 define( 'ATAI_VERSION', '1.10.15' ); 37 37 38 38 /** -
alttext-ai/trunk/changelog.txt
r3373984 r3402727 1 1 *** AltText.ai Changelog *** 2 2 3 2025-1 0-06 - version 1.10.144 * Fixed: ChatGPT prompts with special characters (β, →, ₂) now save correctly5 * Improved: Real-time error messages when ChatGPT prompts are invalid6 * Improved: Live character counter shows remaining space for ChatGPT prompts3 2025-11-25 - version 1.10.15 4 * Improved: WPML translations now process automatically when generating alt text for primary images 5 * Improved: Bulk generation prevents double-processing of WPML translated images 6 * Improved: Better error handling and tracking for multilingual alt text generation 7 7 8 8 2025-10-06 - version 1.10.13 -
alttext-ai/trunk/includes/class-atai-attachment.php
r3371885 r3402727 1073 1073 } 1074 1074 1075 // Generate alt text for primary attachment 1075 1076 $this->generate_alt( $attachment_id ); 1076 1077 1077 // Generate alt text for WPML translated versions in their respective languages 1078 // Process WPML translations if applicable 1079 $this->process_wpml_translations( $attachment_id ); 1080 } 1081 1082 /** 1083 * Process WPML translations for an attachment. 1084 * Returns success/skipped counts and processed IDs for double-processing prevention. 1085 * 1086 * @since 1.11.0 1087 */ 1088 private function process_wpml_translations( $attachment_id, $options = array() ) { 1089 $results = array( 1090 'success' => 0, 1091 'skipped' => 0, 1092 'processed_ids' => array(), 1093 ); 1094 1078 1095 if ( ! ATAI_Utility::has_wpml() ) { 1079 return ;1096 return $results; 1080 1097 } 1081 1098 1082 1099 $active_languages = apply_filters( 'wpml_active_languages', NULL ); 1083 1084 // Guard against WPML returning null/false1085 1100 if ( empty( $active_languages ) || ! is_array( $active_languages ) ) { 1086 return; 1087 } 1088 1089 $language_codes = array_keys( $active_languages ); 1090 1091 foreach ( $language_codes as $lang ) { 1092 $translated_attachment_id = apply_filters( 'wpml_object_id', $attachment_id, 'attachment', FALSE, $lang ); 1093 1094 // Ensure translated attachment exists, is different, is actually an attachment, and not trashed 1095 if ( ! $translated_attachment_id || $translated_attachment_id === $attachment_id ) { 1101 return $results; 1102 } 1103 1104 foreach ( array_keys( $active_languages ) as $lang ) { 1105 $translated_id = apply_filters( 'wpml_object_id', $attachment_id, 'attachment', FALSE, $lang ); 1106 1107 // Skip source and non-existent translations 1108 if ( ! $translated_id || (int) $translated_id === (int) $attachment_id ) { 1096 1109 continue; 1097 1110 } 1098 1111 1099 $translated_post_type = get_post_type( $translated_attachment_id );1100 $translated_post_status = get_post_status( $translated_attachment_id );1101 1102 if ( 'attachment' !== $translated_post_type || 'trash' === $translated_post_status ) {1112 // Skip invalid or trashed 1113 if ( get_post_type( $translated_id ) !== 'attachment' || get_post_status( $translated_id ) === 'trash' ) { 1114 $results['skipped']++; 1115 $results['processed_ids'][ $translated_id ] = 'skipped'; 1103 1116 continue; 1104 1117 } 1105 1118 1106 // Pass language explicitly to avoid timing issues with WPML metadata 1107 // Note: force_lang setting is enforced inside generate_alt() if enabled 1108 $this->generate_alt( $translated_attachment_id, null, array( 'lang' => $lang ) ); 1109 } 1119 $response = $this->generate_alt( $translated_id, null, array_merge( $options, array( 'lang' => $lang ) ) ); 1120 1121 if ( $this->is_generation_error( $response ) ) { 1122 $results['skipped']++; 1123 $results['processed_ids'][ $translated_id ] = 'error'; 1124 } else { 1125 $results['success']++; 1126 $results['processed_ids'][ $translated_id ] = 'success'; 1127 } 1128 } 1129 1130 return $results; 1131 } 1132 1133 /** 1134 * Check if a generate_alt response is an error. 1135 * 1136 * @since 1.11.0 1137 */ 1138 private function is_generation_error( $response ) { 1139 if ( is_wp_error( $response ) ) { 1140 return true; 1141 } 1142 if ( ! is_string( $response ) || $response === '' ) { 1143 return true; 1144 } 1145 return ( 1146 0 === strpos( $response, 'error_' ) || 1147 0 === strpos( $response, 'invalid_' ) || 1148 in_array( $response, array( 'insufficient_credits', 'url_access_error' ), true ) 1149 ); 1110 1150 } 1111 1151 … … 1156 1196 $images_successful = $images_skipped = $loop_count = 0; 1157 1197 $processed_ids = array(); // Track processed IDs for bulk-select cleanup 1198 $wpml_processed_ids = array(); // Track WPML translation IDs to prevent double-processing 1158 1199 1159 1200 … … 1294 1335 1295 1336 foreach ( $images_to_update as $image ) { 1296 $attachment_id = ( $mode === 'bulk-select' ) ? $image : $image->post_id;1297 1337 $attachment_id = absint( ( $mode === 'bulk-select' ) ? $image : $image->post_id ); 1338 1298 1339 if ( defined( 'ATAI_BULK_DEBUG' ) ) { 1299 1340 ATAI_Utility::log_error( sprintf("BulkGenerate: Attachment ID %d", $attachment_id) ); 1300 1341 } 1301 1342 1343 // Skip if already processed as WPML translation (prevents double-processing) 1344 if ( in_array( $attachment_id, $wpml_processed_ids, true ) ) { 1345 // Don't increment images_skipped to avoid double-counting in stats 1346 $skip_reasons['wpml_already_processed'] = ($skip_reasons['wpml_already_processed'] ?? 0) + 1; 1347 $last_post_id = $attachment_id; 1348 1349 if ( $mode === 'bulk-select' ) { 1350 $processed_ids[] = $attachment_id; 1351 } 1352 1353 if ( ++$loop_count >= $query_limit ) { 1354 break; 1355 } 1356 continue; 1357 } 1358 1302 1359 // Skip if attachment is not eligible 1303 1360 if ( ! $this->is_attachment_eligible( $attachment_id, 'bulk' ) ) { … … 1369 1426 if ( is_string( $response ) && $response !== '' && ! $is_error_code ) { 1370 1427 $images_successful++; 1428 1429 // Process WPML translations for successfully generated primary images 1430 // Note: Translation stats are NOT added to main counters to keep success_count 1431 // aligned with process_count (primary attachments only) 1432 $wpml_results = $this->process_wpml_translations( $attachment_id, array( 1433 'keywords' => $keywords, 1434 'negative_keywords' => $negative_keywords, 1435 ) ); 1436 1437 // Track all WPML translation IDs to prevent double-processing later in the loop 1438 if ( ! empty( $wpml_results['processed_ids'] ) ) { 1439 $wpml_processed_ids = array_merge( $wpml_processed_ids, array_keys( $wpml_results['processed_ids'] ) ); 1440 } 1371 1441 } else { 1372 1442 // API call failed - track the reason … … 1458 1528 $this->generate_alt( $attachment_id ); 1459 1529 1530 // Process WPML translations 1531 $this->process_wpml_translations( $attachment_id ); 1532 1460 1533 // Redirect back to edit page 1461 1534 wp_safe_redirect( wp_get_referer() ); … … 1501 1574 1502 1575 if ( ! is_array( $response ) && $response !== false ) { 1576 // Process WPML translations for successfully generated primary image 1577 $wpml_results = $this->process_wpml_translations( $attachment_id, array( 1578 'keywords' => $keywords, 1579 ) ); 1580 1503 1581 wp_send_json( array( 1504 1582 'status' => 'success', 1505 1583 'alt_text' => $response, 1584 'wpml_success' => $wpml_results['success'], 1506 1585 ) ); 1507 1586 }
Note: See TracChangeset
for help on using the changeset viewer.