fix(voice): recover empty failed lessons
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<text class="voice-record-duration">{{lesson.durationText}} · {{lesson.transcriptLength}}字</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="voice-record-status {{lesson.failedSegmentCount > 0 ? 'voice-record-status-failed' : ''}}">{{lesson.statusText}}</text>
|
||||
<text class="voice-record-status {{lesson.failureRecoveryAction ? 'voice-record-status-failed' : ''}}">{{lesson.statusText}}</text>
|
||||
</view>
|
||||
<text
|
||||
wx:if="{{lesson.transcript}}"
|
||||
@@ -123,19 +123,19 @@
|
||||
bindtap="addVoiceLessonToDraft"
|
||||
>{{lesson.addActionText}}</button>
|
||||
</view>
|
||||
<view wx:if="{{lesson.failedSegmentCount > 0}}" class="voice-record-failure-actions">
|
||||
<view wx:if="{{lesson.failureRecoveryAction}}" class="voice-record-failure-actions">
|
||||
<button
|
||||
class="voice-record-retry-button"
|
||||
data-lesson-id="{{lesson.id}}"
|
||||
disabled="{{recording || voiceBusy}}"
|
||||
catchtap="retryVoiceLesson"
|
||||
>重试转录</button>
|
||||
catchtap="recoverVoiceLesson"
|
||||
>{{lesson.failureRecoveryText}}</button>
|
||||
<button
|
||||
class="voice-record-delete-button"
|
||||
data-lesson-id="{{lesson.id}}"
|
||||
disabled="{{recording || voiceBusy}}"
|
||||
catchtap="discardVoiceLesson"
|
||||
>删除录音</button>
|
||||
>{{lesson.failureDeleteText}}</button>
|
||||
</view>
|
||||
</view>
|
||||
</checkbox-group>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FeedbackSession, VoiceAudioSegment, VoiceLesson } from '../../utils/types'
|
||||
|
||||
export type VoicePrimaryAction = 'save' | 'retry' | 'processing'
|
||||
export type VoiceLessonRecoveryAction = 'retry' | 'rerecord' | null
|
||||
|
||||
export function getVoicePrimaryAction(session: FeedbackSession | null): VoicePrimaryAction {
|
||||
if (!session || session.lessonCount === 0) return 'save'
|
||||
@@ -24,6 +25,15 @@ export function getFailedVoiceSegments(session: FeedbackSession): VoiceAudioSegm
|
||||
)
|
||||
}
|
||||
|
||||
export function getVoiceLessonRecoveryAction(
|
||||
lesson: VoiceLesson
|
||||
): VoiceLessonRecoveryAction {
|
||||
if (lesson.status !== 'failed') return null
|
||||
return lesson.segments.some((segment) => segment.status === 'failed')
|
||||
? 'retry'
|
||||
: 'rerecord'
|
||||
}
|
||||
|
||||
export function getVoiceLessonStatusText(lesson: VoiceLesson, isInDraft = false): string {
|
||||
if (lesson.status === 'failed') return '转录失败'
|
||||
if (lesson.status === 'recording') return '录音中'
|
||||
|
||||
Reference in New Issue
Block a user