Skip to content

Commit

Permalink
improve text to speech web interface
Browse files Browse the repository at this point in the history
  • Loading branch information
ubaldus committed Dec 31, 2024
1 parent 58942cd commit e94abbc
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 56 deletions.
53 changes: 5 additions & 48 deletions assets/static/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ function displayResults(results, type) {
divContent.className = 'ms-2 me-auto';

const titleLink = document.createElement('a');
titleLink.href = `article?id=${result.article_id}`;
if ('speechSynthesis' in window) {
titleLink.href= `/static/tts.html?id=${result.article_id}&locale=${language}`;
} else {
titleLink.href = `article?id=${result.article_id}`;
}
titleLink.className = 'text-decoration-none';
titleLink.textContent = result.title;

Expand All @@ -133,44 +137,10 @@ function displayResults(results, type) {
}
}

function speakPageContent() {
const elementsToSpeak = document.querySelectorAll('h1, h2, h3, h4, h5, h6, p');
const allText = Array.from(elementsToSpeak)
.map(el => el.textContent)
.join(". ");

utterance = new SpeechSynthesisUtterance(allText);
utterance.lang = language;
utterance.rate = 1.0;
utterance.pitch = 1.0;

speakIcon.classList.remove('bi-volume-up-fill');
speakIcon.classList.add('bi-volume-mute-fill');
isSpeaking = true;

speechSynthesis.speak(utterance);

utterance.onend = () => {
speakIcon.classList.remove('bi-volume-mute-fill');
speakIcon.classList.add('bi-volume-up-fill');
isSpeaking = false;
};
}


let utterance;
let isSpeaking = false;

const speechInput = document.getElementById('speechInput');
const startSpeechButton = document.getElementById('startSpeech');
const speakButton = document.querySelector('.speak-button');
const speakIcon = document.querySelector('.speak-button i');
const searchForm = document.getElementById('searchForm');

if (speakButton && (!'speechSynthesis' in window)) {
speakButton.style.display = 'none';
}

if (startSpeechButton && !/Chrome/.test(navigator.userAgent)) {
startSpeechButton.style.display = 'none';
}
Expand Down Expand Up @@ -213,19 +183,6 @@ if ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window) {
document.getElementById("startSpeech").style.display = "none";
}

if (speakButton) {
speakButton.addEventListener('click', () => {
if (isSpeaking) {
speechSynthesis.cancel();
speakIcon.classList.remove('bi-volume-mute-fill');
speakIcon.classList.add('bi-volume-up-fill');
isSpeaking = false;
} else {
speakPageContent();
}
});
}

