feat: add voice transcription feedback workflow
This commit is contained in:
257
utils/api.ts
257
utils/api.ts
@@ -1,11 +1,18 @@
|
||||
import type {
|
||||
FeedbackDraft,
|
||||
FeedbackRecord,
|
||||
FeedbackSession,
|
||||
GeneratedFeedback,
|
||||
HealthStatus,
|
||||
StudentDraft,
|
||||
StudentProfile,
|
||||
StudentProfileDefaults,
|
||||
Term
|
||||
Term,
|
||||
VoiceAudioSegment,
|
||||
VoiceLesson,
|
||||
VoiceLessonCreated,
|
||||
VoiceLessonFinished,
|
||||
VoiceProcessingStatus
|
||||
} from './types'
|
||||
|
||||
const DEFAULT_API_BASE_URL = 'http://127.0.0.1:8080'
|
||||
@@ -50,6 +57,66 @@ interface RawFeedbackRecord {
|
||||
interface RawHealthStatus {
|
||||
status: string
|
||||
database_configured: boolean
|
||||
speech_configured: boolean
|
||||
speech_available: boolean
|
||||
speech_provider: string | null
|
||||
summary_configured: boolean
|
||||
}
|
||||
|
||||
interface RawVoiceAudioSegment {
|
||||
id: string
|
||||
sequence: number
|
||||
duration_ms: number
|
||||
status: 'processing' | 'ready' | 'failed'
|
||||
error_message: string | null
|
||||
}
|
||||
|
||||
interface RawVoiceLesson {
|
||||
id: string
|
||||
sequence: number
|
||||
status: VoiceProcessingStatus
|
||||
duration_ms: number
|
||||
applied_directly: boolean
|
||||
included_in_generation: boolean
|
||||
segments: RawVoiceAudioSegment[]
|
||||
started_at: string
|
||||
ended_at: string | null
|
||||
}
|
||||
|
||||
interface RawFeedbackSession {
|
||||
id: string
|
||||
profile_id: string | null
|
||||
feedback_date: string
|
||||
content: string
|
||||
status: 'active' | 'finalized'
|
||||
lesson_count: number
|
||||
total_duration_ms: number
|
||||
processing_segment_count: number
|
||||
failed_segment_count: number
|
||||
needs_generation: boolean
|
||||
lessons: RawVoiceLesson[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
interface RawVoiceLessonCreated {
|
||||
id: string
|
||||
sequence: number
|
||||
status: VoiceProcessingStatus
|
||||
started_at: string
|
||||
}
|
||||
|
||||
interface RawVoiceLessonFinished {
|
||||
id: string
|
||||
status: VoiceProcessingStatus
|
||||
duration_ms: number
|
||||
transcript: string
|
||||
can_apply_directly: boolean
|
||||
}
|
||||
|
||||
interface RawGeneratedFeedback {
|
||||
content: string
|
||||
method: 'llm' | 'extractive'
|
||||
}
|
||||
|
||||
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||
@@ -81,17 +148,17 @@ export function getApiErrorMessage(error: unknown): string {
|
||||
if (!(error instanceof ApiRequestError)) return '请求失败,请稍后重试'
|
||||
if (error.statusCode === 401) return '开发身份未通过后端校验'
|
||||
if (error.statusCode === 404) return '请求的数据不存在或无权访问'
|
||||
if (error.statusCode === 503) return '后端尚未配置数据库连接'
|
||||
if (error.statusCode === 503 && error.message.includes('database')) return '后端尚未配置数据库连接'
|
||||
return error.message
|
||||
}
|
||||
|
||||
function request<T>(path: string, method: HttpMethod = 'GET', data?: object): Promise<T> {
|
||||
function request<T>(path: string, method: HttpMethod = 'GET', data?: object, timeout = 10000): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.request({
|
||||
url: `${getApiBaseUrl()}${path}`,
|
||||
method,
|
||||
data,
|
||||
timeout: 10000,
|
||||
timeout,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-User-Id': DEVELOPMENT_USER_ID
|
||||
@@ -165,11 +232,57 @@ function mapFeedbackRecord(record: RawFeedbackRecord): FeedbackRecord {
|
||||
}
|
||||
}
|
||||
|
||||
function mapVoiceSegment(segment: RawVoiceAudioSegment): VoiceAudioSegment {
|
||||
return {
|
||||
id: segment.id,
|
||||
sequence: segment.sequence,
|
||||
durationMs: segment.duration_ms,
|
||||
status: segment.status,
|
||||
errorMessage: segment.error_message
|
||||
}
|
||||
}
|
||||
|
||||
function mapVoiceLesson(lesson: RawVoiceLesson): VoiceLesson {
|
||||
return {
|
||||
id: lesson.id,
|
||||
sequence: lesson.sequence,
|
||||
status: lesson.status,
|
||||
durationMs: lesson.duration_ms,
|
||||
appliedDirectly: lesson.applied_directly,
|
||||
includedInGeneration: lesson.included_in_generation,
|
||||
segments: lesson.segments.map(mapVoiceSegment),
|
||||
startedAt: parseTimestamp(lesson.started_at),
|
||||
endedAt: lesson.ended_at ? parseTimestamp(lesson.ended_at) : null
|
||||
}
|
||||
}
|
||||
|
||||
function mapFeedbackSession(session: RawFeedbackSession): FeedbackSession {
|
||||
return {
|
||||
id: session.id,
|
||||
profileId: session.profile_id,
|
||||
feedbackDate: session.feedback_date,
|
||||
content: session.content,
|
||||
status: session.status,
|
||||
lessonCount: session.lesson_count,
|
||||
totalDurationMs: session.total_duration_ms,
|
||||
processingSegmentCount: session.processing_segment_count,
|
||||
failedSegmentCount: session.failed_segment_count,
|
||||
needsGeneration: session.needs_generation,
|
||||
lessons: session.lessons.map(mapVoiceLesson),
|
||||
createdAt: parseTimestamp(session.created_at),
|
||||
updatedAt: parseTimestamp(session.updated_at)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHealth(): Promise<HealthStatus> {
|
||||
const result = await request<RawHealthStatus>('/health')
|
||||
return {
|
||||
status: result.status,
|
||||
databaseConfigured: result.database_configured
|
||||
databaseConfigured: result.database_configured,
|
||||
speechConfigured: result.speech_configured,
|
||||
speechAvailable: result.speech_available,
|
||||
speechProvider: result.speech_provider,
|
||||
summaryConfigured: result.summary_configured
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +322,8 @@ export async function createFeedbackRecord(draft: FeedbackDraft): Promise<Feedba
|
||||
const result = await request<RawFeedbackRecord>('/api/v1/feedback-records', 'POST', {
|
||||
profile_id: draft.profileId,
|
||||
feedback_date: draft.feedbackDate,
|
||||
content: draft.content
|
||||
content: draft.content,
|
||||
feedback_session_id: draft.feedbackSessionId || null
|
||||
})
|
||||
return mapFeedbackRecord(result)
|
||||
}
|
||||
@@ -217,3 +331,134 @@ export async function createFeedbackRecord(draft: FeedbackDraft): Promise<Feedba
|
||||
export async function deleteFeedbackRecord(recordId: string): Promise<void> {
|
||||
await request<unknown>(`/api/v1/feedback-records/${encodeURIComponent(recordId)}`, 'DELETE')
|
||||
}
|
||||
|
||||
export async function getActiveFeedbackSession(profileId: string | null): Promise<FeedbackSession | null> {
|
||||
const query = profileId ? `?profile_id=${encodeURIComponent(profileId)}` : ''
|
||||
const result = await request<RawFeedbackSession | null>(`/api/v1/feedback-sessions/active${query}`)
|
||||
return result ? mapFeedbackSession(result) : null
|
||||
}
|
||||
|
||||
export async function ensureFeedbackSession(
|
||||
profileId: string | null,
|
||||
feedbackDate: string,
|
||||
content: string
|
||||
): Promise<FeedbackSession> {
|
||||
const result = await request<RawFeedbackSession>('/api/v1/feedback-sessions', 'POST', {
|
||||
profile_id: profileId,
|
||||
feedback_date: feedbackDate,
|
||||
content
|
||||
})
|
||||
return mapFeedbackSession(result)
|
||||
}
|
||||
|
||||
export async function updateFeedbackSession(
|
||||
sessionId: string,
|
||||
feedbackDate: string,
|
||||
content: string
|
||||
): Promise<FeedbackSession> {
|
||||
const result = await request<RawFeedbackSession>(
|
||||
`/api/v1/feedback-sessions/${encodeURIComponent(sessionId)}`,
|
||||
'PUT',
|
||||
{ feedback_date: feedbackDate, content }
|
||||
)
|
||||
return mapFeedbackSession(result)
|
||||
}
|
||||
|
||||
export async function createVoiceLesson(sessionId: string): Promise<VoiceLessonCreated> {
|
||||
const result = await request<RawVoiceLessonCreated>(
|
||||
`/api/v1/feedback-sessions/${encodeURIComponent(sessionId)}/lessons`,
|
||||
'POST'
|
||||
)
|
||||
return {
|
||||
id: result.id,
|
||||
sequence: result.sequence,
|
||||
status: result.status,
|
||||
startedAt: parseTimestamp(result.started_at)
|
||||
}
|
||||
}
|
||||
|
||||
export function uploadVoiceSegment(
|
||||
lessonId: string,
|
||||
filePath: string,
|
||||
sequence: number,
|
||||
durationMs: number
|
||||
): Promise<VoiceAudioSegment> {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: `${getApiBaseUrl()}/api/v1/feedback-lessons/${encodeURIComponent(lessonId)}/segments`,
|
||||
filePath,
|
||||
name: 'audio',
|
||||
formData: {
|
||||
sequence: String(sequence),
|
||||
duration_ms: String(Math.max(1, Math.round(durationMs))),
|
||||
audio_format: 'mp3'
|
||||
},
|
||||
timeout: 120000,
|
||||
header: { 'X-User-Id': DEVELOPMENT_USER_ID },
|
||||
success(response) {
|
||||
let body: RawVoiceAudioSegment | ApiErrorBody
|
||||
try {
|
||||
body = JSON.parse(response.data) as RawVoiceAudioSegment | ApiErrorBody
|
||||
} catch {
|
||||
reject(new ApiRequestError('服务返回了无效的录音响应', response.statusCode))
|
||||
return
|
||||
}
|
||||
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||
resolve(mapVoiceSegment(body as RawVoiceAudioSegment))
|
||||
return
|
||||
}
|
||||
reject(new ApiRequestError((body as ApiErrorBody).error || `服务返回 HTTP ${response.statusCode}`, response.statusCode))
|
||||
},
|
||||
fail(error) {
|
||||
reject(new ApiRequestError(`录音上传失败:${error.errMsg}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export async function finishVoiceLesson(lessonId: string): Promise<VoiceLessonFinished> {
|
||||
const result = await request<RawVoiceLessonFinished>(
|
||||
`/api/v1/feedback-lessons/${encodeURIComponent(lessonId)}/finish`,
|
||||
'POST',
|
||||
undefined,
|
||||
120000
|
||||
)
|
||||
return {
|
||||
id: result.id,
|
||||
status: result.status,
|
||||
durationMs: result.duration_ms,
|
||||
transcript: result.transcript,
|
||||
canApplyDirectly: result.can_apply_directly
|
||||
}
|
||||
}
|
||||
|
||||
export async function markVoiceLessonApplied(lessonId: string, content: string): Promise<void> {
|
||||
await request<unknown>(
|
||||
`/api/v1/feedback-lessons/${encodeURIComponent(lessonId)}/mark-applied`,
|
||||
'POST',
|
||||
{ content }
|
||||
)
|
||||
}
|
||||
|
||||
export async function retryVoiceSegment(segmentId: string): Promise<VoiceAudioSegment> {
|
||||
const result = await request<RawVoiceAudioSegment>(
|
||||
`/api/v1/audio-segments/${encodeURIComponent(segmentId)}/retry`,
|
||||
'POST',
|
||||
undefined,
|
||||
120000
|
||||
)
|
||||
return mapVoiceSegment(result)
|
||||
}
|
||||
|
||||
export async function generateFeedbackFromVoice(
|
||||
sessionId: string,
|
||||
existingContent: string
|
||||
): Promise<GeneratedFeedback> {
|
||||
const result = await request<RawGeneratedFeedback>(
|
||||
`/api/v1/feedback-sessions/${encodeURIComponent(sessionId)}/generate`,
|
||||
'POST',
|
||||
{ existing_content: existingContent },
|
||||
120000
|
||||
)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -44,9 +44,72 @@ export interface FeedbackDraft {
|
||||
profileId: string | null
|
||||
feedbackDate: string
|
||||
content: string
|
||||
feedbackSessionId?: string | null
|
||||
}
|
||||
|
||||
export interface HealthStatus {
|
||||
status: string
|
||||
databaseConfigured: boolean
|
||||
speechConfigured: boolean
|
||||
speechAvailable: boolean
|
||||
speechProvider: string | null
|
||||
summaryConfigured: boolean
|
||||
}
|
||||
|
||||
export type VoiceProcessingStatus = 'recording' | 'processing' | 'ready' | 'failed'
|
||||
|
||||
export interface VoiceAudioSegment {
|
||||
id: string
|
||||
sequence: number
|
||||
durationMs: number
|
||||
status: 'processing' | 'ready' | 'failed'
|
||||
errorMessage: string | null
|
||||
}
|
||||
|
||||
export interface VoiceLesson {
|
||||
id: string
|
||||
sequence: number
|
||||
status: VoiceProcessingStatus
|
||||
durationMs: number
|
||||
appliedDirectly: boolean
|
||||
includedInGeneration: boolean
|
||||
segments: VoiceAudioSegment[]
|
||||
startedAt: number
|
||||
endedAt: number | null
|
||||
}
|
||||
|
||||
export interface FeedbackSession {
|
||||
id: string
|
||||
profileId: string | null
|
||||
feedbackDate: string
|
||||
content: string
|
||||
status: 'active' | 'finalized'
|
||||
lessonCount: number
|
||||
totalDurationMs: number
|
||||
processingSegmentCount: number
|
||||
failedSegmentCount: number
|
||||
needsGeneration: boolean
|
||||
lessons: VoiceLesson[]
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
}
|
||||
|
||||
export interface VoiceLessonCreated {
|
||||
id: string
|
||||
sequence: number
|
||||
status: VoiceProcessingStatus
|
||||
startedAt: number
|
||||
}
|
||||
|
||||
export interface VoiceLessonFinished {
|
||||
id: string
|
||||
status: VoiceProcessingStatus
|
||||
durationMs: number
|
||||
transcript: string
|
||||
canApplyDirectly: boolean
|
||||
}
|
||||
|
||||
export interface GeneratedFeedback {
|
||||
content: string
|
||||
method: 'llm' | 'extractive'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user