Pagination of files list. No Playback option Better clearing of played back audio from memory
1945 lines
73 KiB
HTML
1945 lines
73 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||
<title>AllTalk TTS Generator</title>
|
||
|
||
<style>
|
||
body,
|
||
textarea,
|
||
select,
|
||
input[type="range"],
|
||
input[type="number"],
|
||
button {
|
||
font-family: Arial, sans-serif;
|
||
}
|
||
|
||
body {
|
||
margin: 20px;
|
||
background-color: #f7f7f7;
|
||
color: #333;
|
||
}
|
||
|
||
h1 {
|
||
color: #838383;
|
||
text-align: center;
|
||
}
|
||
|
||
.form-container {
|
||
max-width: 80%;
|
||
margin: 50px auto;
|
||
padding: 20px;
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
box-shadow: 0 2px 5px rgba(0, 0, 0, .2);
|
||
}
|
||
|
||
.settings-section {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
justify-content: flex-start;
|
||
/* Align items to the start */
|
||
align-items: center;
|
||
/* Align items vertically */
|
||
gap: 20px;
|
||
/* This creates a consistent gap between all child elements */
|
||
}
|
||
|
||
.setting {
|
||
margin-bottom: 10px;
|
||
/* Space from bottom if wrapped */
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* Removed flex property to stop forcing them to grow */
|
||
}
|
||
|
||
|
||
label {
|
||
font-size: 0.9em;
|
||
color: #333;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
textarea {
|
||
resize: vertical;
|
||
width: 100%;
|
||
padding: 10px;
|
||
margin-bottom: 16px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* Ensure select and input elements do not exceed the width of their parent */
|
||
select,
|
||
input[type="range"],
|
||
input[type="number"] {
|
||
width: 100%;
|
||
/* They will fill the width of their .setting container */
|
||
padding: 10px;
|
||
margin-bottom: 16px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
@media (min-width: 768px) {
|
||
|
||
select,
|
||
input[type="range"],
|
||
input[type="number"] {
|
||
width: auto;
|
||
/* Only auto width for larger screens */
|
||
}
|
||
}
|
||
|
||
|
||
button {
|
||
background-color: #4caf50;
|
||
color: #fff;
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: inline-block;
|
||
margin: 5px 0;
|
||
}
|
||
|
||
button:hover {
|
||
background-color: #337436;
|
||
}
|
||
|
||
.dark-mode {
|
||
background-color: #121212;
|
||
color: #ffffff;
|
||
/* Text color for dark mode background */
|
||
}
|
||
|
||
.dark-mode .form-container {
|
||
background-color: #1e1e1e;
|
||
}
|
||
|
||
/* Modify the text color for elements within dark mode */
|
||
.dark-mode label,
|
||
.dark-mode textarea,
|
||
.dark-mode select,
|
||
.dark-mode input[type="range"],
|
||
.dark-mode input[type="number"],
|
||
.dark-mode button,
|
||
.dark-mode .error {
|
||
color: #ffffff;
|
||
/* Text color for dark mode elements */
|
||
}
|
||
|
||
/* Modify text color for input elements, textarea, and dropdowns in dark mode */
|
||
.dark-mode input,
|
||
.dark-mode textarea,
|
||
.dark-mode select {
|
||
color: #000000;
|
||
/* Text color for input elements, textarea, and dropdowns in dark mode */
|
||
}
|
||
|
||
.dark-mode input[type="number"] {
|
||
color: #000000;
|
||
/* Text color for inputs in dark mode */
|
||
background-color: #C0C0C0;
|
||
/* Light grey background color for inputs in dark mode */
|
||
}
|
||
|
||
/* Set a default color for the input placeholder to ensure it's visible */
|
||
.dark-mode input::placeholder {
|
||
color: #666;
|
||
/* Light grey color for placeholder text */
|
||
}
|
||
|
||
|
||
|
||
/* Set background color for textarea in dark mode */
|
||
.dark-mode textarea {
|
||
background-color: #C0C0C0;
|
||
/* Light grey background color for textarea in dark mode */
|
||
}
|
||
|
||
/* Set background color for option elements within select in dark mode */
|
||
.dark-mode select {
|
||
background-color: #C0C0C0;
|
||
/* Light grey background color for options in dark mode */
|
||
}
|
||
|
||
/* Additional styles if needed for fixed-width dropdowns */
|
||
.fixed-size-dropdown {
|
||
width: 130px;
|
||
/* Adjust the width as needed */
|
||
}
|
||
|
||
.progress-overlay {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
border-radius: 5px;
|
||
padding: 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* Stack items vertically */
|
||
align-items: center;
|
||
/* Center items horizontally within the container */
|
||
justify-content: center;
|
||
/* Center items vertically within the container */
|
||
z-index: 10;
|
||
}
|
||
|
||
/* Ensure the text is wrapped in a container with a class, e.g., .progress-text */
|
||
.progress-text {
|
||
text-align: right;
|
||
/* Center the text horizontally */
|
||
margin-top: 10px;
|
||
/* Add space between the spinner and the text */
|
||
}
|
||
|
||
|
||
#progressIndicator {
|
||
/* Adjust this if you want to position the spinning disk differently */
|
||
display: inline-block;
|
||
margin-left: 20px;
|
||
/* Give some space between the text and the spinner */
|
||
}
|
||
|
||
|
||
.spinner {
|
||
border: 5px solid #f3f3f3;
|
||
border-top: 5px solid #3498db;
|
||
border-radius: 50%;
|
||
width: 45px;
|
||
height: 45px;
|
||
animation: spin 2s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% {
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.error {
|
||
color: #ff0000;
|
||
}
|
||
|
||
|
||
.slider-setting-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.slider-container {
|
||
display: flex;
|
||
align-items: center;
|
||
flex-grow: 1;
|
||
/* Allows the slider and the value to fill the space */
|
||
margin-left: 10px;
|
||
/* Adjust spacing as needed */
|
||
}
|
||
|
||
#chunkSizeValue,
|
||
#volumeValue {
|
||
margin-left: 10px;
|
||
/* Space between slider and value */
|
||
white-space: nowrap;
|
||
/* Prevents the value from wrapping */
|
||
line-height: normal;
|
||
position: relative;
|
||
bottom: 8px;
|
||
/* Nudge the label up */
|
||
display: inline-block;
|
||
/* Ensure it respects top/bottom positioning */
|
||
line-height: normal;
|
||
/* Adjust line height for vertical centering */
|
||
}
|
||
|
||
/* ... rest of your existing styles ... */
|
||
|
||
/* Responsive design adjustments */
|
||
@media (min-width: 768px) {
|
||
.slider-setting-wrapper {
|
||
flex-wrap: nowrap;
|
||
/* Prevents wrapping on larger screens */
|
||
}
|
||
}
|
||
|
||
.ttsListItem {
|
||
display: flex;
|
||
/* Use flexbox for alignment */
|
||
align-items: center;
|
||
/* Align items vertically */
|
||
gap: 10px;
|
||
/* This creates a consistent gap between all child elements */
|
||
margin-bottom: 0px;
|
||
/* Space between each ttsListItem */
|
||
font-size: 0.8em;
|
||
/* Adjust the font size as needed */
|
||
}
|
||
|
||
.item-field {
|
||
margin-right: 10px;
|
||
/* Add space between fields if needed */
|
||
white-space: nowrap;
|
||
/* Prevent wrapping of text */
|
||
overflow: hidden;
|
||
/* Hide overflow */
|
||
text-overflow: ellipsis;
|
||
/* Add ellipsis for overflow */
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
align-items: center;
|
||
/* This will align the buttons and dropdown vertically */
|
||
gap: 10px;
|
||
/* Adjust the space between buttons and dropdown */
|
||
/* Add any additional styling for spacing, background, etc. */
|
||
}
|
||
|
||
/* If the select box still isn't aligning properly, you may need to adjust the line-height */
|
||
.action-buttons select {
|
||
line-height: normal;
|
||
/* Reset line-height */
|
||
}
|
||
|
||
/* Make sure to reset the margin and padding that might be causing misalignment */
|
||
.action-buttons select,
|
||
.action-buttons button {
|
||
margin: 0;
|
||
}
|
||
|
||
/* If you want to have different widths for different fields, you can add more specific classes */
|
||
.item-field.id {
|
||
width: 16px;
|
||
}
|
||
|
||
.item-field.voice {
|
||
width: 20px;
|
||
}
|
||
|
||
.item-field.language {
|
||
width: 16px;
|
||
}
|
||
|
||
.item-field.wav {
|
||
width: 20px;
|
||
flex-grow: 1;
|
||
/* Allow the wav field to take up remaining space */
|
||
}
|
||
|
||
/* Style buttons a bit smaller */
|
||
.ttsListItem button {
|
||
padding: 5px 10px;
|
||
/* Smaller padding */
|
||
font-size: 0.9em;
|
||
/* Smaller text size */
|
||
margin-left: 0;
|
||
}
|
||
|
||
.tts-list-section {
|
||
background: #353535;
|
||
padding: 20px;
|
||
margin-top: 20px;
|
||
margin-right: auto;
|
||
/* Centers the box on the right */
|
||
margin-left: auto;
|
||
/* Centers the box on the left */
|
||
color: white;
|
||
max-width: 80%;
|
||
box-shadow: 0 2px 5px rgba(0, 0, 0, .2);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.highlight {
|
||
background-color: #545454;
|
||
/* Or any color you prefer */
|
||
color: white;
|
||
/* Other styles for highlighting */
|
||
}
|
||
|
||
.highlight-count {
|
||
color: #ff7300;
|
||
/* Change to the color you want */
|
||
}
|
||
|
||
.label {
|
||
font-weight: bold;
|
||
/* Makes the text bold */
|
||
color: #d8d8d8;
|
||
/* A lighter grey to distinguish labels from values */
|
||
}
|
||
|
||
.value {
|
||
color: #bbbbbb;
|
||
/* Keeps the values in white for contrast and readability */
|
||
}
|
||
|
||
.edit-btn {
|
||
/* Styles for the edit button */
|
||
margin-left: 5px;
|
||
padding: 3px 6px;
|
||
font-size: 0.8em;
|
||
}
|
||
|
||
.edit-input {
|
||
/* Styles for the edit input field */
|
||
margin-top: 5px;
|
||
font-size: 0.9em;
|
||
width: 100%;
|
||
/* or adjust as needed */
|
||
}
|
||
|
||
#ttsProgressInfo {
|
||
position: absolute;
|
||
/* Or position it according to your layout */
|
||
top: 10px;
|
||
/* Adjust as needed */
|
||
right: 10px;
|
||
/* Adjust as needed */
|
||
background: #fff;
|
||
/* Or any other color that fits your design */
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
z-index: 10;
|
||
/* Ensure it's above other page content but below overlays */
|
||
}
|
||
|
||
.progress-text-inline {
|
||
margin-left: 45px;
|
||
/* Adjust the value as needed */
|
||
}
|
||
|
||
/* Pagination Buttons Styling */
|
||
.pagination-btn {
|
||
padding: 5px 10px;
|
||
/* Adjust padding to reduce height */
|
||
font-size: 0.8em;
|
||
/* Adjust font size for smaller buttons */
|
||
line-height: 1;
|
||
/* Adjust line height to match the smaller size */
|
||
margin: 2px;
|
||
/* Optional: adjust spacing between buttons */
|
||
}
|
||
|
||
/* Smaller input field for jump-to-page */
|
||
.pagination-input {
|
||
height: 20px;
|
||
/* Adjust height for smaller input field */
|
||
padding: 2px 5px;
|
||
/* Adjust padding for smaller input field */
|
||
font-size: 0.8em;
|
||
/* Adjust font size for smaller input field */
|
||
line-height: 1;
|
||
/* Adjust line height to match the smaller size */
|
||
vertical-align: middle;
|
||
/* Align with buttons if necessary */
|
||
}
|
||
|
||
/* Apply the styles to your pagination controls */
|
||
#paginationControls button {
|
||
/* Apply the button styling */
|
||
height: auto;
|
||
/* Let padding control the height */
|
||
min-height: 20px;
|
||
/* Set a minimum height if needed */
|
||
}
|
||
|
||
#paginationControls input[type="number"] {
|
||
/* Apply the input styling */
|
||
}
|
||
|
||
/* Smaller input field for jump-to-page */
|
||
.pagination-input {
|
||
height: 2px;
|
||
/* Reduce the height */
|
||
padding: 0px 0px;
|
||
/* Reduce the padding to decrease overall height */
|
||
font-size: 0.8em;
|
||
/* Optionally reduce the font size */
|
||
margin-bottom: 13px;
|
||
margin-top: 2px;
|
||
border: 0px solid #ccc;
|
||
/* Adjust border styling if needed */
|
||
line-height: normal;
|
||
/* Adjust line height to match the new size */
|
||
vertical-align: top;
|
||
/* Align vertically with buttons */
|
||
}
|
||
|
||
/* Apply the styles to your pagination controls */
|
||
#paginationControls1 button {
|
||
/* Apply the button styling */
|
||
height: auto;
|
||
/* Let padding control the height */
|
||
min-height: 20px;
|
||
/* Set a minimum height if needed */
|
||
margin-top: 15px
|
||
}
|
||
|
||
#paginationControls1 input[type="number"] {
|
||
/* Apply the input styling */
|
||
margin-top: 15px
|
||
}
|
||
</style>
|
||
|
||
|
||
</style>
|
||
|
||
</head>
|
||
<h1>AllTalk TTS Generator</h1>
|
||
|
||
<div class="form-container">
|
||
<div class="input-section">
|
||
<label for="text_input">Text Input:</label>
|
||
<textarea id="text_input" rows="8"></textarea> <!-- Removed cols attribute -->
|
||
</div>
|
||
|
||
<div class="settings-section">
|
||
|
||
<div class="setting voice-setting">
|
||
<label for="character_voice_gen">Character Voice:</label>
|
||
<select id="character_voice_gen"></select>
|
||
</div>
|
||
|
||
<div class="setting language-setting">
|
||
<label for="language">Language:</label>
|
||
<select id="language" name="language" class="fixed-size-dropdown">
|
||
<option value="ar">Arabic</option>
|
||
<option value="zh-cn">Chinese (Simplified)</option>
|
||
<option value="cs">Czech</option>
|
||
<option value="nl">Dutch</option>
|
||
<option value="en" selected>English</option>
|
||
<option value="fr">French</option>
|
||
<option value="de">German</option>
|
||
<option value="hu">Hungarian</option>
|
||
<option value="it">Italian</option>
|
||
<option value="ja">Japanese</option>
|
||
<option value="ko">Korean</option>
|
||
<option value="pl">Polish</option>
|
||
<option value="pt">Portuguese</option>
|
||
<option value="ru">Russian</option>
|
||
<option value="es">Spanish</option>
|
||
<option value="tr">Turkish</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="setting play-mode-type">
|
||
<label for="playType">Play Type:</label>
|
||
<select id="playType" name="playType" class="fixed-size-dropdown">
|
||
<option value="chunk" selected>Wav Chunks</option>
|
||
<option value="stream">Streaming</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="setting play-mode-setting">
|
||
<label for="playMode">Playback:</label>
|
||
<select id="playMode" name="playMode" class="fixed-size-dropdown">
|
||
<option value="local" selected>In Browser</option>
|
||
<option value="remote">On Server</option>
|
||
<option value="none">No Playback</option>
|
||
</select>
|
||
</div>
|
||
|
||
|
||
<div class="setting slider-setting-wrapper">
|
||
<label for="chunkSize">Chunk Sizes:</label>
|
||
<div class="slider-container">
|
||
<input type="range" id="chunkSize" name="chunkSize" class="slider-setting" min="1" max="8" value="2"
|
||
oninput="updateChunkSizeValue(this.value)">
|
||
<span id="chunkSizeValue">2</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="setting slider-setting-wrapper">
|
||
<label for="volume">Volume:</label>
|
||
<div class="slider-container">
|
||
<input type="range" id="volume" name="volume" class="slider-setting" min="0.0" max="1.0" step="0.1"
|
||
value="0.8" oninput="updateVolumeValue(this.value)">
|
||
<span id="volumeValue">0.8</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="action-buttons">
|
||
<!-- Your existing buttons -->
|
||
<button onclick="generateTTS()">Generate TTS</button>
|
||
<button class="toggle-playback" onclick="togglePausePlayback()">Pause/Resume</button>
|
||
<button onclick="stopPlayback()">Stop Playback</button>
|
||
<button onclick="previewVoice()">Preview Voice</button>
|
||
<select id="themeMode" name="themeMode">
|
||
<option value="light">Light Mode</option>
|
||
<option value="dark">Dark Mode</option>
|
||
</select>
|
||
|
||
<!-- Progress information -->
|
||
<p id="wordCount" class="progress-text-inline">Word Count: 0</p>
|
||
<p id="generationProgress" class="progress-text-inline">         Idle</p>
|
||
</div>
|
||
|
||
|
||
<div id="progressIndicator" class="progress-overlay" style="display: none;">
|
||
<div class="spinner"></div>
|
||
<p id="progressText"></p> <!-- Text to display progress information -->
|
||
</div>
|
||
</div>
|
||
|
||
<div id="ttsListSection" class="tts-list-section">
|
||
<h2>Generated TTS List</h2>
|
||
<input type="number" id="startFromId" value="1" min="1" autocomplete="off" style="width: 60px; margin-right: 10px;">
|
||
<button id="playPauseList" onclick="togglePlayPauseList()" style="margin-right: 10px;">Play List</button>
|
||
<button onclick="stopPlayback()" style="margin-right: 10px;">Stop Playback</button>
|
||
<button id="clearList" onclick="clearTTSList()" style="margin-right: 10px;">Clear List</button>
|
||
<button onclick="exportToJSON()" style="margin-right: 10px;">Export List as JSON</button>
|
||
<button onclick="importFromJSON()" style="margin-right: 10px;">Import JSON to list</button>
|
||
<button onclick="exportMP3()" style="margin-right: 10px;">Export to Wav</button>
|
||
<div id="paginationControls">
|
||
<button id="prevPage" class="pagination-btn">Previous</button>
|
||
<span id="pageIndicator">Page 1 of X</span>
|
||
<button id="nextPage" class="pagination-btn">Next</button>
|
||
<input type="number" id="jumpToPage" class="pagination-input" min="1" placeholder="Jump to Page">
|
||
<button id="jumpButton" class="pagination-btn">Go</button>
|
||
</div>
|
||
<div id="ttsListDisplay" class="tts-list-display">
|
||
<!-- Dynamically populated TTS details will go here -->
|
||
</div>
|
||
<div id="paginationControls1">
|
||
<button id="prevPage1" class="pagination-btn">Previous</button>
|
||
<span id="pageIndicator1">Page 1 of X</span>
|
||
<button id="nextPage1" class="pagination-btn">Next</button>
|
||
<input type="number" id="jumpToPage1" class="pagination-input" min="1" placeholder="Jump to Page">
|
||
<button id="jumpButton1" class="pagination-btn">Go</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Howler.js script inclusion -->
|
||
<script src="./howler.min.js"></script>
|
||
<script type="module">
|
||
// Import Crunker from the CDN
|
||
import Crunker from './crunker.esm.js';
|
||
|
||
async function exportMP3() {
|
||
const crunker = new Crunker();
|
||
const fileUrls = ttsList.map(item => item.fileUrl);
|
||
const progressIndicator = document.getElementById('progressIndicator');
|
||
const progressText = document.getElementById('progressText');
|
||
|
||
// Show progress indicator and update text
|
||
if (progressIndicator && progressText) {
|
||
progressText.textContent = 'Export';
|
||
progressText.style.color = 'black'; // Set the text color to black
|
||
progressIndicator.style.display = 'block';
|
||
}
|
||
|
||
try {
|
||
const audioBuffers = await crunker.fetchAudio(...fileUrls);
|
||
const combined = crunker.concatAudio(audioBuffers);
|
||
const output = crunker.export(combined, "audio/wav");
|
||
crunker.download(output.blob, "combined_audio"); // This will download the file as 'combined_audio.mp3'
|
||
} catch (error) {
|
||
console.error('Error combining files:', error);
|
||
if (progressText) {
|
||
progressText.textContent = 'Error during export!';
|
||
}
|
||
} finally {
|
||
// Hide progress indicator after completion or error
|
||
if (progressIndicator) {
|
||
progressIndicator.style.display = 'none';
|
||
}
|
||
}
|
||
}
|
||
|
||
// Attach exportMP3 to the window object to make it globally accessible
|
||
window.exportMP3 = exportMP3;
|
||
</script>
|
||
|
||
|
||
<script>
|
||
|
||
/////////////////////////////////////////
|
||
//////// Base Settings/variables ////////
|
||
/////////////////////////////////////////
|
||
|
||
let stopTTSGeneration = false;
|
||
let audioQueue = [];
|
||
let currentlyPlayingIndex = 0;
|
||
let isPlaying = false;
|
||
let playModeValue = localStorage.getItem('playMode') || 'local';
|
||
let currentSound = null;
|
||
let preloadThreshold = 2; // Number of files to preload before starting playback
|
||
let isStreamInitialized = false;
|
||
let characterVoice = document.getElementById("character_voice_gen").value;
|
||
let ttsList = []; // Global list to store TTS details
|
||
const howlInstances = {};
|
||
let listPlaying = false;
|
||
let listPlayIndex = 0;
|
||
let currentPage = 1;
|
||
let itemsPerPage = 100; // Adjust as needed
|
||
|
||
|
||
////////////////////////////////////
|
||
//////// Progress Indicator ////////
|
||
////////////////////////////////////
|
||
|
||
function showProgress(currentChunk, totalChunks, playType) {
|
||
const progressIndicator = document.getElementById('progressIndicator');
|
||
const generationProgress = document.getElementById('generationProgress');
|
||
|
||
if (progressIndicator && generationProgress) {
|
||
let message = '';
|
||
//console.log("PlayType is:", playType);
|
||
if (playType === 'stream') {
|
||
// When streaming, display a different message
|
||
message = '<span class="highlight-count">      Streaming...</span>';
|
||
} else {
|
||
// When generating chunks, show the progress of chunks
|
||
message = `      Generating chunk <span class="highlight-count">${currentChunk}</span> of <span class="highlight-count">${totalChunks}</span>`;
|
||
}
|
||
|
||
generationProgress.innerHTML = message;
|
||
progressIndicator.style.display = 'block'; // Show progress overlay
|
||
progressText.textContent = 'Running';
|
||
progressText.style.color = 'black'; // Set the text color to black
|
||
} else {
|
||
console.error("Progress elements not found!");
|
||
}
|
||
}
|
||
|
||
|
||
function hideProgress() {
|
||
const progressIndicator = document.getElementById('progressIndicator');
|
||
if (progressIndicator) {
|
||
progressIndicator.style.display = 'none'; // Hide progress overlay
|
||
}
|
||
}
|
||
|
||
function updateWordCount(wordCount) {
|
||
document.getElementById('wordCount').innerHTML = `Word Count: <span class="highlight-count">${wordCount}</span>`;
|
||
}
|
||
|
||
function updateGenerationProgress(current, total) {
|
||
document.getElementById('generationProgress').innerHTML = `         Idle">${total}</span>`;
|
||
}
|
||
|
||
|
||
///////////////////////////////////////////
|
||
//////// TTS Generation Activities ////////
|
||
///////////////////////////////////////////
|
||
|
||
async function generateTTS() {
|
||
try {
|
||
stopTTSGeneration = false;
|
||
const textInput = document.getElementById("text_input").value;
|
||
const language = document.getElementById("language").value;
|
||
const characterVoice = document.getElementById("character_voice_gen").value;
|
||
const volume = parseFloat(document.getElementById("volume").value);
|
||
const chunkSize = parseInt(document.getElementById("chunkSize").value);
|
||
const playType = document.getElementById('playType').value; // Play type (chunk or stream)
|
||
playModeValue = document.getElementById('playMode').value;
|
||
|
||
let cleanedText = textInput.replace(/ \- | \– /g, ' ');
|
||
cleanedText = cleanedText.replace(/%/g, " percent");
|
||
cleanedText = cleanedText.replace(/\.\s'/g, ".'");
|
||
cleanedText = cleanedText.replace(/[^a-zA-Z0-9\s\.,;:!?\-\'"À-ÿ\u0400-\u04FF\u0150\u0151\u0170\u0171]\$/g, '');
|
||
let sentences = cleanedText.split(/(?<=[.!?:])\s+/);
|
||
|
||
if (!sentences) {
|
||
console.error("No sentences found in the input text.");
|
||
return; // Exit the function if no sentences are found
|
||
}
|
||
|
||
// Calculate total chunks and word count
|
||
let totalChunks = Math.ceil(sentences.length / chunkSize);
|
||
let wordCount = cleanedText.split(/\s+/).length;
|
||
|
||
// Update word count and initial progress information
|
||
updateWordCount(wordCount);
|
||
|
||
if (playType === 'stream') {
|
||
console.log("Starting stream...");
|
||
showProgress(null, null, playType);
|
||
await generateAndStream(cleanedText, language, characterVoice, volume);
|
||
} else {
|
||
const chunks = [];
|
||
for (let i = 0; i < sentences.length; i += chunkSize) {
|
||
let chunk = sentences.slice(i, i + chunkSize).map(s => s.trim()).join(' ');
|
||
chunks.push(chunk);
|
||
}
|
||
|
||
// Process each chunk
|
||
for (let i = 0; i < chunks.length; i++) {
|
||
if (stopTTSGeneration) break;
|
||
showProgress(i + 1, totalChunks, playType);
|
||
if (playModeValue === 'local') {
|
||
// Generate the chunk and play it locally
|
||
await generateAndCache(chunks[i], language, characterVoice, volume);
|
||
} else if (playModeValue === 'remote') {
|
||
// Generate the chunk and play it remotely
|
||
await generateAndPlayRemote(chunks[i], language, characterVoice, volume);
|
||
} else if (playModeValue === 'none') {
|
||
// Generate the chunk but do not play it
|
||
await generateWithoutPlayback(chunks[i], language, characterVoice);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Error in generateTTS:", error);
|
||
} finally {
|
||
// Hide progress when done or on error
|
||
hideProgress();
|
||
}
|
||
}
|
||
|
||
async function processChunks(chunks, language, characterVoice, volume) {
|
||
for (let i = 0; i < chunks.length; i++) {
|
||
if (stopTTSGeneration) break;
|
||
await generateAndCache(chunks[i], language, characterVoice, volume);
|
||
if (audioQueue.length >= preloadThreshold && !isPlaying) {
|
||
playNextLocalChunk();
|
||
}
|
||
}
|
||
}
|
||
|
||
async function generateAndCache(chunk, language, characterVoice, volume, id = null) {
|
||
try {
|
||
const data = new URLSearchParams({
|
||
text_input: chunk.trim(),
|
||
text_filtering: "standard",
|
||
character_voice_gen: characterVoice,
|
||
narrator_enabled: "false",
|
||
narrator_voice_gen: characterVoice,
|
||
text_not_inside: "character",
|
||
language: language,
|
||
output_file_name: "TTS",
|
||
output_file_timestamp: "true",
|
||
autoplay: "false",
|
||
autoplay_volume: volume
|
||
});
|
||
|
||
const response = await fetch("http://127.0.0.1:7851/api/tts-generate", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
},
|
||
body: data,
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.output_file_url) {
|
||
//console.log('TTS Generation Result:', result);
|
||
// If an ID was passed in, we're regenerating an existing item
|
||
if (id !== null) {
|
||
let itemIndex = ttsList.findIndex(item => item.id === id);
|
||
if (itemIndex !== -1) {
|
||
ttsList[itemIndex].fileUrl = result.output_file_url;
|
||
// Other properties can be updated as needed, e.g., in case the language or voice has changed
|
||
ttsList[itemIndex].language = language;
|
||
ttsList[itemIndex].characterVoice = characterVoice;
|
||
}
|
||
} else {
|
||
// No ID was passed in, so we add a new item to the list
|
||
addToTTSList(result.output_file_url, chunk, characterVoice, language);
|
||
}
|
||
const newPage = Math.ceil(ttsList.length / itemsPerPage);
|
||
//console.log('New Page Calculated:', newPage);
|
||
if (currentPage !== newPage) {
|
||
currentPage = newPage; // Move to the new page where the item was added
|
||
//console.log('Updating Current Page to:', currentPage);
|
||
}
|
||
|
||
// Refresh the list display to reflect the new pagination state
|
||
updateUIList();
|
||
|
||
// Update the audio queue and UI
|
||
const sound = new Howl({
|
||
src: [result.output_file_url],
|
||
volume: volume,
|
||
onend: function () {
|
||
isPlaying = false;
|
||
playNextLocalChunk();
|
||
}
|
||
});
|
||
audioQueue.push(sound);
|
||
updateUIList(); // Refresh the list display
|
||
if (audioQueue.length >= preloadThreshold && !isPlaying) {
|
||
playNextLocalChunk();
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Error in generateAndCache:", error);
|
||
}
|
||
}
|
||
|
||
async function generateWithoutPlayback(chunk, language, characterVoice, id = null) {
|
||
// Perform the generation process as usual but do not play the audio
|
||
try {
|
||
const data = new URLSearchParams({
|
||
text_input: chunk.trim(),
|
||
text_filtering: "standard",
|
||
character_voice_gen: characterVoice,
|
||
narrator_enabled: "false",
|
||
narrator_voice_gen: characterVoice,
|
||
text_not_inside: "character",
|
||
language: language,
|
||
output_file_name: "TTS",
|
||
output_file_timestamp: "true",
|
||
autoplay: "false",
|
||
autoplay_volume: "0.1"
|
||
});
|
||
const response = await fetch("http://127.0.0.1:7851/api/tts-generate", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
},
|
||
body: data,
|
||
});
|
||
const result = await response.json();
|
||
if (result.output_file_url) {
|
||
// If an ID was passed in, we're regenerating an existing item
|
||
if (id !== null) {
|
||
let itemIndex = ttsList.findIndex(item => item.id === id);
|
||
if (itemIndex !== -1) {
|
||
ttsList[itemIndex].fileUrl = result.output_file_url;
|
||
// Other properties can be updated as needed, e.g., in case the language or voice has changed
|
||
ttsList[itemIndex].language = language;
|
||
ttsList[itemIndex].characterVoice = characterVoice;
|
||
}
|
||
} else {
|
||
// No ID was passed in, so we add a new item to the list
|
||
addToTTSList(result.output_file_url, chunk, characterVoice, language);
|
||
}
|
||
const newPage = Math.ceil(ttsList.length / itemsPerPage);
|
||
if (currentPage !== newPage) {
|
||
currentPage = newPage; // Move to the new page where the item was added
|
||
}
|
||
updateUIList();
|
||
}
|
||
} catch (error) {
|
||
console.error("Error in generateWithoutPlayback:", error);
|
||
}
|
||
}
|
||
|
||
|
||
function addToTTSList(fileUrl, text, characterVoice, language) {
|
||
try {
|
||
ttsList.push({
|
||
id: ttsList.length + 1,
|
||
fileUrl,
|
||
text,
|
||
characterVoice,
|
||
language
|
||
});
|
||
updateUIList();
|
||
} catch (e) {
|
||
console.error('Error inside addToTTSList:', e);
|
||
}
|
||
}
|
||
|
||
|
||
async function regenerateTTS(id) {
|
||
// Find the details of the TTS to regenerate
|
||
const ttsDetails = ttsList.find(item => item.id === id);
|
||
if (!ttsDetails) return;
|
||
|
||
// Retrieve the current volume setting from the HTML input
|
||
const currentVolume = parseFloat(document.getElementById("volume").value);
|
||
|
||
// Call the TTS generation with the same details and current volume
|
||
await generateAndCache(ttsDetails.text, ttsDetails.language, ttsDetails.characterVoice, currentVolume, id);
|
||
}
|
||
|
||
function updateUIList() {
|
||
const listDisplay = document.getElementById('ttsListDisplay');
|
||
if (!listDisplay) {
|
||
console.error('TTS list display element not found.');
|
||
return;
|
||
}
|
||
listDisplay.innerHTML = ''; // Clear the current list
|
||
|
||
// Determine the range of items to display for pagination
|
||
const start = (currentPage - 1) * itemsPerPage;
|
||
const end = start + itemsPerPage;
|
||
const paginatedItems = ttsList.slice(start, end);
|
||
|
||
// Add items to the display for the current page
|
||
paginatedItems.forEach(item => {
|
||
// Extract just the file name from the URL
|
||
const wavFileName = item.fileUrl.split('/').pop();
|
||
|
||
// Display only the first 50 characters of the text
|
||
const displayText = item.text.length > 175 ? item.text.substring(0, 175) + '...' : item.text;
|
||
|
||
const entryDiv = document.createElement('div');
|
||
entryDiv.id = `item-${item.id}`; // Assign an ID to the div for highlighting
|
||
entryDiv.classList.add('ttsListItem'); // Add a class for styling
|
||
|
||
// Update the Play/Stop button's onclick to call a function that handles playing and highlighting
|
||
entryDiv.innerHTML = `
|
||
<button onclick="regenerateTTS(${item.id})">Regenerate</button>
|
||
<button onclick="playSound('${item.fileUrl}', ${item.id})">Play/Stop</button>
|
||
<span class="label">ID:</span> <span class="value">${item.id}</span>
|
||
<span class="label">Voice:</span> <span class="value">${item.characterVoice}</span>
|
||
<span class="label">Language:</span> <span class="value">${item.language}</span>
|
||
<span class="label">Wav:</span> <span class="value">${wavFileName}</span>
|
||
<button id="edit-btn-${item.id}" class="edit-btn" onclick="editText(${item.id})">Edit</button>
|
||
<span class="item-field text-preview value" title="${item.text}" data-fulltext="${item.text}">${displayText}</span>
|
||
`;
|
||
listDisplay.appendChild(entryDiv);
|
||
});
|
||
|
||
// Update pagination controls
|
||
const lastPage = Math.ceil(ttsList.length / itemsPerPage);
|
||
document.getElementById('pageIndicator').innerText = `Page ${currentPage} of ${lastPage}`;
|
||
document.getElementById('pageIndicator1').innerText = `Page ${currentPage} of ${lastPage}`;
|
||
document.getElementById('prevPage').disabled = (currentPage === 1);
|
||
document.getElementById('nextPage').disabled = (currentPage === lastPage);
|
||
}
|
||
|
||
|
||
async function generateAndPlayRemote(chunk, language, characterVoice, volume) {
|
||
try {
|
||
if (stopTTSGeneration) {
|
||
console.log("TTS generation stopped.");
|
||
return; // Early return if stop flag is set
|
||
}
|
||
const data = new URLSearchParams({
|
||
text_input: chunk.trim(),
|
||
text_filtering: "standard",
|
||
character_voice_gen: characterVoice,
|
||
narrator_enabled: "false",
|
||
narrator_voice_gen: characterVoice,
|
||
text_not_inside: "character",
|
||
language: language,
|
||
output_file_name: "TTS",
|
||
output_file_timestamp: "true",
|
||
autoplay: "true",
|
||
autoplay_volume: volume,
|
||
});
|
||
|
||
const response = await fetch("http://127.0.0.1:7851/api/tts-generate", {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/x-www-form-urlencoded",
|
||
},
|
||
body: data,
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (!response.ok) {
|
||
throw new Error(`API error: ${result.detail || 'Unknown error'}`);
|
||
}
|
||
|
||
console.log("TTS generation complete for chunk:", chunk);
|
||
} catch (error) {
|
||
console.error("Error in generateAndPlayRemote:", error);
|
||
}
|
||
}
|
||
|
||
async function generateAndStream(textInput, language, characterVoice, volume) {
|
||
try {
|
||
const queryParams = new URLSearchParams({
|
||
text: textInput,
|
||
voice: characterVoice,
|
||
language: language,
|
||
output_file: "streaming_file",
|
||
streaming: "True"
|
||
});
|
||
|
||
// Reinitialize currentSound every time
|
||
currentSound = new Howl({
|
||
src: [`http://127.0.0.1:7851/tts-demo-request?${queryParams.toString()}`],
|
||
volume: volume,
|
||
html5: true // Force HTML5 Audio to enable streaming
|
||
});
|
||
|
||
currentSound.play();
|
||
isStreamInitialized = true;
|
||
} catch (error) {
|
||
console.error("Error in generateAndStream:", error);
|
||
}
|
||
}
|
||
|
||
|
||
//////////////////////////////////////////
|
||
//////// Audio Playback Functions ////////
|
||
//////////////////////////////////////////
|
||
|
||
function playSound(fileUrl) {
|
||
if (!fileUrl) {
|
||
console.error('No file URL provided');
|
||
return;
|
||
}
|
||
|
||
// Check if a Howl instance exists for this file URL
|
||
if (!howlInstances[fileUrl]) {
|
||
// Create a new Howl instance if it doesn't exist
|
||
howlInstances[fileUrl] = new Howl({
|
||
src: [fileUrl],
|
||
volume: parseFloat(document.getElementById("volume").value),
|
||
onplay: function () {
|
||
},
|
||
onend: function () {
|
||
},
|
||
onpause: function () {
|
||
},
|
||
onloaderror: function (e) {
|
||
console.error('Load error for', fileUrl, ':', e);
|
||
},
|
||
onplayerror: function (e) {
|
||
console.error('Playback error for', fileUrl, ':', e);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Toggle play/stop
|
||
const sound = howlInstances[fileUrl];
|
||
if (sound.playing()) {
|
||
sound.pause();
|
||
} else {
|
||
sound.play();
|
||
}
|
||
}
|
||
|
||
async function previewVoice() {
|
||
const characterVoice = document.getElementById("character_voice_gen").value;
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append("voice", characterVoice);
|
||
|
||
const response = await fetch("http://127.0.0.1:7851/api/previewvoice/", {
|
||
method: "POST",
|
||
body: formData,
|
||
});
|
||
|
||
const result = await response.json();
|
||
if (result.status === "generate-success") {
|
||
// Append a timestamp to the URL to avoid caching
|
||
const audioUrlWithTimestamp = result.output_file_url + "?t=" + new Date().getTime();
|
||
|
||
// Play the preview audio using Howler.js
|
||
const previewSound = new Howl({
|
||
src: [audioUrlWithTimestamp]
|
||
});
|
||
previewSound.play();
|
||
} else {
|
||
throw new Error('Preview generation failed');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error in previewVoice:', error);
|
||
}
|
||
}
|
||
|
||
// Modified togglePausePlayback function
|
||
function togglePausePlayback() {
|
||
if (currentSound && currentSound.playing()) {
|
||
currentSound.pause();
|
||
} else if (currentSound) {
|
||
currentSound.play();
|
||
}
|
||
}
|
||
|
||
// Function to handle changes in play type selection
|
||
function updatePlayTypeSettings() {
|
||
const playType = document.getElementById('playType').value;
|
||
const chunkSizeInput = document.getElementById('chunkSize');
|
||
const playModeSelect = document.getElementById('playMode');
|
||
|
||
// Disable chunk size and play mode selection for streaming
|
||
if (playType === 'stream') {
|
||
chunkSizeInput.disabled = true;
|
||
playModeSelect.value = 'local';
|
||
playModeSelect.disabled = true;
|
||
} else {
|
||
chunkSizeInput.disabled = false;
|
||
playModeSelect.disabled = false;
|
||
}
|
||
}
|
||
|
||
// Function to toggle between playing and pausing the stream, and update button text
|
||
function toggleStreamPlayback() {
|
||
const streamToggleButton = document.getElementById('streamToggle');
|
||
|
||
if (!isStreamInitialized || !currentSound) {
|
||
// If the stream is not initialized or there is no current sound, start the stream
|
||
generateAndStream(textInput, language, characterVoice, volume); // These variables need to be defined or passed to this function
|
||
streamToggleButton.textContent = 'Pause/Resume'; // Change button text to "Pause"
|
||
} else {
|
||
// If the stream is already initialized, toggle play/pause
|
||
if (currentSound.playing()) {
|
||
currentSound.pause();
|
||
streamToggleButton.textContent = 'Pause/Resume'; // Change button text to "Resume"
|
||
} else {
|
||
currentSound.play();
|
||
streamToggleButton.textContent = 'Pause/Resume'; // Change button text to "Pause"
|
||
}
|
||
}
|
||
isStreamInitialized = true; // Set flag to indicate the stream is initialized
|
||
}
|
||
|
||
// Function to pause the current stream
|
||
function pauseStream() {
|
||
if (currentSound) {
|
||
currentSound.pause();
|
||
}
|
||
}
|
||
|
||
// Function to resume playing the current stream
|
||
function playStream() {
|
||
if (currentSound) {
|
||
currentSound.play();
|
||
}
|
||
}
|
||
|
||
// Unified toggle function for both streaming and chunk playback
|
||
function togglePausePlayback() {
|
||
if (isStreamingMode) {
|
||
// In streaming mode, toggle the stream play/pause
|
||
if (currentSound && currentSound.playing()) {
|
||
currentSound.pause();
|
||
} else if (currentSound) {
|
||
currentSound.play();
|
||
}
|
||
} else {
|
||
// In chunk mode, toggle the chunk play/pause
|
||
if (isPlaying && currentlyPlayingIndex > 0) {
|
||
let currentSound = audioQueue[currentlyPlayingIndex - 1];
|
||
if (currentSound.playing()) {
|
||
currentSound.pause();
|
||
} else {
|
||
currentSound.play();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function playNextLocalChunk() {
|
||
if (audioQueue.length > 0 && currentlyPlayingIndex < audioQueue.length) {
|
||
isPlaying = true;
|
||
let currentSound = audioQueue[currentlyPlayingIndex];
|
||
currentSound.play();
|
||
|
||
// Once a sound finishes playing, unload it
|
||
currentSound.on('end', () => {
|
||
currentSound.unload();
|
||
});
|
||
|
||
currentlyPlayingIndex++;
|
||
} else {
|
||
isPlaying = false;
|
||
}
|
||
}
|
||
|
||
|
||
function stopPlayback() {
|
||
Howler.stop();
|
||
audioQueue = [];
|
||
// Unload each sound in the queue
|
||
audioQueue.forEach(sound => sound.unload());
|
||
currentlyPlayingIndex = 0;
|
||
isPlaying = false;
|
||
}
|
||
|
||
function updatePlaybackButton() {
|
||
const playbackButton = document.querySelector(".toggle-playback");
|
||
|
||
if (isStreamingMode) {
|
||
// Set button text to "Pause" when switching to streaming mode
|
||
playbackButton.textContent = 'Pause/Resume';
|
||
playbackButton.onclick = toggleStreamPlayback; // Set the onclick function to toggleStreamPlayback
|
||
playbackButton.id = 'streamToggle'; // Assign ID for streaming
|
||
} else {
|
||
// If not in streaming mode, the button should say "Pause/Resume Playback"
|
||
playbackButton.textContent = 'Pause/Resume';
|
||
playbackButton.onclick = togglePausePlayback; // Set the onclick function to togglePausePlayback
|
||
playbackButton.removeAttribute('id'); // Remove ID for chunk mode
|
||
}
|
||
}
|
||
|
||
|
||
//////////////////////////////////////////////////
|
||
//////// Generated List Playback Function ////////
|
||
//////////////////////////////////////////////////
|
||
|
||
// Attach the playPauseList function to the Play/Pause button
|
||
document.getElementById('playPauseList').addEventListener('click', playPauseList);
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
// Get elements by ID and assign event listeners to them
|
||
const playPauseButton = document.getElementById('playPauseList');
|
||
playPauseButton.addEventListener('click', togglePlayPauseList);
|
||
|
||
const clearListButton = document.getElementById('clearList'); // Update this ID to match your HTML
|
||
clearListButton.addEventListener('click', clearTTSList);
|
||
|
||
const startFromIdElement = document.getElementById('startFromId');
|
||
if (startFromIdElement) {
|
||
startFromIdElement.setAttribute('value', '1');
|
||
}
|
||
// Other initializations...
|
||
});
|
||
|
||
function togglePlayPauseList() {
|
||
// Get the ID from the user interface
|
||
const startFromId = document.getElementById('startFromId').value;
|
||
// Convert ID to a 0-based index
|
||
const startIndex = startFromId ? parseInt(startFromId, 10) - 1 : 0;
|
||
|
||
// Toggle the playing state
|
||
listPlaying = !listPlaying;
|
||
updatePlayPauseButton(listPlaying);
|
||
|
||
if (listPlaying) {
|
||
// If list is set to play, start playing from the specified index
|
||
// and highlight the corresponding item
|
||
playListFromIndex(startIndex);
|
||
highlightPlaying(startIndex + 1); // Assuming the list items are 1-indexed
|
||
updateCurrentPage(startIndex + 1); // Update the current page based on the playing item
|
||
} else {
|
||
// If list is set to pause, pause the current item
|
||
// and remove highlighting
|
||
if (howlInstances[listPlayIndex]) {
|
||
howlInstances[listPlayIndex].pause();
|
||
}
|
||
highlightPlaying(null); // Call with null to clear highlighting
|
||
}
|
||
}
|
||
|
||
function highlightPlaying(id) {
|
||
// Clear previous highlights
|
||
document.querySelectorAll('.ttsListItem').forEach(item => {
|
||
item.classList.remove('highlight');
|
||
});
|
||
|
||
// Update the startFromId input to show the currently playing item's ID
|
||
const startFromIdInput = document.getElementById('startFromId');
|
||
if (startFromIdInput) {
|
||
startFromIdInput.value = id;
|
||
}
|
||
|
||
// If id is null, no further action is needed
|
||
if (id === null) {
|
||
return;
|
||
}
|
||
|
||
// Otherwise, highlight the new item
|
||
const currentItem = document.getElementById(`item-${id}`);
|
||
if (currentItem) {
|
||
currentItem.classList.add('highlight');
|
||
}
|
||
updateCurrentPage(id);
|
||
}
|
||
|
||
|
||
function updateCurrentPage(itemIndex) {
|
||
// Calculate the page number based on the item index
|
||
// Note: Ensure that the itemIndex is 1-based; if it's 0-based, add 1.
|
||
const newPage = Math.ceil(itemIndex / itemsPerPage);
|
||
|
||
// Update the current page and UI if the new page is different from the current page
|
||
if (currentPage !== newPage) {
|
||
currentPage = newPage; // Set the new current page
|
||
updateUIList(); // Refresh the list display to show the correct page
|
||
}
|
||
|
||
// Optionally, update the 'Jump to Page' input to reflect the current page
|
||
const jumpToPageInput = document.getElementById('jumpToPage');
|
||
if (jumpToPageInput) {
|
||
jumpToPageInput.value = currentPage;
|
||
}
|
||
}
|
||
|
||
|
||
function playListFromIndex(index) {
|
||
if (index < ttsList.length) {
|
||
const ttsDetails = ttsList[index];
|
||
if (ttsDetails) {
|
||
const currentVolume = parseFloat(document.getElementById("volume").value);
|
||
const sound = new Howl({
|
||
src: [ttsDetails.fileUrl],
|
||
volume: currentVolume, // Set the volume for this Howl instance
|
||
onplay: function () {
|
||
// Highlight when the sound starts playing
|
||
highlightPlaying(index + 1);
|
||
},
|
||
onend: function () {
|
||
// Clear highlight when the sound ends
|
||
highlightPlaying(null);
|
||
|
||
// Play the next item if it exists
|
||
if (index + 1 < ttsList.length) {
|
||
playListFromIndex(index + 1);
|
||
} else {
|
||
console.log('Finished playing the list.');
|
||
}
|
||
}
|
||
});
|
||
|
||
// Play the sound
|
||
sound.play();
|
||
} else {
|
||
console.error('TTS details not found for index:', index);
|
||
}
|
||
} else {
|
||
console.log('Index is out of bounds, stopping playback.');
|
||
// Clear any highlights if out of bounds
|
||
highlightPlaying(null);
|
||
updateUIList();
|
||
}
|
||
}
|
||
|
||
function updatePlayPauseButton(isPlaying) {
|
||
const playPauseButton = document.getElementById('playPauseList'); // Ensure this ID matches your HTML
|
||
playPauseButton.textContent = isPlaying ? 'Pause List' : 'Play List';
|
||
}
|
||
|
||
function clearTTSList() {
|
||
Howler.stop();
|
||
audioQueue.forEach(sound => sound.unload());
|
||
audioQueue = [];
|
||
ttsList = [];
|
||
listPlayIndex = 0; // If listPlayIndex is the correct variable, use it instead of currentlyPlayingIndex
|
||
listPlaying = false; // Assuming listPlaying is the correct variable for list playback state
|
||
updateUIList(); // Refresh the list display
|
||
// Reset the 'Start From ID' input if you have this element in your HTML
|
||
const startFromIdElement = document.getElementById('startFromId');
|
||
if (startFromIdElement) {
|
||
startFromIdElement.value = '1';
|
||
}
|
||
const pageIndicator = document.getElementById('pageIndicator');
|
||
if (pageIndicator) {
|
||
pageIndicator.textContent = 'Page 1 of 1';
|
||
}
|
||
const pageIndicator1 = document.getElementById('pageIndicator1');
|
||
if (pageIndicator1) {
|
||
pageIndicator1.textContent = 'Page 1 of 1';
|
||
}
|
||
}
|
||
|
||
function editText(id) {
|
||
// First, check if we're already editing and should save the current value
|
||
const existingInput = document.getElementById(`edit-input-${id}`);
|
||
if (existingInput) {
|
||
existingInput.blur(); // This will trigger the blur event, saving the current value
|
||
return; // Don't proceed further since we're just saving the existing input
|
||
}
|
||
|
||
const entryDiv = document.getElementById(`item-${id}`);
|
||
if (!entryDiv) {
|
||
console.error(`Item with ID ${id} not found.`);
|
||
return;
|
||
}
|
||
|
||
const textPreviewSpan = entryDiv.querySelector('.text-preview');
|
||
const fullText = textPreviewSpan.getAttribute('data-fulltext');
|
||
|
||
// Create an input field for editing
|
||
const inputField = document.createElement('input');
|
||
inputField.type = 'text';
|
||
inputField.value = fullText;
|
||
inputField.classList.add('edit-input');
|
||
inputField.id = `edit-input-${id}`; // Assign an ID to the input field for later access
|
||
|
||
// Handle saving the edited text
|
||
inputField.addEventListener('blur', function () {
|
||
// Update the text in the ttsList array and the data-fulltext attribute
|
||
const itemIndex = ttsList.findIndex(item => item.id === id);
|
||
if (itemIndex !== -1) {
|
||
ttsList[itemIndex].text = this.value;
|
||
}
|
||
|
||
// Create a new span element to replace the input field
|
||
const newTextPreviewSpan = document.createElement('span');
|
||
newTextPreviewSpan.classList.add('item-field', 'text-preview');
|
||
newTextPreviewSpan.setAttribute('data-fulltext', this.value);
|
||
newTextPreviewSpan.title = this.value;
|
||
const displayText = this.value.length > 190 ? this.value.substring(0, 190) + '...' : this.value;
|
||
newTextPreviewSpan.textContent = displayText;
|
||
|
||
// Replace the input field with the new span
|
||
inputField.replaceWith(newTextPreviewSpan);
|
||
});
|
||
|
||
// Replace the span with the input field
|
||
textPreviewSpan.replaceWith(inputField);
|
||
|
||
// Focus the input field and select its content to facilitate editing
|
||
inputField.focus();
|
||
inputField.select();
|
||
}
|
||
|
||
////////////////////////////////////////
|
||
//////// JSON Import and Export ////////
|
||
////////////////////////////////////////
|
||
|
||
function exportToJSON() {
|
||
// Convert the ttsList array to a JSON string
|
||
const jsonString = JSON.stringify(ttsList, null, 2);
|
||
|
||
// Create a Blob object from the JSON string
|
||
const blob = new Blob([jsonString], { type: 'application/json' });
|
||
|
||
// Create a URL for the blob object
|
||
const url = URL.createObjectURL(blob);
|
||
|
||
// Create a temporary anchor element
|
||
const a = document.createElement('a');
|
||
a.href = url;
|
||
a.download = 'ttsList.json'; // Name of the file to be downloaded
|
||
|
||
// Append the anchor to the body, click it to initiate download, and remove it
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
|
||
// Revoke the URL after download
|
||
URL.revokeObjectURL(url);
|
||
}
|
||
|
||
function importFromJSON() {
|
||
const input = document.createElement('input');
|
||
input.type = 'file';
|
||
input.accept = '.json';
|
||
|
||
input.onchange = (e) => {
|
||
const file = e.target.files[0];
|
||
if (file) {
|
||
const reader = new FileReader();
|
||
reader.readAsText(file);
|
||
reader.onload = (e) => {
|
||
try {
|
||
const jsonData = JSON.parse(e.target.result);
|
||
console.log('JSON data imported:', jsonData);
|
||
|
||
// Update the ttsList with the imported data
|
||
ttsList = jsonData;
|
||
|
||
// Refresh the on-screen list
|
||
updateUIList(); // This function updates the UI with the contents of ttsList
|
||
} catch (error) {
|
||
console.error('Error parsing JSON:', error);
|
||
}
|
||
};
|
||
}
|
||
};
|
||
|
||
input.click();
|
||
}
|
||
|
||
//////////////////////////////
|
||
//////// Other Things ////////
|
||
//////////////////////////////
|
||
|
||
function updateLanguage(language) {
|
||
// Update the language in your application logic
|
||
console.log("Selected language:", language);
|
||
// ... your code to handle the language change ...
|
||
}
|
||
|
||
|
||
function updateCharacterVoice(characterVoice) {
|
||
console.log("Selected character voice:", characterVoice);
|
||
}
|
||
|
||
function setPlayMode(mode) {
|
||
playModeValue = mode;
|
||
}
|
||
|
||
// Function to play a new sound (used in all modes)
|
||
function playSoundWithHowler(src, volume) {
|
||
if (currentSound) {
|
||
currentSound.stop(); // Stop any currently playing sound
|
||
}
|
||
|
||
currentSound = new Howl({
|
||
src: [src],
|
||
volume: volume,
|
||
onend: function () {
|
||
currentSound = null; // Reset the currentSound when playback ends
|
||
}
|
||
});
|
||
|
||
currentSound.play();
|
||
}
|
||
|
||
function togglePausePlayback() {
|
||
if (isPlaying && currentlyPlayingIndex > 0) {
|
||
let currentSound = audioQueue[currentlyPlayingIndex - 1];
|
||
if (currentSound.playing()) {
|
||
currentSound.pause();
|
||
} else {
|
||
currentSound.play();
|
||
}
|
||
}
|
||
}
|
||
|
||
async function fetchVoices() {
|
||
try {
|
||
const response = await fetch("http://127.0.0.1:7851/api/voices");
|
||
const result = await response.json();
|
||
const characterVoiceSelect = document.getElementById("character_voice_gen");
|
||
|
||
characterVoiceSelect.innerHTML = ''; // Clear existing options
|
||
result.voices.forEach(voice => {
|
||
const option = document.createElement("option");
|
||
option.value = voice;
|
||
option.text = voice;
|
||
characterVoiceSelect.add(option);
|
||
});
|
||
|
||
// After populating the dropdown, restore the saved voice preference
|
||
const savedCharacterVoice = localStorage.getItem('characterVoice');
|
||
if (savedCharacterVoice) {
|
||
characterVoiceSelect.value = savedCharacterVoice;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
// Replace stopPlayback function to stop all sounds using Howler.js
|
||
function stopPlayback() {
|
||
Howler.stop();
|
||
audioQueue = [];
|
||
currentlyPlayingIndex = 0;
|
||
isPlaying = false;
|
||
highlightPlaying(null);
|
||
document.getElementById('startFromId').value = '1'; // Reset the startFromId input to 1
|
||
}
|
||
|
||
function displayOutput(result) {
|
||
const outputContainer = document.getElementById("output");
|
||
outputContainer.innerHTML = `<p>Status: ${result.status}</p>`;
|
||
if (result.output_file_path) {
|
||
outputContainer.innerHTML += `<p>File Path: ${result.output_file_path}</p>`;
|
||
}
|
||
if (result.output_file_url) {
|
||
outputContainer.innerHTML += `<p>File URL: ${result.output_file_url}</p>`;
|
||
}
|
||
if (result.error) {
|
||
outputContainer.innerHTML += `<p class="error">Error: ${result.error}</p>`;
|
||
}
|
||
}
|
||
|
||
////////////////////////////////////////////
|
||
//////// General Dcoument Listeners ////////
|
||
////////////////////////////////////////////
|
||
|
||
// Event listeners for pagination controls
|
||
document.getElementById('prevPage').addEventListener('click', () => {
|
||
if (currentPage > 1) {
|
||
currentPage--;
|
||
updateUIList();
|
||
}
|
||
});
|
||
|
||
document.getElementById('nextPage').addEventListener('click', () => {
|
||
const lastPage = Math.ceil(ttsList.length / itemsPerPage);
|
||
if (currentPage < lastPage) {
|
||
currentPage++;
|
||
updateUIList();
|
||
}
|
||
});
|
||
|
||
// Initialize the list on page load
|
||
document.addEventListener('DOMContentLoaded', updateUIList);
|
||
|
||
|
||
// For the 'Previous' button at the bottom
|
||
document.getElementById('prevPage1').addEventListener('click', () => {
|
||
if (currentPage > 1) {
|
||
currentPage--;
|
||
updateUIList();
|
||
}
|
||
});
|
||
|
||
// For the 'Next' button at the bottom
|
||
document.getElementById('nextPage1').addEventListener('click', () => {
|
||
let lastPage = Math.ceil(ttsList.length / itemsPerPage);
|
||
if (currentPage < lastPage) {
|
||
currentPage++;
|
||
updateUIList();
|
||
}
|
||
});
|
||
|
||
function handleJumpButtonClick(event) {
|
||
// Determine which set of controls to use based on the button that was clicked
|
||
const suffix = event.target.id === 'jumpButton' ? '' : '1';
|
||
const jumpToInput = document.getElementById(`jumpToPage${suffix}`);
|
||
const jumpToValue = jumpToInput.value.trim(); // Get the trimmed value
|
||
const jumpTo = parseInt(jumpToValue, 10); // Parse the value as an integer
|
||
|
||
if (!isNaN(jumpTo)) {
|
||
let lastPage = Math.ceil(ttsList.length / itemsPerPage);
|
||
|
||
if (jumpTo >= 1 && jumpTo <= lastPage) {
|
||
currentPage = jumpTo;
|
||
updateUIList();
|
||
} else {
|
||
alert(`Please enter a valid page number between 1 and ${lastPage}`);
|
||
}
|
||
} else {
|
||
alert("Please enter a number to jump to.");
|
||
}
|
||
}
|
||
|
||
// Set up the event listener for both jump buttons
|
||
document.getElementById('jumpButton').addEventListener('click', handleJumpButtonClick);
|
||
document.getElementById('jumpButton1').addEventListener('click', handleJumpButtonClick);
|
||
|
||
|
||
|
||
// Event listener for changing play type
|
||
document.getElementById('playType').addEventListener('change', function () {
|
||
isStreamingMode = this.value === 'stream';
|
||
updatePlaybackButton();
|
||
});
|
||
|
||
// Add event listener to the play type dropdown to update settings on change
|
||
document.getElementById('playType').addEventListener('change', updatePlayTypeSettings);
|
||
|
||
// Call updatePlayTypeSettings initially to set the correct state
|
||
updatePlayTypeSettings();
|
||
|
||
// Call this function on page load to ensure correct initial state
|
||
document.getElementById('playType').dispatchEvent(new Event('change'));
|
||
|
||
|
||
// Initialize button state on page load
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
isStreamingMode = false; // Default to non-streaming mode
|
||
updatePlaybackButton(); // Set the correct button text on load
|
||
});
|
||
|
||
// Event listener to update characterVoice when the selection changes
|
||
document.getElementById("character_voice_gen").addEventListener('change', function () {
|
||
characterVoice = this.value;
|
||
});
|
||
|
||
document.getElementById('language').addEventListener('change', function () {
|
||
// Get the selected language from the dropdown
|
||
const selectedLanguage = this.value;
|
||
updateLanguage(selectedLanguage);
|
||
});
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
document.getElementById('playMode').addEventListener('change', function () {
|
||
setPlayMode(this.value);
|
||
});
|
||
});
|
||
|
||
document.getElementById('playType').addEventListener('change', function () {
|
||
var playType = this.value;
|
||
var playModeDropdown = document.getElementById('playMode');
|
||
var chunkSizeSlider = document.getElementById('chunkSize');
|
||
|
||
if (playType === 'stream') {
|
||
// If 'Streaming' is selected, force 'In Browser' playback, disable the dropdown, and disable the chunk size slider
|
||
playModeDropdown.value = 'local';
|
||
playModeDropdown.disabled = true;
|
||
chunkSizeSlider.disabled = true;
|
||
} else {
|
||
// If 'Wav Chunks' is selected, enable the dropdown and the chunk size slider
|
||
playModeDropdown.disabled = false;
|
||
chunkSizeSlider.disabled = false;
|
||
}
|
||
});
|
||
|
||
/////////////////////////////////////////////////////
|
||
//////// Setup Page on Load & Store settings ////////
|
||
/////////////////////////////////////////////////////
|
||
|
||
function updateChunkSizeValue(value) {
|
||
document.getElementById('chunkSizeValue').innerText = value;
|
||
}
|
||
|
||
function updateVolumeValue(value) {
|
||
document.getElementById("volumeValue").innerText = value;
|
||
Howler.volume(parseFloat(value));
|
||
}
|
||
|
||
function toggleThemeMode() {
|
||
// Retrieve the value of the selected option for themeMode from the dropdown
|
||
const themeModeValue = document.getElementById('themeMode').value;
|
||
|
||
if (themeModeValue === "dark") {
|
||
document.body.classList.add('dark-mode');
|
||
localStorage.setItem('darkMode', 'enabled'); // Assuming saveDarkModePreference sets this
|
||
} else {
|
||
document.body.classList.remove('dark-mode');
|
||
localStorage.setItem('darkMode', 'disabled'); // Assuming saveDarkModePreference sets this
|
||
}
|
||
}
|
||
|
||
function setupPage() {
|
||
// Fetch voices or other initial data
|
||
fetchVoices();
|
||
|
||
// Check for dark mode preference in localStorage, default to enabled if not set
|
||
let isDarkModeEnabled = localStorage.getItem('darkMode');
|
||
if (isDarkModeEnabled === null) {
|
||
isDarkModeEnabled = 'enabled'; // Default to dark mode
|
||
localStorage.setItem('darkMode', isDarkModeEnabled); // Save the default preference
|
||
}
|
||
|
||
// Apply dark mode if it's enabled
|
||
if (isDarkModeEnabled === 'enabled') {
|
||
document.body.classList.add('dark-mode');
|
||
document.getElementById('themeMode').value = 'dark';
|
||
} else {
|
||
document.getElementById('themeMode').value = 'light';
|
||
}
|
||
|
||
// Initialize character voice based on preference
|
||
const savedCharacterVoice = localStorage.getItem('characterVoice');
|
||
if (savedCharacterVoice) {
|
||
document.getElementById('character_voice_gen').value = savedCharacterVoice;
|
||
}
|
||
|
||
// Initialize language based on preference
|
||
const savedLanguage = localStorage.getItem('language');
|
||
if (savedLanguage) {
|
||
document.getElementById('language').value = savedLanguage;
|
||
}
|
||
|
||
// Initialize play type based on preference
|
||
const savedPlayType = localStorage.getItem('playType');
|
||
if (savedPlayType) {
|
||
const playTypeSelect = document.getElementById('playType');
|
||
playTypeSelect.value = savedPlayType;
|
||
adjustPlaybackAndChunkSize(savedPlayType); // Adjust selectability on page load
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const savedPlayMode = localStorage.getItem('playMode');
|
||
if (savedPlayMode) {
|
||
document.getElementById('playMode').value = savedPlayMode;
|
||
}
|
||
playModeValue = savedPlayMode || 'local'; // Default to 'local' if nothing is saved
|
||
});
|
||
|
||
// Initialize chunk size based on preference
|
||
const savedChunkSize = localStorage.getItem('chunkSize');
|
||
if (savedChunkSize) {
|
||
document.getElementById('chunkSize').value = savedChunkSize;
|
||
updateChunkSizeValue(savedChunkSize); // Update display to match
|
||
}
|
||
|
||
// Initialize volume based on preference
|
||
const savedVolume = localStorage.getItem('volume');
|
||
if (savedVolume) {
|
||
document.getElementById('volume').value = savedVolume;
|
||
updateVolumeValueDisplay(savedVolume); // Update display to match
|
||
}
|
||
|
||
// Add change event listeners to save preferences when they change
|
||
attachEventListeners();
|
||
}
|
||
|
||
function attachEventListeners() {
|
||
document.getElementById('themeMode').addEventListener('change', function () {
|
||
document.body.classList.toggle('dark-mode', this.value === 'dark');
|
||
savePreferences();
|
||
});
|
||
document.getElementById('character_voice_gen').addEventListener('change', savePreferences);
|
||
document.getElementById('language').addEventListener('change', savePreferences);
|
||
document.getElementById('playType').addEventListener('change', function () {
|
||
savePreferences(); // Save the new play type
|
||
adjustPlaybackAndChunkSize(this.value); // Adjust 'Playback' and 'Chunk Size' selectability
|
||
});
|
||
document.getElementById('playMode').addEventListener('change', savePreferences);
|
||
document.getElementById('chunkSize').addEventListener('input', function () {
|
||
updateChunkSizeValue(this.value);
|
||
savePreferences();
|
||
});
|
||
document.getElementById('volume').addEventListener('input', function () {
|
||
updateVolumeValue(this.value);
|
||
savePreferences();
|
||
});
|
||
}
|
||
|
||
// Use this function to update the volume display, not the actual volume
|
||
function updateVolumeValueDisplay(value) {
|
||
document.getElementById('volumeValue').innerText = value;
|
||
}
|
||
|
||
// Later, when user interacts with the volume control:
|
||
function updateVolumeValue(value) {
|
||
document.getElementById('volumeValue').innerText = value;
|
||
// Setting the volume on Howler is now allowed because it's in response to user interaction
|
||
Howler.volume(parseFloat(value));
|
||
}
|
||
|
||
// Make sure to call updateVolumeValue only in response to user interaction
|
||
document.getElementById('volume').addEventListener('input', function () {
|
||
updateVolumeValue(this.value);
|
||
savePreferences();
|
||
});
|
||
|
||
function adjustPlaybackAndChunkSize(playType) {
|
||
const playbackSelect = document.getElementById('playMode');
|
||
const chunkSizeInput = document.getElementById('chunkSize');
|
||
const isStreaming = playType === 'stream'; // Make sure the value matches the one in the dropdown
|
||
|
||
// Disable or enable 'Playback' and 'Chunk Size' based on 'Play Type'
|
||
playbackSelect.disabled = isStreaming;
|
||
chunkSizeInput.disabled = isStreaming;
|
||
|
||
// If Streaming is selected, also reset the 'Playback' and 'Chunk Size' to default values
|
||
if (isStreaming) {
|
||
playbackSelect.value = 'local';
|
||
chunkSizeInput.value = '1';
|
||
updateChunkSizeValue('1');
|
||
}
|
||
}
|
||
|
||
function savePreferences() {
|
||
// Save character voice preference
|
||
const characterVoice = document.getElementById('character_voice_gen').value;
|
||
localStorage.setItem('characterVoice', characterVoice);
|
||
|
||
// Save language preference
|
||
const language = document.getElementById('language').value;
|
||
localStorage.setItem('language', language);
|
||
|
||
// Save play type preference
|
||
const playType = document.getElementById('playType').value;
|
||
localStorage.setItem('playType', playType);
|
||
|
||
// Save playback preference
|
||
const playMode = document.getElementById('playMode').value;
|
||
localStorage.setItem('playMode', playMode);
|
||
|
||
// Save chunk size preference
|
||
const chunkSize = document.getElementById('chunkSize').value;
|
||
localStorage.setItem('chunkSize', chunkSize);
|
||
|
||
// Save volume preference
|
||
const volume = document.getElementById('volume').value;
|
||
localStorage.setItem('volume', volume);
|
||
|
||
// Save dark mode preference
|
||
const isDarkModeEnabled = document.body.classList.contains('dark-mode');
|
||
localStorage.setItem('darkMode', isDarkModeEnabled ? 'enabled' : 'disabled');
|
||
}
|
||
|
||
window.onload = function () {
|
||
setupPage();
|
||
hideProgress(); // Ensure the progress indicator is hidden on page load
|
||
};
|
||
|
||
|
||
</script>
|
||
</body>
|
||
|
||
</html> |