Files
captionninja/overlay_enhanced.html

1025 lines
29 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" />
<title>Caption Overlay Window</title>
<meta name="title" content="CAPTION.Ninja: Overlay" />
<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;
}
.output {
margin:0;
background-color: #0000;
color: white;
font-family: Cousine, monospace;
font-size: 3.2em;
line-height: 1.1em;
letter-spacing: 0.0em;
padding: 0em;
text-shadow: 0.05em 0.05em 0px rgba(0,0,0,1);
}
.output span {
background-color: black;
padding: 8px 8px 0px 8px;
margin:0;
}
a {
text-transform: none;
}
</style>
</head>
<body>
<div id="interm" class="output"></div>
<div id="output" class="output"></div>
<div id="status" style="display:none;"></div>
<script src="translation-providers.js"></script>
<script>
(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);
let worker;
let modelRegistry;
let version;
let supportedFromCodes = {};
let supportedToCodes = {};
let currentTo = null;
const status = function(message){document.getElementById("status").innerText = message;}
const getLocaleFromLang = (lang) => {
if (!lang) return null;
if (lang.includes("-")) return lang;
// Common language to locale mappings
const localeMap = {
en: "en-US",
es: "es-ES",
fr: "fr-FR",
de: "de-DE",
it: "it-IT",
pt: "pt-PT",
ru: "ru-RU",
zh: "zh-CN",
ja: "ja-JP",
ko: "ko-KR"
};
return localeMap[lang.toLowerCase()] || `${lang}-${lang.toUpperCase()}`;
};
// Default system language
let currentLanguage = "en-US";
// Translation settings
const translationParams = ["translate", "lang", "ln"];
const hasTranslation = translationParams.some(param => urlParams.has(param));
const translationLang = hasTranslation ?
(translationParams.map(param => urlParams.get(param)).find(val => val) || "en") : false;
const langTo = translationLang ? translationLang.split("-")[0] : false;
// Translation provider settings
const fallbackGoogleApiKey = urlParams.get("googlekey") || urlParams.get("gkey") || false;
const fallbackAutoDetect = urlParams.has("autodetect") || urlParams.get("fromlang") === "auto";
const forceLocal = urlParams.has("forcelocal");
const translationConfigBase = (window.CaptionTranslationProviders && window.CaptionTranslationProviders.resolveConfig)
? window.CaptionTranslationProviders.resolveConfig(urlParams, { forceLocal: forceLocal })
: {
provider: fallbackGoogleApiKey ? "google" : "local",
apiKey: fallbackGoogleApiKey || "",
googleApiKey: fallbackGoogleApiKey || "",
endpoint: "",
model: "",
forceLocal: forceLocal,
autoDetect: fallbackAutoDetect
};
const translationProvider = translationConfigBase.provider || "local";
const googleApiKey = translationConfigBase.googleApiKey || fallbackGoogleApiKey || false;
const autoDetectLang = Boolean(translationConfigBase.autoDetect);
// Context settings
const useContext = urlParams.has("context") || urlParams.has("fullcontext");
const contextSize = parseInt(urlParams.get("contextsize")) || 2;
let messageHistory = [];
// Translation cache for remote provider responses
const translationCache = new Map();
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours
// TTS settings initialization - only set if explicitly specified
const ttsParams = ["speech", "speak", "tts"];
const hasTTS = ttsParams.some(param => urlParams.has(param));
const ttsLang = hasTTS ?
(ttsParams.map(param => urlParams.get(param)).find(val => val)) : null;
const langs = { // https://github.com/mozilla/translate - MPL 2.0 - Mozilla
"bg": "Bulgarian",
"cs": "Czech",
"nl": "Dutch",
"en": "English",
"et": "Estonian",
"de": "German",
"fr": "French",
"is": "Icelandic",
"it": "Italian",
"nb": "Norwegian Bokmål",
"nn": "Norwegian Nynorsk",
"fa": "Persian",
"pl": "Polish",
"pt": "Portuguese",
"ru": "Russian",
"es": "Spanish",
"uk": "Ukrainian"
};
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());
}
}
}
async function translateViaProvider(text, sourceLang, targetLang) {
if (!text.trim()) {
return null;
}
if (!window.CaptionTranslationProviders || !window.CaptionTranslationProviders.translateText) {
return null;
}
const useAutoDetect = autoDetectLang || sourceLang === "auto";
const requestConfig = Object.assign({}, translationConfigBase, {
autoDetect: useAutoDetect
});
if (!window.CaptionTranslationProviders.canUseRemoteProvider(requestConfig)) {
return null;
}
const cacheKey = window.CaptionTranslationProviders.buildCacheKey(
requestConfig,
text,
sourceLang,
targetLang,
useAutoDetect
);
if (!useAutoDetect) {
const cached = translationCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.translation;
}
}
const result = await window.CaptionTranslationProviders.translateText(
requestConfig,
text,
sourceLang,
targetLang
);
if (!result || !result.translatedText) {
return null;
}
if (useAutoDetect && result.detectedSourceLanguage) {
console.log(`Auto-detected language: ${result.detectedSourceLanguage}`);
}
if (!useAutoDetect) {
translationCache.set(cacheKey, {
translation: result.translatedText,
timestamp: Date.now(),
detectedLang: result.detectedSourceLanguage || null
});
}
return result.translatedText;
}
// Clean up old cache entries periodically
setInterval(() => {
const now = Date.now();
for (const [key, value] of translationCache.entries()) {
if (now - value.timestamp > CACHE_DURATION) {
translationCache.delete(key);
}
}
}, 60 * 60 * 1000); // Run every hour
class TextToSpeech {
constructor(urlParams = new URLSearchParams(window.location.search)) {
this.speech = false;
this.premiumQueueTTS = [];
this.premiumQueueActive = false;
this.initializeSettings(urlParams);
this.initializeVoices();
this.initializeAudio();
}
initializeSettings(urlParams) {
this.settings = {
// Only set speechLang if explicitly provided in URL
speechLang: ttsLang ? getLocaleFromLang(ttsLang) : langTo ? getLocaleFromLang(langTo) : null,
volume: parseFloat(urlParams.get("volume")) || 1,
pitch: parseFloat(urlParams.get("pitch")) || 0,
rate: parseFloat(urlParams.get("rate")) || 1,
voiceLatency: parseInt(urlParams.get("latency")) || 4,
voiceGender: urlParams.get("gender") || false,
voiceName: urlParams.get("voice") || false,
google: {
apiKey: urlParams.get("ttskey") || urlParams.get("googlettskey") || false,
rate: urlParams.has("googlerate") ? parseFloat(urlParams.get("googlerate")) : 1,
pitch: urlParams.has("googlepitch") ? parseFloat(urlParams.get("googlepitch")) : 0,
audioProfile: urlParams.get("googleaudioprofile") || false,
voiceName: urlParams.get("voicegoogle") || false
},
elevenLabs: {
apiKey: urlParams.get("elevenlabskey") || false,
latency: urlParams.has("elevenlatency") ? parseInt(urlParams.get("elevenlatency")) : 4,
stability: urlParams.has("elevenstability") ? parseFloat(urlParams.get("elevenstability")) : 0.5,
similarity: urlParams.has("elevensimilarity") ? parseFloat(urlParams.get("elevensimilarity")) : 0.75,
style: urlParams.has("elevenstyle") ? parseFloat(urlParams.get("elevenstyle")) : 0.5,
speakingRate: urlParams.has("elevenrate") ? parseFloat(urlParams.get("elevenrate")) : 1.0,
speakerBoost: urlParams.has("elevenspeakerboost"),
voiceName: urlParams.get("voice11") || urlParams.get("elevenlabsvoice") || false,
model: urlParams.get("elevenlabsmodel") || "eleven_multilingual_v2"
},
speechify: {
apiKey: urlParams.get("speechifykey") || false,
speed: urlParams.has("speechifyspeed") ? parseFloat(urlParams.get("speechifyspeed")) : 1.0,
model: urlParams.get("speechifymodel") || 'simba-english',
voiceName: urlParams.get("voicespeechify") || false
}
};
if (urlParams.has("speech") || urlParams.has("speak") || urlParams.has("tts")) {
this.speech = true;
}
}
initializeVoices() {
this.voices = null;
this.voice = false;
try {
this.voices = window.speechSynthesis.getVoices();
if (!this.voices.length && this.isSafari()) {
console.warn("Safari doesn't fully support automatic TTS");
}
} catch (e) {
console.error("TTS not supported");
}
}
initializeAudio() {
if (this.settings.google.apiKey || this.settings.elevenLabs.apiKey || this.settings.speechify.apiKey) {
this.audio = document.createElement("audio");
this.audio.onended = () => this.finishedAudio();
}
}
createUI() {
const template = document.createElement('template');
template.innerHTML = `
<div class="tts-controls">
<button id="ttsToggle" class="tts-toggle">Toggle TTS</button>
<input type="range" id="volumeSlider" min="0" max="100" value="${this.settings.volume * 100}" />
</div>
`;
const style = document.createElement('style');
style.textContent = `
.tts-controls { display: flex; gap: 1em; align-items: center; }
.tts-toggle { padding: 0.5em 1em; }
`;
document.head.appendChild(style);
const ui = template.content.cloneNode(true);
ui.querySelector('#ttsToggle').addEventListener('click', () => this.toggleTTS());
ui.querySelector('#volumeSlider').addEventListener('input', (e) => {
this.settings.volume = e.target.value / 100;
});
return ui;
}
speak(text, allow = false) {
if ((!this.speech && !allow) || !text) return;
// Use explicitly set language or fall back to current language
const languageToUse = this.settings.speechLang || currentLanguage;
if (this.settings.google.apiKey) {
if (!this.premiumQueueActive) {
this.googleTTS(text, languageToUse);
} else {
this.premiumQueueTTS.push({text, language: languageToUse});
}
return;
}
// Similar updates for other TTS services
if (this.settings.elevenLabs.apiKey) {
if (!this.premiumQueueActive) {
this.elevenLabsTTS(text, languageToUse);
} else {
this.premiumQueueTTS.push({text, language: languageToUse});
}
return;
}
if (this.settings.speechify.apiKey) {
if (!this.premiumQueueActive) {
this.speechifyTTS(text, languageToUse);
} else {
this.premiumQueueTTS.push({text, language: languageToUse});
}
return;
}
this.useNativeTTS(text, languageToUse);
}
useNativeTTS(text, language) {
if (!this.voices && this.voices === null) return;
const speechInput = new SpeechSynthesisUtterance();
// Set language before voice selection
speechInput.lang = language;
if (!this.voice) {
this.selectVoice(language);
}
if (this.voice) {
speechInput.voice = this.voice;
}
speechInput.volume = this.settings.volume;
speechInput.rate = this.settings.rate;
speechInput.pitch = this.settings.pitch;
speechInput.text = text;
if (window.speechSynthesis) {
window.speechSynthesis.speak(speechInput);
this.updateUIState(true);
}
}
selectVoice(language) {
if (!this.voices || !this.voices.length) {
if (window.speechSynthesis) {
this.voices = window.speechSynthesis.getVoices();
}
}
if (this.voices) {
// Try to find voice matching name and language
if (this.settings.voiceName) {
this.voice = this.voices.find(v =>
v.name.toLowerCase().includes(this.settings.voiceName.toLowerCase()) &&
v.lang.toLowerCase().startsWith(language.split('-')[0].toLowerCase())
);
}
// If no voice found, try just matching language
if (!this.voice) {
this.voice = this.voices.find(v =>
v.lang.toLowerCase() === language.toLowerCase() ||
v.lang.split('-')[0].toLowerCase() === language.split('-')[0].toLowerCase()
);
}
// Fall back to first available voice if needed
if (!this.voice && this.voices.length) {
this.voice = this.voices[0];
}
}
}
googleTTS(text) {
this.premiumQueueActive = true;
const url = `https://texttospeech.googleapis.com/v1beta1/text:synthesize?key=${this.settings.google.apiKey}`;
const data = {
input: { text },
voice: {
languageCode: this.settings.speechLang.toLowerCase() || 'en-US',
name: this.settings.google.voiceName || "en-GB-Standard-A",
ssmlGender: this.settings.voiceGender ? this.settings.voiceGender.toUpperCase() : "FEMALE"
},
audioConfig: {
audioEncoding: "MP3",
speakingRate: this.settings.google.rate,
pitch: this.settings.google.pitch,
effectsProfileId: [this.settings.google.audioProfile || 'handset-class-device']
}
};
this.fetchAudio(url, {
headers: { "content-type": "application/json; charset=UTF-8" },
body: JSON.stringify(data),
method: "POST"
}, response => `data:audio/mp3;base64,${response.audioContent}`);
}
elevenLabsTTS(text) {
this.premiumQueueActive = true;
const voiceid = this.settings.elevenLabs.voiceName || "VR6AewLTigWG4xSOukaG";
const url = `https://api.elevenlabs.io/v1/text-to-speech/${voiceid}/stream?optimize_streaming_latency=${this.settings.elevenLabs.latency}`;
const data = {
text,
model_id: this.settings.elevenLabs.model,
voice_settings: {
stability: this.settings.elevenLabs.stability,
similarity_boost: this.settings.elevenLabs.similarity,
style: this.settings.elevenLabs.style,
use_speaker_boost: this.settings.elevenLabs.speakerBoost,
speaking_rate: this.settings.elevenLabs.speakingRate
}
};
// ElevenLabs returns binary audio data, not JSON
fetch(url, {
headers: {
"content-type": "application/json",
"xi-api-key": this.settings.elevenLabs.apiKey,
accept: "*/*"
},
body: JSON.stringify(data),
method: "POST"
})
.then(response => response.blob())
.then(blob => {
const blobUrl = URL.createObjectURL(blob);
if (!this.audio) {
this.audio = document.createElement("audio");
this.audio.onended = () => this.finishedAudio();
}
this.audio.src = blobUrl;
if (this.settings.volume) {
this.audio.volume = this.settings.volume;
}
return this.audio.play();
})
.catch(error => {
this.finishedAudio();
console.error("ElevenLabs TTS error:", error);
});
}
speechifyTTS(text) {
this.premiumQueueActive = true;
const url = "https://api.sws.speechify.com/v1/audio/speech";
const data = {
input: `<speak>${text}</speak>`,
voice_id: this.settings.speechify.voiceName || "henry",
model: this.settings.speechify.model,
audio_format: "mp3",
speed: this.settings.speechify.speed
};
this.fetchAudio(url, {
headers: {
"Authorization": `Bearer ${this.settings.speechify.apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify(data),
method: "POST"
}, response => `data:audio/mp3;base64,${response.audio_data}`);
}
async fetchAudio(url, options, processResponse, responseType = 'json') {
try {
const response = await fetch(url, options);
const data = responseType === 'blob' ? await response.blob() : await response.json();
if (!this.audio) {
this.audio = document.createElement("audio");
this.audio.onended = () => this.finishedAudio();
}
this.audio.src = await processResponse(data);
if (this.settings.volume) {
this.audio.volume = this.settings.volume;
}
try {
await this.audio.play();
} catch (e) {
this.finishedAudio();
console.error("User interaction required for audio playback");
}
} catch (error) {
this.finishedAudio();
console.error("Audio fetch error:", error);
}
}
finishedAudio() {
this.premiumQueueActive = false;
if (this.premiumQueueTTS.length) {
this.speak(this.premiumQueueTTS.shift());
}
this.updateUIState(false);
}
clearTTS() {
if (window.speechSynthesis && (window.speechSynthesis.pending || window.speechSynthesis.speaking)) {
window.speechSynthesis.cancel();
} else if (this.premiumQueueActive) {
this.premiumQueueTTS = [];
try {
if (this.audio) {
this.audio.pause();
}
} catch (e) {
console.error(e);
}
}
}
toggleTTS() {
if (window.speechSynthesis && (window.speechSynthesis.pending || window.speechSynthesis.speaking)) {
this.speech = false;
window.speechSynthesis.cancel();
} else if (this.premiumQueueActive) {
this.speech = false;
this.premiumQueueTTS = [];
try {
if (this.audio) {
this.audio.pause();
}
} catch (e) {
console.error(e);
}
this.premiumQueueActive = false;
} else {
this.speech = !this.speech;
}
this.updateUIState(this.speech);
}
updateUIState(isPlaying) {
const toggle = document.getElementById('ttsToggle');
if (toggle) {
toggle.textContent = isPlaying ? 'Stop TTS' : 'Start TTS';
toggle.classList.toggle('active', isPlaying);
}
}
isSafari() {
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
}
}
const tts = new TextToSpeech();
// document.body.appendChild(tts.createUI());
if (langTo){
// Handle auto-detect: langFrom will be "auto" when auto-detection is enabled
var langFrom = autoDetectLang ? "auto" : (urlParams.get("fromlang") || "en");
let remoteTranslationEnabled = (translationProvider !== "local") && !forceLocal;
if (translationProvider === "google" && remoteTranslationEnabled && !googleApiKey) {
remoteTranslationEnabled = false;
console.warn("Google provider selected without API key. Add &googlekey=YOUR_KEY or switch providers.");
status("Google provider requires API key");
}
// Only initialize local translation if remote provider is not active.
// Note: Local translation (Bergamot) does NOT support auto-detect.
if (!remoteTranslationEnabled && !autoDetectLang) {
if (window.Worker) {
worker = new Worker("worker.js");
worker.postMessage(["import"]);
}
}
// Warn if auto-detect is enabled without a remote provider.
if (autoDetectLang && !remoteTranslationEnabled) {
console.warn("Auto-detect language requires a remote translation provider.");
status("Auto-detect requires remote provider");
}
var translateCall = async (text, meta, interm=false) => { // https://github.com/mozilla/translate - MPL 2.0 - Mozilla
if (!text.trim().length) return;
const passThroughText = () => {
if (!meta){
document.getElementById("output").innerText = text;
} else if (meta.final){
processReply(meta, interm);
} else if (meta.interm){
processReply(meta, interm);
} else {
document.getElementById("output").innerText = text;
}
};
// Use remote translation provider if enabled.
if (remoteTranslationEnabled) {
try {
const translation = await translateViaProvider(text, langFrom, langTo);
if (translation) {
if (!meta){
document.getElementById("output").innerText = translation;
} else if (meta.final){
meta.final = translation;
processReply(meta, interm);
} else if (meta.interm){
meta.interm = translation;
processReply(meta, interm);
} else {
document.getElementById("output").innerText = translation;
}
return;
}
} catch (error) {
console.error("Remote translation failed, falling back to local:", error);
status(`Translation error: ${error.message}`);
// Fall back to local translation if available.
if (worker) {
const paragraphs = text.split("\n");
worker.postMessage(["translate", langFrom, langTo, paragraphs, meta, interm]);
return;
}
}
} else if (worker) {
// Use local translation
const paragraphs = text.split("\n");
worker.postMessage(["translate", langFrom, langTo, paragraphs, meta, interm]);
return;
}
// Safe fallback when translation is unavailable.
passThroughText();
};
if (worker) {
worker.onmessage = function (e) {
if (e.data[0] === "translate_reply" && e.data[1]) {
if (!e.data[2]){
document.getElementById("output").innerText = e.data[1].join("<br /><br />");
} else if (e.data[2].final){
e.data[2].final = e.data[1].join("<br /><br />");
processReply(e.data[2], (e.data[3]||false));
} else if (e.data[2].interm){
e.data[2].interm = e.data[1].join("<br /><br />");
processReply(e.data[2], (e.data[3]||false));
} else {
document.getElementById("output").innerText = e.data[1].join("<br /><br />");
}
} else if (e.data[0] === "load_model_reply" && e.data[1]) {
status(e.data[1]);
} else if (e.data[0] === "import_reply" && e.data[1]) {
modelRegistry = e.data[1];
version = e.data[2];
init();
}
};
}
var isSupported = (lngFrom, lngTo) => {
return true; //(`${lngFrom}${lngTo}` in modelRegistry) || ((`${lngFrom}en` in modelRegistry) && (`en${lngTo}` in modelRegistry))
}
var loadModel = () => {
if (langFrom !== langTo && worker) {
status(`Installing model...`);
console.log(`Loading model '${langFrom}${langTo}'`);
worker.postMessage(["load_model", langFrom, langTo]);
} else {
console.log("No model loaded yet");
// do nothing.
}
};
function init() { // https://github.com/mozilla/translate - MPL 2.0 - Mozilla
// parse supported languages and model types (prod or dev)
supportedFromCodes["en"] = "prod";
supportedToCodes["en"] = "prod";
for (const [langPair, value] of Object.entries(modelRegistry)) {
const firstLang = langPair.substring(0, 2);
const secondLang = langPair.substring(2, 4);
if (firstLang !== "en") supportedFromCodes[firstLang] = value.model.modelType;
if (secondLang !== "en") supportedToCodes[secondLang] = value.model.modelType;
}
loadModel();
}
} else {
var translateCall = console.log;
}
////////
var roomID = "test";
if (urlParams.has("room")){
roomID = urlParams.get("room");
} else if (urlParams.has("ROOM")){
roomID = urlParams.get("ROOM");
} else {
window.location.href = "./speechin";
}
var clearOnNew = false;
if (urlParams.has("clear")){
clearOnNew = true;
}
var timeoutSpeed = 5000;
if (urlParams.has("showtime")){
timeoutSpeed = parseInt(urlParams.get("showtime")) || 0;
}
var allowHTML = false;
if (urlParams.has("html")){
allowHTML = true;
}
var idle = null;
var socket;
var reconnectTimer = null;
let retryCount = 0;
const maxRetryDelay = 30000; // Maximum delay of 30 seconds
const baseDelay = 1000; // Start with 1 second delay after first immediate retry
function getRetryDelay() {
if (retryCount === 0) {
return 0; // First retry is immediate
}
// Exponential backoff: baseDelay * 2^(retryCount-1)
const delay = Math.min(baseDelay * Math.pow(2, retryCount - 1), maxRetryDelay);
return delay;
}
function clearReconnectTimer() {
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
}
function scheduleReconnect() {
if (reconnectTimer) {
return;
}
const delay = getRetryDelay();
console.log(`Reconnecting in ${delay}ms (attempt ${retryCount + 1})...`);
reconnectTimer = setTimeout(function() {
reconnectTimer = null;
retryCount++;
connectWebSocket();
}, delay);
}
function handleSocketMessage(event) {
if (!event.data) {
return;
}
let data;
try {
data = JSON.parse(event.data);
} catch (e) {
return;
}
if (data.ln) {
// Update current language from socket unless translating
if (!langTo) {
currentLanguage = data.ln;
}
}
// Handle context for translation
if (langTo && useContext && data.final) {
// Store message in history
messageHistory.push(data.final);
if (messageHistory.length > contextSize + 1) {
messageHistory.shift(); // Keep only recent messages
}
// Create context text
const contextText = messageHistory.join(' ');
if (data.ln && langFrom !== data.ln.split("-")[0]) {
langFrom = data.ln.split("-")[0];
if (!googleApiKey || forceLocal) {
loadModel();
}
}
translateCall(contextText, data, data.interm||false);
} else if (langTo) {
// Original behavior without context
if (data.ln && langFrom !== data.ln.split("-")[0]) {
langFrom = data.ln.split("-")[0];
if (!googleApiKey || forceLocal) {
loadModel();
}
}
translateCall(data.final || data.interm, data, data.interm||false);
} else {
processReply(data);
}
}
function connectWebSocket() {
if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
return;
}
const ws = new WebSocket("wss://api.caption.ninja:443");
socket = ws;
ws.onmessage = function(event) {
if (socket !== ws) {
return;
}
handleSocketMessage(event);
};
ws.onclose = function() {
if (socket !== ws) {
return;
}
socket = null;
console.log("WebSocket connection closed. Attempting to reconnect...");
scheduleReconnect();
};
ws.onopen = function() {
if (socket !== ws) {
return;
}
console.log("WebSocket connected. Joining room...");
// Reset retry count on successful connection
retryCount = 0;
clearReconnectTimer();
ws.send(JSON.stringify({"join":roomID}));
};
ws.onerror = function(error) {
if (socket !== ws) {
return;
}
console.error("WebSocket error:", error);
if (ws.readyState !== WebSocket.CLOSED) {
ws.close();
}
};
}
connectWebSocket();
window.addEventListener('beforeunload', function() {
clearReconnectTimer();
if (socket) {
socket.onclose = null;
try {
socket.close();
} catch (e) {}
socket = null;
}
});
var lastLabel = "";
var label = ""
var sanitize = function(string) {
if (allowHTML){
return string;
}
var temp = document.createElement('div');
temp.innerHTML = string; // Assign to innerHTML to parse entities and HTML structure
let decodedAndStrippedText = temp.textContent || temp.innerText || ""; // Get plain text content; this decodes entities and strips tags
decodedAndStrippedText = decodedAndStrippedText.substring(0, Math.min(decodedAndStrippedText.length, 500));
return decodedAndStrippedText.trim();
};
function processReply(data){
var newLabel = false;
if (data.label){
console.log(data.label);
try {
data.label = sanitize(data.label);
data.label = data.label.replaceAll('_', ' ');
label = "<span class='label'>"+data.label+": </span>";
} catch (e) {
return;
}
} else {
label = "";
}
// console.log(data);
if ("interm" in data){
document.getElementById("interm").innerHTML = label+"<span>"+sanitize(data.interm)+"</span><br/>";
} else if ("final" in data){
tts.speak(data.final);
if (clearOnNew || data.c){
document.getElementById("output").innerHTML = label+"<span>"+sanitize(data.final)+"</span><br/>";
} else {
document.getElementById("output").innerHTML += label+"<span>"+sanitize(data.final)+"</span><br/>";
}
document.getElementById("interm").innerHTML="";
}
if (!clearOnNew){
if (idle){
clearInterval(idle);
}
if (timeoutSpeed){
idle = setTimeout(function(){
document.getElementById("output").innerHTML="";
document.getElementById("interm").innerHTML="";
},timeoutSpeed);
}
}
}
socket.onclose = function (){
setTimeout(function(){window.location.reload(true);},100);
};
</script>
<script src="overlay-style-params.js"></script>
</body>
</html>