OGB2000's picture
ajout de nouveaux dossiers
6d8ba53
Raw
History Blame Contribute Delete
20.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>iLearning SignAI — Real Avatar Demo</title>
<style>
:root{--blue:#2563eb;--cyan:#06b6d4;--muted:#64748b;--border:#e5e7eb}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Inter,Segoe UI,Arial,sans-serif;background:#f8fafc;color:#111827}
nav{height:68px;background:white;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;padding:0 34px;position:sticky;top:0;z-index:50}
.brand{display:flex;align-items:center;gap:12px}.logo{width:44px;height:44px;border-radius:13px;background:linear-gradient(135deg,var(--blue),var(--cyan));display:flex;align-items:center;justify-content:center;color:white;font-size:24px;box-shadow:0 10px 25px #2563eb33}.brand h1{font-size:20px}.brand p{font-size:12px;color:var(--muted)}.status{background:#dcfce7;color:#15803d;padding:7px 14px;border-radius:30px;border:1px solid #bbf7d0;font-weight:800;font-size:13px}
.layout{display:grid;grid-template-columns:minmax(0,1fr) 410px;min-height:calc(100vh - 68px)}
.left{background:radial-gradient(circle at top,#10244c 0,#050816 45%,#020617 100%);padding:22px;display:flex;flex-direction:column;gap:14px}.right{background:white;border-left:1px solid var(--border);padding:18px 20px;overflow:auto;height:calc(100vh - 68px)}
.toolbar{background:#0f172a;border:1px solid #1e293b;border-radius:16px;padding:14px;display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center}.toolbar label{display:block;font-size:12px;color:#93c5fd;margin-bottom:6px;font-weight:800}.select,.file{width:100%;background:#111827;color:#e5e7eb;border:1px solid #334155;border-radius:10px;padding:11px;outline:none}.toolbar-actions{display:flex;gap:8px;align-items:end}.btn{border:0;border-radius:10px;padding:12px 16px;background:var(--blue);color:white;font-weight:900;cursor:pointer}.btn.secondary{background:#1f2937}.btn:hover{filter:brightness(1.08)}
.stage{position:relative;flex:1;background:#000;border-radius:18px;overflow:hidden;border:1px solid #1e293b;min-height:520px;max-height:520px;display:block}.video-zone{position:absolute;inset:0;background:#000;display:flex;align-items:center;justify-content:center}.mainVideo{width:100%;height:100%;object-fit:contain;background:#000}.empty{color:#94a3b8;text-align:center;line-height:1.6;padding:30px}.subtitle{position:absolute;left:20px;right:190px;bottom:20px;background:#020617dd;color:#f8fafc;padding:12px 16px;border-radius:14px;border:1px solid #334155;line-height:1.45;font-size:14px;box-shadow:0 12px 40px #0009;z-index:12}
/* transparent interpreter overlay */
.avatar-zone{position:absolute;right:22px;bottom:92px;width:155px;height:270px;z-index:50;
background:rgba(255,255,255,0.14);border:1px solid rgba(255,255,255,0.25);
border-radius:18px;box-shadow:0 12px 35px rgba(0,0,0,0.30);
overflow:hidden;display:none;pointer-events:none;backdrop-filter:blur(2px)}.avatar-zone.show{display:block}.avatar-canvas-wrap{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;overflow:visible}#avatarCanvas{width:100%;height:100%;display:block;background:transparent!important}.gloss{position:absolute;left:7px;right:7px;bottom:4px;text-align:center;background:rgba(2,6,23,.72);color:white;border:1px solid rgba(255,255,255,.25);border-radius:999px;padding:5px 7px;font-weight:950;font-size:10px;box-shadow:0 10px 28px #0008;z-index:5;backdrop-filter:blur(4px)}.avatar-debug{position:absolute;top:8px;left:8px;right:8px;background:rgba(2,6,23,.70);color:#cbd5e1;border-radius:10px;padding:6px 8px;font-size:10px;text-align:center;display:none}.avatar-debug.show{display:block}
.signs-row{display:flex;gap:7px;overflow-x:auto;padding:10px 0}.chip{background:#eff6ff;border:1px solid #bfdbfe;color:#1d4ed8;border-radius:999px;padding:7px 10px;font-size:12px;font-weight:800;white-space:nowrap}.chip.active{background:#2563eb;color:white;border-color:#2563eb}.chip.finger{background:#fff7ed;color:#c2410c;border-color:#fed7aa}
.card{border:1px solid var(--border);border-radius:16px;padding:16px;margin-bottom:16px;background:#fff;box-shadow:0 6px 20px #0f172a08}.card h2{font-size:17px;margin-bottom:10px;display:flex;gap:8px;align-items:center}.transcript{background:#f8fafc;border:1px solid #eef2f7;border-radius:14px;padding:14px;line-height:1.7;color:#475569;max-height:220px;overflow:auto}.small{font-size:12px;color:var(--muted);line-height:1.5}.qa{display:flex;gap:8px}.qa input{flex:1;border:1px solid var(--border);border-radius:11px;padding:12px}.answer{margin-top:10px;background:#f0f9ff;border:1px solid #bae6fd;color:#0c4a6e;border-radius:12px;padding:12px;line-height:1.5}.progress{display:none;margin-top:8px;color:#93c5fd;font-size:13px}.progress.on{display:block}.stats{display:grid;grid-template-columns:1fr 1fr;gap:10px}.stat{background:#f8fafc;border:1px solid var(--border);border-radius:12px;padding:12px}.stat strong{display:block;color:#111827}.stat span{font-size:12px;color:var(--muted)}
@media(max-width:1050px){.layout{grid-template-columns:1fr}.right{height:auto}.stage{min-height:520px}.avatar-zone{right:12px;bottom:24px;width:190px;height:315px}.subtitle{right:205px;font-size:12px}}
</style>
</head>
<body>
<nav><div class="brand"><div class="logo">🤟</div><div><h1>iLearning SignAI</h1><p>Educational Video → Whisper → ASL Glosses → Real 3D Avatar</p></div></div><div class="status">● Demo Active</div></nav>
<div class="layout">
<section class="left">
<div class="toolbar">
<div><label>Choose a video inside the platform</label><select id="videoSelect" class="select"></select></div>
<div class="toolbar-actions"><button class="btn" onclick="loadSelectedVideo()">Load</button><button class="btn secondary" onclick="processSelectedVideo()">Process</button></div>
<div style="grid-column:1/-1;display:grid;grid-template-columns:1fr auto;gap:10px;align-items:center"><input id="upload" class="file" type="file" accept="video/mp4,video/webm" onchange="loadUploadedVideo()" /><button class="btn secondary" onclick="processUploadedVideo()">Process upload</button></div>
<div id="progress" class="progress">Processing with Whisper. The first run can take time...</div>
</div>
<div class="stage">
<div class="video-zone">
<div id="empty" class="empty">Choose an internal video or upload an MP4.<br>When the video plays, the avatar appears inside the video.</div>
<video id="mainVideo" class="mainVideo" controls style="display:none"></video>
<div id="subtitle" class="subtitle" style="display:none">Process the video to generate subtitles and glosses.</div>
</div>
<div class="avatar-zone" id="avatarPanel">
<div class="avatar-canvas-wrap"><canvas id="avatarCanvas"></canvas></div>
<div id="avatarDebug" class="avatar-debug">Loading avatar...</div>
<div id="currentGloss" class="gloss">WAITING</div>
</div>
</div>
</section>
<aside class="right">
<div class="card"><h2>🎞️ Current lesson</h2><p id="lessonInfo" class="small">No video selected yet.</p></div>
<div class="card"><h2>📝 Whisper transcription</h2><div id="transcript" class="transcript">Process a video to see the real transcript here.</div></div>
<div class="card"><h2>🤟 ASL gloss sequence</h2><div id="signsRow" class="signs-row"><span class="chip">WAITING</span></div><p class="small">Dictionary glosses come from the real transcript. Unknown words use finger-spelling style.</p></div>
<div class="card"><h2>❓ Ask a question</h2><div class="qa"><input id="question" placeholder="Example: What is the lesson about?"/><button class="btn" onclick="askQuestion()">Ask</button></div><div id="answer" class="answer" style="display:none"></div></div>
<div class="card"><h2>📊 Pipeline status</h2><div class="stats"><div class="stat"><strong id="m1">Ready</strong><span>STT</span></div><div class="stat"><strong id="m2">0</strong><span>Glosses</span></div><div class="stat"><strong id="m3">GLB Avatar</strong><span>Mode</span></div><div class="stat"><strong>Demo</strong><span>MVP</span></div></div></div>
</aside>
</div>
<script type="importmap">
{"imports":{"three":"https://unpkg.com/three@0.160.0/build/three.module.js","three/addons/":"https://unpkg.com/three@0.160.0/examples/jsm/"}}
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
let videos=[], selectedVideo=null, uploadedFile=null, transcriptText='', signs=[], timeline=[], activeGloss='WAITING';
let scene, camera, renderer, mixer, avatarRoot, clips=[], currentAction=null, fallbackClock=new THREE.Clock();
let upperArmL, upperArmR, lowerArmL, lowerArmR, handL, handR, headBone;
const $=id=>document.getElementById(id);
window.loadSelectedVideo=loadSelectedVideo; window.processSelectedVideo=processSelectedVideo; window.loadUploadedVideo=loadUploadedVideo; window.processUploadedVideo=processUploadedVideo; window.askQuestion=askQuestion;
async function init(){initAvatar3D();const res=await fetch('/api/videos');const data=await res.json();videos=data.videos||[];const sel=$('videoSelect');sel.innerHTML='';if(!videos.length){sel.innerHTML='<option>No internal videos found in static/videos/</option>';return;}videos.forEach(v=>{const o=document.createElement('option');o.value=v.filename;o.textContent=v.title;sel.appendChild(o)});selectedVideo=videos[0];loadSelectedVideo();}
function connectVideoEvents(){const vid=$('mainVideo');vid.onplay=()=>{showAvatar();updateAvatarFromVideo();};vid.onpause=()=>{};vid.onended=()=>{$('avatarPanel').classList.remove('show');};vid.ontimeupdate=updateAvatarFromVideo;}
function showAvatar(){$('avatarPanel').classList.add('show');resizeAvatar();}
function loadSelectedVideo(){const filename=$('videoSelect').value;selectedVideo=videos.find(v=>v.filename===filename);if(!selectedVideo)return;const vid=$('mainVideo');vid.src=selectedVideo.url;vid.style.display='block';$('empty').style.display='none';$('lessonInfo').textContent=selectedVideo.title+' — stored inside the platform.';connectVideoEvents();}
function loadUploadedVideo(){const file=$('upload').files[0];if(!file)return;uploadedFile=file;const vid=$('mainVideo');vid.src=URL.createObjectURL(file);vid.style.display='block';$('empty').style.display='none';$('lessonInfo').textContent=file.name+' — uploaded by user.';connectVideoEvents();}
async function processSelectedVideo(){if(!selectedVideo){alert('Choose a video first');return;}const fd=new FormData();fd.append('local_video',selectedVideo.filename);await sendProcess(fd);}
async function processUploadedVideo(){if(!uploadedFile){alert('Upload a video first');return;}const fd=new FormData();fd.append('file',uploadedFile);await sendProcess(fd);}
async function sendProcess(fd){$('progress').classList.add('on');$('transcript').textContent='Processing with Whisper...';$('subtitle').style.display='block';$('subtitle').textContent='Processing...';showAvatar();try{const res=await fetch('/api/process-video',{method:'POST',body:fd});const data=await res.json();if(data.error)throw new Error(data.error);transcriptText=data.transcript||'';signs=data.signs||[];timeline=data.timeline||[];$('transcript').textContent=transcriptText;$('m1').textContent=data.metrics?.stt||'Whisper';$('m2').textContent=signs.length;$('m3').textContent='Real GLB';renderSigns();updateAvatarFromVideo();}catch(e){$('transcript').textContent='Error: '+e.message;$('subtitle').textContent='Error: '+e.message;}finally{$('progress').classList.remove('on')}}
function renderSigns(){const row=$('signsRow');row.innerHTML='';if(!signs.length){row.innerHTML='<span class="chip">NO GLOSS</span>';return;}signs.forEach((s,i)=>{const c=document.createElement('span');c.className='chip '+(s.mode==='finger-spelling'?'finger':'');c.id='chip'+i;c.textContent=s.gloss;row.appendChild(c)});}
function updateAvatarFromVideo(){const vid=$('mainVideo');let gloss='WAITING';let subtitle='';let index=-1;if(timeline.length){const t=vid.currentTime||0;const item=timeline.find(x=>t>=x.start&&t<x.end)||timeline[Math.min(Math.floor(t/1.4),timeline.length-1)];if(item){gloss=item.gloss;subtitle=item.source_word?`${item.source_word}${item.gloss}`:item.gloss;index=signs.findIndex(s=>s.gloss===item.gloss);}}else if(signs.length){const i=Math.floor((vid.currentTime||0)/1.4)%signs.length;gloss=signs[i].gloss;subtitle=gloss;index=i;}setActiveGloss(gloss,index);if(subtitle){$('subtitle').style.display='block';$('subtitle').textContent=subtitle;}}
function setActiveGloss(gloss,index=-1){if(!gloss)gloss='WAITING';if(activeGloss!==gloss){activeGloss=gloss;$('currentGloss').textContent=gloss;playClipForGloss(gloss);}document.querySelectorAll('.chip').forEach(c=>c.classList.remove('active'));if(index>=0){const chip=$('chip'+index);if(chip)chip.classList.add('active');}}
function initAvatar3D(){const canvas=$('avatarCanvas');scene=new THREE.Scene();scene.background=null;camera=new THREE.PerspectiveCamera(25,1,0.1,100);camera.position.set(0,0.8,5);renderer=new THREE.WebGLRenderer({canvas,antialias:true,alpha:true});renderer.setClearColor(0x000000,0);renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));scene.add(new THREE.AmbientLight(0xffffff,1.8));const key=new THREE.DirectionalLight(0xffffff,2.8);key.position.set(2.5,4,5);scene.add(key);const fill=new THREE.DirectionalLight(0x8ec5ff,1.3);fill.position.set(-3,2,3);scene.add(fill);const loader=new GLTFLoader();const paths=['/static/models/Woman.glb','/static/models/avatar.glb','/static/Woman.glb'];loadAvatarFromPaths(loader,paths,0);window.addEventListener('resize',resizeAvatar);resizeAvatar();animate();}
function loadAvatarFromPaths(loader,paths,index){if(index>=paths.length){console.error('No avatar GLB found. Put Woman.glb in static/models/Woman.glb');$('avatarDebug').classList.add('show');$('avatarDebug').textContent='Avatar GLB not found';$('currentGloss').textContent='AVATAR NOT FOUND';return;}loader.load(paths[index],(gltf)=>{console.log('GLB LOADED OK:',paths[index]);console.log(gltf);avatarRoot=gltf.scene;scene.add(avatarRoot);avatarRoot.traverse(o=>{if(o.isMesh){o.visible=true;o.frustumCulled=false;if(o.material){o.material.transparent=false;o.material.opacity=1;o.material.side=THREE.DoubleSide;o.material.needsUpdate=true;}}const n=(o.name||'').toLowerCase();if(n.includes('leftarm')||n.includes('upperarm_l')||n.includes('upperarm.l')||n.includes('mixamorigleftarm'))upperArmL=o;if(n.includes('rightarm')||n.includes('upperarm_r')||n.includes('upperarm.r')||n.includes('mixamorigrightarm'))upperArmR=o;if(n.includes('leftforearm')||n.includes('lowerarm_l')||n.includes('lowerarm.l')||n.includes('mixamorigleftforearm'))lowerArmL=o;if(n.includes('rightforearm')||n.includes('lowerarm_r')||n.includes('lowerarm.r')||n.includes('mixamorigrightforearm'))lowerArmR=o;if(n.includes('lefthand')||n.includes('palm.l')||n.includes('hand_l')||n.includes('mixamoriglefthand'))handL=o;if(n.includes('righthand')||n.includes('palm.r')||n.includes('hand_r')||n.includes('mixamorigrighthand'))handR=o;if(n==='head'||n.includes('mixamorighead')||n.includes('head'))headBone=o;});normalizeAvatar(avatarRoot);clips=gltf.animations||[];mixer=new THREE.AnimationMixer(avatarRoot);$('avatarDebug').classList.add('show');$('avatarDebug').textContent='Avatar loaded';setTimeout(()=>$('avatarDebug').classList.remove('show'),1600);playClipByName('idle');},undefined,(err)=>{console.warn('Failed avatar path:',paths[index],err);loadAvatarFromPaths(loader,paths,index+1);});}
function normalizeAvatar(root){
root.scale.set(0.68,0.68,0.68);
root.position.set(0,-1.52,0);
root.rotation.set(0,0,0);
camera.position.set(0,0.35,9.2);
camera.lookAt(0,0.05,0);
console.log('AVATAR SMALL TV INTERPRETER VIEW');
}
function resizeAvatar(){const canvas=$('avatarCanvas');const box=canvas.getBoundingClientRect();if(!box.width||!box.height)return;camera.aspect=box.width/box.height;camera.updateProjectionMatrix();renderer.setSize(box.width,box.height,false);}
function playClipByName(part){if(!mixer||!clips.length)return;const lower=part.toLowerCase();const clip=clips.find(c=>c.name.toLowerCase().includes(lower))||clips.find(c=>c.name.toLowerCase().includes('idle'))||clips[0];if(!clip)return;if(currentAction&&currentAction.getClip().name===clip.name)return;const next=mixer.clipAction(clip);next.reset().fadeIn(0.18).play();next.timeScale=1.1;if(currentAction)currentAction.fadeOut(0.18);currentAction=next;}
function playClipForGloss(gloss){const g=(gloss||'').toUpperCase();if(g.includes('WORK')||g.includes('SKILL')||g.includes('EXPERIENCE'))playClipByName('clap');else if(g.includes('PROFILE')||g.includes('PICTURE')||g.includes('PHOTO')||g.includes('HEADLINE'))playClipByName('idle');else if(g.includes('MATH')||g.includes('SYSTEM')||g.includes('EQUATION')||g.includes('SOLVE'))playClipByName('punch');else if(g.includes('HELLO')||g.includes('WELCOME')||g.includes('CONNECT'))playClipByName('wave');else if(g.includes('EDUCATION')||g.includes('LEARN')||g.includes('COURSE'))playClipByName('jump');else playClipByName('idle');}
function manualSignPose(){
if(!avatarRoot)return;
const t=fallbackClock.getElapsedTime();
const g=(activeGloss||'').toUpperCase();
// Small natural body movement
avatarRoot.rotation.y=Math.sin(t*0.8)*0.045;
avatarRoot.position.x=Math.sin(t*1.4)*0.01;
if(headBone){
headBone.rotation.y=Math.sin(t*1.7)*0.10;
headBone.rotation.x=Math.sin(t*1.2)*0.035;
}
// If bones are not detected, move the whole avatar visibly
if(!upperArmL || !upperArmR){
avatarRoot.rotation.z=Math.sin(t*4.5)*0.035;
avatarRoot.position.y=-1.52+Math.sin(t*3.5)*0.025;
return;
}
// Reset base positions each frame
const fast=Math.sin(t*6);
const medium=Math.sin(t*4);
const slow=Math.sin(t*2.5);
if(g.includes('HELLO')||g.includes('WELCOME')||g.includes('CONNECT')){
// Right-hand wave
upperArmR.rotation.z=0.95 + fast*0.25;
upperArmR.rotation.x=-0.18;
upperArmL.rotation.z=-0.20;
if(lowerArmR) lowerArmR.rotation.z=-0.65 + fast*0.35;
if(handR) handR.rotation.y=fast*0.55;
}
else if(g.includes('PROFILE')||g.includes('PHOTO')||g.includes('PICTURE')||g.includes('HEADLINE')){
// Hand near face
upperArmR.rotation.z=0.72 + medium*0.10;
upperArmR.rotation.x=-0.45;
upperArmL.rotation.z=-0.28 + slow*0.06;
if(lowerArmR) lowerArmR.rotation.z=-0.85 + medium*0.25;
if(handR) handR.rotation.x=medium*0.30;
}
else if(g.includes('WORK')||g.includes('SKILL')||g.includes('EXPERIENCE')){
// Two hands working in front
upperArmL.rotation.z=-0.55 + fast*0.32;
upperArmR.rotation.z=0.55 - fast*0.32;
upperArmL.rotation.x=-0.20;
upperArmR.rotation.x=-0.20;
if(lowerArmL) lowerArmL.rotation.z=0.45 + fast*0.25;
if(lowerArmR) lowerArmR.rotation.z=-0.45 - fast*0.25;
if(handL) handL.rotation.y=fast*0.45;
if(handR) handR.rotation.y=-fast*0.45;
}
else if(g.includes('EDUCATION')||g.includes('LEARN')||g.includes('COURSE')){
// Hands move from book/chest to head
upperArmL.rotation.z=-0.75 + medium*0.18;
upperArmR.rotation.z=0.75 - medium*0.18;
upperArmL.rotation.x=-0.35;
upperArmR.rotation.x=-0.35;
if(lowerArmL) lowerArmL.rotation.z=0.75 + medium*0.20;
if(lowerArmR) lowerArmR.rotation.z=-0.75 - medium*0.20;
}
else if(g.includes('MATH')||g.includes('SYSTEM')||g.includes('EQUATION')||g.includes('SOLVE')||g.includes('LINEAR')){
// Alternating calculation movement
upperArmL.rotation.z=-0.38 + fast*0.22;
upperArmR.rotation.z=0.38 - fast*0.22;
if(lowerArmL) lowerArmL.rotation.z=0.25 + medium*0.35;
if(lowerArmR) lowerArmR.rotation.z=-0.25 - medium*0.35;
if(handL) handL.rotation.x=fast*0.35;
if(handR) handR.rotation.x=-fast*0.35;
}
else{
// Default signing motion
upperArmL.rotation.z=-0.28 + medium*0.16;
upperArmR.rotation.z=0.28 - medium*0.16;
if(lowerArmL) lowerArmL.rotation.z=0.18 + medium*0.18;
if(lowerArmR) lowerArmR.rotation.z=-0.18 - medium*0.18;
if(handL) handL.rotation.y=medium*0.25;
if(handR) handR.rotation.y=-medium*0.25;
}
}
function animate(){requestAnimationFrame(animate);resizeAvatar();const dt=fallbackClock.getDelta();if(mixer)mixer.update(dt);manualSignPose();renderer.render(scene,camera);}
async function askQuestion(){const question=$('question').value.trim();if(!question)return;const res=await fetch('/api/ask',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question,transcript:transcriptText})});const data=await res.json();$('answer').style.display='block';$('answer').textContent=data.answer;}
init();
</script>
</body>
</html>