JJ_MKVMaker/packages/whisper_ggml/lib/src/whisper_controller.dart

205 lines
7.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:universal_io/io.dart';
import 'package:whisper_ggml/src/models/whisper_model.dart';
import 'models/whisper_result.dart';
import 'whisper.dart';
import 'whisper_live.dart';
class WhisperController {
String _modelPath = '';
String? _dir;
Future<void> initModel(WhisperModel model) async {
_dir ??= await getModelDir();
_modelPath = '$_dir/ggml-${model.modelName}.bin';
}
/// Start a live transcription session.
///
/// Provide exactly one of [model] and [modelPath]: [model] resolves to the
/// standard download location (see [getPath]), while [modelPath] runs the
/// session against any local ggml model file.
///
/// [pcm16Stream] must produce 16 kHz mono little-endian PCM16 audio (e.g.
/// the `record` package's `startStream` with
/// `RecordConfig(encoder: AudioEncoder.pcm16bits, sampleRate: 16000,
/// numChannels: 1)`). Partial transcripts are emitted on
/// [WhisperLiveSession.partials] while audio flows; when [pcm16Stream]
/// closes (or [WhisperLiveSession.stop] is called) the session finalizes
/// and `stop()` returns the full transcript.
///
/// Unlike [transcribe], the model is loaded once for the whole session.
/// The `gate*` parameters tune the native energy gate that keeps silence
/// away from the decoder: a chunk counts as voiced when its RMS exceeds
/// `max(gateVoiceRatio * noiseFloor, gateRmsMin)`, where the adaptive
/// noise floor is capped at [gateNoiseFloorCap]. Raise the cap for loud
/// environments; lower [gateRmsMin] for very quiet speakers.
///
/// [keepModelLoaded] parks the model in native memory when the session
/// stops, so the next session (or one-shot [transcribe]) with the same
/// model skips the multi-second load — see
/// [TranscribeRequest.keepModelLoaded]. Release it with [releaseModel].
Future<WhisperLiveSession> transcribeLive({
WhisperModel? model,
String? modelPath,
required Stream<Uint8List> pcm16Stream,
String lang = 'en',
String? initialPrompt,
bool suppressNonSpeechTokens = false,
bool keepModelLoaded = false,
double gateRmsMin = 0.0015,
double gateVoiceRatio = 2.5,
double gateNoiseFloorCap = 0.01,
}) async {
if ((model == null) == (modelPath == null)) {
throw ArgumentError('Provide exactly one of model or modelPath.');
}
if (model != null) {
await initModel(model);
}
final WhisperLiveSession session = await startWhisperLiveSession(
modelPath: modelPath ?? _modelPath,
lang: lang,
initialPrompt: initialPrompt,
suppressNonSpeechTokens: suppressNonSpeechTokens,
keepModelLoaded: keepModelLoaded,
gateRmsMin: gateRmsMin,
gateVoiceRatio: gateVoiceRatio,
gateNoiseFloorCap: gateNoiseFloorCap,
);
pcm16Stream.listen(
session.feed,
onDone: session.stop,
onError: (Object e) {
debugPrint('transcribeLive: audio stream error: $e');
session.stop();
},
);
return session;
}
/// Transcribe [audioPath] with [model].
///
/// When [withSegments] is `true`, the result's
/// [WhisperTranscribeResponse.segments] carries per-segment timestamps
/// (`fromTs`/`toTs`) alongside the text; combine with [splitOnWord] to
/// get one segment per word instead of per phrase.
///
/// [diarize] enables tinydiarize speaker-turn detection: with a `-tdrz`
/// model ([WhisperModel.smallEnTdrz], English only) and [withSegments],
/// segments where the speaker changes afterwards have
/// `speakerTurnNext == true`. Has no effect with regular models.
///
/// [onProgress] reports transcription progress as a 0–100 percentage
/// (coarse steps) while inference runs.
///
/// [keepModelLoaded] keeps the model resident in native memory after the
/// request, so the next transcription with the same model skips the
/// multi-second load — see [TranscribeRequest.keepModelLoaded]. Release
/// it with [releaseModel].
Future<TranscribeResult?> transcribe({
required WhisperModel model,
required String audioPath,
String lang = 'en',
bool diarize = false,
String? initialPrompt,
bool noContext = false,
bool suppressNonSpeechTokens = false,
bool withSegments = false,
bool splitOnWord = false,
bool keepModelLoaded = false,
void Function(int percent)? onProgress,
}) async {
await initModel(model);
final Whisper whisper = Whisper(model: model);
final DateTime start = DateTime.now();
const bool translate = false;
try {
final WhisperTranscribeResponse transcription = await whisper.transcribe(
transcribeRequest: TranscribeRequest(
audio: audioPath,
language: lang,
isTranslate: translate,
isNoTimestamps: !withSegments,
splitOnWord: splitOnWord,
isRealtime: true,
diarize: diarize,
initialPrompt: initialPrompt,
noContext: noContext,
suppressNonSpeechTokens: suppressNonSpeechTokens,
keepModelLoaded: keepModelLoaded,
),
modelPath: _modelPath,
onProgress: onProgress,
);
final Duration transcriptionDuration = DateTime.now().difference(start);
return TranscribeResult(
time: transcriptionDuration,
transcription: transcription,
);
} catch (e) {
debugPrint(e.toString());
return null;
}
}
/// Free the model parked in native memory by a transcription with
/// `keepModelLoaded: true`. Safe to call when nothing is parked.
Future<void> releaseModel() {
// Which model the Whisper instance was created with is irrelevant here:
// the native side frees whatever model is parked.
return const Whisper(model: WhisperModel.tiny).releaseModel();
}
static Future<String> getModelDir() async {
if (!Platform.isAndroid &&
!Platform.isIOS &&
!Platform.isMacOS &&
!Platform.isWindows &&
!Platform.isLinux) {
throw UnsupportedError(
'whisper_ggml supports Android, iOS, macOS, Windows, and Linux. '
'${Platform.operatingSystem} has no native whisper implementation.',
);
}
// getLibraryDirectory only exists on Apple platforms.
final Directory libraryDirectory = Platform.isIOS || Platform.isMacOS
? await getLibraryDirectory()
: await getApplicationSupportDirectory();
return libraryDirectory.path;
}
/// Get local path of model file
Future<String> getPath(WhisperModel model) async {
_dir ??= await getModelDir();
return '$_dir/ggml-${model.modelName}.bin';
}
/// Download [model] to [destinationPath]
Future<String> downloadModel(WhisperModel model) async {
if (!File(await getPath(model)).existsSync()) {
final request = await HttpClient().getUrl(model.modelUri);
final response = await request.close();
final bytes = await consolidateHttpClientResponseBytes(response);
final File file = File(await getPath(model));
await file.writeAsBytes(bytes);
return file.path;
} else {
return await getPath(model);
}
}
}