2420 lines
97 KiB
HTML
2420 lines
97 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
|
||
}
|
||
|
||
#customFileName {
|
||
width: 130px;
|
||
/* Adjust the width as needed */
|
||
padding: 10px;
|
||
margin-bottom: 16px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
background-color: #f1f1f1;
|
||
/* Set the background color to match the other inputs */
|
||
color: #000;
|
||
/* Set the text color */
|
||
}
|
||
|
||
.dark-mode #customFileName {
|
||
background-color: #C0C0C0;
|
||
/* Set the background color for dark mode */
|
||
color: #000;
|
||
/* Set the text color for dark mode */
|
||
}
|
||
</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 voice-setting">
|
||
<label for="RVC_voice_gen">RVC Voice:</label>
|
||
<select id="RVC_voice_gen"></select>
|
||
</div>
|
||
|
||
<div class="setting voice-setting">
|
||
<label for="RVC_voice_pitch">RVC Pitch:</label>
|
||
<select id="RVC_voice_pitch">
|
||
<!-- Loop through -24 to 24, setting 0 as the default option -->
|
||
<option value="-24">-24</option>
|
||
<option value="-23">-23</option>
|
||
<option value="-22">-22</option>
|
||
<option value="-21">-21</option>
|
||
<option value="-20">-20</option>
|
||
<option value="-19">-19</option>
|
||
<option value="-18">-18</option>
|
||
<option value="-17">-17</option>
|
||
<option value="-16">-16</option>
|
||
<option value="-15">-15</option>
|
||
<option value="-14">-14</option>
|
||
<option value="-13">-13</option>
|
||
<option value="-12">-12</option>
|
||
<option value="-11">-11</option>
|
||
<option value="-10">-10</option>
|
||
<option value="-9">-9</option>
|
||
<option value="-8">-8</option>
|
||
<option value="-7">-7</option>
|
||
<option value="-6">-6</option>
|
||
<option value="-5">-5</option>
|
||
<option value="-4">-4</option>
|
||
<option value="-3">-3</option>
|
||
<option value="-2">-2</option>
|
||
<option value="-1">-1</option>
|
||
<option value="0" selected>0</option> <!-- Default option -->
|
||
<option value="1">1</option>
|
||
<option value="2">2</option>
|
||
<option value="3">3</option>
|
||
<option value="4">4</option>
|
||
<option value="5">5</option>
|
||
<option value="6">6</option>
|
||
<option value="7">7</option>
|
||
<option value="8">8</option>
|
||
<option value="9">9</option>
|
||
<option value="10">10</option>
|
||
<option value="11">11</option>
|
||
<option value="12">12</option>
|
||
<option value="13">13</option>
|
||
<option value="14">14</option>
|
||
<option value="15">15</option>
|
||
<option value="16">16</option>
|
||
<option value="17">17</option>
|
||
<option value="18">18</option>
|
||
<option value="19">19</option>
|
||
<option value="20">20</option>
|
||
<option value="21">21</option>
|
||
<option value="22">22</option>
|
||
<option value="23">23</option>
|
||
<option value="24">24</option>
|
||
</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="hi">Hindi</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">
|
||
<label for="customFileName">Custom File Name:</label>
|
||
<input type="text" id="customFileName" name="customFileName" value="TTS">
|
||
</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 RVCVoice = document.getElementById("RVC_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 : '');
|
||
let currentGenerationId = 0;
|
||
|
||
////////////////////////////////////
|
||
//////// 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 RVCVoice = document.getElementById("RVC_voice_gen").value;
|
||
const RVCPitch = document.getElementById("RVC_voice_pitch").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)
|
||
const customFileName = document.getElementById("customFileName").value; // Get the custom file name
|
||
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\u0900-\u097F\u2018\u2019\u201C\u201D\u2026\u3001\u3002\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF\uAC00-\uD7A3\u1100-\u11FF\u3130-\u318F\uFF01\uFF0c\uFF1A\uFF1B\uFF1F]/g, '');
|
||
cleanedText = cleanedText.replace(/[^a-zA-Z0-9\s.,;:!?\-'"$À-ÿ\u00C0-\u017F\u0400-\u04FF\u0150\u0151\u0170\u0171\u0900-\u097F\u2018\u2019\u201C\u201D\u2026\u3001\u3002\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u0600-\u06FF\u0750-\u077F\uFB50-\uFDFF\uFE70-\uFEFF\uAC00-\uD7A3\u1100-\u11FF\u3130-\u318F\uFF01\uFF0c\uFF1A\uFF1B\uFF1F\u011E\u011F\u0130\u0131\u015E\u015F\u00E7\u00C7\u00F6\u00D6]/g, '');
|
||
cleanedText = cleanedText.replace(/-\\s/g, '; ');
|
||
cleanedText = cleanedText.replace(/--/g, '; ');
|
||
// Split by Fullwidth/CJK punctuation regardless of if there are spaces behind them
|
||
// Please test if it works properly. In my test they it should be
|
||
// (?<=[.!?:])\s+ instead of
|
||
// (?<=\[.!?:\])\\s+
|
||
// so I don't know whats going on here
|
||
// let sentences = cleanedText.split(/(?<=[\[.!?:\]])\s+|(?<=[\u3002\uFF01\uFF1A\uFF1F])/);
|
||
let sentences = cleanedText.split(/(?<=[\[.!?:\]]['"\u2018\u2019\u201C\u201D]*)\s+|(?<=[\u3002\uFF01\uFF1A\uFF1F])/);
|
||
|
||
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, RVCVoice, RVCPitch, volume, customFileName);
|
||
} else if (playModeValue === 'remote') {
|
||
// Generate the chunk and play it remotely
|
||
await generateAndPlayRemote(chunks[i], language, characterVoice, RVCVoice, RVCPitch, volume, customFileName);
|
||
} else if (playModeValue === 'none') {
|
||
// Generate the chunk but do not play it
|
||
await generateWithoutPlayback(chunks[i], language, characterVoice, RVCVoice, RVCPitch, customFileName);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Error in generateTTS:", error);
|
||
} finally {
|
||
// Hide progress when done or on error
|
||
hideProgress();
|
||
}
|
||
}
|
||
|
||
async function processChunks(chunks, language, characterVoice, RVCVoice, RVCPitch, volume, customFileName) {
|
||
for (let i = 0; i < chunks.length; i++) {
|
||
if (stopTTSGeneration) break;
|
||
await generateAndCache(chunks[i], language, characterVoice, RVCVoice, RVCPitch, volume, customFileName, i + 1); // Pass custom file name and chunk index
|
||
if (audioQueue.length >= preloadThreshold && !isPlaying) {
|
||
playNextLocalChunk();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Function to generate a 4-character UUID
|
||
function generateUUID() {
|
||
return 'xxxx'.replace(/[x]/g, function (c) {
|
||
var r = Math.random() * 16 | 0;
|
||
return r.toString(16);
|
||
});
|
||
}
|
||
|
||
async function generateAndCache(chunk, language, characterVoice, RVCVoice, RVCPitch, volume, customFileName, chunkIndex = null, id = null) {
|
||
try {
|
||
let paddedChunkIndex;
|
||
|
||
if (chunkIndex === null) {
|
||
// If chunkIndex is not provided, increment the current generation ID
|
||
currentGenerationId++;
|
||
paddedChunkIndex = String(currentGenerationId).padStart(5, '0');
|
||
} else {
|
||
// If chunkIndex is provided (during regeneration), use it as is
|
||
paddedChunkIndex = String(chunkIndex).padStart(5, '0');
|
||
}
|
||
|
||
// Generate a 4-character UUID
|
||
const uuid = generateUUID();
|
||
|
||
const data = new URLSearchParams({
|
||
text_input: chunk.trim(),
|
||
text_filtering: "standard",
|
||
character_voice_gen: characterVoice,
|
||
rvccharacter_voice_gen: RVCVoice,
|
||
rvccharacter_pitch: RVCPitch,
|
||
narrator_enabled: "false",
|
||
narrator_voice_gen: characterVoice,
|
||
text_not_inside: "character",
|
||
language: language,
|
||
output_file_name: `${customFileName}_${paddedChunkIndex}_${uuid}`, // Include the UUID in the file name
|
||
output_file_timestamp: "false",
|
||
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);
|
||
|
||
// Extract the file name from the fileUrl
|
||
const fileName = ttsDetails.fileUrl.split('/').pop();
|
||
|
||
// Extract the custom file name and chunk index from the file name
|
||
const fileNameParts = fileName.split('_');
|
||
const customFileName = fileNameParts[0];
|
||
const chunkIndex = parseInt(fileNameParts[1], 10);
|
||
|
||
// Call the TTS generation with the same details, current volume, and existing file name
|
||
const result = await generateAndCache(ttsDetails.text, ttsDetails.language, ttsDetails.characterVoice, currentVolume, customFileName, chunkIndex, id);
|
||
|
||
// 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, RVCVoice, RVCPitch, customFileName, chunkIndex = null) {
|
||
// Perform the generation process as usual but do not play the audio
|
||
try {
|
||
let paddedChunkIndex;
|
||
if (chunkIndex === null) {
|
||
// If chunkIndex is not provided, increment the current generation ID
|
||
currentGenerationId++;
|
||
paddedChunkIndex = String(currentGenerationId).padStart(5, '0');
|
||
} else {
|
||
// If chunkIndex is provided (during regeneration), use it as is
|
||
paddedChunkIndex = String(chunkIndex).padStart(5, '0');
|
||
}
|
||
|
||
const uuid = generateUUID();
|
||
const data = new URLSearchParams({
|
||
text_input: chunk.trim(),
|
||
text_filtering: "standard",
|
||
character_voice_gen: characterVoice,
|
||
rvccharacter_voice_gen: RVCVoice,
|
||
rvccharacter_pitch: RVCPitch,
|
||
narrator_enabled: "false",
|
||
narrator_voice_gen: characterVoice,
|
||
text_not_inside: "character",
|
||
language: language,
|
||
output_file_name: `${customFileName}_${paddedChunkIndex}_${uuid}`, // Use the custom file name with padded chunk index
|
||
output_file_timestamp: "false",
|
||
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) {
|
||
// Add the 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, RVCVoice, RVCPitch, volume, customFileName, chunkIndex = null) {
|
||
try {
|
||
if (stopTTSGeneration) {
|
||
console.log("TTS generation stopped.");
|
||
return; // Early return if stop flag is set
|
||
}
|
||
|
||
let paddedChunkIndex;
|
||
if (chunkIndex === null) {
|
||
// If chunkIndex is not provided, increment the current generation ID
|
||
currentGenerationId++;
|
||
paddedChunkIndex = String(currentGenerationId).padStart(5, '0');
|
||
} else {
|
||
// If chunkIndex is provided (during regeneration), use it as is
|
||
paddedChunkIndex = String(chunkIndex).padStart(5, '0');
|
||
}
|
||
|
||
const uuid = generateUUID();
|
||
const data = new URLSearchParams({
|
||
text_input: chunk.trim(),
|
||
text_filtering: "standard",
|
||
character_voice_gen: characterVoice,
|
||
rvccharacter_voice_gen: RVCVoice,
|
||
rvccharacter_pitch: RVCPitch,
|
||
narrator_enabled: "false",
|
||
narrator_voice_gen: characterVoice,
|
||
text_not_inside: "character",
|
||
language: language,
|
||
output_file_name: `${customFileName}_${paddedChunkIndex}_${uuid}`, // Use the custom file name with padded chunk index
|
||
output_file_timestamp: "false",
|
||
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) {
|
||
// Add the 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();
|
||
|
||
console.log("TTS generation complete for chunk:", chunk);
|
||
} else {
|
||
throw new Error(`API error: ${result.detail || 'Unknown error'}`);
|
||
}
|
||
} 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;
|
||
const RVCVoice = document.getElementById("RVC_voice_gen").value;
|
||
const RVCPitch = document.getElementById("RVC_voice_pitch").value;
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append("voice", characterVoice);
|
||
formData.append("rvccharacter_voice_gen", RVCVoice);
|
||
formData.append("rvccharacter_pitch", RVCPitch);
|
||
|
||
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
|
||
currentGenerationId = 0;
|
||
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);
|
||
}
|
||
}
|
||
|
||
async function fetchRVCVoices() {
|
||
try {
|
||
const response = await fetch(baseUrl + "/api/rvcvoices");
|
||
const result = await response.json();
|
||
const RVCVoiceSelect = document.getElementById("RVC_voice_gen");
|
||
|
||
RVCVoiceSelect.innerHTML = ''; // Clear existing options
|
||
|
||
// Ensure 'rvcvoices' exists in the response
|
||
if (result.rvcvoices && Array.isArray(result.rvcvoices)) {
|
||
result.rvcvoices.forEach(voice => {
|
||
const option = document.createElement("option");
|
||
option.value = voice;
|
||
option.text = voice;
|
||
RVCVoiceSelect.add(option);
|
||
});
|
||
} else {
|
||
console.error("No valid 'rvcvoices' found in the response");
|
||
}
|
||
|
||
// Restore saved RVC voice preference
|
||
const savedRVCVoice = localStorage.getItem('RVCVoice');
|
||
if (savedRVCVoice) {
|
||
RVCVoiceSelect.value = savedRVCVoice;
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error("Error fetching RVC voices:", 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;
|
||
});
|
||
|
||
// Event listener to update RVCVoice when the selection changes
|
||
document.getElementById("RVC_voice_gen").addEventListener('change', function () {
|
||
RVCVoice = 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();
|
||
fetchRVCVoices();
|
||
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 RVC voice based on preference
|
||
const savedRVCVoice = localStorage.getItem('RVCVoice');
|
||
if (savedRVCVoice) {
|
||
document.getElementById('RVC_voice_gen').value = savedRVCVoice;
|
||
}
|
||
|
||
// 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('RVC_voice_gen').addEventListener('change', savePreferences);
|
||
document.getElementById('language').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 RVC voice preference
|
||
const RVCVoice = document.getElementById('RVC_voice_gen').value;
|
||
localStorage.setItem('RVCVoice', RVCVoice);
|
||
|
||
// 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>
|