speaker-verification
audio
nemotron
tyger
experimental
tyger-speaker-predictor / code /static /desk-robot3d.js
ChristophSchuhmann's picture
Publish audited Robby source and completed Nemotron speaker-prediction experiments
0efd0f1 verified
Raw History Blame Contribute Delete
16.2 kB
// Robby desk prototype. Original procedural geometry, Apache-2.0.
// Software 3D projection + Canvas 2D: no WebGL, CDN, mesh download or GPU needed.
// Millimetre proportions follow the supplied 20 Oct 2026 proposal, NOT buildable CAD.
import {clamp,pose,mix} from './emotions.js';
import {geometry,normalizeControls} from './rig.js';
export const DIMENSIONS=Object.freeze({headDiameter:190,headDepth:110,baseDiameter:220,
baseHeight:70,displayDiameter:150,activeDisplayDiameter:127.6,mountingPitch:75,
cameraAperture:9,totalHeight:315,tiltPivotHeight:118,panLimit:90,tiltLimit:15,roll:0});
const PI=Math.PI,rad=x=>x*PI/180;
const add=(a,b)=>a.map((v,i)=>v+b[i]),sub=(a,b)=>a.map((v,i)=>v-b[i]);
const dot=(a,b)=>a.reduce((s,v,i)=>s+v*b[i],0);
const cross=(a,b)=>[a[1]*b[2]-a[2]*b[1],a[2]*b[0]-a[0]*b[2],a[0]*b[1]-a[1]*b[0]];
const unit=a=>{const n=Math.hypot(...a)||1;return a.map(v=>v/n);};
const ry=(p,a)=>[p[0]*Math.cos(a)+p[2]*Math.sin(a),p[1],-p[0]*Math.sin(a)+p[2]*Math.cos(a)];
const rx=(p,a)=>[p[0],p[1]*Math.cos(a)-p[2]*Math.sin(a),p[1]*Math.sin(a)+p[2]*Math.cos(a)];
const WHITE=[242,241,237],DARK=[37,43,50],BLACK=[19,25,32],BLUE=[138,211,252];
function lathe(profile,axis='y',group='base',center=[0,0,0],color=WHITE,n=32,part=''){
const points=[],polys=[];
for(const [h,r] of profile)for(let i=0;i<n;i++){
const a=2*PI*i/n,c=Math.cos(a)*r,s=Math.sin(a)*r;
points.push(add(axis==='y'?[c,h,s]:axis==='z'?[c,s,h]:[h,c,s],center));
}
for(let j=0;j<profile.length-1;j++)for(let i=0;i<n;i++){
const a=j*n+i,b=(j+1)*n+i,c=(j+1)*n+(i+1)%n,d=j*n+(i+1)%n;
polys.push(axis==='y'?[a,b,c,d]:[a,d,c,b]);
}
return {points,polys,group,color,part};
}
function box(center,size,group='base',color=WHITE,part=''){
const [x,y,z]=size.map(v=>v/2),points=[[-x,-y,-z],[x,-y,-z],[x,y,-z],[-x,y,-z],[-x,-y,z],[x,-y,z],[x,y,z],[-x,y,z]].map(p=>add(p,center));
return {points,polys:[[4,5,6,7],[3,2,1,0],[0,4,7,3],[5,1,2,6],[3,7,6,2],[0,1,5,4]],group,color,part};
}
export function buildDeskGeometry(){
const meshes=[
lathe([[0,0],[0,99],[3,108],[9,110],[12,110],[12,0]],'y','base',[0,0,0],DARK,40,'bottom plate'),
lathe([[9,107],[15,110],[49,106],[61,100],[69,87],[70,50]],'y','base',[0,0,0],WHITE,48,'base shell'),
lathe([[69,0],[69,55],[73,55],[76,51],[77,0]],'y','base',[0,0,0],DARK,40,'pan bearing'),
lathe([[74,49],[76,49],[77,48]],'y','base',[0,0,0],BLUE,40,'pan LED'),
lathe([[76,0],[76,42],[88,42],[92,34],[92,0]],'y','pan',[0,0,0],BLACK,32,'yaw stage'),
box([0,104,-2],[40,35,37],'pan',DARK,'servo carrier'),
box([-29,107,0],[12,43,42],'pan',WHITE,'left carrier'),box([29,107,0],[12,43,42],'pan',WHITE,'right carrier'),
lathe([[-36,0],[-36,23],[36,23],[36,0]],'x','pan',[0,118,0],DARK,32,'pitch axle'),
lathe([[-42,0],[-42,25],[-35,25],[-35,0]],'x','pan',[0,118,0],WHITE,32,'pitch cap left'),
lathe([[35,0],[35,25],[42,25],[42,0]],'x','pan',[0,118,0],WHITE,32,'pitch cap right'),
lathe([[-44,15.5],[-44,18],[-43,18]],'x','pan',[0,118,0],BLUE,32,'pitch indicator'),
lathe([[43,18],[44,18],[44,15.5]],'x','pan',[0,118,0],BLUE,32,'pitch indicator'),
box([-22,-96,0],[10,29,32],'head',WHITE,'tilt adapter'),box([22,-96,0],[10,29,32],'head',WHITE,'tilt adapter'),
lathe([[-60,0],[-57,25],[-48,55],[-34,78],[-15,91],[10,95],[29,93],[41,88],[47,81],[49,75.5]],'z','head',[0,0,0],WHITE,48,'rear shell'),
lathe([[44,86],[49,83],[51,75.5],[46,75.5]],'z','trim',[0,0,0],WHITE,48,'front trim'),
lathe([[48,75],[53,75],[54,63.8],[50,63.8],[48,75]],'z','display',[0,0,0],BLACK,48,'LCD bezel'),
lathe([[-100,0],[-100,22],[-98,24],[-95,22],[-95,0]],'x','head',[0,-3,-3],DARK,32,'head cap left'),
lathe([[95,0],[95,22],[98,24],[100,22],[100,0]],'x','head',[0,-3,-3],DARK,32,'head cap right'),
lathe([[-101,16],[-101,18],[-100.3,18]],'x','head',[0,-3,-3],BLUE,32,'left light ring'),
lathe([[100.3,18],[101,18],[101,16]],'x','head',[0,-3,-3],BLUE,32,'right light ring'),
lathe([[32,0],[32,4.5],[36,4.5],[36,0]],'z','head',[0,86,14],BLACK,24,'camera at twelve oclock'),
lathe([[36.1,0],[36.1,2],[36.5,0]],'z','head',[0,86,14],[42,78,103],20,'camera lens'),
box([0,31,106.9],[42,4.5,1.6],'base',BLUE,'status light'),
box([0,17,109],[5,1.5,1],'base',DARK,'USB microphone opening'),
box([0,34,-100],[18,9,6],'base',DARK,'rear power port'),
box([0,0,29],[65,48,8],'mount',[38,91,110],'controller board')
];
meshes.push(box([-12,-18,-59],[15,7,5],'head',DARK,'rear HDMI opening'),
box([12,-18,-59],[8,5,5],'head',DARK,'rear USB-C opening'));
for(const x of [-37.5,37.5])for(const y of [-37.5,37.5]){
meshes.push(lathe([[30,0],[30,5],[47,5],[47,0]],'z','mount',[x,y,0],[180,183,182],16,'M4 standoff 75 x 75'));
}
for(let i=-3;i<=3;i++)meshes.push(box([i*8,41,-102],[3,14,2],'base',DARK,'vent'));
return meshes;
}
function pathCurve(c,points,closed=false){
if(!points.length)return;c.beginPath();c.moveTo(...points[0]);
const at=j=>points[closed?(j+points.length)%points.length:Math.max(0,Math.min(points.length-1,j))];
for(let i=0;i<(closed?points.length:points.length-1);i++){
const p=at(i-1),q=at(i),r=at(i+1),s=at(i+2);
c.bezierCurveTo(q[0]+(r[0]-p[0])/6,q[1]+(r[1]-p[1])/6,r[0]-(s[0]-q[0])/6,r[1]-(s[1]-q[1])/6,...r);
}if(closed)c.closePath();
}
function paintFace(canvas,p,controls,showPoints){
const c=canvas.getContext('2d'),n=canvas.width;c.setTransform(1,0,0,1,0,0);c.clearRect(0,0,n,n);
const bg=c.createRadialGradient(n*.35,n*.25,0,n*.5,n*.5,n*.65);bg.addColorStop(0,'#fff');bg.addColorStop(1,'#e9effa');
c.fillStyle=bg;c.beginPath();c.arc(n/2,n/2,n/2,0,2*PI);c.fill();c.save();c.clip();
const s=n/450;c.setTransform(s,0,0,s,n/2-400*s,n/2-275*s);
const g=geometry(p,controls),a=p.actions;
for(let i=0;i<2;i++){
pathCurve(c,g.eyes[i],true);c.fillStyle='#182129';c.fill();c.save();c.clip();
const center=g.pupils[i][0];c.fillStyle='#070c12';c.beginPath();c.ellipse(center[0],center[1],28*a.pupil_scale,33*a.pupil_scale,0,0,2*PI);c.fill();
c.fillStyle='#fff';c.beginPath();c.arc(center[0]-8,center[1]-15,6,0,2*PI);c.fill();c.restore();
pathCurve(c,g.brows[i]);c.strokeStyle='#11171d';c.lineWidth=8;c.lineCap='round';c.stroke();
if(p.blush>.08){c.fillStyle=`rgba(233,155,148,${p.blush*.3})`;c.beginPath();c.ellipse(i?535:265,326,30,15,0,0,2*PI);c.fill();}
}
pathCurve(c,g.mouth,true);c.fillStyle='#141b24';c.fill();c.save();c.clip();
if(p.teeth>.1){c.fillStyle=`rgba(255,255,255,${p.teeth})`;c.fillRect(315,339,170,12);}
if(p.tongue>.1){c.fillStyle=`rgba(224,135,148,${p.tongue})`;c.beginPath();c.ellipse(400,382,28,11,0,0,2*PI);c.fill();}c.restore();
if(a.nose_lift>.3){pathCurve(c,g.nose);c.lineWidth=2;c.strokeStyle='#6e7882';c.stroke();}
if(showPoints){c.fillStyle='#26a5b9';for(const [x,y] of g.points.slice(12,89)){c.beginPath();c.arc(x,y,2,0,2*PI);c.fill();}}
c.restore();return g;
}
function affineImage(ctx,texture,screen,uv){
const [[x1,y1],[x2,y2],[x3,y3]]=screen,[[u1,v1],[u2,v2],[u3,v3]]=uv;
const den=(u1-u3)*(v2-v3)-(u2-u3)*(v1-v3);if(Math.abs(den)<1e-9)return;
const a=((x1-x3)*(v2-v3)-(x2-x3)*(v1-v3))/den,c=((x2-x3)*(u1-u3)-(x1-x3)*(u2-u3))/den;
const b=((y1-y3)*(v2-v3)-(y2-y3)*(v1-v3))/den,d=((y2-y3)*(u1-u3)-(y1-y3)*(u2-u3))/den;
// Slightly overlap the clipping edges. Antialiased adjacent triangle clips
// otherwise leave dark hairline cracks across the LCD on software canvases.
const center=[(x1+x2+x3)/3,(y1+y2+y3)/3];
const expanded=screen.map(p=>{const dx=p[0]-center[0],dy=p[1]-center[1],n=Math.hypot(dx,dy)||1;return [p[0]+dx/n,p[1]+dy/n];});
ctx.save();ctx.beginPath();ctx.moveTo(...expanded[0]);ctx.lineTo(...expanded[1]);ctx.lineTo(...expanded[2]);ctx.closePath();ctx.clip();
ctx.transform(a,b,c,d,x3-a*u3-c*v3,y3-b*u3-d*v3);ctx.drawImage(texture,0,0);ctx.restore();
}
export function createDeskRobotRig(container){
const canvas=document.createElement('canvas');canvas.className='desk-robot-canvas';canvas.setAttribute('aria-label','Three-dimensional two-axis desk robot; drag to orbit');
canvas.setAttribute('role','img');container.replaceChildren(canvas);
const ctx=canvas.getContext('2d',{alpha:true}),faceCanvas=document.createElement('canvas');faceCanvas.width=faceCanvas.height=512;
if(!ctx)throw Error('This browser needs Canvas 2D support for the desk robot.');
const meshes=buildDeskGeometry(),state={pan:0,tilt:0,roll:0,view:rad(12),elevation:rad(7),distance:650,
manual:false,manualPan:0,manualTilt:0,exploded:0,blueprint:false,tour:false,fps:30};
let smooth=pose(),controls=normalizeControls(),last={},showPoints=false,lastRender=-1,renderCount=0,totalRenderMs=0,durations=[],disposed=false;
let width=500,height=490,drag=null;
const ro=new ResizeObserver(()=>{const rect=container.getBoundingClientRect();width=Math.max(1,rect.width);height=Math.max(1,rect.height);
const ratio=Math.min(devicePixelRatio||1,1.5);canvas.width=Math.round(width*ratio);canvas.height=Math.round(height*ratio);canvas.style.width=width+'px';canvas.style.height=height+'px';});ro.observe(container);
function transform(p,group){
let q=p;
if(['head','trim','display','mount'].includes(group)){
q=[p[0],p[1],p[2]+(group==='trim'?state.exploded*40:group==='display'?state.exploded*23:group==='mount'?state.exploded*10:-state.exploded*18)];
q=add(rx(add(q,[0,102,0]),-rad(state.tilt)),[0,118,0]);
}
if(group!=='base')q=add(ry(sub(q,[0,80,0]),rad(state.pan)),[0,80,0]);
if(group==='base'&&state.exploded)q=[q[0],q[1]-state.exploded*(q[1]<13?22:0),q[2]];
return q;
}
function view(p){return rx(ry(sub(p,[0,155,0]),-state.view),state.elevation);}
function project(p){const q=view(p),depth=Math.max(100,state.distance-q[2]),s=Math.min(width/360,height/380)*650/depth;
return [width*.5+q[0]*s,height*.52-q[1]*s,depth];}
function facing(world,normal){const p=view(world),n=rx(ry(normal,-state.view),state.elevation);return dot(n,[-p[0],-p[1],state.distance-p[2]])>0;}
const light=unit([-.5,.8,.75]);
function render(time){
const started=performance.now(),ratio=canvas.width/width;ctx.setTransform(ratio,0,0,ratio,0,0);ctx.clearRect(0,0,width,height);
const foot=project([0,0,0]);ctx.save();ctx.translate(foot[0],foot[1]);ctx.scale(1,.18);
const shadow=ctx.createRadialGradient(0,0,5,0,0,width*.29);shadow.addColorStop(0,'#41516b30');shadow.addColorStop(1,'#41516b00');ctx.fillStyle=shadow;ctx.beginPath();ctx.arc(0,0,width*.29,0,2*PI);ctx.fill();ctx.restore();
if(state.blueprint){ctx.strokeStyle='#93a8b72b';ctx.lineWidth=.7;for(let i=-150;i<=150;i+=50){for(const line of [[[i,0,-150],[i,0,150]],[[-150,0,i],[150,0,i]]]){ctx.beginPath();ctx.moveTo(...project(line[0]).slice(0,2));ctx.lineTo(...project(line[1]).slice(0,2));ctx.stroke();}}}
const queue=[];
for(const mesh of meshes){
const world=mesh.points.map(p=>transform(p,mesh.group)),screen=world.map(project);
for(const indices of mesh.polys){
const p=indices.map(i=>world[i]);let normal=[0,0,0];
for(let i=1;i<p.length-1;i++){normal=cross(sub(p[i],p[0]),sub(p[i+1],p[0]));if(Math.hypot(...normal)>1e-5)break;}
if(Math.hypot(...normal)<1e-5||!facing(p[0],normal))continue;normal=unit(normal);
const lum=.68+.31*Math.max(0,dot(normal,light)),color=mesh.color.map(v=>Math.round(clamp(v*lum,0,255)));
queue.push({depth:indices.reduce((s,i)=>s+screen[i][2],0)/indices.length,points:indices.map(i=>screen[i]),color:`rgb(${color.join(',')})`});
}
}
const front=transform([0,0,54],'display'),normal=sub(transform([0,0,55],'display'),front);
if(facing(front,normal))for(let y=0;y<4;y++)for(let x=0;x<4;x++){
const v=[[x,y],[x+1,y],[x+1,y+1],[x,y+1]];
const w=v.map(([u,t])=>transform([-63.8+u*31.9,63.8-t*31.9,54.2],'display')),s=w.map(project);
for(const ids of [[0,1,2],[0,2,3]])queue.push({depth:ids.reduce((a,i)=>a+s[i][2],0)/3,points:ids.map(i=>s[i]),uv:ids.map(i=>v[i].map(k=>k*128))});
}
queue.sort((a,b)=>b.depth-a.depth);
for(const face of queue){
if(face.uv){affineImage(ctx,faceCanvas,face.points,face.uv);continue;}
ctx.beginPath();ctx.moveTo(face.points[0][0],face.points[0][1]);for(const p of face.points.slice(1))ctx.lineTo(p[0],p[1]);ctx.closePath();
ctx.fillStyle=face.color;ctx.fill();ctx.strokeStyle=face.color;ctx.lineWidth=.45;ctx.stroke();
}
if(showPoints||state.blueprint){
for(const [a,b,color,label] of [[[0,70,0],[0,165,0],'#328ba7','PAN'],[[-67,118,0],[67,118,0],'#be8150','TILT']]){
ctx.strokeStyle=color;ctx.lineWidth=1.5;ctx.setLineDash([4,4]);ctx.beginPath();ctx.moveTo(...project(transform(a,'pan')).slice(0,2));ctx.lineTo(...project(transform(b,'pan')).slice(0,2));ctx.stroke();ctx.setLineDash([]);
const p=project(transform(b,'pan'));ctx.fillStyle=color;ctx.font='10px system-ui';ctx.fillText(label,p[0]+4,p[1]-4);
}
}
const ms=performance.now()-started;renderCount++;totalRenderMs+=ms;durations.push(ms);if(durations.length>120)durations.shift();
const sorted=[...durations].sort((a,b)=>a-b);
last={...last,pan:state.pan,tilt:state.tilt,roll:0,dimensions:DIMENSIONS,renderer:'Canvas 2D software 3D · no WebGL',
meshFaces:meshes.reduce((n,m)=>n+m.polys.length,0),visibleFaces:queue.length,renderCount,
renderMs:ms,renderP50Ms:sorted[Math.floor(sorted.length*.5)],renderP95Ms:sorted[Math.floor(sorted.length*.95)],
meanRenderMs:totalRenderMs/renderCount,viewDegrees:state.view*180/PI,exploded:state.exploded,expression:smooth.slug};
}
canvas.style.touchAction='none';canvas.style.cursor='grab';
canvas.onpointerdown=e=>{drag=[e.clientX,e.clientY,state.view,state.elevation];canvas.setPointerCapture(e.pointerId);canvas.style.cursor='grabbing';};
canvas.onpointermove=e=>{if(drag){state.view=drag[2]+(e.clientX-drag[0])*.008;state.elevation=clamp(drag[3]+(e.clientY-drag[1])*.005,rad(-10),rad(40));}};
canvas.onpointerup=canvas.onpointercancel=()=>{drag=null;canvas.style.cursor='grab';};
canvas.addEventListener('wheel',e=>{e.preventDefault();state.distance=clamp(state.distance+e.deltaY*.4,450,950);},{passive:false});
canvas.tabIndex=0;canvas.onkeydown=e=>{if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home'].includes(e.key)){e.preventDefault();if(e.key==='Home'){state.view=rad(12);state.elevation=rad(7);}else if(e.key==='ArrowLeft')state.view-=rad(10);else if(e.key==='ArrowRight')state.view+=rad(10);else state.elevation=clamp(state.elevation+rad(e.key==='ArrowUp'?5:-5),rad(-10),rad(40));}};
return {state,idlePose(){const p=pose();p.actions.brow_arch=.42;p.actions.smile=.62;return p;},showKeypoints(on){showPoints=!!on;},
setManual(pan,tilt,on=true){state.manual=on;state.manualPan=clamp(+pan,-90,90);state.manualTilt=clamp(+tilt,-15,15);},
setExploded(value){state.exploded=clamp(+value,0,1);},setBlueprint(on){state.blueprint=!!on;},setTour(on){state.tour=!!on;},
center(){state.view=rad(12);state.elevation=rad(7);state.distance=650;state.manual=false;state.tour=false;},
update(input={},dtMs=16.7,timeMs=0){if(disposed)return last;
const dt=clamp(dtMs,0,100),alpha=1-Math.exp(-dt/160),target=input.pose||pose();smooth={...mix(smooth,target,alpha),slug:target.slug};
const c=normalizeControls(input);for(const key in controls)controls[key]+=alpha*(c[key]-controls[key]);
const gesture=smooth.gesture;
const panTarget=state.manual?state.manualPan:state.tour?Math.sin(timeMs/1800)*45:clamp(controls.x*52+gesture[0]*.10+Math.sin(timeMs/1700)*gesture[6]*7,-70,70);
const tiltTarget=state.manual?state.manualTilt:state.tour?Math.sin(timeMs/2700)*12:clamp(-controls.y*15-gesture[1]*.55,-15,15);
state.pan=clamp(state.pan+clamp((panTarget-state.pan)*alpha,-45*dt/1000,45*dt/1000),-90,90);
state.tilt=clamp(state.tilt+clamp((tiltTarget-state.tilt)*alpha,-25*dt/1000,25*dt/1000),-15,15);
const clock=timeMs||performance.now();
const interval=1000/state.fps,elapsed=clock-lastRender;
if(dtMs<100&&elapsed<interval)return last;
// Carry frame-time remainder: discarding it often turns a 30-fps budget
// into 20 fps on a slightly jittery 60-Hz software/compositor clock.
lastRender=dtMs>=100?clock:clock-elapsed%interval;
const g=paintFace(faceCanvas,smooth,controls,showPoints);last={...last,pose:smooth,geometry:g};render(timeMs);return last;
},snapshot(){return last;},reset(){smooth=pose();controls=normalizeControls();this.center();this.update({},1000,0);},
dispose(){disposed=true;ro.disconnect();canvas.remove();}
};
}