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 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 transcribeLive({ WhisperModel? model, String? modelPath, required Stream 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 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 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 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 getPath(WhisperModel model) async { _dir ??= await getModelDir(); return '$_dir/ggml-${model.modelName}.bin'; } /// Download [model] to [destinationPath] Future 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); } } }