KairosJS yes, it definitely works out of the box. The US English voices sound really good. What is also nice is that the voice selections are based on what Regional Voice packages you have loaded in Windows (supporting anyone's localized language they are using). It is also private and processes everything within the browser. I've tested other English Voice packs for England, Australia, and Ireland. The voice quality varies though. I prefer [Microsoft Zira Desktop - English (United States) (en-US)] voice. This also reduces dependency on shared local GPU/CPU AI processing and offloads it to local Browser processing. One thing is sometimes the pronunciation of some of the words is off sometimes, but that is to be expected. I'm running it in Chrome and Firefox. Can't remember if I needed to install a browser plug-in to make it work or if it just worked.
I think it uses the Web Speech api browser support.
I am also using it with Koboldcpp Lite and gemma-4-12b-it-Q8_0.gguf generating scenarios and it works wonderfully. If you want to test/hear the voice, here is the setup I'm using for my Browser. It takes a saved .Json scenario file and plays it back converting the scenario text and reads it back with voice. You can use Koboldcpp to run one a quick test and save the scenario output, then download it to your HDD. Then use the code below load the saved json file to interpret it and read it back. You can change the Voice based on what language/voice pack you have loaded in WIndows.
Open up Notepad and Save this code to a file ScenarioReader.html --> (code generated and refined with AI chat). Then use it to open a new tab to load your Json file. Click the button to Speak Text to hear your dialog.
html<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KoboldCpp Scenario Viewer & TTS Reader</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
background-color: #f5f7fb;
color: #333;
}
h1 {
color: #2c3e50;
text-align: center;
}
.controls {
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.file-input-wrapper {
display: flex;
align-items: center;
gap: 10px;
}
.tts-controls {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
select, button {
padding: 10px 15px;
border-radius: 5px;
border: 1px solid #ccc;
font-size: 14px;
cursor: pointer;
}
button {
background-color: #3498db;
color: white;
border: none;
transition: background 0.2s;
}
button:hover {
background-color: #2980b9;
}
button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
#stopBtn {
background-color: #e74c3c;
}
#stopBtn:hover {
background-color: #c0392b;
}
.scenario-card {
background: #fff;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
min-height: 200px;
}
.meta-info {
font-size: 0.9em;
color: #7f8c8d;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
margin-bottom: 20px;
}
.content {
line-height: 1.7;
font-size: 1.1em;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>KoboldCpp Scenario Reader</h1>
<div class="controls">
<div class="file-input-wrapper">
<label for="jsonFile"><strong>Load Kobold JSON:</strong></label>
<input type="file" id="jsonFile" accept=".json">
</div>
<div class="tts-controls">
<label for="voiceSelect"><strong>Voice:</strong></label>
<select id="voiceSelect"><option>Loading voices...</option></select>
<button id="playBtn" disabled>▶ Speak Text</button>
<button id="stopBtn" disabled>⏹ Stop</button>
</div>
</div>
<div class="scenario-card">
<div id="scenarioMeta" class="meta-info">No Kobold data loaded. Select an exported file.</div>
<div id="scenarioContent" class="content">The parsed scenario story text will render here...</div>
</div>
<script>
const jsonFileInput = document.getElementById('jsonFile');
const voiceSelect = document.getElementById('voiceSelect');
const playBtn = document.getElementById('playBtn');
const stopBtn = document.getElementById('stopBtn');
const scenarioMeta = document.getElementById('scenarioMeta');
const scenarioContent = document.getElementById('scenarioContent');
let synth = window.speechSynthesis;
let voices = [];
let cleanNarrativeText = "";
function populateVoiceList() {
if (!synth) return;
voices = synth.getVoices();
voiceSelect.innerHTML = '';
voices.forEach((voice) => {
const option = document.createElement('option');
option.textContent = `${voice.name} (${voice.lang})`;
option.setAttribute('data-lang', voice.lang);
option.setAttribute('data-name', voice.name);
if(voice.lang.includes('en-US') || voice.lang.includes('en_US')) {
option.selected = true;
}
voiceSelect.appendChild(option);
});
}
if (synth) {
populateVoiceList();
if (speechSynthesis.onvoiceschanged !== undefined) {
speechSynthesis.onvoiceschanged = populateVoiceList;
}
} else {
voiceSelect.innerHTML = '<option>TTS not supported</option>';
}
// Helper function to clean text strings from Kobold formats
function cleanText(text) {
if (typeof text !== 'string') return '';
return text
.replace(/\{\{\[INPUT\]\}\}/g, '') // Strip out {{[INPUT]}}
.replace(/\{\{\[OUTPUT\]\}\}/g, '') // Strip out {{[OUTPUT]}}
}
jsonFileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(evt) {
try {
const koboldData = JSON.parse(evt.target.result);
parseKoboldScenario(koboldData);
} catch (error) {
alert('Error parsing JSON. Check if this is a valid Kobold export file.');
console.error(error);
}
};
reader.readAsText(file);
});
function parseKoboldScenario(data) {
let storyBlocks = [];
// 1. Grab base starting prompt text
if (data.prompt) {
const cleanedPrompt = cleanText(data.prompt).trim();
if (cleanedPrompt) storyBlocks.push(cleanedPrompt);
}
// 2. Grab subsequent actions arrays
if (Array.isArray(data.actions)) {
data.actions.forEach(action => {
const cleanedAction = cleanText(action).trim();
if (cleanedAction) {
storyBlocks.push(cleanedAction);
}
});
}
// Fallback checking
if (storyBlocks.length === 0 && data.gametext) {
const cleanedGameText = cleanText(data.gametext).trim();
if (cleanedGameText) storyBlocks.push(cleanedGameText);
}
// Compile blocks into structured presentation text
cleanNarrativeText = storyBlocks.join("\n\n");
if (cleanNarrativeText.trim() === "") {
scenarioMeta.textContent = "Error: JSON structure identified, but no narrative text fields found.";
scenarioContent.textContent = "";
playBtn.disabled = true;
return;
}
// Display Meta details from Kobold configurations
const mode = data.gamemode || "Story/Chat";
const modelUsed = data.model_name || "Local GGUF Model";
scenarioMeta.innerHTML = `<strong>Mode:</strong> ${mode} | <strong>Backend:</strong> ${modelUsed} | <strong>Blocks Parsed:</strong> ${storyBlocks.length}`;
scenarioContent.textContent = cleanNarrativeText;
playBtn.disabled = false;
}
// Speech activation Engine
playBtn.addEventListener('click', () => {
if (!cleanNarrativeText) return;
synth.cancel(); // Flush old processes
const utterance = new SpeechSynthesisUtterance(cleanNarrativeText);
const selectedOption = voiceSelect.selectedOptions[0].getAttribute('data-name');
const targetVoice = voices.find(v => v.name === selectedOption);
if (targetVoice) utterance.voice = targetVoice;
utterance.onstart = () => { playBtn.disabled = true; stopBtn.disabled = false; };
utterance.onend = () => { playBtn.disabled = false; stopBtn.disabled = true; };
utterance.onerror = () => { playBtn.disabled = false; stopBtn.disabled = true; };
synth.speak(utterance);
});
stopBtn.addEventListener('click', () => {
synth.cancel();
playBtn.disabled = false;
stopBtn.disabled = true;
});
</script>
</body>
</html
```>