[multimedia/haruna] src/transcript: transcript: add subtitle parser

George Florea Bănuș <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit c8708e39ff63c883277855420a6b7efe7241aaf9 by George Florea Bănuș, on behalf of M. Sadık Uğursoy.
Committed on 18/07/2026 at 22:47.
Pushed by georgefb into branch 'master'.

transcript: add subtitle parser

M  +13   -0    src/transcript/CMakeLists.txt
A  +21   -0    src/transcript/subtitleline.h     [License: GPL(v3.0+)]
A  +161  -0    src/transcript/subtitleparser.cpp     [License: GPL(v3.0+)]
A  +30   -0    src/transcript/subtitleparser.h     [License: GPL(v3.0+)]

https://invent.kde.org/multimedia/haruna/-/commit/c8708e39ff63c883277855420a6b7efe7241aaf9

diff --git a/src/transcript/CMakeLists.txt b/src/transcript/CMakeLists.txt
index 814118a6..236c0cca 100644
--- a/src/transcript/CMakeLists.txt
+++ b/src/transcript/CMakeLists.txt
@@ -12,8 +12,21 @@ qt_add_qml_module(transcript
     IMPORT_PATH ${CMAKE_BINARY_DIR}
     QML_FILES
         Transcript.qml
+    SOURCES
+        subtitleline.h
+        subtitleparser.h
+        subtitleparser.cpp
+)
+
+target_include_directories(transcript
+    PRIVATE
+        ${AVCODEC_INCLUDE_DIRS}
 )
 
 target_link_libraries(transcript PRIVATE
     Qt6::Core
+
+    KF6::FileMetaData
+
+    utilitiesplugin
 )
