# volcengine_speech_plugin
**Repository Path**: pome/volcengine_speech_plugin
## Basic Information
- **Project Name**: volcengine_speech_plugin
- **Description**: 豆包asr
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: main
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-07-17
- **Last Updated**: 2026-07-19
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Volcengine Speech Plugin
A Flutter plugin for [Volcengine (Volcano Engine / ByteDance)](https://www.volcengine.com) speech services, supporting **ASR (Automatic Speech Recognition)** and **TTS (Text-to-Speech)** on Android and iOS.
## Features
- **ASR** — Real-time streaming speech-to-text with VAD, partial / final results, volume levels.
- **TTS** — Text-to-speech with voice, speed, volume, pitch, and emotion.
- **Platforms** — Android and iOS via method channels.
- **Callbacks** — Engine lifecycle, recognition results, volume, TTS playback progress.
## Getting Started
```yaml
dependencies:
volcengine_speech_plugin: ^0.0.2
permission_handler: ^11.0.0 # microphone permission
```
### Android
`android/app/src/main/AndroidManifest.xml`:
```xml
```
### iOS
`ios/Runner/Info.plist`:
```xml
NSMicrophoneUsageDescription
需要使用麦克风进行语音识别
```
## Credentials
Do **not** hardcode production secrets in source control. Prefer remote config / secure storage:
```dart
const appId = 'your_app_id';
const token = 'Bearer;your_token';
const asrCluster = 'volcengine_input_common';
const ttsCluster = 'volcano_tts';
const voiceType = 'BV700_V2_streaming';
```
## Usage — ASR hold to talk (recommended)
Product flow: **press → start → release to send / cancel → wait for final text**.
Core plugin APIs:
| Method | Role |
| --- | --- |
| `registerListener` / `unRegisterListener` | Bind ASR / TTS callbacks |
| `initASREngine` / `unInitASREngine` | Create / destroy ASR engine |
| `startASREngine` / `stopASREngine` | Start / stop recognition |
| `onEnginePartialResult` | Streaming partial text |
| `onEngineFinalResult` | Final text after `stop` (`seFinalResult`) |
| `onEngineStop` | Native engine stopped |
| `onEngineError` | Native JSON error string |
| `onVolumeLevel` | Recording volume (string) |
### 1. Init + listen
```dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/widgets.dart';
import 'package:volcengine_speech_plugin/volcengine_speech_plugin.dart';
final plugin = VolcengineSpeechPlugin();
String sentences = '';
bool needsEngineReinit = false;
Completer? engineStopCompleter;
Completer? finalResultCompleter;
Future initAsr({bool showNluPun = false}) async {
try {
await plugin.unInitASREngine();
} catch (_) {}
await plugin.registerListener(
SpeechPluginListener(
onEnginePartialResult: (text) {
_absorbText(text, isFinal: false);
},
// After stopASREngine(), native may emit seFinalResult.
// Always handle final, otherwise trailing text can be lost.
onEngineFinalResult: (text) {
_absorbText(text, isFinal: true);
_completeFinalWait();
},
onVolumeLevel: (volumeLevel) {
final volume = double.tryParse(volumeLevel) ?? 0;
// Drive waveform UI when volume > 0.15
debugPrint('volume: $volume');
},
onEngineError: (msg) {
needsEngineReinit = true;
// Common codes in JSON:
// 5000 — app entered background during recognition
// 4001 / 4000 / 4003 — recv timeout / connection issues
try {
final map = jsonDecode(msg as String) as Map;
debugPrint('ASR err_code=${map['err_code']} msg=${map['err_msg']}');
} catch (_) {
debugPrint('ASR error: $msg');
}
_completeEngineStopWait();
},
onEngineStop: () {
_completeEngineStopWait();
},
),
);
final ok = await plugin.initASREngine(
appId: appId,
token: token,
cluster: asrCluster,
showNluPun: showNluPun,
);
needsEngineReinit = ok != true;
return ok;
}
void _absorbText(String text, {required bool isFinal}) {
final normalized = text.replaceAll(RegExp(r'\s+'), '');
if (normalized.isEmpty) return;
if (isFinal) {
if (sentences.isEmpty ||
normalized.length >= sentences.length ||
normalized.startsWith(sentences)) {
sentences = normalized;
} else if (!sentences.contains(normalized)) {
sentences = '$sentences$normalized';
}
} else if (normalized.length >= sentences.length) {
sentences = normalized;
}
}
```
### 2. Press to start / release to finish or cancel
```dart
/// Finger down — start recognition.
Future startHoldRecognize() async {
// 1) Request microphone permission first (see below).
// 2) Reuse engine unless needsEngineReinit.
if (needsEngineReinit) {
await initAsr();
}
sentences = '';
final started = await plugin.startASREngine(
resultType: 'full', // 'full' or 'single'
vadSilenceTime: 10000, // leading silence (ms)
vadEndSilenceTime: 2000, // trailing silence / auto-stop (ms)
maxSpeechDuration: 600000, // max input duration (ms)
enableGetVolume: true,
);
if (started != true) {
await initAsr();
final retry = await plugin.startASREngine(
resultType: 'full',
vadSilenceTime: 10000,
vadEndSilenceTime: 2000,
maxSpeechDuration: 600000,
);
if (retry != true) {
throw StateError('ASR engine start failed');
}
}
}
/// Finger up — send. Wait for final text before reading [sentences].
Future finishHoldRecognize() async {
engineStopCompleter = Completer();
finalResultCompleter = Completer();
await plugin.stopASREngine();
await Future.wait([
_waitEngineStopped(),
_waitFinalResult(),
]);
// Use [sentences] as the utterance result.
}
/// Finger up — cancel. Discard current text.
Future cancelHoldRecognize() async {
engineStopCompleter = Completer();
await plugin.stopASREngine();
await _waitEngineStopped();
sentences = '';
}
Future disposeAsr() async {
await plugin.unRegisterListener();
await plugin.unInitASREngine();
}
void _completeEngineStopWait() {
final c = engineStopCompleter;
if (c == null || c.isCompleted) return;
c.complete();
}
void _completeFinalWait() {
final c = finalResultCompleter;
if (c == null || c.isCompleted) return;
c.complete();
}
Future _waitEngineStopped({
Duration timeout = const Duration(milliseconds: 1200),
}) async {
final c = engineStopCompleter;
if (c == null) return;
try {
await c.future.timeout(timeout);
} catch (_) {}
engineStopCompleter = null;
}
Future _waitFinalResult({
Duration timeout = const Duration(milliseconds: 1000),
}) async {
final c = finalResultCompleter;
if (c == null) return;
try {
await c.future.timeout(timeout);
} catch (_) {
// Timeout: fall back to last partial in [sentences].
}
finalResultCompleter = null;
}
```
### 3. Microphone permission
Request permission **before** `startASREngine`. Example with `permission_handler`:
```dart
import 'dart:io';
import 'package:permission_handler/permission_handler.dart';
Future ensureMicrophonePermission({
Future Function()? onNeedSettings,
}) async {
final permission = Permission.microphone;
if (await permission.request().isGranted) return true;
final status = await permission.status;
if (status.isGranted) return true;
if (status.isDenied ||
status.isPermanentlyDenied ||
status.isRestricted) {
await onNeedSettings?.call(); // show rationale / openAppSettings()
}
return false;
}
```
### 4. Background interrupt
Observe app lifecycle. If recognition is interrupted in background, mark the engine dirty and re-init on next start:
| `err_code` | Meaning |
| --- | --- |
| `5000` | Interrupted because app entered background |
| `4001` / `4000` / `4003` | Recv timeout / connection issues during resume |
```dart
class AsrLifecycleObserver with WidgetsBindingObserver {
bool isResumed = true;
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
isResumed = false;
return;
}
if (state == AppLifecycleState.resumed) {
isResumed = true;
// Optional: auto-retry startHoldRecognize() for init/timeout interrupts.
}
}
}
```
### ASR tips
| Topic | Suggestion |
| --- | --- |
| Session queue | Serialize start / finish / cancel so stop does not race start. |
| Engine reuse | Reuse after stop; re-`initASREngine` only after failure or fatal error. |
| Final text | On finish, wait for `onEngineFinalResult` (with timeout), then fall back to last partial. |
| `resultType` | Use `'full'` for hold-to-talk continuous text; `'single'` for one-shot. |
| Microphone | Deny path must not call `startASREngine`. |
## Usage — ASR minimal one-shot
```dart
final plugin = VolcengineSpeechPlugin();
await plugin.registerListener(
SpeechPluginListener(
onEnginePartialResult: (r) => print('partial: $r'),
onEngineFinalResult: (r) => print('final: $r'),
onEngineError: (e) => print('error: $e'),
onEngineStop: () => print('stop'),
),
);
await plugin.initASREngine(
appId: appId,
token: token,
cluster: asrCluster,
);
await plugin.startASREngine(resultType: 'full');
// ... user speaks ...
await plugin.stopASREngine();
await plugin.unInitASREngine();
```
## Usage — TTS
```dart
await plugin.registerListener(
SpeechPluginListener(
onTTsSynthesisBegin: () => print('Synthesis started'),
onTTsSynthesisEnd: () => print('Synthesis ended'),
onTTsStartPlaying: () => print('Playback started'),
onTTsFinishPlaying: () => print('Playback finished'),
onTTsProgress: (progress) => print('Progress: $progress'),
),
);
await plugin.initTTSEngine(
appId: appId,
token: token,
ttsCluster: ttsCluster,
);
// Single shot
await plugin.startTTSEngine(
isNormal: true,
ttsText: 'Hello, world!',
voiceType: voiceType,
);
// Continuous synthesis
await plugin.startTTSEngine(
isNormal: false,
ttsText: '这是连续合成模式',
voiceType: voiceType,
ttsSpeed: 10,
ttsVolume: 10,
);
await plugin.pausePlayback();
await plugin.resumePlayback();
await plugin.stopTTSEngine();
await plugin.unInitTTSEngine();
```
## Documentation
Official Volcengine speech API documentation: https://www.volcengine.com/docs/6561/79837
## License
MIT