diff --git a/src/editor/components/select-voice/class-assets.php b/src/editor/components/select-voice/class-assets.php index 9e92c54e..95a40c64 100644 --- a/src/editor/components/select-voice/class-assets.php +++ b/src/editor/components/select-voice/class-assets.php @@ -59,6 +59,8 @@ public static function admin_enqueue_scripts( $hook ): void { 'nonce' => wp_create_nonce( 'wp_rest' ), 'root' => esc_url_raw( rest_url() ), 'projectId' => (string) get_option( 'beyondwords_project_id', '' ), + // Lets the script rebuild the Accent dropdown when a language name is picked. + 'languages' => \BeyondWords\Editor\Components\SelectVoice::languages_for_script(), 'selectVoice' => __( 'Select a voice', 'speechkit' ), 'selectModel' => __( 'Select a model', 'speechkit' ), 'standardModel' => __( 'Legacy', 'speechkit' ), diff --git a/src/editor/components/select-voice/class-select-voice.php b/src/editor/components/select-voice/class-select-voice.php index c8262734..c1a9e94d 100644 --- a/src/editor/components/select-voice/class-select-voice.php +++ b/src/editor/components/select-voice/class-select-voice.php @@ -87,6 +87,9 @@ public static function element( $post ) { $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; @@ -98,9 +101,11 @@ public static function element( $post ) { ?>
@@ -213,42 +218,153 @@ private static function get_voices_for_language( $language_code ): array { } /** - * Render the language select dropdown. + * Whether a language row carries the fields the pickers need. * - * @since 6.0.0 - * @since 7.0.0 Refactored to BeyondWords namespace with snake_case methods. + * @since 7.0.0 + * + * @param mixed $language A language record. + * + * @return bool + */ + private static function is_valid_language( $language ): bool { + return is_array( $language ) + && ! empty( $language['code'] ) + && ! empty( $language['name'] ) + && ! empty( $language['accent'] ); + } + + /** + * The distinct language names across the language rows, in API order. + * + * @since 7.0.0 + * + * @param array $languages The languages array. + * + * @return string[] The language names. + */ + public static function language_names( array $languages ): array { + $names = []; + + foreach ( $languages as $language ) { + if ( ! self::is_valid_language( $language ) ) { + continue; + } + if ( ! in_array( $language['name'], $names, true ) ) { + $names[] = $language['name']; + } + } + + return $names; + } + + /** + * The language rows (accents) for a language name, in API order. + * + * @since 7.0.0 + * + * @param array $languages The languages array. + * @param string $name The language name. + * + * @return array The matching language rows. + */ + public static function accents_for_name( array $languages, string $name ): array { + if ( '' === $name ) { + return []; + } + + return array_values( + array_filter( + $languages, + static function ( $language ) use ( $name ) { + return self::is_valid_language( $language ) && $language['name'] === $name; + } + ) + ); + } + + /** + * Find a language row by its code. + * + * @since 7.0.0 + * + * @param array $languages The languages array. + * @param string|false $code The language code. + * + * @return array|null The matching language row, or null. + */ + public static function find_language_by_code( array $languages, $code ): ?array { + foreach ( $languages as $language ) { + if ( self::is_valid_language( $language ) && strval( $language['code'] ) === strval( $code ) ) { + return $language; + } + } + + return null; + } + + /** + * Slim language rows for the classic-editor script, so it can rebuild the + * Accent dropdown client-side without another API round-trip. + * + * @since 7.0.0 + * + * @return array The slim language rows. + */ + public static function languages_for_script(): array { + $rows = []; + + foreach ( self::get_languages() as $language ) { + if ( ! self::is_valid_language( $language ) ) { + continue; + } + + $rows[] = [ + 'code' => 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_select( array $languages, $selected_lang_code ): void { + 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'] ?? ''; ?>

-

- %s', - selected( '', strval( $selected_lang_code ), false ), + selected( '', strval( $selected_name ), false ), esc_html__( 'Select a language…', 'speechkit' ) ); - foreach ( $languages as $language ) { - if ( empty( $language['code'] ) || empty( $language['name'] ) || empty( $language['accent'] ) ) { - continue; - } + foreach ( self::language_names( $languages ) as $name ) { printf( - '', - esc_attr( $language['code'] ), - esc_attr( $language['default_voices']['body']['id'] ?? '' ), - selected( strval( $language['code'] ), strval( $selected_lang_code ) ), - esc_html( $language['name'] ), - esc_html( $language['accent'] ) + '', + esc_attr( $name ), + selected( $name, strval( $selected_name ), false ), + esc_html( $name ) ); } ?> @@ -256,6 +372,94 @@ class="post-attributes-label-wrapper page-template-label-wrapper" 1; + $accent_style = $show_accent ? '' : 'display: none;'; + ?> +
+

+ +

+ +
+ __( 'Native', 'speechkit' ), + 'all' => __( 'All', 'speechkit' ), + ]; + ?> +
+

+ +

+ +
+ and submit on save, so no autosave/Heartbeat hook is needed. */ ( function () { @@ -11,6 +12,7 @@ const data = typeof beyondwordsData !== 'undefined' ? beyondwordsData : null; + const LANGUAGES = ( data && data.languages ) || []; const ELEVENLABS = ( data && data.elevenLabs ) || 'ElevenLabs'; const DEFAULT_MODEL_ID = ( data && data.defaultModelId ) || 'eleven_multilingual_v2'; @@ -57,6 +59,45 @@ return STANDARD_MODEL_KEY; }; + /** + * A voice's primary (native) language code. + * + * @param {Object} voice A voice record. + * + * @return {string} The primary language code, or ''. + */ + const voicePrimaryCode = ( voice ) => { + const language = voice && voice.language; + if ( typeof language === 'string' ) { + return language; + } + if ( language && typeof language === 'object' && language.code ) { + return language.code; + } + return ( + ( voice && voice.languages && voice.languages[ 0 ] + ? voice.languages[ 0 ].code + : '' ) || '' + ); + }; + + /** + * Whether a voice is native to a language code. A voice with no + * determinable primary language is treated as native, so it is never hidden. + * + * @param {Object} voice A voice record. + * @param {string} code The language code. + * + * @return {boolean} Whether the voice is native to the code. + */ + const voiceIsNative = ( voice, code ) => { + const primary = voicePrimaryCode( voice ); + if ( ! primary ) { + return true; + } + return String( primary ) === String( code ); + }; + /** * The distinct model buckets across a language's voices, for the Model dropdown. * @@ -108,6 +149,45 @@ const byId = ( id ) => document.getElementById( id ); + /** + * The language rows (accents) for a language name, in API order. + * + * @param {string} name The language name, or ''. + * + * @return {Array} The matching slim language rows. + */ + const accentsForName = ( name ) => + name ? LANGUAGES.filter( ( language ) => language.name === name ) : []; + + /** + * Find a slim language row by its code. + * + * @param {string} code The language code. + * + * @return {Object|null} The matching row, or null. + */ + const findLanguageByCode = ( code ) => + LANGUAGES.find( + ( language ) => String( language.code ) === String( code ) + ) || null; + + /** + * An Accent