avatar-tts-lipsync / index.html
fernandopruebas's picture
Upload 4 files
d973e38 verified
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8" />
<title>Avatar 3D + TTS + Lipsync (ESM Local)</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
html,body{margin:0;height:100%;background:#0b0d12;color:#e8e8e8;font-family:system-ui,Arial}
#canvas-wrap{position:fixed;inset:0}
#status{
position:fixed;top:10px;left:10px;z-index:99;
background:rgba(0,0,0,.6);border:1px solid #2a2f3a;border-radius:8px;
padding:10px 12px;font-size:12px;max-width:60vw;white-space:pre-wrap
}
</style>
</head>
<body>
<div id="canvas-wrap"></div>
<div id="status">⏳ Cargando módulos locales…</div>
<script type="module">
import * as THREE from "./libs/three.module.js";
import { OrbitControls } from "./libs/OrbitControls.js";
import { GLTFLoader } from "./libs/GLTFLoader.js";
const S = document.getElementById('status');
const log = (...a)=>{ S.textContent += "\n" + a.join(' ') };
const set = (t)=>{ S.textContent = t };
set("✅ Módulos importados (desde ./libs)");
const wrap = document.getElementById('canvas-wrap');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b0d12);
const camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.1, 200);
camera.position.set(0,1.5,3);
const renderer = new THREE.WebGLRenderer({ antialias:true });
renderer.setSize(innerWidth, innerHeight);
wrap.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(0,1,0); controls.update();
scene.add(new THREE.HemisphereLight(0xffffff,0x222233,1));
const dir = new THREE.DirectionalLight(0xffffff,1);
dir.position.set(3,5,3); scene.add(dir);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(0.8,0.8,0.8),
new THREE.MeshStandardMaterial({ color:0x44aa88 })
);
cube.position.set(-1,1,0); scene.add(cube);
addEventListener("resize", ()=>{
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
function animate(){
requestAnimationFrame(animate);
cube.rotation.y += 0.01; cube.rotation.x += 0.005;
renderer.render(scene, camera);
}
animate();
function fitToView(object3D){
const box = new THREE.Box3().setFromObject(object3D);
if (box.isEmpty()) return;
const size = new THREE.Vector3(); box.getSize(size);
const center = new THREE.Vector3(); box.getCenter(center);
object3D.position.sub(center);
const maxDim = Math.max(size.x,size.y,size.z);
const dist = (maxDim/2) / Math.tan(THREE.MathUtils.degToRad(camera.fov/2)) * 1.4;
camera.position.set(0, Math.max(0.8, maxDim*0.25), dist);
controls.target.set(0,0.6,0); controls.update();
}
const loader = new GLTFLoader();
set(S.textContent + "\n⏳ Cargando persona.glb…");
loader.load("persona.glb",(gltf)=>{
const obj=gltf.scene; obj.scale.set(1,1,1);
scene.add(obj); fitToView(obj);
log("✅ persona.glb cargado");
},undefined,(err)=>{
log("⚠️ persona.glb falló → usando casco fallback");
loader.load("https://rawcdn.githack.com/KhronosGroup/glTF-Sample-Models/master/2.0/DamagedHelmet/glTF-Binary/DamagedHelmet.glb",
(gltf)=>{
scene.add(gltf.scene); fitToView(gltf.scene);
log("✅ Fallback cargado");
}
);
});
</script>
</body>
</html>