errogaht/transcribe-bundle

Provider-agnostic audio, video, diarization and live speech transcription for Symfony.

Maintainers

Package info

github.com/errogaht/transcribe-bundle

Type:symfony-bundle

pkg:composer/errogaht/transcribe-bundle

Transparency log

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.1 2026-07-22 20:28 UTC

This package is auto-updated.

Last update: 2026-07-22 20:33:50 UTC


README

Symfony-first audio, video, speaker diarization, timestamps, subtitles and live speech transcription. Configure named profiles in YAML, autowire them like Flysystem storages, and keep provider-specific HTTP, polling and WebSocket details out of application code.

Palatine is the first built-in provider. The public contracts are provider-neutral, so another speech provider can be added as a normal Symfony service.

Requirements

  • PHP 8.1 or newer
  • Symfony 6.4, 7.4 or 8.x
  • ext-curl and ext-fileinfo
  • ffmpeg and ffprobe for video, WebM and traffic-efficient normalization

Installation

composer require errogaht/transcribe-bundle

If Symfony Flex has not enabled the bundle, add it to config/bundles.php:

<?php

return [
    // ...
    Errogaht\TranscribeBundle\TranscribeBundle::class => ['all' => true],
];

Keep the API key in an environment secret:

PALATINE_API_KEY=replace-me

Create config/packages/transcribe.yaml:

transcribe:
    providers:
        palatine:
            type: palatine
            api_key: '%env(PALATINE_API_KEY)%'

    transcribers:
        fast:
            provider: palatine
            mode: plain
            language: ru

        subtitles:
            provider: palatine
            mode: timestamped

        meeting:
            provider: palatine
            mode: diarized
            min_speakers: 2
            max_speakers: 6

    default_transcriber: fast

    streams:
        microphone:
            provider: palatine
            model: palatine_stream
            language: ru
            codec: linear16
            sample_rate: 48000
            channels: 1

    default_stream: microphone

Use a configured transcriber

The default profile is available through TranscriberInterface:

use Errogaht\TranscribeBundle\Contract\TranscriberInterface;

final class VoiceMessageHandler
{
    public function __construct(private TranscriberInterface $transcriber)
    {
    }

    public function __invoke(string $audioPath): string
    {
        return $this->transcriber->transcribe($audioPath)->text;
    }
}

Each named profile is also a non-shared, autowireable service. The argument name selects it:

use Errogaht\TranscribeBundle\Contract\TranscriberInterface;

final class MeetingImporter
{
    public function __construct(private TranscriberInterface $meeting)
    {
    }

    public function import(string $videoPath): void
    {
        $result = $this->meeting->transcribe($videoPath);

        foreach ($result->segments as $segment) {
            // $segment->speaker, ->start, ->end and ->text are provider-neutral.
        }
    }
}

For dynamic selection, inject TranscriberRegistry and call get('subtitles'). Inputs may be a path, SplFileInfo, or in-memory bytes:

use Errogaht\TranscribeBundle\Value\MediaInput;

$result = $transcriber->transcribe(
    MediaInput::fromBytes($upload, 'voice.ogg', 'audio/ogg'),
);

Per-call options can override a profile without mutating it:

use Errogaht\TranscribeBundle\Value\TranscriptionOptions;

$result = $transcriber->transcribe($path, TranscriptionOptions::diarized('ru', 2));

Audio and video preparation

The default auto strategy passes compact AAC, M4A, MP3 and Ogg/Opus audio through unchanged. Video, browser WebM, lossless and unknown inputs are converted to mono Ogg/Opus at 16 kHz and 24 kbit/s. This extracts only the first audio stream and normally reduces upload traffic significantly.

transcribe:
    media:
        strategy: auto # auto, always or never
        ffmpeg_binary: ffmpeg
        ffprobe_binary: ffprobe
        codec: libopus
        bitrate: 24k
        sample_rate: 16000
        channels: 1
        process_timeout: 900

Caller-owned files are never modified or deleted. Temporary files have mode 0600 and are removed in a finally block, including provider failures.

Live WebSocket transcription

Palatine live audio uses a full-duplex session. Audio chunks must already match the configured codec, sample rate and channel count.

