fix(voice): add per-lesson failure recovery
This commit is contained in:
@@ -22,6 +22,7 @@ import type {
|
|||||||
FeedbackSession,
|
FeedbackSession,
|
||||||
FeedbackSummaryRun,
|
FeedbackSummaryRun,
|
||||||
StudentProfile,
|
StudentProfile,
|
||||||
|
VoiceAudioSegment,
|
||||||
VoiceLesson,
|
VoiceLesson,
|
||||||
VoiceLessonFinished
|
VoiceLessonFinished
|
||||||
} from '../../utils/types'
|
} from '../../utils/types'
|
||||||
@@ -58,6 +59,7 @@ interface VoiceLessonView {
|
|||||||
canAdd: boolean
|
canAdd: boolean
|
||||||
summaryEligible: boolean
|
summaryEligible: boolean
|
||||||
summarySelected: boolean
|
summarySelected: boolean
|
||||||
|
failedSegmentCount: number
|
||||||
addActionText: string
|
addActionText: string
|
||||||
emptyText: string
|
emptyText: string
|
||||||
}
|
}
|
||||||
@@ -144,6 +146,7 @@ function buildVoiceLessonViews(
|
|||||||
const transcript = lesson.transcript.trim()
|
const transcript = lesson.transcript.trim()
|
||||||
const isInDraft = Boolean(transcript) && content.includes(transcript)
|
const isInDraft = Boolean(transcript) && content.includes(transcript)
|
||||||
const summaryEligible = lesson.status === 'ready' && Boolean(transcript)
|
const summaryEligible = lesson.status === 'ready' && Boolean(transcript)
|
||||||
|
const failedSegmentCount = lesson.segments.filter((segment) => segment.status === 'failed').length
|
||||||
let emptyText = '转录完成后可查看内容'
|
let emptyText = '转录完成后可查看内容'
|
||||||
if (lesson.status === 'failed') {
|
if (lesson.status === 'failed') {
|
||||||
emptyText = lesson.segments.find((segment) => segment.status === 'failed')?.errorMessage
|
emptyText = lesson.segments.find((segment) => segment.status === 'failed')?.errorMessage
|
||||||
@@ -162,6 +165,7 @@ function buildVoiceLessonViews(
|
|||||||
canAdd: lesson.status === 'ready' && Boolean(transcript) && !isInDraft,
|
canAdd: lesson.status === 'ready' && Boolean(transcript) && !isInDraft,
|
||||||
summaryEligible,
|
summaryEligible,
|
||||||
summarySelected: summaryEligible && !excluded.has(lesson.id),
|
summarySelected: summaryEligible && !excluded.has(lesson.id),
|
||||||
|
failedSegmentCount,
|
||||||
addActionText: isInDraft
|
addActionText: isInDraft
|
||||||
? '已在输入框'
|
? '已在输入框'
|
||||||
: lesson.appliedDirectly
|
: lesson.appliedDirectly
|
||||||
@@ -1056,13 +1060,34 @@ Page({
|
|||||||
if (!session) return
|
if (!session) return
|
||||||
const failedSegments = getFailedVoiceSegments(session)
|
const failedSegments = getFailedVoiceSegments(session)
|
||||||
if (failedSegments.length === 0) return
|
if (failedSegments.length === 0) return
|
||||||
|
await this.retryVoiceSegments(failedSegments)
|
||||||
|
},
|
||||||
|
|
||||||
|
async retryVoiceLesson(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)
|
||||||
|
},
|
||||||
|
|
||||||
|
async retryVoiceSegments(
|
||||||
|
failedSegments: VoiceAudioSegment[],
|
||||||
|
lessonSequence?: number
|
||||||
|
) {
|
||||||
|
const session = this.data.session
|
||||||
|
if (!session || this.data.recording || this.data.voiceBusy) return
|
||||||
logVoiceOperation('transcription_retry_clicked', {
|
logVoiceOperation('transcription_retry_clicked', {
|
||||||
feedbackSessionId: session.id,
|
feedbackSessionId: session.id,
|
||||||
|
lessonSequence: lessonSequence || null,
|
||||||
failedSegmentCount: failedSegments.length
|
failedSegmentCount: failedSegments.length
|
||||||
})
|
})
|
||||||
this.setData({
|
this.setData({
|
||||||
voiceBusy: true,
|
voiceBusy: true,
|
||||||
voiceStatus: `正在重试${failedSegments.length}段转录`,
|
voiceStatus: lessonSequence
|
||||||
|
? `正在重试第${lessonSequence}节转录`
|
||||||
|
: `正在重试${failedSegments.length}段转录`,
|
||||||
primaryActionText: '正在重试转录',
|
primaryActionText: '正在重试转录',
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
errorRequestId: ''
|
errorRequestId: ''
|
||||||
@@ -1078,9 +1103,13 @@ Page({
|
|||||||
)
|
)
|
||||||
logVoiceOperation('transcription_retry_submitted', {
|
logVoiceOperation('transcription_retry_submitted', {
|
||||||
feedbackSessionId: session.id,
|
feedbackSessionId: session.id,
|
||||||
|
lessonSequence: lessonSequence || null,
|
||||||
segmentIds: failedSegments.map((segment) => segment.id)
|
segmentIds: failedSegments.map((segment) => segment.id)
|
||||||
})
|
})
|
||||||
wx.showToast({ title: '已重新提交,正在转录', icon: 'none' })
|
wx.showToast({
|
||||||
|
title: lessonSequence ? `第${lessonSequence}节已重新提交` : '已重新提交,正在转录',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logVoiceOperation('transcription_retry_failed', {
|
logVoiceOperation('transcription_retry_failed', {
|
||||||
feedbackSessionId: session.id,
|
feedbackSessionId: session.id,
|
||||||
@@ -1094,13 +1123,17 @@ Page({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
confirmDiscardFailedSegments(): Promise<boolean> {
|
confirmDiscardVoiceSegments(
|
||||||
const failedSegmentCount = this.data.session?.failedSegmentCount || 0
|
failedSegmentCount: number,
|
||||||
|
lessonSequence?: number
|
||||||
|
): Promise<boolean> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
wx.showModal({
|
wx.showModal({
|
||||||
title: '放弃失败录音',
|
title: lessonSequence ? `删除第${lessonSequence}节录音` : '删除失败录音',
|
||||||
content: `将删除${failedSegmentCount}段失败录音,其余录音和反馈内容不受影响。此操作无法撤销。`,
|
content: lessonSequence
|
||||||
confirmText: '确认放弃',
|
? `将删除第${lessonSequence}节中${failedSegmentCount}段转录失败的录音,其余语音记录不受影响。此操作无法撤销。`
|
||||||
|
: `将删除${failedSegmentCount}段失败录音,其余录音和反馈内容不受影响。此操作无法撤销。`,
|
||||||
|
confirmText: '确认删除',
|
||||||
confirmColor: '#d13f3a',
|
confirmColor: '#d13f3a',
|
||||||
success: (result) => resolve(result.confirm),
|
success: (result) => resolve(result.confirm),
|
||||||
fail: () => resolve(false)
|
fail: () => resolve(false)
|
||||||
@@ -1113,11 +1146,38 @@ Page({
|
|||||||
const session = this.data.session
|
const session = this.data.session
|
||||||
if (!session) return
|
if (!session) return
|
||||||
const failedSegments = getFailedVoiceSegments(session)
|
const failedSegments = getFailedVoiceSegments(session)
|
||||||
if (failedSegments.length === 0 || !(await this.confirmDiscardFailedSegments())) return
|
if (
|
||||||
|
failedSegments.length === 0
|
||||||
|
|| !(await this.confirmDiscardVoiceSegments(failedSegments.length))
|
||||||
|
) return
|
||||||
|
await this.discardVoiceSegments(failedSegments)
|
||||||
|
},
|
||||||
|
|
||||||
|
async discardVoiceLesson(event: LessonActionEvent) {
|
||||||
|
if (this.data.recording || this.data.voiceBusy) return
|
||||||
|
const session = this.data.session
|
||||||
|
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
|
||||||
|
|| !(await this.confirmDiscardVoiceSegments(failedSegments.length, lesson.sequence))
|
||||||
|
) return
|
||||||
|
await this.discardVoiceSegments(failedSegments, lesson.sequence)
|
||||||
|
},
|
||||||
|
|
||||||
|
async discardVoiceSegments(
|
||||||
|
failedSegments: VoiceAudioSegment[],
|
||||||
|
lessonSequence?: number
|
||||||
|
) {
|
||||||
|
const session = this.data.session
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
this.setData({
|
this.setData({
|
||||||
voiceBusy: true,
|
voiceBusy: true,
|
||||||
voiceStatus: `正在放弃${failedSegments.length}段失败录音`,
|
voiceStatus: lessonSequence
|
||||||
|
? `正在删除第${lessonSequence}节失败录音`
|
||||||
|
: `正在删除${failedSegments.length}段失败录音`,
|
||||||
primaryActionText: '正在处理',
|
primaryActionText: '正在处理',
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
errorRequestId: ''
|
errorRequestId: ''
|
||||||
@@ -1133,9 +1193,13 @@ Page({
|
|||||||
)
|
)
|
||||||
logVoiceOperation('failed_transcription_discarded', {
|
logVoiceOperation('failed_transcription_discarded', {
|
||||||
feedbackSessionId: session.id,
|
feedbackSessionId: session.id,
|
||||||
|
lessonSequence: lessonSequence || null,
|
||||||
segmentIds: failedSegments.map((segment) => segment.id)
|
segmentIds: failedSegments.map((segment) => segment.id)
|
||||||
})
|
})
|
||||||
wx.showToast({ title: '已放弃失败录音', icon: 'none' })
|
wx.showToast({
|
||||||
|
title: lessonSequence ? `第${lessonSequence}节失败录音已删除` : '失败录音已删除',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showApiError(error)
|
this.showApiError(error)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
<text class="voice-record-duration">{{lesson.durationText}} · {{lesson.transcriptLength}}字</text>
|
<text class="voice-record-duration">{{lesson.durationText}} · {{lesson.transcriptLength}}字</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="voice-record-status">{{lesson.statusText}}</text>
|
<text class="voice-record-status {{lesson.failedSegmentCount > 0 ? 'voice-record-status-failed' : ''}}">{{lesson.statusText}}</text>
|
||||||
</view>
|
</view>
|
||||||
<text
|
<text
|
||||||
wx:if="{{lesson.transcript}}"
|
wx:if="{{lesson.transcript}}"
|
||||||
@@ -123,6 +123,20 @@
|
|||||||
bindtap="addVoiceLessonToDraft"
|
bindtap="addVoiceLessonToDraft"
|
||||||
>{{lesson.addActionText}}</button>
|
>{{lesson.addActionText}}</button>
|
||||||
</view>
|
</view>
|
||||||
|
<view wx:if="{{lesson.failedSegmentCount > 0}}" class="voice-record-failure-actions">
|
||||||
|
<button
|
||||||
|
class="voice-record-retry-button"
|
||||||
|
data-lesson-id="{{lesson.id}}"
|
||||||
|
disabled="{{recording || voiceBusy}}"
|
||||||
|
catchtap="retryVoiceLesson"
|
||||||
|
>重试转录</button>
|
||||||
|
<button
|
||||||
|
class="voice-record-delete-button"
|
||||||
|
data-lesson-id="{{lesson.id}}"
|
||||||
|
disabled="{{recording || voiceBusy}}"
|
||||||
|
catchtap="discardVoiceLesson"
|
||||||
|
>删除录音</button>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</checkbox-group>
|
</checkbox-group>
|
||||||
<view wx:if="{{eligibleSummaryLessonCount > 0}}" class="voice-summary-controls">
|
<view wx:if="{{eligibleSummaryLessonCount > 0}}" class="voice-summary-controls">
|
||||||
|
|||||||
@@ -386,6 +386,10 @@
|
|||||||
font-size: 22rpx;
|
font-size: 22rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.voice-record-status-failed {
|
||||||
|
color: #b8322d;
|
||||||
|
}
|
||||||
|
|
||||||
.voice-record-transcript,
|
.voice-record-transcript,
|
||||||
.voice-record-empty {
|
.voice-record-empty {
|
||||||
margin-top: 12rpx;
|
margin-top: 12rpx;
|
||||||
@@ -434,6 +438,61 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.voice-record-failure-actions {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 12rpx;
|
||||||
|
margin-top: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-record-retry-button,
|
||||||
|
.voice-record-delete-button {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
height: 62rpx;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
transition: transform 100ms ease-out, background-color 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-record-retry-button {
|
||||||
|
background: #eaf3ff;
|
||||||
|
color: #007aff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-record-delete-button {
|
||||||
|
background: #fff2f2;
|
||||||
|
color: #b8322d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-record-retry-button::after,
|
||||||
|
.voice-record-delete-button::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-record-retry-button:active,
|
||||||
|
.voice-record-delete-button:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-record-retry-button[disabled],
|
||||||
|
.voice-record-delete-button[disabled] {
|
||||||
|
background: #f1f3f6;
|
||||||
|
color: #aeaeb2;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.voice-summary-controls {
|
.voice-summary-controls {
|
||||||
padding: 18rpx 0;
|
padding: 18rpx 0;
|
||||||
border-top: 1rpx solid #e5e5ea;
|
border-top: 1rpx solid #e5e5ea;
|
||||||
@@ -589,6 +648,8 @@
|
|||||||
.voice-button,
|
.voice-button,
|
||||||
.voice-state-actionable,
|
.voice-state-actionable,
|
||||||
.voice-state-arrow,
|
.voice-state-arrow,
|
||||||
|
.voice-record-retry-button,
|
||||||
|
.voice-record-delete-button,
|
||||||
.feedback-error-copy,
|
.feedback-error-copy,
|
||||||
.feedback-error-retry,
|
.feedback-error-retry,
|
||||||
.feedback-error-close {
|
.feedback-error-close {
|
||||||
@@ -597,6 +658,8 @@
|
|||||||
|
|
||||||
.voice-button:active,
|
.voice-button:active,
|
||||||
.voice-state-actionable:active,
|
.voice-state-actionable:active,
|
||||||
|
.voice-record-retry-button:active,
|
||||||
|
.voice-record-delete-button:active,
|
||||||
.feedback-error-copy:active,
|
.feedback-error-copy:active,
|
||||||
.feedback-error-retry:active,
|
.feedback-error-retry:active,
|
||||||
.feedback-error-close:active {
|
.feedback-error-close:active {
|
||||||
|
|||||||
Reference in New Issue
Block a user