|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2014 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing the Microsoft translation engine. |
|
8 """ |
|
9 |
|
10 from PyQt5.QtCore import QUrl, QDateTime, QByteArray, QTimer |
|
11 |
|
12 from .TranslationEngine import TranslationEngine |
|
13 |
|
14 |
|
15 class MicrosoftEngine(TranslationEngine): |
|
16 """ |
|
17 Class implementing the translation engine for the Microsoft |
|
18 translation service. |
|
19 """ |
|
20 AccessTokenUrl = ( |
|
21 "https://api.cognitive.microsoft.com/sts/v1.0/issueToken" |
|
22 ) |
|
23 TranslatorUrl = "https://api.microsofttranslator.com/V2/Http.svc/Translate" |
|
24 TextToSpeechUrl = "https://api.microsofttranslator.com/V2/Http.svc/Speak" |
|
25 |
|
26 def __init__(self, plugin, parent=None): |
|
27 """ |
|
28 Constructor |
|
29 |
|
30 @param plugin reference to the plugin object (TranslatorPlugin) |
|
31 @param parent reference to the parent object (QObject) |
|
32 """ |
|
33 super().__init__(plugin, parent) |
|
34 |
|
35 self.__mappings = { |
|
36 "zh-CN": "zh-CHS", |
|
37 "zh-TW": "zh-CHT", |
|
38 } |
|
39 |
|
40 QTimer.singleShot(0, self.availableTranslationsLoaded.emit) |
|
41 |
|
42 def engineName(self): |
|
43 """ |
|
44 Public method to return the name of the engine. |
|
45 |
|
46 @return engine name (string) |
|
47 """ |
|
48 return "microsoft" |
|
49 |
|
50 def supportedLanguages(self): |
|
51 """ |
|
52 Public method to get the supported languages. |
|
53 |
|
54 @return list of supported language codes (list of string) |
|
55 """ |
|
56 return ["ar", "bg", "ca", "cs", "da", "de", "en", |
|
57 "es", "et", "fi", "fr", "hi", "hu", "id", |
|
58 "it", "ja", "ko", "lt", "lv", "mt", |
|
59 "nl", "no", "pl", "pt", "ro", "ru", "sk", "sl", |
|
60 "sv", "th", "tr", "uk", "vi", "zh-CN", "zh-TW", |
|
61 ] |
|
62 |
|
63 def hasTTS(self): |
|
64 """ |
|
65 Public method indicating the Text-to-Speech capability. |
|
66 |
|
67 @return flag indicating the Text-to-Speech capability (boolean) |
|
68 """ |
|
69 return True |
|
70 |
|
71 def __mapLanguageCode(self, code): |
|
72 """ |
|
73 Private method to map a language code to the Microsoft code. |
|
74 |
|
75 @param code language code (string) |
|
76 @return mapped language code (string) |
|
77 """ |
|
78 if code in self.__mappings: |
|
79 return self.__mapping[code] |
|
80 else: |
|
81 return code |
|
82 |
|
83 def __getClientDataAzure(self): |
|
84 """ |
|
85 Private method to retrieve the client data. |
|
86 |
|
87 @return tuple giving the API subscription key and a flag indicating |
|
88 validity |
|
89 @rtype tuple of (str, bool) |
|
90 """ |
|
91 subscriptionKey = self.plugin.getPreferences("MsTranslatorKey") |
|
92 valid = bool(subscriptionKey) |
|
93 return subscriptionKey, valid |
|
94 |
|
95 def __getAccessToken(self, requestObject): |
|
96 """ |
|
97 Private slot to get an access token. |
|
98 |
|
99 If the stored token is no longer valid, get a new one and store it. |
|
100 |
|
101 @param requestObject reference to the request object |
|
102 (TranslatorRequest) |
|
103 @return access token (string) |
|
104 """ |
|
105 if ( |
|
106 self.plugin.getPreferences("MsAuthTokenExpire") > |
|
107 QDateTime.currentDateTime() |
|
108 ): |
|
109 return self.plugin.getPreferences("MsAuthToken") |
|
110 |
|
111 # Token expired, get a new one |
|
112 subscriptionKey, valid = self.__getClientDataAzure() |
|
113 if not valid: |
|
114 return "" |
|
115 |
|
116 subscriptionHeader = (b"Ocp-Apim-Subscription-Key", |
|
117 subscriptionKey.encode("utf-8")) |
|
118 response, ok = requestObject.post( |
|
119 QUrl(self.AccessTokenUrl), QByteArray(b""), |
|
120 extraHeaders=[subscriptionHeader]) |
|
121 if ok: |
|
122 self.plugin.setPreferences("MsAuthToken", response) |
|
123 self.plugin.setPreferences( |
|
124 "MsAuthTokenExpire", |
|
125 QDateTime.currentDateTime().addSecs(8 * 60)) |
|
126 return response |
|
127 else: |
|
128 return "" |
|
129 |
|
130 def getTranslation(self, requestObject, text, originalLanguage, |
|
131 translationLanguage): |
|
132 """ |
|
133 Public method to translate the given text. |
|
134 |
|
135 @param requestObject reference to the request object |
|
136 (TranslatorRequest) |
|
137 @param text text to be translated (string) |
|
138 @param originalLanguage language code of the original (string) |
|
139 @param translationLanguage language code of the translation (string) |
|
140 @return tuple of translated text (string) and flag indicating |
|
141 success (boolean) |
|
142 """ |
|
143 subscriptionKey, valid = self.__getClientDataAzure() |
|
144 if not valid: |
|
145 return (self.tr("""You have not registered for the Microsoft""" |
|
146 """ Translation service."""), |
|
147 False) |
|
148 |
|
149 accessToken = self.__getAccessToken(requestObject) |
|
150 if not accessToken: |
|
151 return ( |
|
152 self.tr("MS Translator: No valid access token available."), |
|
153 False |
|
154 ) |
|
155 |
|
156 authHeader = (b"Authorization", |
|
157 "Bearer {0}".format(accessToken).encode("utf-8")) |
|
158 params = "?appid=&from={0}&to={1}&text={2}".format( |
|
159 self.__mapLanguageCode(originalLanguage), |
|
160 self.__mapLanguageCode(translationLanguage), |
|
161 text) |
|
162 url = QUrl(self.TranslatorUrl + params) |
|
163 response, ok = requestObject.get(url, extraHeaders=[authHeader]) |
|
164 if ok: |
|
165 response = str(response, "utf-8", "replace").strip() |
|
166 if ( |
|
167 response.startswith("<string") and |
|
168 response.endswith("</string>") |
|
169 ): |
|
170 result = response.split(">", 1)[1].rsplit("<", 1)[0] |
|
171 else: |
|
172 result = self.tr("MS Translator: No translation available.") |
|
173 ok = False |
|
174 return result, ok |
|
175 |
|
176 def getTextToSpeechData(self, requestObject, text, language): |
|
177 """ |
|
178 Public method to pronounce the given text. |
|
179 |
|
180 @param requestObject reference to the request object |
|
181 (TranslatorRequest) |
|
182 @param text text to be pronounced (string) |
|
183 @param language language code of the text (string) |
|
184 @return tuple with pronounce data (QByteArray) or error string (string) |
|
185 and success flag (boolean) |
|
186 """ |
|
187 subscriptionKey, valid = self.__getClientDataAzure() |
|
188 if not valid: |
|
189 return (self.tr("""You have not registered for the Microsoft""" |
|
190 """ Translation service."""), |
|
191 False) |
|
192 |
|
193 accessToken = self.__getAccessToken(requestObject) |
|
194 if not accessToken: |
|
195 return ( |
|
196 self.tr("MS Translator: No valid access token available."), |
|
197 False |
|
198 ) |
|
199 |
|
200 params = "?language={0}&format={1}&options={2}&text={3}".format( |
|
201 self.__mapLanguageCode(language), |
|
202 "audio/wav", |
|
203 "MaxQuality", |
|
204 text) |
|
205 authHeader = (b"Authorization", |
|
206 "Bearer {0}".format(accessToken).encode("utf-8")) |
|
207 url = QUrl(self.TextToSpeechUrl + params) |
|
208 data, ok = requestObject.get(url, extraHeaders=[authHeader]) |
|
209 if not ok: |
|
210 data = self.tr("MS Translator: No Text-to-Speech for the selected" |
|
211 " language available.") |
|
212 return data, ok |