fix(voice): recover empty failed lessons

This commit is contained in:
zhangheng
2026-07-22 21:19:22 +08:00
parent 55084a4784
commit 80e670d72b
8 changed files with 226 additions and 13 deletions

View File

@@ -4,6 +4,7 @@ import {
createFeedbackRecord,
createFeedbackSummaryRun,
createVoiceLesson,
discardEmptyVoiceLesson,
discardVoiceSegment,
ensureFeedbackSession,
finishVoiceLesson,
@@ -27,9 +28,11 @@ import type {
VoiceLessonFinished
} from '../../utils/types'
import { runWithSessionRefresh } from './voice-actions'
import type { VoiceLessonRecoveryAction } from './voice-state'
import {
getFailedVoiceSegments,
getVoiceFailureHint,
getVoiceLessonRecoveryAction,
getVoiceLessonStatusText,
getVoicePrimaryAction,
getVoicePrimaryActionText
@@ -60,6 +63,9 @@ interface VoiceLessonView {
summaryEligible: boolean
summarySelected: boolean
failedSegmentCount: number
failureRecoveryAction: VoiceLessonRecoveryAction
failureRecoveryText: string
failureDeleteText: string
addActionText: string
emptyText: string
}
@@ -127,7 +133,7 @@ function formatRecordingTime(milliseconds: number): string {
function formatVoiceDuration(milliseconds: number): string {
const totalMinutes = Math.floor(milliseconds / 60000)
if (totalMinutes < 1) return `${Math.max(1, Math.round(milliseconds / 1000))}`
if (totalMinutes < 1) return `${Math.max(0, Math.round(milliseconds / 1000))}`
return `${totalMinutes}分钟`
}
@@ -147,6 +153,7 @@ function buildVoiceLessonViews(
const isInDraft = Boolean(transcript) && content.includes(transcript)
const summaryEligible = lesson.status === 'ready' && Boolean(transcript)
const failedSegmentCount = lesson.segments.filter((segment) => segment.status === 'failed').length
const failureRecoveryAction = getVoiceLessonRecoveryAction(lesson)
let emptyText = '转录完成后可查看内容'
if (lesson.status === 'failed') {
emptyText = lesson.segments.find((segment) => segment.status === 'failed')?.errorMessage
@@ -166,6 +173,9 @@ function buildVoiceLessonViews(
summaryEligible,
summarySelected: summaryEligible && !excluded.has(lesson.id),
failedSegmentCount,
failureRecoveryAction,
failureRecoveryText: failureRecoveryAction === 'retry' ? '重试转录' : '重新录制',
failureDeleteText: failureRecoveryAction === 'retry' ? '删除录音' : '删除记录',
addActionText: isInDraft
? '已在输入框'
: lesson.appliedDirectly
@@ -387,8 +397,12 @@ Page({
}
const prefix = `语音记录 · ${session.lessonCount}节 · ${formatVoiceDuration(session.totalDurationMs)}`
const emptyFailedLessonCount = session.lessons.filter((lesson) =>
lesson.status === 'failed' && lesson.segments.length === 0
).length
let suffix = '已就绪'
if (session.failedSegmentCount > 0) suffix = `${session.failedSegmentCount}段转录失败`
else if (emptyFailedLessonCount > 0) suffix = `${emptyFailedLessonCount}节未保留录音`
else if (session.processingSegmentCount > 0) suffix = '正在处理'
else if (session.needsGeneration) suffix = '可生成总结'
const primaryAction = getVoicePrimaryAction(session)
@@ -683,8 +697,13 @@ Page({
return
}
await this.startVoiceRecording()
},
async startVoiceRecording(permissionGranted = false) {
this.setData({ voiceBusy: true, voiceStatus: '正在开启麦克风' })
const authorized = await this.ensureRecordPermission()
let authorized = permissionGranted
if (!authorized) authorized = await this.ensureRecordPermission()
if (!authorized) {
this.setData({ voiceBusy: false })
this.syncVoicePresentation(this.data.session)
@@ -1063,13 +1082,57 @@ Page({
await this.retryVoiceSegments(failedSegments)
},
async retryVoiceLesson(event: LessonActionEvent) {
async recoverVoiceLesson(event: LessonActionEvent) {
const session = this.data.session
const lesson = session?.lessons.find((item) => item.id === event.currentTarget.dataset.lessonId)
if (!lesson) return
const failedSegments = lesson.segments.filter((segment) => segment.status === 'failed')
if (failedSegments.length === 0) return
await this.retryVoiceSegments(failedSegments, lesson.sequence)
if (failedSegments.length > 0) {
await this.retryVoiceSegments(failedSegments, lesson.sequence)
return
}
if (lesson.status === 'failed') await this.rerecordVoiceLesson(lesson)
},
confirmRerecordVoiceLesson(lessonSequence: number): Promise<boolean> {
return new Promise((resolve) => {
wx.showModal({
title: `重新录制第${lessonSequence}`,
content: '该节没有保留可重试的音频。重新录制将删除这条失败记录并开启麦克风。',
confirmText: '重新录制',
success: (result) => resolve(result.confirm),
fail: () => resolve(false)
})
})
},
async rerecordVoiceLesson(lesson: VoiceLesson) {
if (
this.data.recording
|| this.data.voiceBusy
|| !(await this.confirmRerecordVoiceLesson(lesson.sequence))
) return
this.setData({ voiceBusy: true, voiceStatus: `正在准备重新录制第${lesson.sequence}` })
const authorized = await this.ensureRecordPermission()
if (!authorized) {
this.setData({ voiceBusy: false })
this.syncVoicePresentation(this.data.session)
return
}
try {
await discardEmptyVoiceLesson(lesson.id)
logVoiceOperation('empty_failed_lesson_rerecord_started', {
feedbackSessionId: this.data.session?.id || null,
lessonId: lesson.id,
lessonSequence: lesson.sequence
})
await this.startVoiceRecording(true)
} catch (error) {
this.setData({ voiceBusy: false })
await this.loadActiveSession()
this.syncVoicePresentation(this.data.session)
this.showApiError(error)
}
},
async retryVoiceSegments(
@@ -1141,6 +1204,19 @@ Page({
})
},
confirmDiscardEmptyVoiceLesson(lessonSequence: number): Promise<boolean> {
return new Promise((resolve) => {
wx.showModal({
title: `删除第${lessonSequence}节记录`,
content: '该节没有保留可用录音,删除后不会影响其他语音记录。此操作无法撤销。',
confirmText: '确认删除',
confirmColor: '#d13f3a',
success: (result) => resolve(result.confirm),
fail: () => resolve(false)
})
})
},
async discardFailedSegments() {
if (this.data.recording || this.data.voiceBusy) return
const session = this.data.session
@@ -1159,13 +1235,50 @@ Page({
const lesson = session?.lessons.find((item) => item.id === event.currentTarget.dataset.lessonId)
if (!session || !lesson) return
const failedSegments = lesson.segments.filter((segment) => segment.status === 'failed')
if (failedSegments.length === 0) {
if (
lesson.status === 'failed'
&& await this.confirmDiscardEmptyVoiceLesson(lesson.sequence)
) await this.discardEmptyFailedVoiceLesson(lesson)
return
}
if (
failedSegments.length === 0
|| !(await this.confirmDiscardVoiceSegments(failedSegments.length, lesson.sequence))
!(await this.confirmDiscardVoiceSegments(failedSegments.length, lesson.sequence))
) return
await this.discardVoiceSegments(failedSegments, lesson.sequence)
},
async discardEmptyFailedVoiceLesson(lesson: VoiceLesson) {
const session = this.data.session
if (!session) return
this.setData({
voiceBusy: true,
voiceStatus: `正在删除第${lesson.sequence}节失败记录`,
primaryActionText: '正在处理',
errorMessage: '',
errorRequestId: ''
})
try {
await runWithSessionRefresh(
async () => discardEmptyVoiceLesson(lesson.id),
async () => {
await this.loadActiveSession()
}
)
logVoiceOperation('empty_failed_lesson_discarded', {
feedbackSessionId: session.id,
lessonId: lesson.id,
lessonSequence: lesson.sequence
})
wx.showToast({ title: `${lesson.sequence}节失败记录已删除`, icon: 'none' })
} catch (error) {
this.showApiError(error)
} finally {
this.setData({ voiceBusy: false })
this.syncVoicePresentation(this.data.session)
}
},
async discardVoiceSegments(
failedSegments: VoiceAudioSegment[],
lessonSequence?: number