flutter_baidu_speech_tts 1.0.4
flutter_baidu_speech_tts: ^1.0.4 copied to clipboard
Baidu TTS plugin for Flutter: online, offline and mixed speech synthesis on Android, iOS and HarmonyOS.
flutter_baidu_speech_tts #
English | 中文
A Flutter plugin for Baidu Text-to-Speech (TTS), supporting online, offline, and mixed (MIX) synthesis.
Platform support:
- Android: Online / Offline / Mixed synthesis. Supports
accessToken,apiKey + secretKey, andiamKeyauthentication. - iOS: Online / Offline / Mixed synthesis. Supports
accessToken,apiKey + secretKey, andiamKeyauthentication. Device only (SDK static library contains only the arm64 device architecture; simulator architectures are excluded in the podspec). - OHOS (HarmonyOS): Online / Offline / Mixed synthesis. Supports
accessTokenandapiKey + secretKey.iamKeyandofflineOverwriteAssetsare not applicable; if passed, they will be listed in theignoredParamsfield of theinitializereturn value. Offline models are loaded directly from the path undercontext.resourceDir(mapped toresources/resfile/), with no copying.
All three platforms support "plugin self-playback PCM + text highlight following" (initialize
with playbackMode: BaiduTtsPlaybackMode.plugin), see Text Highlight Following.
1. Prerequisites #
Create an app on the Baidu AI Speech Platform to obtain:
- Online synthesis:
apiKey+secretKey(oraccessToken) - Offline synthesis: additionally requires
appId+authSn - Offline model files (
.dat, 8–16 MB each): text model + voice model. The plugin does not bundle models — you must download them separately and place them in your project.
Note: authSn is bound to the app's package name / BundleId. Each platform (Android / iOS / OHOS)
must register its own app with independent credentials. The recommended approach is to dispatch
credentials per-platform on the Dart side (see example lib/utils/tts_config.dart).
2. Add Dependency #
In pubspec.yaml:
dependencies:
flutter_baidu_speech_tts: ^1.0.4
Then run flutter pub get. Native registration on all three platforms is auto-generated by
the Flutter toolchain — no manual setup required:
- Android:
GeneratedPluginRegistrant.javaregisterscom.baidu.flutter.tts.FlutterBaiduTtsPlugin - iOS:
pod installgenerates theflutter_baidu_speech_ttspod - OHOS:
GeneratedPluginRegistrant.etsregistersFlutterBaiduTtsPluginand injectscom.baidu.tts_*.harintoentrydependencies
Core Features #
One codebase · Three platforms · Three synthesis modes — Full coverage for Android / iOS / HarmonyOS (OHOS), with online, offline, and mixed modes freely switchable, and three authentication methods for flexible integration.
Six Out-of-the-Box Capabilities #
| Capability | What You Get |
|---|---|
| 🔊 Three Synthesis Modes | Online mode calls Baidu's cloud engine for the best audio quality; offline mode synthesizes locally with zero network dependency; mixed mode prioritizes online and automatically falls back to offline — seamless, uninterrupted. |
| 📱 Unified Three-Platform API | Android / iOS / OHOS share a single Dart API. Native registration is fully auto-generated — no manual platform integration code needed. |
| ✨ Text Highlight Following | Highlights spoken text in real time by actual playback position, binding directly to a TextField — no separate read-only display area needed. Builds frame-to-character mapping from engine alignment info; error never accumulates across sentences. |
| 🔑 Three Auth Methods | accessToken, apiKey + secretKey, or iamKey — pick one, dispatch flexibly per platform, and manage all credentials centrally in one place for all three platforms. |
| 🛡️ No-Exception Design | All APIs return a {code, message, ...} struct. Check isSuccess in one line — offline results are reported independently. Say goodbye to try/catch guesswork. |
Feature Demo #
Place screenshots in the same directory as this README; images render automatically.
![]() Before Init — Configuration page for entering TTS credentials |
![]() Init Success — Engine loaded; code 0 means success |
![]() Synthesizing / Playing — Text is being converted to speech and played back |
3. Quick Start #
final tts = FlutterBaiduTts();
// typedEvents is a broadcast stream; subscribers must cancel their own subscriptions.
final sub = tts.typedEvents.listen((BaiduTtsEvent e) {
debugPrint('$e');
// Synthesis data callback: e.event == 'SYNTHESIZE_DATA_ARRIVED', PCM data in e.audioData
});
final init = await tts.initializeWithConfig(const BaiduTtsConfig(
apiKey: 'ak',
secretKey: 'sk',
));
if (init.isSuccess) {
await tts.speakText('Hello, Baidu speech synthesis');
}
// On page dispose
await sub.cancel();
await tts.releaseTts();
Credential & Parameter Management #
Refer to lib/utils/tts_config.dart: dispatch credentials per-platform via
Platform.isAndroid / isIOS, then produce a unified BaiduTtsConfig:
static BaiduTtsConfig buildInitConfig() {
return BaiduTtsConfig(
apiKey: _apiKey,
secretKey: _secretKey,
onlineSpeaker: '4100',
onlineTimeoutMs: 2000,
enableOffline: true,
// The following 4 fields are only needed for offline synthesis
appId: _appId,
authSn: _authSn,
offlineTextModelAsset: 'bd_etts_common_text_txt_all_..._v6.0.0_20240731.dat',
offlineSpeechModelAsset: 'bd_etts_common_speech_duxiaomei_..._20251031153737.dat',
);
}
Key points:
offlineTextModelAsset/offlineSpeechModelAssettake file names, not paths; the plugin resolves them per-platform.- If models are downloaded to disk yourself, pass absolute paths via
offlineTextModelPath/offlineSpeechModelPath— paths take priority over asset names. - Do not commit real credentials to public repositories.
Full Call Flow #
final FlutterBaiduTts _tts = FlutterBaiduTts();
// 1) Subscribe to events first (broadcast stream, can listen multiple times, cancel yourself)
_sub = _tts.typedEvents.listen((BaiduTtsEvent e) {
// e.event == 'SYNTHESIZE_DATA_ARRIVED' → e.audioData is a PCM chunk
});
// 2) Initialize
final init = await _tts.initializeWithConfig(TtsConfig.buildInitConfig());
if (!init.isSuccess) {
// init.code / init.message; if offline enabled, also init.offlineCode / offlineMessage
}
// 3) Synthesize & play / synthesize only
await _tts.speakText(text, mode: BaiduTtsMode.offline); // online / offline / mix, default mix
await _tts.synthesizeText(text);
// 4) Control & release
await _tts.pauseTts();
await _tts.resumeTts();
await _tts.stopTts();
await _sub.cancel();
await _tts.releaseTts();
getCuid() returns the SDK device fingerprint, used to apply for offline authorization on
the Baidu platform. It typically only has a value after initializeWithConfig, so you need
to fetch it again after initialization completes.
Text Highlight Following #
BaiduTtsHighlightController advances the "spoken character count" based on the actual
playback position during synthesis. Combined with
BaiduTtsHighlightTextEditingController, it can directly color text inside a TextField —
no separate read-only display area needed. Behavior is consistent across Android / iOS / OHOS.
Prerequisite: initialize with playbackMode: BaiduTtsPlaybackMode.plugin (plugin plays PCM
itself), and set pcmSampleRate to the actual sample rate of the offline voice model
(default 16000). When using the SDK's built-in player
(BaiduTtsPlaybackMode.sdk), the real playback position is unavailable and can only be
estimated by clock — hasRealPlaybackPosition will be false.
final tts = FlutterBaiduTts();
final highlight = BaiduTtsHighlightController();
final textController = BaiduTtsHighlightTextEditingController(
highlight: highlight,
text: 'Long text to be spoken...',
);
highlight.addListener(() => setState(() {}));
await tts.initializeWithConfig(BaiduTtsConfig(
apiKey: 'ak',
secretKey: 'sk',
playbackMode: BaiduTtsPlaybackMode.plugin, // required for highlight
pcmSampleRate: 16000, // match the voice model .dat sample rate
));
// Speak — highlight follows automatically
await highlight.speak(textController.text, mode: BaiduTtsMode.mix);
await highlight.pause();
await highlight.resume();
await highlight.stop();
// Use directly in your UI
TextField(controller: textController);
// On page dispose: dispose the text controller first, then the highlight controller
textController.dispose();
highlight.dispose();
Controller-exposed state (all notified via ChangeNotifier):
text/readLength/synthLength: original text, spoken character count, synthesized character countprogress: speaking progress 0–1isSpeaking/isPaused/hasRealPlaybackPositionlastError: the most recentSYNTHESIZE_ERRORevent
BaiduTtsHighlightOptions lets you tune breakCharacters (sentence-break characters,
default 。!?;\n\r.!?;), maxRequestUnits (max text units per request, default 900,
matching the engine's 1024-byte limit), tickInterval (interpolation refresh interval,
default 16ms), and defaultCharsPerSecond (fallback speech rate before first audio chunk
arrives).
Implementation & trade-offs:
- Per-sentence splitting: Requests are split only at sentence-ending punctuation or line breaks — one sentence per request. Sentence boundaries naturally have pauses, making seams inaudible, while each independent request yields a precise anchor point. Error never accumulates across sentences. Only when a single sentence exceeds the engine limit does it hard-split at shorter pauses like commas.
- Intra-sentence mapping from engine alignment: Each PCM chunk carries synthesis progress
(
BaiduTtsEvent.audioProgress+progressUnit— iOS reports character count, Android/OHOS report GBK byte offset). This builds an "audio frame → character" lookup table, queried by playback position; between two position reports, interpolation runs attickInterval. Highlight only advances forward, never backward. - Trade-off: After splitting, each segment is independently re-synthesized — the engine
redoes prosody planning, and you may hear timbre/tone changes at seams (especially noticeable
with
am-tac-csubgan16kvoice models). - During playback, do not mix
tts.speakText/pauseTts/stopTts— route everything through the controller'sspeak/pause/resume/stop. - Editing the input field during playback stops highlighting (indices are calculated against the original text at speak time; once content changes, indices become misaligned).
Plugin self-playback mode event differences: adds PLAY_POSITION (positionMs / durationMs,
from the hardware playback head); SYNTHESIZE_DATA_ARRIVED no longer carries audioData
(saving one copy per chunk) but still includes audioBytes and sampleRate.
Error Handling Convention #
Every method returns a result object shaped like {code, message, ...}. On failure,
code != 0 — no PlatformException is thrown. Do not use try/catch to determine success;
use isSuccess. Reserved negative error codes:
-1: General error (missing params, not initialized, SDK internal error, etc.)-2:initializein progress (concurrent call)
Other non-zero values come from Baidu SDK error codes (Android: getDetailCode(), iOS:
NSError.code). When initialize fails, the return value also carries offlineCode /
offlineMessage (offline engine load result) and paramErrors (details of params rejected
by the SDK).
To access raw Map return values, use FlutterBaiduTtsPlatform.instance directly.
4. Offline Models #
Offline models (.dat files, 8–16 MB each, ~56 MB total) are Baidu proprietary licensed
files and are not distributed with the plugin. For offline synthesis, integrators must
obtain model files from the Baidu AI Speech Platform,
place them in the appropriate native resource directory of their project, and reference them
by file name (not path) via offlineTextModelAsset / offlineSpeechModelAsset:
await tts.initializeWithConfig(BaiduTtsConfig(
apiKey: 'ak',
secretKey: 'sk',
appId: 'appId',
authSn: 'authSn',
enableOffline: true,
offlineTextModelAsset: 'bd_etts_common_text_txt_all_mand_eng_middle_big_v6.0.0_20240731.dat',
offlineSpeechModelAsset:
'bd_etts_common_speech_duxiaoyu_mand_eng_high_am-tac-csubgan16k_v4.9.0_20240918_20251031153737.dat',
));
Placement directories per platform:
- Android:
android/app/src/main/assets/(copied tofilesDiron firstinitialize;offlineOverwriteAssets: trueforces overwrite) - iOS: Add to Xcode Runner target (enters
Bundle.main) - OHOS:
entry/src/main/resources/resfile/(resolved to a path undercontext.resourceDir, no copying)
You can also download models to disk yourself and pass absolute paths via
offlineTextModelPath / offlineSpeechModelPath — paths take priority over file names. If a
path does not exist, initialize returns failure immediately — no silent fallback.
5. Android Integration #
Minimal changes — the android/ directory is essentially template defaults:
- Permissions: The plugin's own manifest already declares
INTERNETandACCESS_NETWORK_STATE, which merge into the host. The host does not need to redeclare them. Optionally addREAD_PHONE_STATE(makes cuid more stable; Android 10+ can no longer obtain IMEI, only declare if needed) andREAD_EXTERNAL_STORAGE(if models are placed outside the sandbox). applicationId: Must match the package name registered on the Baidu platform.- minSdk / targetSdk / ndkVersion: Use
flutter.*defaults. The SDK's jar (inandroid/libs/) and.sofiles (inandroid/src/main/jniLibs/, covering arm64-v8a / armeabi-v7a / x86 / x86_64) are already packaged in the plugin AAR — no extra repository or abiFilters needed. - ProGuard: Rules provided by the plugin's
consumerProguardFiles(android/consumer-rules.pro); enablingminifyEnabledrequires no extra configuration.
Offline model placement:
android/app/src/main/assets/
bd_etts_common_text_txt_all_mand_eng_middle_big_v6.0.0_20240731.dat # Text model
bd_etts_common_speech_duxiaomei_..._20251031153737.dat # Voice model
6. iOS Integration #
Device only — The Baidu iOS static library contains only the arm64 device slice, and the
podspec sets EXCLUDED_ARCHS[sdk=iphonesimulator*] to exclude simulators.
-
Static library
libBDSpeechTTSBaseKit.a(~239 MB): Not published with the plugin. Duringpod install, the podspec auto-downloads it to the plugin'sios/Libs/(override the download URL with theFLUTTER_BAIDU_TTS_IOS_LIB_URLenvironment variable). Linking is handled byPods-Runner.xcconfig:OTHER_LDFLAGSincludes-ObjC -l"BDSpeechTTSBaseKit", andLIBRARY_SEARCH_PATHSpoints to.symlinks/plugins/flutter_baidu_speech_tts/ios/Libs. If download fails or you prefer manual management, copyBDSClientLib/libBDSpeechTTSBaseKit.afrom the Baidu iOS TTS SDK package (BDSpeechClientSDK_TTS) toios/Libs/. -
Add offline models to the Runner target's Resources (enters
Bundle.main). To avoid duplicate storage, this project references the Android assets directory directly: inios/Runner.xcodeproj/project.pbxproj, each.datfile'spathis set to../android/app/src/main/assets/xxx.datand added toPBXResourcesBuildPhase. In Xcode, drag them in and check the Runner target.Model path resolution order:
offlineXxxModelPathabsolute path →Bundle.main→ sandboxDocuments/. If not found orloadOfflineEnginefails,initializereturns failure immediately — no silent fallback to online. -
ios/Runner/Info.plist: This project addsNSLocalNetworkUsageDescription("This app needs to access the local network to support relevant features"). TTS only plays audio — no microphone permission needed. -
Deployment target:
IPHONEOS_DEPLOYMENT_TARGET = 12.0. ThePodfiledoes not explicitly specifyplatform, using the Flutter default. -
Audio session: Managed by the SDK itself (the plugin sets the category to
playback). If the host needs to manageAVAudioSessionitself, override it afterinitialize.
Note: Do not manually link static libraries outside the project (e.g., absolute paths like
../../BDSpeechClientSDK_.../BDSClientLib/libBDSpeechTTSBaseKit.a). These are local debugging leftovers — the plugin's pod already handles linking the same library. Using such paths will cause build failures on other machines or directories. If such File References exist in your project, remove them.
7. OHOS (HarmonyOS) Integration #
- You must declare network permissions yourself. The HAR's
module.json5does not participate in the final build, so its permissions do not merge into the host. Inohos/entry/src/main/module.json5:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.GET_NETWORK_INFO" }
]
Under products, configure: "buildOption": {
"strictMode": {
"useNormalizedOHMUrl": true
}
Missing INTERNET causes online authorization (PARAM_LICENSE_URL) to fail during
initialization, making synthesis completely unusable. If ohosTest has networked test cases,
declare it there too.
-
Place offline models in
entry/src/main/resources/resfile/and pass file names; the plugin resolves them to paths undercontext.resourceDir— no copying. -
Dependencies:
com.baidu.tts_*.har+authbaselibrary.harare injected from the plugin'sohos/libs/intoentryby the Flutter toolchain (seeohos/entry/oh-package-lock.json5) — no manualoh-package.json5dependencies needed. -
SDK version: The example project uses
compatibleSdkVersion5.0.4(16),runtimeOSHarmonyOS(seeohos/build-profile.json5). -
Limitations:
iamKeyandofflineOverwriteAssetsare not supported; if passed, they are listed in theignoredParamsfield of theinitializereturn value. The offline authorization URL is fixed tohttps://upl.baidu.com/authand cannot be configured.
8. Running the Example #
The example's credentials are centralized in example/lib/utils/tts_config.dart (TtsConfig),
split into Android / iOS / OHOS groups. Before running, replace apiKey / secretKey /
appId / authSn with your own credentials:
cd example
flutter run # Android
flutter run -d <device> # iOS (simulator not supported)
Note: These credentials are currently plaintext constants, for local example runs only. Do not commit real credentials to public repositories.
9. Integration Checklist #
- ❌ Add dependency in
pubspec.yaml, runflutter pub get - ❌ Package name / BundleId matches Baidu platform registration on all three platforms;
appId/authSnare platform-specific - ❌ Android models placed in
android/app/src/main/assets/ - ❌ iOS models added to Runner target Resources; running on device; after
pod install, confirmios/Libs/libBDSpeechTTSBaseKit.aexists - ❌ OHOS models placed in
entry/src/main/resources/resfile/;module.json5declaresohos.permission.INTERNET - ❌ Subscribe to
typedEventsbefore callinginitializeWithConfig - ❌ Use
result.isSuccessto check success; for offline failures, checkofflineCode/offlineMessage - ❌ For highlight following: initialize with
playbackMode: BaiduTtsPlaybackMode.plugin, setpcmSampleRateto match the voice model's sample rate, and route all playback throughBaiduTtsHighlightController - ❌ On page dispose:
sub.cancel()+releaseTts()
10. FAQ #
initializereturnscode != 0: CheckmessageandparamErrors(specific params rejected by the SDK). For offline issues, checkofflineCode/offlineMessage.- Offline doesn't work but online is fine: Model file name is misspelled, models not placed
in the correct resource directory, or
appId/authSnmissing. Withmode: BaiduTtsMode.offline, there is no online fallback; onlymixfalls back from online to offline on failure. - iOS simulator reports architecture error: Expected behavior — the static library has no simulator slice; device only.
- OHOS synthesis is silent or init fails: First check that the
entrymodule declaresohos.permission.INTERNET. getCuid()returns empty: CallinitializeWithConfigfirst, then fetch.- For iOS, if
pod installfails, refer to: https://cloud.baidu.com/doc/SPEECH/s/wltwwnvc9#5-sdk%E9%9B%86%E6%88%90 — follow the official guide to import resources into the project.
11. Known Limitations #
- The iOS static library is 239 MB, exceeding pub.flutter-io.cn's 100 MB single-package limit, so it is
not distributed with the package. It is auto-downloaded during
pod install(override with theFLUTTER_BAIDU_TTS_IOS_LIB_URLenvironment variable). - iOS supports device only (the static library has no simulator slice).
- When upgrading the native SDK, re-verify its transitive dependencies: on the Android side,
there is no dependency metadata after unpacking; the OkHttp used internally by the SDK is
explicitly declared in
android/build.gradle. - The OHOS offline authorization URL is currently fixed to
https://upl.baidu.com/authand cannot be configured. - On the Android side, the SDK's
loadAudioPlayer()is not called; playback uses the SDK's default player. - Text highlight following splits text into multiple requests per sentence — you may hear timbre/tone changes at seams. This is the trade-off for obtaining precise anchor points (see "Text Highlight Following").
- Plugin self-playback (
BaiduTtsPlaybackMode.plugin) sample rate is declared by the caller viapcmSampleRate, not taken from the SDK-reported audio format (the iOS SDK reports 16 kHz as 8 kHz; building the playback format from that would drop pitch by an octave and double the duration). When using an 8 kHz voice model, pass 8000 accordingly.
中文文档


