Spaces:
Sleeping
Sleeping
| "use client" | |
| import React, { MouseEventHandler, useEffect, useRef, useState } from "react" | |
| import { useLocalStorage } from "usehooks-ts" | |
| import { cn } from "@/lib/utils/cn" | |
| import { MediaInfo } from "@/types/general" | |
| import { useLatentEngine } from "./useLatentEngine" | |
| import { PlayPauseButton } from "../components/play-pause-button" | |
| import { StaticOrInteractiveTag } from "../../static-or-interactive-tag" | |
| import { ContentLayer } from "../components/content-layer" | |
| import { localStorageKeys } from "@/app/state/localStorageKeys" | |
| import { defaultSettings } from "@/app/state/defaultSettings" | |
| import { useStore } from "@/app/state/useStore" | |
| import { ClapProject, generateClapFromSimpleStory, serializeClap } from "@aitube/clap" | |
| import { theSimps } from "@/app/latent/samples" | |
| function LatentEngine({ | |
| media, | |
| width, | |
| height, | |
| className = "" }: { | |
| media: MediaInfo | |
| width?: number | |
| height?: number | |
| className?: string | |
| }) { | |
| // used to prevent people from opening multiple sessions at the same time | |
| // note: this should also be enforced with the Hugging Face ID | |
| const [multiTabsLock, setMultiTabsLock] = useLocalStorage<number>( | |
| "AI_TUBE_ENGINE_MULTI_TABS_LOCK", | |
| Date.now() | |
| ) | |
| const [huggingfaceApiKey, setHuggingfaceApiKey] = useLocalStorage<string>( | |
| localStorageKeys.huggingfaceApiKey, | |
| defaultSettings.huggingfaceApiKey | |
| ) | |
| // note here how we transfer the info from one store to another | |
| const jwtToken = useStore(s => s.jwtToken) | |
| const setJwtToken = useLatentEngine(s => s.setJwtToken) | |
| useEffect(() => { | |
| setJwtToken(jwtToken) | |
| }, [jwtToken]) | |
| const setContainerDimension = useLatentEngine(s => s.setContainerDimension) | |
| const isLoaded = useLatentEngine(s => s.isLoaded) | |
| const imagine = useLatentEngine(s => s.imagine) | |
| const open = useLatentEngine(s => s.open) | |
| const videoSimulationVideoPlaybackFPS = useLatentEngine(s => s.videoSimulationVideoPlaybackFPS) | |
| const videoSimulationRenderingTimeFPS = useLatentEngine(s => s.videoSimulationRenderingTimeFPS) | |
| const isLoop = useLatentEngine(s => s.isLoop) | |
| const isStatic = useLatentEngine(s => s.isStatic) | |
| const isLive = useLatentEngine(s => s.isLive) | |
| const isInteractive = useLatentEngine(s => s.isInteractive) | |
| const isPlaying = useLatentEngine(s => s.isPlaying) | |
| const togglePlayPause = useLatentEngine(s => s.togglePlayPause) | |
| const videoLayers = useLatentEngine(s => s.videoLayers) | |
| const interfaceLayers = useLatentEngine(s => s.interfaceLayers) | |
| const onClickOnSegmentationLayer = useLatentEngine(s => s.onClickOnSegmentationLayer) | |
| const stateRef = useRef({ isInitialized: false }) | |
| const [isOverlayVisible, setOverlayVisible] = useState(true) | |
| const overlayTimerRef = useRef<NodeJS.Timeout>() | |
| const videoLayerRef = useRef<HTMLDivElement>(null) | |
| const segmentationLayerRef = useRef<HTMLDivElement>(null) | |
| useEffect(() => { | |
| if (!stateRef.current.isInitialized) { | |
| stateRef.current.isInitialized = true | |
| const fn = async () => { | |
| // if we have a clapUrl (eg. from the database) then we use that | |
| // otherwise we generate it ourselves, chunk by chunk (since we're live) | |
| // TODO Julian work on the chunk mechanism | |
| const mockClap: ClapProject = generateClapFromSimpleStory({ | |
| story: theSimps, | |
| showIntroPoweredByEngine: false, | |
| showIntroDisclaimerAboutAI: false | |
| }) | |
| open(mockClap) | |
| } | |
| fn() | |
| } | |
| }, [media.id]) | |
| const isPlayingRef = useRef(isPlaying) | |
| isPlayingRef.current = isPlaying | |
| const scheduleOverlayInvisibility = () => { | |
| clearTimeout(overlayTimerRef.current) | |
| overlayTimerRef.current = setTimeout(() => { | |
| if (isPlayingRef.current) { | |
| setOverlayVisible(!isPlayingRef.current) | |
| } | |
| clearTimeout(overlayTimerRef.current) | |
| }, 3000) | |
| } | |
| /* | |
| useEffect(() => { | |
| if (isPlaying) { | |
| scheduleOverlayInvisibility() | |
| } else { | |
| clearTimeout(overlayTimerRef.current) | |
| setOverlayVisible(true) | |
| } | |
| return () => { | |
| clearTimeout(overlayTimerRef.current) | |
| } | |
| }, [isPlaying]) | |
| */ | |
| useEffect(() => { | |
| setContainerDimension({ width: width || 256, height: height || 256 }) | |
| }, [width, height]) | |
| return ( | |
| <div | |
| style={{ width, height }} | |
| className={cn(` | |
| relative | |
| flex flex-col | |
| items-center justify-between | |
| w-full h-full | |
| rounded-xl overflow-hidden | |
| bg-black | |
| `, className)}> | |
| {/* <Gsplat /> */} | |
| {/* main content container */} | |
| <ContentLayer | |
| className="pointer-events-auto" | |
| width={width} | |
| height={height} | |
| ref={videoLayerRef} | |
| onClick={onClickOnSegmentationLayer} | |
| >{videoLayers.map(({ id }) => ( | |
| <video | |
| key={id} | |
| id={id} | |
| style={{ width, height }} | |
| className={cn( | |
| `video-buffer`, | |
| `video-buffer-${id}`, | |
| )} | |
| data-segment-id="0" | |
| data-segment-start-at="0" | |
| data-z-index-depth="0" | |
| playsInline={true} | |
| muted={true} | |
| autoPlay={false} | |
| loop={true} | |
| src="/blanks/blank_1sec_512x288.webm" | |
| />))} | |
| </ContentLayer> | |
| <ContentLayer | |
| className="pointer-events-none" | |
| width={width} | |
| height={height} | |
| ref={segmentationLayerRef} | |
| ><canvas | |
| className="segmentation-canvas" | |
| style={{ width, height }} | |
| ></canvas></ContentLayer> | |
| {/* | |
| <ContentLayer | |
| className="pointer-events-auto" | |
| width={width} | |
| height={height} | |
| >{interfaceLayers.map(({ id, element }) => ( | |
| <div | |
| key={id} | |
| id={id} | |
| style={{ width, height }} | |
| className={`interface-layer-${id}`}>{element}</div>))}</ContentLayer> | |
| */} | |
| {/* content overlay, with the gradient, buttons etc */} | |
| <div className={cn(` | |
| absolute | |
| mt-0 mb-0 ml-0 mr-0 | |
| flex flex-col | |
| items-center justify-end | |
| pt-5 px-3 pb-1 | |
| transition-opacity duration-300 ease-in-out | |
| pointer-events-none | |
| `, | |
| isOverlayVisible ? "opacity-100" : "opacity-0" | |
| )} | |
| onMouseMove={() => { | |
| setOverlayVisible(true) | |
| scheduleOverlayInvisibility() | |
| }} | |
| style={{ width, height, boxShadow: "rgba(0, 0, 0, 1) 0px -77px 100px 15px inset" }}> | |
| {/* bottom slider and button bar */} | |
| <div className={cn(` | |
| flex flex-col | |
| self-end items-center justify-center | |
| w-full | |
| `)}> | |
| {/* the (optional) timeline slider bar */} | |
| <div className={cn(` | |
| flex flex-row | |
| items-center | |
| w-full h-0.5 | |
| bg-gray-100/50 | |
| `)}> | |
| <div className={cn(` | |
| flex flex-row | |
| items-center | |
| h-full | |
| `, { | |
| 'bg-yellow-500/100': isInteractive, | |
| 'bg-red-500/100': isLive, | |
| })} | |
| style={{ | |
| width: "100%" // <-- TODO: compute the % of progression within the experience | |
| }} | |
| > | |
| </div> | |
| </div> | |
| {/* button bar */} | |
| <div className={cn(` | |
| flex flex-row flex-none | |
| w-full h-14 | |
| items-center justify-between | |
| pointer-events-auto | |
| `)}> | |
| {/* left-side buttons */} | |
| <div className={cn(` | |
| flex flex-none | |
| items-center justify-center | |
| h-full | |
| `)}> | |
| <PlayPauseButton | |
| isToggledOn={isPlaying} | |
| onClick={togglePlayPause} | |
| /> | |
| <StaticOrInteractiveTag | |
| isInteractive={isInteractive} | |
| size="md" | |
| className="" | |
| /> | |
| </div> | |
| {/* right-side buttons */} | |
| <div className={cn(` | |
| flex flex-none flex-row space-x-2 | |
| items-center justify-center | |
| w-32 h-full | |
| `)}> | |
| {/* | |
| TODO: put a fullscreen button (and mode) here | |
| */} | |
| <div className="mono text-xs text-center">playback: {Math.round(videoSimulationVideoPlaybackFPS * 100) / 100} FPS</div> | |
| <div className="mono text-xs text-center">rendering: {Math.round(videoSimulationRenderingTimeFPS * 100) / 100} FPS</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| export default LatentEngine |