From 5295001a54cc2d191159b00ce51c623f980ffd66 Mon Sep 17 00:00:00 2001 From: gamvo74 Date: Wed, 25 Feb 2026 11:37:43 -0500 Subject: [PATCH] Integrate actual S3 upload and OpenAI Whisper transcription in TranscribePage --- apps/web/app/transcribe/page.tsx | 325 ++++++++++++++++++++++--------- 1 file changed, 237 insertions(+), 88 deletions(-) diff --git a/apps/web/app/transcribe/page.tsx b/apps/web/app/transcribe/page.tsx index d3c27d5..d10cd7c 100644 --- a/apps/web/app/transcribe/page.tsx +++ b/apps/web/app/transcribe/page.tsx @@ -15,35 +15,46 @@ import { FileText, Upload, X, - FileAudio + FileAudio, + Bot // For speaker identification (if available in transcription) } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import { AxiosError } from 'axios'; // Import AxiosError for type checking -// Mock transcription data for demonstration -const mockTranscript = [ - { id: 1, start: 0, end: 5, speaker: "Speaker 1", text: "Welcome to the evidentiary hearing for Barden versus State Farm." }, - { id: 2, start: 5, end: 12, speaker: "Speaker 2", text: "Thank you, Your Honor. We are here today to discuss the lack of personal jurisdiction as outlined in our recent motion." }, - { id: 3, start: 12, end: 18, speaker: "Speaker 1", text: "Proceed. Please state your primary grounds for this challenge." }, - { id: 4, start: 18, end: 25, speaker: "Speaker 2", text: "The defendant has no minimum contacts with the state of North Carolina, as required by the long-arm statute." }, - { id: 5, start: 25, end: 32, speaker: "Speaker 3", text: "Objection, Your Honor. The defendant has maintained an office in Charlotte for over five years." }, - { id: 6, start: 32, end: 40, speaker: "Speaker 1", text: "Overruled. I will allow the defense to finish their opening statement before hearing your rebuttal." }, -]; +// Transcription Segment Type +interface TranscriptionSegment { + id: number; + start: number; + end: number; + speaker: string; + text: string; +} export default function TranscribePage() { + const router = useRouter(); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); - const [duration, setDuration] = useState(40); + const [mediaDuration, setMediaDuration] = useState(0); const [searchQuery, setSearchQuery] = useState(""); const [playbackSpeed, setPlaybackSpeed] = useState(1); const [isUploading, setIsUploading] = useState(false); - const [uploadProgress, setUploadProgress] = useState(0); + const [uploadProgress, setUploadProgress] = useState(0); // Will be updated by polling const [selectedFile, setSelectedFile] = useState(null); - + const [transcription, setTranscription] = useState([]); + const [documentId, setDocumentId] = useState(null); + const [mediaBlobUrl, setMediaBlobUrl] = useState(null); // For uploaded media preview + const [transcriptionStatus, setTranscriptionStatus] = useState('PENDING'); + const audioRef = useRef(null); const transcriptRef = useRef(null); const activeLineRef = useRef(null); const fileInputRef = useRef(null); - // Auto-scroll logic + // Hardcoded matterId for now - MUST be replaced with dynamic value (e.g., from URL or user context) + // For initial testing, ensure you have a matter created in your DB for the user. + const MATTER_ID = 'your_matter_id_here'; + + // --- Auto-scroll and media player controls --- useEffect(() => { if (activeLineRef.current && transcriptRef.current) { activeLineRef.current.scrollIntoView({ @@ -53,30 +64,6 @@ export default function TranscribePage() { } }, [currentTime]); - const handleUpload = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - setSelectedFile(file); - simulateUpload(); - } - }; - - const simulateUpload = () => { - setIsUploading(true); - let progress = 0; - const interval = setInterval(() => { - progress += 10; - setUploadProgress(progress); - if (progress >= 100) { - clearInterval(interval); - setTimeout(() => { - setIsUploading(false); - setUploadProgress(0); - }, 500); - } - }, 200); - }; - const formatTime = (seconds: number) => { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); @@ -87,6 +74,7 @@ export default function TranscribePage() { const handleTimeUpdate = () => { if (audioRef.current) { setCurrentTime(audioRef.current.currentTime); + setMediaDuration(audioRef.current.duration); } }; @@ -97,6 +85,134 @@ export default function TranscribePage() { } }; + // --- Real Upload and Transcription Polling Logic --- + const getAuthToken = () => { + // TODO: Implement actual JWT token retrieval (e.g., from localStorage, context, or auth hook) + // For now, return a placeholder. This MUST be replaced with a real token. + return 'YOUR_JWT_TOKEN_HERE'; + }; + + const handleUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file) return; + + setSelectedFile(file); + setMediaBlobUrl(URL.createObjectURL(file)); // Create URL for local preview + setIsUploading(true); + setUploadProgress(0); + setTranscription([]); + setTranscriptionStatus('PENDING'); + + const formData = new FormData(); + formData.append('file', file); + + const token = getAuthToken(); + if (token === 'YOUR_JWT_TOKEN_HERE') { + alert('Please replace YOUR_JWT_TOKEN_HERE with a real token in transcribe/page.tsx and MATTER_ID with a valid matter.'); + setIsUploading(false); + return; + } + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/documents/${MATTER_ID}/upload`, { + method: 'POST', + body: formData, + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || 'File upload failed'); + } + + const data = await response.json(); + setDocumentId(data.id); // Set the document ID to start polling for transcription + // No longer simulating progress, actual progress will come from polling. + + } catch (error: any) { + setIsUploading(false); + console.error('Upload error:', error); + alert(`Upload failed: ${error.message}`); + } + }; + + // Polling for Transcription Results + useEffect(() => { + let pollingInterval: NodeJS.Timeout; + + const pollTranscription = async () => { + if (!documentId) return; + + const token = getAuthToken(); + if (token === 'YOUR_JWT_TOKEN_HERE') return; + + try { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/documents/${documentId}/transcription`, { + headers: { + 'Authorization': `Bearer ${token}`, + }, + }); + const data = await response.json(); + + setTranscriptionStatus(data.transcriptionStatus); + // Simulate progress based on status (real backend would send progress %) + if (data.transcriptionStatus === 'PROCESSING') { + setUploadProgress(50); + } else if (data.transcriptionStatus === 'COMPLETED') { + setUploadProgress(100); + } + + if (data.transcriptionStatus === 'COMPLETED' && data.transcriptionText) { + const parsedTranscription = JSON.parse(data.transcriptionText); + // Assuming parsedTranscription.segments contains what we need from OpenAI verbose_json + setTranscription(parsedTranscription.segments.map((s: any, index: number) => ({ + id: index, // Use index if OpenAI doesn't provide segment ID directly + start: s.start, + end: s.end, + speaker: s.speaker || `Speaker ${s.id || 0}`, // Whisper doesn't do speaker diarization by default, might need external lib + text: s.text.trim(), + }))); + setMediaDuration(parsedTranscription.duration || audioRef.current?.duration || 0); + setIsUploading(false); // Stop progress once transcription is done + clearInterval(pollingInterval); + } else if (data.transcriptionStatus === 'FAILED') { + setIsUploading(false); + alert('Transcription failed.'); + clearInterval(pollingInterval); + } + } catch (error) { + console.error('Error fetching transcription:', error); + setIsUploading(false); + alert('Error fetching transcription status.'); + clearInterval(pollingInterval); + } + }; + + if (documentId && isUploading) { + pollingInterval = setInterval(pollTranscription, 3000); // Poll every 3 seconds + } + + return () => clearInterval(pollingInterval); // Cleanup interval + }, [documentId, isUploading]); + + const displayStatusMessage = () => { + switch (transcriptionStatus) { + case 'PENDING': + return 'Waiting to start transcription...'; + case 'PROCESSING': + return 'Transcribing in progress...'; + case 'COMPLETED': + return 'Transcription complete!'; + case 'FAILED': + return 'Transcription failed!'; + default: + return 'Unknown status'; + } + }; + + return (
@@ -136,15 +252,17 @@ export default function TranscribePage() {
- {isUploading && ( + {isUploading && (transcriptionStatus !== 'COMPLETED') && (
-

Processing: {selectedFile?.name}

-

{uploadProgress}%

+

+ {selectedFile ? `Uploading & Processing: ${selectedFile.name}` : 'Processing file...'} +

+

{transcriptionStatus === 'COMPLETED' ? '100%' : uploadProgress > 0 ? `${uploadProgress}%` : ''}

+

{displayStatusMessage()}

-
@@ -163,12 +282,26 @@ export default function TranscribePage() { {/* Left: Media Player & Controls */}
- {selectedFile ? ( - + {mediaBlobUrl ? ( + ) : ( - + <> + +

Upload audio/video to begin

+ )} -

{selectedFile ? selectedFile.name : 'Evidentiary_Hearing_022426.mp3'}

+

{selectedFile ? selectedFile.name : 'No file selected'}

Matter: Barden v. State Farm

{/* Minimalist Player UI Overlay */} @@ -176,11 +309,11 @@ export default function TranscribePage() {
{ const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; - seek((x / rect.width) * duration); + seek((x / rect.width) * mediaDuration); }}>
@@ -189,8 +322,14 @@ export default function TranscribePage() { seek(currentTime - 5)} /> seek(currentTime + 5)} />
- {formatTime(duration)} + {formatTime(mediaDuration)}
@@ -209,7 +348,10 @@ export default function TranscribePage() { Playback Speed