] * : Generate audio for specific post ID * * [--post-type=] * : Generate audio for all posts of specific type * * [--limit=] * : Limit number of posts to process * * [--force] * : Force regeneration of existing audio * * [--dry-run] * : Show what would be processed without actually generating * * ## EXAMPLES * * wp aat generate --post-id=123 * wp aat generate --post-type=post --limit=10 * wp aat generate --post-type=page --force * * @param array $args Positional arguments. * @param array $assoc_args Associative arguments. * @since 1.0.6 */ public function generate( $args, $assoc_args ) { $post_id = isset( $assoc_args['post-id'] ) ? intval( $assoc_args['post-id'] ) : null; $post_type = isset( $assoc_args['post-type'] ) ? sanitize_text_field( $assoc_args['post-type'] ) : 'post'; $limit = isset( $assoc_args['limit'] ) ? intval( $assoc_args['limit'] ) : -1; $force = isset( $assoc_args['force'] ); $dry_run = isset( $assoc_args['dry-run'] ); if ( $post_id ) { // Generate for specific post. $this->generate_single_post( $post_id, $force, $dry_run ); } else { // Generate for post type. $this->generate_post_type( $post_type, $limit, $force, $dry_run ); } } /** * Show audio generation status * * ## OPTIONS * * [--post-id=] * : Show status for specific post ID * * [--summary] * : Show summary statistics * * ## EXAMPLES * * wp aat status --post-id=123 * wp aat status --summary * * @param array $args Positional arguments. * @param array $assoc_args Associative arguments. * @since 1.0.6 */ public function status( $args, $assoc_args ) { $post_id = isset( $assoc_args['post-id'] ) ? intval( $assoc_args['post-id'] ) : null; $summary = isset( $assoc_args['summary'] ); if ( $post_id ) { $this->show_post_status( $post_id ); } elseif ( $summary ) { $this->show_summary_status(); } else { \WP_CLI::error( 'Please specify --post-id or --summary' ); } } /** * Manage plugin cache * * ## OPTIONS * * * : Action to perform (clear, status, size) * * [--type=] * : Cache type (audio, static, all) * * ## EXAMPLES * * wp aat cache clear * wp aat cache status * wp aat cache size --type=audio * * @param array $args Positional arguments. * @param array $assoc_args Associative arguments. * @since 1.0.6 */ public function cache( $args, $assoc_args ) { if ( empty( $args[0] ) ) { \WP_CLI::error( 'Please specify an action: clear, status, or size' ); } $action = sanitize_text_field( $args[0] ); $type = isset( $assoc_args['type'] ) ? sanitize_text_field( $assoc_args['type'] ) : 'all'; switch ( $action ) { case 'clear': $this->clear_cache( $type ); break; case 'status': $this->show_cache_status( $type ); break; case 'size': $this->show_cache_size( $type ); break; default: \WP_CLI::error( "Unknown action: $action" ); } } /** * Show plugin statistics * * ## OPTIONS * * [--period=] * : Time period (day, week, month, year) * * [--format=] * : Output format (table, json, csv) * * ## EXAMPLES * * wp aat stats --period=month * wp aat stats --format=json * * @param array $args Positional arguments. * @param array $assoc_args Associative arguments. * @since 1.0.6 */ public function stats( $args, $assoc_args ) { $period = isset( $assoc_args['period'] ) ? sanitize_text_field( $assoc_args['period'] ) : 'month'; $format = isset( $assoc_args['format'] ) ? sanitize_text_field( $assoc_args['format'] ) : 'table'; $this->show_statistics( $period, $format ); } /** * Manage plugin settings * * ## OPTIONS * * * : Action to perform (get, set, list) * * [--key=] * : Setting key * * [--value=] * : Setting value * * ## EXAMPLES * * wp aat settings list * wp aat settings get --key=post_types * wp aat settings set --key=auto_generate --value=true * * @param array $args Positional arguments. * @param array $assoc_args Associative arguments. * @since 1.0.6 */ public function settings( $args, $assoc_args ) { if ( empty( $args[0] ) ) { \WP_CLI::error( 'Please specify an action: get, set, or list' ); } $action = sanitize_text_field( $args[0] ); switch ( $action ) { case 'list': $this->list_settings(); break; case 'get': $key = isset( $assoc_args['key'] ) ? sanitize_text_field( $assoc_args['key'] ) : null; if ( ! $key ) { \WP_CLI::error( 'Please specify --key' ); } $this->get_setting( $key ); break; case 'set': $key = isset( $assoc_args['key'] ) ? sanitize_text_field( $assoc_args['key'] ) : null; $value = isset( $assoc_args['value'] ) ? $assoc_args['value'] : null; if ( ! $key || null === $value ) { \WP_CLI::error( 'Please specify both --key and --value' ); } $this->set_setting( $key, $value ); break; default: \WP_CLI::error( "Unknown action: $action" ); } } /** * Inspect or advance the private-audio migration without contacting a provider. * * ## OPTIONS * * [] * : status (default), or run. * * [--batches=] * : Maximum bounded batches to run, from 1 to 100. Defaults to 1. * * ## EXAMPLES * * wp accessibility-audio-tts migration status * wp accessibility-audio-tts migration run --batches=100 * * @param array $args Positional arguments. * @param array $assoc_args Named arguments. */ public function migration( $args, $assoc_args ) { $action = $args[0] ?? 'status'; if ( ! in_array( $action, array( 'status', 'run' ), true ) ) { \WP_CLI::error( 'Specify migration status or migration run.' ); return; } $status = \Accessibility_Audio_TTS\Core\Audio_Access::migration_status(); if ( 'run' === $action ) { $raw = $assoc_args['batches'] ?? '1'; $batches = filter_var( $raw, FILTER_VALIDATE_INT, array( 'options' => array( 'min_range' => 1, 'max_range' => 100 ) ) ); if ( false === $batches ) { \WP_CLI::error( '--batches must be an integer between 1 and 100.' ); return; } for ( $i = 0; $i < $batches && ! $status['complete']; ++$i ) { $status = \Accessibility_Audio_TTS\Core\Private_Audio_Migration::run_batch( true ); if ( ! empty( $status['busy'] ) || ! empty( $status['failed_in_batch'] ) || ! empty( $status['error'] ) ) { break; } } } \WP_CLI::line( wp_json_encode( $status ) ); if ( 'run' === $action && ! $status['complete'] ) { \WP_CLI::warning( 'Migration is incomplete. Keep protected content in maintenance, resolve any storage errors and run another bounded batch.' ); } } /** * Generate audio for single post * * @param int $post_id Post ID. * @param bool $force Force regeneration. * @param bool $dry_run Dry run mode. * @since 1.0.6 */ private function generate_single_post( $post_id, $force, $dry_run ) { $post = get_post( $post_id ); if ( ! $post ) { \WP_CLI::error( "Post with ID $post_id not found" ); } $audio_url = get_post_meta( $post_id, 'accessibility_audio_tts_url', true ); if ( $audio_url && ! $force ) { \WP_CLI::warning( "Post $post_id already has audio. Use --force to regenerate." ); return; } if ( $dry_run ) { \WP_CLI::log( "Would generate audio for post: $post_id - {$post->post_title}" ); return; } \WP_CLI::log( "Generating audio for post: $post_id - {$post->post_title}" ); $result = \Accessibility_Audio_TTS\Admin\Conversion_Processor::run_generation_job( $post_id, $force ); if ( empty( $result['success'] ) ) { \WP_CLI::error( $result['error'] ?? "Audio generation failed for post $post_id" ); } \WP_CLI::success( "Audio generated for post $post_id" ); } /** * Generate audio for post type * * @param string $post_type Post type. * @param int $limit Limit number of posts. * @param bool $force Force regeneration. * @param bool $dry_run Dry run mode. * @since 1.0.6 */ private function generate_post_type( $post_type, $limit, $force, $dry_run ) { $query_args = array( 'post_type' => $post_type, 'post_status' => 'publish', 'posts_per_page' => $limit, 'fields' => 'ids', ); if ( ! $force ) { $query_args['meta_query'] = array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- required to find posts without audio array( 'key' => 'accessibility_audio_tts_url', 'compare' => 'NOT EXISTS', ), ); } $posts = get_posts( $query_args ); if ( empty( $posts ) ) { \WP_CLI::warning( "No posts found for post type: $post_type" ); return; } $total = count( $posts ); \WP_CLI::log( "Found $total posts to process" ); if ( $dry_run ) { foreach ( $posts as $post_id ) { $post = get_post( $post_id ); \WP_CLI::log( "Would generate audio for: $post_id - {$post->post_title}" ); } return; } $progress = \WP_CLI\Utils\make_progress_bar( 'Generating audio', $total ); $failed = 0; foreach ( $posts as $post_id ) { $result = \Accessibility_Audio_TTS\Admin\Conversion_Processor::run_generation_job( $post_id, $force ); if ( empty( $result['success'] ) ) { ++$failed; \WP_CLI::warning( "Post $post_id: " . ( $result['error'] ?? 'Audio generation failed' ) ); } $progress->tick(); } $progress->finish(); if ( $failed ) { \WP_CLI::error( "Audio generation failed for $failed of $total posts" ); } \WP_CLI::success( "Audio generated for $total posts" ); } /** * Show post status * * @param int $post_id Post ID. * @since 1.0.6 */ private function show_post_status( $post_id ) { $post = get_post( $post_id ); if ( ! $post ) { \WP_CLI::error( "Post with ID $post_id not found" ); } $audio_url = get_post_meta( $post_id, 'accessibility_audio_tts_url', true ); $generation_status = get_post_meta( $post_id, 'accessibility_audio_tts_generation_status', true ); $generation_date = get_post_meta( $post_id, 'accessibility_audio_tts_generation_date', true ); $generation_error = get_post_meta( $post_id, 'accessibility_audio_tts_generation_error', true ); $data = array( array( 'Key', 'Value' ), array( 'Post ID', $post_id ), array( 'Title', $post->post_title ), array( 'Has Audio', $audio_url ? 'Yes' : 'No' ), array( 'Audio URL', $audio_url ? $audio_url : 'N/A' ), array( 'Generation Status', $generation_status ? $generation_status : 'Not Started' ), array( 'Generation Date', $generation_date ? $generation_date : 'N/A' ), array( 'Error', $generation_error ? $generation_error : 'None' ), ); \WP_CLI\Utils\format_items( 'table', $data, array( 'Key', 'Value' ) ); } /** * Show summary status * * @since 1.0.6 */ private function show_summary_status() { $total_posts = wp_count_posts()->publish; $posts_with_audio = get_posts( array( 'post_type' => 'any', 'post_status' => 'publish', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- normalized for consistency array( 'key' => 'accessibility_audio_tts_url', 'compare' => 'EXISTS', ), ), 'numberposts' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); $posts_processing = get_posts( array( 'post_type' => 'any', 'post_status' => 'publish', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- normalized for consistency array( 'key' => 'accessibility_audio_tts_generation_status', 'value' => 'processing', 'compare' => '=', ), ), 'numberposts' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); $posts_failed = get_posts( array( 'post_type' => 'any', 'post_status' => 'publish', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- normalized for consistency array( 'key' => 'accessibility_audio_tts_generation_status', 'value' => 'failed', 'compare' => '=', ), ), 'numberposts' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); $data = array( array( 'Metric', 'Count' ), array( 'Total Published Posts', $total_posts ), array( 'Posts with Audio', count( $posts_with_audio ) ), array( 'Posts Processing', count( $posts_processing ) ), array( 'Posts Failed', count( $posts_failed ) ), array( 'Coverage', round( ( count( $posts_with_audio ) / $total_posts ) * 100, 2 ) . '%' ), ); \WP_CLI\Utils\format_items( 'table', $data, array( 'Metric', 'Count' ) ); } /** * Clear cache * * @param string $type Cache type. * @since 1.0.6 */ private function clear_cache( $type ) { switch ( $type ) { case 'audio': $this->clear_audio_cache(); break; case 'static': wp_cache_flush(); break; case 'all': default: $this->clear_audio_cache(); wp_cache_flush(); break; } \WP_CLI::success( "Cache cleared: $type" ); } /** * Clear audio cache * * @since 1.0.6 */ private function clear_audio_cache() { delete_transient( 'accessibility_audio_tts_stats' ); delete_transient( 'accessibility_audio_tts_settings' ); // Clear any other plugin-specific transients. global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- CLI maintenance: clearing transients by prefix $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_accessibility_audio_tts_%' OR option_name LIKE '_transient_timeout_accessibility_audio_tts_%'" ); } /** * Show cache status * * @param string $type Cache type. * @since 1.0.6 */ private function show_cache_status( $type ) { $upload_dir = wp_upload_dir(); $audio_dir = $upload_dir['basedir'] . '/accessibility-audio-tts/'; $data = array( array( 'Type', 'Status', 'Size', 'Files' ) ); if ( 'audio' === $type || 'all' === $type ) { $audio_stats = $this->get_directory_stats( $audio_dir ); $data[] = array( 'Audio Cache', is_dir( $audio_dir ) ? 'Active' : 'Inactive', $this->format_bytes( $audio_stats['size'] ), $audio_stats['files'], ); } if ( 'static' === $type || 'all' === $type ) { $data[] = array( 'Object Cache', wp_using_ext_object_cache() ? 'External' : 'Internal', 'N/A', 'N/A', ); } \WP_CLI\Utils\format_items( 'table', $data, array( 'Type', 'Status', 'Size', 'Files' ) ); } /** * Show cache size * * @param string $type Cache type. * @since 1.0.6 */ private function show_cache_size( $type ) { $upload_dir = wp_upload_dir(); $audio_dir = $upload_dir['basedir'] . '/accessibility-audio-tts/'; if ( 'audio' === $type || 'all' === $type ) { $stats = $this->get_directory_stats( $audio_dir ); \WP_CLI::log( "Audio cache: {$this->format_bytes($stats['size'])} ({$stats['files']} files)" ); } if ( 'static' === $type || 'all' === $type ) { \WP_CLI::log( 'Object cache: ' . ( wp_using_ext_object_cache() ? 'External cache active' : 'Internal cache only' ) ); } } /** * Show statistics * * @param string $period Time period. * @param string $format Output format. * @since 1.0.6 */ private function show_statistics( $period, $format ) { // Get basic stats. $posts_with_audio = get_posts( array( 'post_type' => 'any', 'post_status' => 'publish', 'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- normalized for consistency array( 'key' => 'accessibility_audio_tts_url', 'compare' => 'EXISTS', ), ), 'numberposts' => -1, 'fields' => 'ids', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); $stats = array( 'period' => $period, 'posts_with_audio' => count( $posts_with_audio ), 'total_listens' => 0, 'average_duration' => 0, ); // Get listening stats if available. if ( class_exists( '\Accessibility_Audio_TTS\Admin\Listening_Statistics' ) ) { $listening_stats = new \Accessibility_Audio_TTS\Admin\Listening_Statistics(); $listen_data = $listening_stats->get_period_stats( $period ); $stats['total_listens'] = $listen_data['total_listens'] ?? 0; $stats['average_duration'] = $listen_data['average_duration'] ?? 0; } if ( 'json' === $format ) { \WP_CLI::log( wp_json_encode( $stats, JSON_PRETTY_PRINT ) ); } else { $data = array( array( 'Metric', 'Value' ), array( 'Period', ucfirst( $period ) ), array( 'Posts with Audio', $stats['posts_with_audio'] ), array( 'Total Listens', $stats['total_listens'] ), array( 'Average Duration', $stats['average_duration'] . 's' ), ); \WP_CLI\Utils\format_items( 'table', $data, array( 'Metric', 'Value' ) ); } } /** * List all settings * * @since 1.0.6 */ private function list_settings() { $settings = get_option( 'accessibility_audio_tts_settings', array() ); if ( empty( $settings ) ) { \WP_CLI::warning( 'No settings found' ); return; } $data = array( array( 'Key', 'Value', 'Type' ) ); foreach ( $settings as $key => $value ) { // Hide sensitive data. if ( in_array( $key, array( 'api_key', 'license_key' ), true ) ) { $value = '***HIDDEN***'; } elseif ( is_array( $value ) ) { $value = implode( ', ', $value ); } elseif ( is_bool( $value ) ) { $value = $value ? 'true' : 'false'; } $data[] = array( $key, $value, gettype( $settings[ $key ] ) ); } \WP_CLI\Utils\format_items( 'table', $data, array( 'Key', 'Value', 'Type' ) ); } /** * Get specific setting * * @param string $key Setting key. * @since 1.0.6 */ private function get_setting( $key ) { $settings = get_option( 'accessibility_audio_tts_settings', array() ); if ( ! array_key_exists( $key, $settings ) ) { \WP_CLI::error( "Setting '$key' not found" ); } $value = $settings[ $key ]; if ( is_array( $value ) ) { \WP_CLI::log( wp_json_encode( $value, JSON_PRETTY_PRINT ) ); } elseif ( is_bool( $value ) ) { \WP_CLI::log( $value ? 'true' : 'false' ); } else { \WP_CLI::log( $value ); } } /** * Set specific setting * * @param string $key Setting key. * @param mixed $value Setting value. * @since 1.0.6 */ private function set_setting( $key, $value ) { // Get current settings. $settings = get_option( 'accessibility_audio_tts_settings', array() ); // Parse value based on type. if ( 'true' === $value ) { $value = true; } elseif ( 'false' === $value ) { $value = false; } elseif ( is_numeric( $value ) ) { $value = false !== strpos( $value, '.' ) ? floatval( $value ) : intval( $value ); } elseif ( false !== strpos( $value, ',' ) ) { $value = array_map( 'trim', explode( ',', $value ) ); } $settings[ $key ] = $value; update_option( 'accessibility_audio_tts_settings', $settings ); \WP_CLI::success( "Setting '$key' updated" ); } /** * Get directory statistics * * @param string $dir Directory path. * @return array Directory stats. * @since 1.0.6 */ private function get_directory_stats( $dir ) { $stats = array( 'size' => 0, 'files' => 0, ); if ( ! is_dir( $dir ) ) { return $stats; } $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $dir, \RecursiveDirectoryIterator::SKIP_DOTS ) ); foreach ( $iterator as $file ) { if ( $file->isFile() ) { $stats['size'] += $file->getSize(); ++$stats['files']; } } return $stats; } /** * Format bytes to human readable format * * @param int $bytes Bytes. * @return string Formatted size. * @since 1.0.6 */ private function format_bytes( $bytes ) { $units = array( 'B', 'KB', 'MB', 'GB', 'TB' ); $units_count = count( $units ) - 1; for ( $i = 0; $bytes > 1024 && $i < $units_count; $i++ ) { $bytes /= 1024; } return round( $bytes, 2 ) . ' ' . $units[ $i ]; } } // Register WP-CLI commands. if ( class_exists( 'WP_CLI' ) ) { \WP_CLI::add_command( 'accessibility-audio-tts', '\Accessibility_Audio_TTS\CLI\WP_CLI_Commands' ); }