# speechkit/6.0.3/src/Component/Post/SelectVoice/SelectVoice.php

BeyondWords – AI audio for publishers, version 6.0.3. 367 lines.

- Page: https://pluginprobe.com/plugins/speechkit/6.0.3/code/src/Component/Post/SelectVoice/SelectVoice.php
- Raw: https://pluginprobe.com/plugins/speechkit/6.0.3/raw/src/Component/Post/SelectVoice/SelectVoice.php
- Modified: 2025-12-16T10:29:00+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/speechkit/6.0.3/code/src/Component/Post/SelectVoice/SelectVoice.php#L10-L20`.

```php
<?php

declare(strict_types=1);

/**
 * BeyondWords Component: Select Voice
 *
 * @package Beyondwords\Wordpress
 * @author  Stuart McAlpine <stu@beyondwords.io>
 * @since   4.0.0
 */

namespace Beyondwords\Wordpress\Component\Post\SelectVoice;

use Beyondwords\Wordpress\Core\CoreUtils;
use Beyondwords\Wordpress\Component\Settings\SettingsUtils;
use Beyondwords\Wordpress\Core\ApiClient;

/**
 * SelectVoice
 *
 * @since 4.0.0
 */
class SelectVoice
{
    /**
     * Init.
     *
     * @since 4.0.0
     * @since 6.0.0 Make static.
     */
    public static function init()
    {
        add_action('rest_api_init', [self::class, 'restApiInit']);
        add_action('admin_enqueue_scripts', [self::class, 'adminEnqueueScripts']);

        add_action('wp_loaded', function (): void {
            $postTypes = SettingsUtils::getCompatiblePostTypes();

            if (is_array($postTypes)) {
                foreach ($postTypes as $postType) {
                    add_action("save_post_{$postType}", [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.
     *
     * @param \WP_Post $post The post object.
     *
     * @return string|null
     */
    public static function element($post)
    {
        $languageCode = self::getLanguageCode($post->ID);
        $voiceId = self::getVoiceId($post->ID);
        $languages = ApiClient::getLanguages();
        $voices = self::getVoicesForLanguage($languageCode);

        wp_nonce_field('beyondwords_select_voice', 'beyondwords_select_voice_nonce');

        self::renderLanguageSelect($languages, $languageCode);
        self::renderVoiceSelect($voices, $voiceId, $languageCode);
        self::renderLoadingSpinner();
    }

    /**
     * Get the language code for a post.
     *
     * @since 6.0.0
     *
     * @param int $postId The post ID.
     * @return string|false The language code or false if not set.
     */
    private static function getLanguageCode(int $postId)
    {
        $postLanguageCode = get_post_meta($postId, 'beyondwords_language_code', true);
        return $postLanguageCode ?: get_option('beyondwords_project_language_code');
    }

    /**
     * Get the voice ID for a post.
     *
     * @since 6.0.0
     *
     * @param int $postId The post ID.
     * @return string|false The voice ID or false if not set.
     */
    private static function getVoiceId(int $postId)
    {
        $postVoiceId = get_post_meta($postId, 'beyondwords_body_voice_id', true);
        return $postVoiceId ?: get_option('beyondwords_project_body_voice_id');
    }

    /**
     * Get voices for a language code.
     *
     * @since 6.0.0
     *
     * @param string|false $languageCode The language code.
     * @return array The voices array.
     */
    private static function getVoicesForLanguage($languageCode): array
    {
        if ($languageCode === false || $languageCode === '') {
            return [];
        }

        $voices = ApiClient::getVoices($languageCode);
        return is_array($voices) ? $voices : [];
    }

    /**
     * Render the language select dropdown.
     *
     * @since 6.0.0
     *
     * @param array $languages The languages array.
     * @param string|false $selectedLanguageCode The selected language code.
     */
    private static function renderLanguageSelect(array $languages, $selectedLanguageCode): void
    {
        ?>
        <p
            id="beyondwords-metabox-select-voice--language-code"
            class="post-attributes-label-wrapper page-template-label-wrapper"
        >
            <label class="post-attributes-label" for="beyondwords_language_code">
                Language
            </label>
        </p>
        <select id="beyondwords_language_code" name="beyondwords_language_code" style="width: 100%;">
            <?php
            foreach ($languages as $language) {
                if (empty($language['code']) || empty($language['name']) || empty($language['accent'])) {
                    continue;
                }
                printf(
                    '<option value="%s" data-default-voice-id="%s" %s>%s (%s)</option>',
                    esc_attr($language['code']),
                    esc_attr($language['default_voices']['body']['id'] ?? ''),
                    selected(strval($language['code']), strval($selectedLanguageCode)),
                    esc_html($language['name']),
                    esc_html($language['accent'])
                );
            }
            ?>
        </select>
        <?php
    }

    /**
     * Render the voice select dropdown.
     *
     * @since 6.0.0
     *
     * @param array $voices The voices array.
     * @param string|false $selectedVoiceId The selected voice ID.
     * @param string|false $languageCode The language code.
     */
    private static function renderVoiceSelect(array $voices, $selectedVoiceId, $languageCode): void
    {
        ?>
        <p
            id="beyondwords-metabox-select-voice--voice-id"
            class="post-attributes-label-wrapper page-template-label-wrapper"
        >
            <label class="post-attributes-label" for="beyondwords_voice_id">
                Voice
            </label>
        </p>
        <select
            id="beyondwords_voice_id"
            name="beyondwords_voice_id"
            style="width: 100%;"
            <?php echo disabled(!strval($languageCode)) ?>
        >
            <?php
            foreach ($voices as $voice) {
                printf(
                    '<option value="%s" %s>%s</option>',
                    esc_attr($voice['id']),
                    selected(strval($voice['id']), strval($selectedVoiceId)),
                    esc_html($voice['name'])
                );
            }
            ?>
        </select>
        <?php
    }

    /**
     * Render the loading spinner.
     *
     * @since 6.0.0
     */
    private static function renderLoadingSpinner(): void
    {
        ?>
        <img
            src="/wp-admin/images/spinner.gif"
            class="beyondwords-settings__loader"
            style="display:none; padding: 3px 0;"
        />
        <?php
    }

    /**
     * Save the meta when the post is saved.
     *
     * @since 4.0.0
     * @since 6.0.0 Make static.
     *
     * @param int $postId The ID of the post being saved.
     */
    public static function save($postId)
    {
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
            return $postId;
        }

        // "save_post" can be triggered at other times, so verify this request came from the our component
        if (
            ! isset($_POST['beyondwords_language_code']) ||
            ! isset($_POST['beyondwords_voice_id']) ||
            ! isset($_POST['beyondwords_select_voice_nonce'])
        ) {
            return $postId;
        }

        // "save_post" can be triggered at other times, so verify this request came from the our component
        if (
            ! wp_verify_nonce(
                sanitize_key($_POST['beyondwords_select_voice_nonce']),
                'beyondwords_select_voice'
            )
        ) {
            return $postId;
        }

        $languageCode = sanitize_text_field(wp_unslash($_POST['beyondwords_language_code']));

        if (! empty($languageCode)) {
            update_post_meta($postId, 'beyondwords_language_code', $languageCode);
        } else {
            delete_post_meta($postId, 'beyondwords_language_code');
        }

        $voiceId = sanitize_text_field(wp_unslash($_POST['beyondwords_voice_id']));

        if (! empty($voiceId)) {
            update_post_meta($postId, 'beyondwords_body_voice_id', $voiceId);
            update_post_meta($postId, 'beyondwords_title_voice_id', $voiceId);
            update_post_meta($postId, 'beyondwords_summary_voice_id', $voiceId);
        } else {
            delete_post_meta($postId, 'beyondwords_body_voice_id');
            delete_post_meta($postId, 'beyondwords_title_voice_id');
            delete_post_meta($postId, 'beyondwords_summary_voice_id');
        }

        return $postId;
    }

    /**
     * Register WP REST API route
     *
     * @since 4.0.0
     * @since 6.0.0 Make static.
     *
     * @return void
     */
    public static function restApiInit()
    {
        // Languages endpoint
        register_rest_route('beyondwords/v1', '/languages', [
            'methods'  => \WP_REST_Server::READABLE,
            'callback' => [self::class, 'languagesRestApiResponse'],
            'permission_callback' => fn() => current_user_can('edit_posts'),
        ]);

        // Voices endpoint
        register_rest_route('beyondwords/v1', '/languages/(?P<languageCode>[a-zA-Z0-9-_]+)/voices', [
            'methods'  => \WP_REST_Server::READABLE,
            'callback' => [self::class, 'voicesRestApiResponse'],
            'permission_callback' => fn() => current_user_can('edit_posts'),
        ]);
    }

    /**
     * "Languages" WP REST API response (required for the Gutenberg editor).
     *
     * @since 4.0.0
     * @since 5.4.0 No longer filter by "Languages" plugin setting.
     * @since 6.0.0 Make static.
     *
     * @return \WP_REST_Response
     */
    public static function languagesRestApiResponse()
    {
        $languages = ApiClient::getLanguages();

        return new \WP_REST_Response($languages);
    }

    /**
     * "Voices" WP REST API response (required for the Gutenberg editor
     * and Block Editor).
     *
     * @since 4.0.0
     * @since 6.0.0 Make static.
     *
     * @return \WP_REST_Response
     */
    public static function voicesRestApiResponse(\WP_REST_Request $data)
    {
        $params = $data->get_url_params();

        $voices = ApiClient::getVoices($params['languageCode']);

        return new \WP_REST_Response($voices);
    }

    /**
     * Register the component scripts.
     *
     * @since 4.0.0
     * @since 6.0.0 Make static.
     *
     * @param string $hook Page hook
     *
     * @return void
     */
    public static function adminEnqueueScripts($hook)
    {
        if (! CoreUtils::isGutenbergPage() && ( $hook === 'post.php' || $hook === 'post-new.php')) {
            wp_register_script(
                'beyondwords-metabox--select-voice',
                BEYONDWORDS__PLUGIN_URI . 'src/Component/Post/SelectVoice/classic-metabox.js',
                ['jquery', 'underscore'],
                BEYONDWORDS__PLUGIN_VERSION,
                true
            );

            /**
             * Localize the script to handle ajax requests
             */
            wp_localize_script(
                'beyondwords-metabox--select-voice',
                'beyondwordsData',
                [
                    'nonce' => wp_create_nonce('wp_rest'),
                    'root' => esc_url_raw(rest_url()),
                ]
            );

            wp_enqueue_script('beyondwords-metabox--select-voice');
        }
    }
}

```
