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:
@@ -0,0 +1,10 @@
|
|||||||
|
API.txt
|
||||||
|
*.env
|
||||||
|
keys*.txt
|
||||||
|
tts/
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
.playwright-mcp/
|
||||||
|
*.md
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# Gizli / gereksiz — repoya ASLA gitmesin
|
||||||
|
API.txt
|
||||||
|
*.env
|
||||||
|
keys*.txt
|
||||||
|
tts/
|
||||||
|
.playwright-mcp/
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
FROM nginx:1.27-alpine
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
COPY . /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
+1045
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,57 @@
|
|||||||
|
# 3D Toplantı Oynatıcı — Plan ve Kullanım
|
||||||
|
|
||||||
|
Dolandırıcılık uzlaşma görüşmesinin (09:00–31:13) **3D odada, zaman çizelgeli, orijinal ⇄ Türkçe geçişli** interaktif web sayfası.
|
||||||
|
|
||||||
|
## Nasıl açılır
|
||||||
|
En kolay: `web` klasöründeki **`index.html`** dosyasına çift tıkla (tarayıcıda açılır — internet gerekmez, her şey yerel).
|
||||||
|
|
||||||
|
> Not: Bazı tarayıcılar `file://` ile ses/otomatik oynatmayı kısıtlarsa, `web` klasöründe küçük bir yerel sunucu çalıştır:
|
||||||
|
> `python -m http.server 8765` → tarayıcıda `http://127.0.0.1:8765/index.html`
|
||||||
|
|
||||||
|
## Özellikler
|
||||||
|
| Özellik | Nasıl |
|
||||||
|
|---|---|
|
||||||
|
| Sahneyi döndür / yakınlaş | Fareyle sürükle · tekerlek |
|
||||||
|
| Bir kişiye atla | O kişiye **çift tıkla** (ilk sözüne gider) |
|
||||||
|
| Orijinal ⇄ Türkçe ses | Sağ üstteki **geçiş düğmesi** |
|
||||||
|
| Zaman çizelgesinde gezin | Alttaki renkli şeride **tıkla/sürükle** |
|
||||||
|
| Önceki/sonraki konuşma | ⏮ ⏭ düğmeleri |
|
||||||
|
| Oynat / dur | ▶ düğmesi veya **boşluk** tuşu |
|
||||||
|
| 5 sn ileri/geri | **← →** ok tuşları |
|
||||||
|
| Aktif konuşmacı | 3D'de **halka + altyazı** (isim, orijinal, Türkçe) |
|
||||||
|
|
||||||
|
Altyazı hem **orijinal dili** (Cebuano/Bisaya + Tagalog + İngilizce) hem **Türkçe** karşılığını gösterir; kayıt saati (09:00–31:13) üstte.
|
||||||
|
|
||||||
|
## Oturma düzeni (diyagrama birebir)
|
||||||
|
6 kişi oturuyor, **Kayınvalide ayakta**. Renkler taraf/grubu gösterir:
|
||||||
|
|
||||||
|
| Rol | Ses (ElevenLabs) | Cinsiyet/Yaş | Renk |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Kaan | Eric | erkek / yetişkin | 🔵 mavi (Kaan tarafı) |
|
||||||
|
| Marissa (eş) | Matilda | kadın / yetişkin | 🔵 mavi |
|
||||||
|
| Uzlaşmacı (U) | Chris | erkek / yetişkin | 🔴 koyu kırmızı |
|
||||||
|
| Lider Uzlaşmacı (L.U) | George | erkek / olgun | 🔴 koyu kırmızı |
|
||||||
|
| Yaşlı adam (V) | **Bill** | erkek / **yaşlı** | 🟡 sarı |
|
||||||
|
| Dolandırıcı (D-) | **Callum** (kurnaz) | erkek / yetişkin | 🟡 sarı |
|
||||||
|
| Kayınvalide (ayakta) | Lily | kadın / olgun | ⚪ gri |
|
||||||
|
|
||||||
|
Seslendirme, karakterlerin **gerçek cinsiyet ve yaşına** göre seçildi (yaşlı adam → yaşlı erkek sesi Bill; dolandırıcı → kurnaz "husky" ses Callum).
|
||||||
|
|
||||||
|
## Dosya yapısı (`web/`)
|
||||||
|
- `index.html` — sayfa (Three.js 3D sahne + oynatıcı)
|
||||||
|
- `data.js` — zaman çizelgesi verisi: roller, orijinal metin, **Türkçe metin**, önem, üst-binme; ayrıca Türkçe dublaj zaman haritası (`TL_tr`)
|
||||||
|
- `toplanti_orijinal.mp3` — orijinal ses (22:16)
|
||||||
|
- `toplanti_turkce.mp3` — **Türkçe dublaj** (7 sesle seslendirildi)
|
||||||
|
- `three.min.js`, `OrbitControls.js` — 3D motoru (çevrimdışı)
|
||||||
|
- `tts/` — tekil seslendirme klipleri + 7 ses örneği (`ornek_*.mp3`)
|
||||||
|
|
||||||
|
## Doğruluk katmanları (yapılan kontroller)
|
||||||
|
1. **Orijinal ses doğrulama** — ElevenLabs (rol etiketli) + **Gemini** (bağımsız 2. motor) aynı sesi ayrı ayrı yazdı; iki motorun anlaştığı yerler yüksek güvenli işaretlendi.
|
||||||
|
2. **Orijinal dil doğrulama** — 6 parça karşılaştırıldı; rakam/tutar (40k, 20k, 400, 900 balas…) iki motorda da tutarlı. Gemini'nin sistematik "scam→come" hataları tespit edildi → ElevenLabs esas alındı.
|
||||||
|
3. **Türkçe çeviri doğrulama** — 6 ajanla blok blok denetlendi; **14 çeviri düzeltmesi** uygulandı (ör. Blok 99 "Mm." yanlış kopya → "Hı hı"; Blok 20 "fayans hariç" → "fayans dahil" ters anlam düzeltildi).
|
||||||
|
4. **Seslendirme** — cinsiyet/yaş/persona workflow ile onaylandı, ElevenLabs multilingual_v2 ile üretildi.
|
||||||
|
|
||||||
|
## Bilinen sınırlar / sonraki adımlar
|
||||||
|
- **Chunk 1 (09:00–13:00)** Gemini tarafında pazarlığın bir kısmını kaçırmıştı; base metin ElevenLabs'ten alındığı için sayfa eksiksiz, ama o aralık tek-motor doğrulamalı.
|
||||||
|
- Cebuano/Bisaya bazı bozuk (garbled) kısımlar her iki motorda da tahminî; kritik olanlar `[belirsiz]`/düşük-güven olarak işaretli.
|
||||||
|
- İstenirse: dublaja karakterler arası kısa boşluk/nefes, duygu (emotion) ayarı, veya SpeechGen (bakiye yüklendiğinde) alternatif sesleri eklenebilir.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# PLAN — 3D Oda + Zaman Çizelgeli · Orijinal/Türkçe Web Oynatıcı
|
||||||
|
|
||||||
|
## Ne istendi
|
||||||
|
- 3D oda görseli olan web sayfası
|
||||||
|
- Masanın etrafında **7 kişi** (diyagram yerleşimi, **kayınvalide ayakta**)
|
||||||
|
- **Orijinal ses ↔ Türkçe seslendirme** arasında geçiş
|
||||||
|
- Altta **zaman çizelgesi**, ileri/geri hareket
|
||||||
|
|
||||||
|
## ✅ Şu an teslim edilen (çalışan prototip)
|
||||||
|
`web/` klasörü — **çift tıkla `index.html`** aç, tamamen çevrimdışı çalışır:
|
||||||
|
- **`index.html`** — sayfa (tek dosya, harici bağımlılık yok)
|
||||||
|
- **`toplanti_orijinal.mp3`** (21 MB) — orijinal ses (09:00–31:13, senle hizalı)
|
||||||
|
- **`data.js`** — 299 replik + 7 rol konumu (zaman çizelgesi verisi)
|
||||||
|
|
||||||
|
**Prototipte olanlar:**
|
||||||
|
1. **3D-perspektif oda**: eğik masa + kayıt cihazı (🎙️) + 7 kişi diyagram konumlarında; renkler gruplara göre (mavi=Kaan tarafı, bordo=uzlaşmacılar, sarı=işçi/dolandırıcı, gri=kayınvalide). **Kayınvalide ayakta** (daha uzun + "🧍 AYAKTA" rozeti).
|
||||||
|
2. **Konuşan kişi vurgulanıyor**: ses oynarken o an konuşan kişi büyüyor/parlıyor + başında konuşma balonu.
|
||||||
|
3. **Orijinal / Türkçe düğmesi**: altyazıda hangi dilin vurgulanacağını değiştirir (ses şimdilik orijinal — Türkçe ses eklenince otomatik devreye girecek, kod hazır).
|
||||||
|
4. **Zaman çizelgesi (alt şerit)**: konuşmacı renkleriyle boyalı, 🔴 kritik anlar tam opak. Tıkla→atla, oynatma çizgisi (playhead) ilerler.
|
||||||
|
5. **Kontroller**: ▶/⏸, ⏮/⏭ (önceki/sonraki replik), boşluk=oynat-dur, ←/→=5 sn, kişiye tıkla=o kişinin ilk repliğine git.
|
||||||
|
|
||||||
|
## 🔜 Tam sürüme yol haritası (planlanan geliştirmeler)
|
||||||
|
|
||||||
|
### Aşama 1 — Türkçe ses entegrasyonu (SpeechGen)
|
||||||
|
- SpeechGen e-postan gelince: 7 role **7 ayrı Türkçe ses** üretilir (kadın→Kayınvalide, erkek→diğerleri).
|
||||||
|
- Her replik için ayrı ses klibi → tek Türkçe parça olarak birleştirilir (zaman çizelgesiyle hizalı).
|
||||||
|
- "Türkçe" düğmesi **gerçekten Türkçe sesi** çalar (kod bu geçişe hazır: `A.src` değişimi).
|
||||||
|
|
||||||
|
### Aşama 2 — Gerçek 3D (opsiyonel, görsel yükseltme)
|
||||||
|
- Şu anki "3D-perspektif CSS" yerine **Three.js** sahne: gerçek 3D oda, masa, 7 karakter (Ready Player Me / basit avatar).
|
||||||
|
- Kamera döndürme, konuşan kişiye yakınlaşma.
|
||||||
|
- Ses-güdümlü basit ağız animasyonu (konuşurken avatar canlanır).
|
||||||
|
|
||||||
|
### Aşama 3 — Zenginleştirmeler
|
||||||
|
- Altyazı arama / repliğe tıklayıp o ana atlama listesi.
|
||||||
|
- Grup filtreleme (sadece kayınvalideyi dinle).
|
||||||
|
- 🔴 "kritik anlar" hızlı gezinme.
|
||||||
|
- Video dışa aktarım (sahne + ses → mp4) — video planındaki hedefe köprü.
|
||||||
|
|
||||||
|
## Teknik notlar
|
||||||
|
- **Veri kaynağı:** `TOPLANTI-9dk-sonrasi-ORIJINAL-ve-TURKCE.json` → `data.js` (0-tabanlı zaman = kayıt zamanı − 9:00, sesle hizalı).
|
||||||
|
- **Neden yerel dosya (Artifact değil):** 21 MB ses + gizlilik → yerelde `file://` ile açılır, dışarı hiçbir şey gitmez.
|
||||||
|
- **Türkçe ses eklenince:** `data.js`'e her repliğe `trAudio` alanı + tek `toplanti_turkce.mp3` eklenir; düğme `A.src`'yi değiştirir.
|
||||||
+262
@@ -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:00–31: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>
|
||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Tek sayfa; her yol index.html'e düşsün
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ses dosyaları: byte-range (seek) + uzun cache
|
||||||
|
location ~* \.(mp3|wav|ogg)$ {
|
||||||
|
add_header Accept-Ranges bytes;
|
||||||
|
add_header Cache-Control "public, max-age=604800";
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(js|css|png|jpg|svg)$ {
|
||||||
|
add_header Cache-Control "public, max-age=86400";
|
||||||
|
}
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_types text/plain text/css application/javascript application/json image/svg+xml;
|
||||||
|
gzip_min_length 1024;
|
||||||
|
}
|
||||||
Vendored
+6
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user