#pragma once #include "llmclient.h" #include "subtitleextractor.h" #include "glossarystore.h" #include #include #include #include #include #include #include #include #include #include class QNetworkAccessManager; // Translates a whole subtitle track with an LLM, batch by batch, caches the result as an // .srt file and saves a copy next to the video (see TranslationStore). class TranslationService : public QObject { Q_OBJECT QML_ELEMENT Q_PROPERTY(bool busy READ isBusy NOTIFY busyChanged) Q_PROPERTY(double progress READ progress NOTIFY progressChanged) Q_PROPERTY(QString statusText READ statusText NOTIFY statusTextChanged) Q_PROPERTY(QString errorText READ errorText NOTIFY errorTextChanged) Q_PROPERTY(QStringList availableModels READ availableModels NOTIFY availableModelsChanged) Q_PROPERTY(QVariantList translationPresets READ translationPresets CONSTANT) Q_PROPERTY(bool fetchingModels READ isFetchingModels NOTIFY fetchingModelsChanged) Q_PROPERTY(QVariantList glossaryEntries READ glossaryEntries NOTIFY glossaryChanged) Q_PROPERTY(QString glossaryDirectory READ glossaryDirectory NOTIFY glossaryChanged) Q_PROPERTY(QString glossaryEpisode READ glossaryEpisode NOTIFY glossaryChanged) Q_PROPERTY(QStringList glossaryEpisodes READ glossaryEpisodes NOTIFY glossaryChanged) Q_PROPERTY(QString glossaryError READ glossaryError NOTIFY glossaryErrorChanged) // The video playing. Its translations are shown when ready; others are only saved. // Opening another video stops a single translation but not a playlist one. Q_PROPERTY(QUrl currentVideo READ currentVideo WRITE setCurrentVideo NOTIFY currentVideoChanged) public: explicit TranslationService(QObject *parent = nullptr); ~TranslationService() override; bool isBusy() const; double progress() const; QString statusText() const; QString errorText() const; QStringList availableModels() const; QVariantList translationPresets() const; bool isFetchingModels() const; QVariantList glossaryEntries() const; QString glossaryDirectory() const; QString glossaryEpisode() const; QStringList glossaryEpisodes() const; QString glossaryError() const; QUrl currentVideo() const; void setCurrentVideo(const QUrl &url); // \a track is an entry of MpvItem::subtitleTracks. Q_INVOKABLE void translate(const QUrl &videoUrl, const QVariantMap &track, const QString &mediaTitle); // Translates every video in \a urls that has no saved translation yet, one after // another, starting at the current video. The current one uses \a currentTrack when // set; the others use the embedded subtitle findTranslationSource() picks. Q_INVOKABLE void translatePlaylist(const QList &urls, const QVariantMap ¤tTrack, const QString ¤tTitle); Q_INVOKABLE void cancel(); // Asks the API configured in the settings for its models. Q_INVOKABLE void fetchModels(); Q_INVOKABLE void clearError(); Q_INVOKABLE void forgetTranslation(); // Path of the translation of \a videoUrl into the target language set in Settings, // or an empty string when there is none. Q_INVOKABLE QString savedTranslation(const QUrl &videoUrl) const; Q_INVOKABLE void loadGlossary(const QUrl &videoUrl); Q_INVOKABLE bool saveGlossary(const QVariantList &entries, const QStringList &episodes = {}); Q_INVOKABLE void clearGlossary(); // File operations use the dialog draft; import only commits when Save is clicked. Q_INVOKABLE QVariantMap importGlossaryFile(const QUrl &url, const QVariantList &entries, const QStringList &episodes); Q_INVOKABLE QVariantMap exportGlossaryFile(const QUrl &url, const QVariantList &entries, const QStringList &episodes); Q_INVOKABLE QStringList sortedGlossaryEpisodes(const QStringList &episodes) const; Q_SIGNALS: void busyChanged(); void progressChanged(); void statusTextChanged(); void errorTextChanged(); void availableModelsChanged(); void fetchingModelsChanged(); void glossaryChanged(); void glossaryErrorChanged(); void translated(const QUrl &subtitleUrl, bool fromCache); void currentVideoChanged(); // A playlist translation ended without being cancelled. Problems are in errorText. void playlistTranslated(int translatedCount, int alreadyTranslatedCount, int problemCount); private: struct Job; struct Batches; // Where translateVideo() left a translation, or why it could not make one. struct Outcome { QString cacheFile; QString suffix; QString configKey; bool fromCache = false; QString error; // The error is about this video's subtitles; other videos may still work. bool skippable = false; static Outcome ready(const QString &cacheFile, const QString &suffix, const QString &configKey, bool fromCache) { Outcome outcome; outcome.cacheFile = cacheFile; outcome.suffix = suffix; outcome.configKey = configKey; outcome.fromCache = fromCache; return outcome; } static Outcome failed(const QString &error, bool skippable = false) { Outcome outcome; outcome.error = error; outcome.skippable = skippable; return outcome; } }; QCoro::Task<> run(std::shared_ptr job, QUrl videoUrl, QVariantMap track); QCoro::Task<> runPlaylist(std::shared_ptr job, QList urls, QVariantMap currentTrack, QString currentTitle); // Reads the settings and the API key for \a job; nothing (after failing it) when they are not usable. QCoro::Task> startJob(std::shared_ptr job); QCoro::Task translateVideo(std::shared_ptr job, LlmClient::Config config, QUrl videoUrl, QVariantMap track); QCoro::Task<> runFetchModels(); // Translates \a lines, retrying failures and asking again for lines the model skipped. QCoro::Task translateLines(std::shared_ptr job, QStringList lines, QStringList context); // Takes batches from \a batches until none are left; several run side by side. QCoro::Task<> translateBatches(std::shared_ptr job, std::shared_ptr batches); QCoro::Task loadCues(std::shared_ptr job, QUrl videoUrl, QVariantMap track); // Reads the settings and the API key. The second value is an error message when // something is missing. QCoro::Task> loadConfig(); void fail(const std::shared_ptr &job, const QString &error); // Saves the translation where players find it and, for the current video, shows it. // Returns an error message on failure. QString publish(const std::shared_ptr &job, const QUrl &videoUrl, const Outcome &outcome); void endJob(const std::shared_ptr &job); // In a playlist, prefixes the episode number and counts finished episodes in the progress. void setJobStatus(const Job &job, const QString &text); void setJobProgress(const Job &job, double episodeProgress); void setLinesStatus(const Job &job, qsizetype done, qsizetype total); void setBusy(bool busy); void setProgress(double progress); void setStatusText(const QString &text); void setErrorText(const QString &text); void setFetchingModels(bool fetching); void applyGlossaryEdits(const GlossaryStore::Glossary &previous, const GlossaryStore::Glossary &updated); QNetworkAccessManager *m_network; std::shared_ptr m_job; bool m_busy = false; double m_progress = 0.0; QString m_statusText; QString m_errorText; QString m_glossaryError; QStringList m_availableModels; bool m_fetchingModels = false; GlossaryStore::Glossary m_glossary; QString m_glossaryEpisode; QString m_lastTranslationPath; QUrl m_currentVideo; };