0, $l <= 125 => 100, $l <= 150 => 80, $l <= 175 => 60, $l <= 200 => 20, default => 0, }; } // === Model cost & remaining helpers (Token Guard) === if ( ! function_exists('aatb_model_cost') ) { function aatb_model_cost(string $model): int { $m = strtolower(trim($model)); // Same mapping as Snippet: 4o=5, 4.1=3, mini=1 return match ($m) { 'gpt-4o' => 5, 'gpt-4.1' => 3, 'gpt-4.1-mini' => 1, default => 1, // safe default }; } } if ( ! function_exists('aatb_model_tier') ) { function aatb_model_tier(string $model): string { $m = strtolower(trim($model)); // Tiering consistent with Snippet return match ($m) { 'gpt-4o' => 'premium', 'gpt-4.1' => 'balanced', default => 'economical', }; } } if ( ! function_exists('aatb_model_alias_pack') ) { function aatb_model_alias_pack(string $model): array { $m = strtolower(trim($model)); switch ($m) { // BALANCED case 'gpt-4.1': case 'gpt-4-1': case 'gpt_4_1': case 'gpt41': case 'gpt4.1': return [ 'canonical' => 'gpt-4.1', 'engine' => 'gpt-4.1', 'family' => 'gpt41', 'aliases' => ['gpt-4.1','gpt-4-1','gpt_4_1','gpt41','gpt4.1'], ]; // PREMIUM case 'gpt-4o': case 'gpt4o': case 'o4': case '4o': return [ 'canonical' => 'gpt-4o', 'engine' => 'gpt-4o', 'family' => 'gpt4o', 'aliases' => ['gpt-4o','gpt4o','o4','4o'], ]; // ECONOMY (DEFAULT) default: return [ 'canonical' => 'gpt-4.1-mini', 'engine' => 'gpt-4.1-mini', 'family' => 'gpt4omini', 'aliases' => ['gpt-4.1-mini','gpt-4o-mini','gpt4o-mini','4o-mini','mini'], ]; } } } /** Vision-capable models */ function aatb_vision_models(): array { return [ 'gpt-4.1-mini', 'gpt-4o', 'gpt-4.1' ]; } /** Base prompt (custom text is appended if provided) */ function aatb_base_prompt( string $lang ): string { return 'You are an assistant that writes short, SEO-friendly ALT text for images in **'.$lang.'**. Rules: - Max 125 characters (hard limit). - Focus: object (≈70%), material/texture (≈20%), dominant color (≈10%). - No AI disclaimers. Avoid camera/brand names unless custom directives explicitly require them. - If custom directives are provided, they OVERRIDE the above rules (except the 125-character limit). Output: - Return ONLY JSON: {"alt_text":"..."}'; } final class AATB_Plugin { private static $ins; public static function get_instance() { return self::$ins ?: self::$ins = new self; } private function __construct() { $this->hooks(); } /* ……………………… HOOKS ……………………… */ private function hooks() { add_action( 'admin_menu', [ $this, 'menu' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'assets' ] ); add_filter( 'attachment_fields_to_edit', [ $this, 'ui' ], 10, 2 ); add_filter( 'attachment_fields_to_save', [ $this, 'save' ], 10, 2 ); add_filter( 'manage_upload_columns', [ $this, 'add_col' ] ); add_action( 'manage_media_custom_column', [ $this, 'col' ], 10, 2 ); add_action( 'wp_ajax_aatb_generate_alt', [ $this, 'ajax' ] ); add_filter( 'bulk_actions-upload', [ $this, 'bulk_items' ] ); add_filter( 'handle_bulk_actions-upload', [ $this, 'bulk_handle' ], 10, 3 ); add_action( 'admin_notices', [ $this, 'bulk_notice' ] ); add_action( 'restrict_manage_posts', [ $this, 'filter_dd' ] ); add_filter( 'posts_clauses', [ $this, 'filter_query' ], 10, 2 ); add_filter( 'plugin_action_links_' . plugin_basename(__FILE__), [ $this, 'plugin_links' ] ); add_action( 'wp_ajax_aatb_bulk_step', [ $this, 'bulk_step_ajax' ] ); add_action( 'wp_ajax_aatb_bulk_cancel', [ $this, 'bulk_cancel_ajax' ] ); add_action( 'wp_ajax_aatb_trial_register', [ $this, 'trial_register_ajax' ] ); add_action( 'wp_ajax_aatb_save_byok_key', [ $this, 'save_byok_key_ajax' ] ); add_action( 'wp_ajax_aatb_remove_byok_key', [ $this, 'remove_byok_key_ajax' ] ); add_action( 'wp_ajax_aatb_remove_trial', [ $this, 'remove_trial_ajax' ] ); add_action( 'add_attachment', [ $this, 'auto_generate_on_upload' ] ); } /* ……………………… MENU ……………………… */ public function menu() { add_menu_page( 'AI Alt Text Builder', 'AI Alt Text Builder', 'manage_options', 'ai-alt-text-builder', 'aatb_render_settings_page', plugin_dir_url(__FILE__) . 'admin/assets/img/ai-alt-text-builder.svg', 25 ); } /* ……………………… ASSETS ……………………… */ public function assets( $hook ) { $need = in_array( $hook, [ 'upload.php', 'media.php' ], true ) || ( strpos( (string) $hook, 'ai-alt-text-builder' ) !== false ) || ( $hook === 'post.php' && function_exists('get_current_screen') && ( get_current_screen()->post_type ?? '' ) === 'attachment' ); if ( ! $need ) return; wp_enqueue_style ( 'aatb-css', AATB_URL . 'admin/assets/css/admin.css', [], AATB_VERSION ); // Dynamic menu icon color fix without admin_head