mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 05:40:26 +02:00
Corrections to code logic
This commit is contained in:
@@ -1,9 +0,0 @@
|
|||||||
from abogen.utils import get_version
|
|
||||||
|
|
||||||
# Program Information
|
|
||||||
PROGRAM_NAME = "abogen"
|
|
||||||
PROGRAM_DESCRIPTION = (
|
|
||||||
"Generate audiobooks from EPUBs, PDFs and text with synchronized captions."
|
|
||||||
)
|
|
||||||
GITHUB_URL = "https://github.com/denizsafak/abogen"
|
|
||||||
VERSION = get_version()
|
|
||||||
@@ -21,7 +21,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QMenu,
|
QMenu,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
from abogen.utils import clean_text, calculate_text_length
|
from utils import clean_text, calculate_text_length
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
from utils import get_version, get_user_config_path
|
||||||
|
|
||||||
|
# Program Information
|
||||||
|
PROGRAM_NAME = "abogen"
|
||||||
|
PROGRAM_DESCRIPTION = (
|
||||||
|
"Generate audiobooks from EPUBs, PDFs and text with synchronized captions."
|
||||||
|
)
|
||||||
|
GITHUB_URL = "https://github.com/denizsafak/abogen"
|
||||||
|
VERSION = get_version()
|
||||||
|
|
||||||
|
# Language description mapping
|
||||||
|
LANGUAGE_DESCRIPTIONS = {
|
||||||
|
"a": "American English",
|
||||||
|
"b": "British English",
|
||||||
|
"e": "Spanish",
|
||||||
|
"f": "French",
|
||||||
|
"h": "Hindi",
|
||||||
|
"i": "Italian",
|
||||||
|
"j": "Japanese",
|
||||||
|
"p": "Brazilian Portuguese",
|
||||||
|
"z": "Mandarin Chinese",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Supported languages for subtitle generation
|
||||||
|
# Currently, only 'a (American English)' and 'b (British English)' are supported for subtitle generation.
|
||||||
|
# This is because tokens that contain timestamps are not generated for other languages in the Kokoro pipeline.
|
||||||
|
# Please refer to: https://github.com/hexgrad/kokoro/blob/6d87f4ae7abc2d14dbc4b3ef2e5f19852e861ac2/kokoro/pipeline.py
|
||||||
|
# 383 English processing (unchanged)
|
||||||
|
# 384 if self.lang_code in 'ab':
|
||||||
|
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION = [
|
||||||
|
"a",
|
||||||
|
"b",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Voice and sample text constants
|
||||||
|
VOICES_INTERNAL = [
|
||||||
|
"af_alloy",
|
||||||
|
"af_aoede",
|
||||||
|
"af_bella",
|
||||||
|
"af_heart",
|
||||||
|
"af_jessica",
|
||||||
|
"af_kore",
|
||||||
|
"af_nicole",
|
||||||
|
"af_nova",
|
||||||
|
"af_river",
|
||||||
|
"af_sarah",
|
||||||
|
"af_sky",
|
||||||
|
"am_adam",
|
||||||
|
"am_echo",
|
||||||
|
"am_eric",
|
||||||
|
"am_fenrir",
|
||||||
|
"am_liam",
|
||||||
|
"am_michael",
|
||||||
|
"am_onyx",
|
||||||
|
"am_puck",
|
||||||
|
"am_santa",
|
||||||
|
"bf_alice",
|
||||||
|
"bf_emma",
|
||||||
|
"bf_isabella",
|
||||||
|
"bf_lily",
|
||||||
|
"bm_daniel",
|
||||||
|
"bm_fable",
|
||||||
|
"bm_george",
|
||||||
|
"bm_lewis",
|
||||||
|
"ef_dora",
|
||||||
|
"em_alex",
|
||||||
|
"em_santa",
|
||||||
|
"ff_siwis",
|
||||||
|
"hf_alpha",
|
||||||
|
"hf_beta",
|
||||||
|
"hm_omega",
|
||||||
|
"hm_psi",
|
||||||
|
"if_sara",
|
||||||
|
"im_nicola",
|
||||||
|
"jf_alpha",
|
||||||
|
"jf_gongitsune",
|
||||||
|
"jf_nezumi",
|
||||||
|
"jf_tebukuro",
|
||||||
|
"jm_kumo",
|
||||||
|
"pf_dora",
|
||||||
|
"pm_alex",
|
||||||
|
"pm_santa",
|
||||||
|
"zf_xiaobei",
|
||||||
|
"zf_xiaoni",
|
||||||
|
"zf_xiaoxiao",
|
||||||
|
"zf_xiaoyi",
|
||||||
|
"zm_yunjian",
|
||||||
|
"zm_yunxi",
|
||||||
|
"zm_yunxia",
|
||||||
|
"zm_yunyang",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Voice and sample text mapping
|
||||||
|
SAMPLE_VOICE_TEXTS = {
|
||||||
|
"a": "This is a sample of the selected voice.",
|
||||||
|
"b": "This is a sample of the selected voice.",
|
||||||
|
"e": "Este es una muestra de la voz seleccionada.",
|
||||||
|
"f": "Ceci est un exemple de la voix sélectionnée.",
|
||||||
|
"h": "यह चयनित आवाज़ का एक नमूना है।",
|
||||||
|
"i": "Questo è un esempio della voce selezionata.",
|
||||||
|
"j": "これは選択した声のサンプルです。",
|
||||||
|
"p": "Este é um exemplo da voz selecionada.",
|
||||||
|
"z": "这是所选语音的示例。",
|
||||||
|
}
|
||||||
|
|
||||||
|
# flags mapping for voice display
|
||||||
|
FLAGS = {
|
||||||
|
"a": "🇺🇸",
|
||||||
|
"b": "🇬🇧",
|
||||||
|
"e": "🇪🇸",
|
||||||
|
"f": "🇫🇷",
|
||||||
|
"h": "🇮🇳",
|
||||||
|
"i": "🇮🇹",
|
||||||
|
"j": "🇯🇵",
|
||||||
|
"p": "🇧🇷",
|
||||||
|
"z": "🇨🇳",
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@ import time
|
|||||||
from PyQt5.QtCore import QThread, pyqtSignal, Qt
|
from PyQt5.QtCore import QThread, pyqtSignal, Qt
|
||||||
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
|
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QDialog, QLabel, QDialogButtonBox
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
from abogen.utils import clean_text, SAMPLE_VOICE_TEXTS, LANGUAGE_DESCRIPTIONS
|
from utils import clean_text
|
||||||
from abogen import PROGRAM_NAME
|
from constants import PROGRAM_NAME, LANGUAGE_DESCRIPTIONS, SAMPLE_VOICE_TEXTS
|
||||||
|
|
||||||
|
|
||||||
def get_sample_voice_text(lang_code):
|
def get_sample_voice_text(lang_code):
|
||||||
@@ -688,6 +688,7 @@ class PlayAudioThread(QThread):
|
|||||||
try:
|
try:
|
||||||
import pygame
|
import pygame
|
||||||
import time as _time
|
import time as _time
|
||||||
|
|
||||||
pygame.mixer.init()
|
pygame.mixer.init()
|
||||||
pygame.mixer.music.load(self.wav_path)
|
pygame.mixer.music.load(self.wav_path)
|
||||||
pygame.mixer.music.play()
|
pygame.mixer.music.play()
|
||||||
|
|||||||
+13
-7
@@ -46,7 +46,7 @@ from PyQt5.QtGui import (
|
|||||||
QColor,
|
QColor,
|
||||||
QMovie,
|
QMovie,
|
||||||
)
|
)
|
||||||
from abogen.utils import (
|
from utils import (
|
||||||
load_config,
|
load_config,
|
||||||
save_config,
|
save_config,
|
||||||
get_gpu_acceleration,
|
get_gpu_acceleration,
|
||||||
@@ -56,14 +56,19 @@ from abogen.utils import (
|
|||||||
calculate_text_length,
|
calculate_text_length,
|
||||||
get_resource_path,
|
get_resource_path,
|
||||||
LoadPipelineThread,
|
LoadPipelineThread,
|
||||||
|
)
|
||||||
|
from conversion import ConversionThread, VoicePreviewThread, PlayAudioThread
|
||||||
|
from book_handler import HandlerDialog
|
||||||
|
from constants import (
|
||||||
|
PROGRAM_NAME,
|
||||||
|
VERSION,
|
||||||
|
GITHUB_URL,
|
||||||
|
PROGRAM_DESCRIPTION,
|
||||||
|
LANGUAGE_DESCRIPTIONS,
|
||||||
FLAGS,
|
FLAGS,
|
||||||
VOICES_INTERNAL,
|
VOICES_INTERNAL,
|
||||||
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION,
|
||||||
LANGUAGE_DESCRIPTIONS,
|
|
||||||
)
|
)
|
||||||
from abogen.conversion import ConversionThread, VoicePreviewThread, PlayAudioThread
|
|
||||||
from abogen.book_handler import HandlerDialog
|
|
||||||
from abogen import PROGRAM_NAME, VERSION, GITHUB_URL, PROGRAM_DESCRIPTION
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
# Import ctypes for Windows-specific taskbar icon
|
# Import ctypes for Windows-specific taskbar icon
|
||||||
@@ -1271,6 +1276,7 @@ class abogen(QWidget):
|
|||||||
if self.preview_playing:
|
if self.preview_playing:
|
||||||
try:
|
try:
|
||||||
import pygame
|
import pygame
|
||||||
|
|
||||||
pygame.mixer.music.stop()
|
pygame.mixer.music.stop()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
@@ -1470,7 +1476,7 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
def show_chapter_options_dialog(self, chapter_count):
|
def show_chapter_options_dialog(self, chapter_count):
|
||||||
"""Show dialog to ask user about chapter processing options when chapters are detected in a .txt file"""
|
"""Show dialog to ask user about chapter processing options when chapters are detected in a .txt file"""
|
||||||
from abogen.conversion import ChapterOptionsDialog
|
from conversion import ChapterOptionsDialog
|
||||||
|
|
||||||
dialog = ChapterOptionsDialog(chapter_count, parent=self)
|
dialog = ChapterOptionsDialog(chapter_count, parent=self)
|
||||||
dialog.setWindowModality(Qt.ApplicationModal)
|
dialog.setWindowModality(Qt.ApplicationModal)
|
||||||
@@ -1539,7 +1545,7 @@ class abogen(QWidget):
|
|||||||
|
|
||||||
def reveal_config_in_explorer(self):
|
def reveal_config_in_explorer(self):
|
||||||
"""Open the configuration file location in file explorer."""
|
"""Open the configuration file location in file explorer."""
|
||||||
from abogen.utils import get_user_config_path
|
from utils import get_user_config_path
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config_path = get_user_config_path()
|
config_path = get_user_config_path()
|
||||||
|
|||||||
+11
-4
@@ -3,9 +3,12 @@ import sys
|
|||||||
import platform
|
import platform
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
from PyQt5.QtGui import QIcon
|
from PyQt5.QtGui import QIcon
|
||||||
from abogen.gui import abogen
|
|
||||||
from abogen.utils import get_resource_path
|
# Add the directory to Python path
|
||||||
from abogen import PROGRAM_NAME, VERSION
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
|
||||||
|
from gui import abogen
|
||||||
|
from utils import get_resource_path
|
||||||
|
from constants import PROGRAM_NAME, VERSION
|
||||||
|
|
||||||
# Ensure sys.stdout and sys.stderr are valid in GUI mode
|
# Ensure sys.stdout and sys.stderr are valid in GUI mode
|
||||||
if sys.stdout is None:
|
if sys.stdout is None:
|
||||||
@@ -28,7 +31,11 @@ if platform.system() == "Windows":
|
|||||||
if platform.system() == "Linux":
|
if platform.system() == "Linux":
|
||||||
xdg_session = os.environ.get("XDG_SESSION_TYPE", "").lower()
|
xdg_session = os.environ.get("XDG_SESSION_TYPE", "").lower()
|
||||||
desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
desktop = os.environ.get("XDG_CURRENT_DESKTOP", "").lower()
|
||||||
if "gnome" in desktop and xdg_session == "wayland" and "QT_QPA_PLATFORM" not in os.environ:
|
if (
|
||||||
|
"gnome" in desktop
|
||||||
|
and xdg_session == "wayland"
|
||||||
|
and "QT_QPA_PLATFORM" not in os.environ
|
||||||
|
):
|
||||||
os.environ["QT_QPA_PLATFORM"] = "wayland"
|
os.environ["QT_QPA_PLATFORM"] = "wayland"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+2
-112
@@ -10,114 +10,6 @@ from threading import Thread
|
|||||||
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
|
os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
|
||||||
warnings.filterwarnings("ignore")
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
# Language description mapping
|
|
||||||
LANGUAGE_DESCRIPTIONS = {
|
|
||||||
"a": "American English",
|
|
||||||
"b": "British English",
|
|
||||||
"e": "Spanish",
|
|
||||||
"f": "French",
|
|
||||||
"h": "Hindi",
|
|
||||||
"i": "Italian",
|
|
||||||
"j": "Japanese",
|
|
||||||
"p": "Brazilian Portuguese",
|
|
||||||
"z": "Mandarin Chinese",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Supported languages for subtitle generation
|
|
||||||
# Currently, only 'a (American English)' and 'b (British English)' are supported for subtitle generation.
|
|
||||||
# This is because tokens that contain timestamps are not generated for other languages in the Kokoro pipeline.
|
|
||||||
# Please refer to: https://github.com/hexgrad/kokoro/blob/6d87f4ae7abc2d14dbc4b3ef2e5f19852e861ac2/kokoro/pipeline.py
|
|
||||||
# 383 English processing (unchanged)
|
|
||||||
# 384 if self.lang_code in 'ab':
|
|
||||||
SUPPORTED_LANGUAGES_FOR_SUBTITLE_GENERATION = [
|
|
||||||
"a",
|
|
||||||
"b",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Voice and sample text constants
|
|
||||||
VOICES_INTERNAL = [
|
|
||||||
"af_alloy",
|
|
||||||
"af_aoede",
|
|
||||||
"af_bella",
|
|
||||||
"af_heart",
|
|
||||||
"af_jessica",
|
|
||||||
"af_kore",
|
|
||||||
"af_nicole",
|
|
||||||
"af_nova",
|
|
||||||
"af_river",
|
|
||||||
"af_sarah",
|
|
||||||
"af_sky",
|
|
||||||
"am_adam",
|
|
||||||
"am_echo",
|
|
||||||
"am_eric",
|
|
||||||
"am_fenrir",
|
|
||||||
"am_liam",
|
|
||||||
"am_michael",
|
|
||||||
"am_onyx",
|
|
||||||
"am_puck",
|
|
||||||
"am_santa",
|
|
||||||
"bf_alice",
|
|
||||||
"bf_emma",
|
|
||||||
"bf_isabella",
|
|
||||||
"bf_lily",
|
|
||||||
"bm_daniel",
|
|
||||||
"bm_fable",
|
|
||||||
"bm_george",
|
|
||||||
"bm_lewis",
|
|
||||||
"ef_dora",
|
|
||||||
"em_alex",
|
|
||||||
"em_santa",
|
|
||||||
"ff_siwis",
|
|
||||||
"hf_alpha",
|
|
||||||
"hf_beta",
|
|
||||||
"hm_omega",
|
|
||||||
"hm_psi",
|
|
||||||
"if_sara",
|
|
||||||
"im_nicola",
|
|
||||||
"jf_alpha",
|
|
||||||
"jf_gongitsune",
|
|
||||||
"jf_nezumi",
|
|
||||||
"jf_tebukuro",
|
|
||||||
"jm_kumo",
|
|
||||||
"pf_dora",
|
|
||||||
"pm_alex",
|
|
||||||
"pm_santa",
|
|
||||||
"zf_xiaobei",
|
|
||||||
"zf_xiaoni",
|
|
||||||
"zf_xiaoxiao",
|
|
||||||
"zf_xiaoyi",
|
|
||||||
"zm_yunjian",
|
|
||||||
"zm_yunxi",
|
|
||||||
"zm_yunxia",
|
|
||||||
"zm_yunyang",
|
|
||||||
]
|
|
||||||
|
|
||||||
# Voice and sample text mapping
|
|
||||||
SAMPLE_VOICE_TEXTS = {
|
|
||||||
"a": "This is a sample of the selected voice.",
|
|
||||||
"b": "This is a sample of the selected voice.",
|
|
||||||
"e": "Este es una muestra de la voz seleccionada.",
|
|
||||||
"f": "Ceci est un exemple de la voix sélectionnée.",
|
|
||||||
"h": "यह चयनित आवाज़ का एक नमूना है।",
|
|
||||||
"i": "Questo è un esempio della voce selezionata.",
|
|
||||||
"j": "これは選択した声のサンプルです。",
|
|
||||||
"p": "Este é um exemplo da voz selecionada.",
|
|
||||||
"z": "这是所选语音的示例。",
|
|
||||||
}
|
|
||||||
|
|
||||||
# flags mapping for voice display
|
|
||||||
FLAGS = {
|
|
||||||
"a": "🇺🇸",
|
|
||||||
"b": "🇬🇧",
|
|
||||||
"e": "🇪🇸",
|
|
||||||
"f": "🇫🇷",
|
|
||||||
"h": "🇮🇳",
|
|
||||||
"i": "🇮🇹",
|
|
||||||
"j": "🇯🇵",
|
|
||||||
"p": "🇧🇷",
|
|
||||||
"z": "🇨🇳",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def get_resource_path(package, resource):
|
def get_resource_path(package, resource):
|
||||||
"""
|
"""
|
||||||
@@ -174,8 +66,6 @@ def get_user_config_path():
|
|||||||
return os.path.join(config_dir, "config.json")
|
return os.path.join(config_dir, "config.json")
|
||||||
|
|
||||||
|
|
||||||
CONFIG_PATH = get_user_config_path()
|
|
||||||
|
|
||||||
_sleep_procs = {"Darwin": None, "Linux": None} # Store sleep prevention processes
|
_sleep_procs = {"Darwin": None, "Linux": None} # Store sleep prevention processes
|
||||||
|
|
||||||
|
|
||||||
@@ -193,7 +83,7 @@ def clean_text(text):
|
|||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
try:
|
try:
|
||||||
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
with open(get_user_config_path(), "r", encoding="utf-8") as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
@@ -201,7 +91,7 @@ def load_config():
|
|||||||
|
|
||||||
def save_config(config):
|
def save_config(config):
|
||||||
try:
|
try:
|
||||||
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
|
with open(get_user_config_path(), "w", encoding="utf-8") as f:
|
||||||
json.dump(config, f, indent=2)
|
json.dump(config, f, indent=2)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|||||||
+43
-28
@@ -2,7 +2,7 @@
|
|||||||
#
|
#
|
||||||
# For WebM (smaller filesize):
|
# For WebM (smaller filesize):
|
||||||
# ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.ass" -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus -shortest demo.webm
|
# ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.ass" -c:v libvpx-vp9 -b:v 0 -crf 30 -c:a libopus -shortest demo.webm
|
||||||
#
|
#
|
||||||
# For MP4 (higher quality):
|
# For MP4 (higher quality):
|
||||||
# ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.ass" -c:v libx264 -preset slow -crf 18 -movflags +faststart -c:a copy -shortest demo.mp4
|
# ffmpeg -loop 1 -framerate 24 -i bg.jpg -i audio.wav -vf "ass=your_subtitle_demo.ass" -c:v libx264 -preset slow -crf 18 -movflags +faststart -c:a copy -shortest demo.mp4
|
||||||
#
|
#
|
||||||
@@ -13,23 +13,27 @@ import re
|
|||||||
import sys
|
import sys
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
|
||||||
def parse_time(s):
|
def parse_time(s):
|
||||||
# For ASS format
|
# For ASS format
|
||||||
if ':' in s and '.' in s:
|
if ":" in s and "." in s:
|
||||||
h, m, s = s.split(':')
|
h, m, s = s.split(":")
|
||||||
sec, cs = s.split('.')
|
sec, cs = s.split(".")
|
||||||
return timedelta(hours=int(h), minutes=int(m), seconds=int(sec), milliseconds=int(cs) * 10)
|
return timedelta(
|
||||||
|
hours=int(h), minutes=int(m), seconds=int(sec), milliseconds=int(cs) * 10
|
||||||
|
)
|
||||||
# For SRT format (00:00:00,000)
|
# For SRT format (00:00:00,000)
|
||||||
elif ':' in s and ',' in s:
|
elif ":" in s and "," in s:
|
||||||
parts = s.split(':')
|
parts = s.split(":")
|
||||||
h = int(parts[0])
|
h = int(parts[0])
|
||||||
m = int(parts[1])
|
m = int(parts[1])
|
||||||
sec_parts = parts[2].split(',')
|
sec_parts = parts[2].split(",")
|
||||||
sec = int(sec_parts[0])
|
sec = int(sec_parts[0])
|
||||||
ms = int(sec_parts[1])
|
ms = int(sec_parts[1])
|
||||||
return timedelta(hours=h, minutes=m, seconds=sec, milliseconds=ms)
|
return timedelta(hours=h, minutes=m, seconds=sec, milliseconds=ms)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def format_time(t):
|
def format_time(t):
|
||||||
total_seconds = int(t.total_seconds())
|
total_seconds = int(t.total_seconds())
|
||||||
cs = int((t.total_seconds() - total_seconds) * 100)
|
cs = int((t.total_seconds() - total_seconds) * 100)
|
||||||
@@ -38,6 +42,7 @@ def format_time(t):
|
|||||||
s = total_seconds % 60
|
s = total_seconds % 60
|
||||||
return f"{h}:{m:02}:{s:02}.{cs:02}"
|
return f"{h}:{m:02}:{s:02}.{cs:02}"
|
||||||
|
|
||||||
|
|
||||||
# Desired script info and style
|
# Desired script info and style
|
||||||
DESIRED_SCRIPT_INFO = """[Script Info]
|
DESIRED_SCRIPT_INFO = """[Script Info]
|
||||||
Title: Centered Subs
|
Title: Centered Subs
|
||||||
@@ -51,8 +56,9 @@ Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour,
|
|||||||
Style: Default,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H64000000,-1,0,0,0,100,100,0,0,1,2,0,5,10,10,10,1
|
Style: Default,Arial,60,&H00FFFFFF,&H000000FF,&H00000000,&H64000000,-1,0,0,0,100,100,0,0,1,2,0,5,10,10,10,1
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def process_subtitle_file(input_file):
|
def process_subtitle_file(input_file):
|
||||||
output_file = input_file.replace('.ass', '_demo.ass').replace('.srt', '_demo.ass')
|
output_file = input_file.replace(".ass", "_demo.ass").replace(".srt", "_demo.ass")
|
||||||
if output_file == input_file:
|
if output_file == input_file:
|
||||||
output_file = f"{input_file}_demo.ass"
|
output_file = f"{input_file}_demo.ass"
|
||||||
|
|
||||||
@@ -67,11 +73,11 @@ def process_subtitle_file(input_file):
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Check if it's an SRT file
|
# Check if it's an SRT file
|
||||||
is_srt = input_file.lower().endswith('.srt')
|
is_srt = input_file.lower().endswith(".srt")
|
||||||
|
|
||||||
if is_srt:
|
if is_srt:
|
||||||
return convert_srt_to_ass(input_file, output_file, lines)
|
return convert_srt_to_ass(input_file, output_file, lines)
|
||||||
|
|
||||||
# Process ASS file
|
# Process ASS file
|
||||||
header = []
|
header = []
|
||||||
events = []
|
events = []
|
||||||
@@ -124,9 +130,12 @@ def process_subtitle_file(input_file):
|
|||||||
if end > next_start:
|
if end > next_start:
|
||||||
end = next_start # cut current subtitle to stop at next one's start
|
end = next_start # cut current subtitle to stop at next one's start
|
||||||
|
|
||||||
fixed_line = re.sub(r"Dialogue: \d,[^,]+,[^,]+,",
|
fixed_line = re.sub(
|
||||||
f"Dialogue: 0,{format_time(start)},{format_time(end)},",
|
r"Dialogue: \d,[^,]+,[^,]+,",
|
||||||
line, count=1)
|
f"Dialogue: 0,{format_time(start)},{format_time(end)},",
|
||||||
|
line,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
fixed_lines.append(fixed_line)
|
fixed_lines.append(fixed_line)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -138,12 +147,13 @@ def process_subtitle_file(input_file):
|
|||||||
print(f"❌ Error writing output file: {e}")
|
print(f"❌ Error writing output file: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def convert_srt_to_ass(input_file, output_file, lines):
|
def convert_srt_to_ass(input_file, output_file, lines):
|
||||||
"""Convert SRT format to ASS format."""
|
"""Convert SRT format to ASS format."""
|
||||||
events = []
|
events = []
|
||||||
subtitle_blocks = []
|
subtitle_blocks = []
|
||||||
current_block = []
|
current_block = []
|
||||||
|
|
||||||
# Parse SRT file
|
# Parse SRT file
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
@@ -153,41 +163,45 @@ def convert_srt_to_ass(input_file, output_file, lines):
|
|||||||
current_block = []
|
current_block = []
|
||||||
else:
|
else:
|
||||||
current_block.append(line)
|
current_block.append(line)
|
||||||
|
|
||||||
# Don't forget the last block
|
# Don't forget the last block
|
||||||
if current_block:
|
if current_block:
|
||||||
subtitle_blocks.append(current_block)
|
subtitle_blocks.append(current_block)
|
||||||
|
|
||||||
# Process subtitle blocks
|
# Process subtitle blocks
|
||||||
for block in subtitle_blocks:
|
for block in subtitle_blocks:
|
||||||
if len(block) < 3:
|
if len(block) < 3:
|
||||||
continue # Invalid block
|
continue # Invalid block
|
||||||
|
|
||||||
# Skip the subtitle number
|
# Skip the subtitle number
|
||||||
timing_line = block[1]
|
timing_line = block[1]
|
||||||
|
|
||||||
# Parse timing information
|
# Parse timing information
|
||||||
timing_match = re.match(r'(\d+:\d+:\d+,\d+)\s+-->\s+(\d+:\d+:\d+,\d+)', timing_line)
|
timing_match = re.match(
|
||||||
|
r"(\d+:\d+:\d+,\d+)\s+-->\s+(\d+:\d+:\d+,\d+)", timing_line
|
||||||
|
)
|
||||||
if not timing_match:
|
if not timing_match:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
start_time = parse_time(timing_match.group(1))
|
start_time = parse_time(timing_match.group(1))
|
||||||
end_time = parse_time(timing_match.group(2))
|
end_time = parse_time(timing_match.group(2))
|
||||||
|
|
||||||
# Combine text lines
|
# Combine text lines
|
||||||
text = "\\N".join(block[2:])
|
text = "\\N".join(block[2:])
|
||||||
|
|
||||||
# Create ASS Dialogue line
|
# Create ASS Dialogue line
|
||||||
dialogue_line = f"Dialogue: 0,{format_time(start_time)},{format_time(end_time)},Default,,0,0,0,,{text}\n"
|
dialogue_line = f"Dialogue: 0,{format_time(start_time)},{format_time(end_time)},Default,,0,0,0,,{text}\n"
|
||||||
events.append(dialogue_line)
|
events.append(dialogue_line)
|
||||||
|
|
||||||
# Create ASS file
|
# Create ASS file
|
||||||
try:
|
try:
|
||||||
with open(output_file, "w", encoding="utf-8") as f:
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
f.write(DESIRED_SCRIPT_INFO)
|
f.write(DESIRED_SCRIPT_INFO)
|
||||||
f.write(DESIRED_STYLE)
|
f.write(DESIRED_STYLE)
|
||||||
f.write("[Events]\n")
|
f.write("[Events]\n")
|
||||||
f.write("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n")
|
f.write(
|
||||||
|
"Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
||||||
|
)
|
||||||
f.writelines(events)
|
f.writelines(events)
|
||||||
print(f"✅ Successfully converted SRT to ASS. Output file: {output_file}")
|
print(f"✅ Successfully converted SRT to ASS. Output file: {output_file}")
|
||||||
return True
|
return True
|
||||||
@@ -195,11 +209,12 @@ def convert_srt_to_ass(input_file, output_file, lines):
|
|||||||
print(f"❌ Error writing output file: {e}")
|
print(f"❌ Error writing output file: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) < 2:
|
if len(sys.argv) < 2:
|
||||||
print("❌ Error: No input file specified")
|
print("❌ Error: No input file specified")
|
||||||
print(f"Usage: {sys.argv[0]} <input_file.ass|input_file.srt>")
|
print(f"Usage: {sys.argv[0]} <input_file.ass|input_file.srt>")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
input_file = sys.argv[1]
|
input_file = sys.argv[1]
|
||||||
process_subtitle_file(input_file)
|
process_subtitle_file(input_file)
|
||||||
|
|||||||
Reference in New Issue
Block a user