Files
2026-07-13 13:03:09 +08:00

4029 lines
241 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from lib.core import *
def build_interface(args:dict)->gr.Blocks:
from lib.classes.tts_engines.common.preset_loader import load_engine_presets
try:
script_mode = args['script_mode']
is_gui_process = args['is_gui_process']
is_gui_shared = args['share']
title = 'Ebook2Audiobook'
gr_glassmask_msg = 'Initialization, please wait…'
models = None
language_options = [
(
f"{details['name']} - {details['native_name']}" if details['name'] != details['native_name'] else details['name'],
lang
)
for lang, details in language_mapping.items()
]
translate_options = []
voice_options = []
tts_engine_options = []
custom_model_options = []
fine_tuned_options = []
audiobook_options = []
options_output_split_hours = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
page_size = 15
visible_gr_tab_xtts_params = interface_component_options['gr_tab_xtts_params']
visible_gr_tab_bark_params = interface_component_options['gr_tab_bark_params']
visible_gr_group_voice_file = interface_component_options['gr_group_voice_file']
visible_gr_group_custom_model = interface_component_options['gr_group_custom_model']
js_hide_elements = 'document.querySelector("#ebook_textarea_toolbar")?.remove();'
js_show_elements = 'window.gr_ebook_textarea_counter();'
theme = gr.themes.Origin(
primary_hue='green',
secondary_hue='amber',
neutral_hue='gray',
radius_size='lg',
font_mono=['JetBrains Mono', 'monospace', 'Consolas', 'Menlo', 'Liberation Mono']
)
header_css = '''
<style>
/* Global Scrollbar Customization */
/* The entire scrollbar */
::-webkit-scrollbar {
width: 6px !important;
height: 6px !important;
cursor: pointer !important;;
}
/* The scrollbar track (background) */
::-webkit-scrollbar-track {
background: none transparent !important;
border-radius: 6px !important;
}
/* The scrollbar thumb (scroll handle) */
::-webkit-scrollbar-thumb {
background: #c09340 !important;
border-radius: 6px !important;
}
/* The scrollbar thumb on hover */
::-webkit-scrollbar-thumb:hover {
background: #ff8c00 !important;
}
/* Firefox scrollbar styling */
html {
scrollbar-width: thin !important;
scrollbar-color: #c09340 none !important;
}
button:disabled {
pointer-events: none;
}
button div.wrap span {
display: none !important;
}
button div.wrap::after {
content: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%231E90FF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4'/><polyline points='17 8 12 3 7 8'/><line x1='12' y1='3' x2='12' y2='15'/></svg>") !important;
width: 24px !important;
height: 24px !important;
display: inline-block !important;
vertical-align: middle !important;
}
body:has(#gr_convert_btn:disabled) table.file-preview button.label-clear-button {
display: none !important;
}
span[data-testid="block-info"] {
font-size: 12px !important;
}
/////////////////////
.wrap-inner {
border: 1px solid #666666;
}
.no-wrap {
flex-wrap: nowrap !important;
}
.selected {
color: var(--secondary-500) !important;
text-shadow: 0.3px 0.3px 0.3px #303030;
}
.overflow-menu {
display: none !important;
}
.gr-glass-mask {
z-index: 9999 !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
background: rgba(0,0,0,0.5) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 1.2rem !important;
color: #ffffff !important;
text-align: center !important;
border: none !important;
opacity: 1;
pointer-events: all !important;
}
.gr-glass-mask.hide {
animation: fadeOut 2s ease-out 2s forwards !important;
}
.small-btn{
background: var(--block-background-fill) !important;
font-size: 22px !important;
width: 60px !important;
height: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
.small-btn:hover {
background: var(--button-primary-background-fill-hover) !important;
font-size: 28px !important;
}
.small-btn-red{
background: var(--block-background-fill) !important;
font-size: 22px !important;
width: 60px !important;
height: 60px !important;
margin: 0 !important;
padding: 0 !important;
}
.small-btn-red:hover {
background-color: #ff5050 !important;
font-size: 28px !important;
}
.small-btn-lock{
background: var(--block-background-fill) !important;
font-size: 18px !important;
width: 60px !important;
height: 60px !important;
margin: 0 !important;
padding: 0 !important;
}
.small-btn-lock:hover {
background-color: #752eb2 !important;
font-size: 20px !important;
}
.small-btn-lock:active {
background: var(--body-text-color) !important;
font-size: 20px !important;
color: var(--body-background-fill) !important;
}
.small-btn:active, .small-btn-red:active {
background: var(--body-text-color) !important;
font-size: 30px !important;
color: var(--body-background-fill) !important;
}
.micro-btn{
font-size: 16px !important;
background: var(--block-background-fill) !important;
width: 26px !important;
height: 26px !important;
margin: 0 !important;
padding: 0 !important;
border-radius: var(--radius-full) !important;
}
.micro-btn:hover {
background-color: #ff5050 !important;
}
.micro-btn:active {
background: var(--body-text-color) !important;
color: var(--body-background-fill) !important;
}
.file-preview-holder {
height: 116px !important;
overflow: auto !important;
}
.progress-bar.svelte-ls20lj {
background: var(--secondary-500) !important;
}
.file-preview-holder {
height: auto !important;
min-height: 0 !important;
max-height: none !important;
}
///////////////////
.gr-tab {
padding: 0 3px 0 3px !important;
margin: 0 !important;
border: none !important;
}
.gr-col {
padding: 0 6px 0 6px !important;
margin: 0 !important;
border: none !important;
}
.gr-group-main > div {
background: none !important;
border-radius: var(--radius-md) !important;
}
.gr-group > div {
background: none !important;
padding: 0 !important;
margin: 0 !important;
border-radius: 0 var(--radius-md) var(--radius-md) var(--radius-md) !important;
}
.gr-group-sides-padded{
background: none !important;
margin: 0 var(--size-2) 0 var(--size-2)!important;;
border-radius: 0 var(--radius-md) var(--radius-md) var(--radius-md) !important;
}
.gr-group-convert-btn{
margin: var(--size-2) !important;;
border-radius: var(--radius-md) !important;
}
.gr-label textarea[data-testid="textbox"]{
padding: 0 0 0 3px !important;
margin: 0 !important;
text-align: left !important;
font-weight: normal !important;
height: auto !important;
font-size: 12px !important;
border: none !important;
overflow-y: hidden !important;
line-height: 12px !important;
}
.gr-markdown p {
margin-top: 8px !important;
width: 90px !important;
padding: 0 !important;
border-radius: var(--radius-md) var(--radius-md) 0 0 !important;
background: var(--block-background-fill) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
text-align: center !important;
}
.gr-markdown-span {
margin-top: 8px !important;
width: 90px !important;
padding: 0 !important;
border-radius: var(--radius-md) var(--radius-md) 0 0 !important;
background: var(--block-background-fill) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
text-align: center !important;
}
.gr-markdown-output-split-hours {
overflow: hidden !important;
background: var(--block-background-fill) !important;
border-radius: 0 !important;
font-size: 12px !important;
text-align: center !important;
vertical-align: middle !important;
padding-top: 4px !important;
padding-bottom: 4px !important;
white-space: nowrap !important;
}
.gr-voice-player {
margin: 0 !important;
padding: 0 !important;
width: 60px !important;
height: 60px !important;
background: var(--block-background-fill) !important;
}
#gr_row_language {
align-items: stretch !important;
}
#gr_row_language > * {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
#gr_translate_enabled {
display: flex !important;
align-items: center !important;
justify-content: center !important;
padding: 0 !important;
}
#gr_translate_enabled > *,
#gr_translate_enabled label,
#gr_translate_enabled .wrap {
margin: 0 !important;
padding: 0 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 100% !important;
height: 100% !important;
}
.play-pause-button:hover svg {
fill: #ffab00 !important;
stroke: #ffab00 !important;
transform: scale(1.2) !important;
}
.gr-convert-btn {
font-size: 30px !important;
}
.gr-convert-btn:hover { background-color: #34d058 !important; }
.gr-convert-btn:active, .button-red:active {
background: var(--body-text-color) !important;
color: var(--body-background-fill) !important;
}
[id^="block_"]:has(input[type="checkbox"]:checked) {
border-left: 3px solid #22c55e !important;
}
[id^="block_"]:has(input[type="checkbox"]:checked) > div {
background-color: rgba(34, 197, 94, 0.08) !important;
}
[id^="block_"]:has(input[type="checkbox"]:not(:checked)) {
border-left: 3px solid #ef4444 !important;
}
[id^="block_"]:has(input[type="checkbox"]:not(:checked)) > div {
background-color: rgba(239, 68, 68, 0.08) !important;
}
////////////////////
#gr_ebook_textarea {
height: auto !important;
min-height: 55px !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
}
#gr_ebook_textarea label, #gr_custom_model_file label {
background: none !important;
border: none !important;
}
#gr_audiobook_player label {
display: none !important;
}
#gr_ebook_src, #gr_custom_model_file, #gr_voice_file {
height: auto !important;
min-height: 100px !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
}
#gr_ebook_src button>div, #gr_ebook_textarea button>div, #gr_custom_model_file button>div, #gr_voice_file button>div {
font-size: 12px !important;
}
#gr_ebook_src .empty, #gr_ebook_textarea .empty, #gr_custom_model_file .empty, #gr_voice_file .empty,
#gr_ebook_src .wrap, #gr_ebook_textarea .wrap, #gr_custom_model_file .wrap, #gr_voice_file .wrap {
height: 100% !important;
min-height: 100px !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
#gr_ebook_src button[aria-label="common.upload"], #gr_ebook_textarea button[aria-label="common.upload"], #gr_custom_model_file button[aria-label="common.upload"], #gr_voice_file button[aria-label="common.upload"] {
display: none !important;
}
#gr_ebook_src .file-preview-holder {
padding-top: 16px !important;
}
.gr-voice-highlight-css { display: none !important; }
#gr_ebook_src table.file-preview tbody > tr.file:hover {
background: var(--color-accent-soft) !important;
}
#gr_voice_selected_filename, #gr_custom_model_train_link {
display: flex !important;
align-items: center !important;
margin: auto !important;
padding-left: 6px !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
background: var(--block-background-fill) !important;
width: 100% !important;
height: 100% !important;
}
#gr_voice_selected_filename p, #gr_custom_model_train_link p {
margin: auto !important;
vertical-align: middle !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
background: var(--block-background-fill) !important;
width: 100% !important;
height: 100% !important;
}
#gr_voice_selected_filename a, #gr_custom_model_train_link a {
text-decoration: none !important;
}
#gr_custom_model_file [aria-label="Clear"], #gr_voice_file [aria-label="Clear"] {
display: none !important;
}
#gr_fine_tuned_list {
height: 80px !important;
}
#gr_voice_list {
height: 60px !important;
}
#gr_output_format_list {
height: 103px !important;
}
#gr_row_output_split_hours {
border-radius: 0 !important;
}
#gr_audiobook_sentence textarea{
margin: auto !important;
text-align: center !important;
}
#gr_session textarea, #gr_progress textarea {
overflow: hidden !important;
overflow-y: auto !important;
scrollbar-width: none !important;
}
#gr_group_progress .progress-bar, #gr_group_progress [role="progressbar"] > div {
background-color: #ff007f !important;
background-image: none !important;
}
#gr_progress {
height: 100px !important;
min-height: 100px !important;
max-height: 100px !important;
resize: none;
}
#gr_session textarea::-webkit-scrollbar, #gr_progress textarea::-webkit-scrollbar {
display: none !important;
}
#gr_ebook_mode span[data-testid="block-info"],
#gr_language span[data-testid="block-info"],
#gr_voice_list span[data-testid="block-info"],
#gr_device span[data-testid="block-info"],
#gr_tts_engine_list span[data-testid="block-info"],
#gr_output_split_hours span[data-testid="block-info"],
#gr_session span[data-testid="block-info"],
#gr_custom_model_list span[data-testid="block-info"],
#gr_audiobook_sentence span[data-testid="block-info"],
#gr_audiobook_list span[data-testid="block-info"],
#gr_progress span[data-testid="block-info"]{
display: none !important;
}
#gr_row_ebook_mode { align-items: center !important; }
#gr_blocks_preview {
align-self: center !important;
overflow: visible !important;
padding: 20px 0 20px 10px !important;
}
#gr_group_output_split {
border-radius: 0 !important;
}
#gr_tts_rating {
overflow: hidden !important;
}
#gr_row_voice_player, #gr_row_custom_model_list, #gr_row_session, #gr_row_audiobook_list {
height: 60px !important;
}
#gr_audiobook_player :is(.volume, .empty, .source-selection, .control-wrapper, .settings-wrapper, label), #gr_audiobook_files label[data-testid="block-label"] {
display: none !important;
}
#gr_audiobook_player audio {
width: 100% !important;
padding-top: 10px !important;
padding-bottom: 10px !important;
border-radius: 0px !important;
background-color: #ebedf0 !important;
color: #ffffff !important;
}
#gr_audiobook_player audio::-webkit-media-controls-panel {
width: 100% !important;
padding-top: 10px !important;
padding-bottom: 10px !important;
border-radius: 0px !important;
background-color: #ebedf0 !important;
color: #ffffff !important;
}
#gr_voice_player_hidden {
z-index: -100 !important;
position: absolute !important;
overflow: hidden !important;
margin: 0 !important;
padding: 0 !important;
width: 60px !important;
height: 60px !important;
}
#gr_session_update, #gr_restore_session, #gr_save_session,
#gr_audiobook_vtt, #gr_playback_time {
display: none !important;
}
#gr_blocks_nav {
overflow:hidden !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
#gr_blocks_nav p {
white-space:nowrap !important;
overflow:hidden !important;
font-size: 16px !important;
text-align: center !important;
}
#gr_row_buttons {
justify-content: center !important;
gap: 100px !important;
}
#gr_blocks_markdown {
background: var(--body-background-fill) !important;
width: 100% !important;
text-align: center !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
padding-bottom: 20px !important;
}
#gr_blocks_markdown p {
background: var(--body-background-fill) !important;
width: 100% !important;
font-size: 18px !important;
font-weight: bold !important;
}
///////////
.fade-in {
animation: fadeIn 1s ease-in !important;
display: inline-block !important;
}
@keyframes fadeIn {
from {
opacity: 0;
visibility: visible !important;
}
to {
opacity: 1;
}
}
@keyframes fadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
visibility: hidden;
pointer-events: none;
}
}
//////////
#custom-gr-modal-container,
#custom-gr-modal-container .gr-modal {
position: fixed !important;
}
.hide-elem {
z-index: -1 !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
}
.gr-modal {
position: fixed !important;
top: 0 !important; left: 0 !important;
width: 100% !important; height: 100% !important;
background-color: rgba(0, 0, 0, 0.5) !important;
z-index: 9999 !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
}
.gr-modal-content {
background-color: #333 !important;
padding: 20px !important;
border-radius: 9px !important;
text-align: center !important;
max-width: 300px !important;
height: auto !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.5) !important;
border: 2px solid #FFA500 !important;
color: white !important;
position: relative !important;
}
.gr-modal-content p {
overflow-wrap: break-word;
word-break: break-word;
white-space: normal;
}
.confirm-buttons {
display: flex !important;
justify-content: space-evenly !important;
margin-top: 20px !important;
}
.confirm-buttons button {
padding: 10px 20px !important;
border: none !important;
border-radius: 6px !important;
font-size: 16px !important;
cursor: pointer !important;
}
.accordion-block-even > button, .accordion-block-odd > button {
padding: 10px 0 10px 0 !important;
}
.accordion-block-even, .accordion-block-even div label textarea, .accordion-block-even .wrap {
background: var(--table-even-background-fill) !important;
}
.accordion-block-odd, .accordion-block-odd div label textarea, .accordion-block-odd .wrap {
background: var(--table-odd-background-fill) !important;
}
.accordion-block-even:hover,
.accordion-block-odd:hover {
background: rgba(255, 200, 50, 0.3) !important;
}
.accordion-block-voice-list {
margin: auto !important;
padding: 0 16px 0 0 !important;
}
.gr-blocks-buttons {
display: flex !important;
justify-content: space-evenly !important;
margin-top: 12px !important;
margin-bottom: 12px !important;
}
.gr-blocks-buttons button {
padding: 12px !important;
border: none !important;
border-radius: 9px !important;
font-size: 16px !important;
cursor: pointer !important;
}
.gr-blocks-buttons:hover { background-color: #34d058 !important; }
.gr-blocks-buttons:active, .button-red:active {
background: var(--body-text-color) !important;
color: var(--body-background-fill) !important;
}
.accordion-block-keep, .accordion-block-keep .wrap{
background: none !important;
}
.accordion-block-reset {
margin-left: 30px !important;
margin-right: 30px !important;
border-radius: 9px !important;
}
.button-green { background-color: #28a745 !important; color: white !important; }
.button-green:hover { background-color: #34d058 !important; }
.button-green:active, .button-red:active {
background: var(--body-text-color) !important;
color: var(--body-background-fill) !important;
}
.button-red {background-color: #dc3545 !important; color: white !important; }
.button-red:hover { background-color: #ff6f71 !important; }
.button-green:active, .button-red:active {
background: var(--body-text-color) !important;
color: var(--body-background-fill) !important;
}
.spinner {
margin: 15px auto !important;
border: 4px solid rgba(255, 255, 255, 0.2) !important;
border-top: 4px solid #FFA500 !important;
border-radius: 50% !important;
width: 30px !important;
height: 30px !important;
animation: spin 1s linear infinite !important;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
'''
with gr.Blocks(theme=theme, title=title, css=header_css, delete_cache=(604800, 86400)) as app:
with gr.Group(visible=True, elem_id='gr_group_main', elem_classes='gr-group-main') as gr_group_main:
with gr.Tabs(elem_id='gr_tabs') as gr_tabs:
with gr.Tab('Dashboard', elem_id='gr_tab_main', elem_classes='gr-tab') as gr_tab_main:
with gr.Row(elem_id='gr_row_tab_main'):
with gr.Column(elem_id='gr_col_1', elem_classes=['gr-col'], scale=3):
with gr.Group(elem_id='gr_group_ebook_src', elem_classes=['gr-group']):
gr_import_markdown = gr.Markdown(elem_id='gr_import_markdown', elem_classes=['gr-markdown'], value='Import')
gr_ebook_src = gr.File(show_label=False, label='-', elem_id='gr_ebook_src', visible=True, file_types=ebook_formats, file_count=ebook_modes['SINGLE'], allow_reordering=True, height=100)
gr_voice_highlight_css = gr.HTML(value='', elem_classes=['gr-voice-highlight-css'])
gr_ebook_textarea = gr.Textbox(show_label=True, label='Text Prompt', elem_id='gr_ebook_textarea', visible=False, lines=8, max_length=max_ebook_textarea_length)
with gr.Row(elem_id='gr_row_ebook_mode') as gr_row_ebook_mode:
gr_ebook_mode = gr.Dropdown(label='', elem_id='gr_ebook_mode', choices=[('File',ebook_modes['SINGLE']), ('Directory',ebook_modes['DIRECTORY']), ('Text',ebook_modes['TEXT'])], interactive=True, scale=2)
gr_blocks_preview = gr.Checkbox(label='Chapters Preview', elem_id='gr_blocks_preview', value=False, interactive=True, scale=1)
with gr.Group(elem_id='gr_group_language', elem_classes=['gr-group']):
gr_language_markdown = gr.Markdown(elem_id='gr_language_markdown', elem_classes=['gr-markdown'], value='Language')
with gr.Row(elem_id='gr_row_language') as gr_row_language:
gr_language = gr.Dropdown(show_label=False, elem_id='gr_language', choices=language_options, value=default_language_code, type='value', interactive=True, scale=2)
gr_translate_enabled = gr.Checkbox(label='Translate', elem_id='gr_translate_enabled', value=False, interactive=True, scale=1, min_width=120)
gr_translate = gr.Dropdown(show_label=False, elem_id='gr_translate', choices=[], value=None, type='value', interactive=True, visible=False, scale=2)
gr_group_voice_file = gr.Group(elem_id='gr_group_voice_file', elem_classes=['gr-group'], visible=visible_gr_group_voice_file)
with gr_group_voice_file:
gr_voice_markdown = gr.Markdown(elem_id='gr_voice_markdown', elem_classes=['gr-markdown'], value='Voices')
gr_voice_file = gr.File(show_label=False, label='Upload Voice', elem_id='gr_voice_file', file_types=voice_formats, value=None, height=100)
with gr.Row(elem_id='gr_row_voice_player') as gr_row_voice_player:
gr_voice_player_hidden = gr.Audio(elem_id='gr_voice_player_hidden', type='filepath', interactive=False, waveform_options=gr.WaveformOptions(show_recording_waveform=False), show_download_button=False, container=False, visible='hidden', show_share_button=True, show_label=False, scale=0, min_width=60)
gr_voice_play = gr.Button('▶', elem_id='gr_voice_play', elem_classes=['small-btn'], variant='secondary', interactive=True, visible=False, scale=0, min_width=60)
gr_voice_list = gr.Dropdown(label='Voices', elem_id='gr_voice_list', choices=voice_options, type='value', interactive=True, scale=2)
gr_voice_selected_filename = gr.Markdown(value='', elem_id='gr_voice_selected_filename', elem_classes=['gr-markdown'], visible=False)
gr_voice_del_btn = gr.Button('🗑', elem_id='gr_voice_del_btn', elem_classes=['small-btn-red'], variant='secondary', interactive=True, visible=False, scale=0, min_width=60)
with gr.Group(elem_id='gr_group_device', elem_classes=['gr-group']):
gr_device_markdown = gr.Markdown(elem_id='gr_device_markdown', elem_classes=['gr-markdown'], value='Processor')
gr_device = gr.Dropdown(label='', elem_id='gr_device', choices=[(k, v['proc']) for k, v in devices.items()], type='value', value=default_device, interactive=True)
with gr.Column(elem_id='gr_col_2', elem_classes=['gr-col'], scale=3):
with gr.Group(elem_id='gr_group_tts_engine', elem_classes=['gr-group']):
gr_tts_rating = gr.Markdown(elem_id='gr_tts_rating', elem_classes=['gr-markdown'], value='TTS Engine')
gr_tts_engine_list = gr.Dropdown(label='', elem_id='gr_tts_engine_list', choices=tts_engine_options, type='value', interactive=True)
with gr.Group(elem_id='gr_group_models', elem_classes=['gr-group']):
gr_models_markdown = gr.Markdown(elem_id='gr_models_markdown', elem_classes=['gr-markdown'], value='Models')
gr_fine_tuned_list = gr.Dropdown(label='Fine Tuned Preset Models', elem_id='gr_fine_tuned_list', choices=fine_tuned_options, type='value', interactive=True)
gr_group_custom_model = gr.Group(visible=False)
with gr_group_custom_model:
gr_custom_model_file = gr.File(show_label=True, label=f"Upload a ZIP File", elem_id='gr_custom_model_file', value=None, file_types=['.zip'], height=100)
gr_row_custom_model_list = gr.Row(elem_id='gr_row_custom_model_list')
with gr_row_custom_model_list:
gr_custom_model_list = gr.Dropdown(label='', elem_id='gr_custom_model_list', choices=custom_model_options, type='value', interactive=True, scale=2)
gr_custom_model_train_link = gr.Markdown(value='<a href="https://huggingface.co/spaces/drewThomasson/xtts-finetune-webui-gpu" target="_blank" rel="noopener noreferrer">Create My Own Model</a>', elem_id='gr_custom_model_train_link', elem_classes=['gr-markdown'], visible=True)
gr_custom_model_del_btn = gr.Button('🗑', elem_id='gr_custom_model_del_btn', elem_classes=['small-btn-red'], variant='secondary', interactive=True, visible=False, scale=0, min_width=60)
with gr.Group(elem_id='gr_group_output_format'):
gr_output_markdown = gr.Markdown(elem_id='gr_output_markdown', elem_classes=['gr-markdown'], value='Output')
with gr.Row(elem_id='gr_row_output_format'):
gr_output_format_list = gr.Dropdown(label='Format', elem_id='gr_output_format_list', choices=output_formats, type='value', value=default_output_format, interactive=True, scale=1)
gr_output_channel_list = gr.Dropdown(label='Channel', elem_id='gr_output_channel_list', choices=['mono', 'stereo'], type='value', value=default_output_channel, interactive=True, scale=1)
with gr.Group(elem_id='gr_group_output_split'):
gr_output_split = gr.Checkbox(label='Split File', elem_id='gr_output_split', value=default_output_split, interactive=True)
gr_row_output_split_hours = gr.Row(elem_id='gr_row_output_split_hours', visible=False)
with gr_row_output_split_hours:
gr_output_split_hours_markdown = gr.Markdown(elem_id='gr_output_split_hours_markdown',elem_classes=['gr-markdown-output-split-hours'], value='Hours<br/>/ Part')
gr_output_split_hours = gr.Dropdown(label='', elem_id='gr_output_split_hours', choices=options_output_split_hours, type='value', value=default_output_split_hours, interactive=True, scale=1)
with gr.Group(elem_id='gr_group_session', elem_classes=['gr-group']):
gr_session_markdown = gr.Markdown(elem_id='gr_session_markdown', elem_classes=['gr-markdown'], value='Session')
gr_session_switch_disable_state = gr.State(None)
gr_session_switch_enable_state = gr.State(None)
with gr.Row(elem_id='gr_row_session'):
gr_session = gr.Textbox(label='', elem_id='gr_session', interactive=False)
gr_session_switch_btn = gr.Button('🔒︎', elem_id='gr_session_switch_btn', elem_classes=['small-btn-lock'], variant='secondary', visible=True, interactive=True, scale=0, min_width=60)
with gr.Tab('XTTS Settings', elem_id='gr_tab_xtts_params', elem_classes='gr-tab', visible=False) as gr_tab_xtts_params:
with gr.Group(elem_id='gr_group_xtts_params', elem_classes=['gr-group']):
gr_xtts_temperature = gr.Slider(
label='Temperature',
minimum=0.05,
maximum=5.0,
step=0.05,
value=float(default_engine_settings[TTS_ENGINES['XTTS']]['temperature']),
elem_id='gr_xtts_temperature',
info='Higher values lead to more creative, unpredictable outputs. Lower values make it more monotone.'
)
gr_xtts_length_penalty = gr.Slider(
label='Length Penalty',
minimum=0.3,
maximum=5.0,
step=0.1,
value=float(default_engine_settings[TTS_ENGINES['XTTS']]['length_penalty']),
elem_id='gr_xtts_length_penalty',
info='Adjusts how much longer sequences are preferred. Higher values encourage the model to produce longer and more natural speech.',
visible=False
)
gr_xtts_num_beams = gr.Slider(
label='Number Beams',
minimum=1,
maximum=10,
step=1,
value=int(default_engine_settings[TTS_ENGINES['XTTS']]['num_beams']),
elem_id='gr_xtts_num_beams',
info='Controls how many alternative sequences the model explores. Higher values improve speech coherence and pronunciation but increase inference time.',
visible=False
)
gr_xtts_repetition_penalty = gr.Slider(
label='Repetition Penalty',
minimum=1.0,
maximum=5.0,
step=0.1,
value=float(default_engine_settings[TTS_ENGINES['XTTS']]['repetition_penalty']),
elem_id='gr_xtts_repetition_penalty',
info='Penalizes repeated phrases. Higher values reduce repetition.'
)
gr_xtts_top_k = gr.Slider(
label='Top-k Sampling',
minimum=10,
maximum=100,
step=1,
value=int(default_engine_settings[TTS_ENGINES['XTTS']]['top_k']),
elem_id='gr_xtts_top_k',
info='Lower values restrict outputs to more likely words and increase speed at which audio generates.'
)
gr_xtts_top_p = gr.Slider(
label='Top-p Sampling',
minimum=0.1,
maximum=1.0,
step=0.01,
value=float(default_engine_settings[TTS_ENGINES['XTTS']]['top_p']),
elem_id='gr_xtts_top_p',
info='Controls cumulative probability for word selection. Lower values make the output more predictable and increase speed at which audio generates.'
)
gr_xtts_speed = gr.Slider(
label='Speed',
minimum=0.5,
maximum=3.0,
step=0.1,
value=float(default_engine_settings[TTS_ENGINES['XTTS']]['speed']),
elem_id='gr_xtts_speed',
info='Adjusts how fast the narrator will speak.'
)
gr_xtts_enable_text_splitting = gr.Checkbox(
label='Enable Text Splitting',
value=default_engine_settings[TTS_ENGINES['XTTS']]['enable_text_splitting'],
elem_id='gr_xtts_enable_text_splitting',
info='Coqui-tts builtin text splitting. Can help against hallucinations bu can also be worse.',
visible=False
)
with gr.Tab('Bark Settings', elem_id='gr_tab_bark_params', elem_classes='gr-tab', visible=False) as gr_tab_bark_params:
gr.Markdown(
elem_id='gr_markdown_tab_bark_params',
value='''
### Customize BARK Parameters
Adjust the settings below to influence how the audio is generated, emotional and voice behavior random or more conservative
'''
)
with gr.Group(elem_id='gr_group_bark_params', elem_classes=['gr-group']):
gr_bark_text_temp = gr.Slider(
label='Text Temperature',
minimum=0.0,
maximum=1.0,
step=0.01,
value=float(default_engine_settings[TTS_ENGINES['BARK']]['text_temp']),
elem_id='gr_bark_text_temp',
info='Higher values lead to more creative, unpredictable outputs. Lower values make it more conservative.'
)
gr_bark_waveform_temp = gr.Slider(
label='Waveform Temperature',
minimum=0.0,
maximum=1.0,
step=0.01,
value=float(default_engine_settings[TTS_ENGINES['BARK']]['waveform_temp']),
elem_id='gr_bark_waveform_temp',
info='Higher values lead to more creative, unpredictable outputs. Lower values make it more conservative.'
)
with gr.Group(elem_id='gr_group_progress', elem_classes=['gr-group-sides-padded']):
gr_progress_markdown = gr.Markdown(elem_id='gr_progress_markdown', elem_classes=['gr-markdown'], value='Status')
gr_progress = gr.Textbox(elem_id='gr_progress', label='', interactive=False, visible=True)
gr_progress_bar = gr.Progress(track_tqdm=True)
with gr.Group(elem_id='gr_group_audiobook_list', elem_classes=['gr-group-sides-padded'], visible=True) as gr_group_audiobook_list:
gr_audiobook_markdown = gr.Markdown(elem_id='gr_audiobook_markdown', elem_classes=['gr-markdown'], value='Audiobook')
gr_audiobook_vtt = gr.Textbox(elem_id='gr_audiobook_vtt', label='', interactive=False, visible='hidden')
gr_playback_time = gr.Number(elem_id="gr_playback_time", label='', interactive=False, visible='hidden', value=0.0)
gr_audiobook_sentence = gr.Textbox(elem_id='gr_audiobook_sentence', label='', value='…', interactive=False, lines=3, max_lines=3)
gr_audiobook_player = gr.Audio(elem_id='gr_audiobook_player', label='', type='filepath', autoplay=False, interactive=False, waveform_options=gr.WaveformOptions(show_recording_waveform=False), show_download_button=False, show_share_button=False, container=True, visible=True)
with gr.Row(elem_id='gr_row_audiobook_list', visible=True) as gr_row_audiobook_list:
gr_audiobook_download_btn = gr.Button(elem_id='gr_audiobook_download_btn', value='↧', elem_classes=['small-btn'], variant='secondary', interactive=True, scale=0, min_width=60)
gr_audiobook_list = gr.Dropdown(elem_id='gr_audiobook_list', label='', choices=audiobook_options, type='value', interactive=True, scale=2)
gr_audiobook_del_btn = gr.Button(elem_id='gr_audiobook_del_btn', value='🗑', elem_classes=['small-btn-red'], variant='secondary', interactive=True, scale=0, min_width=60)
gr_audiobook_files = gr.Files(label='', elem_id='gr_audiobook_files', visible=False)
gr_audiobook_files_state = gr.State(False)
with gr.Group(elem_id='gr_group_convert_btn', elem_classes=['gr-group-convert-btn']) as gr_group_convert_btn:
gr_convert_btn = gr.Button(elem_id='gr_convert_btn', value='📚', elem_classes='gr-convert-btn', variant='primary', interactive=False)
gr_blocks_page = gr.Number(value=0, visible=False, precision=0)
gr_blocks_data = gr.State([])
gr_blocks_expands = gr.State([False] * page_size)
with gr.Group(visible=False, elem_id='gr_group_blocks', elem_classes='gr-group-main') as gr_group_blocks:
gr_blocks_markdown = gr.Markdown(elem_id='gr_blocks_markdown', elem_classes=['gr-markdown'], value='')
with gr.Row(elem_id='gr_blocks_nav') as gr_blocks_nav:
gr_blocks_back_btn = gr.Button('◀', elem_id='gr_blocks_back_btn', interactive=False, scale=1)
gr_blocks_header = gr.Markdown('', elem_id='gr_blocks_header')
gr_blocks_next_btn = gr.Button('▶', elem_id='gr_blocks_next_btn', interactive=False, scale=1)
block_components = []
with gr.Column(elem_id='gr_column_blocks', elem_classes=['gr-col']):
for i in range(page_size):
acc_class = 'accordion-block-even' if i % 2 == 0 else 'accordion-block-odd'
with gr.Accordion(
f'Block {i}',
elem_id=f'block_{i}',
elem_classes=[acc_class],
visible=False,
open=False
) as acc:
with gr.Row(elem_id=f'block_options_row_{i}', elem_classes=[acc_class, 'no-wrap']) as block_options_row:
acc_keep = gr.Checkbox(
label='',
elem_id=f'block_keep_{i}',
elem_classes=['accordion-block-keep'],
value=True,
interactive=True,
scale=0,
visible=True
)
acc_voice_list = gr.Dropdown(
show_label=False,
elem_id=f'block_voice_{i}',
elem_classes=['accordion-block-voice-list'],
choices=voice_options,
type='value',
interactive=True,
scale=1
)
acc_reset_btn = gr.Button(
'↺',
elem_id=f'block_reset_{i}',
elem_classes=['accordion-block-reset'],
variant='secondary',
interactive=True,
scale=0,
min_width=40
)
acc_text = gr.Textbox(
show_label=False,
elem_id=f'block_text_{i}',
lines=18,
max_lines=18,
container=False,
interactive=True
)
acc_reset_btn.click(
fn=lambda session, _i=i: _click_reset_block(session, _i),
inputs=[gr_session],
outputs=[acc_text]
)
acc.expand(
fn=lambda expands, _i=i: [
expands[j] if j != _i else True
for j in range(page_size)
],
inputs=[gr_blocks_expands],
outputs=[gr_blocks_expands]
)
acc.collapse(
fn=lambda expands, _i=i: [
expands[j] if j != _i else False
for j in range(page_size)
],
inputs=[gr_blocks_expands],
outputs=[gr_blocks_expands]
)
block_components.append((acc, acc_keep, acc_voice_list, acc_text))
with gr.Row(elem_id='gr_row_buttons', visible=True) as gr_row_buttons:
gr_blocks_cancel_btn = gr.Button('🡄', elem_classes=['gr-blocks-buttons'], variant='stop', scale=0, size='md')
gr_blocks_confirm_btn = gr.Button('🡆', elem_classes=['gr-blocks-buttons'], variant='primary', scale=0, size='md')
blocks_components_flat = [comp for quad in block_components for comp in quad]
blocks_keeps = [c[1] for c in block_components]
blocks_voices = [c[2] for c in block_components]
blocks_texts = [c[3] for c in block_components]
gr_version_markdown = gr.Markdown(elem_id='gr_version_markdown', value=f'''
<div style="right:0;margin:auto;padding:10px;text-align:center">
<a href="https://github.com/DrewThomasson/ebook2audiobook" style="text-decoration:none;font-size:14px" target="_blank">
<b>{title}</b>&nbsp;<b style="color:orange; text-shadow: 0.3px 0.3px 0.3px #303030">{prog_version}</b></a>
</div>
'''
)
gr_modal = gr.HTML(visible=False)
gr_glassmask = gr.HTML(gr_glassmask_msg, elem_id='gr_glassmask', elem_classes=['gr-glass-mask'])
gr_data_field_hidden = gr.Textbox(elem_id='gr_data_field_hidden', visible=False)
gr_deletion_cancel_btn = gr.Button(elem_id='gr_deletion_cancel_btn', elem_classes=['hide-elem'], value='🡄', variant='stop', visible=True, scale=0, size='sm', min_width=0)
gr_deletion_confirm_btn = gr.Button(elem_id='gr_deletion_confirm_btn', elem_classes=['hide-elem'], value='🡆', variant='primary', visible=True, scale=0, size='sm', min_width=0)
gr_override_cancel_btn = gr.Button(elem_id='gr_override_cancel_btn', elem_classes=['hide-elem'], value='🡄', variant='stop', visible=True, scale=0, size='sm', min_width=0)
gr_override_confirm_btn = gr.Button(elem_id='gr_override_confirm_btn', elem_classes=['hide-elem'], value='🡆', variant='primary', visible=True, scale=0, size='sm', min_width=0)
gr_restore_session = gr.JSON(elem_id='gr_restore_session', visible='hidden')
gr_session_update = gr.State({'hash': None})
gr_save_session = gr.JSON(elem_id='gr_save_session', visible='hidden')
gr_event = gr.Number(value=0, visible=False, precision=0)
gr_blocks_event = gr.Number(value=0, visible=False, precision=0)
gr_end_event = gr.Number(value=0, visible=False, precision=0)
gr_backup_session = gr.State(value=None)
gr_dummy_bool = gr.State(value=False)
############## End of Gradio Components creation
def _disable_components(session_id:str, exceptions:list|None=None)->tuple:
if session_id is None:
outputs = tuple(gr.update() for _ in range(22))
else:
if exceptions is None:
exceptions = []
if 'gr_session_switch_btn' in exceptions:
outputs = tuple(gr.update(interactive=False) for _ in range(21)) + (gr.update(interactive=True),)
else:
outputs = tuple(gr.update(interactive=False) for _ in range(22))
return outputs
def _enable_components(session_id:str)->tuple:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
if session['status'] in [status_tags['READY'], status_tags['END']]:
session['status'] = status_tags['READY']
session['cancellation_requested'] = False
outputs = list(gr.update(interactive=True) for _ in range(22))
visible_custom_model_del_btn = True if session['custom_model'] is not None else False
enabled_convert_btn = False
if session['ebook_mode'] == ebook_modes['DIRECTORY']:
if session.get('ebook_list'):
enabled_convert_btn = True
elif session['ebook_mode'] == ebook_modes['SINGLE']:
if session.get('ebook_src'):
enabled_convert_btn = True
elif session['ebook_mode'] == ebook_modes['TEXT']:
enabled_convert_btn = True
return tuple(outputs) + (gr.update(interactive=True, visible=visible_custom_model_del_btn), gr.update(value=''), gr.update(interactive=enabled_convert_btn))
except Exception as e:
error = f'_enable_components(): {e}'
exception_alert(session_id, error)
outputs = tuple(gr.update() for _ in range(25))
return outputs
def _disable_on_voice_upload()->tuple:
outputs = tuple([gr.update(interactive=False) for _ in range(10)])
return outputs + (gr.update(visible='hidden'), gr.update(visible='hidden'))
def _enable_on_voice_upload(session_id:str)->tuple:
session = context.get_session(session_id)
visible_buttons = 'hidden'
enabled_convert_btn = False
outputs = tuple([gr.update(interactive=True) for _ in range(9)])
if session and session.get('id', False):
enabled_convert_btn = True if session['ebook'] is not None else enabled_convert_btn
visible_buttons = True if session['voice'] is not None else visible_buttons
return outputs + (gr.update(interactive=enabled_convert_btn), gr.update(visible=visible_buttons), gr.update(visible=visible_buttons))
def _disable_on_custom_upload()->tuple:
outputs = tuple([gr.update(interactive=False) for _ in range(11)])
return outputs + (gr.update(visible='hidden'),)
def _enable_on_custom_upload(custom_model:str|None, ebook_data:any, ebook_textarea:any)->tuple:
outputs = tuple([gr.update(interactive=True) for _ in range(10)])
enabled_convert_btn = True if ebook_data or ebook_textarea else False
visible_custom_model_del_btn = True if custom_model is not None else False
return outputs + (gr.update(interactive=enabled_convert_btn), gr.update(visible=visible_custom_model_del_btn))
def _show_gr_modal(type:str, msg:str)->str:
return f'''
<div id="custom-gr_modal" class="gr-modal">
<div class="gr-modal-content">
<p style="color:#ffffff">{msg}</p>
{_show_gr_modal_buttons(type)}
</div>
</div>
'''
def _show_gr_modal_buttons(type:str)->str:
if type in [status_tags['DELETION'], status_tags['OVERRIDE']]:
cancel_btn = f'#gr_{type}_cancel_btn'
confirm_btn = f'#gr_{type}_confirm_btn'
return f'''
<div class="confirm-buttons">
<button class="button-red" style="width:50px; height:50px" onclick="document.querySelector('{cancel_btn}').click()">✖</button>
<button class="button-green" style="width:50px; height:50px" onclick="document.querySelector('{confirm_btn}').click()">✔</button>
</div>
'''
else:
return '<div class="spinner"></div>'
def _yellow_stars(n:int):
return "".join(
"<span style='color:#f0bc00; font-size:12px'>★</span>" for _ in range(n)
)
def _color_box(value:int)->str:
if value <= 4:
color = "#4CAF50" # Green = low
elif value <= 8:
color = "#FF9800" # Orange = medium
else:
color = "#F44336" # Red = high
return f"<span style='background:{color};color:white; padding: 0 3px 0 3px; border-radius:3px; font-size:11px; white-space: nowrap'>{str(value)} GB</span>"
def _show_rating(tts_engine:str)->str:
rating = default_engine_settings[tts_engine]['rating']
return f'''
<div style="display:flex; justify-content:space-between; align-items:flex-end;">
<span class="gr-markdown-span">TTS Engine</span>
<table style="
display:inline-block;
border-collapse:collapse;
border:none;
margin:0;
padding:0;
font-size:12px;
line-height:1.2; /* compact, but no clipping */
">
<tr style="border:none; vertical-align:bottom;">
<td style="padding:0 5px 0 2.5px; border:none; vertical-align:bottom;">
<b>VRAM:</b> {_color_box(int(rating['VRAM']))}
</td>
<td style="padding:0 5px 0 2.5px; border:none; vertical-align:bottom;">
<b>CPU:</b> {_yellow_stars(int(rating['CPU']))}
</td>
<td style="padding:0 5px 0 2.5px; border:none; vertical-align:bottom;">
<b>RAM:</b> {_color_box(int(rating['RAM']))}
</td>
<td style="padding:0 5px 0 2.5px; border:none; vertical-align:bottom;">
<b>Realism:</b> {_yellow_stars(int(rating['Realism']))}
</td>
</tr>
</table>
</div>
'''
def _is_valid_gradio_cache(path):
if not path or not os.path.isfile(path):
return False
path = os.path.normpath(path)
parent = os.path.dirname(path)
return (
parent.startswith(gradio_cache_dir) and
len(os.path.basename(parent)) >= 32
)
def _build_translate_targets(lang:str)->list:
try:
if not lang:
return []
return ArgosTranslator().get_target_options(lang)
except Exception as e:
error = f'_build_translate_targets() error: {e}'
print(error)
return []
def _restore_interface(session_id:str, req:gr.Request)->tuple:
try:
nonlocal translate_options
session = context.get_session(session_id)
if session and session.get('id', False):
socket_hash = str(req.session_hash)
if not session.get(socket_hash):
outputs = tuple([gr.update() for _ in range(25)])
return outputs
ebook_data = None
ebook_textarea = None
upload_mode = session['ebook_mode']
ebook_file_count = ebook_modes['SINGLE']
visible_ebook_src = False
visible_ebook_textarea = False
enabled_convert_btn = False
if session.get('ebook_mode') == ebook_modes['TEXT']:
ebook_textarea = session['ebook_textarea']
visible_ebook_textarea = True
enabled_convert_btn = True
elif session.get('ebook_mode') == ebook_modes['DIRECTORY']:
ebook_file_count = ebook_modes['DIRECTORY']
if session.get('ebook_list', None) is not None:
if len(session['ebook_list']) > 0:
ebook_data = [f for f in session['ebook_list'] if _is_valid_gradio_cache(f)]
if ebook_data:
enabled_convert_btn = True
else:
ebook_data = None
visible_ebook_src = True
elif session.get('ebook_mode') == ebook_modes['SINGLE']:
if _is_valid_gradio_cache(session['ebook_src']):
ebook_data = session['ebook_src']
if ebook_data:
enabled_convert_btn = True
visible_ebook_src = True
visible_row_split_hours = True if session['output_split'] else False
visible_group_custom_model = visible_gr_group_custom_model if session['fine_tuned'] == 'internal' and session['tts_engine'] in tts_engines_with_custom_model else False
visible_voice_buttons = True if session.get('voice') is not None else False
visible_custom_model_del_btn = True if session['custom_model'] is not None else False
voice_file = session.get('voice')
translate_enabled_state = bool(session.get('translate_enabled'))
language = session.get('language')
translate = session.get('translate')
translate_options = _build_translate_targets(language) if language else []
translate_codes = {o[1] for o in translate_options}
if translate not in translate_codes:
translate = translate_options[0][1] if translate_options else None
session['translate'] = translate
try:
session['translate_iso1'] = Lang(translate).pt1
except Exception:
session['translate_iso1'] = None
translate_visible = translate_enabled_state and bool(translate_options)
return (
gr.update(visible=visible_ebook_src, value=ebook_data, file_count=ebook_file_count),
gr.update(visible=visible_ebook_textarea, value=ebook_textarea),
gr.update(value=session['ebook_mode']),
gr.update(value=bool(session['blocks_preview'])),
gr.update(value=session['device']),
gr.update(value=session['language']),
gr.update(value=translate_enabled_state),
gr.update(visible=translate_visible, choices=translate_options, value=translate),
_update_gr_voice_list(session_id),
_update_gr_tts_engine_list(session_id),
gr.update(value=_show_rating(session['tts_engine'])),
_update_gr_custom_model_list(session_id),
_update_gr_fine_tuned_list(session_id),
gr.update(value=session['output_format']),
gr.update(value=session['output_channel']),
gr.update(value=bool(session['output_split'])),
gr.update(value=session['output_split_hours']),
gr.update(visible=visible_row_split_hours),
_update_gr_audiobook_list(session_id),
gr.update(visible=visible_group_custom_model),
gr.update(interactive=enabled_convert_btn),
gr.update(value=voice_file),
gr.update(visible=visible_voice_buttons),
gr.update(visible=visible_voice_buttons),
gr.update(label=f"Upload a {session['tts_engine'].upper()} ZIP file (Required: {', '.join(models[default_fine_tuned]['files'])})"),
gr.update(visible=visible_custom_model_del_btn)
)
except Exception as e:
error = f'_restore_interface(): {e}'
exception_alert(session_id, error)
outputs = tuple([gr.update() for _ in range(25)])
return outputs
def _restore_audiobook_player(session_id:str, audiobook:str|None)->tuple:
try:
visible = True if audiobook is not None else False
return gr.update(visible=visible), gr.update(value=audiobook), gr.update(active=True)
except Exception as e:
error = f'_restore_audiobook_player(): {e}'
exception_alert(session_id, error)
outputs = tuple([gr.update() for _ in range(3)])
return outputs
def _refresh_interface(session_id:str)->tuple:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
if session['cancellation_requested']:
session['status'] = status_tags['READY']
if session['status'] in [status_tags['READY'], status_tags['END']]:
visible_main = True
visible_xtts = False
visible_bark = False
visible_ebook_src = False
visible_ebook_textarea = False
enabled_convert_btn = False
ebook_data = None
ebook_textarea = None
if session['tts_engine'] == TTS_ENGINES['XTTS']:
visible_xtts = visible_gr_tab_xtts_params
elif session['tts_engine'] == TTS_ENGINES['BARK']:
visible_bark = visible_gr_tab_bark_params
if session['ebook_mode'] == ebook_modes['DIRECTORY']:
visible_ebook_src = True
ebook_data = session['ebook_list']
elif session['ebook_mode'] == ebook_modes['SINGLE']:
visible_ebook_src = True
ebook_data = session['ebook_src']
elif session['ebook_mode'] == ebook_modes['TEXT']:
visible_ebook_textarea = True
ebook_textarea = session['ebook_textarea']
enabled_convert_btn = True if session['ebook_mode'] == ebook_modes['TEXT'] or ebook_data is not None else False
return (
gr.update(value='', visible=False), gr.update(visible=visible_main),
gr.update(visible=visible_xtts), gr.update(visible=visible_bark),
gr.update(interactive=enabled_convert_btn), gr.update(visible=visible_ebook_src, value=ebook_data), gr.update(visible=visible_ebook_textarea, value=ebook_textarea),
gr.update(value=session['device']), gr.update(value=session['audiobook']), _update_gr_audiobook_list(session_id),
_update_gr_voice_list(session_id), gr.update(''), gr.update(value='')
)
elif session['status'] in [status_tags['CONVERTING']]:
return (
gr.update(), gr.update(), gr.update(),
gr.update(), gr.update(), gr.update(visible=True, value=session['ebook_list']), gr.update(),
gr.update(), gr.update(), gr.update(),
gr.update(), gr.update(), gr.update()
)
except Exception as e:
error = f'_refresh_interface(): {e}'
exception_alert(session_id, error)
outputs = tuple([gr.update() for _ in range(13)])
return outputs
def _change_gr_audiobook_list(session_id:str, selected:str|None)->dict:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get('audiobook') != selected:
session['audiobook'] = selected
visible = session['audiobook'] is not None
return gr.update(visible=visible)
except Exception as e:
error = f'_change_gr_audiobook_list(): {e}'
exception_alert(session_id, error)
return gr.update(visible=False)
def _update_gr_audiobook_player(session_id:str)->tuple:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
if session['audiobook'] is not None:
vtt = Path(session['audiobook']).with_suffix('.vtt')
if not os.path.exists(session['audiobook']) or not os.path.exists(vtt):
error = f"{Path(session['audiobook']).name} does not exist!"
exception_alert(session_id, error)
return gr.update(value=0.0), gr.update(value=None), gr.update(value=None)
audio_info = mediainfo(session['audiobook'])
duration = audio_info.get('duration', False)
if duration:
session['duration'] = float(audio_info['duration'])
with open(vtt, "r", encoding="utf-8-sig", errors="replace") as f:
vtt_content = f.read()
return gr.update(value=0.0), gr.update(value=session['audiobook']), gr.update(value=vtt_content)
else:
error = f"{Path(session['audiobook']).name} corrupted or not encoded!"
exception_alert(session_id, error)
except Exception as e:
error = f'_update_gr_audiobook_player(): {e}'
exception_alert(session_id, error)
return gr.update(value=0.0), gr.update(value=None), gr.update(value=None)
def _update_gr_glassmask(str:str=gr_glassmask_msg, attr:list=['gr-glass-mask'])->dict:
return gr.update(value=str, elem_id='gr_glassmask', elem_classes=attr)
def _build_voice_highlight_css(row_index:int|None)->str:
"""Emit a <style> block highlighting tr.file:nth-child(N+1) inside #gr_ebook_src, or '' to clear."""
if row_index is None:
return ''
return (
f'<style>#gr_ebook_src table.file-preview tbody > tr.file:nth-child({row_index + 1}) '
f'{{ background: var(--color-accent-soft) !important; '
f'box-shadow: inset 4px 0 0 var(--color-accent) !important; '
f'font-weight: 600; }}</style>'
)
def _voice_player_visible(session)->bool:
"""gr_row_voice_player hides only in DIRECTORY mode with no row currently selected."""
if not session:
return True
if session.get('ebook_mode') != ebook_modes['DIRECTORY']:
return True
return bool(session.get('ebook_selected'))
def _upload_gr_ebook_src(session_id:str, ebook_mode:str)->None:
if ebook_mode == ebook_modes['DIRECTORY']:
session = context.get_session(session_id)
if session and session.get('id', False):
session['ebook_selected'] = None
session['voice_map'] = {}
msg = 'Click on each file in the list to set its voice individually.'
show_alert(session_id, {
'type': 'info',
'msg': msg
})
def _change_gr_ebook_src(session_id:str, ebook_mode:str, data:any)->tuple:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
if (session.get('ebook_src') == data and ebook_mode == ebook_modes['SINGLE']) or (session.get('ebook_list') == data and ebook_mode == ebook_modes['DIRECTORY']):
return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(value='')
if ebook_mode == ebook_modes['SINGLE']:
session['ebook_src'] = data
elif ebook_mode == ebook_modes['DIRECTORY']:
session['ebook_list'] = data
files = data or []
abs_files = [os.path.abspath(f) for f in files] if isinstance(files, list) else []
prev_map = dict(session.get('voice_map') or {}) # read once
default_voice = session.get('voice')
new_map = {p: (prev_map[p] if p in prev_map else default_voice) for p in abs_files}
session['voice_map'] = new_map
prev_selected = session.get('ebook_selected')
if prev_selected and prev_selected in abs_files:
new_row = abs_files.index(prev_selected)
if data is None:
session['cancellation_requested'] = True
else:
session['cancellation_requested'] = False
return (
gr.update(),
gr.update(value=_build_voice_highlight_css(new_row)),
gr.update(),
gr.update(visible=True),
gr.update(value=Path(prev_selected).name, visible=True),
gr.update()
)
else:
session['ebook_selected'] = None
voice_update = gr.update(value=session.get('voice')) if prev_selected else gr.update()
if data is None and session.get('status', None) in [status_tags['EDIT'], status_tags['CONVERTING']]:
session['cancellation_requested'] = True
msg = 'Cancellation requested, please wait…'
return gr.update(value=_show_gr_modal('wait', msg), visible=True), gr.update(value=''), voice_update, gr.update(visible=False), gr.update(value='', visible=False), gr.update(value='')
session['cancellation_requested'] = False
return gr.update(), gr.update(value=''), voice_update, gr.update(visible=False), gr.update(value='', visible=False), gr.update(value='')
if data is None:
if session.get('status', None) in [status_tags['EDIT'], status_tags['CONVERTING']]:
session['cancellation_requested'] = True
msg = 'Cancellation requested, please wait…'
return gr.update(value=_show_gr_modal('wait', msg), visible=True), gr.update(value=''), gr.update(), gr.update(), gr.update(), gr.update(value='')
session['cancellation_requested'] = False
except Exception as e:
error = f'_change_gr_ebook_src(): {e}'
exception_alert(session_id, error)
return gr.update(), gr.update(value=''), gr.update(), gr.update(), gr.update(), gr.update()
def _select_gr_ebook_src(session_id:str, ebook_mode:str, ebook_src:list|None, evt:gr.SelectData)->tuple:
try:
session = context.get_session(session_id)
if not (session and session.get('id', False)):
return tuple(gr.update() for _ in range(4))
if ebook_mode != ebook_modes['DIRECTORY'] or evt.index is None:
return gr.update(), gr.update(value=''), gr.update(), gr.update(value='', visible=False)
if session.get('status') != status_tags['READY']:
return tuple(gr.update() for _ in range(4))
# evt.index can be int or (row, col) tuple — normalise.
row = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
live_list = ebook_src if isinstance(ebook_src, list) and ebook_src else None
ebook_list = live_list if live_list is not None else (session.get('ebook_list') or [])
if not isinstance(ebook_list, list) or row < 0 or row >= len(ebook_list):
return gr.update(), gr.update(value=''), gr.update(), gr.update(value='', visible=False)
abs_path = os.path.abspath(ebook_list[row])
session['ebook_selected'] = abs_path
if live_list is not None and session.get('ebook_list') != live_list:
session['ebook_list'] = live_list
voice_map = session.get('voice_map') or {}
assigned_voice = voice_map[abs_path] if abs_path in voice_map else session.get('voice')
style = _build_voice_highlight_css(row)
filename = Path(abs_path).name
return (
gr.update(value=assigned_voice, label='Voices'),
gr.update(value=style),
gr.update(visible=True),
gr.update(value=filename, visible=True),
)
except Exception as e:
error = f'_select_gr_ebook_src(): {e}'
exception_alert(session_id, error)
return tuple(gr.update() for _ in range(4))
def _change_gr_ebook_textarea(session_id:str, ebook_textarea:str)->None:
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get('ebook_textarea') != ebook_textarea:
session['ebook_textarea'] = ebook_textarea
return
def _change_gr_ebook_mode(session_id:str, val:str)->tuple:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get('ebook_mode') == val:
return tuple(gr.update() for _ in range(6))
session['ebook_mode'] = val
css_update = gr.update() if val == ebook_modes['DIRECTORY'] else gr.update(value='')
if val != ebook_modes['DIRECTORY']:
session['ebook_selected'] = None
row_visible = _voice_player_visible(session)
if val == ebook_modes['DIRECTORY'] and session.get('ebook_selected'):
filename_update = gr.update(value=Path(session['ebook_selected']).name, visible=True)
else:
filename_update = gr.update(value='', visible=False)
enabled_convert_btn = False
if val == ebook_modes['SINGLE']:
if session.get('ebook_src'):
enabled_convert_btn = True
return gr.update(visible=True, label='-', file_count=ebook_modes['SINGLE'], value=session['ebook_src']), gr.update(visible=False), gr.update(interactive=enabled_convert_btn), css_update, gr.update(visible=row_visible), filename_update
elif val == ebook_modes['DIRECTORY']:
if session.get('ebook_list'):
enabled_convert_btn = True
return gr.update(visible=True, label='-', file_count=ebook_modes['DIRECTORY'], value=session['ebook_list']), gr.update(visible=False), gr.update(interactive=enabled_convert_btn), css_update, gr.update(visible=row_visible), filename_update
elif val == ebook_modes['TEXT']:
return gr.update(visible=False), gr.update(visible=True, value=session['ebook_textarea']), gr.update(interactive=True), css_update, gr.update(visible=row_visible), filename_update
except Exception as e:
error = f'_change_gr_ebook_mode(): {e}'
exception_alert(session_id, error)
return tuple(gr.update() for _ in range(6))
def _change_gr_voice_file(session_id:str, f:str|None)->tuple:
try:
state = {}
if f is not None:
if len(voice_options) > max_custom_voices:
error = f'You are allowed to upload a max of {max_custom_voices} voices'
state['type'] = 'warning'
state['msg'] = error
elif os.path.splitext(f.name)[1] not in voice_formats:
error = f'The audio file format selected is not valid.'
state['type'] = 'warning'
state['msg'] = error
else:
session = context.get_session(session_id)
if session and session.get('id', False):
voice_name = os.path.splitext(os.path.basename(f))[0].replace('&', 'And')
voice_name = get_sanitized(voice_name)
final_voice_file = os.path.join(session['voice_dir'], f'{voice_name}.wav')
extractor = VoiceExtractor(session, f, voice_name)
status, msg = extractor.extract_voice()
if status:
session['voice'] = final_voice_file
msg = f'Voice {voice_name} added to the voices list'
state['type'] = 'success'
state['msg'] = msg
show_alert(session_id, state)
return _update_gr_voice_list(session_id)
else:
error = 'failed! Check if you audio file is compatible.'
state['type'] = 'warning'
state['msg'] = error
show_alert(session_id, state)
except Exception as e:
error = f'_change_gr_voice_file(): {e}'
exception_alert(session_id, error)
return gr.update()
def _change_gr_voice_list(session_id:str, selected:str|None)->tuple:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
if not voice_options or selected is None:
new_voice = None
else:
voice_value = voice_options[0][1]
new_voice = next(
(value for label, value in voice_options if value == selected),
voice_value,
)
if session.get('ebook_mode') == ebook_modes['DIRECTORY'] and session.get('ebook_selected'):
voice_map = dict(session.get('voice_map') or {})
voice_map[session['ebook_selected']] = new_voice
session['voice_map'] = voice_map
else:
session['voice'] = new_voice
visible_voice_buttons = new_voice is not None
return gr.update(value=new_voice), gr.update(visible=visible_voice_buttons), gr.update(visible=visible_voice_buttons)
except Exception as e:
error = f'_change_gr_voice_list(): {e}'
exception_alert(session_id, error)
return tuple(gr.update() for _ in range(3))
def _click_gr_voice_del_btn(session_id:str, selected:str)->tuple:
try:
if selected is not None:
session = context.get_session(session_id)
if session and session.get('id', False):
speaker_path = os.path.abspath(selected)
speaker = re.sub(r'\.wav$|\.npz|\.pth$', '', os.path.basename(selected))
builtin_root = os.path.join(voices_dir, session['language'])
is_in_builtin = os.path.commonpath([speaker_path, os.path.abspath(builtin_root)]) == os.path.abspath(builtin_root)
is_in_models = os.path.commonpath([speaker_path, os.path.abspath(session['custom_model_dir'])]) == os.path.abspath(session['custom_model_dir'])
is_builtin = any(
speaker in settings.get('voices', {})
for settings in (default_engine_settings[engine] for engine in TTS_ENGINES.values())
)
if is_builtin and is_in_builtin:
error = f'Voice file {speaker} is a builtin voice and cannot be deleted.'
show_alert(session_id, {"type": "warning", "msg": error})
return gr.update(visible=False), gr.update()
if is_in_models:
error = f'Voice file {speaker} is a voice of one of your custom model and cannot be deleted.'
show_alert(session_id, {"type": "warning", "msg": error})
return gr.update(visible=False), gr.update()
try:
selected_path = Path(selected).resolve()
parent_path = Path(session['voice_dir']).parent.resolve()
if parent_path in selected_path.parents:
session['status'] = status_tags['DELETION']
msg = f'Are you sure to delete {speaker}?'
return (
gr.update(value=_show_gr_modal(session['status'], msg), visible=True),
gr.update(value='confirm_voice_del')
)
else:
error = f'{speaker} is part of the global voices directory. Only your own custom uploaded voices can be deleted!'
show_alert(session_id, {"type": "warning", "msg": error})
return gr.update(visible=False), gr.update()
except Exception as e:
error = f'Could not delete the voice file {selected}!\n{e}'
exception_alert(session_id, error)
return gr.update(visible=False), gr.update()
return gr.update(visible=False), gr.update()
except Exception as e:
error = f'_click_gr_voice_del_btn(): {e}'
exception_alert(session_id, error)
return gr.update(visible=False), gr.update()
def _click_gr_custom_model_del_btn(session_id:str, selected:str)->tuple:
try:
if selected is not None:
session = context.get_session(session_id)
if session and session.get('id', False):
selected_name = os.path.basename(selected)
session['status'] = status_tags['DELETION']
msg = f'Are you sure to delete {selected_name}?'
return gr.update(value=_show_gr_modal(session['status'], msg), visible=True), gr.update(value='confirm_custom_model_del')
except Exception as e:
error = f'Could not delete the custom model {selected_name}!'
exception_alert(session_id, error)
return gr.update(visible=False), gr.update()
def _click_gr_audiobook_del_btn(session_id:str, selected:str)->tuple:
try:
if selected is not None:
session = context.get_session(session_id)
if session and session.get('id', False):
selected_name = Path(selected).stem
session['status'] = status_tags['DELETION']
msg = f'Are you sure to delete {selected_name}?'
return gr.update(value=_show_gr_modal(session['status'], msg), visible=True), gr.update(value='confirm_audiobook_del')
except Exception as e:
error = f'Could not delete the audiobook {selected_name}!'
exception_alert(session_id, error)
return gr.update(visible=False), gr.update()
def _click_gr_deletion(session_id:str, voice_path:str, custom_model:str, audiobook:str, method:str|None=None)->tuple:
try:
nonlocal models, voice_options
if method is not None:
session = context.get_session(session_id)
if session and session.get('id', False):
if session['status'] == status_tags['DELETION']:
session['status'] = status_tags['READY']
models = load_engine_presets(session['tts_engine'])
if method == 'confirm_voice_del':
selected_name = Path(voice_path).stem
pattern = re.sub(r'\.wav$', '*.wav', voice_path)
files2remove = glob(pattern)
for file in files2remove:
try:
os.remove(file)
except FileNotFoundError:
pass
shutil.rmtree(os.path.join(os.path.dirname(voice_path), 'bark', selected_name), ignore_errors=True)
deleted_voice = session['voice']
fallback = None if session['tts_engine'] in tts_engines_with_inner_speaker else default_engine_settings[session['tts_engine']]['voice']
blocks_current = session.get('blocks_current') or {}
changed = False
for block in blocks_current.get('blocks', []):
if block.get('voice') == deleted_voice:
block['voice'] = fallback
changed = True
if blocks_current.get('voice') == deleted_voice:
blocks_current['voice'] = fallback
changed = True
if changed:
session['blocks_current'] = blocks_current
save_db_blocks(session_id)
session['voice'] = fallback
voice_options[:] = [(i, v) for i, v in voice_options if v != deleted_voice]
msg = f'Voice file {re.sub(r".wav$", "", selected_name)} deleted!'
show_alert(session_id, {'type': 'info', 'msg': msg})
return gr.update(value='', visible=False), gr.update(), gr.update(), _update_gr_voice_list(session_id)
elif method == 'confirm_custom_model_del':
selected_name = os.path.basename(custom_model)
shutil.rmtree(custom_model, ignore_errors=True)
msg = f'Custom model {selected_name} deleted!'
if session['custom_model'] is not None and session['voice'] is not None:
if session['custom_model'] in session['voice']:
session['voice'] = models[session['fine_tuned']]['voice']
session['custom_model'] = None
show_alert(session_id, {"type": "info", "msg": msg})
return gr.update(value='', visible=False), _update_gr_custom_model_list(session_id), gr.update(), gr.update()
elif method == 'confirm_audiobook_del':
selected_name = Path(audiobook).stem
base_selected_name = re.sub(r'_part\d+$', '', selected_name)
count_files = sum(1 for f, _ in audiobook_options if re.sub(r'_part\d+$', '', Path(f).stem) == base_selected_name)
if os.path.isdir(audiobook):
shutil.rmtree(audiobook, ignore_errors=True)
else:
try:
os.remove(audiobook)
except FileNotFoundError:
pass
if count_files <= 1:
vtt_path = Path(audiobook).with_suffix('.vtt')
if os.path.exists(vtt_path):
os.remove(vtt_path)
language = session['translate'] if session['translate_enabled'] and session['translate'] is not None else session['language']
process_dir = os.path.join(session['session_dir'], hashlib.md5((selected_name).encode()).hexdigest())
shutil.rmtree(process_dir, ignore_errors=True)
msg = f'Audiobook {selected_name} deleted!'
session['audiobook'] = None
show_alert(session_id, {"type": "info", "msg": msg})
return gr.update(value='', visible=False), gr.update(), _update_gr_audiobook_list(session_id), gr.update()
except Exception as e:
error = f'_click_gr_deletion(): {e}!'
exception_alert(session_id, error)
return gr.update(value='', visible=False), gr.update(), gr.update(), gr.update()
def _update_gr_voice_list(session_id:str)->dict:
try:
nonlocal models, voice_options
session = context.get_session(session_id)
if session and session.get('id', False):
models = load_engine_presets(session['tts_engine'])
language = session['translate'] if session['translate_enabled'] and session['translate'] is not None else session['language']
lang_dir = language if language != 'con' else 'con-' # Bypass Windows CON reserved name
file_pattern = "*.wav"
eng_options = []
bark_options = []
piper_options = []
builtin_dir = Path(os.path.join(voices_dir, lang_dir))
builtin_options = [
(base, str(f))
for f in builtin_dir.rglob(file_pattern)
for base in [os.path.splitext(f.name)[0]]
]
builtin_names = {t[0]: None for t in builtin_options}
if language in default_engine_settings[TTS_ENGINES['XTTS']].get('languages', {}):
eng_dir = Path(os.path.join(voices_dir, "eng"))
eng_options = [
(base, str(f))
for f in eng_dir.rglob(file_pattern)
for base in [os.path.splitext(f.name)[0]]
if base not in builtin_names
]
if session['tts_engine'] == TTS_ENGINES['BARK']:
lang_dict = Lang(language)
if lang_dict:
lang_iso1 = lang_dict.pt1
lang = lang_iso1.lower()
speakers_path = Path(default_engine_settings[TTS_ENGINES['BARK']]['speakers_path'])
pattern_speaker = re.compile(r"^.*?_speaker_(\d+)$")
bark_options = [
(pattern_speaker.sub(r"Speaker \1", f.stem), str(f.with_suffix(".wav")))
for f in speakers_path.rglob(f"{lang}_speaker_*.npz")
]
elif session['tts_engine'] == TTS_ENGINES['PIPER']:
engine_config = default_engine_settings[TTS_ENGINES['PIPER']]
lang_piper = engine_config['languages'][language] # e.g., "en_GB"
speakers_path = Path(engine_config['speakers_path'])
voices_map = engine_config['voices']
for f in speakers_path.iterdir():
if f.name.startswith(lang_piper):
file_stem = f.stem
display_name = f'Speaker {voices_map.get(file_stem, file_stem)}'
wav_path = str(f.with_suffix('.wav'))
piper_options.append((display_name, wav_path))
voice_options = builtin_options + eng_options + bark_options + piper_options
session['voice_dir'] = os.path.join(voices_dir, '__sessions', f'voice-{session_id}', language)
os.makedirs(session['voice_dir'], exist_ok=True)
if session['voice_dir'] is not None:
session_voice_dir = Path(session['voice_dir'])
voice_options += [
(os.path.splitext(f.name)[0], str(f))
for f in session_voice_dir.rglob(file_pattern)
if f.is_file()
]
if session.get('custom_model_dir'):
voice_options.extend(
(f.stem, str(f))
for f in Path(session['custom_model_dir']).rglob('*.wav')
if f.is_file()
)
if session['tts_engine'] in tts_engines_with_inner_speaker:
voice_options = [('Default', None)] + sorted(voice_options, key=lambda x: x[0].lower())
else:
voice_options = sorted(voice_options, key=lambda x: x[0].lower())
if session['voice'] is not None and isinstance(session.get('voice'), str):
if session['voice_dir'] not in session['voice']:
if not any(v[1] == session['voice'] for v in voice_options):
voice_path = Path(session['voice'])
parts = list(voice_path.parts)
if "voices" in parts:
idx = parts.index("voices")
if idx + 1 < len(parts):
parts[idx + 1] = language
new_voice_path = str(Path(*parts))
if os.path.exists(new_voice_path) and any(v[1] == new_voice_path for v in voice_options):
session['voice'] = new_voice_path
else:
parts[idx + 1] = 'eng'
new_voice_path = str(Path(*parts))
if os.path.exists(new_voice_path) and any(v[1] == new_voice_path for v in voice_options):
session['voice'] = new_voice_path
else:
session['voice'] = voice_options[0][1]
else:
if voice_options and voice_options[0][1] is not None:
new_voice_path = models[session['fine_tuned']]['voice']
if os.path.exists(new_voice_path) and any(v[1] == new_voice_path for v in voice_options):
session['voice'] = new_voice_path
else:
session['voice'] = voice_options[0][1]
return gr.update(choices=voice_options, value=session['voice'])
except Exception as e:
error = f'_update_gr_voice_list(): {e}!'
exception_alert(session_id, error)
return gr.update()
def _update_gr_translate_list(session_id:str)->dict:
session = context.get_session(session_id)
if not session or not session.get('id', False):
return gr.update()
lang = session.get('language')
translate = session.get('translate')
translate_iso1 = session['translate_iso1']
translate_options = _build_translate_targets(lang)
if translate_options:
lang_order = {val: i for i, (_, val) in enumerate(language_options)}
translate_options.sort(key=lambda item: lang_order.get(item[1], len(language_options)))
if not translate or not any(translate == val for _, val in translate_options):
translate = translate_options[0][1]
try:
translate_iso1 = Lang(translate).pt1
except Exception:
translate_iso1 = None
else:
msg = 'No translate languages available'
translate = None
translate_iso1 = None
translate_options.append((msg, None))
session['translate'] = translate
session['translate_iso1'] = translate_iso1
visible_gr_translate = True if session.get('translate_enabled') else False
return gr.update(visible=visible_gr_translate, choices=translate_options, value=translate)
def _update_gr_tts_engine_list(session_id:str)->dict:
try:
nonlocal tts_engine_options
session = context.get_session(session_id)
if session and session.get('id', False):
language = session['language']
if session.get('translate_enabled') and session.get('translate'):
language = session['translate']
tts_engine_options = get_compatible_tts_engines(language)
if session['tts_engine'] not in tts_engine_options:
session['tts_engine'] = tts_engine_options[0]
return gr.update(choices=tts_engine_options, value=session['tts_engine'])
except Exception as e:
error = f'_update_gr_tts_engine_list(): {e}!'
exception_alert(session_id, error)
return gr.update()
def _update_gr_custom_model_list(session_id:str)->dict:
try:
nonlocal custom_model_options
session = context.get_session(session_id)
if session and session.get('id', False):
custom_model_tts_dir = _check_custom_model_tts(session['custom_model_dir'], session['tts_engine'])
custom_model_options = [('None', None)] + [
(
str(dir),
os.path.join(custom_model_tts_dir, dir)
)
for dir in os.listdir(custom_model_tts_dir)
if os.path.isdir(os.path.join(custom_model_tts_dir, dir))
]
session['custom_model'] = session['custom_model'] if session['custom_model'] in [option[1] for option in custom_model_options] else custom_model_options[0][1]
model_paths = {v[1] for v in custom_model_options}
return gr.update(choices=custom_model_options, value=session['custom_model'])
except Exception as e:
error = f'_update_gr_custom_model_list(): {e}!'
exception_alert(session_id, error)
return gr.update()
def _update_gr_fine_tuned_list(session_id:str)->dict:
try:
nonlocal fine_tuned_options
nonlocal models
session = context.get_session(session_id)
if session and session.get('id', False):
models = load_engine_presets(session['tts_engine'])
fine_tuned_options = [
name
for name, details in models.items()
if details.get("lang") in ("multi", session['language'])
]
if session['fine_tuned'] in fine_tuned_options:
fine_tuned = session['fine_tuned']
else:
fine_tuned = default_fine_tuned
session['fine_tuned'] = fine_tuned
return gr.update(choices=fine_tuned_options, value=session['fine_tuned'])
except Exception as e:
error = f'_update_gr_fine_tuned_list(): {e}!'
exception_alert(session_id, error)
return gr.update()
def _change_gr_device(session_id:str, selected:str)->None:
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get('device') != selected:
session['device'] = selected
return
def _change_gr_language(session_id:str, selected:str)->tuple:
session = context.get_session(session_id)
if not session or not session.get('id', False):
return tuple(gr.update() for _ in range(4))
if session.get('language') != selected:
session['language'] = selected
return (
_update_gr_translate_list(session_id),
_update_gr_tts_engine_list(session_id),
_update_gr_custom_model_list(session_id),
_update_gr_fine_tuned_list(session_id)
)
return tuple(gr.update() for _ in range(4))
def _click_gr_translate_enabled(session_id:str, checked:bool)->tuple:
session = context.get_session(session_id)
if not session or not session.get('id', False):
return tuple(gr.update() for _ in range(5))
if session['translate_enabled'] != checked:
session['translate_enabled'] = checked
return (
_update_gr_translate_list(session_id),
_update_gr_tts_engine_list(session_id),
_update_gr_custom_model_list(session_id),
_update_gr_fine_tuned_list(session_id),
_update_gr_voice_list(session_id)
)
return tuple(gr.update() for _ in range(5))
def _change_gr_translate(session_id:str, translate:str)->tuple:
session = context.get_session(session_id)
if not session or not session.get('id', False):
return tuple(gr.update() for _ in range(3))
if translate != session['translate']:
session['translate'] = translate
try:
session['translate_iso1'] = Lang(translate).pt1
except Exception:
session['translate_iso1'] = None
return (
_update_gr_tts_engine_list(session_id),
_update_gr_custom_model_list(session_id),
_update_gr_fine_tuned_list(session_id)
)
return tuple(gr.update() for _ in range(3))
def _check_custom_model_tts(custom_model_dir:str, tts_engine:str)->str|None:
dir_path = None
if custom_model_dir is not None and tts_engine is not None:
dir_path = os.path.join(custom_model_dir, tts_engine)
if not os.path.isdir(dir_path):
os.makedirs(dir_path, exist_ok=True)
return dir_path
def _change_gr_custom_model_file(session_id:str, custom_file:str|None, tts_engine:str)->tuple:
try:
nonlocal models
if custom_file is not None:
state = {}
if len(custom_model_options) > max_custom_model:
error = f'You are allowed to upload a max of {max_custom_model} models'
state['type'] = 'warning'
state['msg'] = error
else:
session = context.get_session(session_id)
if session and session.get('id', False):
models = load_engine_presets(session['tts_engine'])
session['tts_engine'] = tts_engine
if analyze_uploaded_file(custom_file, models['internal']['files']):
session['custom_model'] = custom_file
model = extract_custom_model(session_id)
if model is not None:
session['custom_model'] = model
if session['tts_engine'] not in tts_engines_with_inner_speaker:
session['voice'] = os.path.join(model, f'{os.path.basename(os.path.normpath(model))}.wav')
msg = f'{os.path.basename(model)} added to the custom models list'
state['type'] = 'success'
state['msg'] = msg
show_alert(session_id, state)
return gr.update(value=None), _update_gr_custom_model_list(session_id)
else:
error = f'Cannot extract custom model zip file {os.path.basename(custom_file)}'
state['type'] = 'warning'
state['msg'] = error
else:
error = f'{os.path.basename(custom_file)} is not a valid model or some required files are missing'
state['type'] = 'warning'
state['msg'] = error
show_alert(session_id, state)
except Exception as e:
error = f'_change_gr_custom_model_file(): {e}'
exception_alert(session_id, error)
return gr.update(value=None), gr.update()
def _change_gr_custom_model_list(session_id:str, selected:str|None)->tuple:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
session['custom_model'] = selected
if selected is not None and session['tts_engine'] not in tts_engines_with_inner_speaker:
session['voice'] = os.path.join(selected, f'{os.path.basename(selected)}.wav')
visible_fine_tuned = True if session['custom_model'] is None else False
visible_del_btn = True if session['custom_model'] is not None else False
return gr.update(visible=visible_fine_tuned), _update_gr_voice_list(session_id), gr.update(visible=visible_del_btn)
except Exception as e:
error = f'_change_gr_custom_model_list(): {e}'
exception_alert(session_id, error)
return tuple(gr.update() for _ in range(3))
def _change_gr_tts_engine_list(session_id:str, engine:str)->tuple:
try:
nonlocal models
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get('tts_engine') != engine or session.get('translate_enabled'):
models = load_engine_presets(engine)
session['voice'] = None if session['voice'] == default_engine_settings[session['tts_engine']]['voice'] else session['voice']
session['tts_engine'] = engine
session['fine_tuned'] = default_fine_tuned
visible_xtts = visible_gr_tab_xtts_params if session['tts_engine'] == TTS_ENGINES['XTTS'] else False
visible_bark = visible_gr_tab_bark_params if session['tts_engine'] == TTS_ENGINES['BARK'] else False
supports_custom = session['tts_engine'] in tts_engines_with_custom_model
visible_custom_model = supports_custom and session['fine_tuned'] == 'internal'
if supports_custom:
file_label = f"Upload a {session['tts_engine'].upper()} ZIP file (Required: {', '.join(models[default_fine_tuned]['files'])})"
custom_model_list_update = _update_gr_custom_model_list(session_id)
else:
file_label = f"*Upload Custom Model not available for {session['tts_engine']}"
custom_model_list_update = gr.update()
return (
gr.update(value=_show_rating(session['tts_engine'])),
gr.update(visible=visible_xtts),
gr.update(visible=visible_bark),
gr.update(visible=visible_custom_model),
_update_gr_fine_tuned_list(session_id),
gr.update(label=file_label),
custom_model_list_update
)
except Exception as e:
error = f'_change_gr_tts_engine_list(): {e}'
exception_alert(session_id, error)
return tuple(gr.update() for _ in range(7))
def _change_gr_fine_tuned_list(session_id:str, selected:str)->dict:
try:
if selected:
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get('fine_tuned') != selected:
session['fine_tuned'] = selected
if selected == 'internal':
visible_custom_model = visible_gr_group_custom_model if session['fine_tuned'] == 'internal' and session['tts_engine'] in tts_engines_with_custom_model else False
else:
visible_custom_model = False
session['voice'] = models[session['fine_tuned']]['voice']
return gr.update(visible=visible_custom_model)
except Exception as e:
error = f'_change_gr_fine_tuned_list(): {e}'
exception_alert(session_id, error)
return gr.update()
def _change_gr_output_format_list(session_id:str, val:str)->None:
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get('output_format') != val:
session['output_format'] = val
return
def _change_gr_output_channel_list(session_id:str, val:str)->None:
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get('output_channel') != val:
session['output_channel'] = val
return
def _change_gr_output_split(session_id:str, val:str)->dict:
session = context.get_session(session_id)
if session and session.get('id', False):
session['output_split'] = val
return gr.update(visible=val)
def _click_gr_session_switch_btn(session_id:str, backup_session_id:str|None)->tuple:
try:
if backup_session_id is not None:
back_id = backup_session_id
new_id = session_id
else:
back_id = session_id
new_id = None
session = context.get_session(back_id)
if session and session.get('id', False):
if session['status'] == status_tags['READY']:
session['status'] = status_tags['SWITCH']
msg = 'Backup your current session ID before to start with a new one!'
show_alert(back_id, {"type": "warning", "msg": msg})
return gr.update(), gr.update(interactive=True), back_id, gr.update(value='🔑︎'), back_id, None
elif session['status'] == status_tags['SWITCH']:
if new_id is None or not new_id.strip():
msg = 'Session ID cannot be empty'
show_alert(back_id, {"type": "warning", "msg": msg})
return gr.update(), gr.update(), backup_session_id, gr.update(), None, None
new_session_id = new_id.strip()
session['status'] = status_tags['READY']
if new_session_id == back_id:
return gr.update(), gr.update(interactive=False), back_id, gr.update(value='🔒︎'), None, back_id
new_session_dir = os.path.join(tmp_dir, f'proc-{new_session_id}')
new_session = context.get_session(new_session_id)
if os.path.exists(new_session_dir) or new_session:
if not new_session:
new_session = context.set_session(new_session_id)
new_session['status'] = status_tags['READY']
return gr.update(value=json.dumps(new_session, cls=JSONDictProxyEncoder)), gr.update(interactive=False), None, gr.update(value='🔒︎'), None, new_session_id
else:
session['status'] = status_tags['SWITCH']
msg = 'Session not found!'
show_alert(back_id, {"type": "warning", "msg": msg})
except Exception as e:
error = f'_click_gr_session_switch_btn(): {e}'
exception_alert(back_id, error)
return gr.update(), gr.update(), backup_session_id, gr.update(), None, None
def _change_gr_playback_time(session_id:str, time:float)->None:
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get('playback_time') != time:
session['playback_time'] = time
return
def _toggle_audiobook_files(session_id:str, audiobook:str, is_visible:bool, refresh_only:bool=False)->tuple:
try:
if not audiobook:
error = 'No audiobook selected.'
show_alert(session_id, {"type": "error", "msg": error})
return gr.update(), False
if is_visible and not refresh_only:
return gr.update(visible=False, value=None), False
file = Path(audiobook)
if not file.exists():
error = f'Audio not found: {file}'
show_alert(session_id, {"type": "error", "msg": error})
return gr.update(visible=False, value=None), False
files = [str(file)]
vtt = file.with_suffix('.vtt')
if vtt.exists():
files.append(str(vtt))
return gr.update(visible=True, value=files), True
except Exception as e:
error = f'_toggle_audiobook_files(): {e}!'
exception_alert(session_id, error)
return gr.update(), False
def _change_param(key:str, session_id:str, val:Any, val2:Any=None)->None:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
if session.get(key) != val:
session[key] = val
state = {}
if key == 'xtts_length_penalty':
if val2 is not None:
if float(val) > float(val2):
error = 'Length penalty must be always lower than num beams if greater than 1.0 or equal if 1.0'
state['type'] = 'warning'
state['msg'] = error
show_alert(session_id, state)
elif key == 'xtts_num_beams':
if val2 is not None:
if float(val) < float(val2):
error = 'Num beams must be always higher than length penalty or equal if its value is 1.0'
state['type'] = 'warning'
state['msg'] = error
show_alert(session_id, state)
except Exception as e:
error = f'_change_param(): {e}'
exception_alert(session_id, error)
return
def _start_conversion(
session_id:str, device:str, ebook_mode:str, ebook_src:str|list|None, ebook_textarea:str|None, blocks_preview:bool, tts_engine:str, language:str, voice:str, custom_model:str, fine_tuned:str, output_format:str, output_channel:str, xtts_temperature:float,
xtts_length_penalty:int, xtts_num_beams:int, xtts_repetition_penalty:float, xtts_top_k:int, xtts_top_p:float, xtts_speed:float, xtts_enable_text_splitting:bool, bark_text_temp:float, bark_waveform_temp:float,
output_split:bool, output_split_hours:str,
translate_enabled:bool, translate_target:str|None
)->tuple:
error = None
try:
session = context.get_session(session_id)
reset_ebook_session(session_id, force=True, filter_keys=False)
if session and session.get('id', False):
if not session['cancellation_requested']:
args = {
"id": session_id,
"is_gui_process": session['is_gui_process'],
"script_mode": script_mode,
"blocks_preview": blocks_preview,
"device": device,
"tts_engine": tts_engine,
"ebook": None,
"ebook_mode": ebook_mode,
"ebook_src": ebook_src if ebook_mode == ebook_modes['SINGLE'] else session['ebook_src'],
"ebook_list": ebook_src if ebook_mode == ebook_modes['DIRECTORY'] else session['ebook_list'],
"ebook_textarea": ebook_textarea if ebook_mode == ebook_modes['TEXT'] else session['ebook_textarea'],
"voice": voice,
"language": language,
"custom_model": custom_model,
"fine_tuned": fine_tuned,
"output_format": output_format,
"output_channel": output_channel,
"xtts_temperature": float(xtts_temperature),
"xtts_length_penalty": float(xtts_length_penalty),
"xtts_num_beams":int(session['xtts_num_beams']),
"xtts_repetition_penalty": float(xtts_repetition_penalty),
"xtts_top_k":int(xtts_top_k),
"xtts_top_p": float(xtts_top_p),
"xtts_speed": float(xtts_speed),
"xtts_enable_text_splitting":bool(xtts_enable_text_splitting),
"bark_text_temp": float(bark_text_temp),
"bark_waveform_temp": float(bark_waveform_temp),
"output_split":bool(output_split),
"output_split_hours": output_split_hours,
"translate_enabled": bool(translate_enabled),
"translate": translate_target if translate_enabled else None,
"translate_iso1": (Lang(translate_target).pt1 if (translate_enabled and translate_target) else None)
}
if args['ebook_mode'] == ebook_modes['DIRECTORY']:
if isinstance(args['ebook_list'], list):
if not args['ebook_list']:
error = 'A directory with ebook files is required.'
elif args['ebook_mode'] == ebook_modes['SINGLE']:
if not args['ebook_src']:
error = 'An ebook file is required.'
elif args['ebook_mode'] == ebook_modes['TEXT']:
if not args['ebook_textarea']:
error = 'Textarea is empty.'
elif len(args['ebook_textarea']) < 10:
error = 'Textarea must be > 10 chars.'
else:
args['ebook_textarea'] = args['ebook_textarea'].strip()
if len(args['ebook_textarea']) < 10:
error = 'Textarea must be > 10 chars.'
#elif args['xtts_num_beams'] < args['xtts_length_penalty']:
# error = 'num beams must be greater or equal than length penalty.'
if error is None:
session['ticker'] = len(audiobook_options)
if args['ebook_mode'] == ebook_modes['DIRECTORY']:
if args['ebook_list']:
if isinstance(args['ebook_list'], list):
default_voice = session.get('voice')
voice_map = dict(session.get('voice_map') or {})
clean_list = sorted([
f for f in args['ebook_list']
if any(f.endswith(ext) for ext in ebook_formats)
])
for skipped in [f for f in args['ebook_list'] if f not in clean_list]:
show_alert(session_id, {
"type": "warning",
"msg": f'{Path(skipped).name} has not a supported format! skipping'
})
ebook_list_full = copy.deepcopy(clean_list)
args['ebook_list'] = ebook_list_full
queue = list(ebook_list_full)
while queue:
file = queue.pop(0)
args['ebook_src'] = file
abs_file = os.path.abspath(file)
if abs_file in voice_map:
override = voice_map[abs_file]
elif os.path.basename(file) in voice_map:
override = voice_map[os.path.basename(file)]
else:
override = default_voice
if override is not None and not os.path.exists(override):
msg = f'Voice override for {Path(file).name} not found, using default.'
show_alert(session_id, {
"type": "warning",
"msg": msg
})
override = default_voice
args['voice'] = override
progress_status, passed = convert_ebook(args)
if passed:
return gr.update(value=progress_status)
else:
error = progress_status
break
elif args['ebook_mode'] == ebook_modes['SINGLE']:
progress_status, passed = convert_ebook(args)
if passed:
return gr.update(value=progress_status)
else:
error = progress_status
elif args['ebook_mode'] == ebook_modes['TEXT']:
progress_status, passed = convert_ebook(args)
if passed:
return gr.update(value=progress_status)
else:
error = progress_status
if error is not None:
show_alert(session_id, {"type": "warning", "msg": error})
if session['cancellation_requested'] and session['status'] == status_tags['DISCONNECTED']:
context_tracker.end_session(session_id, session['socket_hash'])
return gr.update()
session['status'] = status_tags['END']
return gr.update(value=error)
else:
return gr.update()
except Exception as e:
session['status'] = status_tags['END']
error = f'_start_conversion(): {e}'
exception_alert(session_id, error)
return gr.update(value=error)
def _update_gr_audiobook_list(session_id:str)->dict:
try:
nonlocal audiobook_options
session = context.get_session(session_id)
if session and session.get('id', False):
if session['audiobooks_dir'] is not None:
audiobook_options = [
(f, os.path.join(session['audiobooks_dir'], str(f)))
for f in os.listdir(session['audiobooks_dir'])
if not f.lower().endswith(".vtt")
]
if len(audiobook_options) > 0:
audiobook_options.sort(
key=lambda x: os.path.getmtime(x[1]),
reverse=True
)
session['audiobook'] = (
session['audiobook']
if session['audiobook'] in [option[1] for option in audiobook_options]
else None
)
if session['audiobook'] is None:
session['audiobook'] = audiobook_options[0][1]
else:
session['audiobook'] = None
return gr.update(choices=audiobook_options, value=session['audiobook'])
except Exception as e:
error = f'_update_gr_audiobook_list(): {e}!'
exception_alert(session_id, error)
return gr.update()
def _check_override_ebook(session_id:str, ebook_mode:str, ebook_data:any, ebook_textarea:str, blocks_preview:bool, event:int, translate_enabled:bool, translate_target:str|None)->tuple:
try:
session = context.get_session(session_id)
source = None
error = None
if session and session.get('id', False):
if not session['cancellation_requested']:
if not session['status'] in [status_tags['SKIP'], status_tags['END']]:
if session['status'] in [status_tags['EDIT']]:
return gr.update(), event
elif session['status'] in [status_tags['OVERRIDE'], status_tags['CONVERTING']]:
if ebook_mode == ebook_modes['DIRECTORY']:
if isinstance(session['ebook_list'], list):
if len(session['ebook_list']) > 0:
source = session['ebook_list'][0]
elif ebook_mode == ebook_modes['SINGLE']:
source = session['ebook_src']
else:
if ebook_mode == ebook_modes['DIRECTORY']:
if not ebook_data:
error = 'A directory with ebook files is required.'
else:
source = ebook_data[0]
elif ebook_mode == ebook_modes['SINGLE']:
if not ebook_data:
error = 'An ebook file is required.'
else:
source = ebook_data
elif ebook_mode == ebook_modes['TEXT']:
if not ebook_textarea:
error = 'Textarea is empty.'
elif len(ebook_textarea) < 10:
error = 'Textarea must be > 10 chars.'
else:
ebook_textarea = ebook_textarea.strip()
if len(ebook_textarea) < 10:
error = 'Textarea must be > 10 chars.'
else:
source = ebook_textarea
if error is None:
if source is not None:
if ebook_mode == ebook_modes['TEXT']:
session['status'] = status_tags['SKIP']
session['ebook_textarea'] = source
return gr.update(), (event + 1)
else:
session['ebook_src'] = source
stem_base = get_sanitized(Path(source).stem)
if translate_enabled and translate_target and translate_target != session.get('language'):
language = translate_target
ebook_name = f"{stem_base}_{translate_target}"
else:
language = session.get('language')
ebook_name = stem_base
final_name = f"{ebook_name}.{session['output_format']}"
process_dir = os.path.join(session['session_dir'], hashlib.md5((ebook_name).encode()).hexdigest())
chapters_dir = os.path.join(process_dir, 'chapters')
sentences_dir = os.path.join(chapters_dir, 'sentences')
pre_name = f"{ebook_name}{'_part1.' if session['output_split'] else '.'}{default_audio_proc_format}"
pre_file = os.path.join(process_dir, pre_name)
final_file = os.path.join(session['audiobooks_dir'], final_name)
audio_sentences_exist = False
if os.path.exists(sentences_dir):
audio_sentences_exist = any(Path(sentences_dir).rglob(f'*.{default_audio_proc_format}'))
if os.path.exists(pre_file) or audio_sentences_exist:
session['status'] = status_tags['OVERRIDE']
session['audiobook_overridden'] = final_file
msg = f"Warning! audio sentences or final file {final_name} of this conversion already exists. If you continue resume will restart from the last sentence converted!"
return gr.update(value=_show_gr_modal(session['status'], msg), visible=True), event
else:
session['status'] = status_tags['SKIP']
return gr.update(), (event + 1)
else:
show_alert(session_id, {"type": "warning", "msg": error})
session['status'] = status_tags['END']
except Exception as e:
error = f'_check_override_ebook(): {e}'
exception_alert(session_id, error)
return gr.update(), event
def _click_gr_override_cancel_btn(session_id:str)->dict:
session = context.get_session(session_id)
if session and session.get('id', False):
session['status'] = status_tags['END']
session['audiobook_overridden'] = None
return gr.update(value='', visible=False)
def _click_gr_override_confirm_btn(session_id:str, event:int, audiobook_files_state:bool)->tuple:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
file_converting = session['audiobook_overridden']
files_update = gr.update()
files_state_update = gr.update()
if file_converting:
idx = next((i for i, t in enumerate(audiobook_options) if t[1] == file_converting), -1)
new_list = [t for t in audiobook_options if t[1] != file_converting]
if session['audiobook'] == file_converting or session['audiobook'] in new_list:
print('_click_gr_override_confirm_btn(): select audiobook is the converting file')
new_selected = None
if new_list:
new_idx = max(0, idx - 1)
new_selected = new_list[new_idx][1]
session['audiobook'] = new_selected
if audiobook_files_state and new_selected is not None:
files_update, files_state_update = _toggle_audiobook_files(session_id, new_selected, False)
else:
files_update = gr.update(visible=False, value=None)
files_state_update = False
return gr.update(value='', visible=False), (event + 1), gr.update(choices=new_list, value=new_selected), files_update, files_state_update
return gr.update(value='', visible=False), (event + 1), gr.update(), files_update, files_state_update
except Exception as e:
error = f'_click_gr_override_confirm_btn(): {e}'
exception_alert(session_id, error)
return gr.update(), event, gr.update(), gr.update(), gr.update()
def _populate_page(session_id:str, page:int, blocks:list[dict])->tuple:
session = context.get_session(session_id)
if session and session.get('id', False):
if session['status'] in [status_tags['EDIT']]:
start = int(page) * page_size
updates = []
expands = []
for i in range(page_size):
idx = start + i
if idx < len(blocks):
b = blocks[idx]
exp = b.get('expand', False)
expands.append(exp)
updates.append(gr.update(label=f'Block {idx}', visible=True, open=exp))
updates.append(gr.update(value=b['keep']))
updates.append(gr.update(value=b.get('voice'), choices=voice_options))
updates.append(gr.update(value=b['text']))
else:
expands.append(False)
updates.append(gr.update(visible=False))
updates.append(gr.update())
updates.append(gr.update())
updates.append(gr.update())
end = min(start + page_size, len(blocks))
header = gr.update(value=f'Blocks {start}{end-1} of {len(blocks)-1}')
return (*updates, header, expands)
def _navigate(session_id:str, page:int, blocks:list[dict], direction:int, *args)->tuple:
new_blocks = _collect_page(page, blocks, *args)
max_page = max((len(new_blocks) - 1) // page_size, 0)
new_page = max(0, min(int(page) + direction, max_page))
try:
session = context.get_session(session_id)
if session and session.get('id', False):
blocks_current = session['blocks_current']
if blocks_current.get('page') != new_page:
blocks_current['page'] = new_page
session['blocks_current'] = blocks_current
save_db_stamp(session_id)
except Exception as e:
error = f'_navigate(): {e}'
exception_alert(session_id, error)
return (
new_blocks,
new_page,
gr.update(interactive=new_page > 0),
gr.update(interactive=new_page < max_page),
)
def _edit_blocks(session_id:str)->tuple:
try:
session = context.get_session(session_id)
if session and session.get('id', False):
if not session['cancellation_requested']:
if session['status'] in [status_tags['EDIT']]:
visible_main = False
visible_blocks = True
ebook_name = Path(session['ebook']).stem
blocks_current = session['blocks_current']
blocks = blocks_current['blocks']
max_page = max((len(blocks) - 1) // page_size, 0)
page = max(0, min(int(blocks_current.get('page', 0)), max_page))
page_updates = list(_populate_page(session_id, page, blocks))
if session['cancellation_requested']:
visible_main = True
visible_blocks = False
result = (
gr.update(value=ebook_name),
gr.update(visible=visible_main), gr.update(visible=visible_blocks),
blocks, page,
gr.update(interactive=page > 0),
gr.update(interactive=page < max_page),
gr.update(interactive=True),
gr.update(interactive=True),
*page_updates
)
return result
except Exception as e:
error = f'_edit_blocks(): {e}'
exception_alert(session_id, error)
n = len(blocks_components_flat) + 1
return tuple(gr.update() for _ in range(9 + n + 1))
def _click_reset_block(session_id:str, block_id:int)->dict:
session = context.get_session(session_id)
if session and session.get('id', False):
text = session['blocks_orig']['blocks'][block_id]['text']
return gr.update(value=text)
return gr.update()
def _collect_page(page:int, blocks:list[dict], *args)->list[dict]:
expands = args[0]
keeps = args[1:page_size + 1]
voices = args[page_size + 1:2 * page_size + 1]
texts = args[2 * page_size + 1:]
new_blocks = [dict(b) for b in blocks]
start = int(page) * page_size
for i in range(page_size):
idx = start + i
if idx < len(new_blocks):
new_blocks[idx]['expand'] = expands[i] if i < len(expands) else False
new_blocks[idx]['keep'] = keeps[i]
new_blocks[idx]['voice'] = voices[i]
new_blocks[idx]['text'] = texts[i]
return new_blocks
def _change_current_blocks(session_id:str, page:int, blocks:list[dict], *args)->None:
session = context.get_session(session_id)
if session and session.get('id', False):
new_blocks = _collect_page(page, blocks, *args)
blocks_current = session['blocks_current']
old_blocks = blocks_current['blocks']
for idx, b in enumerate(new_blocks):
if 'voice' not in b:
b['voice'] = session.get('voice')
if 'tts_engine' not in b:
b['tts_engine'] = session.get('tts_engine', '')
if 'fine_tuned' not in b:
b['fine_tuned'] = session.get('fine_tuned', '')
old_b = old_blocks[idx] if idx < len(old_blocks) else None
if 'id' not in b and old_b is not None:
b['id'] = old_b.get('id')
if old_b and old_b.get('text', '').strip() != b.get('text', '').strip():
b['sentences'] = []
blocks_current['blocks'] = new_blocks
session['blocks_current'] = blocks_current
save_db_blocks(session_id)
def _click_gr_blocks_cancel_btn(session_id:str, page:int, blocks:list[dict], *args)->tuple:
session = context.get_session(session_id)
if session and session.get('id', False):
if session['status'] in [status_tags['EDIT']]:
session['status'] = status_tags['READY']
_change_current_blocks(session_id, page, blocks, *args)
return gr.update(interactive=True), gr.update(visible=True), _update_gr_audiobook_list(session_id), gr.update(visible=False), session['blocks_current']['blocks']
def _click_gr_blocks_confirm_btn(session_id:str, event:int, page:int, blocks:list[dict], *args)->tuple:
session = context.get_session(session_id)
if session and session.get('id', False):
if session['status'] in [status_tags['EDIT']]:
if not any(b['keep'] and b['text'].strip() for b in blocks):
error = 'At least one block must be kept.'
show_alert(session_id, {'type': 'warning', 'msg': error})
return tuple(gr.update() for _ in range(6))
_change_current_blocks(session_id, page, blocks, *args)
return gr.update(interactive=False), gr.update(interactive=False), gr.update(visible=True), gr.update(visible=False), _update_gr_audiobook_list(session_id), (event + 1)
return tuple(gr.update() for _ in range(6))
def _change_gr_restore_session(data:DictProxy|None, state:dict, req:gr.Request)->tuple:
try:
nonlocal models
msg = 'Error while loading saved session. Please try to delete your cookies and refresh the page'
if not data.get('id', False):
session = context.set_session(str(uuid.uuid4()))
else:
session = context.set_session(data.get('id'))
if len(active_sessions) == 0 or (data and data.get('status') in (None, status_tags['READY'])):
restore_session_from_data(
data, session,
force=bool(data and data.get('status') == status_tags['READY']),
filter_keys=True,
)
if not context_tracker.start_session(session['id']):
error = "Your session is already active.<br>If it's not the case please close your browser and relaunch it."
return gr.update(), gr.update(), gr.update(value=''), _update_gr_glassmask(str=error)
else:
active_sessions.add(req.session_hash)
session[req.session_hash] = req.session_hash
session['cancellation_requested'] = False
if isinstance(session.get('ebook'), str):
if not os.path.exists(session['ebook']):
session['ebook'] = session['ebook_src'] = None
if isinstance(session.get('voice'), str):
if not os.path.exists(session['voice']):
session['voice'] = session['ebook_src'] = None
if isinstance(session.get('custom_model'), str):
custom_model_dir = session.get('custom_model_dir')
if isinstance(custom_model_dir, str) and not os.path.exists(custom_model_dir):
session['custom_model'] = None
if isinstance(session.get('tts_engine'), str):
models = load_engine_presets(session['tts_engine'])
if models:
if session['fine_tuned'] not in models.keys():
session['fine_tuned'] = default_fine_tuned
else:
session['tts_engine'] = default_tts_engine
session['fine_tuned'] = default_fine_tuned
if session.get('translate_enabled'):
translate_target = session.get('translate')
if not translate_target or translate_target not in language_mapping:
session['translate_enabled'] = False
session['translate'] = None
session['translate_iso1'] = None
effective_lang = session.get('language')
if session.get('translate_enabled') and session.get('translate'):
effective_lang = session['translate']
if effective_lang:
compatible = get_compatible_tts_engines(effective_lang)
if compatible and session.get('tts_engine') not in compatible:
session['tts_engine'] = compatible[0]
session['fine_tuned'] = default_fine_tuned
models = load_engine_presets(session['tts_engine'])
if isinstance(session.get('audiobook'), str):
if not os.path.exists(session['audiobook']):
session['audiobook'] = None
session['status'] = status_tags['READY']
session['is_gui_process'] = is_gui_process
session['system'] = DEVICE_SYSTEM
session['session_dir'] = os.path.join(tmp_dir, f"proc-{session['id']}")
session['custom_model_dir'] = os.path.join(models_dir, '__sessions', f"model-{session['id']}")
session['voice_dir'] = os.path.join(voices_dir, '__sessions', f"voice-{session['id']}", session['language'])
os.makedirs(session['custom_model_dir'], exist_ok=True)
os.makedirs(session['voice_dir'], exist_ok=True)
if is_gui_shared:
msg = f' Note: access limit time: {interface_shared_tmp_expire} days. '
session['audiobooks_dir'] = os.path.join(audiobooks_gradio_dir, f"web-{session['id']}")
delete_unused_tmp_dirs(session['id'], audiobooks_gradio_dir, interface_shared_tmp_expire)
else:
msg = f' Note: if no activity is detected after {tmp_expire} days, your session will be cleaned up. '
session['audiobooks_dir'] = os.path.join(audiobooks_host_dir, f"web-{session['id']}")
delete_unused_tmp_dirs(session['id'], audiobooks_host_dir, tmp_expire)
msg += 'Your browser needs cookies enabled to resume the conversions.'
if not os.path.exists(session['audiobooks_dir']):
os.makedirs(session['audiobooks_dir'], exist_ok=True)
previous_hash = state['hash']
new_hash = hash_proxy_dict(MappingProxyType(session))
state['hash'] = new_hash
show_alert(session['id'], {"type": "info", "msg": msg})
return gr.update(value=json.dumps(session, cls=JSONDictProxyEncoder)), gr.update(value=state), gr.update(value=session['id']), gr.update()
except Exception as e:
error = f'_change_gr_restore_session(): {e}'
exception_alert(None, error)
return tuple(gr.update() for _ in range(4))
async def _update_gr_save_session(session_id:str, state:dict)->tuple:
try:
session = context.get_session(session_id)
if not session or (session and not session.get('id', False)):
yield tuple(gr.update() for _ in range(3))
return
previous_hash = state.get("hash")
if session.get('status', None) == status_tags['CONVERTING']:
try:
if session.get('ticker') != len(audiobook_options):
session['ticker'] = len(audiobook_options)
new_hash = hash_proxy_dict(MappingProxyType(session))
state['hash'] = new_hash
session_filtered = {k: v for k, v in session.items() if k not in save_session_keys_except}
session_dict = json.dumps(session_filtered, cls=JSONDictProxyEncoder)
yield (
gr.update(value=session_dict),
gr.update(value=state),
_update_gr_audiobook_list(session_id),
)
else:
yield tuple(gr.update() for _ in range(3))
except NameError:
new_hash = hash_proxy_dict(MappingProxyType(session))
state['hash'] = new_hash
session_filtered = {k: v for k, v in session.items() if k not in save_session_keys_except}
session_dict = json.dumps(session_filtered, cls=JSONDictProxyEncoder)
yield (
gr.update(value=session_dict),
gr.update(value=state),
gr.update(),
)
elif session.get('status', None) != status_tags['SKIP']:
if session.get('status', None) == status_tags['EDIT']:
save_db_blocks(session_id)
new_hash = hash_proxy_dict(MappingProxyType(session))
if previous_hash == new_hash:
yield tuple(gr.update() for _ in range(3))
else:
state['hash'] = new_hash
session_filtered = {k: v for k, v in session.items() if k not in save_session_keys_except}
session_dict = json.dumps(session_filtered, cls=JSONDictProxyEncoder)
yield (
gr.update(value=session_dict),
gr.update(value=state),
gr.update(),
)
yield tuple(gr.update() for _ in range(3))
except Exception as e:
error = f'_update_gr_save_session(): {e}!'
exception_alert(session_id, error)
yield gr.update(), gr.update(value=e), gr.update()
################## Events Section
def _chain_check_override(event):
return event.then(
fn=_check_override_ebook,
inputs=[gr_session, gr_ebook_mode, gr_ebook_src, gr_ebook_textarea, gr_blocks_preview, gr_event, gr_translate_enabled, gr_translate],
outputs=[gr_modal, gr_event],
show_progress_on=[gr_progress]
)
def _chain_refresh(event):
return event.then(
fn=_refresh_interface,
inputs=[gr_session],
outputs=outputs_refresh_interface,
show_progress_on=[gr_progress]
)
def _chain_enable(event, always=False):
if always:
return event.then(
fn=lambda s: (
list(_enable_components(s)) + [
1 if context.get_session(s).get('ebook_mode') == ebook_modes['TEXT']
else 0
]
),
inputs=[gr_session],
outputs=outputs_enable_components + [gr_end_event],
show_progress_on=[gr_progress]
).then(
fn=None,
inputs=[gr_end_event],
outputs=None,
js=f'(gr_end_event)=>{{if(gr_end_event){{{js_show_elements}}}}}'
)
else:
return event.then(
fn=lambda s: (
_enable_components(s) + (1,)
if context.get_session(s)['status'] in [status_tags['END'], status_tags['READY']]
and context.get_session(s)['ebook_mode'] == ebook_modes['TEXT']
else (
_enable_components(s) + (0,)
if context.get_session(s)['status'] in [status_tags['END'], status_tags['READY']]
else [gr.update()] * len(outputs_enable_components) + [0]
)
),
inputs=[gr_session],
outputs=outputs_enable_components + [gr_end_event],
show_progress_on=[gr_progress]
).then(
fn=None,
inputs=[gr_end_event],
outputs=None,
js=f'(gr_end_event)=>{{if(gr_end_event){{{js_show_elements}}}}}'
)
######## grouped tuples
inputs_start_conversion = [
gr_session, gr_device, gr_ebook_mode, gr_ebook_src, gr_ebook_textarea, gr_blocks_preview, gr_tts_engine_list, gr_language, gr_voice_list,
gr_custom_model_list, gr_fine_tuned_list, gr_output_format_list, gr_output_channel_list,
gr_xtts_temperature, gr_xtts_length_penalty, gr_xtts_num_beams, gr_xtts_repetition_penalty, gr_xtts_top_k, gr_xtts_top_p, gr_xtts_speed, gr_xtts_enable_text_splitting,
gr_bark_text_temp, gr_bark_waveform_temp, gr_output_split, gr_output_split_hours,
gr_translate_enabled, gr_translate
]
outputs_disable_components = [
gr_ebook_textarea, gr_ebook_mode, gr_blocks_preview, gr_language, gr_voice_file, gr_voice_list,
gr_device, gr_tts_engine_list, gr_fine_tuned_list, gr_custom_model_file,
gr_custom_model_list, gr_output_format_list, gr_output_channel_list, gr_output_split, gr_output_split_hours,
gr_translate_enabled, gr_translate,
gr_convert_btn, gr_voice_play, gr_voice_del_btn, gr_custom_model_del_btn, gr_session_switch_btn
]
outputs_enable_components = [
gr_ebook_textarea, gr_ebook_mode, gr_blocks_preview, gr_language, gr_voice_file, gr_voice_list,
gr_device, gr_tts_engine_list, gr_fine_tuned_list, gr_custom_model_file,
gr_custom_model_list, gr_output_format_list, gr_output_channel_list, gr_output_split, gr_output_split_hours,
gr_translate_enabled, gr_translate,
gr_voice_play, gr_voice_del_btn, gr_session_switch_btn, gr_blocks_cancel_btn, gr_blocks_confirm_btn, gr_custom_model_del_btn, gr_modal, gr_convert_btn
]
outputs_edit_blocks = [
gr_blocks_markdown, gr_group_main, gr_group_blocks,
gr_blocks_data, gr_blocks_page,
gr_blocks_back_btn, gr_blocks_next_btn,
gr_blocks_cancel_btn, gr_blocks_confirm_btn,
*blocks_components_flat, gr_blocks_header, gr_blocks_expands
]
outputs_restore_interface = [
gr_ebook_src, gr_ebook_textarea, gr_ebook_mode, gr_blocks_preview, gr_device, gr_language,
gr_translate_enabled, gr_translate, gr_voice_list, gr_tts_engine_list, gr_tts_rating,
gr_custom_model_list, gr_fine_tuned_list, gr_output_format_list, gr_output_channel_list,
gr_output_split, gr_output_split_hours, gr_row_output_split_hours, gr_audiobook_list, gr_group_custom_model, gr_convert_btn,
gr_voice_player_hidden, gr_voice_play, gr_voice_del_btn, gr_custom_model_file, gr_custom_model_del_btn
]
outputs_refresh_interface = [
gr_modal, gr_group_main, gr_tab_xtts_params, gr_tab_bark_params, gr_convert_btn,
gr_ebook_src, gr_ebook_textarea, gr_device, gr_audiobook_player, gr_audiobook_list,
gr_voice_list, gr_voice_highlight_css, gr_progress
]
outputs_on_voice_upload = [
gr_ebook_src, gr_ebook_textarea, gr_ebook_mode, gr_language, gr_tts_engine_list,
gr_fine_tuned_list, gr_custom_model_file, gr_custom_model_list, gr_session_switch_btn,
gr_convert_btn, gr_voice_play, gr_voice_del_btn
]
outputs_on_custom_upload = [
gr_ebook_src, gr_ebook_textarea, gr_ebook_mode, gr_language, gr_tts_engine_list,
gr_fine_tuned_list, gr_voice_file, gr_session_switch_btn,
gr_voice_play, gr_voice_del_btn, gr_convert_btn, gr_custom_model_del_btn
]
######### event triggers
gr_ebook_src.upload(
fn=_upload_gr_ebook_src,
inputs=[gr_session, gr_ebook_mode],
outputs=None
)
_chain_enable(
gr_ebook_src.change(
fn=_change_gr_ebook_src,
inputs=[gr_session, gr_ebook_mode, gr_ebook_src],
outputs=[gr_modal, gr_voice_highlight_css, gr_voice_list, gr_row_voice_player, gr_voice_selected_filename, gr_progress],
show_progress_on=[gr_ebook_src]
),
always=True
)
gr_ebook_src.select(
fn=_select_gr_ebook_src,
inputs=[gr_session, gr_ebook_mode, gr_ebook_src],
outputs=[gr_voice_list, gr_voice_highlight_css, gr_row_voice_player, gr_voice_selected_filename],
show_progress='hidden'
)
gr_ebook_textarea.change(
fn=_change_gr_ebook_textarea,
inputs=[gr_session, gr_ebook_textarea],
outputs=None
)
gr_ebook_mode.change(
fn=_change_gr_ebook_mode,
inputs=[gr_session, gr_ebook_mode],
outputs=[gr_ebook_src, gr_ebook_textarea, gr_convert_btn, gr_voice_highlight_css, gr_row_voice_player, gr_voice_selected_filename],
show_progress_on=[gr_progress]
).then(
fn=None,
inputs=[gr_ebook_mode],
outputs=None,
js=f'''(mode)=>{{if(mode === "{ebook_modes['TEXT']}"){{window.gr_ebook_textarea_counter();}}}}'''
)
gr_blocks_preview.select(
fn=lambda session_id, val: _change_param('blocks_preview', session_id, bool(val)),
inputs=[gr_session, gr_blocks_preview],
outputs=None
)
gr_voice_file.upload(
fn=_disable_on_voice_upload,
inputs=None,
outputs=outputs_on_voice_upload,
show_progress_on=[gr_voice_file]
).then(
fn=_change_gr_voice_file,
inputs=[gr_session, gr_voice_file],
outputs=[gr_voice_list],
show_progress_on=[gr_voice_list]
).then(
fn=lambda: gr.update(value=None),
inputs=None,
outputs=[gr_voice_file],
show_progress_on=[gr_voice_list]
).then(
fn=_enable_on_voice_upload,
inputs=[gr_session],
outputs=outputs_on_voice_upload,
show_progress_on=[gr_voice_list]
)
gr_voice_list.change(
fn=_change_gr_voice_list,
inputs=[gr_session, gr_voice_list],
outputs=[gr_voice_player_hidden, gr_voice_play, gr_voice_del_btn],
show_progress_on=[gr_progress]
)
gr_voice_del_btn.click(
fn=_click_gr_voice_del_btn,
inputs=[gr_session, gr_voice_list],
outputs=[gr_modal, gr_data_field_hidden],
show_progress_on=[gr_progress]
)
gr_device.change(
fn=_change_gr_device,
inputs=[gr_session, gr_device],
outputs=None
)
gr_language.change(
fn=_change_gr_language,
inputs=[gr_session, gr_language],
outputs=[gr_translate, gr_tts_engine_list, gr_custom_model_list, gr_fine_tuned_list],
show_progress_on=[gr_progress]
).then(
fn=_update_gr_voice_list,
inputs=[gr_session],
outputs=[gr_voice_list],
show_progress_on=[gr_progress]
)
gr_translate_enabled.change(
fn=_click_gr_translate_enabled,
inputs=[gr_session, gr_translate_enabled],
outputs=[gr_translate, gr_tts_engine_list, gr_custom_model_list, gr_fine_tuned_list, gr_voice_list],
show_progress_on=[gr_progress]
)
gr_translate.change(
fn=_change_gr_translate,
inputs=[gr_session, gr_translate],
outputs=[gr_tts_engine_list, gr_custom_model_list, gr_fine_tuned_list],
show_progress_on=[gr_progress]
).then(
fn=_change_gr_tts_engine_list,
inputs=[gr_session, gr_tts_engine_list],
outputs=[gr_tts_rating, gr_tab_xtts_params, gr_tab_bark_params, gr_group_custom_model, gr_fine_tuned_list, gr_custom_model_file, gr_custom_model_list],
show_progress_on=[gr_progress]
)
gr_tts_engine_list.change(
fn=_change_gr_tts_engine_list,
inputs=[gr_session, gr_tts_engine_list],
outputs=[gr_tts_rating, gr_tab_xtts_params, gr_tab_bark_params, gr_group_custom_model, gr_fine_tuned_list, gr_custom_model_file, gr_custom_model_list],
show_progress_on=[gr_progress]
).then(
fn=_update_gr_voice_list,
inputs=[gr_session],
outputs=[gr_voice_list],
show_progress_on=[gr_progress]
)
gr_fine_tuned_list.change(
fn=_change_gr_fine_tuned_list,
inputs=[gr_session, gr_fine_tuned_list],
outputs=[gr_group_custom_model],
show_progress_on=[gr_progress]
).then(
fn=_update_gr_voice_list,
inputs=[gr_session],
outputs=[gr_voice_list],
show_progress_on=[gr_progress]
)
gr_custom_model_file.upload(
fn=_disable_on_custom_upload,
inputs=None,
outputs=outputs_on_custom_upload,
show_progress_on=[gr_custom_model_file]
).then(
fn=_change_gr_custom_model_file,
inputs=[gr_session, gr_custom_model_file, gr_tts_engine_list],
outputs=[gr_custom_model_file, gr_custom_model_list],
show_progress_on=[gr_custom_model_list]
).then(
fn=_update_gr_voice_list,
inputs=[gr_session],
outputs=[gr_voice_list],
show_progress_on=[gr_custom_model_list]
).then(
fn=_enable_on_custom_upload,
inputs=[gr_custom_model_list, gr_ebook_src, gr_ebook_textarea],
outputs=outputs_on_custom_upload,
show_progress_on=[gr_custom_model_list]
)
gr_custom_model_list.change(
fn=_change_gr_custom_model_list,
inputs=[gr_session, gr_custom_model_list],
outputs=[gr_fine_tuned_list, gr_voice_list, gr_custom_model_del_btn],
show_progress_on=[gr_progress]
)
gr_custom_model_del_btn.click(
fn=_click_gr_custom_model_del_btn,
inputs=[gr_session, gr_custom_model_list],
outputs=[gr_modal, gr_data_field_hidden],
show_progress_on=[gr_progress]
)
gr_output_format_list.change(
fn=_change_gr_output_format_list,
inputs=[gr_session, gr_output_format_list],
outputs=None
)
gr_output_channel_list.change(
fn=_change_gr_output_channel_list,
inputs=[gr_session, gr_output_channel_list],
outputs=None
)
gr_output_split.select(
fn=_change_gr_output_split,
inputs=[gr_session, gr_output_split],
outputs=[gr_row_output_split_hours],
show_progress_on=[gr_progress]
)
gr_output_split_hours.change(
fn=lambda session_id, val: _change_param('output_split_hours', session_id, str(val)),
inputs=[gr_session, gr_output_split_hours],
outputs=None
)
gr_session_switch_btn.click(
fn=_click_gr_session_switch_btn,
inputs=[gr_session, gr_backup_session],
outputs=[gr_restore_session, gr_session, gr_backup_session, gr_session_switch_btn, gr_session_switch_disable_state, gr_session_switch_enable_state],
show_progress_on=[gr_session_switch_btn]
)
gr_session_switch_disable_state.change(
fn=lambda session_id: _disable_components(session_id, exceptions=['gr_session_switch_btn']),
inputs=[gr_session_switch_disable_state],
outputs=outputs_disable_components,
show_progress_on=[gr_session_switch_btn],
queue=False
).then(
fn=lambda: None,
inputs=None,
outputs=[gr_session_switch_disable_state],
js=f'''
()=>{{
const elem = document.querySelector("#gr_session textarea");
if(elem){{
elem.select();
}}
{js_hide_elements}
}}
'''
)
gr_session_switch_enable_state.change(
fn=_enable_components,
inputs=[gr_session_switch_enable_state],
outputs=outputs_enable_components,
show_progress_on=[gr_session_switch_btn]
).then(
fn=lambda: None,
inputs=None,
outputs=[gr_session_switch_enable_state],
js=f'''
()=>{{
const elem = document.querySelector("#gr_session textarea");
if(elem){{
elem.setSelectionRange(0, 0);
}}
{js_show_elements}
}}
'''
)
gr_progress.change(
fn=None,
inputs=[gr_progress],
js=r'''
(filename)=>{
if(filename){
const gr_root = (window.gradioApp && window.gradioApp()) || document;
const gr_ebook_src = gr_root.querySelector("#gr_ebook_src");
if(!gr_ebook_src){
return;
}
function normalizeForGradio(name){
return name
.normalize("NFC")
// Remove chars not supported by OS paths
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "")
// Remove Gradio-sanitized odd punctuation (including quotes)
.replace(/[!(){}\[\]']/g, "")
// Collapse multiple dots/spaces before extension
.replace(/\s+\./g, ".")
// Strip trailing spaces/dots (Windows forbids)
.replace(/[. ]+$/, "")
// Remove Arabic tatweel/harakat
.replace(/[\u0640\u0651\u064B-\u065F]/g, "")
.trim();
}
const rows = gr_ebook_src.querySelectorAll("table.file-preview tr.file");
rows.forEach((row, idx) => {
const filenameCell = row.querySelector("td.filename");
if (filenameCell) {
const rowName = normalizeForGradio(filenameCell.getAttribute("aria-label"));
filename = filename.split("/")[0].trim();
if (rowName === filename) {
row.style.display = "none";
}
}
});
}
}
'''
)
gr_playback_time.change(
fn=_change_gr_playback_time,
inputs=[gr_session, gr_playback_time],
js='''
(time)=>{
try{
window.session_storage.playback_time = Number(time);
}catch(e){
console.warn("gr_playback_time.change error: "+e);
}
}
'''
)
gr_audiobook_download_btn.click(
fn=_toggle_audiobook_files,
inputs=[gr_session, gr_audiobook_list, gr_audiobook_files_state],
outputs=[gr_audiobook_files, gr_audiobook_files_state],
show_progress_on=[gr_audiobook_list]
)
gr_audiobook_list.change(
fn=_change_gr_audiobook_list,
inputs=[gr_session, gr_audiobook_list],
outputs=[gr_group_audiobook_list],
show_progress_on=[gr_audiobook_list]
).then(
fn=_update_gr_audiobook_player,
inputs=[gr_session],
outputs=[gr_playback_time, gr_audiobook_player, gr_audiobook_vtt],
show_progress_on=[gr_audiobook_list]
).then(
fn=lambda: (gr.update(visible=False, value=None), False),
inputs=None,
outputs=[gr_audiobook_files, gr_audiobook_files_state],
show_progress_on=[gr_audiobook_list],
js='()=>{window.load_vtt();}'
)
gr_audiobook_del_btn.click(
fn=_click_gr_audiobook_del_btn,
inputs=[gr_session, gr_audiobook_list],
outputs=[gr_modal, gr_data_field_hidden],
show_progress_on=[gr_audiobook_list]
)
########### XTTS Params
gr_tab_xtts_params.select(
fn=None,
inputs=None,
outputs=None,
js='''
() => {
if (!window._xtts_sliders_initialized) {
const checkXttsExist = setInterval(() => {
const slider = document.querySelector("#gr_xtts_speed input[type=range]");
if(slider){
clearInterval(checkXttsExist);
window._xtts_sliders_initialized = true;
init_xtts_sliders();
}
}, 500);
}
}
'''
)
gr_xtts_temperature.change(
fn=lambda session_id, val: _change_param('xtts_temperature', session_id, float(val)),
inputs=[gr_session, gr_xtts_temperature],
outputs=None
)
gr_xtts_length_penalty.change(
fn=lambda session_id, val, val2: _change_param('xtts_length_penalty', session_id, int(val), int(val2)),
inputs=[gr_session, gr_xtts_length_penalty, gr_xtts_num_beams],
outputs=None,
)
gr_xtts_num_beams.change(
fn=lambda session_id, val, val2: _change_param('xtts_num_beams', session_id, int(val), int(val2)),
inputs=[gr_session, gr_xtts_num_beams, gr_xtts_length_penalty],
outputs=None,
)
gr_xtts_repetition_penalty.change(
fn=lambda session_id, val: _change_param('xtts_repetition_penalty', session_id, float(val)),
inputs=[gr_session, gr_xtts_repetition_penalty],
outputs=None
)
gr_xtts_top_k.change(
fn=lambda session_id, val: _change_param('xtts_top_k', session_id, int(val)),
inputs=[gr_session, gr_xtts_top_k],
outputs=None
)
gr_xtts_top_p.change(
fn=lambda session_id, val: _change_param('xtts_top_p', session_id, float(val)),
inputs=[gr_session, gr_xtts_top_p],
outputs=None
)
gr_xtts_speed.change(
fn=lambda session_id, val: _change_param('xtts_speed', session_id, float(val)),
inputs=[gr_session, gr_xtts_speed],
outputs=None
)
gr_xtts_enable_text_splitting.select(
fn=lambda session_id, val: _change_param('xtts_enable_text_splitting', session_id, bool(val)),
inputs=[gr_session, gr_xtts_enable_text_splitting],
outputs=None
)
########### BARK Params
gr_tab_bark_params.select(
fn=None,
inputs=None,
outputs=None,
js='''
()=>{
if (!window._bark_sliders_initialized) {
const checkBarkExist = setInterval(() => {
const slider = document.querySelector("#gr_bark_waveform_temp input[type=range]");
if(slider){
clearInterval(checkBarkExist);
window._bark_sliders_initialized = true;
init_bark_sliders();
}
}, 500);
}
}
'''
)
gr_bark_text_temp.change(
fn=lambda session_id, val: _change_param('bark_text_temp', session_id, float(val)),
inputs=[gr_session, gr_bark_text_temp],
outputs=None
)
gr_bark_waveform_temp.change(
fn=lambda session_id, val: _change_param('bark_waveform_temp', session_id, float(val)),
inputs=[gr_session, gr_bark_waveform_temp],
outputs=None
)
############ Timer to save session to localStorage
gr_timer = gr.Timer(9, active=False)
gr_timer.tick(
fn=_update_gr_save_session,
inputs=[gr_session, gr_session_update],
outputs=[gr_save_session, gr_session_update, gr_audiobook_list]
)
########### Main chains
_chain_enable(
_chain_check_override(
gr_convert_btn.click(
fn=_disable_components,
inputs=[gr_session],
outputs=outputs_disable_components,
show_progress_on=[gr_progress],
queue=False
)
),
always=False
).then(
js=f'()=>{{{js_hide_elements}}}'
)
_chain_enable(
gr_override_cancel_btn.click(
fn=_click_gr_override_cancel_btn,
inputs=[gr_session],
outputs=[gr_modal],
show_progress_on=[gr_progress]
),
always=True
)
gr_override_confirm_btn.click(
fn=_click_gr_override_confirm_btn,
inputs=[gr_session, gr_event, gr_audiobook_files_state],
outputs=[gr_modal, gr_event, gr_audiobook_list, gr_audiobook_files, gr_audiobook_files_state],
show_progress_on=[gr_progress]
)
_chain_enable(
_chain_check_override(
_chain_refresh(
gr_event.change(
fn=_disable_components,
inputs=[gr_session],
outputs=outputs_disable_components,
show_progress_on=[gr_progress],
queue=False
).then(
fn=lambda: None,
js=f'()=>{{{js_hide_elements}}}'
).then(
fn=_start_conversion,
inputs=inputs_start_conversion,
outputs=[gr_progress],
show_progress_on=[gr_progress],
).then(
fn=_edit_blocks,
inputs=[gr_session],
outputs=outputs_edit_blocks,
show_progress_on=[gr_progress]
)
)
),
always=False
)
_chain_enable(
gr_blocks_cancel_btn.click(
fn=lambda: (gr.update(interactive=False), gr.update(interactive=False)),
outputs=[gr_blocks_cancel_btn, gr_blocks_confirm_btn],
show_progress_on=[gr_progress],
queue=False
).then(
fn=_click_gr_blocks_cancel_btn,
inputs=[gr_session, gr_blocks_page, gr_blocks_data, gr_blocks_expands, *blocks_keeps, *blocks_voices, *blocks_texts],
outputs=[gr_convert_btn, gr_group_main, gr_audiobook_list, gr_group_blocks, gr_blocks_data],
show_progress_on=[gr_progress]
),
always=True
)
_chain_enable(
gr_blocks_confirm_btn.click(
fn=lambda page, blocks, expands, *args: _collect_page(page, blocks, expands, *args),
inputs=[gr_blocks_page, gr_blocks_data, gr_blocks_expands, *blocks_keeps, *blocks_voices, *blocks_texts],
outputs=[gr_blocks_data],
show_progress_on=[gr_progress]
).then(
fn=_click_gr_blocks_confirm_btn,
inputs=[gr_session, gr_blocks_event, gr_blocks_page, gr_blocks_data, gr_blocks_expands, *blocks_keeps, *blocks_voices, *blocks_texts],
outputs=[gr_blocks_cancel_btn, gr_blocks_confirm_btn, gr_group_main, gr_group_blocks, gr_audiobook_list, gr_blocks_event],
show_progress_on=[gr_progress]
)
)
_chain_enable(
_chain_check_override(
_chain_refresh(
gr_blocks_event.change(
fn=finalize_audiobook,
inputs=[gr_session],
outputs=[gr_progress, gr_dummy_bool],
#show_progress_on=[gr_progress]
)
)
),
always=True
).then(
fn=None,
inputs=None,
outputs=None,
js='()=>{window.load_vtt();}'
)
###########
gr_blocks_back_btn.click(
fn=lambda session_id, page, blocks, *args: _navigate(session_id, page, blocks, -1, *args),
inputs=[gr_session, gr_blocks_page, gr_blocks_data, gr_blocks_expands, *blocks_keeps, *blocks_voices, *blocks_texts],
outputs=[gr_blocks_data, gr_blocks_page, gr_blocks_back_btn, gr_blocks_next_btn],
show_progress_on=[gr_blocks_nav]
).then(
fn=_populate_page,
inputs=[gr_session, gr_blocks_page, gr_blocks_data],
outputs=[*blocks_components_flat, gr_blocks_header, gr_blocks_expands],
show_progress_on=[gr_blocks_nav]
)
gr_blocks_next_btn.click(
fn=lambda session_id, page, blocks, *args: _navigate(session_id, page, blocks, 1, *args),
inputs=[gr_session, gr_blocks_page, gr_blocks_data, gr_blocks_expands, *blocks_keeps, *blocks_voices, *blocks_texts],
outputs=[gr_blocks_data, gr_blocks_page, gr_blocks_back_btn, gr_blocks_next_btn],
show_progress_on=[gr_blocks_nav]
).then(
fn=_populate_page,
inputs=[gr_session, gr_blocks_page, gr_blocks_data],
outputs=[*blocks_components_flat, gr_blocks_header, gr_blocks_expands],
show_progress_on=[gr_blocks_nav]
)
#############
gr_save_session.change(
fn=None,
inputs=[gr_save_session],
js='''
(data)=>{
try{
if(data){
localStorage.clear();
data.playback_time = Number(window.session_storage.playback_time);
data.playback_volume = parseFloat(window.session_storage.playback_volume);
localStorage.setItem("data", JSON.stringify(data));
}
}catch(e){
console.warn("gr_save_session.change error: "+e);
}
}
'''
)
gr_restore_session.change(
fn=_change_gr_restore_session,
inputs=[gr_restore_session, gr_session_update],
outputs=[gr_save_session, gr_session_update, gr_session, gr_glassmask],
show_progress_on=[gr_progress]
).then(
fn=_restore_interface,
inputs=[gr_session],
outputs=outputs_restore_interface,
show_progress_on=[gr_progress]
).then(
fn=_restore_audiobook_player,
inputs=[gr_session, gr_audiobook_list],
outputs=[gr_group_audiobook_list, gr_audiobook_player, gr_timer],
show_progress_on=[gr_progress]
).then(
fn=lambda session: _update_gr_glassmask(attr=['gr-glass-mask', 'hide']) if session else gr.update(),
inputs=[gr_session],
outputs=[gr_glassmask],
show_progress_on=[gr_progress]
).then(
fn=None,
inputs=None,
js='()=>{window.init_interface();}'
)
gr_deletion_confirm_btn.click(
fn=_click_gr_deletion,
inputs=[gr_session, gr_voice_list, gr_custom_model_list, gr_audiobook_list, gr_data_field_hidden],
outputs=[gr_modal, gr_custom_model_list, gr_audiobook_list, gr_voice_list],
show_progress_on=[gr_progress]
)
gr_deletion_cancel_btn.click(
fn=_click_gr_deletion,
inputs=[gr_session, gr_voice_list, gr_custom_model_list, gr_audiobook_list],
outputs=[gr_modal, gr_custom_model_list, gr_audiobook_list, gr_voice_list],
show_progress_on=[gr_progress]
)
############
app.load(
fn=None,
js=r'''
()=>{
try{
let gr_root = (window.gradioApp && window.gradioApp()) || document;
let gr_checkboxes;
let gr_radios;
let gr_voice_player_hidden;
let gr_audiobook_vtt;
let gr_audiobook_sentence;
let gr_audiobook_player;
let gr_playback_time;
let gr_progress;
let gr_voice_play;
let gr_ebook_textarea;
let tabs_open = false;
let init_elements_timeout;
let init_audiobook_player_timeout;
let audio_filter = "";
let cues = [];
if(typeof window.onElementAvailable !== "function"){
window.onElementAvailable = (selector, callback, { root = (window.gradioApp && window.gradioApp()) || document, once = false } = {})=> {
const seen = new WeakSet();
const fireFor = (context) => {
context.querySelectorAll(selector).forEach((el) => {
if (seen.has(el)) return;
const success = callback(el);
if (success !== false) {
// Mark as seen only if callback succeeded
seen.add(el);
if (once) return;
} else {
// Retry check later (in case conditions werent met yet)
setTimeout(() => fireFor(root), 300);
}
});
};
fireFor(root);
const observer = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (n.nodeType !== 1) continue;
if (n.matches?.(selector)) {
if (!seen.has(n)) {
const success = callback(n);
if (success !== false) {
seen.add(n);
if (once) {
observer.disconnect();
return;
}
} else {
setTimeout(() => fireFor(root), 300);
}
}
} else {
fireFor(n);
}
}
}
});
observer.observe(root, { childList: true, subtree: true });
return () => observer.disconnect();
}
}
if(typeof window.init_interface !== "function"){
window.init_interface = ()=>{
try {
gr_root = (window.gradioApp && window.gradioApp()) || document;
gr_progress = (gr_root) ? gr_root.querySelector("#gr_progress") : undefined;
if(!gr_root || !gr_progress){
clearTimeout(init_elements_timeout);
console.warn("Components not ready… retrying");
init_elements_timeout = setTimeout(init_interface, 1000);
return;
}
// Function to apply theme borders
function applyThemeBorders(){
const url = new URL(window.location);
const theme = url.searchParams.get("__theme");
let elColor = "#666666";
if(theme == "dark"){
elColor = "#fff";
}else if(!theme){
const osTheme = window.matchMedia?.("(prefers-color-scheme: dark)").matches;
if(osTheme){
elColor = "#fff";
}
}
gr_root.querySelectorAll("input[type='checkbox'], input[type='radio']")
.forEach(cb => cb.style.border = "1px solid " + elColor);
}
// Run once on init
applyThemeBorders();
// Re-run when DOM changes (tabs, redraws, etc.)
new MutationObserver(applyThemeBorders).observe(gr_root, {
childList: true,
subtree: true
});
// Keep your progress observer
new MutationObserver(tab_progress).observe(gr_progress, {
attributes: true,
childList: true,
subtree: true,
characterData: true
});
// new MutationObserver(tab_progress).observe(gr_progress.parentElement, { ... });
// gr_progress.addEventListener("change", tab_progress);
if(!window._tab_progress_interval){
window._tab_progress_interval = setInterval(tab_progress, 500);
}
window.gr_ebook_textarea_counter();
}catch(e){
console.warn("init_interface error:", e);
}
};
}
if(typeof(window._restoreSlider) !== "function"){
window._restoreSlider = (slider, parseFn = parseFloat)=>{
if(!slider) return;
const container = slider.closest("div[id]");
if(!container) return;
const key = container.id.replace(/^gr_/, "");
const saved = window.session_storage?.[key];
if(saved === undefined || saved === null || saved === ""){
return;
}
const parsed = parseFn(saved);
if(!Number.isFinite(parsed)){
return;
}
slider.value = parsed;
slider.dispatchEvent(new Event("input", { bubbles: true }));
};
}
if(typeof(window.init_xtts_sliders) !== "function"){
window.init_xtts_sliders = ()=>{
try{
const q = (id) => gr_root.querySelector(`#gr_${id} input[type=number]`);
window._restoreSlider(q("xtts_temperature"));
window._restoreSlider(q("xtts_repetition_penalty"));
window._restoreSlider(q("xtts_top_k"), (v) => parseInt(v, 10));
window._restoreSlider(q("xtts_top_p"));
window._restoreSlider(q("xtts_speed"));
}catch(e){
console.warn("init_xtts_sliders error:", e);
}
};
}
if(typeof(window.init_bark_sliders) !== "function"){
window.init_bark_sliders = ()=>{
try{
const q = (id) => gr_root.querySelector(`#gr_${id} input[type=number]`);
window._restoreSlider(q("bark_text_temp"));
window._restoreSlider(q("bark_waveform_temp"));
}catch(e){
console.warn("init_bark_sliders error:", e);
}
};
}
if(typeof window.init_voice_player_hidden !== "function"){
window.init_voice_player_hidden = ()=>{
try{
const gr_voice_player_hidden = gr_root.querySelector("#gr_voice_player_hidden audio");
const gr_voice_play = gr_root.querySelector("#gr_voice_play");
if(gr_voice_player_hidden && gr_voice_play){
if(gr_voice_play.dataset.bound === "true") return;
gr_voice_play.dataset.bound = "true";
gr_voice_player_hidden.addEventListener("loadeddata", ()=>{
gr_voice_play.textContent = "▶";
});
gr_voice_play.addEventListener("click", ()=>{
if(gr_voice_player_hidden.paused){
gr_voice_player_hidden.play().then(()=>{
gr_voice_play.textContent = "⏸";
}).catch(err => console.warn("Play failed:", err));
}else{
gr_voice_player_hidden.pause();
gr_voice_play.textContent = "▶";
}
});
gr_voice_player_hidden.addEventListener("pause", ()=>{
gr_voice_play.textContent = "▶";
});
gr_voice_player_hidden.addEventListener("ended", ()=>{
gr_voice_play.textContent = "▶";
});
gr_voice_player_hidden.addEventListener("play", ()=>{
const v = window.session_storage?.playback_volume ?? 1;
gr_voice_player_hidden.volume = v;
});
return true;
}else{
console.warn("Voice player not found yet, retrying…");
setTimeout(window.init_voice_player_hidden, 500);
}
}catch(e){
console.warn("init_voice_player_hidden error:", e);
}
return false;
};
}
if(typeof(window.init_audiobook_player) !== "function"){
window.init_audiobook_player = ()=>{
try{
if(gr_root){
gr_audiobook_player = gr_root.querySelector("#gr_audiobook_player audio");
gr_audiobook_sentence = gr_root.querySelector("#gr_audiobook_sentence textarea");
gr_playback_time = gr_root.querySelector("#gr_playback_time input");
let lastCue = null;
let fade_timeout = null;
let last_time = 0;
if(gr_audiobook_player && gr_audiobook_sentence && gr_playback_time){
function trackPlayback(){
try {
window.session_storage.playback_time = parseFloat(gr_audiobook_player.currentTime);
const cue = findCue(window.session_storage.playback_time);
if(cue && cue !== lastCue){
if(fade_timeout){
gr_audiobook_sentence.style.opacity = "1";
}else{
gr_audiobook_sentence.style.opacity = "0";
}
gr_audiobook_sentence.style.transition = "none";
gr_audiobook_sentence.value = cue.text;
clearTimeout(fade_timeout);
fade_timeout = setTimeout(() => {
gr_audiobook_sentence.style.transition = "opacity 0.15s ease-in";
gr_audiobook_sentence.style.opacity = "1";
fade_timeout = null;
}, 33);
lastCue = cue;
}else if(!cue && lastCue !== null){
lastCue = null;
}
const now = performance.now();
if(now - last_time > 1000){
gr_playback_time.value = String(window.session_storage.playback_time);
gr_playback_time.dispatchEvent(new Event("input", {bubbles: true}));
last_time = now;
}
}catch(e){
console.warn("gr_audiobook_player tracking error:", e);
}
if(!gr_audiobook_player.ended){
requestAnimationFrame(trackPlayback);
}
}
gr_audiobook_player.addEventListener("loadeddata", ()=>{
gr_audiobook_player.style.transition = "filter 1s ease";
gr_audiobook_player.style.filter = audio_filter;
gr_audiobook_player.currentTime = parseFloat(window.session_storage?.playback_time) || 0;
gr_audiobook_player.volume = window.session_storage.playback_volume;
});
gr_audiobook_player.addEventListener("play", ()=>{
requestAnimationFrame(trackPlayback);
});
gr_audiobook_player.addEventListener("seeked", ()=>{
window.session_storage.playback_time = gr_audiobook_player.currentTime;
requestAnimationFrame(trackPlayback);
});
gr_audiobook_player.addEventListener("ended", ()=>{
gr_audiobook_sentence.value = "…";
window.session_storage.playback_time = 0;
lastCue = null;
});
gr_audiobook_player.addEventListener("volumechange", ()=>{
window.session_storage.playback_volume = gr_audiobook_player.volume;
gr_voice_player_hidden = gr_root.querySelector("#gr_voice_player_hidden audio");
if(gr_voice_player_hidden){
gr_voice_player_hidden.volume = gr_audiobook_player.volume;
gr_voice_player_hidden.dispatchEvent(new Event("volumechange", { bubbles: true }));
}
});
const themURL = new URL(window.location);
const theme = themURL.searchParams.get("__theme");
let osTheme;
if(theme){
if(theme == "dark"){
audio_filter = "invert(1) hue-rotate(180deg)";
}
}else{
osTheme = window.matchMedia?.("(prefers-color-scheme: dark)").matches;
if(osTheme){
audio_filter = "invert(1) hue-rotate(180deg)";
}
}
gr_audiobook_player.style.transition = "filter 1s ease";
gr_audiobook_player.style.filter = audio_filter;
gr_audiobook_player.volume = window.session_storage.playback_volume;
return true;
}
}
}catch(e){
console.warn("init_audiobook_player error:", e);
}
return false;
};
}
if(typeof window.gr_ebook_textarea_counter !== "function"){
const max_ebook_textarea_length = __max_ebook_textarea_length__;
window.gr_ebook_textarea_counter = function(){
const container = document.querySelector("#gr_ebook_textarea");
if(container){
const textarea = container.querySelector("textarea");
const ebook_textarea_toolbar = document.querySelector("#ebook_textarea_toolbar");
document.querySelector("#ebook_textarea_toolbar")?.remove();
container.style.position = "relative";
const toolbar = document.createElement("div");
toolbar.id = toolbar.name = "ebook_textarea_toolbar";
toolbar.style.cssText = "position:absolute;top:4px;right:8px;display:flex;align-items:center;gap:6px;z-index:1;";
const counter = document.createElement("span");
counter.style.cssText = "font-size:0.85em;color:var(--body-text-color);";
counter.textContent = textarea.value.length + " / " + max_ebook_textarea_length;
toolbar.appendChild(counter);
const btn = document.createElement("button");
btn.textContent = "🗑";
btn.id = btn.name = "clear_ebook_textarea";
btn.className = "micro-btn";
btn.addEventListener("click", ()=>{
textarea.value = "";
textarea.dispatchEvent(new Event("input", {bubbles: true}));
counter.textContent = "0 / " + max_ebook_textarea_length;
counter.style.color = "var(--body-text-color)";
});
textarea.addEventListener("input", ()=>{
const len = textarea.value.length;
counter.textContent = len + " / " + max_ebook_textarea_length;
counter.style.color = len >= max_ebook_textarea_length ? "red" : "var(--body-text-color)";
});
toolbar.appendChild(btn);
container.appendChild(toolbar);
}
};
}
if(typeof(window.tab_progress) !== "function"){
window.tab_progress = ()=>{
try{
const gr_root = (window.gradioApp && window.gradioApp()) || document;
const el = gr_root.querySelector("#gr_progress");
const val = el?.value || el?.textContent || "";
const valArray = splitAtLastDash(val);
if(valArray[1]){
const title = valArray[0].trim().split(/ (.*)/)[1].trim();
const percentage = valArray[1].trim();
const titleShort = title.length >= 20 ? title.slice(0, 20).trimEnd() + "…" : title;
document.title = titleShort + ": " + percentage;
}else{
document.title = "Ebook2Audiobook";
}
}catch(e){
console.warn("tab_progress error:", e);
}
};
}
if(typeof(splitAtLastDash) !== "function"){
function splitAtLastDash(s){
const idx = s.lastIndexOf("-");
if(idx === -1){
return [s];
}
return [s.slice(0, idx).trim(), s.slice(idx + 1).trim()];
}
}
if(typeof(window.load_vtt) !== "function"){
window.load_vtt = ()=>{
try{
gr_audiobook_vtt = gr_root.querySelector("#gr_audiobook_vtt textarea");
gr_audiobook_sentence = gr_root.querySelector("#gr_audiobook_sentence textarea");
if(gr_audiobook_sentence){
gr_audiobook_sentence.style.fontSize = "14px";
gr_audiobook_sentence.style.fontWeight = "bold";
gr_audiobook_sentence.style.width = "100%";
gr_audiobook_sentence.style.height = "auto";
gr_audiobook_sentence.style.textAlign = "center";
gr_audiobook_sentence.style.margin = "0";
gr_audiobook_sentence.style.padding = "7px 0 7px 0";
gr_audiobook_sentence.style.lineHeight = "14px";
const txt = gr_audiobook_vtt.value;
if(txt == ""){
gr_audiobook_sentence.value = "…";
}else{
parseVTT(txt);
}
}
}catch(e){
console.warn("load_vtt error:", e);
}
};
}
if(typeof(window.parseVTT) !== "function"){
window.parseVTT = (vtt)=>{
function pushCue(){
if(start !== null && end !== null && textBuffer.length){
cues.push({ start, end, text: textBuffer.join("\n") });
}
start = end = null;
textBuffer.length = 0;
}
const lines = vtt.split(/\r?\n/);
const timePattern = /(\d{2}:)?\d{2}:\d{2}\.\d{3}/;
let start = null, end = null;
cues = [];
textBuffer = [];
for(let i = 0, len = lines.length; i < len; i++){
const line = lines[i];
if(!line.trim()){ pushCue(); continue; }
if(line.includes("-->")){
const [s, e] = line.split("-->").map(l => l.trim().split(" ")[0]);
if(timePattern.test(s) && timePattern.test(e)){
start = toSeconds(s);
end = toSeconds(e);
}
}else if(!timePattern.test(line)){
textBuffer.push(line);
}
}
pushCue();
}
}
if(typeof(toSeconds) !== "function"){
function toSeconds(ts){
const parts = ts.split(":");
if(parts.length === 3){
return parseInt(parts[0], 10) * 3600 +
parseInt(parts[1], 10) * 60 +
parseFloat(parts[2]);
}
return parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
}
}
if(typeof(findCue) !== "function"){
function findCue(time){
let lo = 0, hi = cues.length - 1;
while(lo <= hi){
const mid = (lo + hi) >> 1;
const cue = cues[mid];
if(time < cue.start){
hi = mid - 1;
}else if(time >= cue.end){
lo = mid + 1;
}else{
return cue;
}
}
return null;
}
}
if(typeof(splitAtLastDash) !== "function"){
function splitAtLastDash(s){
const idx = s.lastIndexOf("-");
if(idx === -1){
return [s];
}
return [s.slice(0, idx).trim(), s.slice(idx + 1).trim()];
}
}
if(typeof(show_glassmask) !== "function"){
function show_glassmask(msg){
let glassmask = document.querySelector("#gr_glassmask");
if(!glassmask){
glassmask = document.createElement("div");
glassmask.id = "gr_glassmask";
document.body.appendChild(glassmask);
}
glassmask.className = "gr-glass-mask";
glassmask.innerHTML = `${msg}`;
}
}
if(typeof(create_uuid) !== "function"){
function create_uuid(){
try{
return crypto.randomUUID();
}catch(e){
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c =>{
const r = Math.random() * 16 | 0;
const v = c === "x" ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
}
}
//////////////////////
const bc = new BroadcastChannel("E2A-channel");
const tab_id = create_uuid();
bc.onmessage = (event)=>{
try{
const msg = event.data;
if(!msg || msg.senderId === tab_id){
return;
}
switch (msg.type){
case "check-existing":
bc.postMessage({ type: "already-open", senderId: tab_id });
break;
case "already-open":
tabs_open = true;
break;
case "new-tab-open":
show_glassmask(msg.text);
break;
}
}catch(e){
console.warn("bc.onmessage error:", e);
}
};
window.addEventListener("beforeunload", ()=>{
try{
const newStorage = JSON.parse(localStorage.getItem("data") || "{}");
if(newStorage.tab_id == window.tab_id || !newStorage.tab_id){
delete newStorage.tab_id;
delete newStorage.status;
newStorage.playback_time = Number(window.session_storage.playback_time);
newStorage.playback_volume = parseFloat(window.session_storage.playback_volume);
localStorage.setItem("data", JSON.stringify(newStorage));
}
}catch(e){
console.warn("Error updating status on unload:", e);
}
});
window.onElementAvailable("#gr_voice_player_hidden audio", (el)=>{
window.init_voice_player_hidden();
}, {once: false});
window.onElementAvailable("#gr_audiobook_player audio", (el)=>{
window.init_audiobook_player();
}, {once: false});
if (!window._fetch_patched) {
const originalFetch = window.fetch;
window._original_fetch = window._original_fetch || originalFetch;
window.fetch = async function(url, options) {
if (typeof url === "string" && url.includes("/upload") && options?.body instanceof FormData){
let has_files = false;
for(const [, value] of options.body.entries()){
if(value instanceof File && value.size > 0){
has_files = true;
break;
}
}
if(!has_files){
console.warn("Blocked empty folder upload");
return new Response(JSON.stringify([]), {
status: 200,
headers: {"Content-Type": "application/json"},
});
}
}
return window._original_fetch.apply(this, arguments);
};
window._fetch_patched = true;
}
try{
bc.postMessage({ type: "check-existing", senderId: tab_id });
setTimeout(()=>{
if(tabs_open){
bc.postMessage({
type: "new-tab-open",
text: "Session expired.<br/>You can close this window",
senderId: tab_id
});
}
}, 250);
}catch(e){
console.warn("bc.postMessage error:", e);
}
const currentStorage = localStorage.getItem("data");
if(currentStorage){
window.session_storage = JSON.parse(currentStorage);
window.session_storage.tab_id = tab_id;
if(window.session_storage.playback_volume === 0){
window.session_storage.playback_volume = 1.0;
}
}else{
window.session_storage = {};
window.session_storage.playback_time = 0;
window.session_storage.playback_volume = 1.0;
}
return window.session_storage;
}catch(e){
console.warn("gr_raed_data js error:", e);
}
return null;
}
'''.replace('__max_ebook_textarea_length__', str(max_ebook_textarea_length)),
outputs=[gr_restore_session],
)
app.unload(on_unload)
all_ips = get_all_ip_addresses()
msg = f'IPs available for connection:\n{all_ips}\nNote: 0.0.0.0 is not the IP to connect. Instead use an IP above to connect and port {interface_port}'
show_alert(None, {"type": "info", "msg": msg})
os.environ['no_proxy'] = ' ,'.join(all_ips)
return app
except Exception as e:
error = f'An unexpected error occurred: {e}'
exception_alert(None, error)
return None