if (searchForm) {
createSearchCheckboxes();
searchForm.addEventListener('submit', function (event) {
Expand Down
315 changes: 315 additions & 0 deletions assets/static/tts.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="author" content="ubaldo@eja.it">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="//eja.it/logo/eja.png" rel="icon" type="image/png">
<link href="/static/bootstrap.min.css" rel="stylesheet">
<link href="/static/bootstrap-icons.css" rel="stylesheet">
<style>
.highlight {
background-color: #fff3cd;
transition: background-color 0.3s;
}
.highlight-section {
background-color: #e2e3e5;
transition: background-color 0.3s;
}
.control-buttons {
position: fixed;
top: 20px;
right: 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
z-index: 2000;
}
.control-button {
display: flex;
align-items: center;
justify-content: center;
}
.button-row {
display: flex;
gap: 5px;
}
</style>
</head>
<body>
<div class="container mt-4">
<h1 id="articleTitle" class="text-center mb-4"></h1>
<div id="articleContent" class="mb-4"></div>
</div>
<div class="control-buttons">
<div class="button-row">
<button class="btn btn-sm btn-outline-secondary control-button" id="prevSection">
<i class="bi bi-chevron-up"></i>
</button>
</div>
<div class="button-row">
<button class="btn btn-sm btn-outline-secondary control-button" id="prevText">
<i class="bi bi-chevron-left"></i>
</button>
<button class="btn btn-sm btn-primary control-button" id="playPause">
<i class="bi bi-play-fill" id="playPauseIcon"></i>
</button>
<button class="btn btn-sm btn-outline-secondary control-button" id="nextText">
<i class="bi bi-chevron-right"></i>
</button>
</div>
<div class="button-row">
<button class="btn btn-sm btn-outline-secondary control-button" id="nextSection">
<i class="bi bi-chevron-down"></i>
</button>
</div>
</div>
<script>
let article = null;
let currentSection = 0;
let currentText = 0;
let utterance = null;
let isPlaying = false;
let isLastItem = false;
let isSpeakingSection = false;
let language = 'en-US';

async function fetchArticle(articleId) {
try {
const response = await fetch(`/api/article?id=${articleId}`);
const data = await response.json();
if (data.status === 'success') {
article = data.article[0];
displayArticle();
}
} catch (error) {
console.error('Error fetching article:', error);
}
}

function displayArticle() {
document.getElementById('articleTitle').textContent = article.title;
document.title = article.title;
const container = document.getElementById('articleContent');
container.innerHTML = '';
article.sections.forEach((section, sectionIndex) => {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'mb-4';
const title = document.createElement('h2');
title.textContent = section.title;
title.id = `section-${sectionIndex}`;
sectionDiv.appendChild(title);
section.texts.forEach((text, textIndex) => {
const p = document.createElement('p');
p.textContent = text;
p.id = `text-${sectionIndex}-${textIndex}`;
sectionDiv.appendChild(p);
});
container.appendChild(sectionDiv);
});
}

function speak(text, isSectionTitle = false) {
if (utterance) {
speechSynthesis.cancel();
}
utterance = new SpeechSynthesisUtterance(text);
utterance.lang = language;
isSpeakingSection = isSectionTitle;
if (isSectionTitle) {
utterance.volume = 1.0;
utterance.rate = 0.9;
text += '... ... ...';
}
utterance.onend = () => {
if (isPlaying && !isLastItem) {
if (isSpeakingSection) {
setTimeout(() => {
currentText = 0;
const firstTextElement = document.getElementById(`text-${currentSection}-${currentText}`);
if (firstTextElement) {
speak(firstTextElement.textContent, false);
highlightCurrent(false);
}
}, 2000);
} else {
if (currentText === article.sections[currentSection].texts.length - 1) {
if (currentSection < article.sections.length - 1) {
currentSection++;
currentText = 0;
const sectionTitle = document.getElementById(`section-${currentSection}`);
if (sectionTitle) {
speak(sectionTitle.textContent, true);
highlightCurrent(true);
}
} else {
isLastItem = true;
isPlaying = false;
document.getElementById('playPauseIcon').className = 'bi bi-play-fill';
}
} else {
nextText();
}
}
}
};
speechSynthesis.speak(utterance);
}

function speakCurrent(section = false) {
const currentElement = document.getElementById(`text-${currentSection}-${currentText}`);
const sectionTitle = document.getElementById(`section-${currentSection}`);
const articleTitle = document.getElementById('articleTitle');
if (section) {
if (sectionTitle) {
speak(sectionTitle.textContent, true);
highlightCurrent(true);
}
} else {
if (currentSection === 0 && currentText === 0 && !isSpeakingSection) {
speak(articleTitle.textContent, true);
highlightCurrent(true);
} else if (currentElement) {
speak(currentElement.textContent, false);
highlightCurrent(false);
}
}
}

function highlightCurrent(section = false) {
document.querySelectorAll('.highlight, .highlight-section').forEach(el => {
el.classList.remove('highlight', 'highlight-section');
});
if (section) {
const sectionTitle = document.getElementById(`section-${currentSection}`);
if (sectionTitle) {
sectionTitle.classList.add('highlight-section');
sectionTitle.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
} else {
const currentElement = document.getElementById(`text-${currentSection}-${currentText}`);
if (currentElement) {
currentElement.classList.add('highlight');
currentElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
}

function playPause() {
isPlaying = !isPlaying;
isLastItem = false;
const icon = document.getElementById('playPauseIcon');
if (isPlaying) {
icon.className = 'bi bi-pause-fill';
speakCurrent();
} else {
icon.className = 'bi bi-play-fill';
speechSynthesis.cancel();
}
}

function nextSection() {
if (currentSection < article.sections.length - 1) {
currentSection++;
currentText = 0;
isLastItem = false;
if (isPlaying) {
speakCurrent(true);
} else {
highlightCurrent(true);
}
}
}

function prevSection() {
if (currentSection > 0) {
currentSection--;
currentText = 0;
isLastItem = false;
if (isPlaying) {
speakCurrent(true);
} else {
highlightCurrent(true);
}
}
}

function nextText() {
if (currentText < article.sections[currentSection].texts.length - 1) {
currentText++;
isLastItem = false;
} else if (currentSection < article.sections.length - 1) {
currentSection++;
currentText = 0;
isLastItem = false;
} else {
isLastItem = true;
isPlaying = false;
document.getElementById('playPauseIcon').className = 'bi bi-play-fill';
return;
}
if (isPlaying) {
speakCurrent();
} else {
highlightCurrent();
}
}

function prevText() {
if (currentText > 0) {
currentText--;
} else if (currentSection > 0) {
currentSection--;
currentText = article.sections[currentSection].texts.length - 1;
}
isLastItem = false;
if (isPlaying) {
speakCurrent();
} else {
highlightCurrent();
}
}

document.addEventListener('keydown', (event) => {
switch(event.code) {
case 'Space':
event.preventDefault();
playPause();
break;
case 'ArrowRight':
event.preventDefault();
nextText();
break;
case 'ArrowLeft':
event.preventDefault();
prevText();
break;
case 'ArrowUp':
event.preventDefault();
prevSection();
break;
case 'ArrowDown':
event.preventDefault();
nextSection();
break;
}
});

document.getElementById('playPause').addEventListener('click', playPause);
document.getElementById('nextText').addEventListener('click', nextText);
document.getElementById('prevText').addEventListener('click', prevText);
document.getElementById('nextSection').addEventListener('click', nextSection);
document.getElementById('prevSection').addEventListener('click', prevSection);

const articleId = new URLSearchParams(window.location.search).get('id');
const locale = new URLSearchParams(window.location.search).get('locale');
if (locale) {
language = locale;
}
if (articleId) {
fetchArticle(articleId);
}
</script>
</body>
</html>
Loading

0 comments on commit e94abbc

Please sign in to comment.