Skip to content

Extension points

Use Drupal service decoration, field configuration, plugins, hooks and event subscribers. The core has no AI provider dependency.

Boundary Site/module alteration
Transcript bundles and metadata transcription_type config entities; Manage Fields/Form Display/View Display
Utterance metadata Paragraphs transcribe_utterance configurable fields and its standard form display; the custom editor renders additional components
Media eligibility and upload defaults Exportable transcribe.settings; the optional installer does not restrict other bundles
Speaker eligibility speaker_types maps content entity type IDs to allowed bundle IDs; empty bundle list allows all bundles; transcribe.speaker_policy can be decorated
Exact text and coordinated saves Inject TranscriptEditorInterface; decorate transcribe.editor for editorial policy
Full text TranscriptTextAssemblerInterface; decorate transcribe.text_assembler rather than adding editable full text
Preferred transcript Configure created/changed sort; decorate transcribe.preferred_resolver for review, moderation or editorial priority
Retrieval/export/rendering Decorate transcribe.query, transcribe.exporter, transcribe.view; preserve their access and cache contracts
Access Standard Drupal entity/field access hooks; source Media access is intersected with transcript permissions
Lifecycle Symfony subscribers to TranscriptEvent constants
Presentation Twig template override, library alteration and CSS custom properties

TranscriptEvent exposes the selected transcription and optional context. Events are imported, revised, speaker remapped, generated, published, unpublished and deleted. Editor events fire inside its database transaction and subscribers may abort it. Publication events describe changes to the default parent revision, including ordinary entity saves; a non-default AI draft does not publish the current transcript. Queue work with revision IDs and idempotency keys for external processing, and verify that a referenced revision exists before executing. Sending an email or making a vendor call inside a database transaction cannot be rolled back.

Media adapters

Implement TranscribableMediaSourceInterface as a Drupal plugin under Plugin/TranscribableMediaSource. Add #[TranscribableMediaSource(id: ..., label: ..., sourceIds: [...], weight: ...)]. The plugin manager selects by the Media source plugin ID, configured eligibility and weight; hook_transcribe_media_source_info_alter() can change definitions. Use ContainerFactoryPluginInterface for injected dependencies.

Return a SourceDescriptor with original-source fingerprint, player description, optional managed input URI/MIME/filename and optional true source duration. Coordinates must refer to the original, untrimmed, original-speed media. Local adapters read the source field defined by the Media source plugin, support Drupal stream wrappers and fingerprint managed file bytes. The remote-video adapter keeps YouTube playback identity separate from input acquisition. Set Media-type third-party configuration transcribe.input_file_field to the managed file field containing a prepared rendition. No downloader or transcoder is silently invoked.

To add a player, register an async factory in Drupal.transcribe.players, depending on the transcribe/player library. Its input is (root, {signal}); return currentTime(), seek(seconds), play(), pause(), duration(), destroy(). Honor cancellation and destroy provider resources on unload. The core renderer currently selects native HTML and YouTube; a renderer decorator supplies additional markup and behavior for other adapters. Canonical timing stays in milliseconds; this player boundary uses seconds.

Optional AI integration

ai_transcribe defines connector and result-normalizer plugin managers with ai_transcribe_connector_info_alter and ai_transcribe_normalizer_info_alter hooks. Profiles snapshot provider/model, connector/normalizer, capability requirements, safe provider options and bounded retry/input/retention limits. Drupal AI remains responsible for provider discovery/configuration and Key credentials. A native asynchronous connector complements its generic speech-to-text operation.

Profiles export use_webhook (default true). Disable it for local or private sites that the provider cannot reach. Such jobs generate no callback URL, secret or authentication hash; scheduled polling still retrieves authoritative results. Run Drupal cron or drush transcribe:work regularly in either mode. Existing profile snapshots without this setting keep authenticated webhooks enabled. The profile form preserves empty operation options and saves all execution limits, including elapsed job time.

Authenticated callbacks wake jobs; workers poll the provider for results. Connector implementations must exclude callback credentials from retained results and honor the profile’s retention limit. Provider artifacts are outside public content APIs.

