Spaces:
Sleeping
Sleeping
Download static/script.js from KitagawaC36/VCT: direct link, hf CLI and curl.
- Browser
- Download file 16.6 kB
-
https://huggingface.co/spaces/KitagawaC36/VCT/resolve/main/static/script.js
- Command line
-
hf download hf://spaces/KitagawaC36/VCT/static/script.js
-
curl -L -o script.js https://huggingface.co/spaces/KitagawaC36/VCT/resolve/main/static/script.js
16.6 kB
| 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'); | |
| }); | |
| } | |
| } | |
| }); |