Abdullahrasheed45 commited on
Commit
56e382c
·
verified ·
1 Parent(s): 7770ae5

Create CaptioningView.tsx

Browse files
Files changed (1) hide show
  1. src/components/CaptioningView.tsx +198 -0
src/components/CaptioningView.tsx ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useEffect, useCallback } from "react";
2
+ import WebcamCapture from "./WebcamCapture";
3
+ import PromptInput from "./PromptInput";
4
+ import LiveCaption, { type HistoryEntry } from "./LiveCaption";
5
+ import { useVLMContext } from "../context/useVLMContext";
6
+ import { PROMPTS, TIMING } from "../constants";
7
+
8
+ interface CaptioningViewProps {
9
+ videoRef: React.RefObject<HTMLVideoElement | null>;
10
+ }
11
+
12
+ function useCaptioningLoop(
13
+ videoRef: React.RefObject<HTMLVideoElement | null>,
14
+ isRunning: boolean,
15
+ promptRef: React.RefObject<string>,
16
+ onCaptionUpdate: (caption: string) => void,
17
+ onError: (error: string) => void,
18
+ onGenerationComplete: (caption: string) => void,
19
+ onStatsUpdate: (stats: { tps?: number; ttft?: number }) => void,
20
+ ) {
21
+ const { isLoaded, runInference } = useVLMContext();
22
+ const abortControllerRef = useRef<AbortController | null>(null);
23
+ const onCaptionUpdateRef = useRef(onCaptionUpdate);
24
+ const onErrorRef = useRef(onError);
25
+ const onGenerationCompleteRef = useRef(onGenerationComplete);
26
+ const onStatsUpdateRef = useRef(onStatsUpdate);
27
+
28
+ useEffect(() => {
29
+ onCaptionUpdateRef.current = onCaptionUpdate;
30
+ }, [onCaptionUpdate]);
31
+
32
+ useEffect(() => {
33
+ onErrorRef.current = onError;
34
+ }, [onError]);
35
+
36
+ useEffect(() => {
37
+ onGenerationCompleteRef.current = onGenerationComplete;
38
+ }, [onGenerationComplete]);
39
+
40
+ useEffect(() => {
41
+ onStatsUpdateRef.current = onStatsUpdate;
42
+ }, [onStatsUpdate]);
43
+
44
+ useEffect(() => {
45
+ abortControllerRef.current?.abort();
46
+ if (!isRunning || !isLoaded) return;
47
+
48
+ abortControllerRef.current = new AbortController();
49
+ const signal = abortControllerRef.current.signal;
50
+ const video = videoRef.current;
51
+ const captureLoop = async () => {
52
+ while (!signal.aborted) {
53
+ if (
54
+ video &&
55
+ video.readyState >= 2 &&
56
+ !video.paused &&
57
+ video.videoWidth > 0
58
+ ) {
59
+ try {
60
+ const currentPrompt = promptRef.current || "";
61
+ const result = await runInference(
62
+ video,
63
+ currentPrompt,
64
+ onCaptionUpdateRef.current,
65
+ (stats) => onStatsUpdateRef.current(stats),
66
+ );
67
+ if (result && !signal.aborted) {
68
+ onCaptionUpdateRef.current(result);
69
+ onGenerationCompleteRef.current(result);
70
+ }
71
+ } catch (error) {
72
+ if (!signal.aborted) {
73
+ const message =
74
+ error instanceof Error ? error.message : String(error);
75
+ onErrorRef.current(message);
76
+ console.error("Error processing frame:", error);
77
+ }
78
+ }
79
+ }
80
+ if (signal.aborted) break;
81
+ await new Promise((resolve) =>
82
+ setTimeout(resolve, TIMING.FRAME_CAPTURE_DELAY),
83
+ );
84
+ }
85
+ };
86
+
87
+ // NB: Wrap with a setTimeout to ensure abort controller can run before starting the loop
88
+ // This is necessary for React's strict mode which calls effects twice in development.
89
+ setTimeout(captureLoop, 0);
90
+
91
+ return () => {
92
+ abortControllerRef.current?.abort();
93
+ };
94
+ }, [isRunning, isLoaded, runInference, promptRef, videoRef]);
95
+ }
96
+
97
+ export default function CaptioningView({ videoRef }: CaptioningViewProps) {
98
+ const { imageSize, setImageSize } = useVLMContext();
99
+ const [caption, setCaption] = useState<string>("");
100
+ const [isLoopRunning, setIsLoopRunning] = useState<boolean>(true);
101
+ const [currentPrompt, setCurrentPrompt] = useState<string>(PROMPTS.default);
102
+ const [error, setError] = useState<string | null>(null);
103
+ const [history, setHistory] = useState<HistoryEntry[]>([]);
104
+ const [stats, setStats] = useState<{ tps?: number; ttft?: number }>({});
105
+
106
+ // Use ref to store current prompt to avoid loop restarts
107
+ const promptRef = useRef<string>(currentPrompt);
108
+
109
+ // Update prompt ref when state changes
110
+ useEffect(() => {
111
+ promptRef.current = currentPrompt;
112
+ }, [currentPrompt]);
113
+
114
+ const handleCaptionUpdate = useCallback((newCaption: string) => {
115
+ setCaption(newCaption);
116
+ setError(null);
117
+ }, []);
118
+
119
+ const handleError = useCallback((errorMessage: string) => {
120
+ setError(errorMessage);
121
+ setCaption(`Error: ${errorMessage}`);
122
+ }, []);
123
+
124
+ const handleGenerationComplete = useCallback((text: string) => {
125
+ const now = new Date();
126
+ const timeString = now.toLocaleTimeString("en-US", {
127
+ hour12: false,
128
+ hour: "2-digit",
129
+ minute: "2-digit",
130
+ second: "2-digit",
131
+ });
132
+
133
+ setHistory((prev) =>
134
+ [
135
+ {
136
+ timestamp: timeString,
137
+ text: text,
138
+ },
139
+ ...prev,
140
+ ].slice(0, 50),
141
+ );
142
+ }, []);
143
+
144
+ const handleStatsUpdate = useCallback(
145
+ (newStats: { tps?: number; ttft?: number }) => {
146
+ setStats((prev) => ({ ...prev, ...newStats }));
147
+ },
148
+ [],
149
+ );
150
+
151
+ useCaptioningLoop(
152
+ videoRef,
153
+ isLoopRunning,
154
+ promptRef,
155
+ handleCaptionUpdate,
156
+ handleError,
157
+ handleGenerationComplete,
158
+ handleStatsUpdate,
159
+ );
160
+
161
+ const handlePromptChange = useCallback((prompt: string) => {
162
+ setCurrentPrompt(prompt);
163
+ setError(null);
164
+ }, []);
165
+
166
+ const handleToggleLoop = useCallback(() => {
167
+ setIsLoopRunning((prev) => !prev);
168
+ if (error) setError(null);
169
+ }, [error]);
170
+
171
+ return (
172
+ <div className="absolute inset-0 text-white">
173
+ <div className="relative w-full h-full">
174
+ <WebcamCapture
175
+ isRunning={isLoopRunning}
176
+ onToggleRunning={handleToggleLoop}
177
+ error={error}
178
+ imageSize={imageSize}
179
+ onImageSizeChange={setImageSize}
180
+ />
181
+ {/* Prompt Input - Bottom Left */}
182
+ <div className="absolute bottom-5 left-5 z-30 w-[540px]">
183
+ <PromptInput onPromptChange={handlePromptChange} />
184
+ </div>
185
+ {/* Live Caption - Bottom Right */}
186
+ <div className="absolute bottom-5 right-5 z-30 w-[720px]">
187
+ <LiveCaption
188
+ caption={caption}
189
+ isRunning={isLoopRunning}
190
+ error={error}
191
+ history={history}
192
+ stats={stats}
193
+ />
194
+ </div>
195
+ </div>
196
+ </div>
197
+ );
198
+ }