Intron Voice Dart SDK
intron_voice is a standalone Dart SDK for the Intron Voice API. It provides
typed request, response, streaming, cancellation, retry, and error primitives for
Flutter and server-side Dart applications. The package does not own microphone
capture or audio playback.
Official API documentation is available at https://docs.voice.intron.io/.
What This Package Provides
This package is the core SDK. It gives you typed Dart APIs for Intron Voice, but it deliberately does not bundle a concrete HTTP client, WebSocket client, microphone recorder, audio player, Flutter widgets, or platform permissions.
You provide these pieces from your app:
IntronHttpTransportfor REST calls such as file upload, STT status, and TTS.IntronWebSocketTransportfor streaming STT and streaming TTS.IntronFileUploadSourcefor audio bytes when uploading files.- Microphone capture and audio playback, if your app needs them.
This keeps the SDK usable from Flutter, server-side Dart, tests, and command line tools. A Flutter app can use this package directly, but Flutter-specific recording, playback, permissions, and lifecycle handling stay in the host app.
Installation
Add the package after publication, or depend on this repository during local development.
dependencies:
intron_voice: ^0.0.1
For local development before publication, use a path dependency:
dependencies:
intron_voice:
path: ../intron_voice
or a Git dependency:
dependencies:
intron_voice:
git:
url: https://github.com/intron-health/intron_voice_dart.git
Quickstart
The shortest useful flow is: create a client, provide an HTTP transport, provide an upload source, submit audio, and poll for the transcript.
import 'dart:io';
import 'dart:typed_data';
import 'package:intron_voice/intron_voice.dart';
Future<void> main() async {
final transport = MyHttpTransport();
final client = IntronClient(
config: IntronClientConfig(
tokenProvider: MyTokenProvider(),
),
httpTransport: transport,
);
try {
final job = await client.uploadAudioFile(
audioFile: SttAudioFile(
LocalFileUploadSource(File('audio/visit.wav')),
),
options: SttUploadOptions.forLanguage(IntronLanguage.english),
);
final result = await client.waitForTranscription(
fileId: job.fileId,
pollingInterval: const Duration(seconds: 2),
timeout: const Duration(minutes: 30),
);
print(result.transcript);
} on IntronRateLimitException catch (error) {
print('Rate limited. Retry after: ${error.retryAfter}');
} on IntronApiException catch (error) {
print('Intron request failed: ${error.message}');
} finally {
await client.dispose();
}
}
final class MyTokenProvider implements IntronTokenProvider {
@override
Future<String> resolveToken() async {
return fetchShortLivedTokenFromYourBackend();
}
}
final class LocalFileUploadSource implements IntronFileUploadSource {
LocalFileUploadSource(this.file);
final File file;
@override
String get fileName => file.uri.pathSegments.last;
@override
int? get length => file.lengthSync();
@override
Stream<Uint8List> openRead() {
return file.openRead().map(Uint8List.fromList);
}
}
MyHttpTransport is your adapter from IntronHttpRequest to whichever HTTP
client your app already uses. The SDK owns request modeling, authentication,
retry metadata, typed parsing, and errors; your transport only sends bytes and
returns IntronHttpResponse.
Authentication
Create an IntronClient with either a static API key or an
IntronTokenProvider. The SDK formats credentials as:
Authorization: Bearer <token>
Static keys are useful for local development and trusted server processes. Do
not embed long-lived API keys in mobile or web apps. Production apps should call
your backend token broker, then provide short-lived credentials through an
IntronTokenProvider.
final client = IntronClient(
config: IntronClientConfig(tokenProvider: MyTokenProvider()),
httpTransport: myHttpTransport,
webSocketTransport: myWebSocketTransport,
);
final class MyTokenProvider implements IntronTokenProvider {
@override
Future<String> resolveToken() async {
final token = await fetchShortLivedTokenFromYourBackend();
return token;
}
}
Configuration strings, exceptions, and logging helpers redact credentials before exposing diagnostics.
Providing Transports
The SDK depends on small transport interfaces instead of a bundled networking
stack. That lets you use dart:io, package:http, Dio, browser networking, a
Flutter plugin, or fakes in tests.
An HTTP adapter must:
- send the method, URL, headers, body, multipart fields, and multipart files
from
IntronHttpRequest; - return the status code, headers, and response bytes as
IntronHttpResponse; - avoid logging
Authorizationheaders or request bodies containing sensitive content; - implement
close()if it owns resources.
A WebSocket adapter must:
- connect to the URL and headers in
IntronWebSocketRequest; - expose incoming server messages through
messages; - send JSON text through
addText; - close the socket from
close().
For file uploads, implement IntronFileUploadSource. On native/server Dart,
the local-file implementation from the quickstart is enough. In Flutter mobile,
you can wrap a file selected by your picker or recorder. In Flutter web, wrap
browser bytes and return a Stream<Uint8List>.
Languages And Voices
The SDK includes catalog helpers for documented language codes and voice values. The authoritative lists remain the public documentation and may change before an SDK release:
- STT languages: https://docs.voice.intron.io/docs/stt/supported-languages
- TTS languages and accents: https://docs.voice.intron.io/docs/tts/supported-languages-and-accents
Use the catalog constants where they fit, or pass raw documented strings when the service adds a value before the SDK catalog is updated.
Language selection is required for all ASR/STT and TTS requests. The SDK exposes language as a required option so requests include the documented language field before they are sent.
final sttOptions = SttUploadOptions.forLanguage(IntronLanguage.swahili);
final voice = TtsVoiceConfig.fromCatalog(
language: IntronLanguage.english,
accent: TtsVoiceAccent.swahili,
gender: TtsVoiceGender.female,
outputFormat: TtsOutputAudioFormat.opus,
);
STT Examples
Synchronous File Transcription
Use transcribeAudioFileSync for audio of 120 seconds or less when a single
request/response flow is appropriate.
final result = await client.transcribeAudioFileSync(
audioFile: SttAudioFile(
audioSource,
duration: const Duration(seconds: 90),
),
options: SttUploadOptions.forLanguage(
IntronLanguage.english,
enableDiarization: true,
),
);
print(result.transcript);
If the service returns HTTP 503 after queueing work, the SDK preserves the
continuation file ID on SttResult.continuationFileId so you can continue with
status polling.
Asynchronous File Transcription
Use uploadAudioFile, getFileStatus, and waitForTranscription for longer
audio or when queueing is preferred.
final job = await client.uploadAudioFile(
audioFile: SttAudioFile(audioSource),
options: SttUploadOptions.forLanguage(IntronLanguage.swahili),
);
final result = await client.waitForTranscription(
fileId: job.fileId,
pollingInterval: const Duration(seconds: 2),
timeout: const Duration(minutes: 30),
);
print(result.transcript);
Uploads validate supported extensions and send the documented multipart fields
audio_file_name and audio_file_blob.
Streaming STT
Streaming STT accepts externally supplied PCM16 little-endian audio chunks. The SDK does not capture microphone audio; your app owns capture and chunking.
final session = await client.startStreamingTranscription(
audioStream: pcmAudioChunks,
options: SttStreamingOptions.forLanguage(
IntronLanguage.english,
sampleRate: 16000,
bitDepth: 16,
channels: 1,
),
);
await for (final transcript in session.transcriptEvents) {
if (transcript.isFinal) {
print(transcript.text);
}
}
The SDK waits for SESSION_CREATED, sends INPUT_AUDIO_CHUNK with sequential
ack_id values, and sends COMMIT when the audio stream ends.
TTS Examples
Synchronous TTS
Use generateSpeech for text up to the documented 4096 character limit when a
single request/response flow is appropriate.
final result = await client.generateSpeech(
request: TtsRequest(
text: 'Hello from Intron Voice.',
voice: TtsVoiceConfig.fromCatalog(
language: IntronLanguage.english,
accent: TtsVoiceAccent.swahili,
gender: TtsVoiceGender.female,
),
),
);
print(result.audioPath);
Queued TTS
Use enqueueSpeech and waitForSpeech when queueing is preferred.
final job = await client.enqueueSpeech(
request: TtsRequest(
text: 'Queue this text for synthesis.',
voice: TtsVoiceConfig.fromCatalog(
language: IntronLanguage.english,
accent: TtsVoiceAccent.swahili,
gender: TtsVoiceGender.female,
outputFormat: TtsOutputAudioFormat.opus,
),
),
);
final status = await client.waitForSpeech(textId: job.textId);
print(status.audioPath);
Streaming TTS
Streaming TTS accepts text chunks between 10 and 100 characters. The caller asks
for generated audio chunks with fetchAudioChunk and can listen for decoded
audio bytes.
final session = await client.startStreamingSpeech(
textStream: Stream<String>.fromIterable(<String>[
'This is the first text chunk.',
'This is the second text chunk.',
]),
options: TtsStreamingOptions(
voice: TtsVoiceConfig.fromCatalog(
language: IntronLanguage.english,
accent: TtsVoiceAccent.swahili,
gender: TtsVoiceGender.female,
),
),
);
session.fetchAudioChunk(1);
await for (final audio in session.audioChunkEvents) {
playOrStore(audio.audioBytes);
}
The SDK sends INPUT_TEXT_CHUNK, FETCH_AUDIO_CHUNK, and COMMIT messages
using the documented WebSocket protocol.
Streaming Lifecycle And Reconnection
STT and TTS streaming sessions expose:
eventsfor low-level typed protocol events.stateEventsfor lifecycle transitions.donefor deterministic shutdown.dispose()to close local resources and the WebSocket.
The service documents a 300 second maximum WebSocket session lifetime and a 60
second idle gap. The SDK rolls streaming sessions over before the 300 second
limit by default, commits the old session, opens a new WebSocket, resets ack_id
values, and preserves pending input that has not yet been accepted by the server.
Supported Formats And Limits
STT file uploads currently validate these extensions: .wav, .mp3, .mp4,
.m4a, .ogg, .webm, and .flac. Synchronous STT validates the documented
120 second maximum when the caller supplies duration metadata.
Streaming STT expects PCM16 little-endian audio chunks between 1 KB and 32 KB.
TTS output formats are wav and opus. TTS text generation accepts up to 4096
characters for REST calls, while streaming TTS chunks must be 10 to 100
characters.
Rate Limits
The public TTS generate documentation currently lists 30 requests per minute and
includes Retry-After guidance. SDK REST responses preserve safe rate-limit
metadata such as retryAfter, request IDs, server error codes, and retryability
on typed exceptions or response metadata. Check the official docs for current
endpoint-specific limits before changing production traffic patterns.
Cancellation And Disposal
Use IntronCancellationTokenSource to cancel long-running REST polling,
uploads, retry waits, and streaming sessions.
final cancellation = IntronCancellationTokenSource();
final future = client.waitForTranscription(
fileId: fileId,
cancellationToken: cancellation.token,
);
cancellation.cancel();
try {
await future;
} on IntronRequestCancelledException {
// The operation was cancelled by the caller.
}
Always call dispose() on streaming sessions that you stop early, and call
client.dispose() when your application no longer needs the client.
Error Handling
Catch IntronApiException for safe SDK errors, or catch narrower types such as
IntronAuthenticationException, IntronRateLimitException,
IntronRequestCancelledException, IntronProtocolException, and
IntronTransportException.
try {
final result = await client.generateSpeech(request: request);
print(result.audioPath);
} on IntronRateLimitException catch (error) {
scheduleRetryAfter(error.retryAfter);
} on IntronApiException catch (error) {
logSafeSdkError(error.message, requestId: error.requestId);
}
Privacy Considerations
Do not log bearer tokens, patient identifiers, raw transcripts, audio bytes, or clinical notes unless your application has explicit consent and compliant storage. Prefer short-lived tokens, least-privilege backend services, explicit retention policies, and synthetic or public test audio in development.
Testing Without The Live Service
All network surfaces are injectable. Unit tests can use fake transports that record requests and return deterministic responses, without calling the live service.
final transport = FakeHttpTransport(
IntronHttpResponse(
statusCode: 200,
headers: <String, String>{'content-type': 'application/json'},
body: utf8.encode(jsonEncode(<String, Object?>{
'file_id': 'file-1',
'processing_status': 'FILE_QUEUED',
})),
),
);
final client = IntronClient(
config: IntronClientConfig(apiKey: 'test-key'),
httpTransport: transport,
);
final memoryUploadSource = MemoryUploadSource(
fileName: 'sample.wav',
bytes: Uint8List.fromList(<int>[1, 2, 3]),
);
final job = await client.uploadAudioFile(
audioFile: SttAudioFile(memoryUploadSource),
options: SttUploadOptions.forLanguage(IntronLanguage.english),
);
expect(job.fileId, 'file-1');
expect(transport.requests.single.headers['Authorization'], 'Bearer test-key');
final class FakeHttpTransport implements IntronHttpTransport {
FakeHttpTransport(this.response);
final IntronHttpResponse response;
final List<IntronHttpRequest> requests = <IntronHttpRequest>[];
@override
Future<IntronHttpResponse> send(IntronHttpRequest request) async {
requests.add(request);
return response;
}
@override
Future<void> close() async {}
}
final class MemoryUploadSource implements IntronFileUploadSource {
MemoryUploadSource({required this.fileName, required Uint8List bytes})
: _bytes = bytes;
final Uint8List _bytes;
@override
final String fileName;
@override
int? get length => _bytes.length;
@override
Stream<Uint8List> openRead() => Stream<Uint8List>.value(_bytes);
}
For WebSocket tests, fake IntronWebSocketTransport.connect, capture outgoing
text messages, and push documented JSON messages into the connection stream.
The repository test suite contains fuller fake HTTP and WebSocket transports if you want examples of retry, streaming, and cancellation tests.
Development
Run the local validation suite before committing changes:
dart format --set-exit-if-changed .
dart analyze
dart test
License
This SDK is open source under the MIT License.
Libraries
- intron_voice
- Typed primitives for integrating with the Intron Voice API.