Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
85.92% |
61 / 71 |
|
66.67% |
2 / 3 |
CRAP | |
0.00% |
0 / 1 |
UtteranceGenerator | |
85.92% |
61 / 71 |
|
66.67% |
2 / 3 |
12.40 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
setUtteranceStore | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUtterance | |
85.07% |
57 / 67 |
|
0.00% |
0 / 1 |
10.33 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Wikispeech\Utterance; |
4 | |
5 | /** |
6 | * @file |
7 | * @ingroup Extensions |
8 | * @license GPL-2.0-or-later |
9 | */ |
10 | |
11 | use ConfigException; |
12 | use ExternalStoreException; |
13 | use FormatJson; |
14 | use InvalidArgumentException; |
15 | use MediaWiki\Logger\LoggerFactory; |
16 | use MediaWiki\Wikispeech\InputTextValidator; |
17 | use MediaWiki\Wikispeech\Segment\Segment; |
18 | use MediaWiki\Wikispeech\Segment\TextFilter\Sv\SwedishFilter; |
19 | use MediaWiki\Wikispeech\SpeechoidConnector; |
20 | use MediaWiki\Wikispeech\SpeechoidConnectorException; |
21 | use MediaWiki\Wikispeech\VoiceHandler; |
22 | use Psr\Log\LoggerInterface; |
23 | |
24 | /** |
25 | * @since 0.1.11 |
26 | */ |
27 | |
28 | class 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 | } |