use Errogaht\TranscribeBundle\Contract\StreamingTranscriberInterface;
use Errogaht\TranscribeBundle\Enum\StreamEventType;

final class LiveCaptionService
{
    public function __construct(private StreamingTranscriberInterface $microphone)
    {
    }

    public function run(iterable $pcmChunks): string
    {
        $session = $this->microphone->open();

        try {
            foreach ($pcmChunks as $chunk) {
                $session->send($chunk);
                $event = $session->receive();

                if ($event?->type === StreamEventType::Partial) {
                    // Publish $event->chunkText or $event->fullText to the UI.
                }
            }

            $session->finish();
            while (($event = $session->receive(10.0)) !== null) {
                if ($event->type === StreamEventType::Final) {
                    return $event->fullText ?? '';
                }
            }

            return '';
        } finally {
            $session->close();
        }
    }
}

The provider token stays server-side. Browsers should connect to an application-owned WebSocket or streaming controller; do not expose Palatine credentials to frontend code.

Output formats

TranscriptionResult always contains normalized text, language, duration, segments, words, provider, model and safe metadata. Inject FormatterRegistry to render text, timestamped, speaker, json, srt or vtt.

$srt = $formatters->get('srt')->render($result);

The CLI offers the same path:

bin/console transcribe:media interview.mp4 --transcriber=meeting --format=speaker
bin/console transcribe:media movie.mkv --transcriber=subtitles --format=srt --output=movie.srt
bin/console transcribe:providers
bin/console transcribe:debug

Long files, polling and retries

In auto transport mode, diarization, media at least 55 seconds long, or prepared files at least 5 MB use Palatine's task API. The bundle schedules, polls, normalizes and releases the task internally; application code receives one completed result. Direct calls are used for smaller files. Retryable transport, rate-limit and server failures are retried with bounded backoff.

All thresholds and timeouts are configurable under a provider. Set transport: direct or transport: polling on a profile only when you need an explicit override.

Messenger

The bundle intentionally does not own your queue, retries or result persistence. Put the same configured transcriber inside an application message handler:

use Symfony\Component\Messenger\Attribute\AsMessageHandler;

#[AsMessageHandler]
final class TranscribeRecordingHandler
{
    public function __construct(private TranscriberInterface $meeting)
    {
    }

    public function __invoke(TranscribeRecording $message): void
    {
        $result = $this->meeting->transcribe($message->path);
        // Persist the completed result and let Messenger own delivery retries.
    }
}

Do not pass audio bytes through the queue. Pass an immutable storage identifier or path that remains valid until the worker finishes.

Neuron AI integration

There is no hard dependency on errogaht/neuron-ai-bundle. Inject a configured transcriber into an agent tool or orchestration service and pass TranscriptionResult::text to the named agent:

final class VoiceAgent
{
    public function __construct(
        private TranscriberInterface $fast,
        private SupportAgent $supportAgent,
    ) {
    }

    public function answer(string $voicePath): mixed
    {
        return $this->supportAgent->chat($this->fast->transcribe($voicePath)->text);
    }
}

This keeps speech recognition replaceable and lets Neuron AI agents, workflows, RAG and Messenger remain configured by their own bundle.

Custom providers and formatters

Implement TranscriptionProviderInterface and register the service:

transcribe:
    providers:
        private_speech:
            type: service
            service: App\Speech\PrivateSpeechProvider
    default_provider: private_speech

Implement StreamingTranscriptionProviderInterface on the same service if it supports live sessions. A custom formatter implements TranscriptFormatterInterface and carries the transcribe.formatter tag.

See configuration, runtime API, provider extension guide, security, and the architecture graph.

Events, errors and observability

Completed file calls dispatch TranscriptionStartedEvent, TranscriptionCompletedEvent and TranscriptionFailedEvent. Live sessions dispatch StreamOpenedEvent, StreamEventReceivedEvent and StreamClosedEvent. Logs include profile, provider, media size, duration and normalized counts, but never API keys, raw audio or transcript text.

All bundle failures implement TranscriptionExceptionInterface. Provider exceptions expose isRetryable() and statusCode() so application and Messenger policies can distinguish invalid credentials from temporary failures.

Development

composer validate --strict
composer check

License: MIT.