112 lines
3.0 KiB
Python
112 lines
3.0 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2025-2026 Renato Xavier da Silveira Rosa
|
|
# See [LICENSE](./LICENSE) or [BSD-3-Clause-Clear](https://spdx.org/licenses/BSD-3-Clause-Clear.html)
|
|
"""Postprocess audio files into an audiobook.
|
|
"""
|
|
import subprocess
|
|
|
|
|
|
|
|
# stdlib modules
|
|
import os
|
|
import sys
|
|
|
|
# pip installed packages
|
|
import numpy as np
|
|
import soundfile
|
|
import torch
|
|
from tqdm import tqdm
|
|
from kokoro import KPipeline
|
|
from ebooklib import epub
|
|
import soundfile as sf
|
|
from mutagen import mp4
|
|
from pydub import AudioSegment
|
|
from mutagen import mp4
|
|
# Local imports
|
|
from .tts_generic import GenericTTSBackend
|
|
from . import logger, PathLike
|
|
from .preprocess import preprocess_book
|
|
|
|
def generate_metadata(files, author, title, chapter_titles):
|
|
chap = 0
|
|
start_time = 0
|
|
with open("FFMETADATAFILE", "w") as file:
|
|
file.write(";FFMETADATA1\n")
|
|
file.write(f"ARTIST={author}\n")
|
|
file.write(f"ALBUM={title}\n")
|
|
file.write(f"TITLE={title}\n")
|
|
file.write("DESCRIPTION=Made with https://github.com/aedocw/epub2tts-kokoro\n")
|
|
for file_name in files:
|
|
duration = get_duration(file_name)
|
|
file.write("[CHAPTER]\n")
|
|
file.write("TIMEBASE=1/1000\n")
|
|
file.write(f"START={start_time}\n")
|
|
file.write(f"END={start_time + duration}\n")
|
|
file.write(f"title={chapter_titles[chap]}\n")
|
|
chap += 1
|
|
start_time += duration
|
|
|
|
def get_duration(file_path):
|
|
audio = AudioSegment.from_file(file_path)
|
|
duration_milliseconds = len(audio)
|
|
return duration_milliseconds
|
|
|
|
def make_m4b(files, sourcefile, speaker):
|
|
filelist = "filelist.txt"
|
|
basefile = sourcefile.replace(".txt", "")
|
|
outputm4a = f"{basefile} ({speaker}).m4a"
|
|
outputm4b = f"{basefile} ({speaker}).m4b"
|
|
with open(filelist, "w") as f:
|
|
for filename in files:
|
|
filename = filename.replace("'", "'\\''")
|
|
f.write(f"file '{filename}'\n")
|
|
ffmpeg_command = [
|
|
"ffmpeg",
|
|
"-f",
|
|
"concat",
|
|
"-safe",
|
|
"0",
|
|
"-i",
|
|
filelist,
|
|
"-codec:a",
|
|
"flac",
|
|
"-f",
|
|
"mp4",
|
|
"-strict",
|
|
"-2",
|
|
outputm4a,
|
|
]
|
|
subprocess.run(ffmpeg_command)
|
|
ffmpeg_command = [
|
|
"ffmpeg",
|
|
"-i",
|
|
outputm4a,
|
|
"-i",
|
|
"FFMETADATAFILE",
|
|
"-map_metadata",
|
|
"1",
|
|
"-codec",
|
|
"aac",
|
|
outputm4b,
|
|
]
|
|
subprocess.run(ffmpeg_command)
|
|
os.remove(filelist)
|
|
os.remove("FFMETADATAFILE")
|
|
os.remove(outputm4a)
|
|
for f in files:
|
|
os.remove(f)
|
|
return outputm4b
|
|
|
|
def add_cover(cover_img, filename):
|
|
try:
|
|
if os.path.isfile(cover_img):
|
|
m4b = mp4.MP4(filename)
|
|
cover_image = open(cover_img, "rb").read()
|
|
m4b["covr"] = [mp4.MP4Cover(cover_image)]
|
|
m4b.save()
|
|
else:
|
|
print(f"Cover image {cover_img} not found")
|
|
except:
|
|
print(f"Cover image {cover_img} not found")
|