vumichien's picture
Update src/App.js
771c22f
raw
history blame
13.3 kB
import React, { Suspense, useEffect, useRef, useState, useMemo } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { useGLTF, useTexture, Loader, Environment, useFBX, useAnimations, OrthographicCamera } from '@react-three/drei';
import { MeshStandardMaterial } from 'three/src/materials/MeshStandardMaterial';
import { LinearEncoding, sRGBEncoding } from 'three/src/constants';
import { LineBasicMaterial, MeshPhysicalMaterial, Vector2 } from 'three';
import ReactAudioPlayer from 'react-audio-player';
import createAnimation from './converter';
import blinkData from './blendDataBlink.json';
import * as THREE from 'three';
import axios from 'axios';
const _ = require('lodash');
const host = 'https://detomo-ai-avatar-backend.hf.space'
function Avatar({ avatar_url, speak, chat, setSpeak, text, language, setAudioSource, playing }) {
let gltf = useGLTF(avatar_url);
let morphTargetDictionaryBody = null;
let morphTargetDictionaryLowerTeeth = null;
const [
bodyTexture,
eyesTexture,
teethTexture,
bodySpecularTexture,
bodyRoughnessTexture,
bodyNormalTexture,
teethNormalTexture,
// teethSpecularTexture,
hairTexture,
tshirtDiffuseTexture,
tshirtNormalTexture,
tshirtRoughnessTexture,
hairAlphaTexture,
hairNormalTexture,
hairRoughnessTexture,
] = useTexture([
"/images/body.webp",
"/images/eyes.webp",
"/images/teeth_diffuse.webp",
"/images/body_specular.webp",
"/images/body_roughness.webp",
"/images/body_normal.webp",
"/images/teeth_normal.webp",
// "/images/teeth_specular.webp",
"/images/h_color.webp",
"/images/tshirt_diffuse.webp",
"/images/tshirt_normal.webp",
"/images/tshirt_roughness.webp",
"/images/h_alpha.webp",
"/images/h_normal.webp",
"/images/h_roughness.webp",
]);
_.each([
bodyTexture,
eyesTexture,
teethTexture,
teethNormalTexture,
bodySpecularTexture,
bodyRoughnessTexture,
bodyNormalTexture,
tshirtDiffuseTexture,
tshirtNormalTexture,
tshirtRoughnessTexture,
hairAlphaTexture,
hairNormalTexture,
hairRoughnessTexture
], t => {
t.encoding = sRGBEncoding;
t.flipY = false;
});
bodyNormalTexture.encoding = LinearEncoding;
tshirtNormalTexture.encoding = LinearEncoding;
teethNormalTexture.encoding = LinearEncoding;
hairNormalTexture.encoding = LinearEncoding;
gltf.scene.traverse(node => {
if(node.type === 'Mesh' || node.type === 'LineSegments' || node.type === 'SkinnedMesh') {
node.castShadow = true;
node.receiveShadow = true;
node.frustumCulled = false;
if (node.name.includes("Body")) {
node.castShadow = true;
node.receiveShadow = true;
node.material = new MeshPhysicalMaterial();
node.material.map = bodyTexture;
// node.material.shininess = 60;
node.material.roughness = 1.7;
// node.material.specularMap = bodySpecularTexture;
node.material.roughnessMap = bodyRoughnessTexture;
node.material.normalMap = bodyNormalTexture;
node.material.normalScale = new Vector2(0.6, 0.6);
morphTargetDictionaryBody = node.morphTargetDictionary;
node.material.envMapIntensity = 0.8;
// node.material.visible = false;
}
if (node.name.includes("Eyes")) {
node.material = new MeshStandardMaterial();
node.material.map = eyesTexture;
// node.material.shininess = 100;
node.material.roughness = 0.1;
node.material.envMapIntensity = 0.5;
}
if (node.name.includes("Brows")) {
node.material = new LineBasicMaterial({color: 0x000000});
node.material.linewidth = 1;
node.material.opacity = 0.5;
node.material.transparent = true;
node.visible = false;
}
if (node.name.includes("Teeth")) {
node.receiveShadow = true;
node.castShadow = true;
node.material = new MeshStandardMaterial();
node.material.roughness = 0.1;
node.material.map = teethTexture;
node.material.normalMap = teethNormalTexture;
node.material.envMapIntensity = 0.7;
}
if (node.name.includes("Hair")) {
node.material = new MeshStandardMaterial();
node.material.map = hairTexture;
node.material.alphaMap = hairAlphaTexture;
node.material.normalMap = hairNormalTexture;
node.material.roughnessMap = hairRoughnessTexture;
node.material.transparent = true;
node.material.depthWrite = false;
node.material.side = 2;
node.material.color.setHex(0x000000);
node.material.envMapIntensity = 0.3;
}
if (node.name.includes("TSHIRT")) {
node.material = new MeshStandardMaterial();
node.material.map = tshirtDiffuseTexture;
node.material.roughnessMap = tshirtRoughnessTexture;
node.material.normalMap = tshirtNormalTexture;
node.material.color.setHex(0xffffff);
node.material.envMapIntensity = 0.5;
}
if (node.name.includes("TeethLower")) {
morphTargetDictionaryLowerTeeth = node.morphTargetDictionary;
}
}
});
const [clips, setClips] = useState([]);
const mixer = useMemo(() => new THREE.AnimationMixer(gltf.scene), []);
useEffect(() => {
if (speak === false)
return;
if (chat === true) {
makeChat(text)
.then(response => {
// Xử lý dữ liệu từ OpenAI API tại đây, ví dụ:
let returnedText = response.data.response;
// Gửi dữ liệu này tới backend:
makeSpeech(returnedText, language)
.then(backendResponse => {
let {blendData, filename} = backendResponse.data;
let newClips = [
createAnimation(blendData, morphTargetDictionaryBody, 'HG_Body'),
createAnimation(blendData, morphTargetDictionaryLowerTeeth, 'HG_TeethLower')
];
filename = host + filename;
setClips(newClips);
setAudioSource(filename);
});
})
.catch(err => {
console.error(err);
setSpeak(false);
})
} else {
makeSpeech(text, language)
.then(response => {
let {blendData, filename} = response.data;
let newClips = [
createAnimation(blendData, morphTargetDictionaryBody, 'HG_Body'),
createAnimation(blendData, morphTargetDictionaryLowerTeeth, 'HG_TeethLower')
];
filename = host + filename;
setClips(newClips);
setAudioSource(filename);
})
.catch(err => {
console.error(err);
setSpeak(false);
})
}
}, [speak]);
let idleFbx = useFBX('/idle.fbx');
let { clips: idleClips } = useAnimations(idleFbx.animations);
idleClips[0].tracks = _.filter(idleClips[0].tracks, track => {
return track.name.includes("Head") || track.name.includes("Neck") || track.name.includes("Spine2");
});
idleClips[0].tracks = _.map(idleClips[0].tracks, track => {
if (track.name.includes("Head")) {
track.name = "head.quaternion";
}
if (track.name.includes("Neck")) {
track.name = "neck.quaternion";
}
if (track.name.includes("Spine")) {
track.name = "spine2.quaternion";
}
return track;
});
useEffect(() => {
let idleClipAction = mixer.clipAction(idleClips[0]);
idleClipAction.play();
let blinkClip = createAnimation(blinkData, morphTargetDictionaryBody, 'HG_Body');
let blinkAction = mixer.clipAction(blinkClip);
blinkAction.play();
}, []);
// Play animation clips when available
useEffect(() => {
if (playing === false)
return;
_.each(clips, clip => {
let clipAction = mixer.clipAction(clip);
clipAction.setLoop(THREE.LoopOnce);
clipAction.play();
});
}, [playing]);
useFrame((state, delta) => {
mixer.update(delta);
});
return (
<group name="avatar">
<primitive object={gltf.scene} dispose={null} />
</group>
);
}
function makeSpeech(text, language) {
return axios.post(host + '/talk', { text, language });
}
function makeChat(text) {
return axios.post(host + '/chat', { text });
}
function ToggleSwitch({ checked, onChange }) {
return (
<div
onClick={onChange}
style={{
...STYLES.speak,
width: '60px',
height: '30px',
borderRadius: '15px',
padding: 0,
position: 'relative',
backgroundColor: checked ? '#555' : '#222',
transition: 'background-color 0.3s'
}}
>
<div
style={{
width: '28px',
height: '28px',
borderRadius: '50%',
backgroundColor: '#FFF',
position: 'absolute',
top: '1px',
left: checked ? '32px' : '1px',
transition: 'left 0.3s'
}}
/>
</div>
);
}
const STYLES = {
area: {position: 'absolute', bottom:'10px', left: '30%', zIndex: 500, display: 'flex', flexDirection: 'column', alignItems: 'center'},
text: {margin: '0px', width:'700px', padding: '5px', background: 'none', color: '#000000', fontSize: '1.6em', border: 'none'},
speak: {padding: '10px', display: 'inline-block', color: '#FFFFFF', background: '#222222', border: 'None', marginLeft: '10px'},
area2: {position: 'absolute', top:'5px', right: '15px', zIndex: 500},
label: {color: '#777777', fontSize:'1.2em'},
languageSelect: {border: 'none', padding: '10px', backgroundColor: '#222222', color: '#fff', display: 'inline-block'},
buttonContainer: {display: 'flex', justifyContent: 'center', gap: '10px'}
}
function App() {
const audioPlayer = useRef();
const [speak, setSpeak] = useState(false);
const [text, setText] = useState("My name is Arwen. I'm a virtual human who can speak whatever you type here along with realistic facial movements.");
const [audioSource, setAudioSource] = useState(null);
const [playing, setPlaying] = useState(false);
const [language, setLanguage] = useState('en-US');
const [loop, setLoop] = useState(false);
const [chat, setChat] = useState(false);
// End of play
function playerEnded(e) {
setAudioSource(null);
setSpeak(false);
setPlaying(false);
if (loop) {
setTimeout(() => {
setSpeak(true); // Khởi động lại yêu cầu đến backend sau 5s
}, 5000);
}
}
// Player is read
function playerReady(e) {
audioPlayer.current.audioEl.current.play();
setPlaying(true);
}
return (
<div className="full">
<div style={STYLES.area}>
<textarea rows={4} type="text" style={STYLES.text} value={text} onChange={(e) => setText(e.target.value.substring(0, 500))} />
<div style={STYLES.buttonContainer}>
<button onClick={() => setSpeak(true)} style={STYLES.speak}> { speak? 'Running...': 'Generate' }</button>
<label>
<select style={STYLES.languageSelect} value={language} onChange={e => setLanguage(e.target.value)}>
<option value="en-US">English (US)</option>
<option value="ja-JP">Japanese</option>
</select>
</label>
<ToggleSwitch checked={!loop} onChange={() => setLoop(!loop)} />
<label style={{...STYLES.label, marginLeft: '10px'}}>
Loop
</label>
<ToggleSwitch checked={!chat} onChange={() => setChat(!chat)} />
<label style={{...STYLES.label, marginLeft: '10px'}}>
Chat
</label>
</div>
</div>
<ReactAudioPlayer
src={audioSource}
ref={audioPlayer}
onEnded={playerEnded}
onCanPlayThrough={playerReady}
/>
{/* <Stats /> */}
<Canvas dpr={2} onCreated={(ctx) => {
ctx.gl.physicallyCorrectLights = true;
}}>
<OrthographicCamera
makeDefault
zoom={2000}
position={[0, 1.65, 1]}
/>
{/* <OrbitControls
target={[0, 1.65, 0]}
/> */}
<Suspense fallback={null}>
<Environment background={false} files="/images/photo_studio_loft_hall_1k.hdr" />
</Suspense>
<Suspense fallback={null}>
<Bg />
</Suspense>
<Suspense fallback={null}>
<Avatar
avatar_url="/model.glb"
speak={speak}
chat={chat}
setSpeak={setSpeak}
text={text}
language={language}
setAudioSource={setAudioSource}
playing={playing}
/>
</Suspense>
</Canvas>
<Loader dataInterpolation={(p) => `Loading... please wait`} />
</div>
)
}
function Bg() {
const texture = useTexture('/images/bg.webp');
return(
<mesh position={[0, 1.5, -2]} scale={[0.8, 0.8, 0.8]}>
<planeBufferGeometry />
<meshBasicMaterial map={texture} />
</mesh>
)
}
export default App;