isl-detection / templates /avatar_interface.html
Deepak Roshan
Add minimalist splash transitions for Phase 1 and Phase 2 with auto-redirects
991d9e5
Raw
History Blame Contribute Delete
32.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
<title>ISL Fingerspelling β€” AI Sign Language</title>
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700&family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
:root{
--bg:#0d1117;--surf:#131922;--card:#1a2233;
--bdr:rgba(212,168,83,.13);--bdrm:rgba(212,168,83,.30);--bdrh:rgba(212,168,83,.58);
--gold:#d4a853;--goldb:#f0c76a;--goldd:#a07c3a;
--sage:#7eb89a;--txt:#f0ede6;--txtm:#a8a096;--txtl:#5a5650;--red:#c0614a;
--r8:8px;--r14:14px;
--fD:'Playfair Display',Georgia,serif;
--fB:'DM Sans',system-ui,sans-serif;
--fM:'DM Mono',monospace;
}
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%;overflow:hidden}
body{font-family:var(--fB);background:var(--bg);color:var(--txt);-webkit-font-smoothing:antialiased;display:flex;flex-direction:column}
#app{display:flex;flex:1;overflow:hidden}
/* ── Canvas pane ── */
#cvp{flex:1;display:flex;flex-direction:column;min-width:0;background:var(--bg);position:relative}
#hc{flex:1;width:100%;display:block;touch-action:none}
.topbar{display:flex;align-items:center;justify-content:space-between;padding:10px 16px 0;flex-shrink:0}
.brand{display:flex;align-items:baseline;gap:8px}
.brand-t{font-family:var(--fD);font-size:1.1rem;font-weight:700;color:var(--gold)}
.brand-s{font-size:.65rem;color:var(--txtl);text-transform:uppercase;letter-spacing:.07em}
.badge{font-size:.65rem;font-family:var(--fM);color:var(--sage);background:rgba(126,184,154,.1);border:1px solid rgba(126,184,154,.25);padding:3px 9px;border-radius:20px;transition:all .3s}
.vrow{display:flex;gap:4px;padding:6px 16px 0;flex-shrink:0}
.vb{padding:4px 10px;font-size:.63rem;font-family:var(--fM);background:transparent;border:1px solid var(--bdr);color:var(--txtl);border-radius:6px;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:.03em}
.vb:hover{color:var(--txt);border-color:var(--bdrm)}
.vb.on{background:rgba(212,168,83,.18);color:var(--gold);border-color:var(--bdrm)}
.gov{position:absolute;bottom:0;left:0;right:0;padding:12px 18px 14px;background:linear-gradient(to top,rgba(13,17,23,1) 55%,transparent);display:flex;align-items:flex-end;justify-content:space-between;gap:10px;pointer-events:none}
.gl{font-family:var(--fD);font-size:clamp(2rem,5vw,3.2rem);font-weight:700;color:var(--goldb);line-height:1}
.gd{font-size:.72rem;color:var(--txtm);text-align:right;max-width:180px;line-height:1.45}
.gp{position:absolute;bottom:0;left:0;right:0;height:3px;background:rgba(212,168,83,.1)}
.gb{height:100%;width:0%;background:linear-gradient(90deg,var(--goldd),var(--goldb));transition:width .12s linear;position:relative;overflow:hidden}
.gb::after{content:'';position:absolute;inset:0;background:linear-gradient(90deg,transparent,rgba(255,255,255,.3),transparent);animation:sh 1.8s infinite}
@keyframes sh{from{transform:translateX(-100%)}to{transform:translateX(200%)}}
/* ── Control pane ── */
#cp{width:340px;flex-shrink:0;display:flex;flex-direction:column;background:var(--surf);border-left:1px solid var(--bdr);overflow-y:auto}
#cp::-webkit-scrollbar{width:4px}
#cp::-webkit-scrollbar-thumb{background:var(--bdrm);border-radius:4px}
@media(max-width:720px){
#app{flex-direction:column;height:100%}
#cvp{flex:0 0 46vh;min-height:260px;max-height:46vh}
#cp{width:100%;flex:1;border-left:none;border-top:1px solid var(--bdr);overflow-y:auto}
}
.pan{padding:14px 16px;display:flex;flex-direction:column;gap:12px}
.slbl{font-size:.65rem;font-weight:600;text-transform:uppercase;letter-spacing:.08em;color:var(--txtl);display:flex;align-items:center;gap:6px}
textarea{width:100%;height:82px;resize:none;background:var(--card);border:1px solid var(--bdr);border-radius:var(--r14);color:var(--txt);font-family:var(--fB);font-size:.92rem;line-height:1.5;padding:11px 13px 26px;outline:none;transition:border-color .25s}
textarea::placeholder{color:var(--txtl)}
textarea:focus{border-color:var(--bdrh)}
.iwrap{position:relative}
.cc{position:absolute;bottom:8px;right:10px;font-size:.62rem;font-family:var(--fM);color:var(--txtl)}
/* ── Speech warning banner ── */
.mic-warn{background:rgba(212,168,83,.07);border:1px solid rgba(212,168,83,.25);border-radius:var(--r8);padding:10px 13px;font-size:.72rem;color:var(--txtm);line-height:1.7;display:none}
.mic-warn.show{display:block}
.mic-warn strong{color:var(--gold)}
.mic-warn code{background:rgba(212,168,83,.12);border:1px solid rgba(212,168,83,.2);border-radius:4px;padding:1px 5px;font-family:var(--fM);font-size:.68rem;color:var(--goldb)}
/* ── Mic status inline ── */
.mic-status{display:none;align-items:center;gap:7px;padding:6px 11px;border-radius:var(--r8);background:rgba(126,184,154,.07);border:1px solid rgba(126,184,154,.2);font-size:.72rem;color:var(--sage);font-family:var(--fM)}
.mic-status.show{display:flex}
.mic-status.listening{background:rgba(192,97,74,.08);border-color:rgba(192,97,74,.3);color:#e07060}
.mic-dot{width:7px;height:7px;border-radius:50%;background:var(--sage);animation:pd .9s infinite;flex-shrink:0}
.mic-status.listening .mic-dot{background:#e07060}
@keyframes pd{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.5;transform:scale(.75)}}
/* ── Buttons ── */
.bg4{display:grid;grid-template-columns:repeat(4,1fr);gap:7px}
.btn{display:flex;align-items:center;justify-content:center;gap:4px;padding:10px 6px;border:1px solid transparent;border-radius:var(--r14);font-family:var(--fB);font-size:.76rem;font-weight:600;cursor:pointer;transition:all .22s;white-space:nowrap;overflow:hidden}
.btn:active{transform:scale(.96)}
.btn:disabled{opacity:.38;cursor:not-allowed}
.btn:disabled:active{transform:none}
.bc{background:linear-gradient(135deg,#c49a3a,#d4a853,#e8c06a);color:#0d1117;border-color:rgba(212,168,83,.4);box-shadow:0 2px 10px rgba(212,168,83,.2)}
.bc:not(:disabled):hover{box-shadow:0 4px 18px rgba(212,168,83,.38)}
.bm{background:linear-gradient(135deg,#4e7d66,#7eb89a);color:#0d1117;border-color:rgba(126,184,154,.35)}
.bm:not(:disabled):hover{box-shadow:0 4px 16px rgba(126,184,154,.3)}
.bm.listening{background:linear-gradient(135deg,#8a3a2a,#c0614a)!important;color:#fff!important;border-color:rgba(192,97,74,.4)!important;animation:pulse .8s infinite}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(192,97,74,.4)}50%{box-shadow:0 0 0 8px rgba(192,97,74,0)}}
.bs{background:transparent;color:var(--red);border-color:rgba(192,97,74,.3)}
.bs:hover{background:rgba(192,97,74,.08);border-color:rgba(192,97,74,.55)}
.bg2{background:transparent;color:var(--txtm);border-color:var(--bdr)}
.bg2:hover{border-color:var(--bdrm);color:var(--txt)}
/* ── Status pill ── */
.st{display:flex;align-items:center;gap:7px;padding:7px 13px;border-radius:30px;font-size:.73rem;font-weight:500;border:1px solid var(--bdr);background:var(--card);color:var(--txtm);transition:all .3s;min-height:34px}
.dot{width:7px;height:7px;border-radius:50%;background:var(--txtl);flex-shrink:0;transition:all .3s}
.st.act .dot{background:var(--sage);box-shadow:0 0 8px var(--sage);animation:pd 1.4s infinite}
.st.act{border-color:rgba(126,184,154,.3);color:var(--sage)}
.st.rec .dot{background:var(--gold);box-shadow:0 0 8px var(--gold);animation:pd .9s infinite}
.st.rec{border-color:var(--bdrm);color:var(--gold)}
.st.err .dot{background:var(--red)}
.st.err{border-color:rgba(192,97,74,.3);color:var(--red)}
.div{height:1px;background:var(--bdr);margin:0 -16px}
.sh{display:flex;align-items:center;justify-content:space-between}
.sbadge{font-family:var(--fM);font-size:.62rem;background:rgba(212,168,83,.12);color:var(--gold);border:1px solid rgba(212,168,83,.2);padding:2px 8px;border-radius:20px}
.ll{max-height:180px;overflow-y:auto;display:flex;flex-direction:column;gap:4px;padding-right:2px}
.ll::-webkit-scrollbar{width:3px}
.ll::-webkit-scrollbar-thumb{background:var(--bdrm);border-radius:3px}
.li{display:flex;align-items:center;gap:9px;padding:7px 11px;border-radius:var(--r8);background:var(--card);border:1px solid var(--bdr);transition:all .25s;flex-shrink:0}
.li.on{background:rgba(212,168,83,.1);border-color:var(--bdrh);box-shadow:0 0 14px rgba(212,168,83,.1)}
.li.sp{background:rgba(126,184,154,.05);border-color:rgba(126,184,154,.15)}
.lc{font-family:var(--fD);font-size:1.05rem;font-weight:700;color:var(--gold);width:22px;text-align:center;flex-shrink:0}
.li.on .lc{color:var(--goldb)}
.li.sp .lc{color:var(--sage);font-size:.75rem}
.lh{font-size:.68rem;color:var(--txtl);flex:1}
.li.on .lh{color:var(--txtm)}
.es{text-align:center;padding:24px 0;color:var(--txtl);font-size:.78rem;line-height:1.7}
.sg{display:grid;grid-template-columns:1fr 1fr;gap:7px}
.st2{background:var(--card);border:1px solid var(--bdr);border-radius:var(--r14);padding:11px 13px;transition:border-color .25s}
.st2:hover{border-color:var(--bdrm)}
.sl2{font-size:.6rem;text-transform:uppercase;letter-spacing:.07em;color:var(--txtl);font-weight:600;margin-bottom:3px}
.sv{font-family:var(--fD);font-size:1.6rem;font-weight:700;color:var(--gold);line-height:1}
.abt{font-size:.77rem;color:var(--txtm);line-height:1.7}
.abt p{margin-bottom:8px}
.al{list-style:none;margin-bottom:8px}
.al li{padding-left:12px;position:relative;font-size:.75rem;padding-bottom:2px}
.al li::before{content:'β€Ί';position:absolute;left:0;color:var(--gold);font-weight:700}
.acr{margin-top:12px;padding-top:10px;border-top:1px solid var(--bdr)}
.an{font-family:var(--fD);color:var(--gold);font-weight:600;font-size:.88rem}
.ar{font-size:.67rem;color:var(--txtl);margin:2px 0 3px}
.ae{color:var(--sage);font-size:.68rem;font-family:var(--fM)}
.nav{display:flex;gap:7px;padding:0 16px 20px}
.na{flex:1;display:flex;align-items:center;justify-content:center;gap:6px;padding:12px 8px;border-radius:var(--r14);border:1px solid var(--bdr);background:var(--card);color:var(--txtm);text-decoration:none;font-size:.76rem;font-weight:600;transition:all .25s}
.na:hover{border-color:var(--bdrm);color:var(--txt)}
.na.on{background:rgba(212,168,83,.1);border-color:var(--bdrh);color:var(--gold)}
</style>
</head>
<body>
<div id="app">
<!-- Canvas side -->
<div id="cvp">
<div class="topbar">
<div class="brand">
<span class="brand-t">ISL Fingerspelling</span>
<span class="brand-s">Speech Β· Sign Β· AI</span>
</div>
<span class="badge" id="badge">● Ready</span>
</div>
<div class="vrow">
<button class="vb on" onclick="setView('front',this)">Front</button>
<button class="vb" onclick="setView('side',this)">Side</button>
<button class="vb" onclick="setView('top',this)">Top</button>
<button class="vb" onclick="setView('spin',this)">Spin</button>
</div>
<canvas id="hc"></canvas>
<div class="gov">
<div id="gl" class="gl">🀟</div>
<div id="gd" class="gd">Type or speak to begin</div>
<div class="gp"><div class="gb" id="gb"></div></div>
</div>
</div>
<!-- Control side -->
<div id="cp">
<div class="pan">
<div class="slbl">✍️ Enter Text</div>
<div class="iwrap">
<textarea id="txt" placeholder="Type your message here…" maxlength="200"></textarea>
<span class="cc" id="cc">0/200</span>
</div>
<div class="mic-warn" id="mwarn"></div>
<div class="mic-status" id="mstat">
<div class="mic-dot"></div>
<span id="mstxt">Listening…</span>
</div>
<div class="bg4">
<button class="btn bc" onclick="doConvert()">✨ Convert</button>
<button class="btn bm" onclick="doSpeak()" id="bspk">🎀 Speak</button>
<button class="btn bs" onclick="doStop()">⏹ Stop</button>
<button class="btn bg2" onclick="doClear()">βœ• Clear</button>
</div>
<div class="st" id="st"><div class="dot"></div><span id="stxt">Ready to translate</span></div>
<div class="div"></div>
<div class="sh">
<div class="slbl">πŸ“‹ Letter Sequence</div>
<span class="sbadge" id="scnt">0 letters</span>
</div>
<div class="ll" id="ll">
<div class="es">Type text or speak<br>to see the sequence</div>
</div>
<div class="div"></div>
<div class="slbl">πŸ“Š Session Stats</div>
<div class="sg">
<div class="st2"><div class="sl2">Conversions</div><div class="sv" id="s0">0</div></div>
<div class="st2"><div class="sl2">Letters Signed</div><div class="sv" id="s1">0</div></div>
<div class="st2"><div class="sl2">Speech Uses</div><div class="sv" id="s2">0</div></div>
<div class="st2"><div class="sl2">Words</div><div class="sv" id="s3">0</div></div>
</div>
<div class="div"></div>
<div class="slbl">ℹ️ About</div>
<div class="abt">
<p>AI Powered ISL converts typed or spoken language into Indian Sign Language 3D animation, letter by letter.</p>
<ul class="al">
<li>Deaf &amp; hard-of-hearing individuals</li>
<li>Inclusive education environments</li>
<li>Public service &amp; assistive AI</li>
</ul>
<div class="acr">
<div class="an">Deepak Roshan</div>
<div class="ar">AI Engineer</div>
<div class="ae">deepakroshan380@gmail.com</div>
</div>
</div>
</div>
<!-- FIX: Nav links built dynamically β€” works on localhost AND cloud domain -->
<div class="nav">
<a href="#" class="na" id="nav-gesture">😊 Gesture Recognition</a>
<a href="#" class="na on" id="nav-avatar">🀟 Speech-to-Sign</a>
</div>
</div>
</div>
<script>
// ══════════════════════════════════════════════════════════
// ISL POSE TABLE A–Z (unchanged)
// ══════════════════════════════════════════════════════════
const ISL={
a:{thumb:.1, index:1.5,middle:1.5,ring:1.5,pinky:1.5,rz:0},
b:{thumb:1.2,index:0, middle:0, ring:0, pinky:0, rz:0},
c:{thumb:.3, index:.5, middle:.5, ring:.5, pinky:.5, rz:0},
d:{thumb:1.2,index:0, middle:1.5,ring:1.5,pinky:1.5,rz:0},
e:{thumb:.8, index:1.2,middle:1.2,ring:1.2,pinky:1.2,rz:0},
f:{thumb:.8, index:.8, middle:0, ring:0, pinky:0, rz:0},
g:{thumb:0, index:0, middle:1.5,ring:1.5,pinky:1.5,rz:1.57},
h:{thumb:1.5,index:0, middle:0, ring:1.5,pinky:1.5,rz:1.57},
i:{thumb:1.5,index:1.5,middle:1.5,ring:1.5,pinky:0, rz:0},
j:{thumb:1.5,index:1.5,middle:1.5,ring:1.5,pinky:0, rz:0},
k:{thumb:.5, index:0, middle:0, ring:1.5,pinky:1.5,rz:.78},
l:{thumb:0, index:0, middle:1.5,ring:1.5,pinky:1.5,rz:0},
m:{thumb:.8, index:1.5,middle:1.5,ring:1.5,pinky:1.5,rz:0},
n:{thumb:.8, index:1.5,middle:1.5,ring:1.5,pinky:1.5,rz:0},
o:{thumb:.5, index:.8, middle:.8, ring:.8, pinky:.8, rz:0},
p:{thumb:.5, index:0, middle:0, ring:1.5,pinky:1.5,rz:-.78},
q:{thumb:0, index:0, middle:1.5,ring:1.5,pinky:1.5,rz:-1.57},
r:{thumb:1.2,index:0, middle:0, ring:1.5,pinky:1.5,rz:0},
s:{thumb:.5, index:1.5,middle:1.5,ring:1.5,pinky:1.5,rz:0},
t:{thumb:.8, index:1.5,middle:1.5,ring:1.5,pinky:1.5,rz:0},
u:{thumb:1.2,index:0, middle:0, ring:1.5,pinky:1.5,rz:0},
v:{thumb:1.2,index:0, middle:0, ring:1.5,pinky:1.5,rz:0},
w:{thumb:1.2,index:0, middle:0, ring:0, pinky:1.5,rz:0},
x:{thumb:1.5,index:.8, middle:1.5,ring:1.5,pinky:1.5,rz:0},
y:{thumb:0, index:1.5,middle:1.5,ring:1.5,pinky:0, rz:0},
z:{thumb:1.5,index:0, middle:1.5,ring:1.5,pinky:1.5,rz:0},
};
const DESC={
a:'Fist with thumb at side',b:'Four fingers straight up',c:'Curved C shape',
d:'Index pointing up',e:'All fingers curled',f:'OK sign variant',
g:'Index + thumb sideways',h:'Two fingers horizontal',i:'Pinky raised',
j:'Pinky traces J',k:'V with thumb between',l:'L shape',
m:'Three fingers over thumb',n:'Two fingers over thumb',o:'O ring shape',
p:'K pointing down',q:'G pointing down',r:'Fingers crossed',
s:'Fist thumb across',t:'Thumb between fingers',u:'Two fingers together',
v:'Peace sign',w:'Three fingers wide',x:'Index hooked',
y:'Hang loose',z:'Index traces Z'
};
const REST={thumb:.05,index:.05,middle:.05,ring:.05,pinky:.05,rz:0};
const SKIN=0xfdb4c4,JOINT=0xf5a3b5;
// ══════════════════════════════════════════════════════════
// THREE.JS (unchanged)
// ══════════════════════════════════════════════════════════
let scene,cam,ren,hg,fing={};
let autoSpin=false;
let cC={thumb:.05,index:.05,middle:.05,ring:.05,pinky:.05};
let tC={...cC},cRZ=0,tRZ=0;
let aRun=false,aCan=false;
function initThree(){
const cv=document.getElementById('hc');
const cp=document.getElementById('cvp');
scene=new THREE.Scene();
scene.background=new THREE.Color(0x0d1117);
scene.fog=new THREE.FogExp2(0x0d1117,.022);
const W=cp.clientWidth,H=cp.clientHeight;
cam=new THREE.PerspectiveCamera(50,W/H,.1,1000);
cam.position.set(0,0,8);cam.lookAt(0,0,0);
ren=new THREE.WebGLRenderer({canvas:cv,antialias:true,powerPreference:'low-power'});
ren.setSize(W,H,false);ren.setPixelRatio(Math.min(devicePixelRatio,1.5));
ren.shadowMap.enabled=true;ren.info.autoReset=false;
scene.add(new THREE.AmbientLight(0xffffff,.65));
const kl=new THREE.DirectionalLight(0xfff4e0,.85);kl.position.set(5,10,7);kl.castShadow=true;scene.add(kl);
const fl=new THREE.DirectionalLight(0xd0e8ff,.3);fl.position.set(-6,2,4);scene.add(fl);
const rl=new THREE.DirectionalLight(0xffddbb,.2);rl.position.set(1,-4,-5);scene.add(rl);
buildHand();loop();
}
function mkFinger(cfg){
const fg=new THREE.Group();fg.userData={segs:[]};
const sl=cfg.length/3;let cy=0;
for(let i=0;i<3;i++){
const sg=new THREE.Group();const r=cfg.thickness*(1-i*.15);
const seg=new THREE.Mesh(new THREE.CylinderGeometry(r,r*.9,sl,12),new THREE.MeshStandardMaterial({color:SKIN,roughness:.7}));
seg.position.y=sl/2;seg.castShadow=true;sg.add(seg);
if(i<2){const jt=new THREE.Mesh(new THREE.SphereGeometry(r*.7,10,10),new THREE.MeshStandardMaterial({color:JOINT}));jt.position.y=sl;sg.add(jt);}
sg.position.y=cy;cy+=sl;
i===0?fg.add(sg):fg.userData.segs[i-1].add(sg);
fg.userData.segs.push(sg);
}
return fg;
}
function buildHand(){
hg=new THREE.Group();
hg.add(new THREE.Mesh(new THREE.BoxGeometry(1.8,2.5,.6),new THREE.MeshStandardMaterial({color:SKIN,roughness:.7})));
[{name:'thumb',pos:[-1.1,.5,.15],rot:[0,0,-.5],length:1.2,thickness:.35},
{name:'index',pos:[-.7,1.4,0],rot:[0,0,.1],length:1.8,thickness:.28},
{name:'middle',pos:[0,1.5,0],rot:[0,0,0],length:2.0,thickness:.28},
{name:'ring',pos:[.7,1.4,0],rot:[0,0,-.1],length:1.7,thickness:.26},
{name:'pinky',pos:[1.1,1.0,0],rot:[0,0,-.3],length:1.3,thickness:.24}].forEach(d=>{
const f=mkFinger(d);f.position.set(...d.pos);f.rotation.set(...d.rot);hg.add(f);fing[d.name]=f;
});
scene.add(hg);
}
function bend(name,amt){
const f=fing[name];if(!f)return;
f.userData.segs.forEach((s,i)=>{if(s)s.rotation.x=[amt*.4,amt*.6,amt*.8][i];});
}
function setPose(p){
['thumb','index','middle','ring','pinky'].forEach(k=>{tC[k]=p[k]??0.05;});
tRZ=p.rz||0;
}
function loop(){
requestAnimationFrame(loop);
const t=Date.now()*.001;
if(!aRun){hg.position.y=Math.sin(t*.45)*.18;hg.rotation.y=Math.sin(t*.18)*.05;}
else{hg.rotation.y=0;}
['thumb','index','middle','ring','pinky'].forEach(k=>{cC[k]+=(tC[k]-cC[k])*.13;bend(k,cC[k]);});
cRZ+=(tRZ-cRZ)*.13;hg.rotation.z=cRZ;
if(autoSpin){cam.position.x=Math.sin(t*.45)*8;cam.position.z=Math.cos(t*.45)*8;cam.lookAt(0,0,0);}
ren.render(scene,cam);
}
function setView(v,btn){
document.querySelectorAll('.vb').forEach(b=>b.classList.remove('on'));btn.classList.add('on');
autoSpin=(v==='spin');if(autoSpin)return;
const tp={front:[0,0,8],side:[8,0,0],top:[0,8,3]}[v]||[0,0,8];
animCam(...tp);
}
function animCam(tx,ty,tz){
const sx=cam.position.x,sy=cam.position.y,sz=cam.position.z;
const s0=performance.now();
function f(n){const e=ease(Math.min((n-s0)/900,1));cam.position.set(lerp(sx,tx,e),lerp(sy,ty,e),lerp(sz,tz,e));cam.lookAt(0,0,0);if(e<1)requestAnimationFrame(f);}
requestAnimationFrame(f);
}
window.addEventListener('resize',()=>{
if(!ren)return;
const cp=document.getElementById('cvp');
cam.aspect=cp.clientWidth/cp.clientHeight;cam.updateProjectionMatrix();
ren.setSize(cp.clientWidth,cp.clientHeight,false);
});
document.addEventListener('visibilitychange',()=>{if(document.hidden)aCan=true;});
// ══════════════════════════════════════════════════════════
// SPEECH RECOGNITION β€” ROBUST LOCAL + CLOUD HANDLER (unchanged)
// ══════════════════════════════════════════════════════════
let recog=null,isRec=false,micAllowed=false;
function isMicSafe(){
const isLocalhost=location.hostname==='localhost'||location.hostname==='127.0.0.1';
const isHttps=location.protocol==='https:';
return (isLocalhost||isHttps);
}
function showMicWarn(html){
const w=document.getElementById('mwarn');
w.innerHTML=html;w.classList.add('show');
}
function initSpeech(){
const SR=window.SpeechRecognition||window.webkitSpeechRecognition;
const btn=document.getElementById('bspk');
if(!SR){
btn.disabled=true;
btn.title='Use Chrome or Edge for voice input';
showMicWarn(
'πŸ”‡ <strong>Voice input requires Chrome or Edge.</strong><br>'+
'Firefox and Safari do not support the Web Speech API.<br>'+
'You can still <strong>type text</strong> above and press Convert.'
);
return;
}
if(!isMicSafe()){
btn.disabled=true;
btn.title='Mic blocked on HTTP β€” HTTPS required on cloud';
showMicWarn(
'πŸ”’ <strong>Microphone is blocked on plain HTTP.</strong><br>'+
'On your Oracle Cloud server, enable HTTPS to use voice input.<br>'+
'Until then, <strong>type your text</strong> above β€” Convert always works.'
);
return;
}
recog=new SR();
recog.lang='en-IN';
recog.continuous=false;
recog.interimResults=false;
recog.maxAlternatives=3;
recog.onstart=()=>{
setBadge('🎀 Listening…',true);
setSt('rec','Listening… speak clearly in English');
setMicStat(true,'Listening β€” speak now');
document.getElementById('bspk').classList.add('listening');
};
recog.onspeechstart=()=>{
setMicStat(true,'Speech detected…');
setSt('rec','Processing your speech…');
};
recog.onresult=ev=>{
let best='',bestConf=0;
const results=ev.results[0];
for(let i=0;i<results.length;i++){
if(results[i].confidence>=bestConf){bestConf=results[i].confidence;best=results[i].transcript;}
}
const t=best.trim();
if(!t){resetSpk();setSt('err','No speech detected β€” try again');return;}
document.getElementById('txt').value=t;
document.getElementById('cc').textContent=t.length+'/200';
setSt('act','Got: "'+t+'" ('+Math.round(bestConf*100)+'% conf)');
setMicStat(false,'');
isRec=false;resetSpk();
ST.sp++;ST.wd+=t.trim().split(/\s+/).length;updSt();
const seq=buildSeq(t);renderLL(seq);
ST.cv++;updSt();
setTimeout(()=>play(seq),300);
};
recog.onnomatch=()=>{
isRec=false;resetSpk();
setMicStat(false,'');
setSt('err','No match β€” speak more clearly or type instead');
};
recog.onerror=e=>{
isRec=false;resetSpk();setMicStat(false,'');
const msgs={
'not-allowed' :'πŸ”’ Mic permission denied β€” click the πŸ”’ icon in the address bar to allow',
'no-speech' :'No speech detected β€” try speaking louder',
'network' :'Network error β€” check your internet connection',
'aborted' :'Recording cancelled',
'audio-capture':'No microphone found β€” check your device settings',
'service-not-allowed':'Speech service blocked β€” use HTTPS on cloud',
};
setSt('err',msgs[e.error]||'Speech error: '+e.error);
if(e.error==='not-allowed'){
showMicWarn(
'πŸ”’ <strong>Microphone permission was denied.</strong><br>'+
'Click the πŸ”’ padlock icon in your browser address bar,<br>'+
'set Microphone to <strong>Allow</strong>, then refresh the page.'
);
document.getElementById('bspk').disabled=true;
}
};
recog.onend=()=>{
if(isRec){isRec=false;resetSpk();}
setMicStat(false,'');
};
}
function setMicStat(show,msg){
const el=document.getElementById('mstat');
const tx=document.getElementById('mstxt');
if(show){el.classList.add('show','listening');tx.textContent=msg;}
else{el.classList.remove('show','listening');}
}
function resetSpk(){
const b=document.getElementById('bspk');
b.disabled=false;b.classList.remove('listening');
setBadge('● Ready',false);
}
// ══════════════════════════════════════════════════════════
// SEQUENCE + PLAYBACK (unchanged)
// ══════════════════════════════════════════════════════════
function buildSeq(text){
const out=[];
for(const ch of text.toLowerCase().replace(/[^a-z\s]/g,'')){
if(ch===' ')out.push({isSpace:true,name:'SPACE',desc:'Pause between words'});
else if(ISL[ch])out.push({letter:ch,name:ch.toUpperCase(),pose:ISL[ch],desc:DESC[ch]||''});
}
return out;
}
async function play(seq){
if(aRun)return;
aRun=true;aCan=false;
setBadge('● Signing',true);setSt('act','Fingerspelling…');
for(let i=0;i<seq.length&&!aCan;i++){
const item=seq[i];
document.querySelectorAll('.li').forEach(e=>e.classList.remove('on'));
const el=document.getElementById('li'+i);
if(el){el.classList.add('on');el.scrollIntoView({behavior:'smooth',block:'nearest'});}
document.getElementById('gl').textContent=item.isSpace?'Β·':item.name;
document.getElementById('gd').textContent=item.desc;
if(item.isSpace){setPose(REST);await slp(500);}
else{setPose(item.pose);await slp(880);}
if(aCan)break;
document.getElementById('gb').style.width=((i+1)/seq.length*100)+'%';
ST.lt++;updSt();
}
aRun=false;
if(!aCan){
setBadge('βœ“ Done',false);setSt('act','Complete!');setPose(REST);
setTimeout(()=>{
document.getElementById('gb').style.width='0%';
document.getElementById('gl').textContent='🀟';
document.getElementById('gd').textContent='Type or speak to begin';
setBadge('● Ready',false);setSt('','Ready');
},2000);
}
}
// ══════════════════════════════════════════════════════════
// BUTTON ACTIONS (unchanged)
// ══════════════════════════════════════════════════════════
const ST={cv:0,lt:0,sp:0,wd:0};
function doConvert(){
if(aRun)doStop();
const t=document.getElementById('txt').value.trim();
if(!t){setSt('err','Please enter some text first');return;}
const seq=buildSeq(t);renderLL(seq);
ST.cv++;ST.wd+=t.split(/\s+/).length;updSt();
play(seq);
}
function doSpeak(){
if(!recog){setSt('err','Voice input not available β€” see instructions above');return;}
if(isRec)return;
isRec=true;
document.getElementById('bspk').disabled=true;
try{
recog.start();
}catch(e){
isRec=false;resetSpk();
setSt('err','Please wait a moment and try again');
}
}
function doStop(){
aCan=true;aRun=false;
if(isRec&&recog){try{recog.stop();}catch(e){}}
isRec=false;resetSpk();setMicStat(false,'');
document.getElementById('gb').style.width='0%';
setPose(REST);setBadge('⏹ Stopped',false);setSt('err','Stopped');
setTimeout(()=>{setSt('','Ready');setBadge('● Ready',false);},1400);
}
function doClear(){
doStop();
document.getElementById('txt').value='';
document.getElementById('cc').textContent='0/200';
renderLL([]);setSt('','Cleared');
setTimeout(()=>setSt('','Ready'),1200);
}
// ══════════════════════════════════════════════════════════
// UI HELPERS (unchanged)
// ══════════════════════════════════════════════════════════
function renderLL(seq){
const el=document.getElementById('ll');
if(!seq.length){el.innerHTML='<div class="es">Type text or speak<br>to see the sequence</div>';document.getElementById('scnt').textContent='0 letters';return;}
let n=0;
el.innerHTML=seq.map((x,i)=>{
if(x.isSpace)return`<div class="li sp" id="li${i}"><span class="lc">⎡</span><span class="lh">Word space</span></div>`;
n++;return`<div class="li" id="li${i}"><span class="lc">${x.name}</span><span class="lh">${x.desc}</span></div>`;
}).join('');
document.getElementById('scnt').textContent=n+' letter'+(n!==1?'s':'');
}
function setSt(cls,msg){
document.getElementById('stxt').textContent=msg;
document.getElementById('st').className='st'+(cls?' '+cls:'');
}
function setBadge(msg,blink){
const b=document.getElementById('badge');b.textContent=msg;
b.style.animation=blink?'pd .9s infinite':'none';
}
function updSt(){
[ST.cv,ST.lt,ST.sp,ST.wd].forEach((v,i)=>document.getElementById('s'+i).textContent=v);
}
function lerp(a,b,t){return a+(b-a)*t;}
function ease(t){return t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;}
function slp(ms){return new Promise(r=>setTimeout(r,ms));}
// ══════════════════════════════════════════════════════════
// FIX: Nav links β€” dynamic URL so redirect works on any domain
// Local dev β†’ uses explicit port numbers (:5000 / :5001)
// Cloud/Nginx β†’ uses path-based routing (/ and /avatar/)
// ══════════════════════════════════════════════════════════
(function(){
const proto = location.protocol;
const host = location.hostname;
const port = location.port;
if (port && port !== '80' && port !== '443') {
// Local development with explicit ports
document.getElementById('nav-gesture').href = proto + '//' + host + ':5000/';
document.getElementById('nav-avatar').href = proto + '//' + host + ':5001/';
} else {
// Cloud / Nginx β€” path-based routing
document.getElementById('nav-gesture').href = proto + '//' + host + '/';
document.getElementById('nav-avatar').href = proto + '//' + host + '/avatar/';
}
})();
// Keyboard shortcuts
document.addEventListener('keydown',e=>{
if((e.ctrlKey||e.metaKey)&&e.key==='Enter'){e.preventDefault();doConvert();}
if(e.key==='Escape')doStop();
if((e.ctrlKey||e.metaKey)&&e.key===' '){e.preventDefault();doSpeak();}
});
document.getElementById('txt').addEventListener('input',function(){
document.getElementById('cc').textContent=this.value.length+'/200';
});
// ══════════════════════════════════════════════════════════
// BOOT
// ══════════════════════════════════════════════════════════
window.addEventListener('DOMContentLoaded',()=>{
initThree();
initSpeech();
updSt();
setTimeout(()=>setPose(REST),400);
});
</script>
</body>
</html>