epub2speech 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epub2speech-0.0.1/LICENSE +21 -0
- epub2speech-0.0.1/PKG-INFO +161 -0
- epub2speech-0.0.1/README.md +124 -0
- epub2speech-0.0.1/epub2speech/__init__.py +9 -0
- epub2speech-0.0.1/epub2speech/chapter_tts.py +179 -0
- epub2speech-0.0.1/epub2speech/cli.py +144 -0
- epub2speech-0.0.1/epub2speech/convertor.py +139 -0
- epub2speech-0.0.1/epub2speech/epub_picker.py +202 -0
- epub2speech-0.0.1/epub2speech/extractor.py +74 -0
- epub2speech-0.0.1/epub2speech/m4b_generator.py +178 -0
- epub2speech-0.0.1/epub2speech/tts/__init__.py +7 -0
- epub2speech-0.0.1/epub2speech/tts/azure_provider.py +138 -0
- epub2speech-0.0.1/epub2speech/tts/protocol.py +19 -0
- epub2speech-0.0.1/pyproject.toml +70 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OOMOL Lab
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: epub2speech
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Convert EPUB e-books into high-quality audiobooks using Azure Text-to-Speech technology
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: epub,audiobook,text-to-speech,tts,azure,m4b,ebook
|
|
7
|
+
Author: TaoZeyu
|
|
8
|
+
Author-email: i@taozeyu.com
|
|
9
|
+
Maintainer: TaoZeyu
|
|
10
|
+
Maintainer-email: i@taozeyu.com
|
|
11
|
+
Requires-Python: >=3.11,<3.14
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
|
|
21
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: azure-cognitiveservices-speech (>=1.46.0,<2.0.0)
|
|
25
|
+
Requires-Dist: ebooklib (>=0.19,<0.20)
|
|
26
|
+
Requires-Dist: numpy (>=2.3.3,<3.0.0)
|
|
27
|
+
Requires-Dist: pytest (>=7.0.0) ; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-cov (>=4.0.0) ; extra == "dev"
|
|
29
|
+
Requires-Dist: resource-segmentation (==0.0.5)
|
|
30
|
+
Requires-Dist: soundfile (>=0.13.1,<0.14.0)
|
|
31
|
+
Requires-Dist: spacy (>=3.8.7,<4.0.0)
|
|
32
|
+
Project-URL: Bug Tracker, https://github.com/oomol-lab/epub2speech/issues
|
|
33
|
+
Project-URL: Documentation, https://github.com/oomol-lab/epub2speech/blob/main/README.md
|
|
34
|
+
Project-URL: Homepage, https://github.com/oomol-lab/epub2speech
|
|
35
|
+
Project-URL: Repository, https://github.com/oomol-lab/epub2speech
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
<div align=center>
|
|
39
|
+
<h1>EPUB to Speech</h1>
|
|
40
|
+
<p>English | <a href="./README_zh-CN.md">δΈζ</a></p>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
Convert EPUB e-books into high-quality audiobooks using Azure Text-to-Speech technology.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **π EPUB Support**: Compatible with EPUB 2 and EPUB 3 formats
|
|
48
|
+
- **ποΈ High-Quality TTS**: Uses Azure Cognitive Services Speech for natural voice synthesis
|
|
49
|
+
- **π Multi-Language Support**: Supports various languages and voices via Azure TTS
|
|
50
|
+
- **π± M4B Output**: Generates standard M4B audiobook format with chapter navigation
|
|
51
|
+
- **π§ CLI Interface**: Easy-to-use command-line tool with progress tracking
|
|
52
|
+
|
|
53
|
+
## Basic Usage
|
|
54
|
+
|
|
55
|
+
Convert an EPUB file to audiobook:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
epub2speech input.epub output.m4b --voice zh-CN-XiaoxiaoNeural --azure-key YOUR_KEY --azure-region YOUR_REGION
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
### Prerequisites
|
|
64
|
+
|
|
65
|
+
- Python 3.11 or higher
|
|
66
|
+
- FFmpeg (for audio processing)
|
|
67
|
+
- Azure Speech Service credentials
|
|
68
|
+
|
|
69
|
+
### Install Dependencies
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Install Python dependencies
|
|
73
|
+
pip install poetry
|
|
74
|
+
poetry install
|
|
75
|
+
|
|
76
|
+
# Install FFmpeg
|
|
77
|
+
# macOS: brew install ffmpeg
|
|
78
|
+
# Ubuntu/Debian: sudo apt install ffmpeg
|
|
79
|
+
# Windows: Download from https://ffmpeg.org/download.html
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Azure Speech Service Setup
|
|
83
|
+
|
|
84
|
+
1. Create an Azure account at https://azure.microsoft.com
|
|
85
|
+
2. Create a Speech Service resource in Azure Portal
|
|
86
|
+
3. Get your subscription key and region from the Azure dashboard
|
|
87
|
+
|
|
88
|
+
## Quick Start
|
|
89
|
+
|
|
90
|
+
### Environment Variables
|
|
91
|
+
|
|
92
|
+
Set your Azure credentials as environment variables:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
export AZURE_SPEECH_KEY="your-subscription-key"
|
|
96
|
+
export AZURE_SPEECH_REGION="your-region"
|
|
97
|
+
|
|
98
|
+
epub2speech input.epub output.m4b --voice zh-CN-XiaoxiaoNeural
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Advanced Options
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Limit to first 5 chapters
|
|
105
|
+
epub2speech input.epub output.m4b --voice en-US-AriaNeural --max-chapters 5
|
|
106
|
+
|
|
107
|
+
# Use custom workspace directory
|
|
108
|
+
epub2speech input.epub output.m4b --voice zh-CN-YunxiNeural --workspace /tmp/my-workspace
|
|
109
|
+
|
|
110
|
+
# Quiet mode (no progress output)
|
|
111
|
+
epub2speech input.epub output.m4b --voice ja-JP-NanamiNeural --quiet
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Available Voices
|
|
115
|
+
|
|
116
|
+
For a complete list, see [Azure Neural Voices](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support#neural-voices).
|
|
117
|
+
|
|
118
|
+
## How It Works
|
|
119
|
+
|
|
120
|
+
1. **EPUB Parsing**: Extracts text content and metadata from EPUB files
|
|
121
|
+
2. **Chapter Detection**: Identifies chapters using EPUB navigation data
|
|
122
|
+
3. **Text Processing**: Cleans and segments text for optimal speech synthesis
|
|
123
|
+
4. **Audio Generation**: Converts text to speech using Azure TTS
|
|
124
|
+
5. **M4B Creation**: Combines audio files with chapter metadata into M4B format
|
|
125
|
+
|
|
126
|
+
## Development
|
|
127
|
+
|
|
128
|
+
### Running Tests
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
python test.py
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Run specific test modules:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python test.py --test test_epub_picker
|
|
138
|
+
python test.py --test test_tts
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Contributing
|
|
142
|
+
|
|
143
|
+
Contributions are welcome! Please feel free to submit issues or pull requests.
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
148
|
+
|
|
149
|
+
## Acknowledgments
|
|
150
|
+
|
|
151
|
+
- [Azure Cognitive Services](https://azure.microsoft.com/services/cognitive-services/) for text-to-speech technology
|
|
152
|
+
- [ebooklib](https://github.com/aerkalov/ebooklib) for EPUB parsing
|
|
153
|
+
- [FFmpeg](https://ffmpeg.org/) for audio processing
|
|
154
|
+
- [spaCy](https://spacy.io/) for natural language processing
|
|
155
|
+
|
|
156
|
+
## Support
|
|
157
|
+
|
|
158
|
+
For issues and questions:
|
|
159
|
+
1. Check existing GitHub issues
|
|
160
|
+
2. Create a new issue with detailed information
|
|
161
|
+
3. Include EPUB file samples if relevant (ensure no copyright restrictions)βοΌβfile_pathβ:
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<div align=center>
|
|
2
|
+
<h1>EPUB to Speech</h1>
|
|
3
|
+
<p>English | <a href="./README_zh-CN.md">δΈζ</a></p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
Convert EPUB e-books into high-quality audiobooks using Azure Text-to-Speech technology.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **π EPUB Support**: Compatible with EPUB 2 and EPUB 3 formats
|
|
11
|
+
- **ποΈ High-Quality TTS**: Uses Azure Cognitive Services Speech for natural voice synthesis
|
|
12
|
+
- **π Multi-Language Support**: Supports various languages and voices via Azure TTS
|
|
13
|
+
- **π± M4B Output**: Generates standard M4B audiobook format with chapter navigation
|
|
14
|
+
- **π§ CLI Interface**: Easy-to-use command-line tool with progress tracking
|
|
15
|
+
|
|
16
|
+
## Basic Usage
|
|
17
|
+
|
|
18
|
+
Convert an EPUB file to audiobook:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
epub2speech input.epub output.m4b --voice zh-CN-XiaoxiaoNeural --azure-key YOUR_KEY --azure-region YOUR_REGION
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### Prerequisites
|
|
27
|
+
|
|
28
|
+
- Python 3.11 or higher
|
|
29
|
+
- FFmpeg (for audio processing)
|
|
30
|
+
- Azure Speech Service credentials
|
|
31
|
+
|
|
32
|
+
### Install Dependencies
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Install Python dependencies
|
|
36
|
+
pip install poetry
|
|
37
|
+
poetry install
|
|
38
|
+
|
|
39
|
+
# Install FFmpeg
|
|
40
|
+
# macOS: brew install ffmpeg
|
|
41
|
+
# Ubuntu/Debian: sudo apt install ffmpeg
|
|
42
|
+
# Windows: Download from https://ffmpeg.org/download.html
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Azure Speech Service Setup
|
|
46
|
+
|
|
47
|
+
1. Create an Azure account at https://azure.microsoft.com
|
|
48
|
+
2. Create a Speech Service resource in Azure Portal
|
|
49
|
+
3. Get your subscription key and region from the Azure dashboard
|
|
50
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
### Environment Variables
|
|
54
|
+
|
|
55
|
+
Set your Azure credentials as environment variables:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export AZURE_SPEECH_KEY="your-subscription-key"
|
|
59
|
+
export AZURE_SPEECH_REGION="your-region"
|
|
60
|
+
|
|
61
|
+
epub2speech input.epub output.m4b --voice zh-CN-XiaoxiaoNeural
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Advanced Options
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Limit to first 5 chapters
|
|
68
|
+
epub2speech input.epub output.m4b --voice en-US-AriaNeural --max-chapters 5
|
|
69
|
+
|
|
70
|
+
# Use custom workspace directory
|
|
71
|
+
epub2speech input.epub output.m4b --voice zh-CN-YunxiNeural --workspace /tmp/my-workspace
|
|
72
|
+
|
|
73
|
+
# Quiet mode (no progress output)
|
|
74
|
+
epub2speech input.epub output.m4b --voice ja-JP-NanamiNeural --quiet
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Available Voices
|
|
78
|
+
|
|
79
|
+
For a complete list, see [Azure Neural Voices](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support#neural-voices).
|
|
80
|
+
|
|
81
|
+
## How It Works
|
|
82
|
+
|
|
83
|
+
1. **EPUB Parsing**: Extracts text content and metadata from EPUB files
|
|
84
|
+
2. **Chapter Detection**: Identifies chapters using EPUB navigation data
|
|
85
|
+
3. **Text Processing**: Cleans and segments text for optimal speech synthesis
|
|
86
|
+
4. **Audio Generation**: Converts text to speech using Azure TTS
|
|
87
|
+
5. **M4B Creation**: Combines audio files with chapter metadata into M4B format
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
### Running Tests
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
python test.py
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Run specific test modules:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python test.py --test test_epub_picker
|
|
101
|
+
python test.py --test test_tts
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Contributing
|
|
105
|
+
|
|
106
|
+
Contributions are welcome! Please feel free to submit issues or pull requests.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
111
|
+
|
|
112
|
+
## Acknowledgments
|
|
113
|
+
|
|
114
|
+
- [Azure Cognitive Services](https://azure.microsoft.com/services/cognitive-services/) for text-to-speech technology
|
|
115
|
+
- [ebooklib](https://github.com/aerkalov/ebooklib) for EPUB parsing
|
|
116
|
+
- [FFmpeg](https://ffmpeg.org/) for audio processing
|
|
117
|
+
- [spaCy](https://spacy.io/) for natural language processing
|
|
118
|
+
|
|
119
|
+
## Support
|
|
120
|
+
|
|
121
|
+
For issues and questions:
|
|
122
|
+
1. Check existing GitHub issues
|
|
123
|
+
2. Create a new issue with detailed information
|
|
124
|
+
3. Include EPUB file samples if relevant (ensure no copyright restrictions)βοΌβfile_pathβ:
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
import numpy as np
|
|
3
|
+
import soundfile as sf
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Optional, Callable, Generator
|
|
6
|
+
from spacy.lang.xx import MultiLanguage
|
|
7
|
+
from spacy.language import Language
|
|
8
|
+
from spacy.tokens import Span
|
|
9
|
+
|
|
10
|
+
from .tts import TextToSpeechProtocol
|
|
11
|
+
from resource_segmentation import split, Resource, Segment
|
|
12
|
+
|
|
13
|
+
SEGMENT_LEVEL = 1
|
|
14
|
+
SENTENCE_LEVEL = 2
|
|
15
|
+
|
|
16
|
+
class ChapterTTS:
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
tts_protocol: TextToSpeechProtocol,
|
|
20
|
+
sample_rate: int = 24000,
|
|
21
|
+
max_segment_length: int = 500,
|
|
22
|
+
language_model: Optional[str] = None
|
|
23
|
+
):
|
|
24
|
+
self.tts_protocol = tts_protocol
|
|
25
|
+
self.sample_rate = sample_rate
|
|
26
|
+
self.max_segment_length = max_segment_length
|
|
27
|
+
self._nlp = self._load_language_model(language_model)
|
|
28
|
+
|
|
29
|
+
def _load_language_model(self, language_model: Optional[str]) -> Language:
|
|
30
|
+
if language_model:
|
|
31
|
+
try:
|
|
32
|
+
import spacy
|
|
33
|
+
return spacy.load(language_model)
|
|
34
|
+
except OSError:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
nlp: Language = MultiLanguage()
|
|
38
|
+
nlp.add_pipe("sentencizer")
|
|
39
|
+
return nlp
|
|
40
|
+
|
|
41
|
+
def process_chapter(
|
|
42
|
+
self,
|
|
43
|
+
text: str,
|
|
44
|
+
output_path: Path,
|
|
45
|
+
workspace_path: Path,
|
|
46
|
+
voice: str,
|
|
47
|
+
progress_callback: Optional[Callable[[int, int], None]] = None
|
|
48
|
+
) -> None:
|
|
49
|
+
# TODO: ιζ temp ι»θΎοΌδΈιθ¦ temp θΏδΈͺζ¦εΏ΅δΊ
|
|
50
|
+
segments = list(self.split_text_into_segments(text))
|
|
51
|
+
if not segments:
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
audio_segments = []
|
|
55
|
+
temp_files_created = []
|
|
56
|
+
try:
|
|
57
|
+
for i, segment in enumerate(segments):
|
|
58
|
+
if progress_callback:
|
|
59
|
+
progress_callback(i + 1, len(segments))
|
|
60
|
+
|
|
61
|
+
session_id = str(uuid.uuid4())[:8]
|
|
62
|
+
temp_audio_path = workspace_path / f"{session_id}_segment_{i:04d}.wav"
|
|
63
|
+
temp_files_created.append(temp_audio_path)
|
|
64
|
+
|
|
65
|
+
self.tts_protocol.convert_text_to_audio(
|
|
66
|
+
text=segment,
|
|
67
|
+
output_path=temp_audio_path,
|
|
68
|
+
voice=voice
|
|
69
|
+
)
|
|
70
|
+
if not temp_audio_path.exists():
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
audio_data: np.ndarray
|
|
74
|
+
sr: int
|
|
75
|
+
audio_data, sr = sf.read(temp_audio_path)
|
|
76
|
+
if sr != self.sample_rate:
|
|
77
|
+
pass
|
|
78
|
+
audio_segments.append(audio_data)
|
|
79
|
+
|
|
80
|
+
final_audio = np.concatenate(audio_segments)
|
|
81
|
+
sf.write(output_path, final_audio, self.sample_rate)
|
|
82
|
+
|
|
83
|
+
finally:
|
|
84
|
+
for temp_file in temp_files_created:
|
|
85
|
+
if temp_file.exists():
|
|
86
|
+
temp_file.unlink()
|
|
87
|
+
|
|
88
|
+
def split_text_into_segments(self, text: str) -> Generator[str, None, None]:
|
|
89
|
+
text = text.strip()
|
|
90
|
+
if not text:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
all_resources = []
|
|
94
|
+
doc = self._nlp(text)
|
|
95
|
+
next_start_incision = 2
|
|
96
|
+
|
|
97
|
+
for sent in doc.sents:
|
|
98
|
+
segment_text = sent.text.strip()
|
|
99
|
+
if not segment_text:
|
|
100
|
+
continue
|
|
101
|
+
|
|
102
|
+
resources = list(self._build_segment_internal_structure(sent))
|
|
103
|
+
if not resources:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
resources[0].start_incision = next_start_incision
|
|
107
|
+
resources[-1].end_incision = 2
|
|
108
|
+
next_start_incision = 2
|
|
109
|
+
|
|
110
|
+
all_resources.extend(resources)
|
|
111
|
+
|
|
112
|
+
if not all_resources:
|
|
113
|
+
text_resource = Resource(
|
|
114
|
+
count=len(text),
|
|
115
|
+
start_incision=SENTENCE_LEVEL,
|
|
116
|
+
end_incision=SENTENCE_LEVEL,
|
|
117
|
+
payload=text
|
|
118
|
+
)
|
|
119
|
+
all_resources.append(text_resource)
|
|
120
|
+
|
|
121
|
+
yield from self._split_by_resource_segmentation(all_resources)
|
|
122
|
+
|
|
123
|
+
def _build_segment_internal_structure(self, sent: Span) -> Generator[Resource, None, None]:
|
|
124
|
+
current_fragment: list[str] = []
|
|
125
|
+
for token in sent:
|
|
126
|
+
if token.is_punct:
|
|
127
|
+
if current_fragment:
|
|
128
|
+
fragment_text = "".join(current_fragment)
|
|
129
|
+
fragment_resource = Resource(
|
|
130
|
+
count=len(fragment_text),
|
|
131
|
+
start_incision=SEGMENT_LEVEL,
|
|
132
|
+
end_incision=SEGMENT_LEVEL,
|
|
133
|
+
payload=fragment_text
|
|
134
|
+
)
|
|
135
|
+
yield fragment_resource
|
|
136
|
+
current_fragment = []
|
|
137
|
+
|
|
138
|
+
punct_resource = Resource(
|
|
139
|
+
count=len(token.text),
|
|
140
|
+
start_incision=SEGMENT_LEVEL,
|
|
141
|
+
end_incision=SEGMENT_LEVEL,
|
|
142
|
+
payload=token.text
|
|
143
|
+
)
|
|
144
|
+
yield punct_resource
|
|
145
|
+
else:
|
|
146
|
+
current_fragment.append(token.text_with_ws)
|
|
147
|
+
|
|
148
|
+
if current_fragment:
|
|
149
|
+
fragment_text = "".join(current_fragment)
|
|
150
|
+
fragment_resource = Resource(
|
|
151
|
+
count=len(fragment_text),
|
|
152
|
+
start_incision=SEGMENT_LEVEL,
|
|
153
|
+
end_incision=SEGMENT_LEVEL,
|
|
154
|
+
payload=fragment_text
|
|
155
|
+
)
|
|
156
|
+
yield fragment_resource
|
|
157
|
+
|
|
158
|
+
def _split_by_resource_segmentation(self, resources: List[Resource]) -> Generator[str, None, None]:
|
|
159
|
+
max_byte_length = self.max_segment_length * 3
|
|
160
|
+
groups = list(split(
|
|
161
|
+
iter(resources),
|
|
162
|
+
max_segment_count=max_byte_length,
|
|
163
|
+
border_incision=1,
|
|
164
|
+
gap_rate=0.0,
|
|
165
|
+
tail_rate=0.0
|
|
166
|
+
))
|
|
167
|
+
|
|
168
|
+
for group in groups:
|
|
169
|
+
segment_chars = []
|
|
170
|
+
for item in group.body:
|
|
171
|
+
if isinstance(item, Segment):
|
|
172
|
+
for resource in item.resources:
|
|
173
|
+
segment_chars.append(resource.payload)
|
|
174
|
+
elif isinstance(item, Resource):
|
|
175
|
+
segment_chars.append(item.payload)
|
|
176
|
+
|
|
177
|
+
combined_text = "".join(segment_chars).strip()
|
|
178
|
+
if combined_text:
|
|
179
|
+
yield combined_text
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .convertor import convert_epub_to_m4b, ConversionProgress
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def progress_callback(progress: ConversionProgress) -> None:
|
|
11
|
+
print(f"Progress: {progress.progress:.1f}% - Chapter {progress.current_chapter}/{progress.total_chapters}: {progress.chapter_title}")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
parser = argparse.ArgumentParser(
|
|
16
|
+
description="Convert EPUB files to audiobooks (M4B format)",
|
|
17
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
18
|
+
epilog="""
|
|
19
|
+
Examples:
|
|
20
|
+
%(prog)s input.epub output.m4b --voice zh-CN-XiaoxiaoNeural
|
|
21
|
+
%(prog)s input.epub output.m4b --voice zh-CN-XiaoxiaoNeural --max-chapters 5
|
|
22
|
+
%(prog)s input.epub output.m4b --voice zh-CN-XiaoxiaoNeural --workspace /tmp/workspace
|
|
23
|
+
"""
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"epub_path",
|
|
28
|
+
type=str,
|
|
29
|
+
help="Input EPUB file path"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"output_path",
|
|
34
|
+
type=str,
|
|
35
|
+
help="Output M4B file path"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--voice",
|
|
40
|
+
type=str,
|
|
41
|
+
default="zh-CN-XiaoxiaoNeural",
|
|
42
|
+
help="TTS voice name (default: zh-CN-XiaoxiaoNeural)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--max-chapters",
|
|
47
|
+
type=int,
|
|
48
|
+
help="Maximum number of chapters to convert (optional)"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--workspace",
|
|
53
|
+
type=str,
|
|
54
|
+
help="Workspace directory path (default: system temp directory)"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--azure-key",
|
|
59
|
+
type=str,
|
|
60
|
+
default=os.environ.get("AZURE_SPEECH_KEY"),
|
|
61
|
+
help="Azure Speech Service Key (can also be set via AZURE_SPEECH_KEY environment variable)"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--azure-region",
|
|
66
|
+
type=str,
|
|
67
|
+
default=os.environ.get("AZURE_SPEECH_REGION"),
|
|
68
|
+
help="Azure Speech Service region (can also be set via AZURE_SPEECH_REGION environment variable)"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--quiet",
|
|
73
|
+
action="store_true",
|
|
74
|
+
help="Quiet mode, do not show progress information"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
args = parser.parse_args()
|
|
78
|
+
|
|
79
|
+
epub_path = Path(args.epub_path)
|
|
80
|
+
if not epub_path.exists():
|
|
81
|
+
print(f"Error: EPUB file does not exist: {epub_path}", file=sys.stderr)
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
if not epub_path.suffix.lower() == '.epub':
|
|
85
|
+
print(f"Error: Input file must be in EPUB format: {epub_path}", file=sys.stderr)
|
|
86
|
+
sys.exit(1)
|
|
87
|
+
|
|
88
|
+
if not args.azure_key or not args.azure_region:
|
|
89
|
+
print("Error: Azure Speech Service credentials must be provided", file=sys.stderr)
|
|
90
|
+
print("Please provide via --azure-key and --azure-region parameters, or set AZURE_SPEECH_KEY and AZURE_SPEECH_REGION environment variables", file=sys.stderr)
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
if args.workspace:
|
|
94
|
+
workspace = Path(args.workspace)
|
|
95
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
else:
|
|
97
|
+
import tempfile
|
|
98
|
+
workspace = Path(tempfile.mkdtemp(prefix="epub2speech_"))
|
|
99
|
+
|
|
100
|
+
output_path = Path(args.output_path)
|
|
101
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
from .tts.azure_provider import AzureTextToSpeech
|
|
105
|
+
tts_provider = AzureTextToSpeech(
|
|
106
|
+
subscription_key=args.azure_key,
|
|
107
|
+
region=args.azure_region,
|
|
108
|
+
default_voice=args.voice
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
print(f"Starting conversion: {epub_path.name}")
|
|
112
|
+
print(f"Output file: {output_path}")
|
|
113
|
+
print(f"Workspace: {workspace}")
|
|
114
|
+
print(f"Using voice: {args.voice}")
|
|
115
|
+
if args.max_chapters:
|
|
116
|
+
print(f"Maximum chapters: {args.max_chapters}")
|
|
117
|
+
print()
|
|
118
|
+
|
|
119
|
+
result_path = convert_epub_to_m4b(
|
|
120
|
+
epub_path=epub_path,
|
|
121
|
+
workspace=workspace,
|
|
122
|
+
output_path=output_path,
|
|
123
|
+
tts_protocol=tts_provider,
|
|
124
|
+
voice=args.voice,
|
|
125
|
+
max_chapters=args.max_chapters,
|
|
126
|
+
progress_callback=None if args.quiet else progress_callback
|
|
127
|
+
)
|
|
128
|
+
if result_path:
|
|
129
|
+
print(f"\nConversion complete! Output file: {result_path}")
|
|
130
|
+
print(f"File size: {result_path.stat().st_size / (1024*1024):.1f} MB")
|
|
131
|
+
else:
|
|
132
|
+
print("\nConversion failed: no output file generated", file=sys.stderr)
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
except KeyboardInterrupt:
|
|
136
|
+
print("\nConversion interrupted by user", file=sys.stderr)
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f"\nConversion failed: {e}", file=sys.stderr)
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
main()
|