File indexing completed on 2025-04-27 03:58:39

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2018-02-22
0007  * Description : A text translator using web-services - TTS support.
0008  *
0009  * SPDX-FileCopyrightText: 2018-2022 by Hennadii Chernyshchyk <genaloner at gmail dot com>
0010  * SPDX-FileCopyrightText: 2021-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0011  *
0012  * SPDX-License-Identifier: GPL-2.0-or-later
0013  *
0014  * ============================================================ */
0015 
0016 #include "donlinetranslator_p.h"
0017 
0018 namespace Digikam
0019 {
0020 
0021 DOnlineTts::DOnlineTts(QObject* const parent)
0022     : QObject(parent),
0023       d(new Private)
0024 {
0025 }
0026 
0027 DOnlineTts::~DOnlineTts()
0028 {
0029     delete d;
0030 }
0031 
0032 void DOnlineTts::generateUrls(const QString& text,
0033                               DOnlineTranslator::Engine engine,
0034                               DOnlineTranslator::Language lang,
0035                               Voice voice,
0036                               Emotion emotion)
0037 {
0038     // Get speech
0039 
0040     QString unparsedText = text;
0041 
0042     switch (engine)
0043     {
0044         case DOnlineTranslator::Google:
0045         {
0046             if (voice != NoVoice)
0047             {
0048                 setError(UnsupportedVoice, i18nc("@info", "Selected engine %1 does not support voice settings",
0049                                                  QString::fromUtf8(QMetaEnum::fromType<DOnlineTranslator::Engine>().valueToKey(engine))));
0050                 return;
0051             }
0052 
0053             if (emotion != NoEmotion)
0054             {
0055                 setError(UnsupportedEmotion, i18nc("@info", "Selected engine %1 does not support emotion settings",
0056                                                    QString::fromUtf8(QMetaEnum::fromType<DOnlineTranslator::Engine>().valueToKey(engine))));
0057                 return;
0058             }
0059 
0060             const QString langString = languageApiCode(engine, lang);
0061 
0062             if (langString.isNull())
0063                 return;
0064 
0065             // Google has a limit of characters per tts request. If the query is larger, then it should be splited into several
0066 
0067             while (!unparsedText.isEmpty())
0068             {
0069                 const int splitIndex = DOnlineTranslator::getSplitIndex(unparsedText, d->s_googleTtsLimit); // Split the part by special symbol
0070 
0071                 // Generate URL API for add it to the playlist
0072 
0073                 QUrl apiUrl(QStringLiteral("https://translate.googleapis.com/translate_tts"));
0074                 const QString query = QStringLiteral("ie=UTF-8&client=gtx&tl=%1&q=%2")
0075                                     .arg(langString, QString::fromUtf8(QUrl::toPercentEncoding(unparsedText.left(splitIndex))));
0076 
0077 #if defined(Q_OS_LINUX)
0078 
0079                 apiUrl.setQuery(query);
0080 
0081 #elif defined(Q_OS_WIN)
0082 
0083                 apiUrl.setQuery(query, QUrl::DecodedMode);
0084 
0085 #endif
0086                 d->media.append(apiUrl);
0087 
0088                 // Remove the said part from the next saying
0089 
0090                 unparsedText = unparsedText.mid(splitIndex);
0091             }
0092 
0093             break;
0094         }
0095 
0096         case DOnlineTranslator::Yandex:
0097         {
0098             const QString langString = languageApiCode(engine, lang);
0099 
0100             if (langString.isNull())
0101                 return;
0102 
0103             const QString voiceString = voiceApiCode(engine, voice);
0104 
0105             if (voiceString.isNull())
0106                 return;
0107 
0108             const QString emotionString = emotionApiCode(engine, emotion);
0109 
0110             if (emotionString.isNull())
0111                 return;
0112 
0113             // Yandex has a limit of characters per tts request. If the query is larger, then it should be splited into several
0114 
0115             while (!unparsedText.isEmpty())
0116             {
0117                 const int splitIndex = DOnlineTranslator::getSplitIndex(unparsedText, d->s_yandexTtsLimit); // Split the part by special symbol
0118 
0119                 // Generate URL API for add it to the playlist
0120                 QUrl apiUrl(QStringLiteral("https://tts.voicetech.yandex.net/tts"));
0121                 const QString query = QStringLiteral("text=%1&lang=%2&speaker=%3&emotion=%4&format=mp3")
0122                                         .arg(QString::fromUtf8(QUrl::toPercentEncoding(unparsedText.left(splitIndex))), langString, voiceString, emotionString);
0123 
0124 #if defined(Q_OS_LINUX)
0125 
0126                 apiUrl.setQuery(query);
0127 
0128 #elif defined(Q_OS_WIN)
0129 
0130                 apiUrl.setQuery(query, QUrl::DecodedMode);
0131 
0132 #endif
0133 
0134                 d->media.append(apiUrl);
0135 
0136                 // Remove the said part from the next saying
0137 
0138                 unparsedText = unparsedText.mid(splitIndex);
0139             }
0140 
0141             break;
0142         }
0143 
0144         case DOnlineTranslator::Bing:
0145         case DOnlineTranslator::LibreTranslate:
0146         case DOnlineTranslator::Lingva:
0147         {
0148             // NOTE:
0149             // Lingva returns audio in strange format, use placeholder, until we'll figure it out
0150             //
0151             // Example: https://lingva.ml/api/v1/audio/en/Hello%20World!
0152             // Will return json with uint bytes array, according to documentation
0153             // See: https://github.com/TheDavidDelta/lingva-translate#public-apis
0154             setError(UnsupportedEngine, i18nc("@info", "%1 engine does not support TTS",
0155                                               QString::fromUtf8(QMetaEnum::fromType<DOnlineTranslator::Engine>().valueToKey(engine))));
0156             break;
0157         }
0158     }
0159 }
0160 
0161 QList<QUrl> DOnlineTts::media() const
0162 {
0163     return d->media;
0164 }
0165 
0166 QString DOnlineTts::errorString() const
0167 {
0168     return d->errorString;
0169 }
0170 
0171 DOnlineTts::TtsError DOnlineTts::error() const
0172 {
0173     return d->error;
0174 }
0175 
0176 QString DOnlineTts::voiceCode(Voice voice)
0177 {
0178     return Private::s_voiceCodes.value(voice);
0179 }
0180 
0181 QString DOnlineTts::emotionCode(Emotion emotion)
0182 {
0183     return Private::s_emotionCodes.value(emotion);
0184 }
0185 
0186 DOnlineTts::Emotion DOnlineTts::emotion(const QString& emotionCode)
0187 {
0188     return Private::s_emotionCodes.key(emotionCode, NoEmotion);
0189 }
0190 
0191 DOnlineTts::Voice DOnlineTts::voice(const QString& voiceCode)
0192 {
0193     return Private::s_voiceCodes.key(voiceCode, NoVoice);
0194 }
0195 
0196 void DOnlineTts::setError(TtsError error, const QString& errorString)
0197 {
0198     d->error       = error;
0199     d->errorString = errorString;
0200 }
0201 
0202 // Returns engine-specific language code for tts
0203 QString DOnlineTts::languageApiCode(DOnlineTranslator::Engine engine, DOnlineTranslator::Language lang)
0204 {
0205     switch (engine)
0206     {
0207         case DOnlineTranslator::Google:
0208         case DOnlineTranslator::Lingva: // Lingva is a frontend to Google Translate
0209         {
0210             if (lang != DOnlineTranslator::Auto)
0211             {
0212                 return DOnlineTranslator::languageApiCode(engine, lang); // Google use the same codes for tts (except 'auto')
0213             }
0214 
0215             break;
0216         }
0217 
0218         case DOnlineTranslator::Yandex:
0219         {
0220             switch (lang)
0221             {
0222                 case DOnlineTranslator::Russian:
0223                 {
0224                     return QStringLiteral("ru_RU");
0225                 }
0226 
0227                 case DOnlineTranslator::Tatar:
0228                 {
0229                     return QStringLiteral("tr_TR");
0230                 }
0231 
0232                 case DOnlineTranslator::English:
0233                 {
0234                     return QStringLiteral("en_GB");
0235                 }
0236 
0237                 default:
0238                 {
0239                     break;
0240                 }
0241             }
0242 
0243             break;
0244         }
0245 
0246         default:
0247         {
0248             break;
0249         }
0250     }
0251 
0252     setError(UnsupportedLanguage, i18nc("@info", "Selected language %1 is not supported for %2",
0253                                         QString::fromUtf8(QMetaEnum::fromType<DOnlineTranslator::Language>().valueToKey(lang)),
0254                                         QString::fromUtf8(QMetaEnum::fromType<DOnlineTranslator::Engine>().valueToKey(engine))));
0255 
0256     return QString();
0257 }
0258 
0259 QString DOnlineTts::voiceApiCode(DOnlineTranslator::Engine engine, Voice voice)
0260 {
0261     if (engine == DOnlineTranslator::Yandex)
0262     {
0263         if (voice == NoVoice)
0264         {
0265             return voiceCode(Zahar);
0266         }
0267 
0268         return voiceCode(voice);
0269     }
0270 
0271     setError(UnsupportedVoice, i18nc("@info", "Selected voice %1 is not supported for %2",
0272                                      QString::fromUtf8(QMetaEnum::fromType<Voice>().valueToKey(voice)),
0273                                      QString::fromUtf8(QMetaEnum::fromType<DOnlineTranslator::Engine>().valueToKey(engine))));
0274 
0275     return QString();
0276 }
0277 
0278 QString DOnlineTts::emotionApiCode(DOnlineTranslator::Engine engine, Emotion emotion)
0279 {
0280     if (engine == DOnlineTranslator::Yandex)
0281     {
0282         if (emotion == NoEmotion)
0283         {
0284             return emotionCode(Neutral);
0285         }
0286 
0287         return emotionCode(emotion);
0288     }
0289 
0290     setError(UnsupportedEmotion, i18nc("@info", "Selected emotion %1 is not supported for %2",
0291                                        QString::fromUtf8(QMetaEnum::fromType<Emotion>().valueToKey(emotion)),
0292                                        QString::fromUtf8(QMetaEnum::fromType<DOnlineTranslator::Engine>().valueToKey(engine))));
0293 
0294     return QString();
0295 }
0296 
0297 } // namespace Digikam