diff --git a/README.md b/README.md index 0638a66..38baace 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,14 @@ Virtual try-on lets shoppers see how clothing looks on them in real time, using just a webcam. Decart's `lucy_2_rt` model takes a live camera feed and a garment reference image, then streams back video of the person wearing that garment - all through a WebRTC connection with no server-side rendering. This repo provides drop-in examples so you can add try-on to your own app in minutes. -Three production-ready Next.js examples that show how to integrate Decart's realtime virtual try-on. Each example is self-contained and runs independently. +Four production-ready Next.js examples that show how to integrate Decart's realtime virtual try-on. Each example is self-contained and runs independently. | Example | Use case | Integration style | |---------|----------|-------------------| | [**E-commerce**](examples/ecommerce/) | "Try it on" button on product pages | Simple - hardcoded prompts, modal overlay | | [**Standalone**](examples/standalone/) | Dedicated try-on experience | Advanced - AI-generated prompts via LLM | | [**Person Detection**](examples/person-detection/) | Kiosks and unattended displays | Auto-connect - only uses credits when someone is in frame | +| [**Full-Featured**](examples/full-featured/) | Complete try-on experience | All features - person detection, AI prompts, file upload | --- @@ -141,7 +142,7 @@ This is a good fallback, but for the best results we still recommend writing det ### Generating prompts with an LLM -For garments where you don't have a pre-written prompt (e.g. user-uploaded images), you can use any vision LLM to auto-generate one from the garment image. The [standalone example](examples/standalone/) demonstrates this using OpenAI's GPT-4o-mini, but any vision-capable LLM works. +For garments where you don't have a pre-written prompt (e.g. user-uploaded images), you can use any vision LLM to auto-generate one from the garment image. The [standalone example](examples/standalone/) demonstrates this using OpenAI's GPT-4o-mini, but any vision-capable LLM works (Claude, Gemini, etc.). A built-in Decart solution is coming soon. The example sends both the garment image and a camera frame of the person to `/api/enhance-prompt`: @@ -201,7 +202,7 @@ Your permanent `DECART_API_KEY` never leaves the server. The browser receives a | Variable | Required | Purpose | |----------|----------|---------| | `DECART_API_KEY` | Yes | Creates client tokens for realtime WebRTC connections | -| `OPENAI_API_KEY` | Standalone only | Powers `/api/enhance-prompt` - auto-generates prompts from garment images | +| `OPENAI_API_KEY` | Standalone / Full-Featured | Powers `/api/enhance-prompt` - auto-generates prompts from garment images. Can be swapped for any vision-capable LLM. | --- diff --git a/examples/full-featured/.env.example b/examples/full-featured/.env.example new file mode 100644 index 0000000..d41b6c8 --- /dev/null +++ b/examples/full-featured/.env.example @@ -0,0 +1,2 @@ +DECART_API_KEY=sk_your_key_here +OPENAI_API_KEY=sk_your_openai_key_here diff --git a/examples/full-featured/README.md b/examples/full-featured/README.md new file mode 100644 index 0000000..d569173 --- /dev/null +++ b/examples/full-featured/README.md @@ -0,0 +1,126 @@ +# Full-Featured Virtual Try-On + +> Combines all features: person detection, AI-generated prompts, file upload, and a product catalog. The most complete integration example. + +Auto-detects when a person is in the camera frame, generates try-on prompts with GPT-4o-mini, lets users upload their own garment images, and includes a product catalog sidebar — all in one app. + + +--- + +## Quick start + +### 1. Install dependencies + +```bash +cd examples/full-featured +npm install +``` + +### 2. Set your API keys + +```bash +cp .env.example .env.local +``` + +Open `.env.local` and add both keys: + +```env +DECART_API_KEY=sk_your_key_here +OPENAI_API_KEY=sk_your_openai_key_here +``` + +> **Note:** This example requires both keys. The Decart key powers the realtime try-on, and the OpenAI key powers the prompt generation. + +### 3. Start the dev server + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000). Your camera will start and MediaPipe will begin scanning. Step in front of the camera to trigger a connection, then click a product or upload your own garment. + +--- + +## How it works + +``` +Page loads + → Camera starts automatically (getUserMedia) + → MediaPipe PoseLandmarker loads (WASM + WebGL, runs in browser) + → Every 1s: detectForVideo() checks for body landmarks + → Person detected (landmarks found) + → Fetch client token from /api/tokens + → Connect to Decart's lucy_2_rt model (WebRTC) + → User clicks a product OR uploads a garment image + → Capture a frame from the camera + → Send garment image + person frame to /api/enhance-prompt + → GPT-4o-mini generates a try-on prompt + → setImage(garment, prompt) sends the garment to the model + → AI video stream shows the user wearing the garment + → Person leaves (3 consecutive misses, ~3s) + → Disconnect from Decart (stops billing) + → Person returns → fresh token → reconnect → re-apply last garment +``` + +--- + +## Features + +| Feature | Description | +|---------|-------------| +| **Person detection** | Auto-connects when someone is in frame, disconnects when they leave (saves credits) | +| **AI-generated prompts** | Generates descriptive try-on prompts from the garment image + camera frame. This example uses GPT-4o-mini, but any vision-capable LLM works (Claude, Gemini, etc.). A built-in Decart solution is coming soon. | +| **File upload** | Users can upload their own garment images and get AI-generated prompts automatically | +| **Product catalog** | Sidebar with product grid, click to try on | +| **Editable prompts** | View and edit the generated prompt, re-apply with one click | +| **Auto-reconnect** | When a person leaves and returns, the last garment is automatically re-applied | + +--- + +## Key files + +| File | Purpose | +|------|---------| +| `app/page.tsx` | Main orchestration — detection-driven connect/disconnect with AI prompt generation | +| `components/CombinedView.tsx` | Video display with detection status badges, processing overlay, and prompt editor | +| `components/ProductSidebar.tsx` | Product grid with file upload section | +| `hooks/usePersonDetection.ts` | MediaPipe PoseLandmarker polling, exposes `personPresent` state | +| `hooks/useDecartRealtime.ts` | Decart WebRTC connection management | +| `hooks/useCamera.ts` | Camera access via getUserMedia | +| `lib/enhance-prompt.ts` | Client-side helper for calling `/api/enhance-prompt` | +| `app/api/enhance-prompt/route.ts` | Server-side GPT-4o-mini prompt generation | +| `app/api/tokens/route.ts` | Server-side Decart client token creation | + +--- + +## Customization + +### Tuning detection sensitivity + +In `hooks/usePersonDetection.ts`: + +- **`DETECTION_INTERVAL_MS`** (default: 1000) — how often to check for a person +- **`MISS_THRESHOLD`** (default: 3) — consecutive missed detections before disconnecting + +### Add your own products + +Edit `lib/products.ts`. Each product just needs a name, image path, and price — prompts are generated automatically: + +```typescript +{ + name: "Striped Polo", + image: "/products/striped-polo.jpg", + price: 45, +} +``` + +Place the garment image in `public/products/`. + +--- + +## Environment variables + +| Variable | Required | Purpose | +|----------|----------|---------| +| `DECART_API_KEY` | Yes | Creates client tokens for realtime WebRTC connections | +| `OPENAI_API_KEY` | Yes | Powers `/api/enhance-prompt` for auto-generating prompts. Can be swapped for any vision-capable LLM. | diff --git a/examples/full-featured/app/api/enhance-prompt/route.ts b/examples/full-featured/app/api/enhance-prompt/route.ts new file mode 100644 index 0000000..e5b1f87 --- /dev/null +++ b/examples/full-featured/app/api/enhance-prompt/route.ts @@ -0,0 +1,118 @@ +import { NextRequest, NextResponse } from "next/server"; + +const SYSTEM_PROMPT = `You write prompts for a virtual try-on model. You receive a reference clothing image, and optionally a camera frame showing the person. + +Follow these steps: + +Step 1 - Examine the person's camera frame (if provided): +Identify what the person is currently wearing on their upper body, lower body, head, etc. Note the specific garment (e.g. "a plain white t-shirt", "dark blue jeans", "a grey hoodie"). + +Step 2 - Examine the reference clothing image: +Describe it with material, texture, pattern, fit, and colors. Be specific (e.g. "a red plaid flannel shirt with a relaxed fit" not just "a shirt"). + +Step 3 - Choose the action: +- If the person is ALREADY WEARING something in the same slot as the reference item (e.g. they wear a t-shirt and the reference is a blouse), use SUBSTITUTE: + "Substitute the [description of current clothing] with [description of reference clothing]" + Example: "Substitute the plain white t-shirt with a red plaid flannel shirt with a relaxed fit and chest pockets" + +- If the person is NOT wearing anything in that slot (e.g. no hat, no jacket over their shirt), use ADD: + "Add [description of reference clothing] to the person's [body part]" + Example: "Add a wide-brimmed natural straw hat with a chin strap to the person's head" + +Fallback: If no person frame is provided or the relevant body part is not visible, use "the current top" for upper-body items or "the current bottoms" for lower-body items. +Example: "Substitute the current top with a navy cable-knit sweater with a crew neck" + +Keep the total prompt between 20-30 words. Include colors, textures, and patterns. Return only the final prompt, nothing else.`; + +interface ChatMessage { + type: string; + text?: string; + image_url?: { url: string; detail: string }; +} + +export async function POST(req: NextRequest) { + const apiKey = process.env.OPENAI_API_KEY; + if (!apiKey) { + return NextResponse.json( + { error: "OPENAI_API_KEY not set" }, + { status: 500 } + ); + } + + try { + const formData = await req.formData(); + const file = formData.get("image") as File | null; + if (!file) { + return NextResponse.json( + { error: "No image provided" }, + { status: 400 } + ); + } + + const buffer = await file.arrayBuffer(); + const base64 = Buffer.from(buffer).toString("base64"); + const mimeType = file.type || "image/png"; + const clothingDataUri = `data:${mimeType};base64,${base64}`; + + const userContent: ChatMessage[] = [ + { + type: "text", + text: "Generate a try-on prompt for this clothing item:", + }, + { + type: "image_url", + image_url: { url: clothingDataUri, detail: "low" }, + }, + ]; + + const personFrame = formData.get("personFrame") as File | null; + if (personFrame) { + const personBuffer = await personFrame.arrayBuffer(); + const personBase64 = Buffer.from(personBuffer).toString("base64"); + const personMime = personFrame.type || "image/jpeg"; + const personDataUri = `data:${personMime};base64,${personBase64}`; + userContent.push( + { type: "text", text: "Here is the person from the camera:" }, + { + type: "image_url", + image_url: { url: personDataUri, detail: "low" }, + } + ); + } + + const res = await fetch("https://api.openai.com/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "gpt-4o-mini", + max_tokens: 200, + messages: [ + { role: "system", content: SYSTEM_PROMPT }, + { role: "user", content: userContent }, + ], + }), + }); + + if (!res.ok) { + const err = await res.text(); + console.error("OpenAI API error:", err); + return NextResponse.json( + { error: "Failed to generate prompt" }, + { status: 500 } + ); + } + + const data = await res.json(); + const prompt = data.choices[0]?.message?.content?.trim() || ""; + return NextResponse.json({ prompt }); + } catch (error) { + console.error("Prompt generation failed:", error); + return NextResponse.json( + { error: "Prompt generation failed" }, + { status: 500 } + ); + } +} diff --git a/examples/full-featured/app/api/tokens/route.ts b/examples/full-featured/app/api/tokens/route.ts new file mode 100644 index 0000000..b433c45 --- /dev/null +++ b/examples/full-featured/app/api/tokens/route.ts @@ -0,0 +1,24 @@ +import { createDecartClient } from "@decartai/sdk"; +import { NextResponse } from "next/server"; + +export async function POST() { + const apiKey = process.env.DECART_API_KEY; + if (!apiKey) { + return NextResponse.json( + { error: "DECART_API_KEY is not set" }, + { status: 500 } + ); + } + + try { + const client = createDecartClient({ apiKey }); + const token = await client.tokens.create(); + return NextResponse.json(token); + } catch (error) { + console.error("Failed to create token:", error); + return NextResponse.json( + { error: "Failed to create token" }, + { status: 500 } + ); + } +} diff --git a/examples/full-featured/app/globals.css b/examples/full-featured/app/globals.css new file mode 100644 index 0000000..d2f6bef --- /dev/null +++ b/examples/full-featured/app/globals.css @@ -0,0 +1,9 @@ +@import "tailwindcss"; + +@theme inline { + --font-sans: system-ui, -apple-system, sans-serif; +} + +button { + cursor: pointer; +} diff --git a/examples/full-featured/app/layout.tsx b/examples/full-featured/app/layout.tsx new file mode 100644 index 0000000..270e074 --- /dev/null +++ b/examples/full-featured/app/layout.tsx @@ -0,0 +1,20 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Virtual Try-On - Full Featured Example", + description: + "Combines person detection, AI-generated prompts, and file upload", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} diff --git a/examples/full-featured/app/page.tsx b/examples/full-featured/app/page.tsx new file mode 100644 index 0000000..9b18b61 --- /dev/null +++ b/examples/full-featured/app/page.tsx @@ -0,0 +1,225 @@ +"use client"; + +import { useState, useEffect, useCallback, useRef } from "react"; +import { Product } from "@/lib/products"; +import { useCamera } from "@/hooks/useCamera"; +import { useDecartRealtime } from "@/hooks/useDecartRealtime"; +import { usePersonDetection } from "@/hooks/usePersonDetection"; +import { urlToImageBlob, resizeImageBlob } from "@/lib/image-utils"; +import { enhancePrompt } from "@/lib/enhance-prompt"; +import { ProductSidebar } from "@/components/ProductSidebar"; +import { CombinedView } from "@/components/CombinedView"; + +export default function FullFeaturedPage() { + const [activeProduct, setActiveProduct] = useState(null); + const [prompt, setPrompt] = useState(""); + const [processingStatus, setProcessingStatus] = useState(null); + const [uploadedImage, setUploadedImage] = useState(null); + const [isUploadActive, setIsUploadActive] = useState(false); + + const { stream, startCamera, stopCamera } = useCamera(); + const { status, error, connect, disconnect, clientRef } = + useDecartRealtime(); + + const remoteVideoRef = useRef>(null); + const [localVideoElement, setLocalVideoElement] = + useState(null); + const localVideoRef = useRef(null); + const garmentBlobRef = useRef(null); + const lastPromptRef = useRef(""); + const connectingRef = useRef(false); + const uploadedImageRef = useRef(null); + + const { personPresent, isReady: detectionReady } = + usePersonDetection(localVideoElement); + + const handleRemoteStreamRef = useCallback( + (ref: React.RefObject) => { + remoteVideoRef.current = ref; + }, + [] + ); + + const handleLocalVideoRef = useCallback( + (ref: React.RefObject) => { + setLocalVideoElement(ref.current); + localVideoRef.current = ref.current; + }, + [] + ); + + // Start camera on mount + useEffect(() => { + startCamera(); + return () => { + stopCamera(); + }; + }, [startCamera, stopCamera]); + + // Connect/disconnect based on person detection + useEffect(() => { + if (!detectionReady || !stream) return; + + if (personPresent && !clientRef.current && !connectingRef.current) { + connectingRef.current = true; + + (async () => { + try { + const res = await fetch("/api/tokens", { method: "POST" }); + if (!res.ok) throw new Error(`Token fetch failed: ${res.status}`); + const { apiKey } = await res.json(); + + const rtClient = await connect({ + apiKey, + stream, + onRemoteStream: (remoteStream) => { + if (remoteVideoRef.current?.current) { + remoteVideoRef.current.current.srcObject = remoteStream; + } + }, + }); + + // Re-apply last garment if reconnecting + if (rtClient && garmentBlobRef.current && lastPromptRef.current) { + rtClient.setImage(garmentBlobRef.current, { + prompt: lastPromptRef.current, + enhance: false, + }); + } + } finally { + connectingRef.current = false; + } + })(); + } else if (!personPresent && clientRef.current && !connectingRef.current) { + disconnect(); + } + }, [personPresent, detectionReady, stream, connect, disconnect, clientRef]); + + // Cleanup on unmount + useEffect(() => { + return () => { + disconnect(); + if (uploadedImageRef.current) { + URL.revokeObjectURL(uploadedImageRef.current); + } + }; + }, [disconnect]); + + // Apply garment blob + generate prompt (shared by product select and upload) + const applyGarment = useCallback( + async (blob: Blob, label: string) => { + const resized = await resizeImageBlob(blob); + garmentBlobRef.current = resized; + + if (!clientRef.current) return; + + setProcessingStatus(`Generating try-on prompt for ${label}...`); + let generatedPrompt: string | null = null; + try { + generatedPrompt = await enhancePrompt(resized, localVideoRef.current); + } catch (err) { + console.error("Prompt generation failed:", err); + } finally { + setProcessingStatus(null); + } + + const finalPrompt = generatedPrompt || "Try on this garment"; + setPrompt(finalPrompt); + lastPromptRef.current = finalPrompt; + + if (!clientRef.current) return; + clientRef.current.setImage(resized, { + prompt: finalPrompt, + enhance: false, + }); + }, + [clientRef] + ); + + const handleSelectProduct = useCallback( + async (product: Product) => { + setActiveProduct(product); + setIsUploadActive(false); + + const blob = await urlToImageBlob(product.image); + await applyGarment(blob, product.name); + }, + [applyGarment] + ); + + const handleUploadGarment = useCallback( + async (file: File) => { + if (file.size === 0) return; + + if (uploadedImageRef.current) { + URL.revokeObjectURL(uploadedImageRef.current); + } + + const previewUrl = URL.createObjectURL(file); + uploadedImageRef.current = previewUrl; + setUploadedImage(previewUrl); + setIsUploadActive(true); + setActiveProduct(null); + + await applyGarment(file, "your upload"); + }, + [applyGarment] + ); + + const handleReactivateUpload = useCallback(() => { + if (!garmentBlobRef.current || !lastPromptRef.current) return; + setIsUploadActive(true); + setActiveProduct(null); + + if (!clientRef.current) return; + clientRef.current.setImage(garmentBlobRef.current, { + prompt: lastPromptRef.current, + enhance: false, + }); + }, [clientRef]); + + const handleClearUpload = useCallback(() => { + if (uploadedImageRef.current) { + URL.revokeObjectURL(uploadedImageRef.current); + uploadedImageRef.current = null; + } + setUploadedImage(null); + setIsUploadActive(false); + }, []); + + const handlePromptSubmit = useCallback(() => { + if (!clientRef.current || !garmentBlobRef.current) return; + lastPromptRef.current = prompt; + clientRef.current.setImage(garmentBlobRef.current, { + prompt, + enhance: false, + }); + }, [clientRef, prompt]); + + return ( +
+ + +
+ ); +} diff --git a/examples/full-featured/components/CombinedView.tsx b/examples/full-featured/components/CombinedView.tsx new file mode 100644 index 0000000..7523463 --- /dev/null +++ b/examples/full-featured/components/CombinedView.tsx @@ -0,0 +1,200 @@ +"use client"; + +import { useRef, useEffect } from "react"; +import { ConnectionStatus } from "@/hooks/useDecartRealtime"; + +interface CombinedViewProps { + localStream: MediaStream | null; + status: ConnectionStatus; + error: string | null; + personPresent: boolean; + detectionReady: boolean; + prompt: string; + processingStatus: string | null; + onPromptChange: (prompt: string) => void; + onPromptSubmit: () => void; + onRemoteStream: (ref: React.RefObject) => void; + onLocalVideo?: (ref: React.RefObject) => void; +} + +function getStatusLabel( + detectionReady: boolean, + personPresent: boolean, + status: ConnectionStatus, + processingStatus: string | null +): { text: string; className: string; pulse: boolean } { + if (!detectionReady) { + return { + text: "Loading detection...", + className: "bg-black/60 text-white", + pulse: false, + }; + } + + if (processingStatus) { + return { + text: processingStatus, + className: "bg-blue-500/90 text-white", + pulse: true, + }; + } + + if (status === "generating") { + return { + text: "Live", + className: "bg-green-500/90 text-white", + pulse: true, + }; + } + + if (status === "connecting") { + return { + text: "Person detected - connecting...", + className: "bg-blue-500/90 text-white", + pulse: true, + }; + } + + if (status === "connected") { + return { + text: "Connected - click a product to try on", + className: "bg-blue-500/90 text-white", + pulse: false, + }; + } + + if (personPresent) { + return { + text: "Person detected", + className: "bg-blue-500/90 text-white", + pulse: true, + }; + } + + return { + text: "Scanning...", + className: "bg-black/60 text-white", + pulse: false, + }; +} + +export function CombinedView({ + localStream, + status, + error, + personPresent, + detectionReady, + prompt, + processingStatus, + onPromptChange, + onPromptSubmit, + onRemoteStream, + onLocalVideo, +}: CombinedViewProps) { + const localVideoRef = useRef(null); + const remoteVideoRef = useRef(null); + + useEffect(() => { + if (localVideoRef.current && localStream) { + localVideoRef.current.srcObject = localStream; + } + }, [localStream]); + + useEffect(() => { + onRemoteStream(remoteVideoRef); + }, [onRemoteStream]); + + useEffect(() => { + onLocalVideo?.(localVideoRef); + }, [onLocalVideo]); + + const isGenerating = status === "generating"; + const label = getStatusLabel(detectionReady, personPresent, status, processingStatus); + + return ( +
+
+ {/* Local camera feed */} +
+ ); +} diff --git a/examples/full-featured/components/ProductSidebar.tsx b/examples/full-featured/components/ProductSidebar.tsx new file mode 100644 index 0000000..b4ad147 --- /dev/null +++ b/examples/full-featured/components/ProductSidebar.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useRef } from "react"; +import { PRODUCTS, Product } from "@/lib/products"; +import Image from "next/image"; + +interface ProductSidebarProps { + activeProduct: Product | null; + uploadedImage: string | null; + isUploadActive: boolean; + onSelectProduct: (product: Product) => void; + onUploadGarment: (file: File) => void; + onReactivateUpload: () => void; + onClearUpload: () => void; +} + +export function ProductSidebar({ + activeProduct, + uploadedImage, + isUploadActive, + onSelectProduct, + onUploadGarment, + onReactivateUpload, + onClearUpload, +}: ProductSidebarProps) { + const fileInputRef = useRef(null); + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + onUploadGarment(file); + e.target.value = ""; + } + }; + + return ( + + ); +} diff --git a/examples/full-featured/hooks/useCamera.ts b/examples/full-featured/hooks/useCamera.ts new file mode 100644 index 0000000..e561e07 --- /dev/null +++ b/examples/full-featured/hooks/useCamera.ts @@ -0,0 +1,36 @@ +"use client"; + +import { useState, useCallback, useRef } from "react"; + +export function useCamera() { + const [stream, setStream] = useState(null); + const [error, setError] = useState(null); + const streamRef = useRef(null); + + const startCamera = useCallback(async () => { + try { + const mediaStream = await navigator.mediaDevices.getUserMedia({ + audio: false, + video: { facingMode: "user" }, + }); + streamRef.current = mediaStream; + setStream(mediaStream); + setError(null); + return mediaStream; + } catch (err) { + const msg = err instanceof Error ? err.message : "Camera access denied"; + setError(msg); + return null; + } + }, []); + + const stopCamera = useCallback(() => { + if (streamRef.current) { + streamRef.current.getTracks().forEach((t) => t.stop()); + streamRef.current = null; + setStream(null); + } + }, []); + + return { stream, error, startCamera, stopCamera }; +} diff --git a/examples/full-featured/hooks/useDecartRealtime.ts b/examples/full-featured/hooks/useDecartRealtime.ts new file mode 100644 index 0000000..828b8de --- /dev/null +++ b/examples/full-featured/hooks/useDecartRealtime.ts @@ -0,0 +1,76 @@ +"use client"; + +import { useState, useCallback, useRef } from "react"; +import { createDecartClient, models } from "@decartai/sdk"; + +type RealtimeClient = Awaited< + ReturnType["realtime"]["connect"]> +>; + +export type ConnectionStatus = + | "idle" + | "connecting" + | "connected" + | "generating" + | "reconnecting" + | "disconnected" + | "error"; + +interface ConnectOptions { + apiKey: string; + stream: MediaStream; + prompt?: string; + onRemoteStream: (stream: MediaStream) => void; +} + +export function useDecartRealtime() { + const [status, setStatus] = useState("idle"); + const [error, setError] = useState(null); + const clientRef = useRef(null); + + const connect = useCallback(async (options: ConnectOptions) => { + const { apiKey, stream, prompt, onRemoteStream } = options; + setStatus("connecting"); + setError(null); + + try { + const client = createDecartClient({ apiKey }); + const model = models.realtime("lucy_2_rt"); + + const rtClient = await client.realtime.connect(stream, { + model, + onRemoteStream, + ...(prompt && { + initialState: { prompt: { text: prompt, enhance: false } }, + }), + }); + + rtClient.on("connectionChange", (state) => { + setStatus(state); + }); + + rtClient.on("error", (err) => { + setError(err.message); + setStatus("error"); + }); + + clientRef.current = rtClient; + return rtClient; + } catch (err) { + const msg = err instanceof Error ? err.message : "Connection failed"; + setError(msg); + setStatus("error"); + return null; + } + }, []); + + const disconnect = useCallback(() => { + if (clientRef.current) { + clientRef.current.disconnect(); + clientRef.current = null; + } + setStatus("disconnected"); + }, []); + + return { status, error, connect, disconnect, clientRef }; +} diff --git a/examples/full-featured/hooks/usePersonDetection.ts b/examples/full-featured/hooks/usePersonDetection.ts new file mode 100644 index 0000000..6edf7af --- /dev/null +++ b/examples/full-featured/hooks/usePersonDetection.ts @@ -0,0 +1,91 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { PoseLandmarker, FilesetResolver } from "@mediapipe/tasks-vision"; + +const DETECTION_INTERVAL_MS = 1000; +const MISS_THRESHOLD = 3; + +export function usePersonDetection( + videoElement: HTMLVideoElement | null +) { + const [personPresent, setPersonPresent] = useState(false); + const [isReady, setIsReady] = useState(false); + const landmarkerRef = useRef(null); + const missCountRef = useRef(0); + const intervalRef = useRef | null>(null); + + // Initialize PoseLandmarker + useEffect(() => { + let cancelled = false; + + async function init() { + const vision = await FilesetResolver.forVisionTasks( + "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm" + ); + if (cancelled) return; + + const landmarker = await PoseLandmarker.createFromOptions(vision, { + baseOptions: { + modelAssetPath: + "https://storage.googleapis.com/mediapipe-models/pose_landmarker/pose_landmarker_lite/float16/1/pose_landmarker_lite.task", + delegate: "GPU", + }, + runningMode: "VIDEO", + numPoses: 1, + }); + if (cancelled) return; + + landmarkerRef.current = landmarker; + setIsReady(true); + } + + init(); + + return () => { + cancelled = true; + landmarkerRef.current?.close(); + landmarkerRef.current = null; + }; + }, []); + + // Poll video for person detection + useEffect(() => { + if (!isReady || !videoElement) return; + + function detect() { + if ( + !landmarkerRef.current || + !videoElement || + videoElement.readyState < 2 + ) + return; + + const result = landmarkerRef.current.detectForVideo( + videoElement, + performance.now() + ); + + if (result.landmarks.length > 0) { + missCountRef.current = 0; + setPersonPresent(true); + } else { + missCountRef.current++; + if (missCountRef.current >= MISS_THRESHOLD) { + setPersonPresent(false); + } + } + } + + intervalRef.current = setInterval(detect, DETECTION_INTERVAL_MS); + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [isReady, videoElement]); + + return { personPresent, isReady }; +} diff --git a/examples/full-featured/lib/enhance-prompt.ts b/examples/full-featured/lib/enhance-prompt.ts new file mode 100644 index 0000000..002f964 --- /dev/null +++ b/examples/full-featured/lib/enhance-prompt.ts @@ -0,0 +1,26 @@ +import { captureVideoFrame } from "./image-utils"; + +/** + * Generates a try-on prompt from a garment image using GPT-4o-mini. + * Optionally captures a frame from the camera to provide context about + * what the person is currently wearing. + */ +export async function enhancePrompt( + garmentBlob: Blob, + localVideo?: HTMLVideoElement | null +): Promise { + const formData = new FormData(); + formData.append("image", garmentBlob); + + if (localVideo && localVideo.videoWidth > 0) { + const personFrame = await captureVideoFrame(localVideo); + formData.append("personFrame", personFrame); + } + + const res = await fetch("/api/enhance-prompt", { + method: "POST", + body: formData, + }); + const data = await res.json(); + return data.prompt || null; +} diff --git a/examples/full-featured/lib/image-utils.ts b/examples/full-featured/lib/image-utils.ts new file mode 100644 index 0000000..e694410 --- /dev/null +++ b/examples/full-featured/lib/image-utils.ts @@ -0,0 +1,87 @@ +export async function urlToImageBlob(url: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = img.naturalWidth || 512; + canvas.height = img.naturalHeight || 512; + const ctx = canvas.getContext("2d")!; + + ctx.fillStyle = "#FFFFFF"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + canvas.toBlob( + (blob) => { + if (blob) resolve(blob); + else reject(new Error("Failed to convert image")); + }, + "image/jpeg", + 0.9 + ); + }; + img.onerror = () => reject(new Error("Failed to load image")); + img.src = url; + }); +} + +export async function resizeImageBlob( + blob: Blob, + maxSize = 1024 +): Promise { + const img = await loadImage(blob); + const { naturalWidth: w, naturalHeight: h } = img; + if (w <= maxSize && h <= maxSize) return blob; + + const scale = maxSize / Math.max(w, h); + const canvas = document.createElement("canvas"); + canvas.width = Math.round(w * scale); + canvas.height = Math.round(h * scale); + const ctx = canvas.getContext("2d")!; + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + return new Promise((resolve, reject) => { + canvas.toBlob( + (b) => (b ? resolve(b) : reject(new Error("Failed to resize image"))), + "image/jpeg", + 0.8 + ); + }); +} + +export function captureVideoFrame( + video: HTMLVideoElement, + maxSize = 320 +): Promise { + const { videoWidth: w, videoHeight: h } = video; + const scale = maxSize / Math.max(w, h); + const canvas = document.createElement("canvas"); + canvas.width = Math.round(w * scale); + canvas.height = Math.round(h * scale); + const ctx = canvas.getContext("2d")!; + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + + return new Promise((resolve, reject) => { + canvas.toBlob( + (b) => (b ? resolve(b) : reject(new Error("Failed to capture frame"))), + "image/jpeg", + 0.7 + ); + }); +} + +export function loadImage(blob: Blob): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + URL.revokeObjectURL(img.src); + resolve(img); + }; + img.onerror = () => { + URL.revokeObjectURL(img.src); + reject(new Error("Failed to load image")); + }; + img.src = URL.createObjectURL(blob); + }); +} diff --git a/examples/full-featured/lib/products.ts b/examples/full-featured/lib/products.ts new file mode 100644 index 0000000..e8a4d64 --- /dev/null +++ b/examples/full-featured/lib/products.ts @@ -0,0 +1,58 @@ +export interface Product { + name: string; + image: string; + price: number; +} + +export const PRODUCTS: Product[] = [ + { + name: "Decart Beanie", + image: "/products/decart-beanie.png", + price: 35, + }, + { + name: "Decart Bomber Jacket", + image: "/products/decart-bomber.png", + price: 149, + }, + { + name: "Decart Cap", + image: "/products/decart-cap.png", + price: 29, + }, + { + name: "Decart Crewneck", + image: "/products/decart-crewneck.png", + price: 89, + }, + { + name: "Decart Hoodie", + image: "/products/decart-hoodie.png", + price: 99, + }, + { + name: "Decart Polo", + image: "/products/decart-polo.png", + price: 69, + }, + { + name: "Decart T-Shirt", + image: "/products/decart-tshirt.png", + price: 45, + }, + { + name: "Decart Zip-Up Hoodie", + image: "/products/decart-zipup.png", + price: 109, + }, + { + name: "Decart Rain Jacket", + image: "/products/decart-rainjacket.png", + price: 129, + }, + { + name: "Decart Blazer", + image: "/products/decart-blazer.png", + price: 199, + }, +]; diff --git a/examples/full-featured/next.config.ts b/examples/full-featured/next.config.ts new file mode 100644 index 0000000..cb651cd --- /dev/null +++ b/examples/full-featured/next.config.ts @@ -0,0 +1,5 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default nextConfig; diff --git a/examples/full-featured/package-lock.json b/examples/full-featured/package-lock.json new file mode 100644 index 0000000..7ad669f --- /dev/null +++ b/examples/full-featured/package-lock.json @@ -0,0 +1,1709 @@ +{ + "name": "decart-tryon-full-featured", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "decart-tryon-full-featured", + "version": "1.0.0", + "dependencies": { + "@decartai/sdk": "^0.0.52", + "@mediapipe/tasks-vision": "^0.10.18", + "next": "15.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@decartai/sdk": { + "version": "0.0.52", + "resolved": "https://registry.npmjs.org/@decartai/sdk/-/sdk-0.0.52.tgz", + "integrity": "sha512-fsNE5/n37H2NFPTDUSoLXSHmWwrela1YD+Z6uSDYNggtjGvUOb5TbMoEtM2mWys3c85cGaBA2ZpXoJIR1vTGgw==", + "license": "MIT", + "dependencies": { + "mitt": "^3.0.1", + "p-retry": "^6.2.1", + "zod": "^4.0.17" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mediapipe/tasks-vision": { + "version": "0.10.32", + "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.32.tgz", + "integrity": "sha512-3tiAZnmKloYnRXYoO3dKltTUGnqeCwzC4lV03uY0vCsE+aveJTyEVQyZHOlQGQNsjK+gRHzkf9q08C99Qm2K0Q==", + "license": "Apache-2.0" + }, + "node_modules/@next/env": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.0.tgz", + "integrity": "sha512-UcCO481cROsqJuszPPXJnb7GGuLq617ve4xuAyyNG4VSSocJNtMU5Fsx+Lp6mlN8c7W58aZLc5y6D/2xNmaK+w==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.0.tgz", + "integrity": "sha512-ZU8d7xxpX14uIaFC3nsr4L++5ZS/AkWDm1PzPO6gD9xWhFkOj2hzSbSIxoncsnlJXB1CbLOfGVN4Zk9tg83PUw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.0.tgz", + "integrity": "sha512-DQ3RiUoW2XC9FcSM4ffpfndq1EsLV0fj0/UY33i7eklW5akPUCo6OX2qkcLXZ3jyPdo4sf2flwAED3AAq3Om2Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.0.tgz", + "integrity": "sha512-M+vhTovRS2F//LMx9KtxbkWk627l5Q7AqXWWWrfIzNIaUFiz2/NkOFkxCFyNyGACi5YbA8aekzCLtbDyfF/v5Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.0.tgz", + "integrity": "sha512-Qn6vOuwaTCx3pNwygpSGtdIu0TfS1KiaYLYXLH5zq1scoTXdwYfdZtwvJTpB1WrLgiQE2Ne2kt8MZok3HlFqmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.0.tgz", + "integrity": "sha512-yeNh9ofMqzOZ5yTOk+2rwncBzucc6a1lyqtg8xZv0rH5znyjxHOWsoUtSq4cUTeeBIiXXX51QOOe+VoCjdXJRw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.0.tgz", + "integrity": "sha512-t9IfNkHQs/uKgPoyEtU912MG6a1j7Had37cSUyLTKx9MnUpjj+ZDKw9OyqTI9OwIIv0wmkr1pkZy+3T5pxhJPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.0.tgz", + "integrity": "sha512-WEAoHyG14t5sTavZa1c6BnOIEukll9iqFRTavqRVPfYmfegOAd5MaZfXgOGG6kGo1RduyGdTHD4+YZQSdsNZXg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.0.tgz", + "integrity": "sha512-J1YdKuJv9xcixzXR24Dv+4SaDKc2jj31IVUEMdO5xJivMTXuE6MAdIi4qPjSymHuFG8O5wbfWKnhJUcHHpj5CA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", + "jiti": "^2.6.1", + "lightningcss": "1.31.1", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" + } + }, + "node_modules/@types/node": { + "version": "20.19.33", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", + "integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT", + "optional": true + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "license": "MIT", + "optional": true + }, + "node_modules/is-network-error": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", + "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/next/-/next-15.1.0.tgz", + "integrity": "sha512-QKhzt6Y8rgLNlj30izdMbxAwjHMFANnLwDwZ+WQh5sMhyt4lEBqDK9QpvWHtIM4rINKPoJ8aiRZKg5ULSybVHw==", + "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.", + "license": "MIT", + "dependencies": { + "@next/env": "15.1.0", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.1.0", + "@next/swc-darwin-x64": "15.1.0", + "@next/swc-linux-arm64-gnu": "15.1.0", + "@next/swc-linux-arm64-musl": "15.1.0", + "@next/swc-linux-x64-gnu": "15.1.0", + "@next/swc-linux-x64-musl": "15.1.0", + "@next/swc-win32-arm64-msvc": "15.1.0", + "@next/swc-win32-x64-msvc": "15.1.0", + "sharp": "^0.33.5" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/examples/full-featured/package.json b/examples/full-featured/package.json new file mode 100644 index 0000000..d3b3c28 --- /dev/null +++ b/examples/full-featured/package.json @@ -0,0 +1,25 @@ +{ + "name": "decart-tryon-full-featured", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@decartai/sdk": "^0.0.52", + "@mediapipe/tasks-vision": "^0.10.18", + "next": "15.1.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/examples/full-featured/postcss.config.mjs b/examples/full-featured/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/examples/full-featured/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/examples/full-featured/public/products/decart-beanie.png b/examples/full-featured/public/products/decart-beanie.png new file mode 100644 index 0000000..6ce1dd3 Binary files /dev/null and b/examples/full-featured/public/products/decart-beanie.png differ diff --git a/examples/full-featured/public/products/decart-blazer.png b/examples/full-featured/public/products/decart-blazer.png new file mode 100644 index 0000000..47203e8 Binary files /dev/null and b/examples/full-featured/public/products/decart-blazer.png differ diff --git a/examples/full-featured/public/products/decart-bomber.png b/examples/full-featured/public/products/decart-bomber.png new file mode 100644 index 0000000..993a569 Binary files /dev/null and b/examples/full-featured/public/products/decart-bomber.png differ diff --git a/examples/full-featured/public/products/decart-cap.png b/examples/full-featured/public/products/decart-cap.png new file mode 100644 index 0000000..3540bb6 Binary files /dev/null and b/examples/full-featured/public/products/decart-cap.png differ diff --git a/examples/full-featured/public/products/decart-crewneck.png b/examples/full-featured/public/products/decart-crewneck.png new file mode 100644 index 0000000..74a023d Binary files /dev/null and b/examples/full-featured/public/products/decart-crewneck.png differ diff --git a/examples/full-featured/public/products/decart-hoodie.png b/examples/full-featured/public/products/decart-hoodie.png new file mode 100644 index 0000000..b5ba1f7 Binary files /dev/null and b/examples/full-featured/public/products/decart-hoodie.png differ diff --git a/examples/full-featured/public/products/decart-polo.png b/examples/full-featured/public/products/decart-polo.png new file mode 100644 index 0000000..d1537c8 Binary files /dev/null and b/examples/full-featured/public/products/decart-polo.png differ diff --git a/examples/full-featured/public/products/decart-rainjacket.png b/examples/full-featured/public/products/decart-rainjacket.png new file mode 100644 index 0000000..b2c801b Binary files /dev/null and b/examples/full-featured/public/products/decart-rainjacket.png differ diff --git a/examples/full-featured/public/products/decart-tshirt.png b/examples/full-featured/public/products/decart-tshirt.png new file mode 100644 index 0000000..bf33120 Binary files /dev/null and b/examples/full-featured/public/products/decart-tshirt.png differ diff --git a/examples/full-featured/public/products/decart-zipup.png b/examples/full-featured/public/products/decart-zipup.png new file mode 100644 index 0000000..81fd1b9 Binary files /dev/null and b/examples/full-featured/public/products/decart-zipup.png differ diff --git a/examples/full-featured/tsconfig.json b/examples/full-featured/tsconfig.json new file mode 100644 index 0000000..01b4ddb --- /dev/null +++ b/examples/full-featured/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/standalone/README.md b/examples/standalone/README.md index 1d48846..2b416ad 100644 --- a/examples/standalone/README.md +++ b/examples/standalone/README.md @@ -1,6 +1,6 @@ # Standalone Virtual Try-On (with AI-generated prompts) -> A dedicated try-on experience with AI-generated prompts. Products on the left, live camera in the center. Click a product and GPT-4o-mini generates the try-on prompt automatically from the garment image and the person's camera frame. +> A dedicated try-on experience with AI-generated prompts. Products on the left, live camera in the center. Click a product and a vision LLM generates the try-on prompt automatically from the garment image and the person's camera frame. This example uses GPT-4o-mini, but any vision-capable LLM works (Claude, Gemini, etc.). A built-in Decart solution is coming soon. Unlike the [e-commerce example](../ecommerce/) which uses hardcoded prompts, this example shows how to use the `/api/enhance-prompt` endpoint to generate prompts dynamically - useful when you don't know what garments users will upload. @@ -117,4 +117,4 @@ This example uses Next.js + Tailwind, but the core Decart integration works with | Variable | Required | Purpose | |----------|----------|---------| | `DECART_API_KEY` | Yes | Creates client tokens for realtime connections | -| `OPENAI_API_KEY` | Yes | Powers `/api/enhance-prompt` for auto-generating prompts | +| `OPENAI_API_KEY` | Yes | Powers `/api/enhance-prompt` for auto-generating prompts. Can be swapped for any vision-capable LLM. |