* @since 4.0.0 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods. */ namespace BeyondWords\Editor\Components; /** * SelectVoice * * @since 4.0.0 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods. */ defined( 'ABSPATH' ) || exit; class SelectVoice { /** * The only voice service whose voices carry a selectable `model_id`. * * @since 7.0.0 */ public const ELEVENLABS_SERVICE = 'ElevenLabs'; /** * The model listed first in the Model dropdown. * * @since 7.0.0 */ public const DEFAULT_ELEVENLABS_VOICE_MODEL_ID = 'eleven_multilingual_v2'; /** * Bucket key for voices without an ElevenLabs model_id (e.g. standard voices). * * @since 7.0.0 */ public const STANDARD_MODEL_KEY = 'standard'; /** * Init. * * @since 4.0.0 * @since 6.0.0 Make static. * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods. */ public static function init() { add_action( 'rest_api_init', [ self::class, 'rest_api_init_callback'] ); add_action( 'wp_loaded', function (): void { $post_types = \BeyondWords\Settings\Utils::get_compatible_post_types(); if ( is_array( $post_types ) ) { foreach ( $post_types as $post_type ) { add_action( "save_post_{$post_type}", [ self::class, 'save'], 10 ); } } } ); } /** * HTML output for this component. * * @since 4.0.0 * @since 4.5.1 Hide element if no language data exists. * @since 5.4.0 Always display all languages and associated voices. * @since 6.0.0 Make static. * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods. * * @param \WP_Post $post The post object. * * @return string|null */ public static function element( $post ) { $language_code = self::get_language_code( $post->ID ); $voice_id = self::get_voice_id( $post->ID ); $languages = self::get_languages(); $voices = self::get_voices_for_language( $language_code ); $native_filter = self::default_native_filter( $voices, $language_code, $voice_id ); $filtered_voices = self::filter_voices_by_native( $voices, $language_code, $native_filter, $voice_id ); // "Customize" is opt-in: a post is customised once it has an explicit // language or voice; when off we store nothing so project defaults apply. $customize = '' !== (string) $language_code || '' !== (string) $voice_id; $fields_style = $customize ? '' : 'display: none;'; wp_nonce_field( 'beyondwords_select_voice', 'beyondwords_select_voice_nonce' ); self::render_customize_toggle( $customize ); ?>
/> strval( $language['code'] ), 'name' => strval( $language['name'] ), 'accent' => strval( $language['accent'] ), 'defaultVoiceId' => strval( $language['default_voices']['body']['id'] ?? '' ), ]; } return $rows; } /** * Render the Language select: one entry per language NAME (e.g. "English"). * Carries no `name` attribute, so it is not submitted — the Accent select * holds the `beyondwords_language_code` field. * * @since 6.0.0 As render_language_select, combining name + accent. * @since 7.0.0 Split into Language (name) + Accent selects. * * @param array $languages The languages array. * @param string|false $selected_lang_code The selected language code. */ private static function render_language_name_select( array $languages, $selected_lang_code ): void { $selected_language = self::find_language_by_code( $languages, $selected_lang_code ); $selected_name = $selected_language['name'] ?? ''; ?>
1; $accent_style = $show_accent ? '' : 'display: none;'; ?> __( 'Native', 'speechkit' ), 'all' => __( 'All', 'speechkit' ), ]; ?> 1; ?> 1; if ( $show_model ) { $bucket_voices = array_values( array_filter( $voices, static function ( $voice ) use ( $selected_key ) { return self::voice_model_key( $voice ) === $selected_key; } ) ); } else { $bucket_voices = array_values( $voices ); } // Model gates the Voice list: hide it until a model is chosen. $show_voice = count( $voices ) > 0 && ( ! $show_model || '' !== strval( $selected_key ) ); $voice_style = $show_voice ? '' : 'display: none;'; ?> $key, 'label' => self::voice_model_label( $key ), ]; }, $model_ids ); if ( $has_standard ) { $models[] = [ 'key' => self::STANDARD_MODEL_KEY, 'label' => __( 'Legacy', 'speechkit' ), ]; } return $models; } /** * Human label for a voice model_id slug. * * Mirrors voiceModelLabel() in settings-panel/helpers.js. * * @since 7.0.0 * * @param string $model_id The model_id slug (e.g. `eleven_flash_v2_5`). * * @return string A display label. */ public static function voice_model_label( string $model_id ): string { $labels = [ 'eleven_v3' => __( 'v3', 'speechkit' ), 'eleven_multilingual_v2' => __( 'Multilingual v2', 'speechkit' ), 'eleven_flash_v2_5' => __( 'Flash v2.5', 'speechkit' ), 'eleven_turbo_v2_5' => __( 'Turbo v2.5', 'speechkit' ), ]; if ( isset( $labels[ $model_id ] ) ) { return $labels[ $model_id ]; } $slug = preg_replace( '/^eleven_/', '', $model_id ); $slug = str_replace( '_', ' ', (string) $slug ); return ucwords( $slug ); } /** * Render the loading spinner. * * @since 6.0.0 * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods. */ private static function render_loading_spinner(): void { ?> \WP_REST_Server::READABLE, 'callback' => [ self::class, 'languages_rest_api_response'], 'permission_callback' => fn() => current_user_can( 'edit_posts' ), ] ); register_rest_route( 'beyondwords/v1', '/languages/(?P