FOUNDRY
C8 Platform
← 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