3D toplanti oynatici: orijinal + Turkce dublaj, zaman cizelgesi

- Three.js 3D oda, 7 kisi (6 oturan + ayakta kayinvalide)
- Orijinal/Turkce ses gecisi, konusmaci vurgusu, cift-dil altyazi
- 299 blok Turkce dublaj (7 ElevenLabs sesi, cinsiyet/yas uyumlu)
- nginx + Dockerfile (Coolify deploy)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
yilsem
2026-07-04 04:26:09 +03:00
co-authored by Claude Opus 4.8
commit 5037c7114f
12 changed files with 1463 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Toplantı 3D — Orijinal / Türkçe</title>
<style>
:root{ --bg:#0d1117; --panel:#161b22cc; --border:#30363d; --txt:#e6edf3; --mut:#8b949e; --accent:#2f81f7; }
*{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--bg);color:var(--txt);font-family:"Segoe UI",system-ui,sans-serif;overflow:hidden}
#scene{position:fixed;inset:0}
#topbar{position:fixed;top:0;left:0;right:0;height:52px;display:flex;align-items:center;gap:14px;
padding:0 16px;background:linear-gradient(#0d1117ee,#0d111700);z-index:10;pointer-events:none}
#topbar h1{font-size:15px;margin:0;font-weight:600;pointer-events:auto}
#topbar .sub{font-size:12px;color:var(--mut);font-weight:400}
.spacer{flex:1}
.toggle{pointer-events:auto;display:flex;background:#21262d;border:1px solid var(--border);border-radius:20px;overflow:hidden}
.toggle button{background:none;border:0;color:var(--mut);padding:7px 15px;font-size:13px;cursor:pointer;font-weight:600}
.toggle button.on{background:var(--accent);color:#fff}
.toggle button.on.trm{background:#8957e5}
#legend{position:fixed;top:60px;left:12px;z-index:10;background:var(--panel);border:1px solid var(--border);
border-radius:10px;padding:10px 12px;font-size:11.5px;backdrop-filter:blur(6px);max-width:220px}
#legend .row{display:flex;align-items:center;gap:7px;margin-top:5px}
#legend .dot{width:11px;height:11px;border-radius:50%;flex:none}
#subtitle{position:fixed;left:50%;transform:translateX(-50%);bottom:148px;width:min(780px,92vw);z-index:10;
background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:12px 16px;backdrop-filter:blur(8px);
text-align:center;transition:opacity .2s}
#subtitle .who{font-weight:700;font-size:13px;margin-bottom:5px}
#subtitle .orig{font-size:14px;line-height:1.45}
#subtitle .tr{font-size:14px;line-height:1.45;color:var(--mut);margin-top:4px}
#subtitle.trmode .orig{color:var(--mut);font-size:12.5px}
#subtitle.trmode .tr{color:var(--txt)}
.badge{display:inline-block;font-size:10px;background:#3d2a00;color:#e0b400;border:1px solid #6b4a00;
border-radius:6px;padding:1px 6px;margin-left:6px;vertical-align:middle}
#controls{position:fixed;left:0;right:0;bottom:0;z-index:10;background:linear-gradient(#0d111700,#0d1117ee 42%);padding:12px 18px 14px}
#tl{width:100%;height:34px;display:block;cursor:pointer;border-radius:6px}
#bar{display:flex;align-items:center;gap:12px;margin-top:8px}
.btn{background:#21262d;border:1px solid var(--border);color:var(--txt);width:40px;height:40px;border-radius:50%;
font-size:15px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.btn:hover{border-color:var(--accent)}
.btn.play{width:46px;height:46px;font-size:17px;background:var(--accent);border-color:var(--accent)}
#time{font-variant-numeric:tabular-nums;font-size:13px;color:var(--mut);min-width:190px}
#hint{font-size:11px;color:var(--mut)}
@media(max-width:640px){#subtitle{bottom:138px}#legend{display:none}#hint{display:none}}
</style>
</head>
<body>
<div id="scene"></div>
<div id="topbar">
<h1>Toplantı — 3D Oda <span class="sub">· uzlaşma görüşmesi · 09:0031:13</span></h1>
<div class="spacer"></div>
<div class="toggle" id="langToggle">
<button data-m="orig" class="on">🔊 Orijinal</button>
<button data-m="tr">🇹🇷 Türkçe</button>
</div>
</div>
<div id="legend"><b>Masadaki kişiler</b><div id="legendRows"></div></div>
<div id="subtitle">
<div class="who" id="subWho"></div>
<div class="orig" id="subOrig">Başlamak için ▶ tuşuna bas</div>
<div class="tr" id="subTr"></div>
</div>
<div id="controls">
<canvas id="tl"></canvas>
<div id="bar">
<button class="btn play" id="play"></button>
<button class="btn" id="prev" title="Önceki konuşma"></button>
<button class="btn" id="next" title="Sonraki konuşma"></button>
<span id="time">00:00 / 00:00</span>
<div class="spacer"></div>
<span id="hint">Zaman çizelgesine tıkla/sürükle · fareyle sahneyi döndür · çift tık kişi→ilk sözü</span>
</div>
</div>
<audio id="ao" src="toplanti_orijinal.mp3" preload="metadata"></audio>
<audio id="at" preload="metadata"></audio>
<script src="three.min.js"></script>
<script src="OrbitControls.js"></script>
<script src="data.js"></script>
<script>
const ROLES=MEET.roles, TL=MEET.TL, OFFSET=540;
const POS3D={
"Kaan":{x:-1.6,z:2.3,stand:false}, "Marissa":{x:1.6,z:2.3,stand:false},
"Dolandırıcı (D-)":{x:-1.6,z:-2.3,stand:false}, "Uzlaşmacı (U)":{x:0.4,z:-2.3,stand:false},
"Yaşlı adam (V)":{x:-3.7,z:0,stand:false}, "Lider Uzlaşmacı (L.U)":{x:3.7,z:0,stand:false},
"Kayınvalide":{x:3.3,z:-2.6,stand:true}
};
const SHORT={"Kaan":"Kaan","Marissa":"Marissa (eş)","Dolandırıcı (D-)":"Dolandırıcı","Uzlaşmacı (U)":"Uzlaşmacı",
"Yaşlı adam (V)":"Yaşlı adam","Lider Uzlaşmacı (L.U)":"Lider Uzlaşmacı","Kayınvalide":"Kayınvalide ⬆"};
function roundRect(g,x,y,w,h,r){g.beginPath();g.moveTo(x+r,y);g.arcTo(x+w,y,x+w,y+h,r);g.arcTo(x+w,y+h,x,y+h,r);g.arcTo(x,y+h,x,y,r);g.arcTo(x,y,x+w,y,r);g.closePath();}
function makeLabel(text,color,scale){ scale=scale||1;
const c=document.createElement('canvas'); c.width=256; c.height=64; const g=c.getContext('2d');
g.fillStyle="rgba(13,17,23,0.85)"; roundRect(g,4,10,248,44,10); g.fill();
g.strokeStyle=color; g.lineWidth=2; roundRect(g,4,10,248,44,10); g.stroke();
g.fillStyle=color; g.font="bold 24px Segoe UI"; g.textAlign="center"; g.textBaseline="middle"; g.fillText(text,128,33);
const spr=new THREE.Sprite(new THREE.SpriteMaterial({map:new THREE.CanvasTexture(c),transparent:true,depthTest:false}));
spr.scale.set(1.9*scale,0.48*scale,1); return spr;
}
const scene=new THREE.Scene();
scene.background=new THREE.Color(0x0d1117);
scene.fog=new THREE.Fog(0x0d1117,16,34);
const camera=new THREE.PerspectiveCamera(48,innerWidth/innerHeight,0.1,100);
camera.position.set(0,6.2,9.6);
const renderer=new THREE.WebGLRenderer({antialias:true});
renderer.setSize(innerWidth,innerHeight); renderer.setPixelRatio(Math.min(devicePixelRatio,2));
document.getElementById('scene').appendChild(renderer.domElement);
const controls=new THREE.OrbitControls(camera,renderer.domElement);
controls.enableDamping=true; controls.dampingFactor=0.08; controls.target.set(0,1,0);
controls.maxPolarAngle=Math.PI*0.49; controls.minDistance=5; controls.maxDistance=22;
scene.add(new THREE.AmbientLight(0xffffff,0.55));
const key=new THREE.DirectionalLight(0xffffff,0.9); key.position.set(6,12,8); scene.add(key);
const fill=new THREE.DirectionalLight(0x88aaff,0.35); fill.position.set(-8,6,-6); scene.add(fill);
const floor=new THREE.Mesh(new THREE.PlaneGeometry(40,40),new THREE.MeshStandardMaterial({color:0x1c2230,roughness:0.95}));
floor.rotation.x=-Math.PI/2; scene.add(floor);
const rug=new THREE.Mesh(new THREE.CircleGeometry(7.5,48),new THREE.MeshStandardMaterial({color:0x232a3a,roughness:1}));
rug.rotation.x=-Math.PI/2; rug.position.y=0.01; scene.add(rug);
const TABLE_H=1.0;
const tableTop=new THREE.Mesh(new THREE.BoxGeometry(6.4,0.16,3.2),new THREE.MeshStandardMaterial({color:0x6b4a2b,roughness:0.6}));
tableTop.position.y=TABLE_H; scene.add(tableTop);
[[2.9,1.4],[-2.9,1.4],[2.9,-1.4],[-2.9,-1.4]].forEach(function(p){
const leg=new THREE.Mesh(new THREE.CylinderGeometry(0.11,0.11,TABLE_H,12),new THREE.MeshStandardMaterial({color:0x4a3220}));
leg.position.set(p[0],TABLE_H/2,p[1]); scene.add(leg);
});
const rec=new THREE.Mesh(new THREE.BoxGeometry(0.5,0.12,0.28),new THREE.MeshStandardMaterial({color:0xd21f1f,emissive:0x330000,roughness:0.4}));
rec.position.set(-1.3,TABLE_H+0.14,1.0); scene.add(rec);
const recLbl=makeLabel("🔴 KAYIT","#ff6666",0.55); recLbl.position.set(-1.3,TABLE_H+0.55,1.0); scene.add(recLbl);
const SKIN=0xe8b98c, chars={};
function makeChar(role){
const p=POS3D[role], col=new THREE.Color(ROLES[role].renk);
const grp=new THREE.Group();
const cloth=new THREE.MeshStandardMaterial({color:col,roughness:0.7});
const skin=new THREE.MeshStandardMaterial({color:SKIN,roughness:0.6});
const hair=new THREE.MeshStandardMaterial({color:0x2a2018,roughness:0.9});
let headY;
if(p.stand){
const legL=new THREE.Mesh(new THREE.CylinderGeometry(0.1,0.1,0.85,10),cloth); legL.position.set(-0.13,0.42,0);
const legR=legL.clone(); legR.position.x=0.13; grp.add(legL,legR);
const torso=new THREE.Mesh(new THREE.CylinderGeometry(0.22,0.32,0.85,14),cloth); torso.position.y=1.28; grp.add(torso);
const head=new THREE.Mesh(new THREE.SphereGeometry(0.19,20,20),skin); head.position.y=1.86; grp.add(head);
const h=new THREE.Mesh(new THREE.SphereGeometry(0.205,18,18,0,Math.PI*2,0,Math.PI*0.62),hair); h.position.y=1.9; grp.add(h);
headY=1.86;
} else {
const seat=new THREE.Mesh(new THREE.BoxGeometry(0.56,0.1,0.56),new THREE.MeshStandardMaterial({color:0x394150,roughness:0.8}));
seat.position.set(0,0.5,-0.08); grp.add(seat);
const back=new THREE.Mesh(new THREE.BoxGeometry(0.56,0.62,0.08),new THREE.MeshStandardMaterial({color:0x394150,roughness:0.8}));
back.position.set(0,0.8,-0.34); grp.add(back);
const thigh=new THREE.Mesh(new THREE.BoxGeometry(0.34,0.14,0.42),cloth); thigh.position.set(0,0.6,0.14); grp.add(thigh);
const shinL=new THREE.Mesh(new THREE.CylinderGeometry(0.08,0.08,0.5,10),cloth); shinL.position.set(-0.12,0.3,0.32); grp.add(shinL);
const shinR=shinL.clone(); shinR.position.x=0.12; grp.add(shinR);
const torso=new THREE.Mesh(new THREE.CylinderGeometry(0.2,0.3,0.62,14),cloth); torso.position.y=0.98; grp.add(torso);
const head=new THREE.Mesh(new THREE.SphereGeometry(0.185,20,20),skin); head.position.y=1.44; grp.add(head);
const h=new THREE.Mesh(new THREE.SphereGeometry(0.2,18,18,0,Math.PI*2,0,Math.PI*0.6),hair); h.position.y=1.48; grp.add(h);
headY=1.44;
}
const ring=new THREE.Mesh(new THREE.TorusGeometry(0.55,0.06,10,32),new THREE.MeshStandardMaterial({color:col,emissive:col,emissiveIntensity:1.4}));
ring.rotation.x=-Math.PI/2; ring.position.y=0.04; ring.visible=false; grp.add(ring);
const lbl=makeLabel(SHORT[role],"#"+col.getHexString()); lbl.position.y=headY+0.55; grp.add(lbl);
grp.position.set(p.x,0,p.z); grp.rotation.y=Math.atan2(-p.x,-p.z);
scene.add(grp); chars[role]={grp:grp,ring:ring,active:false};
}
Object.keys(POS3D).forEach(makeChar);
// ---- audio + timeline ----
const ao=document.getElementById('ao'), at=document.getElementById('at');
let mode='orig';
const TL_TR=MEET.TL_tr||null;
if(MEET.trAudio){ at.src=MEET.trAudio; }
const TR_READY=!!MEET.trAudio && !!TL_TR;
function A(){ return (mode==='tr'&&TR_READY)?at:ao; }
function curTL(){ return (mode==='tr'&&TR_READY)?TL_TR:TL; }
const lr=document.getElementById('legendRows');
Object.keys(POS3D).forEach(function(r){
const d=document.createElement('div'); d.className='row';
d.innerHTML='<span class="dot" style="background:'+ROLES[r].renk+'"></span>'+SHORT[r]+(POS3D[r].stand?' <span style="color:#8b949e">(ayakta)</span>':'');
lr.appendChild(d);
});
const tl=document.getElementById('tl'), tg=tl.getContext('2d');
function fitTL(){ tl.width=tl.clientWidth*devicePixelRatio; tl.height=34*devicePixelRatio; tg.setTransform(devicePixelRatio,0,0,devicePixelRatio,0,0); drawTL(); }
function drawTL(){
const T=curTL(), dur=A().duration||T[T.length-1].e, W=tl.clientWidth, H=34;
tg.clearRect(0,0,W,H);
tg.fillStyle="#21262d"; roundRect(tg,0,11,W,12,6); tg.fill();
tg.globalAlpha=0.85;
T.forEach(function(b){ const x=b.t/dur*W, w=Math.max(1,(b.e-b.t)/dur*W); tg.fillStyle=(ROLES[b.rol]&&ROLES[b.rol].renk)||"#888"; tg.fillRect(x,11,w,12); });
tg.globalAlpha=1;
const ph=(A().currentTime/dur)*W; tg.fillStyle="#fff"; tg.fillRect(ph-1,6,2,22); tg.beginPath(); tg.arc(ph,6,4,0,7); tg.fill();
}
function seekTL(e){ const r=tl.getBoundingClientRect(); const x=(e.clientX-r.left)/r.width; const a=A(); a.currentTime=Math.max(0,Math.min(1,x))*(a.duration||1); update(); }
let drag=false;
tl.addEventListener('mousedown',function(e){drag=true;seekTL(e);});
addEventListener('mousemove',function(e){if(drag)seekTL(e);});
addEventListener('mouseup',function(){drag=false;});
function curIndex(){ const t=A().currentTime, T=curTL(); for(let i=0;i<T.length;i++){ if(t>=T[i].t&&t<T[i].e) return i; } return t<T[0].t?-1:T.length-1; }
let lastRole=null;
function highlight(role){ if(role===lastRole)return; lastRole=role; for(const r in chars){ const on=(r===role); chars[r].active=on; chars[r].ring.visible=on; } }
const fmt=function(s){s=Math.max(0,s|0);return String(s/60|0).padStart(2,'0')+':'+String(s%60).padStart(2,'0');};
const sub=document.getElementById('subtitle'),sW=document.getElementById('subWho'),sO=document.getElementById('subOrig'),sT=document.getElementById('subTr');
function update(){
const a=A(), T=curTL(), dur=a.duration||T[T.length-1].e;
document.getElementById('time').textContent=fmt(a.currentTime+OFFSET)+' / '+fmt(dur+OFFSET)+' (kayıt saati)';
const i=curIndex();
if(i>=0){ const b=T[i]; highlight(b.rol);
let badge=(mode==='tr'&&!TR_READY)?' <span class="badge">ses hazırlanıyor</span>':'';
sW.innerHTML=(SHORT[b.rol]||b.rol)+badge; sW.style.color=(ROLES[b.rol]&&ROLES[b.rol].renk)||'#fff';
sO.textContent=b.orig; sT.textContent=b.tr; sub.style.opacity=1;
} else { highlight(null); sub.style.opacity=.6; }
drawTL();
}
ao.addEventListener('timeupdate',function(){if(mode==='orig'||!TR_READY)update();});
at.addEventListener('timeupdate',function(){if(mode==='tr'&&TR_READY)update();});
ao.addEventListener('loadedmetadata',function(){fitTL();update();});
at.addEventListener('loadedmetadata',function(){if(mode==='tr')fitTL();});
const playBtn=document.getElementById('play');
playBtn.onclick=function(){ const a=A(); if(a.paused){a.play();playBtn.textContent='⏸';}else{a.pause();playBtn.textContent='▶';} };
document.getElementById('prev').onclick=function(){ const T=curTL(); let i=curIndex(); const a=A(); if(i>0&&a.currentTime-T[i].t<0.4)i--; a.currentTime=T[Math.max(0,i)].t; update(); };
document.getElementById('next').onclick=function(){ const T=curTL(); let i=curIndex(); const a=A(); a.currentTime=T[Math.min(T.length-1,i+1)].t; update(); };
document.addEventListener('keydown',function(e){ if(e.code==='Space'){e.preventDefault();playBtn.onclick();} if(e.code==='ArrowRight')A().currentTime+=5; if(e.code==='ArrowLeft')A().currentTime-=5; });
document.getElementById('langToggle').addEventListener('click',function(e){
const btn=e.target.closest('button'); if(!btn)return; const m=btn.dataset.m; if(m===mode)return;
[].forEach.call(e.currentTarget.children,function(b){ b.classList.toggle('on',b===btn); b.classList.toggle('trm',b===btn&&m==='tr'); });
const wasPlaying=!A().paused; const idx=curIndex(); A().pause();
mode=m; sub.classList.toggle('trmode',m==='tr');
const T=curTL(); const b=T[Math.max(0,idx)]; if(b)A().currentTime=b.t;
fitTL(); update(); if(wasPlaying){A().play();playBtn.textContent='⏸';}
});
renderer.domElement.addEventListener('dblclick',function(ev){
const rc=new THREE.Raycaster(), m=new THREE.Vector2((ev.clientX/innerWidth)*2-1,-(ev.clientY/innerHeight)*2+1);
rc.setFromCamera(m,camera);
const gs=Object.keys(chars).map(function(r){return chars[r].grp;});
const hit=rc.intersectObjects(gs,true)[0]; if(!hit)return;
let g=hit.object; while(g.parent&&gs.indexOf(g)<0)g=g.parent;
const role=Object.keys(chars).find(function(r){return chars[r].grp===g;});
const T=curTL(), b=T.find(function(x){return x.rol===role;}); if(b){A().currentTime=b.t;update();}
});
function loop(){
requestAnimationFrame(loop);
const t=performance.now()/1000;
for(const r in chars){ const c=chars[r];
if(c.active){ c.grp.position.y=Math.sin(t*4)*0.04; c.ring.rotation.z+=0.02; c.ring.material.emissiveIntensity=1.1+Math.sin(t*6)*0.5; }
else if(c.grp.position.y!==0){ c.grp.position.y*=0.85; if(Math.abs(c.grp.position.y)<0.002)c.grp.position.y=0; }
}
controls.update(); renderer.render(scene,camera);
}
loop();
addEventListener('resize',function(){ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); fitTL(); });
fitTL();
</script>
</body>
</html>