diff --git a/src/transcript/subtitleline.h b/src/transcript/subtitleline.h
new file mode 100644
index 00000000..459a1677
--- /dev/null
+++ b/src/transcript/subtitleline.h
@@ -0,0 +1,21 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Muhammet Sadık Uğursoy <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef SUBTITLELINE_H
+#define SUBTITLELINE_H
+
+#include <QString>
+
+struct SubtitleLine {
+    QString text;
+    double startTime;
+    double endTime;
+    QString formattedStartTime;
+    QString formattedEndTime;
+    double duration;
+};
+
+#endif // SUBTITLELINE_H
diff --git a/src/transcript/subtitleparser.cpp b/src/transcript/subtitleparser.cpp
new file mode 100644
index 00000000..a39464e4
--- /dev/null
+++ b/src/transcript/subtitleparser.cpp
@@ -0,0 +1,161 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Muhammet Sadık Uğursoy <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "subtitleparser.h"
+
+#include <QRegularExpression>
+#include <QString>
+#include <QUrl>
+
+#include "miscutils.h"
+#include "subtitleline.h"
+extern "C" {
+#include <libavcodec/avcodec.h>
+#include <libavformat/avformat.h>
+}
+
+using namespace Qt::StringLiterals;
+
+SubtitleParser::SubtitleParser(QObject *parent)
+    : QObject{parent}
+{
+}
+
+void SubtitleParser::parseSubtitle(const QUrl &url, const int streamIndex)
+{
+    if (!url.isLocalFile()) {
+        return;
+    }
+
+    QString path = url.toLocalFile();
+
+    AVFormatContext *fmt_ctx = avformat_alloc_context();
+    if (!fmt_ctx) {
+        return;
+    }
+
+    if (avformat_open_input(&fmt_ctx, path.toUtf8().data(), nullptr, nullptr) < 0) {
+        avformat_free_context(fmt_ctx);
+        return;
+    }
+
+    if (avformat_find_stream_info(fmt_ctx, nullptr) < 0) {
+        avformat_close_input(&fmt_ctx);
+        return;
+    }
+
+    // Initialize the subtitle decoder
+    const AVCodecParameters *codecpar = fmt_ctx->streams[streamIndex]->codecpar;
+    const AVCodec *decoder = avcodec_find_decoder(codecpar->codec_id);
+    if (!decoder) {
+        avformat_close_input(&fmt_ctx);
+        return;
+    }
+
+    AVCodecContext *codecContext = avcodec_alloc_context3(decoder);
+    if (!codecContext) {
+        avformat_close_input(&fmt_ctx);
+        return;
+    }
+
+    if (avcodec_parameters_to_context(codecContext, codecpar) < 0) {
+        avcodec_free_context(&codecContext);
+        avformat_close_input(&fmt_ctx);
+        return;
+    }
+
+    if (avcodec_open2(codecContext, decoder, nullptr) < 0) {
+        avcodec_free_context(&codecContext);
+        avformat_close_input(&fmt_ctx);
+        return;
+    }
+
+    // Read the subtitle data from the codex context
+    AVPacket *packet = av_packet_alloc();
+    if (!packet) {
+        avcodec_free_context(&codecContext);
+        avformat_close_input(&fmt_ctx);
+        return;
+    }
+
+    AVSubtitle subtitle;
+    const AVRational timeBase = fmt_ctx->streams[streamIndex]->time_base;
+    double timeMultiplier = 1.0 * timeBase.num / timeBase.den;
+    int gotSub = 0;
+    while (av_read_frame(fmt_ctx, packet) >= 0) {
+        if (packet->stream_index == int(streamIndex)) {
+            // Decode the subtitle packet
+            int ret = avcodec_decode_subtitle2(codecContext, &subtitle, &gotSub, packet);
+
+            if (ret >= 0 && gotSub != 0) {
+                int64_t startSecs = packet->pts;
+                int64_t duration = packet->duration;
+                int64_t endSecs = startSecs + duration;
+
+                QString text;
+                QString line;
+
+                for (unsigned int i = 0; i < subtitle.num_rects; i++) {
+                    // Note: Even though SUBTITLE_ASS sounds like they are for ASS/SSA only, that is not true.
+                    // Most modern subtitles are handled as SUBTITLE_ASS (srt, sub, ssa, ass, smi) by FFMpeg internally.
+                    // SUBTITLE_TEXT is only used by very simple legacy formats that have no styling capabilities.
+                    switch (subtitle.rects[i]->type) {
+                    case SUBTITLE_TEXT: {
+                        line = QString::fromUtf8(subtitle.rects[i]->text);
+                        break;
+                    }
+                    case SUBTITLE_ASS: {
+                        line = QString::fromUtf8(subtitle.rects[i]->ass);
+                        line = formatASS(line);
+                        break;
+                    }
+                    case SUBTITLE_BITMAP:
+                    case SUBTITLE_NONE: {
+                        break;
+                    }
+                    }
+
+                    text.append(line);
+                }
+
+                SubtitleLine transcriptItem;
+                transcriptItem.text = text;
+                transcriptItem.duration = duration;
+                transcriptItem.startTime = startSecs;
+                transcriptItem.endTime = endSecs;
+                transcriptItem.formattedStartTime = MiscUtils::formatTime(startSecs * timeMultiplier);
+                transcriptItem.formattedEndTime = MiscUtils::formatTime(endSecs * timeMultiplier);
+                Q_EMIT transcriptItemReady(transcriptItem, streamIndex);
+
+                // must be freed when gotSub is set
+                avsubtitle_free(&subtitle);
+            }
+        }
+        av_packet_unref(packet);
+    }
+
+    av_packet_free(&packet);
+    avcodec_free_context(&codecContext);
+    avformat_close_input(&fmt_ctx);
+}
+
+QString SubtitleParser::formatASS(const QString in)
+{
+    // libavc returns the parsed text as a comma separated line, consists of 8 headers and lastly, the text
+    // The text is always guaranteed to be the last one since it can contain commas.
+    // We need to skip the first 8 columns and join the rest.
+    QString text = in.section(QChar::fromLatin1(','), 8);
+
+    // Remove markup text
+    QRegularExpression re(u"\\{[^}]*\\}"_s);
+    while (re.match(text).hasMatch()) {
+        text.remove(re);
+    }
+    // FFMpeg decoder adds \\N as newline for ASS type.
+    text.replace(u"\\N"_s, u"\n"_s, Qt::CaseSensitive);
+
+    return QString::fromUtf8(text.toUtf8());
+}
diff --git a/src/transcript/subtitleparser.h b/src/transcript/subtitleparser.h
new file mode 100644
index 00000000..d2f9a64c
--- /dev/null
+++ b/src/transcript/subtitleparser.h
@@ -0,0 +1,30 @@
+/*
+ * SPDX-FileCopyrightText: 2026 Muhammet Sadık Uğursoy <[email protected]>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#ifndef SUBTITLEPARSER_H
+#define SUBTITLEPARSER_H
+
+#include <QObject>
+
+struct SubtitleLine;
+
+class SubtitleParser : public QObject
+{
+    Q_OBJECT
+
+public:
+    explicit SubtitleParser(QObject *parent = nullptr);
+
+    void parseSubtitle(const QUrl &url, const int streamIndex);
+
+Q_SIGNALS:
+    void transcriptItemReady(const SubtitleLine &item, const int streamIndex);
+
+private:
+    QString formatASS(const QString in);
+};
+
+#endif // SUBTITLEPARSER_H
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.