import 'dart:convert'; import 'dart:ffi'; import 'dart:isolate'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:universal_io/io.dart'; import 'package:whisper_ggml/src/models/whisper_model.dart'; import 'package:whisper_ggml/src/whisper_audio_convert.dart'; import 'models/requests/release_model_request.dart'; import 'models/requests/transcribe_request.dart'; import 'models/requests/transcribe_request_dto.dart'; import 'models/requests/version_request.dart'; import 'models/responses/whisper_transcribe_response.dart'; import 'models/responses/whisper_version_response.dart'; import 'models/whisper_dto.dart'; export 'models/_models.dart'; export 'whisper_audio_convert.dart'; /// Native request type typedef WReqNative = Pointer Function(Pointer body); /// Entry point class Whisper { /// [model] is required /// [modelDir] is path where downloaded model will be stored. /// Default to library directory const Whisper({required this.model, this.modelDir}); /// model used for transcription final WhisperModel model; /// override of model storage path final String? modelDir; /// JJ_MKVMaker 패치: 네이티브가 malloc 으로 만든 응답을 네이티브 free 로 해제한다. static void freeNativeResponse(DynamicLibrary lib, Pointer res) => lib.lookupFunction), void Function(Pointer)>( 'free_response')(res); DynamicLibrary _openLib() { if (Platform.isAndroid) { return DynamicLibrary.open('libwhisper.so'); } else if (Platform.isWindows) { return DynamicLibrary.open('whisper_ggml.dll'); } else if (Platform.isLinux) { return DynamicLibrary.open('libwhisper_ggml.so'); } else { return DynamicLibrary.process(); } } Future> _request({ required WhisperRequestDto whisperRequest, }) async { return Isolate.run(() async { final Pointer data = whisperRequest.toRequestString().toNativeUtf8(); final DynamicLibrary lib = _openLib(); final Pointer res = lib.lookupFunction('request').call(data); final Map result = json.decode(res.toDartString()) as Map; malloc.free(data); // JJ_MKVMaker 패치: 네이티브 C 런타임으로 해제 (Windows 힙 손상 방지) freeNativeResponse(lib, res); return result; }); } /// Transcribe audio file to text /// /// [onProgress] is invoked with whisper.cpp's transcription progress /// (0–100, coarse steps) while inference runs. Future transcribe({ required TranscribeRequest transcribeRequest, required String modelPath, void Function(int percent)? onProgress, }) async { // A listener callable may be invoked from whisper's worker thread; // it delivers to this isolate. Kept open until the request finishes. final NativeCallable? progressCallable = onProgress == null ? null : NativeCallable.listener(onProgress); try { final WhisperAudioConvert converter = WhisperAudioConvert( audioInput: File(transcribeRequest.audio), audioOutput: File('${transcribeRequest.audio}.wav'), ); final File? convertedFile = await converter.convert(); final TranscribeRequest req = transcribeRequest.copyWith( audio: convertedFile?.path ?? transcribeRequest.audio, ); final Map result = await _request( whisperRequest: TranscribeRequestDto.fromTranscribeRequest( req, modelPath, progressCallbackAddress: progressCallable?.nativeFunction.address, ), ); if (result['text'] == null) { throw Exception(result['message']); } return WhisperTranscribeResponse.fromJson(result); } catch (e) { debugPrint(e.toString()); rethrow; } finally { progressCallable?.close(); } } /// Free the model parked in native memory by a transcription with /// `keepModelLoaded: true`. Safe to call when nothing is parked. /// /// A transcription still in flight keeps its model until it completes; /// one that was started with `keepModelLoaded: true` parks the model /// again when it finishes. Future releaseModel() async { final Map result = await _request( whisperRequest: const ReleaseModelRequest(), ); if (result['@type'] == 'error') { throw Exception(result['message']); } } /// Get whisper version Future getVersion() async { final Map result = await _request( whisperRequest: const VersionRequest(), ); final WhisperVersionResponse response = WhisperVersionResponse.fromJson( result, ); return response.message; } }