drewThomasson commited on
Commit
69a5e7d
·
verified ·
1 Parent(s): 5894f30

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +729 -373
app.py CHANGED
@@ -1,5 +1,3 @@
1
- print("starting...")
2
-
3
  import os
4
  import shutil
5
  import subprocess
@@ -13,411 +11,769 @@ import ebooklib
13
  import bs4
14
  from ebooklib import epub
15
  from bs4 import BeautifulSoup
16
- from gradio import Progress
17
- import sys
18
  from nltk.tokenize import sent_tokenize
19
  import csv
20
- nltk.download('punkt_tab')
21
-
22
- # Ensure necessary models are downloaded
23
- # nltk.download('punkt')
24
-
25
- def is_folder_empty(folder_path):
26
- if os.path.exists(folder_path) and os.path.isdir(folder_path):
27
- return not os.listdir(folder_path)
28
- else:
29
- print(f"The path {folder_path} is not a valid folder.")
30
- return None
31
-
32
- def remove_folder_with_contents(folder_path):
33
- try:
34
- shutil.rmtree(folder_path)
35
- print(f"Successfully removed {folder_path} and all of its contents.")
36
- except Exception as e:
37
- print(f"Error removing {folder_path}: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
 
39
- def wipe_folder(folder_path):
 
40
  if not os.path.exists(folder_path):
41
- print(f"The folder {folder_path} does not exist.")
42
  return
 
43
  for item in os.listdir(folder_path):
44
  item_path = os.path.join(folder_path, item)
45
- if os.path.isfile(item_path):
46
- os.remove(item_path)
47
- elif os.path.isdir(item_path):
48
- shutil.rmtree(item_path)
49
-
50
- print(f"All contents wiped from {folder_path}.")
51
-
52
- def create_m4b_from_chapters(input_dir, ebook_file, output_dir):
53
- def sort_key(chapter_file):
54
- numbers = re.findall(r'\d+', chapter_file)
55
- return int(numbers[0]) if numbers else 0
56
-
57
- def extract_metadata_and_cover(ebook_path):
58
  try:
59
- cover_path = ebook_path.rsplit('.', 1)[0] + '.jpg'
60
- subprocess.run(['ebook-meta', ebook_path, '--get-cover', cover_path], check=True)
61
- if (os.path.exists(cover_path)):
62
- return cover_path
63
  except Exception as e:
64
- print(f"Error extracting eBook metadata or cover: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  return None
66
 
67
- def combine_wav_files(chapter_files, output_path):
68
- combined_audio = AudioSegment.empty()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
 
70
- for chapter_file in chapter_files:
71
- audio_segment = AudioSegment.from_wav(chapter_file)
72
- combined_audio += audio_segment
73
- combined_audio.export(output_path, format='wav')
74
- print(f"Combined audio saved to {output_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- def generate_ffmpeg_metadata(chapter_files, metadata_file):
77
- with open(metadata_file, 'w') as file:
78
- file.write(';FFMETADATA1\n')
79
- start_time = 0
80
- for index, chapter_file in enumerate(chapter_files):
81
- duration_ms = len(AudioSegment.from_wav(chapter_file))
82
- file.write(f'[CHAPTER]\nTIMEBASE=1/1000\nSTART={start_time}\n')
83
- file.write(f'END={start_time + duration_ms}\ntitle=Chapter {index + 1}\n')
84
- start_time += duration_ms
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
 
86
- def create_m4b(combined_wav, metadata_file, cover_image, output_m4b):
87
- os.makedirs(os.path.dirname(output_m4b), exist_ok=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  ffmpeg_cmd = ['ffmpeg', '-i', combined_wav, '-i', metadata_file]
90
- if cover_image:
 
91
  ffmpeg_cmd += ['-i', cover_image, '-map', '0:a', '-map', '2:v']
92
  ffmpeg_cmd += ['-c:v', 'png', '-disposition:v', 'attached_pic']
93
  else:
94
  ffmpeg_cmd += ['-map', '0:a']
95
 
96
- ffmpeg_cmd += ['-map_metadata', '1', '-c:a', 'aac', '-b:a', '192k']
97
- ffmpeg_cmd += [output_m4b]
98
-
99
  try:
100
- subprocess.run(ffmpeg_cmd, check=True)
 
101
  except subprocess.CalledProcessError as e:
102
- print("Metadata adding didn't work trying with only the cover art and no Metadata.")
103
- ffmpeg_cmd = ""
104
- ffmpeg_cmd = ['ffmpeg', '-i', combined_wav]
105
- if cover_image:
106
- print("Found Cover art")
107
- ffmpeg_cmd += ['-i', cover_image, '-map', '0:a', '-map', '2:v']
108
- ffmpeg_cmd += ['-c:v', 'png', '-disposition:v', 'attached_pic']
109
- else:
110
- ffmpeg_cmd += ['-map', '0:a']
111
-
112
- ffmpeg_cmd += ['-c:a', 'aac', '-b:a', '192k']
113
  try:
114
- subprocess.run(ffmpeg_cmd, check=True)
 
115
  except subprocess.CalledProcessError as e:
116
- print("Cover art adding didn't work, trying without any metadata or cover art.")
117
- ffmpeg_cmd = ""
118
- ffmpeg_cmd = ['ffmpeg', '-i', combined_wav]
119
- ffmpeg_cmd += ['-map', '0:a']
120
- ffmpeg_cmd += ['-c:a', 'aac', '-b:a', '192k']
121
- try:
122
- subprocess.run(ffmpeg_cmd, check=True)
123
- except subprocess.CalledProcessError:
124
- print("Failed to create M4B file. Trying to at least save the audio.")
125
- try:
126
- ffmpeg_cmd = ['ffmpeg', '-i', combined_wav, '-c:a', 'aac', '-b:a', '192k', output_m4b]
127
- subprocess.run(ffmpeg_cmd, check=True)
128
- except subprocess.CalledProcessError as e:
129
- print(f"Final attempt failed: {e}")
130
- print(f"M4B file created successfully at {output_m4b}")
131
-
132
-
133
- chapter_files = sorted([os.path.join(input_dir, f) for f in os.listdir(input_dir) if f.endswith('.wav')], key=sort_key)
134
- temp_dir = tempfile.gettempdir()
135
- temp_combined_wav = os.path.join(temp_dir, 'combined.wav')
136
- metadata_file = os.path.join(temp_dir, 'metadata.txt')
137
- cover_image = extract_metadata_and_cover(ebook_file)
138
- output_m4b = os.path.join(output_dir, os.path.splitext(os.path.basename(ebook_file))[0] + '.m4b')
139
-
140
- combine_wav_files(chapter_files, temp_combined_wav)
141
- generate_ffmpeg_metadata(chapter_files, metadata_file)
142
- create_m4b(temp_combined_wav, metadata_file, cover_image, output_m4b)
143
-
144
- if os.path.exists(temp_combined_wav):
145
- os.remove(temp_combined_wav)
146
- if os.path.exists(metadata_file):
147
- os.remove(metadata_file)
148
- if cover_image and os.path.exists(cover_image):
149
- os.remove(cover_image)
150
-
151
- def create_chapter_labeled_book(ebook_file_path):
152
- def ensure_directory(directory_path):
153
- if not os.path.exists(directory_path):
154
- os.makedirs(directory_path)
155
- print(f"Created directory: {directory_path}")
156
-
157
- ensure_directory(os.path.join(".", 'Working_files', 'Book'))
158
-
159
- def convert_to_epub(input_path, output_path):
160
- try:
161
- subprocess.run(['ebook-convert', input_path, output_path], check=True)
162
- except subprocess.CalledProcessError as e:
163
- print(f"An error occurred while converting the eBook: {e}")
164
- return False
165
- return True
166
-
167
- def save_chapters_as_text(epub_path):
168
- directory = os.path.join(".", "Working_files", "temp_ebook")
169
- ensure_directory(directory)
170
-
171
- book = epub.read_epub(epub_path)
172
-
173
- previous_filename = ''
174
- chapter_counter = 0
175
-
176
- for item in book.get_items():
177
- if item.get_type() == ebooklib.ITEM_DOCUMENT:
178
- soup = BeautifulSoup(item.get_content(), 'html.parser')
179
- text = soup.get_text()
180
-
181
- if text.strip():
182
- if len(text) < 2300 and previous_filename:
183
- with open(previous_filename, 'a', encoding='utf-8') as file:
184
- file.write('\n' + text)
185
- else:
186
- previous_filename = os.path.join(directory, f"chapter_{chapter_counter}.txt")
187
- chapter_counter += 1
188
- with open(previous_filename, 'w', encoding='utf-8') as file:
189
- file.write(text)
190
- print(f"Saved chapter: {previous_filename}")
191
-
192
- input_ebook = ebook_file_path
193
- output_epub = os.path.join(".", "Working_files", "temp.epub")
194
-
195
- if os.path.exists(output_epub):
196
- os.remove(output_epub)
197
- print(f"File {output_epub} has been removed.")
198
  else:
199
- print(f"The file {output_epub} does not exist.")
200
-
201
- if convert_to_epub(input_ebook, output_epub):
202
- save_chapters_as_text(output_epub)
203
-
204
- # nltk.download('punkt')
205
-
206
- def process_chapter_files(folder_path, output_csv):
207
- with open(output_csv, 'w', newline='', encoding='utf-8') as csvfile:
208
- writer = csv.writer(csvfile)
209
- writer.writerow(['Text', 'Start Location', 'End Location', 'Is Quote', 'Speaker', 'Chapter'])
210
-
211
- chapter_files = sorted(os.listdir(folder_path), key=lambda x: int(x.split('_')[1].split('.')[0]))
212
- for filename in chapter_files:
213
- if filename.startswith('chapter_') and filename.endswith('.txt'):
214
- chapter_number = int(filename.split('_')[1].split('.')[0])
215
- file_path = os.path.join(folder_path, filename)
216
-
217
- try:
218
- with open(file_path, 'r', encoding='utf-8') as file:
219
- text = file.read()
220
- if text:
221
- text = "NEWCHAPTERABC" + text
222
- sentences = nltk.tokenize.sent_tokenize(text)
223
- for sentence in sentences:
224
- start_location = text.find(sentence)
225
- end_location = start_location + len(sentence)
226
- writer.writerow([sentence, start_location, end_location, 'True', 'Narrator', chapter_number])
227
- except Exception as e:
228
- print(f"Error processing file {filename}: {e}")
229
-
230
- folder_path = os.path.join(".", "Working_files", "temp_ebook")
231
- output_csv = os.path.join(".", "Working_files", "Book", "Other_book.csv")
232
-
233
- process_chapter_files(folder_path, output_csv)
234
-
235
- def sort_key(filename):
236
- match = re.search(r'chapter_(\d+)\.txt', filename)
237
- return int(match.group(1)) if match else 0
238
-
239
- def combine_chapters(input_folder, output_file):
240
- os.makedirs(os.path.dirname(output_file), exist_ok=True)
241
-
242
- files = [f for f in os.listdir(input_folder) if f.endswith('.txt')]
243
- sorted_files = sorted(files, key=sort_key)
244
-
245
- with open(output_file, 'w', encoding='utf-8') as outfile:
246
- for i, filename in enumerate(sorted_files):
247
- with open(os.path.join(input_folder, filename), 'r', encoding='utf-8') as infile:
248
- outfile.write(infile.read())
249
- if i < len(sorted_files) - 1:
250
- outfile.write("\nNEWCHAPTERABC\n")
251
-
252
- input_folder = os.path.join(".", 'Working_files', 'temp_ebook')
253
- output_file = os.path.join(".", 'Working_files', 'Book', 'Chapter_Book.txt')
254
-
255
- combine_chapters(input_folder, output_file)
256
-
257
- ensure_directory(os.path.join(".", "Working_files", "Book"))
258
-
259
- def sanitize_sentence(sentence):
260
- # Replace or remove problematic characters that could cause issues in any language
261
- sanitized = sentence.replace('--', ' ').replace('"', '').replace("'", "")
262
- return sanitized
263
-
264
- def convert_chapters_to_audio_espeak(chapters_dir, output_audio_dir, speed="170", pitch="50", voice="en"):
265
- if not os.path.exists(output_audio_dir):
266
- os.makedirs(output_audio_dir)
267
-
268
- for chapter_file in sorted(os.listdir(chapters_dir)):
269
- if chapter_file.endswith('.txt'):
270
- match = re.search(r"chapter_(\d+).txt", chapter_file)
271
- if match:
272
- chapter_num = int(match.group(1))
273
- else:
274
- print(f"Skipping file {chapter_file} as it does not match the expected format.")
275
- continue
276
-
277
- chapter_path = os.path.join(chapters_dir, chapter_file)
278
- output_file_name = f"audio_chapter_{chapter_num}.wav"
279
- output_file_path = os.path.join(output_audio_dir, output_file_name)
280
-
281
- with open(chapter_path, 'r', encoding='utf-8') as file:
282
- chapter_text = file.read()
283
- sentences = nltk.tokenize.sent_tokenize(chapter_text)
284
- combined_audio = AudioSegment.empty()
285
-
286
- for sentence in tqdm(sentences, desc=f"Chapter {chapter_num}"):
287
- success = False
288
- with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as temp_wav:
289
- try:
290
- subprocess.run(["espeak-ng", "-v", voice, "-w", temp_wav.name, f"-s{speed}", f"-p{pitch}", sentence], check=True)
291
- success = True
292
- except subprocess.CalledProcessError:
293
- # If it fails, try with the sanitized sentence
294
- sanitized_sentence = sanitize_sentence(sentence)
295
- try:
296
- subprocess.run(["espeak-ng", "-v", voice, "-w", temp_wav.name, f"-s{speed}", f"-p{pitch}", sanitized_sentence], check=True)
297
- success = True
298
- print(f"Sanitized sentence used for: {sentence}")
299
- except subprocess.CalledProcessError as e:
300
- print(f"Failed to convert sentence to audio: {sentence}")
301
- print(f"Error: {e}")
302
-
303
- if success and os.path.getsize(temp_wav.name) > 0:
304
- combined_audio += AudioSegment.from_wav(temp_wav.name)
305
- else:
306
- print(f"Skipping sentence due to failure or empty WAV: {sentence}")
307
- os.remove(temp_wav.name)
308
-
309
- combined_audio.export(output_file_path, format='wav')
310
- print(f"Converted chapter {chapter_num} to audio.")
311
-
312
-
313
- def convert_ebook_to_audio(ebook_file, speed, pitch, voice, progress=gr.Progress()):
314
- ebook_file_path = ebook_file.name
315
- working_files = os.path.join(".", "Working_files", "temp_ebook")
316
- full_folder_working_files = os.path.join(".", "Working_files")
317
- chapters_directory = os.path.join(".", "Working_files", "temp_ebook")
318
- output_audio_directory = os.path.join(".", 'Chapter_wav_files')
319
- remove_folder_with_contents(full_folder_working_files)
320
- remove_folder_with_contents(output_audio_directory)
321
-
322
- try:
323
- progress(0.1, desc="Creating chapter-labeled book")
324
- except Exception as e:
325
- print(f"Error updating progress: {e}")
326
-
327
- create_chapter_labeled_book(ebook_file_path)
328
- audiobook_output_path = os.path.join(".", "Audiobooks")
329
-
330
- try:
331
- progress(0.3, desc="Converting chapters to audio")
332
- except Exception as e:
333
- print(f"Error updating progress: {e}")
334
-
335
- convert_chapters_to_audio_espeak(chapters_directory, output_audio_directory, speed, pitch, voice.split()[0])
336
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
  try:
338
- progress(0.9, desc="Creating M4B from chapters")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
  except Exception as e:
340
- print(f"Error updating progress: {e}")
341
-
342
- create_m4b_from_chapters(output_audio_directory, ebook_file_path, audiobook_output_path)
343
-
344
- m4b_filename = os.path.splitext(os.path.basename(ebook_file_path))[0] + '.m4b'
345
- m4b_filepath = os.path.join(audiobook_output_path, m4b_filename)
346
 
 
 
 
347
  try:
348
- progress(1.0, desc="Conversion complete")
 
 
 
 
 
 
 
 
 
 
 
 
349
  except Exception as e:
350
- print(f"Error updating progress: {e}")
351
- print(f"Audiobook created at {m4b_filepath}")
352
- return f"Audiobook created at {m4b_filepath}", m4b_filepath
353
-
354
- def list_audiobook_files(audiobook_folder):
 
 
 
 
355
  files = []
356
- for filename in os.listdir(audiobook_folder):
357
  if filename.endswith('.m4b'):
358
- files.append(os.path.join(audiobook_folder, filename))
359
- return files
360
-
361
- def download_audiobooks():
362
- audiobook_output_path = os.path.join(".", "Audiobooks")
363
- return list_audiobook_files(audiobook_output_path)
364
-
365
- def get_available_voices():
366
- result = subprocess.run(['espeak-ng', '--voices'], stdout=subprocess.PIPE, text=True)
367
- lines = result.stdout.splitlines()[1:] # Skip the header line
368
- voices = []
369
- for line in lines:
370
- parts = line.split()
371
- if len(parts) > 1:
372
- voice_name = parts[1]
373
- description = ' '.join(parts[2:])
374
- voices.append((voice_name, description))
375
- return voices
376
-
377
- theme = gr.themes.Soft(
378
- primary_hue="blue",
379
- secondary_hue="blue",
380
- neutral_hue="blue",
381
- text_size=gr.themes.sizes.text_md,
382
- )
383
-
384
- # Gradio UI setup
385
- with gr.Blocks(theme=theme) as demo:
386
- gr.Markdown(
387
- """
388
- # eBook to Audiobook Converter
389
 
390
- Convert your eBooks into audiobooks using eSpeak-NG.
391
-
392
- This interface is based on [ebook2audiobookEspeak](https://github.com/DrewThomasson/ebook2audiobookEspeak).
393
- """
394
- )
395
-
396
- with gr.Row():
397
- with gr.Column(scale=3):
398
- ebook_file = gr.File(label="eBook File")
399
- speed = gr.Slider(minimum=80, maximum=450, value=170, step=1, label="Speed")
400
- pitch = gr.Slider(minimum=0, maximum=99, value=50, step=1, label="Pitch")
401
- voices = get_available_voices()
402
- voice_choices = [f"{voice} ({desc})" for voice, desc in voices]
403
- voice_dropdown = gr.Dropdown(choices=voice_choices, label="Select Voice", value=voice_choices[0])
404
-
405
- convert_btn = gr.Button("Convert to Audiobook", variant="primary")
406
- output = gr.Textbox(label="Conversion Status")
407
- audio_player = gr.Audio(label="Audiobook Player", type="filepath")
408
- download_btn = gr.Button("Download Audiobook Files")
409
- download_files = gr.File(label="Download Files", interactive=False)
410
-
411
- convert_btn.click(
412
- convert_ebook_to_audio,
413
- inputs=[ebook_file, speed, pitch, voice_dropdown],
414
- outputs=[output, audio_player]
415
- )
416
-
417
- download_btn.click(
418
- download_audiobooks,
419
- outputs=[download_files]
420
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
421
 
422
- #demo.launch(share=True)
423
- demo.launch() # Removing share = True cause huggingface space
 
 
 
1
  import os
2
  import shutil
3
  import subprocess
 
11
  import bs4
12
  from ebooklib import epub
13
  from bs4 import BeautifulSoup
 
 
14
  from nltk.tokenize import sent_tokenize
15
  import csv
16
+ import argparse
17
+ import threading
18
+ import logging
19
+ from datetime import datetime
20
+ import time
21
+ import json
22
+
23
+ # Setup logging
24
+ logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
27
+ handlers=[
28
+ logging.FileHandler("audiobook_converter.log"),
29
+ logging.StreamHandler()
30
+ ]
31
+ )
32
+ logger = logging.getLogger("audiobook_converter")
33
+
34
+ # Download NLTK resources if not already present
35
+ try:
36
+ nltk.data.find('tokenizers/punkt')
37
+ except LookupError:
38
+ logger.info("Downloading NLTK punkt tokenizer...")
39
+ nltk.download('punkt', quiet=True)
40
+
41
+ # Utility functions for directory management
42
+ def ensure_directory(directory_path):
43
+ """Create directory if it doesn't exist."""
44
+ if not os.path.exists(directory_path):
45
+ os.makedirs(directory_path)
46
+ logger.info(f"Created directory: {directory_path}")
47
+ return directory_path
48
+
49
+ def remove_directory(folder_path):
50
+ """Remove directory and all its contents."""
51
+ if os.path.exists(folder_path):
52
+ try:
53
+ shutil.rmtree(folder_path)
54
+ logger.info(f"Removed directory: {folder_path}")
55
+ except Exception as e:
56
+ logger.error(f"Error removing directory {folder_path}: {e}")
57
 
58
+ def wipe_directory(folder_path):
59
+ """Remove all contents of a directory without deleting the directory itself."""
60
  if not os.path.exists(folder_path):
61
+ logger.warning(f"Directory does not exist: {folder_path}")
62
  return
63
+
64
  for item in os.listdir(folder_path):
65
  item_path = os.path.join(folder_path, item)
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  try:
67
+ if os.path.isfile(item_path):
68
+ os.remove(item_path)
69
+ elif os.path.isdir(item_path):
70
+ shutil.rmtree(item_path)
71
  except Exception as e:
72
+ logger.error(f"Error removing {item_path}: {e}")
73
+
74
+ logger.info(f"Wiped contents of directory: {folder_path}")
75
+
76
+ # Text processing functions
77
+ def clean_text(text):
78
+ """Clean up text by removing unnecessary whitespace and fixing common issues."""
79
+ # Replace multiple newlines with a single one
80
+ text = re.sub(r'\n\s*\n', '\n\n', text)
81
+ # Replace multiple spaces with a single space
82
+ text = re.sub(r' +', ' ', text)
83
+ # Fix broken sentences (e.g., "word . Next" -> "word. Next")
84
+ text = re.sub(r'(\w) \. (\w)', r'\1. \2', text)
85
+ return text.strip()
86
+
87
+ def split_into_natural_sentences(text):
88
+ """Split text into natural sentences using NLTK with additional rules."""
89
+ # Initial sentence splitting
90
+ sentences = sent_tokenize(text)
91
+
92
+ # Post-process sentences to handle special cases
93
+ processed_sentences = []
94
+ buffer = ""
95
+
96
+ for sent in sentences:
97
+ # Handle quotes that span multiple sentences but should be treated as one
98
+ if buffer:
99
+ current = buffer + " " + sent
100
+ buffer = ""
101
+ else:
102
+ current = sent
103
+
104
+ # Check for unbalanced quotes, which might indicate a continuing sentence
105
+ if current.count('"') % 2 != 0 or current.count("'") % 2 != 0:
106
+ buffer = current
107
+ continue
108
+
109
+ # Check if sentence ends with abbreviation or is too short (might be a continuation)
110
+ if len(current) < 20 and not re.search(r'[.!?]\s*$', current):
111
+ buffer = current
112
+ continue
113
+
114
+ processed_sentences.append(current)
115
+
116
+ # Add any remaining buffer
117
+ if buffer:
118
+ processed_sentences.append(buffer)
119
+
120
+ return processed_sentences
121
+
122
+ # eBook processing functions
123
+ def extract_metadata_and_cover(ebook_path):
124
+ """Extract metadata and cover image from an ebook."""
125
+ cover_path = ebook_path.rsplit('.', 1)[0] + '.jpg'
126
+
127
+ try:
128
+ subprocess.run(['ebook-meta', ebook_path, '--get-cover', cover_path],
129
+ check=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
130
+
131
+ # Check if cover was extracted
132
+ if os.path.exists(cover_path) and os.path.getsize(cover_path) > 0:
133
+ logger.info(f"Cover extracted to: {cover_path}")
134
+ return cover_path
135
+ else:
136
+ logger.warning("Cover extraction failed or resulted in empty file")
137
+ return None
138
+ except Exception as e:
139
+ logger.error(f"Error extracting eBook metadata: {e}")
140
  return None
141
 
142
+ def convert_to_epub(input_path, output_path):
143
+ """Convert any ebook format to EPUB using Calibre."""
144
+ try:
145
+ logger.info(f"Converting {input_path} to EPUB format...")
146
+ result = subprocess.run(
147
+ ['ebook-convert', input_path, output_path, '--enable-heuristics'],
148
+ check=True,
149
+ stderr=subprocess.PIPE,
150
+ stdout=subprocess.PIPE
151
+ )
152
+ logger.info(f"Successfully converted to EPUB: {output_path}")
153
+ return True
154
+ except subprocess.CalledProcessError as e:
155
+ logger.error(f"Error converting to EPUB: {e}")
156
+ logger.error(f"STDERR: {e.stderr.decode('utf-8', errors='replace')}")
157
+ return False
158
 
159
+ def convert_to_text(input_path, output_path):
160
+ """Convert any ebook format directly to TXT using Calibre."""
161
+ try:
162
+ logger.info(f"Converting {input_path} to TXT format...")
163
+ result = subprocess.run(
164
+ ['ebook-convert', input_path, output_path,
165
+ '--enable-heuristics',
166
+ '--chapter-mark=pagebreak',
167
+ '--paragraph-type=unformatted'],
168
+ check=True,
169
+ stderr=subprocess.PIPE,
170
+ stdout=subprocess.PIPE
171
+ )
172
+ logger.info(f"Successfully converted to TXT: {output_path}")
173
+ return True
174
+ except subprocess.CalledProcessError as e:
175
+ logger.error(f"Error converting to TXT: {e}")
176
+ logger.error(f"STDERR: {e.stderr.decode('utf-8', errors='replace')}")
177
+ return False
178
+
179
+ def detect_chapters_from_text(text_path):
180
+ """Detect chapters in a text file based on common patterns."""
181
+ with open(text_path, 'r', encoding='utf-8', errors='replace') as f:
182
+ content = f.read()
183
+
184
+ # Different chapter detection patterns
185
+ chapter_patterns = [
186
+ r'(?:^|\n)(?:\s*)(?:Chapter|CHAPTER)\s+[0-9IVXLCDM]+(?:\s*:|\.\s|\s)(.+?)(?=\n)',
187
+ r'(?:^|\n)(?:\s*)(?:Chapter|CHAPTER)\s+[0-9IVXLCDM]+(?:\s*:|\.\s|\s)',
188
+ r'(?:^|\n)(?:\s*)(?:[0-9]+|[IVXLCDM]+)\.?\s+(.+?)(?=\n)',
189
+ r'(?:^|\n)(?:\s*)\* \* \*(?:\s*\n)',
190
+ r'(?:^|\n)(?:\s*)[-—]\s*(\d+\s*[-—]|\w+)(?:\s*\n)'
191
+ ]
192
+
193
+ chapters = []
194
+
195
+ for pattern in chapter_patterns:
196
+ matches = re.finditer(pattern, content, re.MULTILINE)
197
+ positions = [(m.start(), m.group()) for m in matches]
198
+ if positions:
199
+ # If we found chapters with this pattern, add to our list
200
+ for i, (pos, title) in enumerate(positions):
201
+ end_pos = positions[i+1][0] if i < len(positions)-1 else len(content)
202
+ chapter_text = content[pos:end_pos].strip()
203
+ chapters.append((i+1, clean_text(chapter_text)))
204
+
205
+ # If we found chapters with this pattern, stop looking
206
+ if len(chapters) > 3: # Require at least 3 chapters for a valid detection
207
+ break
208
+
209
+ # If no chapters detected, create artificial chapters based on length
210
+ if not chapters:
211
+ logger.info("No clear chapter markers found, creating artificial chapters")
212
+ chunk_size = min(10000, max(5000, len(content) // 20)) # Aim for ~20 chapters
213
+
214
+ # Split content into chunks
215
+ chunks = [content[i:i+chunk_size] for i in range(0, len(content), chunk_size)]
216
+ chapters = [(i+1, clean_text(chunk)) for i, chunk in enumerate(chunks)]
217
+
218
+ return chapters
219
+
220
+ def save_chapters_as_files(chapters, output_dir):
221
+ """Save detected chapters as individual text files."""
222
+ ensure_directory(output_dir)
223
+
224
+ for chapter_num, chapter_text in chapters:
225
+ filename = f"chapter_{chapter_num:03d}.txt"
226
+ filepath = os.path.join(output_dir, filename)
227
+
228
+ with open(filepath, 'w', encoding='utf-8') as f:
229
+ f.write(chapter_text)
230
+
231
+ logger.info(f"Saved chapter {chapter_num} to {filename}")
232
+
233
+ return len(chapters)
234
 
235
+ def process_ebook_to_chapters(ebook_path, chapters_dir):
236
+ """Process ebook into chapter text files."""
237
+ ensure_directory(chapters_dir)
238
+
239
+ # Create temp directory for intermediate files
240
+ temp_dir = os.path.join(os.path.dirname(chapters_dir), "temp")
241
+ ensure_directory(temp_dir)
242
+
243
+ # Determine file paths
244
+ temp_epub = os.path.join(temp_dir, "converted.epub")
245
+ temp_txt = os.path.join(temp_dir, "converted.txt")
246
+
247
+ # First try direct conversion to text
248
+ if convert_to_text(ebook_path, temp_txt):
249
+ chapters = detect_chapters_from_text(temp_txt)
250
+ num_chapters = save_chapters_as_files(chapters, chapters_dir)
251
+ logger.info(f"Processed {num_chapters} chapters from text conversion")
252
+ return num_chapters
253
+
254
+ # If that fails, try EPUB conversion first
255
+ logger.info("Direct text conversion failed, trying via EPUB...")
256
+ if convert_to_epub(ebook_path, temp_epub) and convert_to_text(temp_epub, temp_txt):
257
+ chapters = detect_chapters_from_text(temp_txt)
258
+ num_chapters = save_chapters_as_files(chapters, chapters_dir)
259
+ logger.info(f"Processed {num_chapters} chapters via EPUB conversion")
260
+ return num_chapters
261
+
262
+ # If both methods fail, return 0 chapters
263
+ logger.error("Failed to process ebook into chapters")
264
+ return 0
265
+
266
+ # Audio processing functions
267
+ def sanitize_for_espeak(text):
268
+ """Sanitize text for espeak compatibility."""
269
+ # Replace problematic characters
270
+ text = re.sub(r'[–—]', '-', text) # Em/en dashes to hyphens
271
+ text = re.sub(r'["""]', '"', text) # Smart quotes to regular quotes
272
+ text = re.sub(r'[''`]', "'", text) # Smart apostrophes to regular apostrophes
273
+ text = re.sub(r'[…]', '...', text) # Ellipsis character to three dots
274
+
275
+ # Remove or replace other problematic characters
276
+ text = re.sub(r'[<>|]', ' ', text)
277
+ text = re.sub(r'[\x00-\x1F\x7F]', '', text) # Control characters
278
+
279
+ return text
280
 
281
+ def convert_text_to_speech(text, output_path, voice="en", speed=170, pitch=50, gap=5):
282
+ """Convert text to speech using espeak-ng."""
283
+ try:
284
+ # Create a temporary file for the text
285
+ with tempfile.NamedTemporaryFile(mode='w+', encoding='utf-8', suffix='.txt', delete=False) as temp_file:
286
+ temp_file.write(sanitize_for_espeak(text))
287
+ temp_file_path = temp_file.name
288
+
289
+ # Call espeak-ng with the text file
290
+ subprocess.run([
291
+ "espeak-ng",
292
+ "-v", voice,
293
+ "-f", temp_file_path,
294
+ "-w", output_path,
295
+ f"-s{speed}",
296
+ f"-p{pitch}",
297
+ f"-g{gap}"
298
+ ], check=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
299
+
300
+ # Remove the temporary file
301
+ os.unlink(temp_file_path)
302
 
303
+ # Verify the output file exists and has content
304
+ if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
305
+ return True
306
+ else:
307
+ logger.error(f"Speech synthesis produced empty file: {output_path}")
308
+ return False
309
+
310
+ except subprocess.CalledProcessError as e:
311
+ logger.error(f"Error in speech synthesis: {e}")
312
+ logger.error(f"STDERR: {e.stderr.decode('utf-8', errors='replace')}")
313
+ return False
314
+ except Exception as e:
315
+ logger.error(f"Unexpected error in speech synthesis: {e}")
316
+ return False
317
+
318
+ def convert_chapters_to_audio(chapters_dir, output_audio_dir, voice="en", speed=170, pitch=50, gap=5, progress_callback=None):
319
+ """Convert all chapter text files to audio files."""
320
+ ensure_directory(output_audio_dir)
321
+
322
+ # Get all chapter files
323
+ chapter_files = [f for f in os.listdir(chapters_dir) if f.startswith('chapter_') and f.endswith('.txt')]
324
+ chapter_files.sort(key=lambda f: int(re.search(r'chapter_(\d+)', f).group(1)))
325
+
326
+ total_chapters = len(chapter_files)
327
+ processed_chapters = 0
328
+ failed_chapters = 0
329
+
330
+ for chapter_file in chapter_files:
331
+ chapter_path = os.path.join(chapters_dir, chapter_file)
332
+ chapter_num = int(re.search(r'chapter_(\d+)', chapter_file).group(1))
333
+ output_file = os.path.join(output_audio_dir, f"audio_chapter_{chapter_num:03d}.wav")
334
+
335
+ logger.info(f"Converting chapter {chapter_num} to audio...")
336
+
337
+ # Read the chapter text
338
+ with open(chapter_path, 'r', encoding='utf-8', errors='replace') as f:
339
+ chapter_text = f.read()
340
+
341
+ # Split into sentences for better processing
342
+ sentences = split_into_natural_sentences(chapter_text)
343
+ combined_audio = AudioSegment.empty()
344
+
345
+ # Process each sentence
346
+ for i, sentence in enumerate(sentences):
347
+ if not sentence.strip():
348
+ continue
349
+
350
+ temp_wav = tempfile.NamedTemporaryFile(suffix=".wav", delete=False)
351
+ temp_wav.close()
352
+
353
+ try:
354
+ # Convert sentence to speech
355
+ if convert_text_to_speech(sentence, temp_wav.name, voice, speed, pitch, gap):
356
+ sentence_audio = AudioSegment.from_wav(temp_wav.name)
357
+ combined_audio += sentence_audio
358
+
359
+ # Add a small pause between sentences
360
+ combined_audio += AudioSegment.silent(duration=50)
361
+ else:
362
+ logger.warning(f"Failed to convert sentence in chapter {chapter_num}: {sentence[:50]}...")
363
+ except Exception as e:
364
+ logger.error(f"Error processing sentence: {e}")
365
+ finally:
366
+ # Clean up temporary file
367
+ if os.path.exists(temp_wav.name):
368
+ os.unlink(temp_wav.name)
369
+
370
+ # Export the combined audio for this chapter
371
+ if len(combined_audio) > 0:
372
+ combined_audio.export(output_file, format='wav')
373
+ logger.info(f"Saved audio for chapter {chapter_num}")
374
+ processed_chapters += 1
375
+ else:
376
+ logger.error(f"No audio generated for chapter {chapter_num}")
377
+ failed_chapters += 1
378
+
379
+ # Update progress
380
+ if progress_callback:
381
+ progress_callback((processed_chapters + failed_chapters) / total_chapters,
382
+ f"Processed {processed_chapters}/{total_chapters} chapters")
383
+
384
+ return processed_chapters, failed_chapters
385
+
386
+ def create_m4b_audiobook(input_audio_dir, ebook_path, output_dir, title=None, author=None, progress_callback=None):
387
+ """Create M4B audiobook from chapter audio files."""
388
+ ensure_directory(output_dir)
389
+
390
+ # Extract base name from ebook path
391
+ base_name = os.path.splitext(os.path.basename(ebook_path))[0]
392
+ output_m4b = os.path.join(output_dir, f"{base_name}.m4b")
393
+
394
+ # Get chapter files
395
+ chapter_files = [f for f in os.listdir(input_audio_dir) if f.startswith('audio_chapter_') and f.endswith('.wav')]
396
+ chapter_files.sort(key=lambda f: int(re.search(r'audio_chapter_(\d+)', f).group(1)))
397
+
398
+ if not chapter_files:
399
+ logger.error("No audio chapter files found")
400
+ return None
401
+
402
+ # Create temporary directory
403
+ temp_dir = tempfile.mkdtemp()
404
+
405
+ try:
406
+ # Combine audio files
407
+ combined_wav = os.path.join(temp_dir, "combined.wav")
408
+ combined_audio = AudioSegment.empty()
409
+
410
+ chapter_positions = []
411
+ current_position = 0
412
+
413
+ for i, chapter_file in enumerate(chapter_files):
414
+ chapter_path = os.path.join(input_audio_dir, chapter_file)
415
+ logger.info(f"Adding chapter {i+1}/{len(chapter_files)} to audiobook")
416
+
417
+ try:
418
+ audio = AudioSegment.from_wav(chapter_path)
419
+ chapter_positions.append((current_position, len(audio), f"Chapter {i+1}"))
420
+ combined_audio += audio
421
+ current_position += len(audio)
422
+
423
+ # Add silence between chapters
424
+ if i < len(chapter_files) - 1:
425
+ silence = AudioSegment.silent(duration=1000) # 1 second
426
+ combined_audio += silence
427
+ current_position += 1000
428
+ except Exception as e:
429
+ logger.error(f"Error processing audio file {chapter_file}: {e}")
430
+
431
+ # Export combined audio
432
+ combined_audio.export(combined_wav, format="wav")
433
+
434
+ # Extract cover
435
+ cover_image = extract_metadata_and_cover(ebook_path)
436
+
437
+ # Create metadata file
438
+ metadata_file = os.path.join(temp_dir, "metadata.txt")
439
+ with open(metadata_file, 'w') as f:
440
+ f.write(';FFMETADATA1\n')
441
+ if title:
442
+ f.write(f"title={title}\n")
443
+ if author:
444
+ f.write(f"artist={author}\n")
445
+
446
+ # Add chapters
447
+ for i, (start, duration, title) in enumerate(chapter_positions):
448
+ f.write(f'[CHAPTER]\nTIMEBASE=1/1000\nSTART={start}\n')
449
+ f.write(f'END={start + duration}\ntitle={title}\n')
450
+
451
+ # Create M4B file with ffmpeg
452
  ffmpeg_cmd = ['ffmpeg', '-i', combined_wav, '-i', metadata_file]
453
+
454
+ if cover_image and os.path.exists(cover_image):
455
  ffmpeg_cmd += ['-i', cover_image, '-map', '0:a', '-map', '2:v']
456
  ffmpeg_cmd += ['-c:v', 'png', '-disposition:v', 'attached_pic']
457
  else:
458
  ffmpeg_cmd += ['-map', '0:a']
459
 
460
+ ffmpeg_cmd += ['-map_metadata', '1', '-c:a', 'aac', '-b:a', '192k', output_m4b]
461
+
462
+ # Execute ffmpeg command
463
  try:
464
+ subprocess.run(ffmpeg_cmd, check=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
465
+ logger.info(f"M4B file created successfully: {output_m4b}")
466
  except subprocess.CalledProcessError as e:
467
+ logger.error(f"Error creating M4B file: {e}")
468
+ logger.error(f"STDERR: {e.stderr.decode('utf-8', errors='replace')}")
469
+
470
+ # Try simplified approach
471
+ logger.info("Trying simplified M4B creation...")
472
+ simple_cmd = ['ffmpeg', '-i', combined_wav, '-c:a', 'aac', '-b:a', '192k', output_m4b]
 
 
 
 
 
473
  try:
474
+ subprocess.run(simple_cmd, check=True)
475
+ logger.info(f"M4B file created with simplified method: {output_m4b}")
476
  except subprocess.CalledProcessError as e:
477
+ logger.error(f"Simplified M4B creation also failed: {e}")
478
+ return None
479
+
480
+ finally:
481
+ # Clean up temporary directory
482
+ shutil.rmtree(temp_dir)
483
+
484
+ if os.path.exists(output_m4b) and os.path.getsize(output_m4b) > 0:
485
+ return output_m4b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  else:
487
+ logger.error("M4B file was not created or is empty")
488
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
 
490
+ # Main conversion function
491
+ def convert_ebook_to_audiobook(ebook_file, speed, pitch, voice, gap, progress=None):
492
+ """Main function to convert ebook to audiobook."""
493
+ start_time = time.time()
494
+
495
+ # Initialize directories
496
+ base_dir = os.path.abspath(os.path.dirname(__file__))
497
+ working_dir = os.path.join(base_dir, "Working_files")
498
+ chapters_dir = os.path.join(working_dir, "chapters")
499
+ audio_dir = os.path.join(base_dir, "Chapter_wav_files")
500
+ output_dir = os.path.join(base_dir, "Audiobooks")
501
+
502
+ # Ensure output directory exists
503
+ ensure_directory(output_dir)
504
+
505
+ # Clean up previous files
506
+ remove_directory(working_dir)
507
+ remove_directory(audio_dir)
508
+
509
+ # Create necessary directories
510
+ ensure_directory(working_dir)
511
+ ensure_directory(chapters_dir)
512
+ ensure_directory(audio_dir)
513
+
514
+ ebook_path = ebook_file.name
515
+ ebook_name = os.path.basename(ebook_path)
516
+
517
  try:
518
+ # Extract basic metadata if possible
519
+ try:
520
+ meta_result = subprocess.run(['ebook-meta', ebook_path],
521
+ stdout=subprocess.PIPE, text=True, check=False)
522
+ title_match = re.search(r'Title\s+:\s+(.*)', meta_result.stdout)
523
+ author_match = re.search(r'Author\(s\)\s+:\s+(.*)', meta_result.stdout)
524
+ title = title_match.group(1) if title_match else None
525
+ author = author_match.group(1) if author_match else None
526
+ except Exception as e:
527
+ logger.warning(f"Could not extract metadata: {e}")
528
+ title = author = None
529
+
530
+ # Process ebook to chapters
531
+ if progress:
532
+ progress(0.1, desc="Extracting chapters from ebook")
533
+
534
+ num_chapters = process_ebook_to_chapters(ebook_path, chapters_dir)
535
+
536
+ if num_chapters == 0:
537
+ return f"Failed to extract chapters from {ebook_name}", None
538
+
539
+ # Convert chapters to audio
540
+ if progress:
541
+ progress(0.3, desc="Converting text to speech")
542
+
543
+ processed, failed = convert_chapters_to_audio(
544
+ chapters_dir,
545
+ audio_dir,
546
+ voice.split()[0],
547
+ int(speed),
548
+ int(pitch),
549
+ int(gap),
550
+ lambda prog, desc: progress(0.3 + prog * 0.6, desc=desc) if progress else None
551
+ )
552
+
553
+ if processed == 0:
554
+ return f"Failed to convert any chapters to audio for {ebook_name}", None
555
+
556
+ # Create M4B audiobook
557
+ if progress:
558
+ progress(0.9, desc="Creating M4B audiobook")
559
+
560
+ m4b_path = create_m4b_audiobook(audio_dir, ebook_path, output_dir, title, author)
561
+
562
+ if not m4b_path:
563
+ return f"Failed to create M4B file for {ebook_name}", None
564
+
565
+ # Conversion complete
566
+ elapsed_time = time.time() - start_time
567
+
568
+ if progress:
569
+ progress(1.0, desc="Conversion complete")
570
+
571
+ return f"Audiobook created: {os.path.basename(m4b_path)} (in {elapsed_time:.1f} seconds)", m4b_path
572
+
573
  except Exception as e:
574
+ logger.error(f"Error converting ebook: {e}", exc_info=True)
575
+ return f"Error: {str(e)}", None
 
 
 
 
576
 
577
+ # Utility functions for Gradio interface
578
+ def get_available_voices():
579
+ """Get list of available espeak-ng voices."""
580
  try:
581
+ result = subprocess.run(['espeak-ng', '--voices'],
582
+ stdout=subprocess.PIPE, text=True, check=True)
583
+ lines = result.stdout.splitlines()[1:] # Skip header
584
+
585
+ voices = []
586
+ for line in lines:
587
+ parts = line.split()
588
+ if len(parts) > 3:
589
+ voice_id = parts[3] # Language code
590
+ description = ' '.join(parts[3:]) # Description
591
+ voices.append(f"{voice_id} ({description})")
592
+
593
+ return sorted(voices)
594
  except Exception as e:
595
+ logger.error(f"Error getting voices: {e}")
596
+ # Return some default voices as fallback
597
+ return ["en (English)", "en-us (American English)", "en-gb (British English)"]
598
+
599
+ def list_audiobooks():
600
+ """List all audiobooks in the output directory."""
601
+ output_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)), "Audiobooks")
602
+ ensure_directory(output_dir)
603
+
604
  files = []
605
+ for filename in os.listdir(output_dir):
606
  if filename.endswith('.m4b'):
607
+ filepath = os.path.join(output_dir, filename)
608
+ files.append(filepath)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
609
 
610
+ return sorted(files)
611
+
612
+ # Gradio interface (continued)
613
+ def create_gradio_interface(port=7860):
614
+ """Create and launch Gradio interface."""
615
+ # Create theme
616
+ theme = gr.themes.Soft(
617
+ primary_hue="blue",
618
+ secondary_hue="green",
619
+ neutral_hue="slate",
620
+ text_size=gr.themes.sizes.text_md,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  )
622
+
623
+ # Create interface
624
+ with gr.Blocks(theme=theme, title="eBook to Audiobook Converter") as demo:
625
+ gr.Markdown(
626
+ """
627
+ # 📚 eBook to Audiobook Converter
628
+
629
+ Convert any eBook format (EPUB, MOBI, PDF, etc.) to an M4B audiobook using eSpeak-NG.
630
+
631
+ ## Features
632
+ - Automatic chapter detection
633
+ - Natural sentence splitting
634
+ - Multiple voice and language options
635
+ - Customizable speech settings
636
+ """
637
+ )
638
+
639
+ with gr.Row():
640
+ with gr.Column(scale=3):
641
+ ebook_file = gr.File(label="eBook File", file_types=[".epub", ".mobi", ".azw", ".azw3", ".fb2", ".txt", ".pdf"])
642
+
643
+ with gr.Row():
644
+ with gr.Column(scale=1):
645
+ speed = gr.Slider(minimum=80, maximum=450, value=170, step=1,
646
+ label="Speech Speed", info="Higher values = faster speech")
647
+ with gr.Column(scale=1):
648
+ pitch = gr.Slider(minimum=0, maximum=99, value=50, step=1,
649
+ label="Voice Pitch", info="Higher values = higher pitch")
650
+
651
+ with gr.Row():
652
+ with gr.Column(scale=1):
653
+ gap = gr.Slider(minimum=0, maximum=20, value=5, step=1,
654
+ label="Pause Length", info="Pause between words (ms)")
655
+ with gr.Column(scale=1):
656
+ voice_dropdown = gr.Dropdown(
657
+ choices=get_available_voices(),
658
+ label="Voice",
659
+ value="en (English)",
660
+ info="Select language and voice variant"
661
+ )
662
+
663
+ convert_btn = gr.Button("Convert to Audiobook", variant="primary")
664
+ cancel_btn = gr.Button("Cancel Conversion", variant="stop")
665
+
666
+ with gr.Row():
667
+ with gr.Column(scale=1):
668
+ conversion_status = gr.Textbox(label="Conversion Status", interactive=False)
669
+ with gr.Column(scale=1):
670
+ audio_player = gr.Audio(label="Preview", type="filepath", interactive=False)
671
+
672
+ gr.Markdown("## Download Audiobooks")
673
+ with gr.Row():
674
+ refresh_btn = gr.Button("Refresh List")
675
+ download_btn = gr.Button("Download Selected File")
676
+
677
+ audiobook_files = gr.Dropdown(
678
+ choices=list_audiobooks(),
679
+ label="Available Audiobooks",
680
+ value=None,
681
+ interactive=True
682
+ )
683
+
684
+ # Define conversion task state
685
+ conversion_task = {"running": False, "thread": None}
686
+
687
+ # Handle events
688
+ def start_conversion(ebook_file, speed, pitch, voice, gap, progress=gr.Progress()):
689
+ # Check if already running
690
+ if conversion_task["running"]:
691
+ return "A conversion is already in progress. Please wait or cancel it.", None
692
+
693
+ if not ebook_file:
694
+ return "Please select an eBook file first.", None
695
+
696
+ conversion_task["running"] = True
697
+ result, output_path = convert_ebook_to_audiobook(ebook_file, speed, pitch, voice, gap, progress)
698
+ conversion_task["running"] = False
699
+
700
+ return result, output_path
701
+
702
+ def cancel_current_conversion():
703
+ if conversion_task["running"]:
704
+ conversion_task["running"] = False
705
+ return "Conversion cancelled."
706
+ else:
707
+ return "No conversion is currently running."
708
+
709
+ def refresh_audiobook_list():
710
+ return gr.Dropdown.update(choices=list_audiobooks())
711
+
712
+ # Connect events
713
+ convert_btn.click(
714
+ start_conversion,
715
+ inputs=[ebook_file, speed, pitch, voice_dropdown, gap],
716
+ outputs=[conversion_status, audio_player]
717
+ )
718
+
719
+ cancel_btn.click(
720
+ cancel_current_conversion,
721
+ outputs=[conversion_status]
722
+ )
723
+
724
+ refresh_btn.click(
725
+ refresh_audiobook_list,
726
+ outputs=[audiobook_files]
727
+ )
728
+
729
+ download_btn.click(
730
+ lambda x: x,
731
+ inputs=[audiobook_files],
732
+ outputs=[audiobook_files]
733
+ )
734
+
735
+ ebook_file.upload(
736
+ lambda: "eBook uploaded successfully",
737
+ outputs=[conversion_status]
738
+ )
739
+
740
+ # Launch the interface
741
+ demo.launch(server_port=port, share=True)
742
+ return demo
743
+
744
+ # Command-line interface
745
+ def main():
746
+ """Command-line entry point."""
747
+ parser = argparse.ArgumentParser(description='Convert eBooks to Audiobooks')
748
+ parser.add_argument('--gui', action='store_true', help='Launch graphical interface')
749
+ parser.add_argument('--port', type=int, default=7860, help='Port for web interface')
750
+ parser.add_argument('--ebook', type=str, help='Path to eBook file')
751
+ parser.add_argument('--voice', default='en', help='eSpeak voice to use')
752
+ parser.add_argument('--speed', type=int, default=170, help='Speech speed')
753
+ parser.add_argument('--pitch', type=int, default=50, help='Voice pitch')
754
+ parser.add_argument('--gap', type=int, default=5, help='Word gap')
755
+ args = parser.parse_args()
756
+
757
+ if args.gui:
758
+ create_gradio_interface(port=args.port)
759
+ elif args.ebook:
760
+ # Create a temporary file-like object for the ebook path
761
+ class FilePath:
762
+ def __init__(self, path):
763
+ self.name = path
764
+
765
+ print(f"Converting {args.ebook} to audiobook...")
766
+ result, output_path = convert_ebook_to_audiobook(
767
+ FilePath(args.ebook),
768
+ args.speed,
769
+ args.pitch,
770
+ args.voice,
771
+ args.gap
772
+ )
773
+ print(result)
774
+ else:
775
+ # Default to GUI if no arguments
776
+ create_gradio_interface()
777
 
778
+ if __name__ == "__main__":
779
+ main()