feat: publish v1.0.1

Signed-off-by: 小草林(田梓萱) <xcl@xuegao-tzx.top>
This commit is contained in:
2024-06-12 09:52:36 +08:00
parent 2e87763b84
commit af91fc48cb
157 changed files with 180217 additions and 0 deletions

273
example/lib/main.dart Normal file
View 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(),
);
},
);
},
)),
),
);
}
}

View 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);

View 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"),
),
),
),
);
}
}

View 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;
}
}

View 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),
);

View 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;
}