Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
machinetranslator.cpp
Go to the documentation of this file.
1// Copyright (C) 2025 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
5#include "ollama.h"
9#include "simtexth.h"
10
11#include <QtNetwork/qnetworkaccessmanager.h>
12#include <QtNetwork/qnetworkreply.h>
13#include <QtNetwork/qnetworkrequest.h>
14
15using namespace Qt::Literals::StringLiterals;
16
17QT_BEGIN_NAMESPACE
18
19MachineTranslator::MachineTranslator()
20 : m_request(std::make_unique<QNetworkRequest>()),
21 m_manager(std::make_unique<QNetworkAccessManager>())
22{
23 m_request->setHeader(QNetworkRequest::ContentTypeHeader, "application/json"_L1);
24 m_request->setTransferTimeout(TranslationSettings::transferTimeoutMs());
25}
26
28{
29 switch (type) {
30 case TranslationApiType::Ollama:
31 m_translator = std::make_unique<Ollama>();
32 break;
33 case TranslationApiType::OpenAICompatible:
34 m_translator = std::make_unique<OpenAICompatible>();
35 break;
36 }
37}
38
40
41void MachineTranslator::translate(const Messages &messages, const QString &userContext)
42{
43 QMutexLocker locker(&m_queueMutex);
44
45 m_pendingBatches.clear();
46 auto batches = m_translator->makeBatches(messages, userContext);
47
48 for (auto &b : batches)
49 m_pendingBatches.enqueue(std::move(b));
50
51 processNextBatches();
52}
53
54void MachineTranslator::start() noexcept
55{
56 QMutexLocker locker(&m_queueMutex);
57 m_pendingBatches.clear();
58 m_session++;
59 m_stopped = false;
60}
61
62void MachineTranslator::setUrl(const QString &url)
63{
64 m_translator->setUrl(url);
65 m_request->setUrl(m_translator->translationEndpoint());
66}
67
68void MachineTranslator::setApiKey(const QString &apiKey)
69{
70 if (!apiKey.isEmpty())
71 m_request->setRawHeader("Authorization", "Bearer " + apiKey.toUtf8());
72 else
73 m_request->setRawHeader("Authorization", QByteArray());
74}
75
76void MachineTranslator::activateTranslationModel(const QString &modelName)
77{
78 if (auto wakeupPayload = m_translator->stageModel(modelName)) {
79 // after several minutes of being idle, Ollama offloads the model
80 // and the rest API needs to be waken up. Trying to connect with
81 // Ollama for the first time after several minutes wakes up the
82 // server, but for some reason the server doesn't queue the
83 // connection request that was sent to it while it was not awake. As a result,
84 // the connection in QNetworkAccessManager is half broken and not working,
85 // without us knowing about it.
86 // Here we are using a new QNetworkAccessManager instance to send
87 // the first connection request and wake up the model. Then we delete the
88 // QNetworkAccessManager instance since it contains a broken connection
89 // and will try to use it for the next requests otherwise.
90
91 auto *tempManager = new QNetworkAccessManager(this);
92
93 QNetworkRequest wakeupRequest(m_translator->translationEndpoint());
94 wakeupRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"_L1);
95 wakeupRequest.setTransferTimeout(30000);
96
97 QNetworkReply *reply = tempManager->post(wakeupRequest, *wakeupPayload);
98
99 connect(reply, &QNetworkReply::finished, this, [tempManager, reply]() {
100 reply->deleteLater();
101 tempManager->deleteLater();
102 });
103 }
104}
105
107{
108 QNetworkRequest req(m_translator->discoveryEndpoint());
109 // Copy Authorization header for APIs that require authentication (e.g., OpenRouter)
110 if (m_request->hasRawHeader("Authorization"))
111 req.setRawHeader("Authorization", m_request->rawHeader("Authorization"));
112 QNetworkReply *reply = m_manager->get(req);
113 connect(reply, &QNetworkReply::finished, this, [this, reply]() {
114 reply->deleteLater();
115 QStringList models;
116 if (reply->error() == QNetworkReply::NoError) {
117 const QByteArray response = reply->readAll();
118 models = m_translator->extractModels(response);
119 }
120 emit modelsReceived(std::move(models));
121 });
122}
123
124void MachineTranslator::translateBatch(Batch b)
125{
126 Q_ASSERT_X(!m_queueMutex.tryLock(), Q_FUNC_INFO,
127 "The function requires m_queueMutex to be held.");
128 if (m_stopped)
129 return;
130 m_inFlightCount++;
131 const QByteArray body = m_translator->payload(b);
132 emit debugLog(body, false);
133 QNetworkReply *reply = m_manager->post(*m_request, body);
134 connect(reply, &QNetworkReply::finished, this,
135 [this, reply, batch = std::move(b), session = m_session.load()] {
136 translationReceived(reply, std::move(batch), session);
137 });
138}
139
140void MachineTranslator::processNextBatches()
141{
142 Q_ASSERT_X(!m_queueMutex.tryLock(), Q_FUNC_INFO,
143 "The function requires m_queueMutex to be held.");
144 if (m_stopped || m_pendingBatches.isEmpty())
145 return;
146
147 const int maxConcurrent = TranslationSettings::maxConcurrentBatches();
148 const int batchesToSchedule = qMin(maxConcurrent - m_inFlightCount, m_pendingBatches.size());
149 for (int i = 0; i < batchesToSchedule; ++i) {
150 Batch batch = m_pendingBatches.dequeue();
151 translateBatch(std::move(batch));
152 }
153}
154
155void MachineTranslator::translationReceived(QNetworkReply *reply, Batch b, int session)
156{
157 reply->deleteLater();
158
159 if (m_stopped || session != m_session) {
160 QMutexLocker locker(&m_queueMutex);
161 m_inFlightCount--;
162 processNextBatches();
163 return;
164 }
165
166 bool shouldRetry = false;
167 const QByteArray response = reply->readAll();
168 emit debugLog(response, true);
169 const int maxRetries = TranslationSettings::maxRetries();
170
171 if (reply->error() != QNetworkReply::NoError) {
172 const auto error = reply->error();
173
174 if (error == QNetworkReply::ProtocolInvalidOperationError)
175 m_translator->onRequestRejected();
176
177 const bool isRetriableError = error == QNetworkReply::OperationCanceledError
178 || error == QNetworkReply::TimeoutError
179 || error == QNetworkReply::UnknownNetworkError
180 || error == QNetworkReply::ProtocolInvalidOperationError;
181 shouldRetry = b.tries < maxRetries && isRetriableError;
182 if (!shouldRetry) {
183 QList<const TranslatorMessage *> failed;
184 for (const auto &i : std::as_const(b.items))
185 failed.append(i.msg);
186 emit translationFailed(std::move(failed));
187 }
188 } else {
189 QList<Item> items = std::move(b.items);
190 QHash<const TranslatorMessage *, QStringList> out;
191 QHash<QString, QStringList> translations =
192 m_translator->extractTranslations(response, b.pluralFormsCount > 1);
193
194 // First pass: exact matches
195 QList<Item> nonMatched;
196 for (Item &i : items) {
197 if (i.msg->translation().isEmpty()) {
198 if (auto translation = translations.find(i.msg->sourceText());
199 translation != translations.end()) {
200 out[i.msg] = *translation;
201 translations.erase(translation);
202 } else {
203 nonMatched.append(std::move(i));
204 }
205 }
206 }
207
208 // Second pass: fuzzy matching for non-matched items with unused translations
209 constexpr int similarityThreshold = 200;
210 for (Item &i : nonMatched) {
211 StringSimilarityMatcher matcher(i.msg->sourceText());
212 QString bestMatch;
213 int bestScore = 0;
214 for (auto it = translations.cbegin(); it != translations.cend(); ++it) {
215 const int score = matcher.getSimilarityScore(it.key());
216 if (score >= similarityThreshold && score > bestScore) {
217 bestScore = score;
218 bestMatch = it.key();
219 }
220 }
221
222 if (!bestMatch.isEmpty())
223 out[i.msg] = translations.take(bestMatch);
224 else
225 b.items.append(std::move(i));
226 }
227
228 const bool nonTranslatedItems = !b.items.empty();
229 shouldRetry = nonTranslatedItems && b.tries < maxRetries;
230 if (nonTranslatedItems && !shouldRetry) {
231 QList<const TranslatorMessage *> failed;
232 for (const auto &i : std::as_const(b.items))
233 failed.append(i.msg);
234 emit translationFailed(std::move(failed));
235 }
236 if (!out.empty())
237 emit batchTranslated(std::move(out));
238 }
239
240 QMutexLocker locker(&m_queueMutex);
241 m_inFlightCount--;
242 if (shouldRetry) {
243 b.tries++;
244 m_pendingBatches.prepend(std::move(b)); // Front of queue for priority
245 }
246 processNextBatches();
247}
248
249QT_END_NAMESPACE
void start() noexcept
void setUrl(const QString &url)
void translate(const Messages &messages, const QString &userContext=QString())
void activateTranslationModel(const QString &modelName)
void setApiType(TranslationApiType type)
void setApiKey(const QString &apiKey)
TranslationApiType