Files

300 lines
9.3 KiB
HTML

<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta content="utf-8" http-equiv="encoding" />
<meta name="copyright" content="&copy; 2020 Steve Seguin" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="shortcut icon" href="favicon.svg" />
<!-- Primary Meta Tags -->
<title>CAPTION.Ninja</title>
<meta name="title" content="CAPTION.Ninja" />
<meta name="description" content="This is a free-to-use captioning tool for OBS. Speech-to-text is done using Machine Learning" />
<meta name="author" content="Steve Seguin" />
<style>
@font-face {
font-family: 'Cousine';
src: url('fonts/Cousine-Bold.ttf') format('truetype');
}
body {
margin:0;
padding:0 10px;
height:100%;
border: 0;
display: flex;
flex-direction: column-reverse;
position:absolute;
bottom:0;
overflow:hidden;
width: 100%;
}
.output {
margin:0;
color: black;
font-family: Cousine, monospace;
font-size: 3.2em;
line-height: 1.1em;
letter-spacing: 0.0em;
text-transform: uppercase;
padding: 0em;
}
.output span {
padding: 8px 8px 0px 8px;
margin:0;
display: block;
}
#interm {
color:blue;
margin:5px;
}
a {
color:blue;
font-size:1.2em;
text-transform: none;
}
input {
width: 100%;
padding: 20px;
margin: 10px 0;
font-size: 2em;
}
</style>
</head>
<body>
<input type="textarea" id="userinput" />
<div id="interm" class="output">
<font style='color:black;font-size:60%;line-height: 1.4em;' ><div >This is a free-to-use captioning tool.</div></font><font style='color:black;font-size:30%;line-height: 1.4em;' >
<div ><br /><br />
The output of this app is mirrored here: <a id="shareLink" href="overlay.html" target='_blank'>*ERROR GENERATING LINK*</a>. Add it to OBS as a browser source overlay if wishing to use it for a live stream.
<br /><br />
For vMix, use this no-highlight overlay link: <a id="vmixLink" href="vmix.html" target='_blank'>*ERROR GENERATING LINK*</a>.
<br /><br />
Overlay style options include <code>&amp;nobg</code> or <code>&amp;bg=transparent</code> to remove the black highlight, <code>&amp;bg=COLOR</code> to set it, <code>&amp;color=COLOR</code>, <code>&amp;fontsize=SIZE</code>, <code>&amp;scale=N</code>, <code>&amp;align=center</code>, <code>&amp;valign=top</code>, <code>&amp;shadow=none</code>, and <code>&amp;outline=2&amp;outlinecolor=%23000</code>.
</div></font>
<br /><br />
</div>
<div id="output" class="output"></div>
<script src="tts-integration.js"></script>
<script src="ws-publisher.js"></script>
<script src="text-rules.js"></script>
<script>
function updateURL(param, force=false) {
var para = param.split('=');
if (!(urlParams.has(para[0].toLowerCase()))){
if (history.pushState){
var arr = window.location.href.split('?');
var newurl;
if (arr.length > 1 && arr[1] !== '') {
newurl = window.location.href + '&' +param;
} else {
newurl = window.location.href + '?' +param;
}
window.history.pushState({path:newurl},'',newurl);
}
} else if (force){
if (history.pushState){
var href = new URL(window.location.href);
if (para.length==1){
href.searchParams.set(para[0].toLowerCase(), "");
} else {
href.searchParams.set(para[0].toLowerCase(), para[1]);
}
log(href.toString());
window.history.pushState({path:href.toString()},'',href.toString());
}
}
}
(function (w) {
w.URLSearchParams = w.URLSearchParams || function (searchString) {
var self = this;
self.searchString = searchString;
self.get = function (name) {
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(self.searchString);
if (results == null) {
return null;
}
else {
return decodeURI(results[1]) || 0;
}
};
};
})(window);
var urlParams = new URLSearchParams(window.location.search);
// Secure alphanumeric room ID generator (>=12 chars)
function generateSecureRoomID(len = 16) {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < len; i++){
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
};
var label = false;
if (urlParams.has("label")){
label = urlParams.get("label");
}
var roomID = "test";
if (urlParams.has("room")){
roomID = urlParams.get("room");
} else if (urlParams.has("ROOM")){
roomID = urlParams.get("ROOM");
} else {
var storedRoom = "";
try { storedRoom = localStorage.getItem('cn_room_id') || ""; } catch (e) {}
if (storedRoom && /^[A-Za-z0-9]{12,}$/.test(storedRoom)) {
roomID = storedRoom;
updateURL("room="+roomID);
} else {
roomID = generateSecureRoomID(16);
updateURL("room="+roomID);
}
}
try { localStorage.setItem('cn_room_id', roomID); } catch (e) {}
var counter=0;
var url = document.URL.substr(0,document.URL.lastIndexOf('/'));
document.getElementById("shareLink").href= url+"/overlay?room="+roomID;
document.getElementById("shareLink").innerHTML = url+"/overlay?room="+roomID;
document.getElementById("vmixLink").href= url+"/vmix.html?room="+roomID;
document.getElementById("vmixLink").innerHTML = url+"/vmix.html?room="+roomID;
navigator.clipboard.writeText(url+"/overlay?room="+roomID).then(() => {
/* clipboard successfully set */
}, () => {
/* clipboard write failed */
});
var apiKey = false;
var wsPublisher = null;
var textRulesConfig = (window.CaptionTextRules && window.CaptionTextRules.parseRulesFromUrlParams)
? window.CaptionTextRules.parseRulesFromUrlParams(urlParams)
: { replacements: [], restricted: [], matchMode: 'phonetic' };
var textRulesEngine = (window.CaptionTextRules && window.CaptionTextRules.createEngine)
? window.CaptionTextRules.createEngine(textRulesConfig)
: null;
function applyTextRules(text) {
if (!textRulesEngine || typeof text !== "string") return text;
try {
return textRulesEngine.apply(text);
} catch (e) {
console.warn("Text rules failed:", e);
return text;
}
}
function connectWebSocket() {
wsPublisher = createWSPublisher({
url: "wss://api.caption.ninja:443",
room: roomID,
onStateChange: function(nextState, snapshot) {
if (nextState === "error") {
console.error("WebSocket error state:", snapshot && snapshot.lastError);
}
if (snapshot && snapshot.blockedSuspected) {
console.warn("WebSocket may be blocked by network or firewall.");
}
},
onError: function(error) {
console.error("WebSocket error:", error);
}
});
wsPublisher.connect();
}
// Initial connection
connectWebSocket();
function interResults(){
counter+=1;
var interim_transcript = applyTextRules(document.getElementById("userinput").value);
document.getElementById("interm").textContent = interim_transcript;
if (wsPublisher) {
wsPublisher.publish({"msg":true, "interm":interim_transcript, "id":counter, "label":label});
}
try {
if (window.CAPTION_TTS && window.CAPTION_TTS.stream && typeof window.CAPTION_TTS.speak === 'function') {
window.CAPTION_TTS.speak(interim_transcript, true);
}
} catch (e) { console.warn(e); }
}
function finalResults(){
counter+=1;
var final_transcript = applyTextRules(document.getElementById("userinput").value);
if (wsPublisher) {
if (final_transcript==""){
wsPublisher.publish({"msg":true, "final":"", "id":counter, "label":label});
} else {
wsPublisher.publish({"msg":true, "final":final_transcript, "id":counter, "label":label});
}
}
try {
if (window.CAPTION_TTS && typeof window.CAPTION_TTS.speak === 'function') {
window.CAPTION_TTS.speak(final_transcript);
}
} catch (e) { console.warn(e); }
document.getElementById("output").appendChild(document.createTextNode(final_transcript));
document.getElementById("interm").textContent="";
document.getElementById("userinput").value = "";
}
document.getElementById("userinput").addEventListener("keyup", function(event) {
if (event.code === 'Enter') {
finalResults();
} else {
interResults();
}
});
window.copyWsDebugInfo = function () {
if (!wsPublisher) return;
var snapshot = wsPublisher.getSnapshot();
var text = "[Caption.Ninja WS Debug]\\n" +
"room=" + roomID + "\\n" +
"state=" + snapshot.state + "\\n" +
"retries=" + snapshot.retryCount + "\\n" +
"queue=" + snapshot.queueLength + "\\n" +
"dropped=" + snapshot.droppedCount + "\\n" +
"blocked=" + snapshot.blockedSuspected + "\\n" +
"lastError=" + (snapshot.lastError || "") + "\\n" +
"userAgent=" + navigator.userAgent;
navigator.clipboard.writeText(text).catch(function(){});
};
// Optional: open a separate TTS window powered by tts.rocks bridge
try {
if (urlParams.has('ttspopout')) {
var qs = new URLSearchParams(window.location.search);
qs.set('room', roomID);
var base = window.location.origin + window.location.pathname.replace(/\/[^^/]*$/, '');
var bridgeUrl = base + '/tts.rocks/caption-bridge.html?' + qs.toString();
window.open(bridgeUrl, '_blank', 'noopener');
}
} catch (e) { console.warn(e); }
</script>
</body>
</html>