✓' . esc_html__( 'Included', 'ai-alt-text-builder' ) . ''; } else { echo '' . esc_html__( 'Not included', 'ai-alt-text-builder' ) . ''; } } function aatb_render_settings_page() { $st = []; $opt = get_option( 'aatb_settings', [] ); $token = $opt['site_token'] ?? ''; $model = $opt['model_choice'] ?? 'gpt-4.1-mini'; $lang = $opt['lang_choice'] ?? 'en'; $mode = function_exists( 'aatb_active_mode' ) ? aatb_active_mode() : 'none'; /* ───────────── FORM SUBMISSIONS (nonce-protected) ───────────── */ if ( isset($_POST['aatb_action']) ) { $action = sanitize_key( wp_unslash( $_POST['aatb_action'] ) ); if ( ! check_admin_referer( $action ) ) { wp_die( 'Bad nonce' ); } if ( ! current_user_can( 'manage_options' ) ) { wp_die( 'Unauthorized' ); } /* Save Site Key (Pro) */ if ( $action === 'aatb_save_site_key' ) { $new = sanitize_text_field( wp_unslash( $_POST['site_token'] ?? '' ) ); // A key copied from email, a PDF or a chat can carry invisible characters (no-break or // zero-width spaces, a byte order mark) or wrapping quotes or backticks at either end. // No valid key starts or ends with them, so they are removed before the check. $new = (string) preg_replace( '/^[\s\p{Z}\p{Cf}`"\']+|[\s\p{Z}\p{Cf}`"\']+$/u', '', $new ); $chk = $new ? aatb_check_token_status( $new ) : [ 'error' => 'empty' ]; if ( ! empty( $chk['error'] ) ) { // The key already saved stays: a typo or a short outage must not remove a working key // and leave the free trial or an OpenAI key in charge of this site. echo '

Site Key Error: ' . esc_html( (string)$chk['error'] ) . '

'; } else { $opt['site_token'] = $new; $is_edu_key = '' !== aatb_institution_slug( $new ); if ( $is_edu_key ) { // An institutional site never falls back to the shared trial or to OpenAI, even if // its key were removed later, so those credentials are cleared. $opt['trial_token'] = ''; $opt['trial_remaining'] = 0; $opt['openai_key'] = ''; } $saved_msg = $is_edu_key ? __( 'Site key saved. Institutional plan activated.', 'ai-alt-text-builder' ) : __( 'Site Key saved. Pro mode activated!', 'ai-alt-text-builder' ); echo '

' . esc_html( $saved_msg ) . '

'; } update_option( 'aatb_settings', $opt ); $token = $opt['site_token'] ?? ''; $mode = function_exists( 'aatb_active_mode' ) ? aatb_active_mode() : 'none'; } /* Alt-Text settings (model + language + custom prompt + auto_generate) */ if ( $action === 'aatb_save_alt_settings' ) { $allowed = ['gpt-4.1-mini','gpt-4o','gpt-4.1']; $chosen = sanitize_text_field( wp_unslash( $_POST['model_choice'] ?? 'gpt-4.1-mini' ) ); $opt['model_choice'] = in_array( $chosen, $allowed, true ) ? $chosen : 'gpt-4.1-mini'; $sel = sanitize_text_field( wp_unslash( $_POST['lang_choice'] ?? 'en' ) ); $opt['lang_choice'] = ( $sel === 'other' ) ? sanitize_text_field( wp_unslash( $_POST['lang_custom'] ?? 'en' ) ) : $sel; // The field itself stops at 1,500 characters (maxlength); this cut only catches a form // sent without it. Line breaks arrive as \r\n but count as one character in the field. $cp = str_replace( "\r\n", "\n", sanitize_textarea_field( wp_unslash( $_POST['custom_prompt'] ?? '' ) ) ); $cut = mb_strlen( $cp ) > 1500; if ( $cut ) { $cp = mb_substr( $cp, 0, 1500 ); } $opt['custom_prompt'] = $cp; $opt['auto_generate'] = ! empty( $_POST['auto_generate'] ) ? '1' : ''; update_option( 'aatb_settings', $opt ); $model = $opt['model_choice']; $lang = $opt['lang_choice']; if ( $cut ) { echo '

' . esc_html__( 'Settings saved. Custom directives were shortened to the 1,500-character limit.', 'ai-alt-text-builder' ) . '

'; } else { echo '

Settings saved.

'; } } } /* ───────────── PRO MODE: TOKEN / USAGE NORMALIZATION ───────────── */ $usage=$limit=$remain=$site_usage=null; $membership='free'; $allowed_models_ui = ['gpt-4.1-mini','gpt-4o','gpt-4.1']; // Institution slug of an rpedu_ key (requests go to the institution's own API), '' otherwise. $edu_slug = ( $token && $mode === 'pro' ) ? aatb_institution_slug( (string) $token ) : ''; if ( $token && $mode === 'pro' ) { $st = aatb_check_token_status( $token ); if ( empty( $st['error'] ) ) { $site_usage = (int)($st['site_usage'] ?? 0); $limit = (int)( $st['plan_limit_total'] ?? $st['plan_limit'] ?? $st['token_limit'] ?? 0 ); $usage_total = null; foreach ( ['usage_total','all_usage','total_usage','token_usage','usage'] as $k ) { if ( isset($st[$k]) ) { $usage_total = (int)$st[$k]; break; } } if ( $usage_total === null ) { $usage_total = max(0, (int)$site_usage); } $remain = aatb_status_remaining( $st ); $usage = (int)$usage_total; $membership = strtolower( trim( $st['membership'] ?? 'free' ) ); if ( ! empty( $st['allowed_models'] ) && is_array( $st['allowed_models'] ) ) { $allowed_models_ui = array_values( array_intersect( $allowed_models_ui, $st['allowed_models'] ) ); if ( empty($allowed_models_ui) ) { $allowed_models_ui = ['gpt-4.1-mini']; } } } $canonical = ['gpt-4.1-mini','gpt-4o','gpt-4.1']; $gate = [ 'free' => ['gpt-4.1-mini'], 'starter' => ['gpt-4.1-mini','gpt-4.1'], 'creator' => ['gpt-4.1-mini','gpt-4.1','gpt-4o'], 'expert' => ['gpt-4.1-mini','gpt-4.1','gpt-4o'], ]; // 'institutional' is left out on purpose: its tiers are the allowed_models // intersection above, so all three show when the institution's API lists them. // When the institution's API cannot be reached, $membership is only the 'free' fallback. // An institutional plan then keeps its tiers and its saved choice instead of being reset // to Economical on every page view. Other keys keep this gate as before. if ( empty( $st['error'] ) || '' === $edu_slug ) { if ( isset($gate[$membership]) ) { $allowed_models_ui = array_values( array_intersect($canonical, $gate[$membership]) ); } if ( ! in_array($model, $allowed_models_ui, true) ) { $model = 'gpt-4.1-mini'; $opt['model_choice'] = $model; update_option('aatb_settings', $opt); } } } $trial_remaining = (int) ( $opt['trial_remaining'] ?? 0 ); $byok_key_masked = ''; if ( ! empty( $opt['openai_key'] ) ) { $k = $opt['openai_key']; $byok_key_masked = substr( $k, 0, 7 ) . str_repeat( '*', max( 0, strlen($k) - 11 ) ) . substr( $k, -4 ); } $is_pro = $mode === 'pro'; // Institutional plan: an rpedu_ key (requests go to the institution's own API) or the // "institutional" membership from token-info. Shown as "Institutional" instead of "Pro". $is_institutional = $is_pro && ( '' !== $edu_slug || 'institutional' === $membership ); // The institution's API counts images, not tokens: every tier uses one image of the plan. $edu_cost = '' !== $edu_slug ? __( '1 image from your plan', 'ai-alt-text-builder' ) : ''; ?>
AI Alt Text Builder

AI Alt Text Builder

Media Library ALT Text Generator

__( 'Free trial', 'ai-alt-text-builder' ), 'byok' => __( 'Own API key', 'ai-alt-text-builder' ), 'pro' => $is_institutional ? __( 'Institutional', 'ai-alt-text-builder' ) : __( 'Pro', 'ai-alt-text-builder' ) ]; ?> v

