| | import {useCallback, useEffect, useRef, useState} from 'react'; |
| | import { |
| | Canvas, |
| | createPortal, |
| | extend, |
| | useFrame, |
| | useThree, |
| | } from '@react-three/fiber'; |
| | import ThreeMeshUI from 'three-mesh-ui'; |
| |
|
| | import {ARButton, XR, Hands, XREvent} from '@react-three/xr'; |
| |
|
| | import {TextGeometry} from 'three/examples/jsm/geometries/TextGeometry.js'; |
| | import {TranslationSentences} from '../types/StreamingTypes'; |
| | import Button from './Button'; |
| | import {RoomState} from '../types/RoomState'; |
| | import ThreeMeshUIText, {ThreeMeshUITextType} from './ThreeMeshUIText'; |
| | import {BLACK, WHITE} from './Colors'; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | import robotoFontFamilyJson from '../assets/RobotoMono-Regular-msdf.json?url'; |
| | import robotoFontTexture from '../assets/RobotoMono-Regular.png'; |
| | import {getURLParams} from '../URLParams'; |
| | import TextBlocks from './TextBlocks'; |
| | import {BufferedSpeechPlayer} from '../createBufferedSpeechPlayer'; |
| | import {CURSOR_BLINK_INTERVAL_MS} from '../cursorBlinkInterval'; |
| | import supportedCharSet from './supportedCharSet'; |
| |
|
| | |
| | extend(ThreeMeshUI); |
| | extend({TextGeometry}); |
| |
|
| | |
| | function CameraLinkedObject({children}) { |
| | const camera = useThree((state) => state.camera); |
| | return createPortal(<>{children}</>, camera); |
| | } |
| |
|
| | function ThreeMeshUIComponents({ |
| | translationSentences, |
| | skipARIntro, |
| | roomState, |
| | animateTextDisplay, |
| | }: XRConfigProps & {skipARIntro: boolean}) { |
| | |
| | useFrame(() => { |
| | ThreeMeshUI.update(); |
| | }); |
| | const [started, setStarted] = useState<boolean>(skipARIntro); |
| | return ( |
| | <> |
| | <CameraLinkedObject> |
| | {getURLParams().ARTranscriptionType === 'single_block' ? ( |
| | <TranscriptPanelSingleBlock |
| | started={started} |
| | animateTextDisplay={animateTextDisplay} |
| | roomState={roomState} |
| | translationSentences={translationSentences} |
| | /> |
| | ) : ( |
| | <TranscriptPanelBlocks translationSentences={translationSentences} /> |
| | )} |
| | {skipARIntro ? null : ( |
| | <IntroPanel started={started} setStarted={setStarted} /> |
| | )} |
| | </CameraLinkedObject> |
| | </> |
| | ); |
| | } |
| |
|
| | |
| | function TranscriptPanelSingleBlock({ |
| | animateTextDisplay, |
| | started, |
| | translationSentences, |
| | roomState, |
| | }: { |
| | animateTextDisplay: boolean; |
| | started: boolean; |
| | translationSentences: TranslationSentences; |
| | roomState: RoomState | null; |
| | }) { |
| | const textRef = useRef<ThreeMeshUITextType>(); |
| | const [didReceiveTranslationSentences, setDidReceiveTranslationSentences] = |
| | useState(false); |
| |
|
| | const hasActiveTranscoders = (roomState?.activeTranscoders ?? 0) > 0; |
| |
|
| | const [cursorBlinkOn, setCursorBlinkOn] = useState(false); |
| |
|
| | |
| | if (!didReceiveTranslationSentences && translationSentences.length > 0) { |
| | setDidReceiveTranslationSentences(true); |
| | } |
| |
|
| | const width = 1; |
| | const height = 0.3; |
| | const fontSize = 0.03; |
| |
|
| | useEffect(() => { |
| | if (animateTextDisplay && hasActiveTranscoders) { |
| | const interval = setInterval(() => { |
| | setCursorBlinkOn((prev) => !prev); |
| | }, CURSOR_BLINK_INTERVAL_MS); |
| |
|
| | return () => clearInterval(interval); |
| | } else { |
| | setCursorBlinkOn(false); |
| | } |
| | }, [animateTextDisplay, hasActiveTranscoders]); |
| |
|
| | useEffect(() => { |
| | if (textRef.current != null) { |
| | const initialPrompt = |
| | 'Welcome to the presentation. We are excited to share with you the work we have been doing... Our model can now translate languages in less than 2 second latency.'; |
| | |
| | const maxLines = 6; |
| | const charsPerLine = 55; |
| |
|
| | const transcriptSentences: string[] = didReceiveTranslationSentences |
| | ? translationSentences |
| | : [initialPrompt]; |
| |
|
| | |
| | |
| | const linesToDisplay = transcriptSentences.flatMap((sentence, idx) => { |
| | const blinkingCursor = |
| | cursorBlinkOn && idx === transcriptSentences.length - 1 ? '|' : ' '; |
| | const words = sentence.concat(blinkingCursor).split(/\s+/); |
| | |
| | return words.reduce( |
| | (wordChunks, currentWord) => { |
| | const filteredWord = [...currentWord] |
| | .filter((c) => { |
| | if (supportedCharSet().has(c)) { |
| | return true; |
| | } |
| | console.error( |
| | `Unsupported char ${c} - make sure this is supported in the font family msdf file`, |
| | ); |
| | return false; |
| | }) |
| | .join(''); |
| | const lastLineSoFar = wordChunks[wordChunks.length - 1]; |
| | const charCount = lastLineSoFar.length + filteredWord.length + 1; |
| | if (charCount <= charsPerLine) { |
| | wordChunks[wordChunks.length - 1] = |
| | lastLineSoFar + ' ' + filteredWord; |
| | } else { |
| | wordChunks.push(filteredWord); |
| | } |
| | return wordChunks; |
| | }, |
| | [''], |
| | ); |
| | }); |
| |
|
| | |
| | linesToDisplay.splice(0, linesToDisplay.length - maxLines); |
| | textRef.current.set({content: linesToDisplay.join('\n')}); |
| | } |
| | }, [ |
| | translationSentences, |
| | textRef, |
| | didReceiveTranslationSentences, |
| | cursorBlinkOn, |
| | ]); |
| |
|
| | const opacity = started ? 1 : 0; |
| | return ( |
| | <block |
| | args={[{padding: 0.05, backgroundOpacity: opacity}]} |
| | position={[0, -0.4, -1.3]}> |
| | <block |
| | args={[ |
| | { |
| | width, |
| | height, |
| | fontSize, |
| | textAlign: 'left', |
| | backgroundOpacity: opacity, |
| | // TODO: support more language charsets |
| | // This renders using MSDF format supported in WebGL. Renderable characters are defined in the "charset" json |
| | // Currently supports most default keyboard inputs but this would exclude many non latin charset based languages. |
| | // You can use https://msdf-bmfont.donmccurdy.com/ for easily generating these files |
| | // fontFamily: '/src/assets/Roboto-msdf.json', |
| | // fontTexture: '/src/assets/Roboto-msdf.png' |
| | fontFamily: robotoFontFamilyJson, |
| | fontTexture: robotoFontTexture, |
| | }, |
| | ]}> |
| | <ThreeMeshUIText |
| | ref={textRef} |
| | content={'Transcript'} |
| | fontOpacity={opacity} |
| | /> |
| | </block> |
| | </block> |
| | ); |
| | } |
| |
|
| | |
| | |
| | function TranscriptPanelBlocks({ |
| | translationSentences, |
| | }: { |
| | translationSentences: TranslationSentences; |
| | }) { |
| | return ( |
| | <TextBlocks |
| | translationText={'Listening...\n' + translationSentences.join('\n')} |
| | /> |
| | ); |
| | } |
| |
|
| | function IntroPanel({started, setStarted}) { |
| | const width = 0.5; |
| | const height = 0.4; |
| | const padding = 0.03; |
| |
|
| | |
| | |
| | |
| | const xCoordinate = started ? 1000000 : 0; |
| |
|
| | const commonArgs = { |
| | backgroundColor: WHITE, |
| | width, |
| | height, |
| | padding, |
| | backgroundOpacity: 1, |
| | textAlign: 'center', |
| | fontFamily: robotoFontFamilyJson, |
| | fontTexture: robotoFontTexture, |
| | }; |
| | return ( |
| | <> |
| | <block |
| | args={[ |
| | { |
| | ...commonArgs, |
| | fontSize: 0.02, |
| | }, |
| | ]} |
| | position={[xCoordinate, -0.1, -0.5]}> |
| | <ThreeMeshUIText |
| | content="FAIR Seamless Streaming Demo" |
| | fontColor={BLACK} |
| | /> |
| | </block> |
| | <block |
| | args={[ |
| | { |
| | ...commonArgs, |
| | fontSize: 0.016, |
| | backgroundOpacity: 0, |
| | }, |
| | ]} |
| | position={[xCoordinate, -0.15, -0.5001]}> |
| | <ThreeMeshUIText |
| | fontColor={BLACK} |
| | content="Welcome to the Seamless team streaming demo experience! In this demo, you would experience AI powered text and audio translation in real time." |
| | /> |
| | </block> |
| | <block |
| | args={[ |
| | { |
| | width: 0.1, |
| | height: 0.1, |
| | backgroundOpacity: 1, |
| | backgroundColor: BLACK, |
| | }, |
| | ]} |
| | position={[xCoordinate, -0.23, -0.5002]}> |
| | <Button |
| | onClick={() => setStarted(true)} |
| | content={'Start Experience'} |
| | width={0.2} |
| | height={0.035} |
| | fontSize={0.015} |
| | padding={0.01} |
| | borderRadius={0.01} |
| | /> |
| | </block> |
| | </> |
| | ); |
| | } |
| |
|
| | export type XRConfigProps = { |
| | animateTextDisplay: boolean; |
| | bufferedSpeechPlayer: BufferedSpeechPlayer; |
| | translationSentences: TranslationSentences; |
| | roomState: RoomState | null; |
| | roomID: string | null; |
| | startStreaming: () => Promise<void>; |
| | stopStreaming: () => Promise<void>; |
| | debugParam: boolean | null; |
| | onARVisible?: () => void; |
| | onARHidden?: () => void; |
| | }; |
| |
|
| | export default function XRConfig(props: XRConfigProps) { |
| | const {bufferedSpeechPlayer, debugParam} = props; |
| | const skipARIntro = getURLParams().skipARIntro; |
| | const defaultDimensions = {width: 500, height: 500}; |
| | const [dimensions, setDimensions] = useState( |
| | debugParam ? defaultDimensions : {width: 0, height: 0}, |
| | ); |
| | const {width, height} = dimensions; |
| |
|
| | |
| | |
| | const resetBuffers = useCallback( |
| | (event: XREvent<XRSessionEvent>) => { |
| | const session = event.target; |
| | if (!(session instanceof XRSession)) { |
| | return; |
| | } |
| | switch (session.visibilityState) { |
| | case 'visible': |
| | bufferedSpeechPlayer.start(); |
| | break; |
| | case 'hidden': |
| | bufferedSpeechPlayer.stop(); |
| | break; |
| | } |
| | }, |
| | [bufferedSpeechPlayer], |
| | ); |
| |
|
| | return ( |
| | <div style={{height, width, margin: '0 auto', border: '1px solid #ccc'}}> |
| | {/* This is the button that triggers AR flow if available via a button */} |
| | <ARButton |
| | onError={(e) => console.error(e)} |
| | onClick={() => setDimensions(defaultDimensions)} |
| | style={{ |
| | position: 'absolute', |
| | bottom: '24px', |
| | left: '50%', |
| | transform: 'translateX(-50%)', |
| | padding: '12px 24px', |
| | border: '1px solid white', |
| | borderRadius: '4px', |
| | backgroundColor: '#465a69', |
| | color: 'white', |
| | font: 'normal 0.8125rem sans-serif', |
| | outline: 'none', |
| | zIndex: 99999, |
| | cursor: 'pointer', |
| | }} |
| | /> |
| | {/* Canvas to draw if in browser but if in AR mode displays in pass through mode */} |
| | {/* The camera here just works in 2D mode. In AR mode it starts at at origin */} |
| | {/* <Canvas camera={{position: [0, 0, 1], fov: 60}}> */} |
| | <Canvas camera={{position: [0, 0, 0.001], fov: 60}}> |
| | <color attach="background" args={['grey']} /> |
| | <XR referenceSpace="local" onVisibilityChange={resetBuffers}> |
| | {/* |
| | Uncomment this for controllers to show up |
| | <Controllers /> |
| | */} |
| | <Hands /> |
| | |
| | {/* |
| | Uncomment this for moving with controllers |
| | <MovementController /> |
| | */} |
| | {/* |
| | Uncomment this for turning the view in non-vr mode |
| | <OrbitControls |
| | autoRotateSpeed={0.85} |
| | zoomSpeed={1} |
| | minPolarAngle={Math.PI / 2.5} |
| | maxPolarAngle={Math.PI / 2.55} |
| | /> |
| | */} |
| | <ThreeMeshUIComponents {...props} skipARIntro={skipARIntro} /> |
| | {/* Just for testing */} |
| | {/* <RandomComponents /> */} |
| | </XR> |
| | </Canvas> |
| | </div> |
| | ); |
| | } |
| | |