-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathreader.py
338 lines (276 loc) · 10.7 KB
/
reader.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
#!/usr/bin/env python3
"""
Web Article to Audio CLI Program
A command-line tool that extracts article text from a URL,
converts it to speech, and saves the audio file.
Usage:
python reader.py # Interactive mode: prompts for URL
python reader.py [URL] # Process a web article URL
python reader.py url [URL] # Same as above
python reader.py config # View configuration
python reader.py config key=value # Set configuration
python reader.py config engines # List available TTS engines
python reader.py --help # Show help message
Optional arguments:
--verbose, -v # Enable verbose output
--quiet, -q # Suppress non-error output
--output, -o FOLDER # Set output folder
--engine, -e ENGINE # Select TTS engine (gtts, edge, system)
--open # Open folder containing the file when done
--play # Play audio with QuickTime (macOS only)
"""
import sys
import argparse
from datetime import datetime
# Import our modules
from src.utils import log, args_cache, exit_with_error, EXIT_SUCCESS, EXIT_INVALID_ARGS
from src.config import load_config, setup_config, display_config, modify_config
from src.extractor import extract_article
from src.tts import text_to_speech, get_available_tts_engines
from src.player import get_file_url, open_file_location, play_with_quicktime
from src.utils import sanitize_filename
def prompt_for_url():
"""Prompt the user to enter a URL"""
print("\nEnter a URL to convert to audio:")
url = input("> ").strip()
if not url:
print("No URL provided. Exiting.")
return None
return url
def parse_arguments():
"""Parse command line arguments"""
parser = argparse.ArgumentParser(
description="Convert web articles to audio and save them to a file."
)
# Create subparsers for different commands
subparsers = parser.add_subparsers(dest="command", help="Command to run")
# Config command
config_parser = subparsers.add_parser("config", help="View or modify configuration")
config_parser.add_argument(
"setting",
nargs="?",
help="Setting to change in format key=value or 'engines' to list TTS engines",
)
# URL command (default)
url_parser = subparsers.add_parser("url", help="Process a web article URL")
url_parser.add_argument("url_value", help="URL of the web article to convert")
url_parser.add_argument(
"--output", "-o", help="Specify output folder for this conversion"
)
url_parser.add_argument(
"--engine", "-e", help="Specify TTS engine to use (gtts, edge, system)"
)
url_parser.add_argument(
"--open", action="store_true", help="Open folder containing the file when done"
)
url_parser.add_argument(
"--play", action="store_true", help="Play audio with QuickTime (macOS only)"
)
# Add global optional arguments
parser.add_argument(
"--verbose", "-v", action="store_true", help="Enable verbose output"
)
parser.add_argument(
"--quiet", "-q", action="store_true", help="Suppress all non-error output"
)
# Also allow URL as a positional argument to the main parser (for backward compatibility)
parser.add_argument("url", nargs="?", help="URL of the web article to convert")
parser.add_argument(
"--output", "-o", help="Specify output folder for this conversion"
)
parser.add_argument(
"--engine", "-e", help="Specify TTS engine to use (gtts, edge, system)"
)
parser.add_argument(
"--open", action="store_true", help="Open folder containing the file when done"
)
parser.add_argument(
"--play", action="store_true", help="Play audio with QuickTime (macOS only)"
)
args = parser.parse_args()
# Store the TTS engine in args for use by the TTS module
if hasattr(args, "engine") and args.engine:
args.tts_engine = args.engine
return args
def process_url(
url,
config,
output=None,
engine=None,
open_folder=False,
play=False,
):
"""Process a URL and convert it to audio"""
import os
import time
from src.utils import validate_folder
# Set output folder
output_folder = output if output else config["Settings"]["save_folder"]
log(f"Using output folder: {output_folder}", "verbose")
# Set TTS engine
tts_engine = engine if engine else config["Settings"]["tts_engine"]
# Check if the engine is valid
available_engines = get_available_tts_engines()
if tts_engine not in available_engines:
log(f"TTS engine '{tts_engine}' not available. Using default.", "warning")
tts_engine = next(iter(available_engines.keys())) # Use first available engine
log(f"Using TTS engine: {tts_engine} ({available_engines[tts_engine]})", "verbose")
# Validate output folder
if not validate_folder(output_folder):
log("Invalid output folder. Exiting.", "error")
return
# Extract article from URL
log(f"Processing URL: {url}", "info")
title, text, success = extract_article(url)
if not success:
log("Article extraction failed. Exiting.", "error")
return
# Generate filename base from title
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
sanitized_title = sanitize_filename(title)
basename = f"{sanitized_title}_{timestamp}"
# Create file paths for both audio and transcript
audio_path = os.path.join(output_folder, f"{basename}.mp3")
transcript_path = os.path.join(output_folder, f"{basename}.txt")
log(f"Article extracted successfully:", "info")
log(f"Title: {title}", "info")
log(f"Text length: {len(text)} characters", "info")
log(f"Output audio file: {audio_path}", "info")
log(f"Output transcript file: {transcript_path}", "info")
# Save transcript file
try:
with open(transcript_path, "w", encoding="utf-8") as f:
f.write(f"Title: {title}\n\n")
f.write(f"Source URL: {url}\n\n")
f.write(text)
log("Transcript saved successfully", "success")
except Exception as e:
log(f"Failed to save transcript: {str(e)}", "error")
# Convert text to speech
log("Starting text-to-speech conversion...", "info")
start_time = time.time()
tts_success = text_to_speech(text, audio_path, tts_engine)
end_time = time.time()
if not tts_success:
log("Text-to-speech conversion failed. Exiting.", "error")
return
duration = end_time - start_time
log(f"Text-to-speech conversion completed in {duration:.1f} seconds", "info")
# Get the URL/path to the files
audio_url = get_file_url(audio_path)
transcript_url = get_file_url(transcript_path)
# Print success message with the file locations
log("Files created successfully:", "success")
log(f"Audio: {audio_path}", "success")
log(f"Transcript: {transcript_path}", "success")
log(f"Audio URL: {audio_url}", "success")
log(f"Transcript URL: {transcript_url}", "success")
# Open folder if requested
if open_folder:
log("Opening file location...", "info")
if open_file_location(audio_path):
log("File location opened in file explorer", "success")
else:
log("Could not open file location", "warning")
# Play audio with QuickTime if requested
if play:
log("Opening audio in QuickTime Player...", "info")
if play_with_quicktime(audio_path):
log("Audio playing in QuickTime Player", "success")
else:
log("Could not play audio in QuickTime Player", "warning")
return audio_path
def prompt_for_play_options():
"""Prompt the user for playback options"""
print("\nPlayback options:")
print("1. No playback (just save the file)")
print("2. Open the file location")
print("3. Play with QuickTime (macOS only)")
choice = input("\nEnter your choice (1-3) [3]: ").strip() or "3"
open_folder = False
play = False
if choice == "2":
open_folder = True
elif choice == "3" or choice == "":
play = True
return open_folder, play
def interactive_mode(config):
"""Run the program in interactive mode by prompting for input"""
# Ensure configuration is set up
if not config["Settings"]["save_folder"]:
log("First-time setup required.")
config = setup_config(config)
# Prompt for URL
url = prompt_for_url()
if not url:
return EXIT_INVALID_ARGS
# Prompt for playback options
open_folder, play = prompt_for_play_options()
# Process the URL with the selected options
process_url(
url,
config,
output=None, # Use default from config
engine=None, # Use default from config
open_folder=open_folder,
play=play,
)
return EXIT_SUCCESS
def main():
"""Main function"""
global args_cache
from src.utils import set_args_cache
# Parse arguments and store for logging functions
args = parse_arguments()
set_args_cache(args)
# Load configuration
config = load_config()
# Process different commands
if args.command == "config":
# Config command handling
if args.setting:
config = modify_config(config, args.setting)
else:
display_config(config)
return EXIT_SUCCESS
elif args.command == "url":
# URL command handling
process_url(
args.url_value,
config,
args.output,
args.engine,
args.open,
args.play,
)
return EXIT_SUCCESS
# If no command specified but URL is provided as positional argument
if args.url:
process_url(
args.url,
config,
args.output,
args.engine,
args.open,
args.play,
)
return EXIT_SUCCESS
# If no command or URL, enter interactive mode
if len(sys.argv) == 1:
return interactive_mode(config)
else:
# Some arguments were provided but no recognized command
print("Invalid command or missing required arguments.")
print("Use --help for more information")
return EXIT_INVALID_ARGS
if __name__ == "__main__":
try:
exit_code = main()
sys.exit(exit_code)
except KeyboardInterrupt:
print("\nOperation cancelled by user")
sys.exit(130) # Standard exit code for Ctrl+C
except Exception as e:
from src.utils import EXIT_UNKNOWN_ERROR
print(f"Unexpected error: {str(e)}")
sys.exit(EXIT_UNKNOWN_ERROR)