Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
31.58% covered (danger)
31.58%
12 / 38
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
GoogleEngine
31.58% covered (danger)
31.58%
12 / 38
50.00% covered (danger)
50.00%
2 / 4
34.95
0.00% covered (danger)
0.00%
0 / 1
 register
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getSupportedLanguages
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 getAudioData
n/a
0 / 0
n/a
0 / 0
2
 makeGoogleRequest
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getSsml
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\Phonos\Engine;
4
5use DOMDocument;
6use MediaWiki\Extension\Phonos\Exception\PhonosException;
7use stdClass;
8use WANObjectCache;
9
10class 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}