-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patheli-tts.swift
More file actions
132 lines (115 loc) · 4.38 KB
/
eli-tts.swift
File metadata and controls
132 lines (115 loc) · 4.38 KB
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
/// eli-tts — persistent text-to-speech service for macOS
/// Reads text lines from stdin, speaks them using AVSpeechSynthesizer.
///
/// Protocol:
/// stdin: text to speak (one line = one utterance)
/// stdin: STOP — interrupt current utterance
/// stdout: DONE — utterance finished (or stopped)
/// stderr: "TTS: ready" — ready for input
import AppKit
import AVFoundation
var voiceName = ""
var rate: Float = AVSpeechUtteranceDefaultSpeechRate
// Parse args
var i = 1
while i < CommandLine.arguments.count {
let arg = CommandLine.arguments[i]
switch arg {
case "-v", "--voice":
i += 1
voiceName = CommandLine.arguments[i]
case "-r", "--rate":
i += 1
rate = Float(CommandLine.arguments[i]) ?? AVSpeechUtteranceDefaultSpeechRate
case "-h", "--help":
fputs("eli-tts — text-to-speech service\n", stderr)
fputs("Usage: eli-tts [-v voice] [-r rate]\n", stderr)
fputs(" -v Voice name or identifier (default: system default)\n", stderr)
fputs(" -r Speech rate (0.0-1.0, default: \(AVSpeechUtteranceDefaultSpeechRate))\n", stderr)
fputs("Reads text lines from stdin, speaks them, prints DONE after each.\n", stderr)
fputs("\nAvailable voices:\n", stderr)
for voice in AVSpeechSynthesisVoice.speechVoices() {
if voice.language.hasPrefix("en") {
fputs(" \(voice.name) [\(voice.language)] quality=\(voice.quality.rawValue)\n", stderr)
}
}
exit(0)
default:
break
}
i += 1
}
signal(SIGTERM) { _ in exit(0) }
signal(SIGINT) { _ in exit(0) }
class TTSDelegate: NSObject, AVSpeechSynthesizerDelegate {
var onFinish: (() -> Void)?
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
onFinish?()
}
}
class AppDelegate: NSObject, NSApplicationDelegate {
let synthesizer = AVSpeechSynthesizer()
let ttsDelegate = TTSDelegate()
var selectedVoice: AVSpeechSynthesisVoice?
func applicationDidFinishLaunching(_ notification: Notification) {
synthesizer.delegate = ttsDelegate
// Find voice
if !voiceName.isEmpty {
let voices = AVSpeechSynthesisVoice.speechVoices()
let matches = voices.filter { $0.name.localizedCaseInsensitiveContains(voiceName) }
selectedVoice = matches.max(by: { $0.quality.rawValue < $1.quality.rawValue })
?? voices.first { $0.identifier.localizedCaseInsensitiveContains(voiceName) }
if let v = selectedVoice {
fputs("TTS: using voice \(v.name) [\(v.language)]\n", stderr)
} else {
fputs("TTS: voice '\(voiceName)' not found, using default\n", stderr)
}
}
fputs("TTS: ready\n", stderr)
// Read text lines on background thread
DispatchQueue.global(qos: .userInitiated).async {
while let line = readLine() {
let text = line.trimmingCharacters(in: .whitespacesAndNewlines)
guard !text.isEmpty else { continue }
if text == "STOP" {
DispatchQueue.main.async {
if self.synthesizer.isSpeaking {
self.synthesizer.stopSpeaking(at: .immediate)
}
}
continue
}
let sem = DispatchSemaphore(value: 0)
DispatchQueue.main.async {
self.speak(text) { sem.signal() }
}
sem.wait()
}
// stdin closed — wait for any in-progress speech
DispatchQueue.main.async {
if self.synthesizer.isSpeaking {
self.ttsDelegate.onFinish = { exit(0) }
} else {
exit(0)
}
}
}
}
func speak(_ text: String, completion: (() -> Void)? = nil) {
let utterance = AVSpeechUtterance(string: text)
utterance.rate = rate
if let voice = selectedVoice {
utterance.voice = voice
}
ttsDelegate.onFinish = {
print("DONE")
fflush(stdout)
completion?()
}
synthesizer.speak(utterance)
}
}
let delegate = AppDelegate()
let app = NSApplication.shared
app.delegate = delegate
app.run()