Getting Started

Choose how you want to generate alt text for your images.

Your plan

Mode, usage and key for this site.

FREE

Free Trial

Start instantly with 25 free alt text generations. No API key needed.

  • 25 free generations
  • Economical quality tier
  • English only
  • One image at a time
  • Alt text quality labels

No credit card required

UNLIMITED

Your Own API Key

Use your own OpenAI API key. You pay OpenAI directly, we don't touch your key.

  • Unlimited generations
  • OpenAI GPT-4.1 mini with your key
  • English only
  • One image at a time
  • Alt text quality labels

Your key stays on your server

PRO

RankPilotAI Pro

Full power with zero API hassle. We handle everything, just use your tokens.

  • No API key needed
  • Bulk generation
  • Auto-generate on upload
  • All quality tiers
  • All languages
  • Custom directives
  • Alt text quality labels

Get a site key

Free Trial Active

of 25 tokens remaining

/ 25

Your trial tokens are used up. Choose one of these options to continue:

Upgrade to Pro

OpenAI API Key Active

Unlimited single image generations

Full access to all features

Connected
Website
Used
Limit
Remaining
% used
8 ? substr( $token, 0, 4 ) . str_repeat( '•', 8 ) . substr( $token, -4 ) : str_repeat( '•', 8 ); ?>

Alt text settings

Configure how AI generates alt text for your images.

Quality tier

Economical only

Alt text language

English only

Select "Other / Custom" and type the language exactly as you'd like it.

English All languages with Pro

Custom directives

PRO

Optional, up to 1,500 characters. Add brand or style guidance. Your note is appended to the built-in instructions and takes priority over general rules; the alt text itself always stays within 125 characters.

Add custom brand or style guidance to your alt text generations.

Available with Pro

Auto-generate on upload

PRO

Automatically generate alt text every time you upload an image.

Available with Pro

Compare modes

See what's available in each mode.

Feature Free Trial Own API Key Pro
No API key needed Your OpenAI key We handle it
One-click generation
Bulk generation
Auto-generate on upload
Alt text quality labels
Languages English English All languages
Quality tiers Economical GPT-4.1 mini Economical / Balanced / Premium
Custom directives
Generations 25 lifetime Unlimited Plan-based