code

Index.js: <!DOCTYPE html>

<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>

  &nbsp; &nbsp;

  <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>

  &nbsp; &nbsp;

  <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();

});