Spaces:
Running
Running
| import { useRef, useState, useCallback, type ReactNode } from "react"; | |
| import { | |
| AutoProcessor, | |
| Gemma4ForConditionalGeneration, | |
| TextStreamer, | |
| load_image, | |
| } from "@huggingface/transformers"; | |
| import { LLMContext, type LoadingStatus, type DetectionResult } from "./LLMContext"; | |
| import { parseJsonDetection } from "../utils/detection-parser"; | |
| const MODEL_ID = "onnx-community/gemma-4-E2B-it-ONNX"; | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| type AnyModel = any; | |
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | |
| type AnyProcessor = any; | |
| async function pickDtype(): Promise<"q4f16" | "q4"> { | |
| try { | |
| const adapter = await navigator.gpu?.requestAdapter({ | |
| powerPreference: "high-performance", | |
| }); | |
| if (adapter) { | |
| const features = [...adapter.features]; | |
| console.log("[Gemma4] WebGPU adapter features:", features); | |
| console.log("[Gemma4] WebGPU adapter limits:", adapter.limits); | |
| if (adapter.features.has("shader-f16")) { | |
| console.log("[Gemma4] shader-f16 is AVAILABLE — using q4f16 (fast)"); | |
| return "q4f16"; | |
| } | |
| console.log("[Gemma4] shader-f16 NOT available — falling back to q4 (slow)"); | |
| } | |
| } catch (err) { | |
| console.warn("[Gemma4] Adapter probe failed:", err); | |
| } | |
| return "q4"; | |
| } | |
| export function LLMProvider({ children }: { children: ReactNode }) { | |
| const modelRef = useRef<Promise<AnyModel> | null>(null); | |
| const processorRef = useRef<AnyProcessor | null>(null); | |
| const [status, setStatus] = useState<LoadingStatus>({ state: "idle" }); | |
| const [isGenerating, setIsGenerating] = useState(false); | |
| const isGeneratingRef = useRef(false); | |
| const [tps, setTps] = useState(0); | |
| const [dtype, setDtype] = useState<"q4f16" | "q4" | null>(null); | |
| const [result, setResult] = useState<DetectionResult | null>(null); | |
| const abortRef = useRef(false); | |
| isGeneratingRef.current = isGenerating; | |
| const loadModel = useCallback(() => { | |
| if (modelRef.current) return; | |
| modelRef.current = (async () => { | |
| setStatus({ state: "loading", message: "Loading processor..." }); | |
| try { | |
| const pickedDtype = await pickDtype(); | |
| setDtype(pickedDtype); | |
| console.log(`[Gemma4] Using dtype: ${pickedDtype}`); | |
| const processor = await AutoProcessor.from_pretrained(MODEL_ID); | |
| processorRef.current = processor; | |
| setStatus({ state: "loading", message: "Downloading model..." }); | |
| const model = await Gemma4ForConditionalGeneration.from_pretrained(MODEL_ID, { | |
| dtype: pickedDtype, | |
| device: "webgpu", | |
| progress_callback: (p: { status: string; progress?: number }) => { | |
| // Use aggregated progress_total event for smooth, monotonic progress. | |
| if (p.status === "progress_total" && typeof p.progress === "number") { | |
| setStatus({ | |
| state: "loading", | |
| progress: p.progress, | |
| message: "Loading model...", | |
| }); | |
| } | |
| }, | |
| }); | |
| setStatus({ state: "ready" }); | |
| return model; | |
| } catch (err) { | |
| console.error("[Gemma4] Model loading failed:", err); | |
| const msg = err instanceof Error ? err.message : String(err); | |
| setStatus({ state: "error", error: msg }); | |
| modelRef.current = null; | |
| throw err; | |
| } | |
| })(); | |
| }, []); | |
| const detect = useCallback(async (imageUrl: string, prompt: string) => { | |
| if (!modelRef.current) { | |
| console.warn("[Gemma4] detect() called before model loaded"); | |
| return; | |
| } | |
| if (isGeneratingRef.current) { | |
| console.warn("[Gemma4] detect() called while already generating"); | |
| return; | |
| } | |
| console.log("[Gemma4] Starting detection..."); | |
| setIsGenerating(true); | |
| setTps(0); | |
| setResult(null); | |
| abortRef.current = false; | |
| try { | |
| const model = await modelRef.current; | |
| const processor = processorRef.current!; | |
| // Build multimodal message | |
| const messages = [ | |
| { | |
| role: "user", | |
| content: [ | |
| { type: "image" as const }, | |
| { type: "text" as const, text: prompt }, | |
| ], | |
| }, | |
| ]; | |
| // Apply chat template | |
| const text = processor.apply_chat_template(messages, { | |
| enable_thinking: false, | |
| add_generation_prompt: true, | |
| }); | |
| // Load and process image | |
| const image = await load_image(imageUrl); | |
| const inputs = await processor(text, image, null, { | |
| add_special_tokens: false, | |
| }); | |
| // Stream generation | |
| let outputText = ""; | |
| let tokenCount = 0; | |
| let firstTokenTime = 0; | |
| const streamer = new TextStreamer(processor.tokenizer, { | |
| skip_prompt: true, | |
| skip_special_tokens: true, | |
| callback_function: (token: string) => { | |
| if (abortRef.current) return; | |
| outputText += token; | |
| const detections = parseJsonDetection(outputText); | |
| setResult({ text: outputText, detections }); | |
| }, | |
| token_callback_function: () => { | |
| tokenCount++; | |
| if (tokenCount === 1) { | |
| firstTokenTime = performance.now(); | |
| } else { | |
| const elapsed = (performance.now() - firstTokenTime) / 1000; | |
| if (elapsed > 0) { | |
| setTps(Math.round(((tokenCount - 1) / elapsed) * 10) / 10); | |
| } | |
| } | |
| }, | |
| }); | |
| await model.generate({ | |
| ...inputs, | |
| max_new_tokens: 128, | |
| do_sample: true, | |
| temperature: 1.0, | |
| top_p: 0.95, | |
| top_k: 64, | |
| streamer, | |
| }); | |
| // Final parse | |
| const detections = parseJsonDetection(outputText); | |
| setResult({ text: outputText, detections }); | |
| console.log(`[Gemma4] Detection done. ${detections.length} objects found.`); | |
| } catch (err) { | |
| console.error("[Gemma4] Detection error:", err); | |
| const rawMsg = err instanceof Error ? err.message : String(err); | |
| const isOOM = | |
| rawMsg.includes("OUT_OF_DEVICE_MEMORY") || | |
| rawMsg.includes("Invalid Buffer") || | |
| rawMsg.includes("Softmax"); | |
| const friendly = isOOM | |
| ? "GPU ran out of memory. Enable chrome://flags/#enable-unsafe-webgpu and chrome://flags/#enable-vulkan, restart Chrome, and close other GPU-heavy tabs." | |
| : rawMsg; | |
| setResult({ text: `Error: ${friendly}`, detections: [] }); | |
| } | |
| setIsGenerating(false); | |
| }, []); | |
| const stop = useCallback(() => { | |
| abortRef.current = true; | |
| }, []); | |
| return ( | |
| <LLMContext.Provider | |
| value={{ status, isGenerating, tps, dtype, result, loadModel, detect, stop }} | |
| > | |
| {children} | |
| </LLMContext.Provider> | |
| ); | |
| } | |