| 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> |
| ); |
| } |
| |