Program Listing for File transcript_worker.hpp#

Return to documentation for file (src/analysis/transcript_worker.hpp)

#pragma once
#include <QByteArray>
#include <QObject>
#include <QStringList>
#include <memory>

namespace mosaic {

// Runs the Python live-transcription subprocess (analysis/run_live_transcribe.py,
// launched against analysis/.venv — NOT python/.venv, see that script's own
// doc comment for why: faster-whisper/torch already live in the
// mosaic-analysis venv used by the post-hoc diarization pipeline, avoiding
// duplicating that heavy dependency set into python/'s otherwise-light
// mosaic-pose venv) and provides streaming speech-to-text.
//
// Mirrors PoseWorker's shape/lifecycle exactly (start/stop/is_running/
// set_paused/is_paused), but audio is a continuous stream rather than
// discrete frames — see submit_chunk()'s doc comment for the wire format.
class TranscriptWorker : public QObject {
    Q_OBJECT
   public:
    explicit TranscriptWorker(QObject* parent = nullptr);
    ~TranscriptWorker() override;

    // extraArgs is appended after scriptPath (e.g. {"--model", "tiny"}) —
    // unlike PoseWorker's frame_server.py, run_live_transcribe.py takes CLI
    // args for model size, mirroring run_diarize.py's own argparse CLI.
    bool start(const QString& interpreter, const QString& scriptPath,
               const QStringList& extraArgs = {});
    void stop();
    [[nodiscard]] bool is_running() const;

    // Exact same semantics/reasoning as PoseWorker::set_paused() — does NOT
    // tear down the subprocess; submit_chunk() becomes a no-op so the
    // Python side idles on a blocking stdin read. Wired from MainWindow
    // against RecordManager::recording_started/stopped, same as PoseWorker
    // (see main_window.cpp) — a global resource policy, not owned by any tab.
    void set_paused(bool paused);
    [[nodiscard]] bool is_paused() const;

   public slots:
    // pcm16 is interleaved int16 LE PCM, sampleCount * channels * 2 bytes —
    // exactly what AudioRecorder::raw_pcm_ready() emits, unconverted.
    // Written to the subprocess as a 24-byte header (mic index, sample
    // rate, channel count, sample format [always 0 = int16 today], sample
    // count, reserved) followed by the raw PCM bytes — see
    // run_live_transcribe.py's module doc comment for the exact byte layout.
    void submit_chunk(int micIndex, int sampleRate, int channels, QByteArray pcm16);

   signals:
    // One call per confirmed segment (a single Python message can carry
    // more than one — see run_live_transcribe.py's new_final_segments
    // array, split into individual emits here for a simpler C++ consumer).
    void transcript_final(int micIndex, QString text);
    // Replaces (not appends) whatever tentative text was last shown for
    // this mic — may be an empty string, meaning "clear the tentative line".
    void transcript_partial(int micIndex, QString text);
    void process_error(QString message);
    void paused_changed(bool paused);

   private:
    struct Impl;
    std::unique_ptr<Impl> d;
};

} // namespace mosaic