feat: add AI voice transcription fallback for domestic Android ROMs

This commit is contained in:
shen
2026-09-15 22:02:26 +08:00
parent 670260f8e2
commit 35b493e652
8 changed files with 683 additions and 90 deletions
@@ -1,3 +1,4 @@
import '../../core/ai_service.dart';
import 'package:flutter/material.dart';
import '../../core/app_state.dart';
@@ -32,10 +33,11 @@ class _AssessmentPreparationPageState extends State<AssessmentPreparationPage> {
Future<void> _checkMicrophone() async {
setState(() => checkingMicrophone = true);
final ready = await VoiceService.instance.initializeSpeech();
final sttReady = await VoiceService.instance.initializeSpeech();
final recReady = await VoiceService.instance.hasRecordPermission();
if (!mounted) return;
setState(() {
microphoneReady = ready;
microphoneReady = sttReady || recReady;
checkingMicrophone = false;
});
}
@@ -135,6 +137,8 @@ class _AssessmentPageState extends State<AssessmentPage> {
bool transcriptEdited = false;
String lastTranscript = '';
bool listening = false;
bool transcribing = false;
bool aiVoiceRecording = false;
bool audioPlayed = false;
bool speakingUnavailable = false;
AssessmentRecord? completedRecord;
@@ -166,31 +170,98 @@ class _AssessmentPageState extends State<AssessmentPage> {
}
Future<void> _mic() async {
if (aiVoiceRecording) {
final path = await VoiceService.instance.stopRecording();
if (!mounted) return;
setState(() {
aiVoiceRecording = false;
listening = false;
transcribing = true;
});
if (path != null) {
final config = widget.state.aiConfig;
final transcribed = await AiService.instance.transcribeAudio(
filePath: path,
provider: config.provider,
endpoint: config.endpoint,
model: config.model,
);
if (mounted) {
setState(() {
transcribing = false;
if (transcribed != null && transcribed.trim().isNotEmpty) {
controller.text = transcribed.trim();
usedMic = true;
lastTranscript = transcribed.trim();
transcriptEdited = false;
speakingUnavailable = false;
}
});
if (transcribed == null || transcribed.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('未识别到清晰语音,请再试一次。')),
);
}
}
} else {
if (mounted) setState(() => transcribing = false);
}
return;
}
if (listening) {
await VoiceService.instance.stopListening();
if (mounted) setState(() => listening = false);
return;
}
final ready = await VoiceService.instance.startListening((text, _) {
final ready = await VoiceService.instance.startListening(
(text, _) {
if (mounted) {
setState(() {
controller.text = text;
usedMic = true;
lastTranscript = text;
transcriptEdited = false;
});
}
},
onStatus: (status) {
if (mounted && (status == 'notListening' || status == 'done')) {
setState(() => listening = false);
}
},
onError: (err) {
if (mounted) {
setState(() => listening = false);
}
},
);
if (!ready) {
final recordStarted = await VoiceService.instance.startRecording();
if (mounted) {
setState(() {
controller.text = text;
usedMic = true;
lastTranscript = text;
aiVoiceRecording = recordStarted;
listening = recordStarted;
speakingUnavailable = !recordStarted;
});
if (recordStarted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('已启动麦克风录音,回答后再次点击,AI 将自动转写为英文。')),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('无法访问麦克风,口语可稍后补测。')),
);
}
}
});
return;
}
if (mounted) {
setState(() {
listening = ready;
speakingUnavailable = !ready;
});
}
if (!ready && mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('语音识别不可用;口语可稍后补测,不会判为语言错误。')),
);
}
}
bool _openCorrect() {
@@ -409,8 +480,10 @@ class _AssessmentPageState extends State<AssessmentPage> {
),
if (task.skill == AssessmentSkill.speaking)
SecondaryButton(
label: listening ? '停止录音' : '使用麦克风回答',
onPressed: _mic,
label: transcribing
? '正在 AI 识别…'
: (listening ? '停止录音并识别' : '使用麦克风回答'),
onPressed: transcribing ? null : _mic,
),
if (task.skill == AssessmentSkill.speaking && speakingUnavailable)
SecondaryButton(