← Tasks
[CP-VOICE-02] VoiceCapture screen — mic button triggers voice-to-voter data extraction (retry 1)
completedcode_genP1
Description
## CP-VOICE-02: VoiceCapture screen — mic button triggers voice-to-voter data extraction
Section: Voice Integration | Milestone: M3 | Owner: VS Lu
### Failing Tests
- **voice_capture_file** (file_exists): File `campaignpro/src/screens/VoiceCaptureScreen.tsx` must exist
- **voice_livekit** (grep): Pattern `useVoiceRoom|LiveKit|RoomAudioRenderer|useRoom` in `campaignpro/src/screens/VoiceCaptureScreen.tsx` (need 1+ matches)
- **voice_transcript** (grep): Pattern `transcript|transcription|onTranscript` in `campaignpro/src/screens/VoiceCaptureScreen.tsx` (need 1+ matches)
### Acceptance Criteria
- File `campaignpro/src/screens/VoiceCaptureScreen.tsx` must exist
- File matching `campaignpro/src/screens/VoiceCaptureScreen.tsx` must contain pattern `useVoiceRoom|LiveKit|RoomAudioRenderer|useRoom`
- File matching `campaignpro/src/screens/VoiceCaptureScreen.tsx` must contain pattern `transcript|transcription|onTranscript`
### Codebase Conventions
- Trilingual comments: // EN: ... // VI: ... // ES: ...
- SPEC section refs in comments
- Zod at module level with .safeParse()
- (supabase as any) with eslint-disable
- NextResponse.json() responses
- Error format: { error: 'message' }
### Dependencies
- CP-VOICE-01: FAIL
- CP-API-01: FAIL
Assignee
-
Claimed By
Cloud Lu → Claude Sonnet
Created
1d ago
Completed
1d ago
Quality
Overall
99%
Pass
PASS
Graded By
-
Graded At
-
Cost
Input Tokens
-
Output Tokens
-
Est. USD
-
Result
{
"cost": {
"input_tokens": 455,
"estimated_usd": 0.052185,
"output_tokens": 3388
},
"model": "claude-sonnet-4-20250514",
"status": "success",
"transport": "anthropic_api",
"latency_ms": 42199,
"response_text": "I'll create the VoiceCapture screen component that integrates with LiveKit for voice-to-voter data extraction.\n\n```typescript\n// campaignpro/src/screens/VoiceCaptureScreen.tsx\n\nimport React, { useState, useEffect, useRef } from 'react';\nimport { View, Text, TouchableOpacity, StyleSheet, Alert } from 'react-native';\nimport { useVoiceRoom } from '../hooks/useVoiceRoom';\nimport { RoomAudioRenderer } from '@livekit/react-native';\nimport { Room, Track } from 'livekit-client';\nimport { z } from 'zod';\n\n// EN: Voice capture validation schema\n// VI: Lược đồ xác thực thu âm giọng nói \n// ES: Esquema de validación de captura de voz\nconst VoiceDataSchema = z.object({\n voterName: z.string().min(1),\n phoneNumber: z.string().optional(),\n address: z.string().optional(),\n notes: z.string().optional(),\n confidence: z.number().min(0).max(1),\n duration: z.number().positive(),\n});\n\ntype VoiceData = z.infer<typeof VoiceDataSchema>;\n\ninterface VoiceCaptureScreenProps {\n onVoterDataExtracted?: (data: VoiceData) => void;\n campaignId?: string;\n}\n\n// EN: Voice capture screen for extracting voter data from speech\n// VI: Màn hình thu âm giọng nói để trích xuất dữ liệu cử tri từ lời nói\n// ES: Pantalla de captura de voz para extraer datos de votantes del habla\n// SPEC: CP-VOICE-02 - Voice-to-voter data extraction with mic button\nexport const VoiceCaptureScreen: React.FC<VoiceCaptureScreenProps> = ({\n onVoterDataExtracted,\n campaignId,\n}) => {\n const [isRecording, setIsRecording] = useState(false);\n const [transcript, setTranscript] = useState('');\n const [isProcessing, setIsProcessing] = useState(false);\n const transcriptionRef = useRef<string>('');\n\n // EN: Initialize voice room connection with LiveKit\n // VI: Khởi tạo kết nối phòng giọng nói với LiveKit\n // ES: Inicializar conexión de sala de voz con LiveKit\n const {\n room,\n isConnected,\n connect,\n disconnect,\n startRecording,\n stopRecording,\n error: roomError,\n } = useVoiceRoom({\n serverUrl: process.env.EXPO_PUBLIC_LIVEKIT_URL || '',\n onTranscript: handleTranscriptUpdate,\n });\n\n // EN: Handle transcript updates from voice recognition\n // VI: Xử lý cập nhật bản ghi từ nhận dạng giọng nói\n // ES: Manejar actualizaciones de transcripción del reconocimiento de voz\n function handleTranscriptUpdate(newTranscript: string) {\n transcriptionRef.current = newTranscript;\n setTranscript(newTranscript);\n }\n\n // EN: Extract voter data from transcription using AI\n // VI: Trích xuất dữ liệu cử tri từ bản ghi bằng AI\n // ES: Extraer datos de votante de la transcripción usando IA\n const extractVoterData = async (transcriptionText: string): Promise<VoiceData | null> => {\n try {\n const response = await fetch('/api/voice/extract-voter-data', {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n transcript: transcriptionText,\n campaignId,\n }),\n });\n\n if (!response.ok) {\n throw new Error('Failed to extract voter data');\n }\n\n const data = await response.json();\n const validation = VoiceDataSchema.safeParse(data);\n\n if (!validation.success) {\n console.error('Invalid voter data format:', validation.error);\n return null;\n }\n\n return validation.data;\n } catch (error) {\n console.error('Error extracting voter data:', error);\n return null;\n }\n };\n\n // EN: Handle microphone button press to start/stop recording\n // VI: Xử lý nhấn nút micro để bắt đầu/dừng ghi âm\n // ES: Manejar presión del botón del micrófono para iniciar/detener grabación\n const handleMicPress = async () => {\n if (!isConnected) {\n try {\n await connect();\n } catch (error) {\n Alert.alert('Connection Error', 'Failed to connect to voice service');\n return;\n }\n }\n\n if (isRecording) {\n // EN: Stop recording and process transcription\n // VI: Dừng ghi âm và xử lý bản ghi\n // ES: Detener grabación y procesar transcripción\n setIsRecording(false);\n setIsProcessing(true);\n \n try {\n await stopRecording();\n \n if (transcriptionRef.current.trim()) {\n const voterData = await extractVoterData(transcriptionRef.current);\n \n if (voterData && onVoterDataExtracted) {\n onVoterDataExtracted(voterData);\n } else {\n Alert.alert(\n 'No Data Found',\n 'Could not extract voter information from the recording. Please try again with clearer speech.'\n );\n }\n }\n } catch (error) {\n Alert.alert('Processing Error', 'Failed to process voice recording');\n } finally {\n setIsProcessing(false);\n }\n } else {\n // EN: Start recording\n // VI: Bắt đầu ghi âm\n // ES: Iniciar grabación\n setIsRecording(true);\n setTranscript('');\n transcriptionRef.current = '';\n \n try {\n await startRecording();\n } catch (error) {\n setIsRecording(false);\n Alert.alert('Recording Error', 'Failed to start voice recording');\n }\n }\n };\n\n // EN: Cleanup on component unmount\n // VI: Dọn dẹp khi component bị hủy\n // ES: Limpieza al desmontar el componente\n useEffect(() => {\n return () => {\n if (isConnected) {\n disconnect();\n }\n };\n }, [isConnected, disconnect]);\n\n return (\n <View style={styles.container}>\n {/* LiveKit Audio Renderer */}\n {room && <RoomAudioRenderer room={room} />}\n \n <View style={styles.header}>\n <Text style={styles.title}>\n {/* EN: Voice Capture */}\n {/* VI: Thu Âm Giọng Nói */}\n {/* ES: Captura de Voz */}\n Voice Capture\n </Text>\n <Text style={styles.subtitle}>\n {/* EN: Tap the microphone to record voter information */}\n {/* VI: Nhấn vào micro để ghi thông tin cử tri */}\n {/* ES: Toca el micrófono para grabar información del votante */}\n Tap the microphone to record voter information\n </Text>\n </View>\n\n <View style={styles.micContainer}>\n <TouchableOpacity\n style={[\n styles.micButton,\n isRecording && styles.micButtonActive,\n (isProcessing || roomError) && styles.micButtonDisabled,\n ]}\n onPress={handleMicPress}\n disabled={isProcessing}\n >\n <Text style={styles.micIcon}>\n {isProcessing ? '⏳' : isRecording ? '⏹️' : '🎤'}\n </Text>\n </TouchableOpacity>\n \n <Text style={styles.micStatus}>\n {isProcessing \n ? 'Processing...'\n : isRecording \n ? 'Recording... Tap to stop'\n : 'Tap to start recording'\n }\n </Text>\n </View>\n\n {transcript && (\n <View style={styles.transcriptContainer}>\n <Text style={styles.transcriptLabel}>Live Transcription:</Text>\n <Text style={styles.transcriptText}>{transcript}</Text>\n </View>\n )}\n\n {roomError && (\n <View style={styles.errorContainer}>\n <Text style={styles.errorText}>\n Connection Error: {roomError}\n </Text>\n </View>\n )}\n\n <View style={styles.instructions}>\n <Text style={styles.instructionText}>\n {/* EN: Speak clearly and include: Name, Phone, Address */}\n {/* VI: Nói rõ ràng và bao gồm: Tên, Số điện thoại, Địa chỉ */}\n {/* ES: Habla claramente e incluye: Nombre, Teléfono, Dirección */}\n 💡 Speak clearly and include: Name, Phone, Address\n </Text>\n </View>\n </View>\n );\n};\n\nconst styles = StyleSheet.create({\n container: {\n flex: 1,\n backgroundColor: '#f5f5f5',\n padding: 20,\n },\n header: {\n alignItems: 'center',\n marginBottom: 40,\n marginTop: 20,\n },\n title: {\n fontSize: 28,\n fontWeight: 'bold',\n color: '#2c3e50',\n marginBottom: 8,\n },\n subtitle: {\n fontSize: 16,\n color: '#7f8c8d',\n textAlign: 'center',\n lineHeight: 22,\n },\n micContainer: {\n alignItems: 'center',\n marginBottom: 40,\n },\n micButton: {\n width: 120,\n height: 120,\n borderRadius: 60,\n backgroundColor: '#3498db',\n justifyContent: 'center',\n alignItems: 'center',\n elevation: 5,\n shadowColor: '#000',\n shadowOffset: {\n width: 0,\n height: 4,\n },\n shadowOpacity: 0.3,\n shadowRadius: 4.65,\n marginBottom: 16,\n },\n micButtonActive: {\n backgroundColor: '#e74c3c',\n },\n micButtonDisabled: {\n backgroundColor: '#95a5a6',\n opacity: 0.6,\n },\n micIcon: {\n fontSize: 40,\n },\n micStatus: {\n fontSize: 16,\n color: '#34495e',\n textAlign: 'center',\n },\n transcriptContainer: {\n backgroundColor: '#ffffff',\n padding: 20,\n borderRadius: 12,\n marginBottom: 20,\n elevation: 2,\n shadowColor: '#000',\n shadowOffset: {\n width: 0,\n height: 2,\n },\n shadowOpacity: 0.1,\n shadowRadius: 3.84,\n },\n transcriptLabel: {\n fontSize: 14,\n fontWeight: '600',\n color: '#2c3e50',\n marginBottom: 8,\n },\n transcriptText: {\n fontSize: 16,\n color: '#34495e',\n lineHeight: 24,\n },\n errorContainer: {\n backgroundColor: '#ffe6e6',\n padding: 16,\n borderRadius: 8,\n marginBottom: 20,\n borderLeftWidth: 4,\n borderLeftColor: '#e74c3c',\n },\n errorText: {\n color: '#c0392b',\n fontSize: 14,\n fontWeight: '500',\n },\n instructions: {\n backgroundColor: '#e8f6ff',\n padding: 16,\n borderRadius: 8,\n borderLeftWidth: 4,\n borderLeftColor: '#3498db',\n },\n instructionText: {\n fontSize: 14,\n color: '#2980b9',\n lineHeight: 20,\n },\n});\n\nexport default VoiceCaptureScreen;\n```\n\nThis implementation provides:\n\n1. **File Creation**: Creates the required `VoiceCaptureScreen.tsx` file\n2. **LiveKit Integration**: Uses `useVoiceRoom`, `RoomAudioRenderer`, and LiveKit `Room` components\n3. **Transcription Handling**: Implements transcript processing with `onTranscript` callback\n4. **Voice-to-Voter Data Extraction**: Mic button triggers recording and data extraction\n5. **Trilingual Comments**: All major sections include EN/VI/ES comments\n6. **Zod Validation**: Schema validation for extracted voter data\n7. **Error Handling**: Comprehensive error handling for connection and processing issues\n8. **UI/UX**: Clean, accessible interface with visual feedback for recording states\n\nThe component integrates with the LiveKit voice service, processes transcriptions in real-time, and extracts structured voter data from speech input."
}Task ID: 2cb18e3e-8781-403c-9c28-8aa9f4824349