Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
85.92% covered (warning)
85.92%
61 / 71
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
UtteranceGenerator
85.92% covered (warning)
85.92%
61 / 71
66.67% covered (warning)
66.67%
2 / 3
12.40
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setUtteranceStore
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUtterance
85.07% covered (warning)
85.07%
57 / 67
0.00% covered (danger)
0.00%
0 / 1
10.33
1<?php
2
3namespace MediaWiki\Wikispeech\Utterance;
4
5/**
6 * @file
7 * @ingroup Extensions
8 * @license GPL-2.0-or-later
9 */
10
11use ConfigException;
12use ExternalStoreException;
13use FormatJson;
14use InvalidArgumentException;
15use MediaWiki\Logger\LoggerFactory;
16use MediaWiki\Wikispeech\InputTextValidator;
17use MediaWiki\Wikispeech\Segment\Segment;
18use MediaWiki\Wikispeech\Segment\TextFilter\Sv\SwedishFilter;
19use MediaWiki\Wikispeech\SpeechoidConnector;
20use MediaWiki\Wikispeech\SpeechoidConnectorException;
21use MediaWiki\Wikispeech\VoiceHandler;
22use Psr\Log\LoggerInterface;
23
24 /**
25  * @since 0.1.11
26  */
27
28class UtteranceGenerator {
29
30    /** @var UtteranceStore */
31    private $utteranceStore;
32
33    /** @var VoiceHandler */
34    private $voiceHandler;
35
36    /** @var LoggerInterface */
37    private $logger;
38
39    /** @var SpeechoidConnector */
40    private $speechoidConnector;
41
42    /** @var InputTextValidator */
43    private $InputTextValidator;
44
45    public function __construct(
46        SpeechoidConnector $speechoidConnector
47    ) {
48        $this->logger = LoggerFactory::getInstance( 'Wikispeech' );
49        $this->speechoidConnector = $speechoidConnector;
50
51        $this->utteranceStore = new UtteranceStore();
52    }
53
54    /**
55     * Sets a custom UtteranceStore instance, typically for testing.
56     *
57     * @since 0.1.11
58     * @param UtteranceStore $utteranceStore
59     * @return void
60     */
61    public function setUtteranceStore( UtteranceStore $utteranceStore ): void {
62        $this->utteranceStore = $utteranceStore;
63    }
64
65    /**
66     * Return the utterance corresponding to the request.
67     *
68     * These are either retrieved from storage or synthesize (and then stored).
69     *
70     * @since 0.1.5
71     * @param string|null $consumerUrl
72     * @param string $voice
73     * @param string $language
74     * @param int $pageId
75     * @param Segment $segment
76     * @return array Containing base64 'audio' and synthesisMetadata 'tokens'.
77     * @throws ExternalStoreException
78     * @throws ConfigException
79     * @throws InvalidArgumentException
80     * @throws SpeechoidConnectorException
81     */
82    public function getUtterance(
83        ?string $consumerUrl,
84        string $voice,
85        string $language,
86        int $pageId,
87        Segment $segment
88    ) {
89        if ( $pageId !== 0 && !$pageId ) {
90            throw new InvalidArgumentException( 'Page ID must be set.' );
91        }
92        $segmentHash = $segment->getHash();
93        if ( $segmentHash === null ) {
94            throw new InvalidArgumentException( 'Segment hash must be set.' );
95        }
96        if ( !$voice ) {
97            $voice = $this->voiceHandler->getDefaultVoice( $language );
98            if ( !$voice ) {
99                throw new ConfigException( "Invalid default voice configuration." );
100            }
101        }
102        $utterance = $this->utteranceStore->findUtterance(
103            $consumerUrl,
104            $pageId,
105            $language,
106            $voice,
107            $segmentHash
108        );
109
110        if ( !$utterance ) {
111            $this->logger->debug( __METHOD__ . ': Creating new utterance for {pageId} {segmentHash}', [
112                'pageId' => $pageId,
113                'segmentHash' => $segment->getHash()
114            ] );
115
116            // Make a string of all the segment contents.
117            $segmentText = '';
118            foreach ( $segment->getContent() as $content ) {
119                $segmentText .= $content->getString();
120            }
121
122            $this->InputTextValidator = new InputTextValidator();
123            $this->InputTextValidator->validateText( $segmentText );
124
125            /** @var string $ssml text/xml Speech Synthesis Markup Language */
126            $ssml = null;
127            if ( $language === 'sv' ) {
128                // @todo implement a per language selecting content text filter facade
129                $textFilter = new SwedishFilter( $segmentText );
130                $ssml = $textFilter->process();
131            }
132            if ( $ssml !== null ) {
133                $speechoidResponse = $this->speechoidConnector->synthesize(
134                    $language,
135                    $voice,
136                    [ 'ssml' => $ssml ]
137                );
138            } else {
139                $speechoidResponse = $this->speechoidConnector->synthesizeText(
140                    $language,
141                    $voice,
142                    $segmentText
143                );
144            }
145            $this->utteranceStore->createUtterance(
146                $consumerUrl,
147                $pageId,
148                $language,
149                $voice,
150                $segmentHash,
151                $speechoidResponse['audio_data'],
152                FormatJson::encode(
153                    $speechoidResponse['tokens']
154                )
155            );
156            return [
157                'audio' => $speechoidResponse['audio_data'],
158                'tokens' => $speechoidResponse['tokens']
159            ];
160        }
161        $this->logger->debug( __METHOD__ . ': Using cached utterance for {pageId} {segmentHash}', [
162            'pageId' => $pageId,
163            'segmentHash' => $segmentHash
164        ] );
165        return [
166            'audio' => $utterance->getAudio(),
167            'tokens' => FormatJson::parse(
168                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable synthesis metadata is set
169                $utterance->getSynthesisMetadata(),
170                FormatJson::FORCE_ASSOC
171            )->getValue()
172        ];
173    }
174}