TTS Generator - Various interface bugs & filtering options cleaned up. TTS Generator - TTSDiff now scans generated text and TTS for errors. TTS Generator - TTSSRT now creates subtitle files for video production e.g. a Youtube video. Finetune - Now uses a customised tokenizer to deal with Japanese. Finetune - Pre flight check and warning messages. Finetune - Extra documentation and warnings. Entire file structure has been re-organised to simplify management and future changes. Documentation (built in and Github) has been rewritten/tidied up. Requirements files have been cleaned up and simplified. ATsetup has been re-written as necessary with additional options. DeepSpeed moved up to version 14. Standalone Application moved to PyTorch 2.2.1. Nvidia CUDA Toolkit installation is NO LONGER needed (other than to compile DeepSpeed on Linux) Tested on Linux and Windows.
2211 lines
86 KiB
HTML
2211 lines
86 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 */
|
||
}
|
||
|
||
.problematic {
|
||
background-color: rgb(179, 104, 104);
|
||
/* Or any light red color of your choice */
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/*Disable and enable buttons*/
|
||
button:disabled {
|
||
background-color: #919191;
|
||
color: #666;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.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.1" 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 class="conditional-btn" id="playPauseList" onclick="togglePlayPauseList()" style="margin-right: 10px;">Play
|
||
List</button>
|
||
<button class="conditional-btn" onclick="stopPlayback()" style="margin-right: 10px;">Stop Playback</button>
|
||
<button class="conditional-btn" id="clearList" onclick="clearTTSList()" style="margin-right: 40px;">Clear
|
||
List</button>
|
||
<button onclick="importFromJSON()" style="margin-right: 10px;">Import JSON</button>
|
||
<button class="conditional-btn" onclick="exportToJSON()" style="margin-right: 10px;">Export JSON</button>
|
||
<button class="conditional-btn" id="exportsrttitles" style="margin-right: 10px;">Export SRT</button>
|
||
<button class="conditional-btn" onclick="exportMP3()" style="margin-right: 10px;">Export to WAV</button>
|
||
<label for="splitEvery">Export Split:</label>
|
||
<select id="splitEvery" name="splitEvery" style="margin-right: 40px;">
|
||
<option value="100">100</option>
|
||
<option value="200">200</option>
|
||
<option value="300">300</option>
|
||
<option value="400">400</option>
|
||
<option value="500" selected>500</option>
|
||
<option value="600">600</option>
|
||
</select>
|
||
<button class="conditional-btn" id="saveAndAnalyzeTTS" style="margin-right: 10px;">Analyze TTS</button>
|
||
<label for="AnalyzeAccuracy">% Accuracy:</label>
|
||
<select id="AnalyzeAccuracy" name="AnalyzeAccuracy" style="margin-right: 10px;">
|
||
<option value="99">99</option>
|
||
<option value="98" selected>98</option>
|
||
<option value="97">97</option>
|
||
<option value="96">96</option>
|
||
<option value="95">95</option>
|
||
<option value="94">94</option>
|
||
<option value="93">93</option>
|
||
<option value="92">92</option>
|
||
<option value="91">91</option>
|
||
<option value="90">90</option>
|
||
</select>
|
||
<div id="paginationControls">
|
||
<button class="conditional-btn pagination-btn" id="prevPage">Previous</button>
|
||
<span id="pageIndicator">Page 1 of X</span>
|
||
<button class="conditional-btn pagination-btn" id="nextPage">Next</button>
|
||
<input type="number" id="jumpToPage" class="conditional-btn pagination-input" min="1"
|
||
placeholder="Jump to Page">
|
||
<button class="conditional-btn pagination-btn" id="jumpButton">Go</button>
|
||
</div>
|
||
<div id="ttsListDisplay" class="tts-list-display">
|
||
<!-- Dynamically populated TTS details will go here -->
|
||
</div>
|
||
<div id="paginationControls1">
|
||
<button class="conditional-btn pagination-btn" id="prevPage1">Previous</button>
|
||
<span id="pageIndicator1">Page 1 of X</span>
|
||
<button class="conditional-btn pagination-btn" id="nextPage1">Next</button>
|
||
<input type="number" id="jumpToPage1" class="conditional-btn pagination-input" min="1"
|
||
placeholder="Jump to Page">
|
||
<button class="conditional-btn pagination-btn" id="jumpButton1">Go</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Howler.js script inclusion -->
|
||
<script src="./howler.min.js"></script>
|
||
<script type="module">
|
||
import Crunker from './crunker.esm.js';
|
||
|
||
async function exportMP3() {
|
||
const crunker = new Crunker();
|
||
const fileUrls = ttsList.map(item => item.fileUrl);
|
||
const splitEvery = parseInt(document.getElementById('splitEvery').value) || 500;
|
||
const progressIndicator = document.getElementById('progressIndicator');
|
||
const progressText = document.getElementById('progressText');
|
||
|
||
if (progressIndicator && progressText) {
|
||
progressIndicator.style.display = 'block';
|
||
progressText.textContent = 'Starting export...';
|
||
}
|
||
|
||
try {
|
||
for (let i = 0; i < fileUrls.length; i += splitEvery) {
|
||
const currentBatch = Math.floor(i / splitEvery) + 1;
|
||
const totalBatches = Math.ceil(fileUrls.length / splitEvery);
|
||
if (progressText) {
|
||
progressText.textContent = `Processing batch ${currentBatch} of ${totalBatches}`;
|
||
progressText.style.color = 'black'; // Set the text color to black
|
||
}
|
||
|
||
const batchUrls = fileUrls.slice(i, Math.min(i + splitEvery, fileUrls.length));
|
||
const audioBuffers = await crunker.fetchAudio(...batchUrls);
|
||
const combined = crunker.concatAudio(audioBuffers);
|
||
const output = crunker.export(combined, "audio/wav");
|
||
crunker.download(output.blob, `combined_audio_part_${currentBatch}.wav`);
|
||
|
||
// Clear audioBuffers array to free up memory
|
||
audioBuffers.length = 0;
|
||
|
||
console.log(`Exported batch ${currentBatch} of ${totalBatches}`);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error combining files:', error);
|
||
if (progressText) {
|
||
progressText.textContent = 'Error during export!';
|
||
}
|
||
} finally {
|
||
if (progressIndicator && progressText) {
|
||
progressIndicator.style.display = 'none';
|
||
progressText.textContent = ''; // Reset progress text
|
||
}
|
||
}
|
||
}
|
||
|
||
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
|
||
let problematicIds = []; // Global list to store ProblematicIDs
|
||
const howlInstances = {};
|
||
let listPlaying = false;
|
||
let listPlayIndex = 0;
|
||
let currentPage = 1;
|
||
let itemsPerPage = 100; // Adjust as needed
|
||
var protocol = window.location.protocol;
|
||
var host = window.location.hostname;
|
||
var port = window.location.port;
|
||
var baseUrl = protocol + "//" + host + (port ? ':' + port : '');
|
||
|
||
////////////////////////////////////
|
||
//////// 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>`;
|
||
}
|
||
|
||
|
||
//////////////////////////////////
|
||
//////// Compare TTS list ////////
|
||
//////////////////////////////////
|
||
|
||
document.getElementById('saveAndAnalyzeTTS').addEventListener('click', async function () {
|
||
showProgress(); // Call this function to show the progress indicator at the start of the process
|
||
try {
|
||
// Serialize the ttsList data structure to a JSON string
|
||
const ttsDataJSON = JSON.stringify(ttsList);
|
||
|
||
// Send the serialized ttsList to the '/save-tts-data' endpoint
|
||
const saveResponse = await fetch('/api/save-tts-data', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: ttsDataJSON // Use the serialized JSON string as the request body
|
||
});
|
||
|
||
const saveData = await saveResponse.json();
|
||
|
||
// Log the server's response
|
||
console.log(saveData.message);
|
||
|
||
// If the server confirms the data was saved successfully, trigger analysis
|
||
if (saveData.message === "Data saved successfully") {
|
||
const accuracy = parseInt(document.getElementById('AnalyzeAccuracy').value) || 98;
|
||
const analyzeResponse = await fetch(`/api/trigger-analysis?threshold=${accuracy}`);
|
||
const analyzeData = await analyzeResponse.json();
|
||
if (analyzeData.summary.flagged_ids && analyzeData.summary.flagged_ids.length > 0) {
|
||
// Update the global problematicIds array with the new flagged IDs
|
||
problematicIds = analyzeData.summary.flagged_ids;
|
||
console.log("Initial problematicIds from analysis:", problematicIds); // Debugging: Check the initial array after receiving analysis results
|
||
|
||
// Call highlightProblems with the array of flagged IDs
|
||
highlightProblems(problematicIds);
|
||
} else {
|
||
console.log("No problematic IDs or error retrieving data.");
|
||
|
||
// Clear the problematicIds array since there are no problematic IDs
|
||
problematicIds = [];
|
||
highlightProblems(problematicIds);
|
||
}
|
||
// Optionally, you might want to provide feedback in the UI about the analysis status
|
||
} else {
|
||
throw new Error('Data save was unsuccessful.');
|
||
}
|
||
} catch (error) {
|
||
// Handle any errors that occurred during the fetch process
|
||
console.error('Error:', error);
|
||
} finally {
|
||
hideProgress(); // Hide the progress indicator when the process is complete or if an error occurs
|
||
}
|
||
});
|
||
|
||
function highlightProblems(idsToHighlight) {
|
||
// First, clear all previous 'problematic' highlights
|
||
document.querySelectorAll('.problematic').forEach(item => {
|
||
item.classList.remove('problematic');
|
||
});
|
||
|
||
// Now, highlight all the IDs that should still be problematic
|
||
idsToHighlight.forEach(id => {
|
||
const itemElement = document.getElementById(`item-${id}`);
|
||
if (itemElement) {
|
||
itemElement.classList.add('problematic');
|
||
}
|
||
});
|
||
}
|
||
|
||
/////////////////////////////////
|
||
//////// Export SRT Subs ////////
|
||
/////////////////////////////////
|
||
|
||
document.getElementById('exportsrttitles').addEventListener('click', async function () {
|
||
showProgress(); // Call this function to show the progress indicator at the start of the process
|
||
try {
|
||
const ttsDataJSON = JSON.stringify(ttsList);
|
||
|
||
const saveResponse = await fetch('/api/save-tts-data', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: ttsDataJSON
|
||
});
|
||
const saveData = await saveResponse.json();
|
||
console.log(saveData.message);
|
||
|
||
if (saveData.message === "Data saved successfully") {
|
||
// Trigger the SRT generation and handle file download
|
||
const analyzeResponse = await fetch(`/api/srt-generation`);
|
||
if (analyzeResponse.ok) {
|
||
// Create a temporary anchor element
|
||
const a = document.createElement('a');
|
||
// Create a blob URL from the response
|
||
const blob = await analyzeResponse.blob();
|
||
const url = window.URL.createObjectURL(blob);
|
||
a.href = url;
|
||
// Set the download name for the file
|
||
a.download = "subtitles.srt";
|
||
// Append the anchor to the document
|
||
document.body.appendChild(a);
|
||
// Trigger the download
|
||
a.click();
|
||
// Clean up by removing the anchor element and revoking the blob URL
|
||
window.URL.revokeObjectURL(url);
|
||
a.remove();
|
||
} else {
|
||
console.log("Error running SRT generation.");
|
||
}
|
||
} else {
|
||
throw new Error('Data save was unsuccessful.');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error:', error);
|
||
} finally {
|
||
hideProgress(); // Hide the progress indicator when the process is complete or if an error occurs
|
||
}
|
||
});
|
||
|
||
///////////////////////////////////////////
|
||
//////// 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(/(?<!\w)[']+(?!\w)/g, '');
|
||
cleanedText = cleanedText.replace(/"/g, "'");
|
||
cleanedText = cleanedText.replace(/[^a-zA-Z0-9\s\.,;:!?\-\'"À-ÿ\u0400-\u04FF\u0150\u0151\u0170\u0171]\$/g, '');
|
||
// Replace dash followed by space with comma and space
|
||
cleanedText = cleanedText.replace(/-\s/g, '; ');
|
||
// Replace double dash "--" with semicolon "; "
|
||
cleanedText = cleanedText.replace(/--/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(baseUrl + "/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();
|
||
updateButtonStyles()
|
||
|
||
// 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();
|
||
}
|
||
}
|
||
return result
|
||
} catch (error) {
|
||
console.error("Error in generateAndCache:", error);
|
||
}
|
||
}
|
||
|
||
async function regenerateTTS(id) {
|
||
console.log("Before regeneration:", problematicIds); // Debugging: Check the array before regeneration
|
||
|
||
// 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
|
||
const result = await generateAndCache(ttsDetails.text, ttsDetails.language, ttsDetails.characterVoice, currentVolume, id);
|
||
|
||
if (result && result.status === "generate-success") {
|
||
const index = problematicIds.indexOf(id);
|
||
if (index !== -1) {
|
||
problematicIds.splice(index, 1); // Remove the id from problematicIds
|
||
console.log(`ID ${id} removed from problematicIds. Updated array:`, problematicIds); // Debugging: Check the array after removing an ID
|
||
}
|
||
|
||
// Find the element for this ID and remove the 'problematic' class
|
||
const problemItemElement = document.getElementById(`item-${id}`);
|
||
if (problemItemElement) {
|
||
problemItemElement.classList.remove('problematic');
|
||
}
|
||
} else {
|
||
console.log(`Regeneration failed for ID ${id}.`, result); // If regeneration fails, log the result
|
||
}
|
||
|
||
// Refresh the UI list to reflect the new data
|
||
updateUIList(); // Assume this function exists and it updates the display list
|
||
// Reapply highlighting to the items still in problematicIds
|
||
highlightProblems(problematicIds);
|
||
console.log("After regeneration and re-highlighting:", problematicIds); // Debugging: Check the array after re-highlighting
|
||
}
|
||
|
||
|
||
|
||
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(baseUrl + "/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);
|
||
}
|
||
}
|
||
|
||
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('prevPage1').disabled = (currentPage === 1);
|
||
document.getElementById('nextPage').disabled = (currentPage === lastPage);
|
||
document.getElementById('nextPage1').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(baseUrl + "/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
|
||
var srcUrl = baseUrl + "/tts-demo-request?" + queryParams.toString();
|
||
currentSound = new Howl({
|
||
src: [srcUrl],
|
||
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(baseUrl + "/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 - 3];
|
||
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 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';
|
||
}
|
||
updateButtonStyles()
|
||
}
|
||
|
||
|
||
function encodeHtmlEntities(text) {
|
||
return text.replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
|
||
function decodeHtmlEntities(text) {
|
||
var textArea = document.createElement('textarea');
|
||
textArea.innerHTML = text;
|
||
return textArea.value;
|
||
}
|
||
|
||
function editText(id) {
|
||
console.log(`Initiating edit for ID: ${id}`);
|
||
console.log("Before edit, ttsList:", JSON.parse(JSON.stringify(ttsList)));
|
||
// First, check if we're already editing and should save the current value
|
||
const existingInput = document.getElementById(`edit-input-${id}`);
|
||
if (existingInput) {
|
||
console.log(`Existing input found for ID: ${id}, triggering blur event to save.`);
|
||
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');
|
||
// Retrieve the full text and immediately encode it
|
||
let fullText = textPreviewSpan.getAttribute('data-fulltext');
|
||
fullText = decodeHtmlEntities(fullText);
|
||
//fullText = encodeHtmlEntities(fullText);
|
||
console.log(`Current full text for ID: ${id} is: ${fullText}`);
|
||
|
||
// Create an input field for editing
|
||
const inputField = document.createElement('input');
|
||
inputField.type = 'text';
|
||
// The value is already encoded at this point
|
||
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 () {
|
||
console.log(`Saving edited text for ID: ${id}: ${this.value}`);
|
||
// Update the text in the ttsList array and the data-fulltext attribute
|
||
const itemIndex = ttsList.findIndex(item => item.id === id);
|
||
if (itemIndex !== -1) {
|
||
console.log(`Updating ttsList with new text for ID: ${id}`);
|
||
ttsList[itemIndex].text = this.value;
|
||
// If this item is in the list of problematic IDs, un-highlight it
|
||
if (problematicIds.includes(id)) {
|
||
console.log(`ID: ${id} is problematic, removing highlight class`);
|
||
const problemItemElement = document.getElementById(`item-${id}`);
|
||
problemItemElement.classList.remove('problematic');
|
||
}
|
||
}
|
||
|
||
// 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
|
||
console.log(`Replacing input field with text span for ID: ${id}`);
|
||
inputField.replaceWith(newTextPreviewSpan);
|
||
});
|
||
|
||
// Replace the span with the input field
|
||
console.log(`Replacing text span with input field for ID: ${id}`);
|
||
textPreviewSpan.replaceWith(inputField);
|
||
|
||
// Focus the input field and select its content to facilitate editing
|
||
inputField.focus();
|
||
inputField.select();
|
||
console.log("After edit, ttsList:", JSON.parse(JSON.stringify(ttsList)));
|
||
}
|
||
|
||
////////////////////////////////////////
|
||
//////// 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 = async (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);
|
||
|
||
ttsList = jsonData;
|
||
currentPage = 1; // Reset to the first page
|
||
updateUIList();
|
||
updateButtonStyles()
|
||
updatePaginationControls(); // Update pagination controls
|
||
} catch (error) {
|
||
console.error('Error parsing JSON:', error);
|
||
}
|
||
};
|
||
}
|
||
};
|
||
|
||
input.click();
|
||
}
|
||
|
||
function updatePaginationControls() {
|
||
const lastPage = Math.ceil(ttsList.length / itemsPerPage);
|
||
const prevPageButtons = document.querySelectorAll('.pagination-btn[id^="prevPage"]');
|
||
const nextPageButtons = document.querySelectorAll('.pagination-btn[id^="nextPage"]');
|
||
|
||
// Update page indicators and buttons
|
||
prevPageButtons.forEach(btn => btn.disabled = (currentPage === 1));
|
||
nextPageButtons.forEach(btn => btn.disabled = (currentPage === lastPage));
|
||
|
||
// Assuming the page indicators are consistent with button IDs
|
||
document.querySelectorAll('span[id^="pageIndicator"]').forEach(indicator => {
|
||
indicator.textContent = `Page ${currentPage} of ${lastPage}`;
|
||
});
|
||
|
||
// Update 'Jump to Page' inputs and 'Go' buttons
|
||
const jumpToPageInputs = document.querySelectorAll('.pagination-input[id^="jumpToPage"]');
|
||
const jumpButtons = document.querySelectorAll('.pagination-btn[id^="jumpButton"]');
|
||
const moreThanOnePage = lastPage > 1;
|
||
|
||
jumpToPageInputs.forEach(input => input.disabled = !moreThanOnePage);
|
||
jumpButtons.forEach(button => button.disabled = !moreThanOnePage);
|
||
}
|
||
|
||
//////////////////////////////
|
||
//////// Other Things ////////
|
||
//////////////////////////////
|
||
|
||
function updateButtonStyles() {
|
||
const ttsList = document.getElementById('ttsListDisplay');
|
||
const buttons = document.querySelectorAll('.conditional-btn');
|
||
|
||
if (!ttsList.hasChildNodes()) { // Check if the list is empty
|
||
buttons.forEach(button => button.disabled = true); // Disable buttons
|
||
} else {
|
||
buttons.forEach(button => button.disabled = false); // Enable buttons
|
||
}
|
||
}
|
||
|
||
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(baseUrl + "/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();
|
||
updateButtonStyles()
|
||
|
||
// 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
|
||
if (window.location.protocol !== 'http:' && window.location.protocol !== 'https:') {
|
||
alert('AllTalk TTS Generator is not designed to be run from your disk folder. Please run it from the correct URL e.g. http://127.0.0.1:7851/static/tts_chunk_player/tts_chunk_player.html');
|
||
}
|
||
};
|
||
|
||
</script>
|
||
</body>
|
||
|
||
</html> |