Skip to content

Commit a90e78d

Browse files
committed
FIX: 跟读录音参考时长与最大时长按播放倍速换算
- RepeatFlowConfig 新增 playbackSpeed,单句跟读与难句跟读接入 - 参考时长/最大时长按「原句时长 ÷ 倍速」计算,0.8x 时不再因有声时长超过原句时长把动态静音兜底压到 1s 而截断录音 - 新增引擎倍速回归测试
1 parent b7559fd commit a90e78d

4 files changed

Lines changed: 82 additions & 4 deletions

File tree

‎lib/providers/learning_session/review_difficult_practice_provider.dart‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,7 @@ class ReviewDifficultPractice extends _$ReviewDifficultPractice {
954954
config: RepeatFlowConfig(
955955
audioItemId: session.audioItemId ?? '',
956956
promptIdPrefix: 'rdp',
957+
playbackSpeed: () => state.settings.playbackSpeed,
957958
getRepeatCount: (_) => state.targetRepeatCount,
958959
getIntervalDuration: (s) => listenAndRepeatPauseCalculator(s.duration),
959960
isManualMode: () => state.isManualMode,

‎lib/providers/listen_and_repeat/listen_and_repeat_controller.dart‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,8 @@ class ListenAndRepeatController extends _$ListenAndRepeatController {
336336
final config = RepeatFlowConfig(
337337
audioItemId: audioItemId,
338338
promptIdPrefix: 'lar',
339+
playbackSpeed: () =>
340+
ref.read(listenAndRepeatSettingsProvider).playbackSpeed,
339341
getRepeatCount: (_) =>
340342
ref.read(listenAndRepeatSettingsProvider).repeatCount,
341343
getIntervalDuration: (s) {

‎lib/providers/repeat_flow/repeat_flow_engine.dart‎

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ class RepeatFlowConfig {
6363
/// promptId 前缀(默认 'lar',各页面可自定义)
6464
final String promptIdPrefix;
6565

66+
/// 当前播放倍速(默认 1.0)。
67+
///
68+
/// 用户跟读的是按此倍速播放后的音频,录音的参考时长与最大时长都按
69+
/// 「实际听到的时长」= 原句时长 ÷ 倍速 计算,避免慢速播放时提前截断录音。
70+
final double Function()? playbackSpeed;
71+
6672
const RepeatFlowConfig({
6773
required this.audioItemId,
6874
required this.getRepeatCount,
@@ -71,6 +77,7 @@ class RepeatFlowConfig {
7177
this.onBeforeSentenceStart,
7278
this.onSentencePlayed,
7379
this.promptIdPrefix = 'lar',
80+
this.playbackSpeed,
7481
});
7582
}
7683

@@ -820,21 +827,37 @@ class RepeatFlowEngine {
820827
'promptId=$promptId',
821828
);
822829

830+
final heardDuration = _heardDuration(sentence);
823831
final computed =
824-
sentence.duration * kRecordingDurationMultiplier +
825-
kRecordingTimeoutBase;
832+
heardDuration * kRecordingDurationMultiplier + kRecordingTimeoutBase;
826833
final maxDuration = computed < kRecordingMinTimeout
827834
? kRecordingMinTimeout
828835
: computed;
829836

830837
callbacks.setMaxRecordingDuration(maxDuration);
831838

832-
AppLogger.log(logTag, '开始录音: $promptId');
839+
AppLogger.log(
840+
logTag,
841+
'开始录音: $promptId referenceDuration=${heardDuration.inMilliseconds}ms '
842+
'(原句${sentence.duration.inMilliseconds}ms) '
843+
'maxDuration=${maxDuration.inMilliseconds}ms',
844+
);
833845
callbacks.startRecording(
834846
promptId: promptId,
835847
referenceText: sentence.text,
836848
maxDuration: maxDuration,
837-
referenceDuration: sentence.duration,
849+
referenceDuration: heardDuration,
850+
);
851+
}
852+
853+
/// 用户实际听到的句子时长:原句时长 ÷ 播放倍速。
854+
///
855+
/// 倍速未配置或非法(≤0、非有限值)时按 1.0x 处理。
856+
Duration _heardDuration(Sentence sentence) {
857+
final speed = _config.playbackSpeed?.call() ?? 1.0;
858+
if (!speed.isFinite || speed <= 0) return sentence.duration;
859+
return Duration(
860+
microseconds: (sentence.duration.inMicroseconds / speed).round(),
838861
);
839862
}
840863

‎test/providers/repeat_flow/repeat_flow_engine_test.dart‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,4 +496,56 @@ void main() {
496496
await replay;
497497
expect(playCalls, 2);
498498
});
499+
500+
// 回归:0.8x 播放时用户跟读的是放慢后的音频,参考时长若仍按 1.0x 原句时长,
501+
// 有声时长很快超过「原句时长」,动态静音兜底被压到 1s,把仍在说话的用户截断。
502+
test('录音参考时长与最大时长按播放倍速换算为实际听到的时长', () async {
503+
final recordings = <({Duration max, Duration? reference})>[];
504+
final engine = RepeatFlowEngine(
505+
onStateChanged: (_) {},
506+
callbacks: RepeatFlowCallbacks(
507+
pauseAudio: () {},
508+
playSentence: (_, _) async => SentencePlaybackResult.completed,
509+
startRecording:
510+
({
511+
required promptId,
512+
required referenceText,
513+
required maxDuration,
514+
referenceDuration,
515+
}) {
516+
recordings.add((max: maxDuration, reference: referenceDuration));
517+
},
518+
cancelRecording: () async {},
519+
stopAndEvaluate: ({required referenceText}) async {},
520+
clearRecording: () {},
521+
setMaxRecordingDuration: (_) {},
522+
hasDetectedSpeech: () => false,
523+
),
524+
);
525+
engine.prepare(
526+
sentences: [
527+
Sentence(
528+
index: 0,
529+
text: 'A slower sentence.',
530+
startTime: Duration.zero,
531+
endTime: const Duration(seconds: 4),
532+
),
533+
],
534+
config: RepeatFlowConfig(
535+
audioItemId: 'audio-1',
536+
getRepeatCount: (_) => 1,
537+
getIntervalDuration: (_) => Duration.zero,
538+
isManualMode: () => false,
539+
playbackSpeed: () => 0.8,
540+
),
541+
);
542+
543+
await engine.startPlaying();
544+
545+
// 4s ÷ 0.8 = 5s;最大时长 = 5s × 2.5 + 5s = 17.5s
546+
expect(recordings, hasLength(1));
547+
expect(recordings.single.reference, const Duration(seconds: 5));
548+
expect(recordings.single.max, const Duration(milliseconds: 17500));
549+
engine.dispose();
550+
});
499551
}

0 commit comments

Comments
 (0)