'', 'limit' => 50, 'remaining' => null, 'resets_at' => '', 'pro' => false, ), $state ); } /** * Saves registration and quota. * * @param array $state State. */ private static function save_state( array $state ) { update_option( self::OPTION, $state, false ); } /** * Registers this site with the service. * * @return string|\WP_Error Token. */ public static function register() { $response = wp_remote_post( self::base_url() . '/v1/sites', array( 'timeout' => 20, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'site_url' => home_url( '/' ), 'version' => FIXLIGHT_VERSION, ) ), ) ); $error = Ai::http_error( $response, 'Fixlight' ); if ( $error ) { return $error; } $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( empty( $data['token'] ) ) { return new \WP_Error( 'fixlight_hosted_register', __( 'The Fixlight service did not register this site. Try again later.', 'fixlight' ), array( 'status' => 502 ) ); } self::save_state( array( 'token' => (string) $data['token'], 'limit' => isset( $data['limit'] ) ? (int) $data['limit'] : 50, 'remaining' => isset( $data['remaining'] ) ? (int) $data['remaining'] : null, 'resets_at' => isset( $data['resets_at'] ) ? sanitize_text_field( (string) $data['resets_at'] ) : '', ) ); return (string) $data['token']; } /** * Asks the service for alt text. * * @param array $image mime, data (base64). * @param array $context language, text. * @param string $model Unused; the service picks the model. * @param bool $retry Whether a stale token may be replaced once. * @return array|\WP_Error alt, decorative, model, quota. */ public static function describe( array $image, array $context, $model = '', $retry = true ) { $state = self::state(); $token = $state['token']; if ( '' === $token ) { $token = self::register(); if ( is_wp_error( $token ) ) { return $token; } } $response = wp_remote_post( self::base_url() . '/v1/alt', array( 'timeout' => 45, /** * Filters the headers sent to the Fixlight alt text service. * Fixlight Pro adds its site token here, so its plan's allowance applies. * * @param array $headers Request headers. */ 'headers' => (array) apply_filters( 'fixlight_ai_hosted_headers', array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $token, ) ), 'body' => wp_json_encode( array( 'image' => $image['data'], 'mime' => $image['mime'], 'context' => $context['text'], 'language' => $context['language'], ) ), ) ); $code = is_wp_error( $response ) ? 0 : (int) wp_remote_retrieve_response_code( $response ); $data = is_wp_error( $response ) ? array() : json_decode( wp_remote_retrieve_body( $response ), true ); $data = is_array( $data ) ? $data : array(); if ( 401 === $code && $retry ) { // The token is unknown (for example after the site moved): register again once. self::save_state( array_merge( $state, array( 'token' => '' ) ) ); return self::describe( $image, $context, $model, false ); } if ( isset( $data['remaining'] ) ) { $state = array_merge( self::state(), array( 'remaining' => (int) $data['remaining'], 'limit' => isset( $data['limit'] ) ? (int) $data['limit'] : 50, 'resets_at' => isset( $data['resets_at'] ) ? sanitize_text_field( (string) $data['resets_at'] ) : '', 'pro' => ! empty( $data['pro'] ), ) ); self::save_state( $state ); } if ( 429 === $code && isset( $data['error'] ) && 'quota' === $data['error'] ) { $date = '' !== $state['resets_at'] ? date_i18n( get_option( 'date_format' ), strtotime( $state['resets_at'] ) ) : ''; if ( ! empty( $data['pro'] ) ) { return new \WP_Error( 'fixlight_hosted_quota', sprintf( /* translators: 1: number of images, 2: date. */ __( 'Your Fixlight Pro plan\'s %1$d AI descriptions for this month are used. They reset on %2$s. You can still write alt text yourself.', 'fixlight' ), (int) $state['limit'], $date ) . self::connectors_hint(), array( 'status' => 429 ) ); } return new \WP_Error( 'fixlight_hosted_quota', sprintf( /* translators: 1: number of images, 2: date. */ __( 'This site has used its %1$d free AI descriptions for this month. They reset on %2$s. You can still write alt text yourself.', 'fixlight' ), (int) $state['limit'], $date ) . self::connectors_hint(), array( 'status' => 429 ) ); } $error = Ai::http_error( $response, 'Fixlight' ); if ( $error ) { return $error; } if ( ! array_key_exists( 'alt', $data ) ) { return new \WP_Error( 'fixlight_ai_parse', __( 'The Fixlight service returned an answer Fixlight could not read. Try again.', 'fixlight' ), array( 'status' => 502 ) ); } return array( 'alt' => (string) $data['alt'], 'decorative' => ! empty( $data['decorative'] ), 'model' => isset( $data['model'] ) ? sanitize_text_field( (string) $data['model'] ) : 'fixlight', 'quota' => array( 'remaining' => $state['remaining'], 'limit' => $state['limit'], 'resets_at' => $state['resets_at'], ), ); } /** * A pointer to the AI connected to WordPress, where this WordPress has it. * * @return string */ private static function connectors_hint() { return Ai_WordPress::available() ? ' ' . __( 'Or choose the AI connected to WordPress in Fixlight → Settings for unlimited descriptions.', 'fixlight' ) : ''; } }