File size: 16,638 Bytes
46b6961
 
221f950
 
46b6961
 
 
a659887
46b6961
a659887
 
221f950
a659887
221f950
a659887
c0c6a58
221f950
 
36847de
 
db39d79
f5c0e09
36847de
221f950
a659887
 
 
 
221f950
 
f96535d
356c055
221f950
81df5c7
36847de
81df5c7
 
 
a659887
221f950
a659887
221f950
a659887
221f950
 
a659887
 
 
 
 
 
 
 
46b6961
221f950
81df5c7
 
 
f5c0e09
 
 
81df5c7
f5c0e09
 
36847de
 
 
f5c0e09
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36847de
 
 
 
81df5c7
36847de
81df5c7
 
221f950
a659887
 
 
 
ee77d7e
 
 
 
 
 
356c055
ee77d7e
 
356c055
 
a659887
 
 
 
 
46b6961
221f950
a659887
 
 
f96535d
a659887
 
 
 
6d10fa1
f5c0e09
a659887
 
221f950
a659887
 
 
 
 
4f41db6
 
f5c0e09
81df5c7
 
f5c0e09
4f41db6
81df5c7
f5c0e09
81df5c7
36847de
4f41db6
f5c0e09
81df5c7
36847de
 
 
4f41db6
 
db39d79
a659887
 
81df5c7
36847de
81df5c7
 
 
db39d79
 
a659887
 
221f950
46b6961
 
a659887
f5c0e09
81df5c7
0db7678
a659887
9f23d03
0db7678
221f950
 
36847de
18a4d1d
46b6961
221f950
81df5c7
a659887
46b6961
a659887
 
0db7678
 
 
 
 
a659887
 
 
 
0db7678
 
 
 
 
 
 
46b6961
 
 
 
221f950
a659887
221f950
0db7678
a659887
221f950
0db7678
 
a659887
 
 
0db7678
 
 
 
a659887
 
 
 
81df5c7
221f950
 
a659887
 
 
 
0db7678
a659887
221f950
81df5c7
0db7678
a659887
0db7678
 
 
 
 
 
a659887
 
 
 
0db7678
a659887
81df5c7
 
 
221f950
81df5c7
0db7678
a659887
 
 
0db7678
a659887
81df5c7
 
 
221f950
 
 
81df5c7
0db7678
 
 
 
a659887
0db7678
 
 
a659887
 
 
 
 
221f950
a659887
0db7678
 
221f950
0db7678
 
a659887
 
 
 
 
4f41db6
221f950
a659887
 
 
 
 
221f950
46b6961
 
 
 
 
a659887
46b6961
a659887
 
 
 
 
 
 
46b6961
a659887
221f950
6d10fa1
a659887
 
81df5c7
 
88aaffc
a659887
 
88aaffc
81df5c7
46b6961
 
 
1541a66
46b6961
 
a659887
46b6961
 
 
1541a66
a659887
75992c8
 
 
 
 
 
 
 
 
 
 
a659887
81df5c7
75992c8
c0c6a58
81df5c7
a659887
221f950
a659887
 
 
 
 
 
221f950
a659887
 
221f950
 
 
a659887
 
 
221f950
a659887
221f950
 
46b6961
 
221f950
 
 
 
 
 
 
a659887
 
 
 
 
 
 
 
 
221f950
 
46b6961
 
221f950
46b6961
a659887
221f950
a659887
221f950
a659887
 
 
 
221f950
 
a659887
 
65ada14
221f950
 
a659887
 
 
 
65ada14
221f950
 
a659887
 
