-
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 4779aa2
Showing
2 changed files
with
341 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,341 @@ | ||
<!-- | ||
사용법 | ||
음성 인식 사이트: https://cc.go.alejo47.com/recorder | ||
1. 위 음성 인식 사이트에 "최신 버전 크롬"으로 접속합니다. | ||
2. 음성 인식 사이트에 방송하고자 하는 트위치 계정으로 로그인합니다. | ||
3. 음성 인식 사이트의 Language 항목을 사용하는 언어(한국어)로 변경합니다. | ||
4. 음성 인식 사이트의 Start 버튼을 클릭하고 마이크 사용 권한을 허용합니다. | ||
5. 음성 인식 사이트에서 마이크에 말한 내용을 인식한 내용이 출력되는 것을 확인합니다. | ||
6. 자막을 사용하는 동안 음성 인식 사이트는 계속 Start 상태로 켜 두어야 합니다. (사용을 종료하려면 Stop 버튼을 클릭하거나 창을 닫습니다.) | ||
7. 크롬 새 탭에서 자막 오버레이 파일(caption.html)을 엽니다. | ||
8. 크롬에서 연 자막 오버레이 파일 주소의 끝에 "?channel=트위치채널명" 을 덧붙여줍니다 (예시: file:///C:/data/caption.html?channel=sleeping_ce) | ||
9. 자막 오버레이에서 정상적으로 마이크에 말한 내용이 출력되는 것을 확인합니다. | ||
10. 정상 동작을 확인한 자막 오버레이 파일 주소를 방송 송출 프로그램(OBS, Xsplit 등)에서 브라우저 소스로 등록합니다. | ||
11. 이제 자막 기능을 사용하시려면 음성 인식 사이트에 접속하셔서 Start 버튼만 클릭하시면 됩니다. | ||
Author: Sleeping C. elegans <sleeping.c.elegans@gmail.com> | ||
--> | ||
<!DOCTYPE html> | ||
|
||
<head> | ||
<link rel="preload" as="style" href="https://fonts.googleapis.com/css?family=Jua:400"> | ||
<style> | ||
@import url('https://fonts.googleapis.com/css?family=Jua:400'); | ||
|
||
body { | ||
overflow: hidden; | ||
} | ||
|
||
.root-container { | ||
font-family: "Jua", sans-serif; | ||
font-size: 20px; | ||
position: absolute; | ||
bottom: 0; | ||
width: 100%; | ||
} | ||
|
||
.cc-wrapper { | ||
height: 6em; | ||
overflow: hidden; | ||
position: relative; | ||
} | ||
|
||
.cc-container { | ||
bottom: 0.1em; | ||
left: 3.2em; | ||
right: 1.5em; | ||
position: absolute; | ||
text-align: center; | ||
opacity: 1; | ||
transition: opacity 0.6s cubic-bezier(0.19, 1, 0.22, 1); | ||
} | ||
|
||
.cc-container.hide { | ||
opacity: 0; | ||
} | ||
|
||
.cc-content { | ||
background: rgba(255, 255, 255, 0.7); | ||
border: 0.12em solid #95BBDF; | ||
border-radius: 1.3em; | ||
padding: 0.5em 0.8em; | ||
max-height: 4.1em; | ||
display: inline-block; | ||
overflow: hidden; | ||
} | ||
|
||
.text-transition-wrapper { | ||
height: 0; | ||
width: 0; | ||
overflow: hidden; | ||
transition: height 0.15s cubic-bezier(0.075, 0.82, 0.165, 1), width 0.15s cubic-bezier(0.075, 0.82, 0.165, 1); | ||
} | ||
|
||
.text-transition { | ||
position: absolute; | ||
} | ||
|
||
.text-wrapper { | ||
display: flex; | ||
flex-direction: column-reverse; | ||
max-height: 4.1em; | ||
overflow: hidden; | ||
} | ||
|
||
.text { | ||
align-self: flex-end; | ||
} | ||
|
||
.text p { | ||
margin: 0; | ||
color: #000000; | ||
} | ||
|
||
#interim { | ||
color: #333333; | ||
} | ||
|
||
.profile { | ||
position: absolute; | ||
margin-left: -4em; | ||
bottom: 0; | ||
} | ||
|
||
.profile img { | ||
height: 2.3em; | ||
width: 2.3em; | ||
border-radius: 2.3em; | ||
object-fit: cover; | ||
object-position: center; | ||
border: 0.12em solid #95BBDF; | ||
background: #95BBDF; | ||
} | ||
</style> | ||
</head> | ||
|
||
<body> | ||
<div class="root-container"> | ||
<div class="cc-wrapper"> | ||
<div class="cc-container"> | ||
<div class="cc-content"> | ||
<div class="profile"> | ||
<img src="./profile.png"> | ||
</div> | ||
<div class="text-transition-wrapper"> | ||
<div class="text-transition"> | ||
<div class="text-wrapper"> | ||
<div class="text"> | ||
<p id="finalStr"></p> | ||
<p id="interim"></p> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
</div> | ||
<script> | ||
const CLIENT_ID = "jzkbprff40iqj646a697cyrvl0zt2m6"; // twitch player id | ||
const TEST_TIMEOUT_DURATION = 4 * 1000; | ||
|
||
const FINAL_STR_DOM = document.getElementById('finalStr'); | ||
const INTERIM_DOM = document.getElementById('interim'); | ||
const CC_CONTAINER_DOM = document.getElementsByClassName('cc-container')[0]; | ||
const TEXT_TRANSITION_DOM = document.getElementsByClassName('text-transition')[0]; | ||
const TEXT_TRANSITION_WRAPPER_DOM = document.getElementsByClassName('text-transition-wrapper')[0]; | ||
|
||
var last_final_str = ""; | ||
var last_interim = ""; | ||
var text_timeout_timer = null; | ||
|
||
function addTransitionEndEventListener(el, callback) { | ||
el.addEventListener('webkitTransitionEnd', callback, false); | ||
el.addEventListener('transitionend', callback, false); | ||
el.addEventListener('msTransitionEnd', callback, false); | ||
el.addEventListener('oTransitionEnd', callback, false); | ||
} | ||
|
||
function getHeight(el) { | ||
return parseFloat(getComputedStyle(el, null).height.replace("px", "")) | ||
} | ||
|
||
function getWidth(el) { | ||
return parseFloat(getComputedStyle(el, null).width.replace("px", "")) | ||
} | ||
|
||
function setHeight(el, val) { | ||
if (typeof val === "function") val = val(); | ||
if (typeof val === "string") el.style.height = val; | ||
else el.style.height = val + "px"; | ||
} | ||
|
||
function setWidth(el, val) { | ||
if (typeof val === "function") val = val(); | ||
if (typeof val === "string") el.style.width = val; | ||
else el.style.width = val + "px"; | ||
} | ||
|
||
function requestChannelInfo(callback) { | ||
const url_params = new URLSearchParams(window.location.search); | ||
const channel_name = url_params.get('channel'); | ||
|
||
if (channel_name === null) { | ||
updateCaption("⚠️ 채널 정보를 받아올 수 없습니다 ⚠️", ""); | ||
return; | ||
} | ||
|
||
try { | ||
fetch("https://api.twitch.tv/kraken/channels/" + channel_name, { | ||
headers: { | ||
"client-id": CLIENT_ID | ||
}, | ||
}) | ||
.then(data => data.json()) | ||
.then(function (data) { | ||
callback(data._id.toString()) | ||
}).catch(function (error) { | ||
updateCaption("⚠️ 채널 정보를 받아올 수 없습니다 ⚠️", ""); | ||
}); | ||
} catch (error) { | ||
updateCaption("⚠️ 채널 정보를 받아올 수 없습니다 ⚠️", ""); | ||
} | ||
} | ||
|
||
function listenSocket(channel_id) { | ||
const socket = new WebSocket("wss://cc.go.alejo47.com/ws"); | ||
socket.onopen = function (e) { | ||
socket.send(JSON.stringify({ | ||
command: "subscribe", | ||
room: channel_id | ||
})); | ||
}; | ||
socket.onmessage = function (e) { | ||
if (e.data.lastIndexOf('PING', 0) === 0) { | ||
return; // pass PING | ||
} | ||
const res = JSON.parse(e.data) | ||
if (res.command == "message") { | ||
if (res.params.final) { | ||
updateCaption(last_interim, ''); | ||
} else { | ||
updateCaption(last_final_str, res.data); | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
||
function updateCaption(final_str, interim) { | ||
if (last_final_str != final_str) { | ||
updateFinalStr(final_str); | ||
} | ||
if (interim.length == 0) { | ||
hideInterim(); | ||
} else { | ||
updateInterim(interim); | ||
} | ||
updateTextTransitionGroup(); | ||
} | ||
|
||
function updateFinalStr(final_str) { | ||
last_final_str = final_str; | ||
FINAL_STR_DOM.textContent = final_str; | ||
showFinalStr(); | ||
setTextTimeoutTimer(); | ||
} | ||
|
||
function updateInterim(interim) { | ||
last_interim = interim; | ||
INTERIM_DOM.textContent = interim; | ||
showInterim(); | ||
} | ||
|
||
function cancelTextTimeoutTimer() { | ||
if (text_timeout_timer != null) { | ||
clearTimeout(text_timeout_timer) | ||
} | ||
} | ||
|
||
function setTextTimeoutTimer() { | ||
cancelTextTimeoutTimer() | ||
text_timeout_timer = setTimeout(doTextTimeout, TEST_TIMEOUT_DURATION); | ||
} | ||
|
||
function doTextTimeout() { | ||
if (isInterimShowing()) { | ||
hideFinalStr(); | ||
} else { | ||
hideContainer(); | ||
} | ||
updateTextTransitionGroup(); | ||
} | ||
|
||
function updateTextTransitionGroup() { | ||
const height = getHeight(TEXT_TRANSITION_DOM); | ||
const width = getWidth(TEXT_TRANSITION_DOM); | ||
setHeight(TEXT_TRANSITION_WRAPPER_DOM, height); | ||
setWidth(TEXT_TRANSITION_WRAPPER_DOM, width); | ||
} | ||
|
||
function isFinalStrShowing() { | ||
return FINAL_STR_DOM.style.display != 'none'; | ||
} | ||
|
||
function hideFinalStr() { | ||
FINAL_STR_DOM.style.display = 'none'; | ||
if (!isInterimShowing()) { | ||
hideContainer(); | ||
} | ||
} | ||
|
||
function showFinalStr() { | ||
FINAL_STR_DOM.style.display = ''; | ||
if (!isContainerShowing()) { | ||
showContainer(); | ||
} | ||
} | ||
|
||
function isInterimShowing() { | ||
return INTERIM_DOM.style.display != 'none'; | ||
} | ||
|
||
function hideInterim() { | ||
INTERIM_DOM.style.display = 'none'; | ||
if (!isFinalStrShowing()) { | ||
hideContainer(); | ||
} | ||
} | ||
|
||
function showInterim() { | ||
INTERIM_DOM.style.display = ''; | ||
if (!isContainerShowing()) { | ||
showContainer(); | ||
} | ||
} | ||
|
||
function isContainerShowing() { | ||
return !CC_CONTAINER_DOM.classList.contains('hide'); | ||
} | ||
|
||
function hideContainer() { | ||
CC_CONTAINER_DOM.classList.add('hide'); | ||
} | ||
|
||
function showContainer() { | ||
CC_CONTAINER_DOM.classList.remove('hide'); | ||
} | ||
|
||
hideFinalStr(); | ||
hideInterim(); | ||
requestChannelInfo(listenSocket); | ||
addTransitionEndEventListener(CC_CONTAINER_DOM, function (event) { | ||
if (!isContainerShowing()) { | ||
hideFinalStr(); | ||
updateTextTransitionGroup(); | ||
} | ||
}); | ||
</script> | ||
</body> |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.