Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialTestListen
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 6
156
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getRestriction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
12
 submitCallbackText
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
20
 makeAudioElement
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 submitCallbackAudioData
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Wikispeech\Specials;
4
5/**
6 * @file
7 * @ingroup Extensions
8 * @license GPL-2.0-or-later
9 */
10
11use MediaWiki\Html\Html;
12use MediaWiki\HTMLForm\HTMLForm;
13use MediaWiki\Languages\LanguageNameUtils;
14use MediaWiki\Wikispeech\SpeechoidConnector;
15use MediaWiki\Wikispeech\VoiceHandler;
16use SpecialPage;
17use Wikimedia\Codex\Utility\Codex;
18use Wikimedia\Codex\Utility\Sanitizer;
19
20/**
21 * Special page for listening to a synthesised utterance.
22 *
23 * @since 0.1.13
24 */
25class SpecialTestListen extends SpecialPage {
26    use LanguageOptionsTrait;
27
28    /** @var SpeechoidConnector */
29    private $speechoidConnector;
30
31    /** @var VoiceHandler */
32    private $voiceHandler;
33
34    /**
35     * @since 0.1.13
36     * @param LanguageNameUtils $languageNameUtils
37     * @param mixed $speechoidConnector
38     * @param VoiceHandler $voiceHandler
39     */
40    public function __construct(
41        $languageNameUtils,
42        $speechoidConnector,
43        VoiceHandler $voiceHandler
44    ) {
45        // MW <1.46 requires restriction in constructor, ≥1.46 uses getRestriction().
46        // TODO: Remove when Wikispeech supports MW 1.46 (T425352)
47        if ( version_compare( MW_VERSION, '1.46', '>=' ) ) {
48            parent::__construct( 'TestListen' );
49        } else {
50            parent::__construct( 'TestListen', 'wikispeech-listen' );
51        }
52        $this->languageNameUtils = $languageNameUtils;
53        $this->speechoidConnector = $speechoidConnector;
54        $this->voiceHandler = $voiceHandler;
55    }
56
57    /** @inheritDoc */
58    public function getRestriction(): string {
59        return 'wikispeech-listen';
60    }
61
62    /**
63     * @since 0.1.13
64     * @param string|null $subPage
65     */
66    public function execute( $subPage ) {
67        $this->setHeaders();
68        $this->checkPermissions();
69
70        $form = HTMLForm::factory(
71            'codex',
72            [
73                'text' => [
74                    'name' => 'text',
75                    'type' => 'text',
76                    'label' => $this->msg( 'wikispeech-testlisten-text' )->text()
77                ],
78                'language' => [
79                    'name' => 'language',
80                    'type' => 'select',
81                    'label' => $this->msg( 'wikispeech-language' )->text(),
82                    'options' => $this->getLanguageOptions()
83                ],
84                'ssml' => [
85                    'name' => 'ssml',
86                    'type' => 'check',
87                    'label' => $this->msg( 'wikispeech-testlisten-ssml' )->text()
88                ],
89                'audioData' => [
90                    'name' => 'audioData',
91                    'type' => 'textarea',
92                    'label' => $this->msg( 'wikispeech-testlisten-audio-data' )->text(),
93                    'rows' => 5
94                ],
95            ],
96            $this->getContext()
97        );
98
99        $codex = new Codex();
100        // phpcs:ignore Generic.Files.LineLength
101        $ssmlSpeakTag = '<speak xml:lang="en-US" version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.w3.org/2001/10/synthesis http://www.w3.org/TR/speech-synthesis/synthesis.xsd">...</speak>';
102        $sanitizer = new Sanitizer();
103        $tag = $sanitizer->sanitizeText( $ssmlSpeakTag );
104        // This note intentionally doesn't use messages. It's likely that it
105        // will change or be removed before it's relevant to end users.
106        $noteContent = "<p>This page is only intened to help developers.</p>"
107            . '<p>When SSML is enabled the input has to be a speak tag like the one below.</p>'
108            . "<pre>$tag</pre>";
109        $note = $codex
110            ->message()
111            ->setType( 'notice' )
112            ->setHeading( 'For development' )
113            ->setContentHtml(
114                $codex
115                    ->htmlSnippet()
116                    ->setContent( $noteContent )
117                    ->build()
118            )
119            ->build()
120            ->getHtml();
121        $form->addHeaderHtml( $note );
122
123        $form->setSubmitCallback( function ( $data, $form ) {
124            if ( $data['text'] ) {
125                return $this->submitCallbackText( $data, $form );
126            } elseif ( $data['audioData'] ) {
127                return $this->submitCallbackAudioData( $data, $form );
128            } else {
129                return 'Either text or audio data must be provided.';
130            }
131        } );
132        $form->show();
133    }
134
135    /**
136     * Make synthesized speech and add an audio element and a table for tokens.
137     *
138     * @param array $data Must contain 'text' or 'ssml'.
139     * @param HTMLForm $form
140     */
141    private function submitCallbackText( array $data, HTMLForm $form ) {
142        $language = $data['language'];
143        $voice = $this->voiceHandler->getDefaultVoice( $language );
144        $speechoidData = [];
145        if ( $data['ssml'] ) {
146            $speechoidData['ssml'] = $data['text'];
147        } else {
148            $speechoidData['text'] = $data['text'];
149        }
150        $speechoidResponse = $this->speechoidConnector->synthesize(
151            $language,
152            $voice,
153            $speechoidData
154        );
155        $html = $this->makeAudioElement( $speechoidResponse['audio_data'] );
156        $html .= Html::openElement( 'table', [ 'class' => 'wikitable' ] );
157        $html .= Html::openElement( 'tr' );
158        $html .= Html::element( 'th', [], 'orth' );
159        $html .= Html::element( 'th', [], 'expanded' );
160        $html .= Html::element( 'th', [], 'endtime' );
161        $html .= Html::openElement( 'tr' );
162        foreach ( $speechoidResponse['tokens'] as $token ) {
163            $html .= Html::openElement( 'tr' );
164            $html .= Html::openElement( 'td' )
165                . Html::element( 'code', [], $token['orth'] )
166                . Html::closeElement( 'td' );
167            $html .= Html::openElement( 'td' );
168            if ( array_key_exists( 'expanded', $token ) ) {
169                $html .= Html::element( 'code', [], $token['expanded'] );
170            }
171            $html .= Html::closeElement( 'td' );
172            $html .= Html::element( 'td', [], $token['endtime'] );
173            $html .= Html::closeElement( 'tr' );
174        }
175        $html .= Html::openElement( 'table' );
176        $form->addFooterHtml( $html );
177    }
178
179    /**
180     * Create an audio element with audio data.
181     *
182     * @param string $audioData Base64 encoded Opus data.
183     * @return string
184     */
185    private function makeAudioElement( string $audioData ) {
186        $audioDataString = "data:audio/ogg;base64,$audioData";
187        $html = Html::element( 'audio', [ 'controls' => '', 'src' => $audioDataString ] );
188        return $html;
189    }
190
191    /**
192     * Add an audio element with the input audio data.
193     *
194     * @param array $data Must contain 'audioData'.
195     * @param HTMLForm $form
196     */
197    private function submitCallbackAudioData( array $data, HTMLForm $form ) {
198        $html = $this->makeAudioElement( $data['audioData'] );
199        $form->addFooterHtml( $html );
200    }
201}