Source code

Revision control

Copy as Markdown

// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "machinetranslator.h"
#include "ollama.h"
#include "openaicompatible.h"
#include "translatormessage.h"
#include "translationsettings.h"
#include "simtexth.h"
#include <QtNetwork/qnetworkaccessmanager.h>
#include <QtNetwork/qnetworkreply.h>
#include <QtNetwork/qnetworkrequest.h>
using namespace Qt::Literals::StringLiterals;
QT_BEGIN_NAMESPACE
MachineTranslator::MachineTranslator()
: m_request(std::make_unique<QNetworkRequest>()),
m_manager(std::make_unique<QNetworkAccessManager>())
{
m_request->setHeader(QNetworkRequest::ContentTypeHeader, "application/json"_L1);
m_request->setTransferTimeout(TranslationSettings::transferTimeoutMs());
}
void MachineTranslator::setApiType(TranslationApiType type)
{
switch (type) {
case TranslationApiType::Ollama:
m_translator = std::make_unique<Ollama>();
break;
case TranslationApiType::OpenAICompatible:
m_translator = std::make_unique<OpenAICompatible>();
break;
}
}
MachineTranslator::~MachineTranslator() = default;
void MachineTranslator::translate(const Messages &messages, const QString &userContext)
{
QMutexLocker locker(&m_queueMutex);
m_pendingBatches.clear();
auto batches = m_translator->makeBatches(messages, userContext);
for (auto &b : batches)
m_pendingBatches.enqueue(std::move(b));
processNextBatches();
}
void MachineTranslator::start() noexcept
{
QMutexLocker locker(&m_queueMutex);
m_pendingBatches.clear();
m_session++;
m_stopped = false;
}
void MachineTranslator::setUrl(const QString &url)
{
m_translator->setUrl(url);
m_request->setUrl(m_translator->translationEndpoint());
}
void MachineTranslator::setApiKey(const QString &apiKey)
{
if (!apiKey.isEmpty())
m_request->setRawHeader("Authorization", "Bearer " + apiKey.toUtf8());
else
m_request->setRawHeader("Authorization", QByteArray());
}
void MachineTranslator::activateTranslationModel(const QString &modelName)
{
if (auto wakeupPayload = m_translator->stageModel(modelName)) {
// after several minutes of being idle, Ollama offloads the model
// and the rest API needs to be waken up. Trying to connect with
// Ollama for the first time after several minutes wakes up the
// server, but for some reason the server doesn't queue the
// connection request that was sent to it while it was not awake. As a result,
// the connection in QNetworkAccessManager is half broken and not working,
// without us knowing about it.
// Here we are using a new QNetworkAccessManager instance to send
// the first connection request and wake up the model. Then we delete the
// QNetworkAccessManager instance since it contains a broken connection
// and will try to use it for the next requests otherwise.
auto *tempManager = new QNetworkAccessManager(this);
QNetworkRequest wakeupRequest(m_translator->translationEndpoint());
wakeupRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"_L1);
wakeupRequest.setTransferTimeout(30000);
QNetworkReply *reply = tempManager->post(wakeupRequest, *wakeupPayload);
connect(reply, &QNetworkReply::finished, this, [tempManager, reply]() {
reply->deleteLater();
tempManager->deleteLater();
});
}
}
void MachineTranslator::requestModels()
{
QNetworkRequest req(m_translator->discoveryEndpoint());
// Copy Authorization header for APIs that require authentication (e.g., OpenRouter)
if (m_request->hasRawHeader("Authorization"))
req.setRawHeader("Authorization", m_request->rawHeader("Authorization"));
QNetworkReply *reply = m_manager->get(req);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
QStringList models;
if (reply->error() == QNetworkReply::NoError) {
const QByteArray response = reply->readAll();
models = m_translator->extractModels(response);
}
emit modelsReceived(std::move(models));
});
}
void MachineTranslator::translateBatch(Batch b)
{
Q_ASSERT_X(!m_queueMutex.tryLock(), Q_FUNC_INFO,
"The function requires m_queueMutex to be held.");
if (m_stopped)
return;
m_inFlightCount++;
const QByteArray body = m_translator->payload(b);
emit debugLog(body, false);
QNetworkReply *reply = m_manager->post(*m_request, body);
connect(reply, &QNetworkReply::finished, this,
[this, reply, batch = std::move(b), session = m_session.load()] {
translationReceived(reply, std::move(batch), session);
});
}
void MachineTranslator::processNextBatches()
{
Q_ASSERT_X(!m_queueMutex.tryLock(), Q_FUNC_INFO,
"The function requires m_queueMutex to be held.");
if (m_stopped || m_pendingBatches.isEmpty())
return;
const int maxConcurrent = TranslationSettings::maxConcurrentBatches();
const int batchesToSchedule = qMin(maxConcurrent - m_inFlightCount, m_pendingBatches.size());
for (int i = 0; i < batchesToSchedule; ++i) {
Batch batch = m_pendingBatches.dequeue();
translateBatch(std::move(batch));
}
}
void MachineTranslator::translationReceived(QNetworkReply *reply, Batch b, int session)
{
reply->deleteLater();
if (m_stopped || session != m_session) {
QMutexLocker locker(&m_queueMutex);
m_inFlightCount--;
processNextBatches();
return;
}
bool shouldRetry = false;
const QByteArray response = reply->readAll();
emit debugLog(response, true);
const int maxRetries = TranslationSettings::maxRetries();
if (reply->error() != QNetworkReply::NoError) {
const auto error = reply->error();
if (error == QNetworkReply::ProtocolInvalidOperationError)
m_translator->onRequestRejected();
const bool isRetriableError = error == QNetworkReply::OperationCanceledError
|| error == QNetworkReply::TimeoutError
|| error == QNetworkReply::UnknownNetworkError
|| error == QNetworkReply::ProtocolInvalidOperationError;
shouldRetry = b.tries < maxRetries && isRetriableError;
if (!shouldRetry) {
QList<const TranslatorMessage *> failed;
for (const auto &i : std::as_const(b.items))
failed.append(i.msg);
emit translationFailed(std::move(failed));
}
} else {
QList<Item> items = std::move(b.items);
QHash<const TranslatorMessage *, QStringList> out;
QHash<QString, QStringList> translations =
m_translator->extractTranslations(response, b.pluralFormsCount > 1);
// First pass: exact matches
QList<Item> nonMatched;
for (Item &i : items) {
if (i.msg->translation().isEmpty()) {
if (auto translation = translations.find(i.msg->sourceText());
translation != translations.end()) {
out[i.msg] = *translation;
translations.erase(translation);
} else {
nonMatched.append(std::move(i));
}
}
}
// Second pass: fuzzy matching for non-matched items with unused translations
constexpr int similarityThreshold = 200;
for (Item &i : nonMatched) {
StringSimilarityMatcher matcher(i.msg->sourceText());
QString bestMatch;
int bestScore = 0;
for (auto it = translations.cbegin(); it != translations.cend(); ++it) {
const int score = matcher.getSimilarityScore(it.key());
if (score >= similarityThreshold && score > bestScore) {
bestScore = score;
bestMatch = it.key();
}
}
if (!bestMatch.isEmpty())
out[i.msg] = translations.take(bestMatch);
else
b.items.append(std::move(i));
}
const bool nonTranslatedItems = !b.items.empty();
shouldRetry = nonTranslatedItems && b.tries < maxRetries;
if (nonTranslatedItems && !shouldRetry) {
QList<const TranslatorMessage *> failed;
for (const auto &i : std::as_const(b.items))
failed.append(i.msg);
emit translationFailed(std::move(failed));
}
if (!out.empty())
emit batchTranslated(std::move(out));
}
QMutexLocker locker(&m_queueMutex);
m_inFlightCount--;
if (shouldRetry) {
b.tries++;
m_pendingBatches.prepend(std::move(b)); // Front of queue for priority
}
processNextBatches();
}
QT_END_NAMESPACE