errogaht / transcribe-bundle
Provider-agnostic audio, video, diarization and live speech transcription for Symfony.
Package info
github.com/errogaht/transcribe-bundle
Type:symfony-bundle
pkg:composer/errogaht/transcribe-bundle
Requires
- php: ^8.1
- ext-curl: *
- ext-fileinfo: *
- amphp/websocket-client: ^2.0
- psr/log: ^1.1 || ^2.0 || ^3.0
- symfony/config: ^6.4 || ^7.4 || ^8.0
- symfony/console: ^6.4 || ^7.4 || ^8.0
- symfony/dependency-injection: ^6.4 || ^7.4 || ^8.0
- symfony/event-dispatcher: ^6.4 || ^7.4 || ^8.0
- symfony/framework-bundle: ^6.4 || ^7.4 || ^8.0
- symfony/http-client: ^6.4 || ^7.4 || ^8.0
- symfony/mime: ^6.4 || ^7.4 || ^8.0
- symfony/process: ^6.4 || ^7.4 || ^8.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.85
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10.5 || ^11.5 || ^12.0
- symfony/phpunit-bridge: ^6.4 || ^7.4 || ^8.0
- symfony/yaml: ^6.4 || ^7.4 || ^8.0
Suggests
- errogaht/neuron-ai-bundle: Pass normalized voice transcripts directly to configured Neuron AI agents.
- symfony/messenger: Run long file transcription in application-owned asynchronous workers.
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-curlandext-fileinfoffmpegandffprobefor 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.