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"
7#include "simtexth.h"
8
9#include <QNetworkRequest>
10#include <QNetworkReply>
11#include <QNetworkAccessManager>
12
13using namespace Qt::Literals::StringLiterals;
14
15QT_BEGIN_NAMESPACE
16
17MachineTranslator::MachineTranslator()
18 : m_request(std::make_unique<QNetworkRequest>()),
19 m_manager(std::make_unique<QNetworkAccessManager>()),
20 m_translator(std::make_unique<Ollama>())
21{
22 m_request->setHeader(QNetworkRequest::ContentTypeHeader, "application/json"_L1);
23 m_request->setTransferTimeout(5 * 60 * 1000);
24}
25
27
28void MachineTranslator::translate(const Messages &messages, const QString &userContext)
29{
30 QMutexLocker locker(&m_queueMutex);
31
32 m_pendingBatches.clear();
33 auto batches = m_translator->makeBatches(messages, userContext);
34
35 for (auto &b : batches)
36 m_pendingBatches.enqueue(std::move(b));
37
38 processNextBatches();
39}
40
41void MachineTranslator::start() noexcept
42{
43 QMutexLocker locker(&m_queueMutex);
44 m_pendingBatches.clear();
45 m_session++;
46 m_stopped = false;
47}
48
49void MachineTranslator::setUrl(const QString &url)
50{
51 m_translator->setUrl(url);
52 m_request->setUrl(m_translator->translationEndpoint());
53}
54
55void MachineTranslator::activateTranslationModel(const QString &modelName)
56{
57 if (auto wakeupPayload = m_translator->stageModel(modelName)) {
58 // after several minutes of being idle, Ollama offloads the model
59 // and the rest API needs to be waken up. Trying to connect with
60 // Ollama for the first time after several minutes wakes up the
61 // server, but for some reason the server doesn't queue the
62 // connection request that was sent to it while it was not awake. As a result,
63 // the connection in QNetworkAccessManager is half broken and not working,
64 // without us knowing about it.
65 // Here we are using a new QNetworkAccessManager instance to send
66 // the first connection request and wake up the model. Then we delete the
67 // QNetworkAccessManager instance since it contains a broken connection
68 // and will try to use it for the next requests otherwise.
69
70 auto *tempManager = new QNetworkAccessManager(this);
71
72 QNetworkRequest wakeupRequest(m_translator->translationEndpoint());
73 wakeupRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"_L1);
74 wakeupRequest.setTransferTimeout(30000);
75
76 QNetworkReply *reply = tempManager->post(wakeupRequest, *wakeupPayload);
77
78 connect(reply, &QNetworkReply::finished, this, [tempManager, reply]() {
79 reply->deleteLater();
80 tempManager->deleteLater();
81 });
82 }
83}
84
86{
87 QNetworkRequest req(m_translator->discoveryEndpoint());
88 QNetworkReply *reply = m_manager->get(req);
89 connect(reply, &QNetworkReply::finished, this, [this, reply]() {
90 reply->deleteLater();
91 QStringList models;
92 if (reply->error() == QNetworkReply::NoError) {
93 const QByteArray response = reply->readAll();
94 models = m_translator->extractModels(response);
95 }
96 emit modelsReceived(std::move(models));
97 });
98}
99
100void MachineTranslator::translateBatch(Batch b)
101{
102 Q_ASSERT_X(!m_queueMutex.tryLock(), Q_FUNC_INFO,
103 "The function requires m_queueMutex to be held.");
104 if (m_stopped)
105 return;
106 m_inFlightCount++;
107 const QByteArray body = m_translator->payload(b);
108 QNetworkReply *reply = m_manager->post(*m_request, body);
109 connect(reply, &QNetworkReply::finished, this,
110 [this, reply, batch = std::move(b), session = m_session.load()] {
111 translationReceived(reply, std::move(batch), session);
112 });
113}
114
115void MachineTranslator::processNextBatches()
116{
117 Q_ASSERT_X(!m_queueMutex.tryLock(), Q_FUNC_INFO,
118 "The function requires m_queueMutex to be held.");
119 if (m_stopped || m_pendingBatches.isEmpty())
120 return;
121
122 const int batchesToSchedule =
123 qMin(s_maxConcurrentBatches - m_inFlightCount, m_pendingBatches.size());
124 for (int i = 0; i < batchesToSchedule; ++i) {
125 Batch batch = m_pendingBatches.dequeue();
126 translateBatch(std::move(batch));
127 }
128}
129
130void MachineTranslator::translationReceived(QNetworkReply *reply, Batch b, int session)
131{
132 reply->deleteLater();
133
134 if (m_stopped || session != m_session) {
135 QMutexLocker locker(&m_queueMutex);
136 m_inFlightCount--;
137 processNextBatches();
138 return;
139 }
140
141 bool shouldRetry = false;
142
143 if (reply->error() != QNetworkReply::NoError) {
144 const bool isRetriableError = reply->error() == QNetworkReply::OperationCanceledError
145 || reply->error() == QNetworkReply::TimeoutError
146 || reply->error() == QNetworkReply::UnknownNetworkError;
147 shouldRetry = b.tries < s_maxTries && isRetriableError;
148 if (!shouldRetry) {
149 QList<const TranslatorMessage *> failed;
150 for (const auto &i : std::as_const(b.items))
151 failed.append(i.msg);
152 emit translationFailed(std::move(failed));
153 }
154 } else {
155 const QByteArray response = reply->readAll();
156 QList<Item> items = std::move(b.items);
157 QHash<const TranslatorMessage *, QString> out;
158 QHash<QString, QString> translations = m_translator->extractTranslations(response);
159
160 // First pass: exact matches
161 QList<Item> nonMatched;
162 for (Item &i : items) {
163 if (i.msg->translation().isEmpty()) {
164 if (auto translation = translations.find(i.msg->sourceText());
165 translation != translations.end()) {
166 out[i.msg] = *translation;
167 translations.erase(translation);
168 } else {
169 nonMatched.append(std::move(i));
170 }
171 }
172 }
173
174 // Second pass: fuzzy matching for non-matched items with unused translations
175 constexpr int similarityThreshold = 200;
176 for (Item &i : nonMatched) {
177 StringSimilarityMatcher matcher(i.msg->sourceText());
178 QString bestMatch;
179 int bestScore = 0;
180 for (auto it = translations.cbegin(); it != translations.cend(); ++it) {
181 const int score = matcher.getSimilarityScore(it.key());
182 if (score >= similarityThreshold && score > bestScore) {
183 bestScore = score;
184 bestMatch = it.key();
185 }
186 }
187
188 if (!bestMatch.isEmpty())
189 out[i.msg] = translations.take(bestMatch);
190 else
191 b.items.append(std::move(i));
192 }
193
194 const bool nonTranslatedItems = !b.items.empty();
195 shouldRetry = nonTranslatedItems && b.tries < s_maxTries;
196 if (nonTranslatedItems && !shouldRetry) {
197 QList<const TranslatorMessage *> failed;
198 for (const auto &i : std::as_const(b.items))
199 failed.append(i.msg);
200 emit translationFailed(std::move(failed));
201 }
202 if (!out.empty())
203 emit batchTranslated(std::move(out));
204 }
205
206 QMutexLocker locker(&m_queueMutex);
207 m_inFlightCount--;
208 if (shouldRetry) {
209 b.tries++;
210 m_pendingBatches.prepend(std::move(b)); // Front of queue for priority
211 }
212 processNextBatches();
213}
214
215QT_END_NAMESPACE
void start() noexcept
void setUrl(const QString &url)
void translate(const Messages &messages, const QString &userContext=QString())
void activateTranslationModel(const QString &modelName)