Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 101 |
|
0.00% |
0 / 6 |
CRAP | |
0.00% |
0 / 1 |
| SpecialTestListen | |
0.00% |
0 / 101 |
|
0.00% |
0 / 6 |
156 | |
0.00% |
0 / 1 |
| __construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
| getRestriction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| execute | |
0.00% |
0 / 58 |
|
0.00% |
0 / 1 |
12 | |||
| submitCallbackText | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
20 | |||
| makeAudioElement | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| submitCallbackAudioData | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace MediaWiki\Wikispeech\Specials; |
| 4 | |
| 5 | /** |
| 6 | * @file |
| 7 | * @ingroup Extensions |
| 8 | * @license GPL-2.0-or-later |
| 9 | */ |
| 10 | |
| 11 | use MediaWiki\Html\Html; |
| 12 | use MediaWiki\HTMLForm\HTMLForm; |
| 13 | use MediaWiki\Languages\LanguageNameUtils; |
| 14 | use MediaWiki\Wikispeech\SpeechoidConnector; |
| 15 | use MediaWiki\Wikispeech\VoiceHandler; |
| 16 | use SpecialPage; |
| 17 | use Wikimedia\Codex\Utility\Codex; |
| 18 | use Wikimedia\Codex\Utility\Sanitizer; |
| 19 | |
| 20 | /** |
| 21 | * Special page for listening to a synthesised utterance. |
| 22 | * |
| 23 | * @since 0.1.13 |
| 24 | */ |
| 25 | class 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 | } |