Spaces:
Runtime error
Runtime error
| <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 & hard-of-hearing individuals</li> | |
| <li>Inclusive education environments</li> | |
| <li>Public service & 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> |