<html>
<head>
<meta charset=»UTF-8″>
<meta http-equiv=»Content-Security-Policy» content=»default-src ‘self’; connect-src ‘self’ https://login.microsoftonline.com; media-src blob:; img-src ‘self’ data:; style-src ‘self’ ‘unsafe-inline’; script-src ‘self’ ‘unsafe-inline'»>
<title>Tekst til tale-botolf v1.3.0</title>
<link rel=»stylesheet» type=»text/css» href=»main.css»>
<link rel=»stylesheet» href=»loading-wheel.css»>
<script src=»index.js»></script>
<script src=»renderer.js»></script>
</head>
<body>
<h1>Tekst til tale-botolf</h1>
<p>Hurtig knapper for SSML funksjoner</p>
<div class=»ssml-buttons»>
<button id=»btn-none»>ingen</button>
<button id=»btn-100ms»>egendefinert</button>
<button id=»btn-x-weak»>X-kort</button>
<button id=»btn-weak»>Kort</button>
<button id=»btn-medium»>Middels</button>
<button id=»btn-strong»>Lang</button>
<button id=»btn-x-strong»>X-lang</button>
<button id=»insert-email»>E-post</button>
<button id=»insert-phone»>Telefon</button>
</div>
<label for=»input-text»>Skriv inn tekst her:</label>
<textarea id=»input-text»></textarea>
<div id=»email-overlay» class=»hidden overlay»>
<div class=»overlay-content»>
<label for=»email-input»>E-post:</label>
<input id=»email-input» type=»text»>
<button id=»email-insert-button»>Sett inn</button>
<button id=»email-cancel-button»>Avbryt</button>
</div>
</div>
<div id=»phone-overlay» class=»hidden overlay»>
<div class=»overlay-content»>
<label for=»phone-input»>Telefon nummer:</label>
<input id=»phone-input» type=»text»>
<button id=»phone-insert-button»>Sett inn</button>
<button id=»phone-cancel-button»>Avbryt</button>
</div>
</div>
<div id=»loading-wheel» class=»loader» style=»display: none;»></div>
<label for=»ssml-output»>ssml-output:</label>
<textarea id=»ssml-output»></textarea>
<label for=»ssml-template-select»>Velg lese metode:</label>
<select id=»ssml-template-select»>
<option value=»template1″>Talemelding</option>
<option value=»template2″>Instruks</option>
<option value=»template3″>Opplesning</option>
</select>
<label for=»voice-select»>Velg stemme:</label>
<select id=»voice-select»>
<option value=»nb-NO-FinnNeural»>Norwegian Bokmål (Finn)</option>
<option value=»nb-NO-IselinNeural»>Norwegian Bokmål (Iselin)</option>
<option value=»nb-NO-PernilleNeural»>Norwegian Bokmål (Pernille)</option>
<option value=»en-GB-SoniaNeural»> English (Sonia)</option>
<option value=»en-GB-RyanNeural»> English (Ryan)</option>
<option value=»uk-UA-OstapNeural»> Ukrainian Ostap</option>
</select>
<label for=»pitch-slider»>Tone:</label>
<input type=»range» id=»pitch-slider» min=»0″ max=»2″ step=»0.1″ value=»1″>
<label for=»speed-slider»>Hastighet:</label>
<input type=»range» id=»speed-slider» min=»0.5″ max=»2″ step=»0.1″ value=»1″>
<button id=»play-recording-btn»>Generer lyd</button>
<button id=»pause-recording-btn»>Pause</button>
<button id=»stop-recording-btn»>Stop</button>
<button id=»synthesize-btn»>Lagre til fil</button>
</body>
</html>
Renderer.js: const { ipcRenderer } = require(‘electron’);
const msal = require(‘@azure/msal-node’);
const config = {
auth: {
clientId: process.env.CLIENT_ID,
authority: process.env.AUTHORITY,
clientSecret: process.env.CLIENT_SECRET,
},
};
const cca = new msal.ConfidentialClientApplication(config);
// Define the scopes
const scopes = [‘user.read’];
const authCodeUrlParameters = {
scopes: scopes,
redirectUri: «talebotolf://redirect»,
};
cca.getAuthCodeUrl(authCodeUrlParameters).then((authUrl) => {
ipcRenderer.send(‘open-login-window’, authUrl);
});
// Event listeners for the loading wheel
ipcRenderer.on(‘loading:show’, () => {
document.getElementById(‘loading-wheel’).style.display = ‘block’;
});
ipcRenderer.on(‘loading:hide’, () => {
document.getElementById(‘loading-wheel’).style.display = ‘none’;
});
// Event listener for when the DOM content is loaded
document.addEventListener(‘DOMContentLoaded’, () => {
const synthesizeBtn = document.getElementById(‘synthesize-btn’);
const playRecordingBtn = document.getElementById(‘play-recording-btn’);
const inputText = document.getElementById(‘input-text’);
const voiceSelect = document.getElementById(‘voice-select’);
const pitchSlider = document.getElementById(‘pitch-slider’);
const speedSlider = document.getElementById(‘speed-slider’);
const ssmlOutput = document.getElementById(‘ssml-output’);
const insertEmailButton = document.getElementById(‘insert-email’);
const emailInput = document.getElementById(’email-input’);
const textInput = document.getElementById(‘input-text’);
const insertPhoneButton = document.getElementById(‘insert-phone’);
const phoneInput = document.getElementById(‘phone-input’);
const emailOverlay = document.getElementById(’email-overlay’);
const phoneOverlay = document.getElementById(‘phone-overlay’);
const emailInsertButton = document.getElementById(’email-insert-button’);
const emailCancelButton = document.getElementById(’email-cancel-button’);
const phoneInsertButton = document.getElementById(‘phone-insert-button’);
const phoneCancelButton = document.getElementById(‘phone-cancel-button’);
const stopRecordingBtn = document.getElementById(‘stop-recording-btn’);
const pauseRecordingBtn = document.getElementById(‘pause-recording-btn’);
// Event listeners
insertPhoneButton.addEventListener(‘click’, () => {
phoneOverlay.classList.remove(‘hidden’);
});
insertEmailButton.addEventListener(‘click’, () => {
emailOverlay.classList.remove(‘hidden’);
});
emailInsertButton.addEventListener(‘click’, () => {
const email = emailInput.value;
if (email) {
const spelledOutEmail = spellOutEmailNorwegian(email);
const wrappedEmail = `<say-as interpret-as=»e-mail»>${email}</say-as>`;
const textInputValue = textInput.value;
textInput.value = textInputValue + wrappedEmail;
emailInput.value = »;
}
emailOverlay.classList.add(‘hidden’);
});
emailCancelButton.addEventListener(‘click’, () => {
emailInput.value = »;
emailOverlay.classList.add(‘hidden’);
});
phoneInsertButton.addEventListener(‘click’, () => {
const phone = phoneInput.value;
if (phone) {
const wrappedPhone = `<say-as interpret-as=»telephone»>${phone}</say-as>`;
const textInputValue = textInput.value;
textInput.value = textInputValue + wrappedPhone;
phoneInput.value = »;
}
phoneOverlay.classList.add(‘hidden’);
});
phoneCancelButton.addEventListener(‘click’, () => {
phoneInput.value = »;
phoneOverlay.classList.add(‘hidden’);
});
// Initialize audio data variable
let audioData = null;
let audio = null;
function getTemplateContent() {
const selectedTemplate = document.getElementById(‘ssml-template-select’).value;
// SSML templates
const templates = {
template1: `<mstts:silence type=»comma-exact» value=»300ms»/><mstts:silence type=»semicolon-exact» value=»400ms»/><mstts:silence type=»enumerationcomma-exact» value=»500ms»/><mstts:silence type=»period» value=»600ms»/>`,
template2: `<mstts:silence type=»comma-exact» value=»200ms»/><mstts:silence type=»semicolon-exact» value=»300ms»/><mstts:silence type=»enumerationcomma-exact» value=»400ms»/>`,
template3: `<mstts:silence type=»comma-exact» value=»100ms»/><mstts:silence type=»semicolon-exact» value=»200ms»/><mstts:silence type=»enumerationcomma-exact» value=»300ms»/>`
};
const templateContent = templates[selectedTemplate];
return templates[selectedTemplate];
}
function updateSSML(text) {
const templateContent = getTemplateContent();
const ssml = `<speak version=»1.0″ xmlns=»http://www.w3.org/2001/10/synthesis» xml:lang=»en-US»>
<voice name=»${voiceSelect.value}»>
<prosody pitch=»${(pitchSlider.value – 1) * 100}%» rate=»${speedSlider.value}x»>
${templateContent}
${text}
</prosody>
</voice>
</speak>`;
ssmlOutput.value = ssml;
}
// Function for updating the state of the synthesize button
function updateSynthesizeButton() {
synthesizeBtn.disabled = inputText.value.trim() === «»;
}
// Function for synthesizing text, playing and saving audio
function synthesizeText(text, voiceName, pitch, speed, templateContent, saveToFile = false, playAudio = true) {
ipcRenderer.send(‘synthesize’, text, voiceName, pitch, speed, templateContent);
ipcRenderer.once(‘synthesize:done’, (event, receivedAudioData) => {
audioData = receivedAudioData;
// synthesizeText function:
if (playAudio) {
audio = new Audio();
audio.src = URL.createObjectURL(new Blob([audioData], { type: ‘audio/wav’ }));
audio.onended = () => {
URL.revokeObjectURL(audio.src);
playRecordingBtn.disabled = false;
stopRecordingBtn.disabled = true;
pauseRecordingBtn.disabled = true;
};
audio.play();
playRecordingBtn.disabled = true;
stopRecordingBtn.disabled = false;
pauseRecordingBtn.disabled = false;
}
if (saveToFile) {
ipcRenderer.send(‘save-audio-file’, audioData);
}
});
ipcRenderer.once(‘synthesize:error’, (event, errorMessage) => {
showErrorDialog(errorMessage);
});
}
// Function for displaying error dialog
function showErrorDialog(errorMessage) {
const existingModal = document.querySelector(‘.error-modal’);
if (existingModal) {
existingModal.querySelector(‘p’).textContent = errorMessage;
return;
}
const modal = document.createElement(‘div’);
modal.classList.add(‘error-modal’);
modal.innerHTML = `
<div class=»error-modal-content»>
<h2>Feil</h2>
<p>${errorMessage}</p>
<button id=»copy-error-btn»>Kopier feilmelding</button>
<button id=»close-error-btn»>Avbryt</button>
</div>
`;
document.body.appendChild(modal);
// Function for copying error message
function copyErrorMessage() {
navigator.clipboard.writeText(errorMessage)
.then(() => {
console.log(‘Error message copied to clipboard’);
// Display the message under the error
const copyMessage = document.createElement(‘p’);
copyMessage.textContent = ‘Feilmeldingen er kopiert til utklippstavlen’;
copyMessage.style.marginTop = ’30px’;
copyMessage.style.color = ‘yellow’;
modal.querySelector(‘.error-modal-content’).appendChild(copyMessage);
setTimeout(() => {
modal.querySelector(‘.error-modal-content’).removeChild(copyMessage);
}, 3000);
})
.catch(err => {
console.error(‘Could not copy text: ‘, err);
});
}
function closeErrorModal() {
const modal = document.querySelector(‘.error-modal’);
if (modal) {
document.body.removeChild(modal);
}
}
// Add event listeners to modal buttons
const copyErrorBtn = modal.querySelector(‘#copy-error-btn’);
const closeErrorBtn = modal.querySelector(‘#close-error-btn’);
copyErrorBtn.addEventListener(‘click’, copyErrorMessage);
closeErrorBtn.addEventListener(‘click’, closeErrorModal);
}
// Function for inserting SSML tags at the cursor position
function insertSSMLTag(tag) {
const inputTextElement = document.getElementById(‘input-text’);
const cursorPosition = inputTextElement.selectionStart;
const beforeCursor = inputTextElement.value.substring(0, cursorPosition);
const afterCursor = inputTextElement.value.substring(cursorPosition);
inputTextElement.value = `${beforeCursor}${tag}${afterCursor}`;
inputTextElement.focus();
inputTextElement.selectionEnd = cursorPosition + tag.length;
updateSSML(inputTextElement.value);
}
// Event listeners for updating SSML and the synthesize button on input
inputText.addEventListener(‘input’, () => {
updateSSML(inputText.value);
updateSynthesizeButton();
});
pitchSlider.addEventListener(‘input’, () => { updateSSML(inputText.value);
});
speedSlider.addEventListener(‘input’, () => {
updateSSML(inputText.value);
});
// Event listener for the play recording button
playRecordingBtn.addEventListener(‘click’, () => {
const text = inputText.value;
const pitch = (pitchSlider.value – 1) * 100;
const speed = speedSlider.value;
const templateContent = getTemplateContent();
synthesizeText(text, voiceSelect.value, pitch, speed, templateContent);
});
// Event listener for the Stop button
stopRecordingBtn.addEventListener(‘click’, () => {
if (audio) {
audio.pause();
audio.currentTime = 0;
playRecordingBtn.disabled = false;
stopRecordingBtn.disabled = true;
}
});
// Event listener for the pause button
pauseRecordingBtn.addEventListener(‘click’, () => {
if (audio) {
if (audio.paused) {
audio.play();
pauseRecordingBtn.textContent = ‘Pause’;
playRecordingBtn.disabled = true;
} else {
audio.pause();
pauseRecordingBtn.textContent = ‘Fortsett’;
playRecordingBtn.disabled = false;
}
}
});
// Event listener for the synthesize button
synthesizeBtn.addEventListener(‘click’, () => {
const text = inputText.value;
const pitch = (pitchSlider.value – 1) * 100;
const speed = speedSlider.value;
const templateContent = getTemplateContent();
synthesizeText(text, voiceSelect.value, pitch, speed, templateContent, true, false);
});
// Event listeners for inserting SSML break tags
document.getElementById(‘btn-none’).addEventListener(‘click’, () => {
insertSSMLTag(‘<mstts:ttsbreak strength=»none» />’);
});
document.getElementById(‘btn-100ms’).addEventListener(‘click’, () => {
insertSSMLTag(‘<break time=»100ms» />’);
});
document.getElementById(‘btn-x-weak’).addEventListener(‘click’, () => {
insertSSMLTag(‘<break strength=»x-weak» />’);
});
document.getElementById(‘btn-weak’).addEventListener(‘click’, () => {
insertSSMLTag(‘<break strength=»weak» />’);
});
document.getElementById(‘btn-medium’).addEventListener(‘click’, () => {
insertSSMLTag(‘<break strength=»medium» />’);
});
document.getElementById(‘btn-strong’).addEventListener(‘click’, () => {
insertSSMLTag(‘<break strength=»strong» />’);
});
document.getElementById(‘btn-x-strong’).addEventListener(‘click’, () => {
insertSSMLTag(‘<break strength=»x-strong» />’);
});
//event listener for the template selection dropdown
document.getElementById(‘ssml-template-select’).addEventListener(‘change’, () => {
updateSSML(inputText.value);
});
//function for spelling out email
function spellOutEmailNorwegian(email) {
return email.split(»).map(char => (char === ‘@’ ? ‘ alfakrøll ‘ : (char === ‘.’ ? ‘ punktum ‘ : char))).join(‘ ‘);
}
// Initialize the synthesize button state
updateSynthesizeButton();
});