mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
Add support for embedding cover images in M4B files and update documentation
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
- New option: **"Subtitle speed adjustment method"**: Choose how to speed up audio when needed:
|
||||
- **TTS Regeneration (better quality):** Re-generates the audio at a faster speed for more natural sound.
|
||||
- **FFmpeg Time-stretch (better speed):** Quickly speeds up the generated audio.
|
||||
- Added support for embedding cover images in M4B files. Abogen now automatically extracts cover images from EPUB and PDF files. You can also manually specify a cover image using the `<<METADATA_COVER_PATH:path>>` tag in your text file.
|
||||
- Fixed `[WinError 1114] A dynamic link library (DLL) initialization routine failed` error on Windows, pre-loading PyTorch DLLs before initializing PyQt6 to avoid DLL initialization errors, mentioned in #98 by @ephr0n.
|
||||
- Potential fix for `CUDA GPU is not available` issue, by ensuring PyTorch is installed correctly with CUDA support on Windows using the installer script.
|
||||
- Improvements in code and documentation.
|
||||
|
||||
@@ -230,7 +230,9 @@ Similar to chapter markers, it is possible to add metadata tags for `M4B` files.
|
||||
<<METADATA_ALBUM_ARTIST:Album Artist>>
|
||||
<<METADATA_COMPOSER:Narrator>>
|
||||
<<METADATA_GENRE:Audiobook>>
|
||||
<<METADATA_COVER_PATH:path/to/cover.jpg>>
|
||||
```
|
||||
> Note: `METADATA_COVER_PATH` is used to embed a cover image into the generated M4B file. Abogen automatically extracts the cover from EPUB and PDF files and adds this tag for you.
|
||||
|
||||
## `About Timestamp-based Text Files`
|
||||
Similar to converting subtitle files to audio, Abogen can automatically detect text files that contain timestamps in `HH:MM:SS` or `HH:MM:SS,ms` format. When timestamps are found inside your text file, Abogen will ask if you want to use them for audio timing. This is useful for creating timed narrations, scripts, or transcripts where you need exact control over when each segment is spoken.
|
||||
@@ -274,6 +276,7 @@ I highly recommend using [MPV](https://mpv.io/installation/) to play your audio
|
||||
# --- MPV Settings ---
|
||||
save-position-on-quit
|
||||
keep-open=yes
|
||||
audio-display=no
|
||||
# --- Subtitle ---
|
||||
sub-ass-override=no
|
||||
sub-margin-y=50
|
||||
|
||||
@@ -89,6 +89,9 @@ class HandlerDialog(QDialog):
|
||||
def __init__(self, book_path, file_type=None, checked_chapters=None, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Normalize path
|
||||
book_path = os.path.normpath(os.path.abspath(book_path))
|
||||
|
||||
# Determine file type if not explicitly provided
|
||||
if file_type:
|
||||
self.file_type = file_type
|
||||
@@ -2211,6 +2214,7 @@ class HandlerDialog(QDialog):
|
||||
def _format_metadata_tags(self):
|
||||
"""Format metadata tags for insertion at the beginning of the text"""
|
||||
import datetime
|
||||
from abogen.utils import get_user_cache_path
|
||||
|
||||
metadata = self.book_metadata
|
||||
filename = os.path.splitext(os.path.basename(self.book_path))[0]
|
||||
@@ -2231,6 +2235,20 @@ class HandlerDialog(QDialog):
|
||||
f"{total_chapters} {'Chapters' if self.file_type == 'epub' else 'Pages'}"
|
||||
)
|
||||
|
||||
# Handle cover image
|
||||
cover_tag = ""
|
||||
if metadata.get("cover_image"):
|
||||
try:
|
||||
import uuid
|
||||
cache_dir = get_user_cache_path()
|
||||
cover_path = os.path.join(cache_dir, f"cover_{uuid.uuid4()}.jpg")
|
||||
cover_path = os.path.normpath(cover_path)
|
||||
with open(cover_path, "wb") as f:
|
||||
f.write(metadata["cover_image"])
|
||||
cover_tag = f"<<METADATA_COVER_PATH:{cover_path}>>"
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to save cover image: {e}")
|
||||
|
||||
# Format metadata tags
|
||||
metadata_tags = [
|
||||
f"<<METADATA_TITLE:{title}>>",
|
||||
@@ -2242,6 +2260,9 @@ class HandlerDialog(QDialog):
|
||||
f"<<METADATA_GENRE:Audiobook>>",
|
||||
]
|
||||
|
||||
if cover_tag:
|
||||
metadata_tags.append(cover_tag)
|
||||
|
||||
return "\n".join(metadata_tags)
|
||||
|
||||
def _get_markdown_selected_text(self):
|
||||
|
||||
+87
-19
@@ -1000,7 +1000,7 @@ class ConversionThread(QThread):
|
||||
static_ffmpeg.add_paths()
|
||||
merged_out_file = None
|
||||
ffmpeg_proc = None
|
||||
metadata_options = (
|
||||
metadata_options, cover_path = (
|
||||
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
|
||||
)
|
||||
# Prepare ffmpeg command for m4b output
|
||||
@@ -1017,13 +1017,32 @@ class ConversionThread(QThread):
|
||||
"1",
|
||||
"-i",
|
||||
"pipe:0",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-q:a",
|
||||
"2",
|
||||
"-movflags",
|
||||
"+faststart+use_metadata_tags",
|
||||
]
|
||||
if cover_path and os.path.exists(cover_path):
|
||||
cmd.extend(
|
||||
[
|
||||
"-i",
|
||||
cover_path,
|
||||
"-map",
|
||||
"0:a",
|
||||
"-map",
|
||||
"1",
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-disposition:v",
|
||||
"attached_pic",
|
||||
]
|
||||
)
|
||||
cmd.extend(
|
||||
[
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-q:a",
|
||||
"2",
|
||||
"-movflags",
|
||||
"+faststart+use_metadata_tags",
|
||||
]
|
||||
)
|
||||
cmd += metadata_options
|
||||
cmd.append(merged_out_path)
|
||||
ffmpeg_proc = create_process(cmd, stdin=subprocess.PIPE, text=False)
|
||||
@@ -1529,7 +1548,7 @@ class ConversionThread(QThread):
|
||||
orig_path = merged_out_path
|
||||
root, ext = os.path.splitext(orig_path)
|
||||
tmp_path = root + ".tmp" + ext
|
||||
metadata_options = (
|
||||
metadata_options, cover_path = (
|
||||
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
|
||||
)
|
||||
cmd = [
|
||||
@@ -1539,15 +1558,35 @@ class ConversionThread(QThread):
|
||||
orig_path,
|
||||
"-i",
|
||||
chapters_info_path,
|
||||
"-map",
|
||||
"0:a",
|
||||
"-map_metadata",
|
||||
"1",
|
||||
"-map_chapters",
|
||||
"1",
|
||||
"-c:a",
|
||||
"copy",
|
||||
]
|
||||
if cover_path and os.path.exists(cover_path):
|
||||
cmd.extend(
|
||||
[
|
||||
"-i",
|
||||
cover_path,
|
||||
"-map",
|
||||
"0:a",
|
||||
"-map",
|
||||
"2",
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-disposition:v",
|
||||
"attached_pic",
|
||||
]
|
||||
)
|
||||
else:
|
||||
cmd.extend(["-map", "0:a"])
|
||||
|
||||
cmd.extend(
|
||||
[
|
||||
"-map_metadata",
|
||||
"1",
|
||||
"-map_chapters",
|
||||
"1",
|
||||
"-c:a",
|
||||
"copy",
|
||||
]
|
||||
)
|
||||
cmd += metadata_options
|
||||
cmd.append(tmp_path)
|
||||
proc = create_process(cmd)
|
||||
@@ -1659,8 +1698,35 @@ class ConversionThread(QThread):
|
||||
static_ffmpeg.add_paths()
|
||||
cmd = ["ffmpeg", "-y", "-thread_queue_size", "32768", "-f", "f32le", "-ar", str(rate), "-ac", "1", "-i", "pipe:0"]
|
||||
if self.output_format == "m4b":
|
||||
cmd.extend(["-c:a", "aac", "-q:a", "2", "-movflags", "+faststart+use_metadata_tags"])
|
||||
cmd.extend(self._extract_and_add_metadata_tags_to_ffmpeg_cmd())
|
||||
metadata_options, cover_path = (
|
||||
self._extract_and_add_metadata_tags_to_ffmpeg_cmd()
|
||||
)
|
||||
if cover_path and os.path.exists(cover_path):
|
||||
cmd.extend(
|
||||
[
|
||||
"-i",
|
||||
cover_path,
|
||||
"-map",
|
||||
"0:a",
|
||||
"-map",
|
||||
"1",
|
||||
"-c:v",
|
||||
"copy",
|
||||
"-disposition:v",
|
||||
"attached_pic",
|
||||
]
|
||||
)
|
||||
cmd.extend(
|
||||
[
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-q:a",
|
||||
"2",
|
||||
"-movflags",
|
||||
"+faststart+use_metadata_tags",
|
||||
]
|
||||
)
|
||||
cmd.extend(metadata_options)
|
||||
elif self.output_format == "opus":
|
||||
cmd.extend(["-c:a", "libopus", "-b:a", "24000"])
|
||||
else:
|
||||
@@ -1901,6 +1967,8 @@ class ConversionThread(QThread):
|
||||
album_artist_match = re.search(r"<<METADATA_ALBUM_ARTIST:([^>]*)>>", text)
|
||||
composer_match = re.search(r"<<METADATA_COMPOSER:([^>]*)>>", text)
|
||||
genre_match = re.search(r"<<METADATA_GENRE:([^>]*)>>", text)
|
||||
cover_match = re.search(r"<<METADATA_COVER_PATH:([^>]*)>>", text)
|
||||
cover_path = cover_match.group(1) if cover_match else None
|
||||
|
||||
# Use display path or filename as fallback for title
|
||||
|
||||
@@ -1964,7 +2032,7 @@ class ConversionThread(QThread):
|
||||
metadata_options.extend(["-metadata", f"genre=Audiobook"])
|
||||
|
||||
# Add these to ffmpeg command
|
||||
return metadata_options
|
||||
return metadata_options, cover_path
|
||||
|
||||
def _srt_time(self, t):
|
||||
"""Helper function to format time for SRT files"""
|
||||
|
||||
+4
-4
@@ -3635,9 +3635,9 @@ Categories=AudioVideo;Audio;Utility;
|
||||
# Get the abogen cache directory
|
||||
cache_dir = get_user_cache_path()
|
||||
|
||||
# Find all .txt files in the abogen cache directory
|
||||
pattern = os.path.join(cache_dir, "*.txt")
|
||||
cache_files = glob.glob(pattern)
|
||||
# Find all .txt files and cover images in the abogen cache directory
|
||||
cache_files = glob.glob(os.path.join(cache_dir, "*.txt"))
|
||||
cache_files.extend(glob.glob(os.path.join(cache_dir, "cover_*.jpg")))
|
||||
|
||||
# Count the files
|
||||
file_count = len(cache_files)
|
||||
@@ -3669,7 +3669,7 @@ Categories=AudioVideo;Audio;Utility;
|
||||
msg_box.setText(msg_text + "\nDo you want to delete them?")
|
||||
|
||||
# Add checkbox for preview cache
|
||||
preview_cache_checkbox = QCheckBox("Clean preview cache", msg_box)
|
||||
preview_cache_checkbox = QCheckBox("Also clean preview cache", msg_box)
|
||||
preview_cache_checkbox.setChecked(False)
|
||||
# Only enable checkbox if preview files exist
|
||||
preview_cache_checkbox.setEnabled(preview_count > 0)
|
||||
|
||||
Reference in New Issue
Block a user