Add your own transcription provider to Transcription Service
Add a speech-to-text service to the Provider list on Settings → Transcription. Register it with the benecaster_transcription_providers filter and implement BenecasterTranscription\Provider\TranscriptionProviderInterface.
- A factory is
callable( string $api_key, array $settings ): TranscriptionProviderInterface.$settingscarriesmodelfor OpenAI and is empty for every other provider. - You can add providers, never replace or remove a built-in. An entry that changes or drops
openai,assemblyai,deepgramorrevaiis ignored. A factory that returns anything else makes the provider fail to load with “The {label} provider could not be loaded.” - The interface:
slug(),label(),submit(),poll(),fetch_result(),verify_credentials()(the Test connection check: one free call that succeeds only with a valid key), and three capability flags,supports_diarization(),supports_language_hint()andmax_concurrency_hint()(guidance shown in settings, never a cap). The flags answer for the provider as configured, its model included. - Synchronous or asynchronous. Return
SubmittedJob::completed( $result )when your API answers with the transcript, andpoll()andfetch_result()are never called. ReturnSubmittedJob::queued( $job_id )when it answers with a job; the add-on polls until it is complete or failed, then fetches. TranscriptResulttakes the provider slug, a list ofSegment( start, end, text, ?speaker )in seconds, optional words, the duration (leave it null if your API gives none), the language and whether it is diarized. Number speakers 1, 2, 3 in order of first appearance.- Throw
ProviderException( ProviderErrorKind, message, ?http_status, ?retry_after )for every failure.RateLimitedandUnavailableare retried with backoff;Auth,TooLarge,InvalidRequest,JobFailedandInvalidResponseare not. The message is shown to the podcaster and logged, so never put the key in it.
The list is built once, on first use, so add your callback on or before init. Your provider’s transcripts are stored with provider custom.
Code
<?php
use BenecasterTranscription\Provider\{ ProviderErrorKind, ProviderException, Segment, SubmittedJob, TranscriptionProviderInterface, TranscriptionRequest, TranscriptResult, JobState };
add_filter( 'benecaster_transcription_providers', function ( array $providers ): array {
$providers['speechco'] = [
'label' => __( 'SpeechCo', 'my-plugin' ),
'factory' => fn( string $api_key, array $settings ): TranscriptionProviderInterface => new My_SpeechCo_Provider( $api_key ),
];
return $providers;
} );
final class My_SpeechCo_Provider implements TranscriptionProviderInterface {
public function __construct( private string $key ) {}
public function slug(): string { return 'speechco'; }
public function label(): string { return __( 'SpeechCo', 'my-plugin' ); }
public function submit( TranscriptionRequest $request ): SubmittedJob {
$response = wp_safe_remote_post( 'https://api.speechco.example/v1/transcribe', [
'timeout' => 300,
'headers' => [ 'Authorization' => 'Bearer ' . $this->key, 'Content-Type' => 'application/json' ],
'body' => wp_json_encode( [ 'url' => $request->audio_url, 'lang' => $request->language ] ),
] );
$code = (int) wp_remote_retrieve_response_code( $response );
if ( is_wp_error( $response ) || $code >= 500 ) {
throw new ProviderException( ProviderErrorKind::Unavailable, __( 'SpeechCo could not be reached.', 'my-plugin' ) );
}
if ( 401 === $code ) {
throw new ProviderException( ProviderErrorKind::Auth, __( 'SpeechCo did not accept the API key.', 'my-plugin' ), 401 );
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
$segments = array_map( fn( array $c ) => new Segment( $c['from'], $c['to'], $c['text'] ), $data['cues'] ?? [] );
return SubmittedJob::completed( new TranscriptResult( 'speechco', $segments, [], $data['seconds'] ?? null, $data['lang'] ?? null ) );
}
// Synchronous: never called.
public function poll( string $job_id ): JobState { throw new LogicException( 'synchronous' ); }
public function fetch_result( string $job_id ): TranscriptResult { throw new LogicException( 'synchronous' ); }
public function verify_credentials(): void { /* one free authenticated GET; throw ProviderErrorKind::Auth on 401 */ }
public function supports_diarization(): bool { return false; }
public function supports_language_hint(): bool { return true; }
public function max_concurrency_hint(): int { return 2; }
}
Hooks Used
Need this built rather than just documented? See our services →