Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
31.58% |
12 / 38 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
GoogleEngine | |
31.58% |
12 / 38 |
|
50.00% |
2 / 4 |
34.95 | |
0.00% |
0 / 1 |
register | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getSupportedLanguages | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
getAudioData | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
makeGoogleRequest | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
getSsml | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Phonos\Engine; |
4 | |
5 | use DOMDocument; |
6 | use MediaWiki\Extension\Phonos\Exception\PhonosException; |
7 | use stdClass; |
8 | use WANObjectCache; |
9 | |
10 | class GoogleEngine extends Engine { |
11 | |
12 | /** @var string */ |
13 | protected $apiEndpoint; |
14 | |
15 | /** @var string */ |
16 | protected $apiKey; |
17 | |
18 | /** |
19 | * @var int |
20 | * @override |
21 | */ |
22 | protected const MIN_FILE_SIZE = 1200; |
23 | |
24 | protected function register(): void { |
25 | $this->apiEndpoint = $this->config->get( 'PhonosApiEndpointGoogle' ); |
26 | $this->apiKey = $this->config->get( 'PhonosApiKeyGoogle' ); |
27 | } |
28 | |
29 | /** |
30 | * @inheritDoc |
31 | */ |
32 | public function getSupportedLanguages(): ?array { |
33 | $langs = $this->wanCache->getWithSetCallback( |
34 | $this->wanCache->makeKey( 'phonos-google-langs' ), |
35 | WANObjectCache::TTL_MONTH, |
36 | function () { |
37 | $response = $this->makeGoogleRequest( 'voices', [] ); |
38 | $langs = []; |
39 | foreach ( $response->voices as $voice ) { |
40 | $langs = array_merge( $langs, $voice->languageCodes ); |
41 | } |
42 | // Remove duplicates and re-index the array. |
43 | return array_values( array_unique( $langs ) ); |
44 | } |
45 | ); |
46 | return $langs; |
47 | } |
48 | |
49 | /** |
50 | * @inheritDoc |
51 | * @codeCoverageIgnore |
52 | * @throws PhonosException |
53 | */ |
54 | public function getAudioData( AudioParams $params ): string { |
55 | $persistedAudio = $this->getPersistedAudio( $params ); |
56 | if ( $persistedAudio ) { |
57 | return $persistedAudio; |
58 | } |
59 | |
60 | $postData = [ |
61 | 'audioConfig' => [ |
62 | 'audioEncoding' => 'MP3', |
63 | ], |
64 | 'input' => [ |
65 | 'ssml' => trim( $this->getSsml( $params ) ), |
66 | ], |
67 | 'voice' => [ |
68 | 'languageCode' => $params->getLang(), |
69 | ], |
70 | ]; |
71 | $options = [ |
72 | 'method' => 'POST', |
73 | 'postData' => json_encode( $postData ) |
74 | ]; |
75 | |
76 | $response = $this->makeGoogleRequest( 'text:synthesize', $options ); |
77 | $audio = base64_decode( $response->audioContent ); |
78 | $this->persistAudio( $params, $audio ); |
79 | |
80 | return $audio; |
81 | } |
82 | |
83 | /** |
84 | * Make a request to the Google Cloud Text-to-Speech API. |
85 | * @param string $method The API method name. |
86 | * @param mixed[] $options HTTP request options. |
87 | * @return stdClass |
88 | * @throws PhonosException If the request is not successful. |
89 | */ |
90 | private function makeGoogleRequest( string $method, array $options ): stdClass { |
91 | if ( $this->apiProxy ) { |
92 | $options['proxy'] = $this->apiProxy; |
93 | } |
94 | |
95 | $request = $this->requestFactory->create( |
96 | $this->apiEndpoint . $method . '?key=' . $this->apiKey, |
97 | $options, |
98 | __METHOD__ |
99 | ); |
100 | $request->setHeader( 'Content-Type', 'application/json; charset=utf-8' ); |
101 | |
102 | $status = $request->execute(); |
103 | |
104 | if ( !$status->isOK() ) { |
105 | // See if the result contains error details. |
106 | $response = json_decode( $request->getContent() ); |
107 | $error = $response->error->message ?? $status->getMessage()->text(); |
108 | throw new PhonosException( 'phonos-engine-error', [ 'Google', $error ] ); |
109 | } |
110 | |
111 | return json_decode( $request->getContent() ); |
112 | } |
113 | |
114 | /** |
115 | * @inheritDoc |
116 | */ |
117 | public function getSsml( AudioParams $params ): string { |
118 | $ssmlDoc = new DOMDocument(); |
119 | |
120 | $speakNode = $ssmlDoc->createElement( 'speak' ); |
121 | $ssmlDoc->appendChild( $speakNode ); |
122 | |
123 | $phonemeNode = $ssmlDoc->createElement( 'phoneme', $params->getText() ); |
124 | $phonemeNode->setAttribute( 'alphabet', 'ipa' ); |
125 | |
126 | // Trim slashes from IPA; see T313497 |
127 | $ipa = trim( $params->getIpa(), '/' ); |
128 | // Replace apostrophes with U+02C8; see T313711 |
129 | $ipa = str_replace( "'", "ˈ", $ipa ); |
130 | |
131 | $phonemeNode->setAttribute( 'ph', $ipa ); |
132 | $speakNode->appendChild( $phonemeNode ); |
133 | |
134 | // Return the documentElement (omitting the <?xml> tag) since it is not |
135 | // needed and Google charges by the number of characters in the payload. |
136 | return $ssmlDoc->saveXML( $ssmlDoc->documentElement ); |
137 | } |
138 | } |