273
example/lib/main.dart
Normal file
273
example/lib/main.dart
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* Copyright (c) 田梓萱[小草林] 2021-2024.
|
||||
* All Rights Reserved.
|
||||
* All codes are protected by China's regulations on the protection of computer software, and infringement must be investigated.
|
||||
* 版权所有 (c) 田梓萱[小草林] 2021-2024.
|
||||
* 所有代码均受中国《计算机软件保护条例》保护,侵权必究.
|
||||
*/
|
||||
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:path_provider/path_provider.dart";
|
||||
import "package:test_whisper/providers.dart";
|
||||
import "package:test_whisper/record_page.dart";
|
||||
import "package:test_whisper/whisper_controller.dart";
|
||||
import "package:test_whisper/whisper_result.dart";
|
||||
import "package:whisper_flutter_new/whisper_flutter_new.dart";
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ProviderScope(
|
||||
child: MaterialApp(
|
||||
title: "Whisper for Flutter",
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: Theme.of(context).colorScheme.primary),
|
||||
useMaterial3: true,
|
||||
),
|
||||
home: const MyHomePage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends ConsumerWidget {
|
||||
const MyHomePage({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final WhisperModel model = ref.watch(modelProvider);
|
||||
final String lang = ref.watch(langProvider);
|
||||
final bool translate = ref.watch(translateProvider);
|
||||
final bool withSegments = ref.watch(withSegmentsProvider);
|
||||
final bool splitWords = ref.watch(splitWordsProvider);
|
||||
|
||||
final WhisperController controller = ref.watch(
|
||||
whisperControllerProvider.notifier,
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
title: const Text(
|
||||
"Whisper flutter demo",
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
minimum: const EdgeInsets.all(20),
|
||||
child: SingleChildScrollView(child: Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final AsyncValue<TranscribeResult?> transcriptionAsync = ref.watch(
|
||||
whisperControllerProvider,
|
||||
);
|
||||
|
||||
return transcriptionAsync.maybeWhen(
|
||||
skipLoadingOnRefresh: true,
|
||||
skipLoadingOnReload: true,
|
||||
data: (TranscribeResult? transcriptionResult) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
const Text("Model :"),
|
||||
DropdownButton(
|
||||
isExpanded: true,
|
||||
value: model,
|
||||
items: WhisperModel.values
|
||||
.map(
|
||||
(WhisperModel model) => DropdownMenuItem(
|
||||
value: model,
|
||||
child: Text(model.modelName),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (WhisperModel? model) {
|
||||
if (model != null) {
|
||||
ref.read(modelProvider.notifier).state = model;
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text("Lang :"),
|
||||
DropdownButton(
|
||||
isExpanded: true,
|
||||
value: lang,
|
||||
items: ["auto", "zh", "en"]
|
||||
.map(
|
||||
(String lang) => DropdownMenuItem(
|
||||
value: lang,
|
||||
child: Text(lang),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (String? lang) {
|
||||
if (lang != null) {
|
||||
ref.read(langProvider.notifier).state = lang;
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
const Text("Translate result :"),
|
||||
DropdownButton(
|
||||
isExpanded: true,
|
||||
value: translate,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: false,
|
||||
child: Text("No"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: true,
|
||||
child: Text("Yes"),
|
||||
),
|
||||
],
|
||||
onChanged: (bool? translate) {
|
||||
if (translate != null) {
|
||||
ref.read(translateProvider.notifier).state =
|
||||
translate;
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text("With segments :"),
|
||||
DropdownButton(
|
||||
isExpanded: true,
|
||||
value: withSegments,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: false,
|
||||
child: Text("No"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: true,
|
||||
child: Text("Yes"),
|
||||
),
|
||||
],
|
||||
onChanged: (bool? withSegments) {
|
||||
if (withSegments != null) {
|
||||
ref.read(withSegmentsProvider.notifier).state =
|
||||
withSegments;
|
||||
}
|
||||
},
|
||||
),
|
||||
const Text("Split word :"),
|
||||
DropdownButton(
|
||||
isExpanded: true,
|
||||
value: splitWords,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
value: false,
|
||||
child: Text("No"),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: true,
|
||||
child: Text("Yes"),
|
||||
),
|
||||
],
|
||||
onChanged: (bool? splitWords) {
|
||||
if (splitWords != null) {
|
||||
ref.read(splitWordsProvider.notifier).state =
|
||||
splitWords;
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final Directory documentDirectory =
|
||||
await getApplicationDocumentsDirectory();
|
||||
final ByteData documentBytes =
|
||||
await rootBundle.load(
|
||||
"assets/jfk.wav",
|
||||
);
|
||||
|
||||
final String jfkPath =
|
||||
"${documentDirectory.path}/jfk.wav";
|
||||
|
||||
await File(jfkPath).writeAsBytes(
|
||||
documentBytes.buffer.asUint8List(),
|
||||
);
|
||||
|
||||
await controller.transcribe(jfkPath);
|
||||
},
|
||||
child: const Text("jfk.wav"),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
final String? recordFilePath =
|
||||
await RecordPage.openRecordPage(
|
||||
context,
|
||||
);
|
||||
|
||||
if (recordFilePath != null) {
|
||||
await controller.transcribe(recordFilePath);
|
||||
}
|
||||
},
|
||||
child: const Text("record"),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (transcriptionResult != null) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
transcriptionResult.transcription.text,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
transcriptionResult.time.toString(),
|
||||
),
|
||||
if (transcriptionResult.transcription.segments !=
|
||||
null) ...[
|
||||
const SizedBox(height: 25),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: transcriptionResult
|
||||
.transcription.segments!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final WhisperTranscribeSegment segment =
|
||||
transcriptionResult
|
||||
.transcription.segments![index];
|
||||
|
||||
final Duration fromTs = segment.fromTs;
|
||||
final Duration toTs = segment.toTs;
|
||||
final String text = segment.text;
|
||||
return Text(
|
||||
"[$fromTs - $toTs] $text",
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) {
|
||||
return const Divider();
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
orElse: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
12
example/lib/providers.dart
Normal file
12
example/lib/providers.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:whisper_flutter_new/whisper_flutter_new.dart";
|
||||
|
||||
final modelProvider = StateProvider.autoDispose((ref) => WhisperModel.base);
|
||||
|
||||
final langProvider = StateProvider.autoDispose((ref) => "auto");
|
||||
|
||||
final translateProvider = StateProvider((ref) => false);
|
||||
|
||||
final withSegmentsProvider = StateProvider((ref) => false);
|
||||
|
||||
final splitWordsProvider = StateProvider((ref) => false);
|
89
example/lib/record_page.dart
Normal file
89
example/lib/record_page.dart
Normal file
@@ -0,0 +1,89 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:path_provider/path_provider.dart";
|
||||
import "package:record/record.dart";
|
||||
|
||||
class RecordController extends StateNotifier<bool> {
|
||||
RecordController() : super(false);
|
||||
|
||||
final AudioRecorder _record = AudioRecorder();
|
||||
|
||||
Future<void> startRecord() async {
|
||||
if (!await _record.hasPermission()) {
|
||||
return;
|
||||
}
|
||||
state = true;
|
||||
final Directory appDirectory = await getApplicationDocumentsDirectory();
|
||||
await _record.start(
|
||||
const RecordConfig(),
|
||||
// encoder: AudioEncoder.pcm16bit,
|
||||
// samplingRate: 16000,
|
||||
path: "${appDirectory.path}/test.m4a",
|
||||
);
|
||||
}
|
||||
|
||||
Future<String?> stopRecord() async {
|
||||
final String? path = await _record.stop();
|
||||
state = false;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
final recordControllerProvider =
|
||||
StateNotifierProvider.autoDispose<RecordController, bool>(
|
||||
(ref) => RecordController(),
|
||||
);
|
||||
|
||||
class RecordPage extends ConsumerWidget {
|
||||
const RecordPage({super.key});
|
||||
|
||||
static Future<String?> openRecordPage(BuildContext context) {
|
||||
return Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const RecordPage(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final RecordController controller = ref.watch(
|
||||
recordControllerProvider.notifier,
|
||||
);
|
||||
final bool isRecording = ref.watch(recordControllerProvider);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Record"),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: isRecording
|
||||
? ElevatedButton(
|
||||
onPressed: () async {
|
||||
final String? outputPath = await controller.stopRecord();
|
||||
|
||||
if (outputPath != null) {
|
||||
final File outputFile = File(outputPath);
|
||||
|
||||
print(outputFile.path);
|
||||
|
||||
Navigator.of(context).pop(outputFile.path);
|
||||
} else {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: const Text("stop"),
|
||||
)
|
||||
: ElevatedButton(
|
||||
onPressed: () async {
|
||||
await controller.startRecord();
|
||||
},
|
||||
child: const Text("start"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
57
example/lib/whisper_audio_convert.dart
Normal file
57
example/lib/whisper_audio_convert.dart
Normal file
@@ -0,0 +1,57 @@
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import "package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart";
|
||||
import "package:ffmpeg_kit_flutter_full_gpl/ffmpeg_session.dart";
|
||||
import "package:ffmpeg_kit_flutter_full_gpl/return_code.dart";
|
||||
import "package:flutter/foundation.dart";
|
||||
|
||||
/// Class used to convert any audio file to wav
|
||||
class WhisperAudioconvert {
|
||||
const WhisperAudioconvert({
|
||||
required this.audioInput,
|
||||
required this.audioOutput,
|
||||
});
|
||||
|
||||
/// Input audio file
|
||||
final File audioInput;
|
||||
|
||||
/// Output audio file
|
||||
final File audioOutput;
|
||||
|
||||
/// convert [audioInput] to wav file
|
||||
Future<File?> convert() async {
|
||||
final FFmpegSession session = await FFmpegKit.execute(
|
||||
[
|
||||
"-y",
|
||||
"-i",
|
||||
audioInput.path,
|
||||
"-ar",
|
||||
"16000",
|
||||
"-ac",
|
||||
"1",
|
||||
"-c:a",
|
||||
"pcm_s16le",
|
||||
audioOutput.path,
|
||||
].join(" "),
|
||||
);
|
||||
|
||||
final ReturnCode? returnCode = await session.getReturnCode();
|
||||
|
||||
if (ReturnCode.isSuccess(returnCode)) {
|
||||
return audioOutput;
|
||||
} else if (ReturnCode.isCancel(returnCode)) {
|
||||
if (kDebugMode) {
|
||||
debugPrint("[Whisper]File convertion canceled");
|
||||
}
|
||||
} else {
|
||||
if (kDebugMode) {
|
||||
debugPrint(
|
||||
"[Whisper]File convertion error with returnCode ${returnCode?.getValue()}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
96
example/lib/whisper_controller.dart
Normal file
96
example/lib/whisper_controller.dart
Normal file
@@ -0,0 +1,96 @@
|
||||
import "dart:io";
|
||||
|
||||
import "package:flutter/foundation.dart";
|
||||
import "package:flutter_riverpod/flutter_riverpod.dart";
|
||||
import "package:path_provider/path_provider.dart";
|
||||
import "package:system_info2/system_info2.dart";
|
||||
import "package:test_whisper/providers.dart";
|
||||
import "package:test_whisper/whisper_audio_convert.dart";
|
||||
import "package:test_whisper/whisper_result.dart";
|
||||
import "package:whisper_flutter_new/whisper_flutter_new.dart";
|
||||
|
||||
class WhisperController extends StateNotifier<AsyncValue<TranscribeResult?>> {
|
||||
WhisperController(this.ref) : super(const AsyncData(null));
|
||||
|
||||
final Ref ref;
|
||||
|
||||
Future<void> transcribe(String filePath) async {
|
||||
final WhisperModel model = ref.read(modelProvider);
|
||||
|
||||
state = const AsyncLoading();
|
||||
|
||||
/// China: https://hf-mirror.com/ggerganov/whisper.cpp/resolve/main
|
||||
/// Other: https://huggingface.co/ggerganov/whisper.cpp/resolve/main
|
||||
final Whisper whisper = Whisper(
|
||||
model: model,
|
||||
downloadHost:
|
||||
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main");
|
||||
|
||||
final DateTime start = DateTime.now();
|
||||
|
||||
final String lang = ref.read(langProvider);
|
||||
|
||||
final bool translate = ref.read(translateProvider);
|
||||
|
||||
final bool withSegments = ref.read(withSegmentsProvider);
|
||||
|
||||
final bool splitWords = ref.read(splitWordsProvider);
|
||||
|
||||
try {
|
||||
if (kDebugMode) {
|
||||
debugPrint("[Whisper]Start");
|
||||
}
|
||||
final String? whisperVersion = await whisper.getVersion();
|
||||
var cores = 2;
|
||||
try {
|
||||
cores = SysInfo.cores.length;
|
||||
} catch (_) {
|
||||
cores = 8;
|
||||
}
|
||||
if (kDebugMode) {
|
||||
debugPrint("[Whisper]Number of core = ${cores}");
|
||||
debugPrint("[Whisper]Whisper version = $whisperVersion");
|
||||
}
|
||||
final Directory documentDirectory =
|
||||
await getApplicationDocumentsDirectory();
|
||||
final WhisperAudioconvert converter = WhisperAudioconvert(
|
||||
audioInput: File(filePath),
|
||||
audioOutput: File("${documentDirectory.path}/convert.wav"),
|
||||
);
|
||||
|
||||
final File? convertedFile = await converter.convert();
|
||||
final WhisperTranscribeResponse transcription = await whisper.transcribe(
|
||||
transcribeRequest: TranscribeRequest(
|
||||
audio: convertedFile?.path ?? filePath,
|
||||
language: lang,
|
||||
nProcessors: (cores * 1.2).toInt(),
|
||||
threads: (cores * 1.2).toInt(),
|
||||
isTranslate: translate,
|
||||
isNoTimestamps: !withSegments,
|
||||
splitOnWord: splitWords,
|
||||
),
|
||||
);
|
||||
|
||||
final Duration transcriptionDuration = DateTime.now().difference(start);
|
||||
if (kDebugMode) {
|
||||
debugPrint("[Whisper]End = $transcriptionDuration");
|
||||
}
|
||||
state = AsyncData(
|
||||
TranscribeResult(
|
||||
time: transcriptionDuration,
|
||||
transcription: transcription,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
debugPrint("[Whisper]Error = $e");
|
||||
}
|
||||
state = const AsyncData(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final whisperControllerProvider = StateNotifierProvider.autoDispose<
|
||||
WhisperController, AsyncValue<TranscribeResult?>>(
|
||||
(ref) => WhisperController(ref),
|
||||
);
|
11
example/lib/whisper_result.dart
Normal file
11
example/lib/whisper_result.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
import "package:whisper_flutter_new/whisper_flutter_new.dart";
|
||||
|
||||
class TranscribeResult {
|
||||
const TranscribeResult({
|
||||
required this.transcription,
|
||||
required this.time,
|
||||
});
|
||||
|
||||
final WhisperTranscribeResponse transcription;
|
||||
final Duration time;
|
||||
}
|
Reference in New Issue
Block a user