Per-recording context

Engines recognise names and local terms better when told what to expect. A profile's operation options are fixed for every job, so per-recording hints come from event subscribers instead. JobDispatcher::dispatch() dispatches TranscriptionContextEvent::NAME after its access, capability and input checks, with the source Media, the profile, the optional target transcription and an empty TranscriptionContext. Subscribers run as the requesting user; add only what that user may view. Several modules can contribute to the same context.

Member Bounds AssemblyAI request
Key terms At most 1,000 phrases, each up to six words and 100 characters; case-insensitive duplicates are dropped Appended to any profile keyterms_prompt; for Universal-2, terms under 5 or over 50 characters are dropped and the list stops at 200
Speakers At most 50; a name up to 128 characters, an optional description up to 500 characters, an optional {entity_type, id} reference speech_understanding.request.speaker_identification with names and descriptions; entity references are not sent
Speaker range Minimum and maximum between 1 and 50 speaker_options.min_speakers_expected / max_speakers_expected, replacing a profile speakers_expected
Language A BCP 47 style code such as en or en_us language_code, replacing profile language detection

Values must be plain single-line text: markup, HTML entities, control and bidirectional override characters are rejected. Invalid input throws InvalidArgumentException and the dispatch fails before a job is created. Speaker hints and a speaker range turn on AssemblyAI diarization, which both features require.

The dispatcher stores a non-empty context in the job's profile snapshot under context. Retries send the stored context, not a recalculated one, and the job page shows it. Connectors receive it as $profile['context'] and ignore what they cannot use; the Drupal AI speech-to-text connector ignores it entirely. The context names people, so it follows the job snapshot's access and retention rather than the artifact retention limit.

Names returned by speaker identification are suggestions. The AssemblyAI normaliser keeps the engine's cluster labels as speaker keys and raw labels, and lists the returned names in the result provenance under speaker_suggestions, each marked source: automatic. When a returned name matches a context speaker with an entity reference, the suggestion carries that reference. Imports store the suggestions on the transcription revision (TranscriptionInterface::getSpeakerSuggestions()); a later import replaces them. The speaker mapping form shows each suggested name and, for a cluster with no assignment yet, prefills the matched entity when SpeakerPolicy allows it for the editor. Nothing is assigned until the editor checks Apply this mapping, so an editor's own mapping is never replaced.

An example subscriber for meeting recordings, in a custom module's *.services.yml:

services:
  mymodule.transcription_context:
    class: Drupal\mymodule\EventSubscriber\MeetingContext
    arguments: ['@entity_type.manager']
    tags:
      - { name: event_subscriber }
namespace Drupal\mymodule\EventSubscriber;

use Drupal\ai_transcribe\Event\TranscriptionContextEvent;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class MeetingContext implements EventSubscriberInterface {

  public function __construct(private EntityTypeManagerInterface $entities) {}

  public static function getSubscribedEvents(): array {
    return [TranscriptionContextEvent::NAME => 'addMeeting'];
  }

  public function addMeeting(TranscriptionContextEvent $event): void {
    $meetings = $this->entities->getStorage('node')->loadByProperties([
      'type' => 'meeting',
      'field_recording' => $event->media->id(),
    ]);
    $meeting = reset($meetings);
    if (!$meeting || !$meeting->access('view')) {
      return;
    }
    $present = 0;
    foreach ($meeting->get('field_attendees')->referencedEntities() as $person) {
      if ($person->access('view')) {
        $event->context->addSpeaker($person->label(), $person->get('field_role')->value, [
          'entity_type' => 'node',
          'id' => $person->id(),
        ]);
        $present++;
      }
    }
    if ($present) {
      // Members of the public may also speak.
      $event->context->setSpeakerRange($present, min(50, $present + 5));
    }
    $terms = [];
    foreach ($meeting->get('field_agenda_items')->referencedEntities() as $item) {
      foreach ($item->get('field_named_terms') as $term) {
        $terms[] = $term->value;
      }
    }
    $event->context->addKeyTerms($terms);
  }

}