46b6961
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
document.addEventListener('DOMContentLoaded', () => {
    const socket = io();
    
    // HTML要素の取得
    const startForm = document.getElementById('startForm');
    const urlInput = document.getElementById('url');
    const stopBtn = document.getElementById('stopBtn');
    const videoFrame = document.getElementById('video-frame');
    const subtitleContainer = document.getElementById('subtitle-container');
    const historyList = document.getElementById('history-list');
    
    // 設定メニュー関連
    const hamburger = document.querySelector('.hamburger');
    const navMenu = document.getElementById('nav-menu');
    const subtitleSizeSelect = document.getElementById('subtitle_size');
    const subtitleLangSelect = document.getElementById('subtitle_lang');
    const bgSelect = document.getElementById('backgroundColor');

    // 文字色設定関連
    const textColorInput = document.getElementById('textColor');
    const textFgPreview = document.getElementById('textFgPreview'); 
    const textColorSettingItem = document.getElementById('textColorSettingItem');

    // 背景設定関連
    const textBackgroundCheck = document.getElementById('textBackground_check');
    const textBackground = document.getElementById('textBackground');
    const textBgPreview = document.getElementById('textBgPreview');
    const transparency = document.getElementById('textBackground_transparency');

    // シアターモード関連
    const theaterBtn = document.getElementById('theaterToggle');
    const closeTheaterBtn = document.getElementById('closeTheaterBtn');
    const videoContainer = document.getElementById('video-container');

    // 話者識別関連
    const identifyCheck = document.getElementById('identify_speakers_check');
    const speakerFeaturesArea = document.getElementById('speaker_features_area');
    const maxSpeakersSelect = document.getElementById('max_speakers');
    const resetSpeakersBtn = document.getElementById('resetSpeakersBtn');
    const speakerColorContainer = document.getElementById("speakerColorSettings");

    let speakerData = {}; 
    let currentFontSize = '24px';

    // --- 初期化処理 ---
    if(localStorage.getItem("savedSubtitleSize")) {
        const s = localStorage.getItem("savedSubtitleSize");
        if(subtitleSizeSelect) subtitleSizeSelect.value = s;
        currentFontSize = s + 'px';
    }
    if(urlInput && localStorage.getItem("savedUrlInput")) {
        urlInput.value = localStorage.getItem("savedUrlInput");
    }

    // --- 話者識別ON/OFFロジック ---
    if (identifyCheck) {
        identifyCheck.checked = false; // デフォルトOFF
        
        // 初期表示設定
        toggleSpeakerSettings();

        identifyCheck.addEventListener('change', () => {
            toggleSpeakerSettings();
            sendSettings(); // 設定変更を即時送信
        });
    }

    // 設定項目の表示/非表示を切り替える関数
    function toggleSpeakerSettings() {
        if (!identifyCheck) return;
        
        if (identifyCheck.checked) {
            // ONのとき: 話者設定を表示、文字色設定を隠す
            if (speakerFeaturesArea) speakerFeaturesArea.style.display = 'block';
            if (textColorSettingItem) textColorSettingItem.style.display = 'none';
        } else {
            // OFFのとき: 話者設定を隠す、文字色設定を表示
            if (speakerFeaturesArea) speakerFeaturesArea.style.display = 'none';
            if (textColorSettingItem) textColorSettingItem.style.display = 'block';
        }
    }

    // --- 文字色プレビュー反映 ---
    if (textColorInput && textFgPreview) {
        textColorInput.addEventListener('input', (e) => {
            textFgPreview.style.backgroundColor = e.target.value;
        });
        textFgPreview.style.backgroundColor = textColorInput.value;
    }

    // --- テーマ設定 ---
    function updateTheme() {
        const val = bgSelect.value;
        const body = document.body;
        body.classList.remove('light-mode', 'dark-mode');

        if (val === 'black') {
            body.classList.add('dark-mode');
        } else if (val === 'device') {
            if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
                body.classList.add('dark-mode');
            } else {
                body.classList.add('light-mode');
            }
        } else {
            body.classList.add('light-mode');
        }
    }
    if (bgSelect) bgSelect.addEventListener('change', updateTheme);
    window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateTheme);
    updateTheme();

    // --- Socketイベント ---
    socket.on('connect', () => {
        socket.emit('request_speaker_profiles');
    });

    socket.on('initial_speaker_profiles', (profiles) => {
        speakerData = profiles || {};
        updateSpeakerColorUI();
    });

    // 字幕受信処理
    socket.on('new_subtitle', (data) => {
        if (!data || !subtitleContainer) return;

        if (!speakerData[data.speaker_id]) {
            socket.emit('request_speaker_profiles');
        }
        const info = speakerData[data.speaker_id] || { name: data.speaker, color: data.color };
        const displayName = info.name;
        const displayColor = info.color;

        // 表示制御ロジック
        const isIdentityEnabled = identifyCheck && identifyCheck.checked;
        let finalDisplayText = "";
        let finalColor = "#FFFFFF"; 

        if (isIdentityEnabled) {
            // ON: 名前表示 + 色反映
            finalDisplayText = `${displayName}: ${data.translated}`;
            finalColor = displayColor;
        } else {
            // OFF: 名前表示なし + 設定した文字色
            finalDisplayText = data.translated;
            if (textColorInput) {
                finalColor = textColorInput.value;
            }
        }

        // 履歴に追加
        if (historyList) {
            const li = document.createElement('li');
            if (isIdentityEnabled) {
                li.innerHTML = `<span style="font-weight:bold; color:${displayColor}">${displayName}:</span> ${data.translated}`;
            } else {
                li.textContent = data.translated;
            }
            historyList.prepend(li);
            if (historyList.children.length > 30) historyList.removeChild(historyList.lastChild);
        }

        // 字幕要素作成
        const p = document.createElement('p');
        p.className = 'subtitle-line';
        p.style.fontSize = currentFontSize;
        p.style.color = finalColor;
        p.textContent = finalDisplayText;
        
        applySubtitleBackground(p);
        subtitleContainer.appendChild(p);

        // 古い字幕削除
        while (subtitleContainer.children.length > 5) {
             subtitleContainer.removeChild(subtitleContainer.firstChild);
        }

        setTimeout(() => p.style.opacity = '0', 4500);
        setTimeout(() => p.remove(), 5000);
    });

    socket.on("speaker_name_changed", data => {
        const { id, new_name } = data;
        if (speakerData[id]) {
            speakerData[id].name = new_name;
            const input = document.getElementById(`name-input-${id}`);
            if (input && input.value !== new_name) input.value = new_name;
        }
    });

    socket.on("speaker_color_changed", data => {
        const { id, color } = data;
        if (speakerData[id]) {
            speakerData[id].color = color;
            const bgDiv = document.getElementById(`color-box-${id}`);
            const input = document.getElementById(`color-input-${id}`);
            if (bgDiv) bgDiv.style.backgroundColor = color;
            if (input) input.value = color;
        }
    });

    socket.on('error_msg', (d) => alert(d.msg));

    // --- 話者設定UI更新 ---
    function updateSpeakerColorUI() {
        if (!speakerColorContainer) return;

        const existingIds = new Set();
        speakerColorContainer.querySelectorAll('.speaker-setting-row').forEach(el => {
            existingIds.add(el.dataset.speakerId);
        });

        Object.keys(speakerData).forEach(speakerId => {
            const info = speakerData[speakerId];
            if (existingIds.has(speakerId)) {
                existingIds.delete(speakerId);
                return; 
            }

            const row = document.createElement("div");
            row.className = "speaker-setting-row";
            row.dataset.speakerId = speakerId;
            row.style.marginBottom = "5px";
            row.style.display = "flex";
            row.style.alignItems = "center";

            const nameInput = document.createElement("input");
            nameInput.type = "text";
            nameInput.value = info.name;
            nameInput.id = `name-input-${speakerId}`;
            nameInput.className = "speaker-name-input";
            nameInput.style.flex = "1";
            nameInput.style.marginRight = "5px";
            
            nameInput.addEventListener("change", () => {
                const newName = nameInput.value.trim();
                if (newName) {
                    socket.emit("update_speaker_name", { id: speakerId, new_name: newName });
                } else {
                    nameInput.value = speakerData[speakerId].name;
                }
            });

            const colorBox = document.createElement("div");
            colorBox.className = "color-preview-box";
            colorBox.id = `color-box-${speakerId}`;
            colorBox.style.backgroundColor = info.color;
            colorBox.style.width = "30px";
            colorBox.style.height = "30px";
            colorBox.style.border = "1px solid #ccc";
            colorBox.style.position = "relative";
            colorBox.style.cursor = "pointer";

            const colorInput = document.createElement("input");
            colorInput.type = "color";
            colorInput.value = info.color;
            colorInput.id = `color-input-${speakerId}`;
            colorInput.className = "color-picker-hidden";
            colorInput.style.opacity = 0;
            colorInput.style.width = "100%";
            colorInput.style.height = "100%";
            colorInput.style.position = "absolute";
            colorInput.style.top = 0;
            colorInput.style.left = 0;
            colorInput.style.cursor = "pointer";
            
            colorInput.addEventListener("input", (e) => {
                colorBox.style.backgroundColor = e.target.value;
            });
            colorInput.addEventListener("change", (e) => {
                const newColor = e.target.value;
                speakerData[speakerId].color = newColor;
                socket.emit("update_speaker_color", { id: speakerId, color: newColor });
            });

            colorBox.appendChild(colorInput);
            row.appendChild(nameInput);
            row.appendChild(colorBox);
            speakerColorContainer.appendChild(row);
        });

        existingIds.forEach(oldId => {
            const el = speakerColorContainer.querySelector(`.speaker-setting-row[data-speaker-id="${oldId}"]`);
            if (el) el.remove();
        });
    }

    if (resetSpeakersBtn) {
        resetSpeakersBtn.addEventListener('click', () => {
            if(confirm("話者設定をリセットしますか?")) {
                speakerData = {}; 
                speakerColorContainer.innerHTML = "";
                socket.emit('reset_speakers');
            }
        });
    }

    // --- フォーム送信・制御 ---
    if (startForm) {
        startForm.addEventListener('submit', (e) => {
            e.preventDefault();
            const url = urlInput.value.trim();
            if (!url) return alert("URLを入力してください");
            localStorage.setItem("savedUrlInput", url);

            let embed = url;
            if(url.includes('twitch.tv')) {
                const m = url.match(/twitch\.tv\/([^\/]+)/);
                if(m) embed = `https://player.twitch.tv/?channel=${m[1]}&parent=${location.hostname}&autoplay=true&muted=true`;
            } else if(url.includes('youtube.com') || url.includes('youtu.be')) {
                 const m = url.match(/(?:v=|\/)([a-zA-Z0-9_-]{11})/);
                 if(m) embed = `https://www.youtube.com/embed/${m[1]}?autoplay=1`;
            }
            videoFrame.src = embed;
            
            subtitleContainer.innerHTML = '';
            historyList.innerHTML = '';

            const isIdentityEnabled = identifyCheck && identifyCheck.checked;
            const maxSpk = isIdentityEnabled ? parseInt(maxSpeakersSelect.value) : 1;
            const targetLang = subtitleLangSelect ? subtitleLangSelect.value : 'ja';
            socket.emit('start_stream', {
                url: url,
                target_lang: targetLang,
                max_speakers: maxSpk
            });
        });
    }

    if (stopBtn) {
        stopBtn.addEventListener('click', () => {
            videoFrame.src = '';
            socket.emit('stop_stream');
        });
    }

    function sendSettings() {
      if (!subtitleLangSelect || !maxSpeakersSelect) return;

      const isIdentityEnabled = identifyCheck && identifyCheck.checked;
      const maxSpk = isIdentityEnabled
        ? parseInt(maxSpeakersSelect.value, 10)
        : 1;

      socket.emit('update_settings', {
        target_lang: subtitleLangSelect.value || 'ja',
        max_speakers: maxSpk
      });
    }


    if (subtitleLangSelect) subtitleLangSelect.addEventListener('change', sendSettings);
    if (maxSpeakersSelect) maxSpeakersSelect.addEventListener('change', sendSettings);

    // --- スタイル反映 ---
    function hexToRgba(hex, alpha) {
        hex = hex.replace(/^#/, '');
        if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
        const r = parseInt(hex.slice(0, 2), 16), g = parseInt(hex.slice(2, 4), 16), b = parseInt(hex.slice(4, 6), 16);
        return `rgba(${r}, ${g}, ${b}, ${alpha / 100})`;
    }

    function applySubtitleBackground(el) {
        if (!textBackgroundCheck.checked) {
            el.style.backgroundColor = 'transparent'; 
            el.style.padding = '0'; 
            return;
        }
        const alpha = transparency.value;
        el.style.backgroundColor = hexToRgba(textBackground.value, alpha);
        el.style.padding = '4px 8px';
        el.style.borderRadius = '4px';
        el.style.marginBottom = '4px';
        el.style.display = 'inline-block';
    }
    
    const updateAll = () => document.querySelectorAll('.subtitle-line').forEach(applySubtitleBackground);
    const updatePreview = () => { if(textBgPreview) textBgPreview.style.backgroundColor = textBackground.value; };

    textBackgroundCheck.addEventListener('change', updateAll);
    textBackground.addEventListener('input', () => { updateAll(); updatePreview(); });
    transparency.addEventListener('input', updateAll);
    updatePreview();

    if (subtitleSizeSelect) {
        subtitleSizeSelect.addEventListener('input', () => {
            currentFontSize = subtitleSizeSelect.value + 'px';
            localStorage.setItem("savedSubtitleSize", subtitleSizeSelect.value);
            document.querySelectorAll('.subtitle-line').forEach(p => p.style.fontSize = currentFontSize);
        });
    }

    // --- ハンバーガーメニュー ---
    if (hamburger && navMenu) {
        hamburger.addEventListener('click', () => {
            hamburger.classList.toggle('active');
            navMenu.classList.toggle('active');
        });
        document.addEventListener('click', (e) => {
            if (!e.target.closest('.nav') && !e.target.closest('.hamburger') && navMenu.classList.contains('active')) {
                hamburger.classList.remove('active');
                navMenu.classList.remove('active');
            }
        });
    }

    // --- シアターモード ---
    if (videoContainer) {
        if (theaterBtn) {
            theaterBtn.addEventListener('click', () => {
                document.body.classList.add('theater-active');
                videoContainer.classList.add('theater-mode');
                videoContainer.classList.remove('normal-mode');
            });
        }
        if (closeTheaterBtn) {
            closeTheaterBtn.addEventListener('click', () => {
                document.body.classList.remove('theater-active');
                videoContainer.classList.remove('theater-mode');
                videoContainer.classList.add('normal-mode');
            });
        }
    }
});