podcast-buddy 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +14 -0
- data/exe/podcast_buddy +250 -136
- data/lib/podcast_buddy/system_dependency.rb +19 -1
- data/lib/podcast_buddy/version.rb +1 -1
- data/lib/podcast_buddy.rb +95 -7
- metadata +45 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e31b071eb8f81295b7dea19da2f25273d7e2b7fc46452fb6f8aba2796fbe2cef
|
4
|
+
data.tar.gz: eddb056050f53b6cacda0240f64aff01a211a1d84b8fbd13a19bc1d5c6c79421
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af3c610c3bfa036bdb9d0a55898824a72d435552b76017713b76e93793d6806f9826be8f496d325f89f4dbb4687328de79cb48c8c0e2234935a16db5e22e812f
|
7
|
+
data.tar.gz: 71a2cfef11c518009357f4b0e91d178118141b6f2ea371553428064bb583b675a499c36ee763f4c9c7f5c49fe7071c71dfc8c086e5b62f4384030c58b4abc985
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.2.0] - 2024-08-13
|
4
|
+
|
5
|
+
* 9edc409 - Refactor to make use of Async
|
6
|
+
* Now properly generates show notes on exit
|
7
|
+
* 7d8db13 - Update show notes prompt to cite sources
|
8
|
+
* c3ef97d - Allow configuring the whisper model to use
|
9
|
+
* e7cb278 - Adds session naming to store files in specific directories
|
10
|
+
* 39e9e78 - Overwrite summary on every periodic update
|
11
|
+
* 0518b56 - Update topic extraction prompt to fix 'Empty response' topics
|
12
|
+
* b72ba8f - Use brew list -1 to /dev/null to quiet initial outputs when already setup
|
13
|
+
* 98a2484 - Remove no longer needed commented code in Signal.trap now that we're Ansync
|
14
|
+
* 62cf6ea - Update periodic summarize/topic calls to use new PodcastBuddy.current_transcript method, and update other current_* methods to re-use the memoized versions
|
15
|
+
|
16
|
+
|
3
17
|
## [0.1.1] - 2024-08-09
|
4
18
|
|
5
19
|
- Fixes typo in topic extraction prompt ([7e07e](https://github.com/codenamev/podcast-buddy/commit/7e07e307135c95cb4bb68dadc354f0b2519c7721))
|
data/exe/podcast_buddy
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
require_relative "../lib/podcast_buddy"
|
4
4
|
require "optparse"
|
5
|
+
require "rainbow"
|
6
|
+
require "timeout"
|
7
|
+
require "fileutils"
|
5
8
|
|
6
9
|
options = {}
|
7
10
|
OptionParser.new do |opts|
|
@@ -10,8 +13,31 @@ OptionParser.new do |opts|
|
|
10
13
|
opts.on("--debug", "Run in debug mode") do |v|
|
11
14
|
options[:debug] = v
|
12
15
|
end
|
16
|
+
|
17
|
+
opts.on("-w", "--whisper MODEL", "Use specific whisper model (default: small.en)") do |v|
|
18
|
+
options[:whisper_model] = v
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("-n", "--name NAME", "A name for the session to label all log files") do |v|
|
22
|
+
options[:name] = v
|
23
|
+
end
|
13
24
|
end.parse!
|
14
25
|
|
26
|
+
def to_human(text, label = :info)
|
27
|
+
case label.to_sym
|
28
|
+
when :info
|
29
|
+
Rainbow(text).blue
|
30
|
+
when :wait
|
31
|
+
Rainbow(text).yellow
|
32
|
+
when :input
|
33
|
+
Rainbow(text).black.bg(:yellow)
|
34
|
+
when :success
|
35
|
+
Rainbow(text).green
|
36
|
+
else
|
37
|
+
text
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
15
41
|
PodcastBuddy.logger.formatter = proc do |severity, datetime, progname, msg|
|
16
42
|
if severity.to_s == "INFO"
|
17
43
|
"#{msg}\n"
|
@@ -21,212 +47,300 @@ PodcastBuddy.logger.formatter = proc do |severity, datetime, progname, msg|
|
|
21
47
|
end
|
22
48
|
|
23
49
|
if options[:debug]
|
24
|
-
PodcastBuddy.logger.info "Turning on debug mode..."
|
50
|
+
PodcastBuddy.logger.info to_human("Turning on debug mode...", :info)
|
25
51
|
PodcastBuddy.logger.level = Logger::DEBUG
|
26
52
|
else
|
27
53
|
PodcastBuddy.logger.level = Logger::INFO
|
28
54
|
end
|
29
55
|
|
56
|
+
if options[:whisper_model]
|
57
|
+
PodcastBuddy.whisper_model = options[:whisper_model]
|
58
|
+
PodcastBuddy.logger.info to_human("Using whisper model: #{options[:whisper_model]}", :info)
|
59
|
+
end
|
60
|
+
|
61
|
+
if options[:name]
|
62
|
+
base_path = "#{PodcastBuddy.root}/tmp/#{options[:name]}"
|
63
|
+
FileUtils.mkdir_p base_path
|
64
|
+
PodcastBuddy.session = options[:name]
|
65
|
+
PodcastBuddy.logger.info to_human("Using custom session name: #{options[:name]}", :info)
|
66
|
+
PodcastBuddy.logger.info to_human(" Saving files to: #{PodcastBuddy.session}", :info)
|
67
|
+
end
|
68
|
+
|
30
69
|
PodcastBuddy.logger.info "Setting up dependencies..."
|
31
70
|
PodcastBuddy.setup
|
32
71
|
PodcastBuddy.logger.info "Setup complete."
|
33
72
|
|
34
73
|
# OpenAI API client setup
|
35
|
-
|
36
|
-
|
37
|
-
client = OpenAI::Client.new(access_token: ENV["OPENAI_ACCESS_TOKEN"], log_errors: true)
|
38
|
-
|
39
|
-
# Signals for question detection
|
40
|
-
question_signal = PodcastBuddy::PodSignal.new
|
41
|
-
end_signal = PodcastBuddy::PodSignal.new
|
74
|
+
# Raises error if no OPENAI_ACCESS_TOKEN env is set
|
75
|
+
PodcastBuddy.openai_client
|
42
76
|
|
43
77
|
# Initialize variables
|
44
78
|
@latest_transcription = ""
|
45
|
-
@
|
46
|
-
@full_transcription = ""
|
79
|
+
@full_transcript = ""
|
47
80
|
@listening_for_question = false
|
48
|
-
@
|
81
|
+
@shutdown = false
|
49
82
|
@threads = []
|
50
83
|
@transcription_queue = Queue.new
|
84
|
+
@question_transcript = Queue.new
|
51
85
|
|
52
86
|
# Method to extract topics and summarize
|
53
|
-
def extract_topics_and_summarize(
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
if current_discussion.length > 1
|
58
|
-
messages << {role: "user", content: "Given the prior summary and topics:\n---\n#{current_discussion}\n---"}
|
59
|
-
messages << {role: "user", content: "Continue the running summary and list of topics using the following discussion:\n---#{text}\n---"}
|
60
|
-
else
|
61
|
-
messages << {role: "user", content: "Extract topics and summarize the following discussion:\n---#{text}\n---"}
|
87
|
+
def extract_topics_and_summarize(text)
|
88
|
+
Sync do |parent|
|
89
|
+
parent.async { update_topics(text) }
|
90
|
+
parent.async { update_summary(text) }
|
62
91
|
end
|
63
|
-
response = client.chat(parameters: {
|
64
|
-
model: "gpt-4o",
|
65
|
-
messages: messages,
|
66
|
-
max_tokens: 150
|
67
|
-
})
|
68
|
-
response.dig("choices", 0, "message", "content").strip
|
69
92
|
end
|
70
93
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
94
|
+
def update_topics(text)
|
95
|
+
Sync do
|
96
|
+
PodcastBuddy.logger.debug "Looking for topics related to: #{text}"
|
97
|
+
response = PodcastBuddy.openai_client.chat(parameters: {
|
98
|
+
model: "gpt-4o",
|
99
|
+
messages: [
|
100
|
+
{role: "system", content: "You are a kind and helpful podcast assistant diligantly extracting topics related to the discussion on the show. Your goal is to prepare this information in an optimal way for participants to ask questions in the context of what is being discussed, and optimally for use in the closing Show Notes."},
|
101
|
+
{role: "user", content: "Extract topics for the following Discussion:\n---\n#{text}\n---\n\nExclude any of these topics that have been previously discussed:\n---\n#{PodcastBuddy.current_topics}\n---\n\nIf there are no new topics to add, return NONE."}
|
102
|
+
],
|
103
|
+
max_tokens: 250
|
104
|
+
})
|
105
|
+
new_topics = response.dig("choices", 0, "message", "content").gsub("NONE", "").strip
|
106
|
+
PodcastBuddy.logger.debug "Updating topics: #{new_topics.split("\n").join(", ")}"
|
107
|
+
PodcastBuddy.add_to_topics(new_topics)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def update_summary(text)
|
112
|
+
Sync do
|
113
|
+
response = PodcastBuddy.openai_client.chat(parameters: {
|
114
|
+
model: "gpt-4o",
|
115
|
+
messages: [
|
116
|
+
{role: "system", content: "You are a kind and helpful podcast assistant diligantly keeping track of the overall discussion on the show. Your goal is to prepare a concise summary of the overall discussion."},
|
117
|
+
{role: "user", content: "Previous Summary:\n---\n#{PodcastBuddy.current_summary}\n---\n\nLatest Discussion:\n---\n#{text}\n---"},
|
118
|
+
{role: "user", content: "Use the Previous Summary, and Latest Discussion, to combine into a single cohesive summary.\nSummary: "}
|
119
|
+
],
|
120
|
+
max_tokens: 250
|
121
|
+
})
|
122
|
+
new_summary = response.dig("choices", 0, "message", "content").strip
|
123
|
+
PodcastBuddy.logger.debug "Updating summary: #{new_summary}"
|
124
|
+
PodcastBuddy.update_summary(new_summary)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def answer_question
|
129
|
+
Async do
|
130
|
+
latest_context = "#{PodcastBuddy.current_summary}\nTopics discussed recently:\n---\n#{PodcastBuddy.current_topics.split("\n").last(10)}\n---\n"
|
131
|
+
previous_discussion = @full_transcript.split("\n").last(25)
|
132
|
+
question = ""
|
133
|
+
question << @question_transcript.pop until @question_transcript.empty?
|
134
|
+
PodcastBuddy.logger.info "Answering question:\n#{question}"
|
135
|
+
PodcastBuddy.logger.debug "Context:\n---#{latest_context}\n---\nPrevious discussion:\n---#{previous_discussion}\n---\nAnswering question:\n---#{question}\n---"
|
136
|
+
response = PodcastBuddy.openai_client.chat(parameters: {
|
137
|
+
model: "gpt-4o",
|
138
|
+
messages: [
|
139
|
+
{role: "system", content: "You are a kind and helpful podcast assistant helping to answer questions as they come up during a recording. Keep the answer succinct and without speculation. Be playful, witty, and kind. If a question is not asked try to add to the conversation as it best relates to the current discussion. Remember, this is a live recording, so there's no need to analyze and report on what is being asked."},
|
140
|
+
{role: "user", content: "Context:\n#{latest_context}\n\nPrevious Discussion:\n#{previous_discussion}\n\nQuestion:\n#{question}\n\nAnswer:"}
|
141
|
+
],
|
142
|
+
max_tokens: 150
|
143
|
+
})
|
144
|
+
answer = response.dig("choices", 0, "message", "content").strip
|
145
|
+
PodcastBuddy.logger.debug "Answer: #{answer}"
|
146
|
+
text_to_speech(answer)
|
147
|
+
PodcastBuddy.logger.debug("Answer converted to speech: #{PodcastBuddy.answer_audio_file_path}")
|
148
|
+
play_answer
|
149
|
+
end
|
85
150
|
end
|
86
151
|
|
87
152
|
# Method to convert text to speech
|
88
|
-
def text_to_speech(
|
89
|
-
response =
|
153
|
+
def text_to_speech(text)
|
154
|
+
response = PodcastBuddy.openai_client.audio.speech(parameters: {
|
90
155
|
model: "tts-1",
|
91
156
|
input: text,
|
92
157
|
voice: "onyx",
|
93
158
|
response_format: "mp3",
|
94
159
|
speed: 1.0
|
95
160
|
})
|
96
|
-
File.binwrite(
|
161
|
+
File.binwrite(PodcastBuddy.answer_audio_file_path, response)
|
162
|
+
end
|
163
|
+
|
164
|
+
def play_answer
|
165
|
+
PodcastBuddy.logger.debug("Playing answer...")
|
166
|
+
system("afplay #{PodcastBuddy.answer_audio_file_path}")
|
97
167
|
end
|
98
168
|
|
99
169
|
# Method to handle audio stream processing
|
100
|
-
def process_audio_stream
|
101
|
-
|
102
|
-
whisper_command = "./whisper.cpp/stream -m ./whisper.cpp/models/ggml-#{PodcastBuddy.whisper_model}.bin -t 4 --step 0 --length 5000 --keep 500 --vad-thold 0.60 --audio-ctx 0 --keep-context -c 1"
|
170
|
+
def process_audio_stream
|
171
|
+
Sync do |parent|
|
103
172
|
PodcastBuddy.logger.info "Buffering audio..."
|
104
|
-
Open3.popen3(whisper_command) do |stdin, stdout, stderr,
|
105
|
-
|
106
|
-
|
107
|
-
end
|
108
|
-
stdout_thread = Thread.new do
|
109
|
-
stdout.each { |line| PodcastBuddy.whisper_logger.debug line }
|
110
|
-
end
|
173
|
+
Open3.popen3(PodcastBuddy.whisper_command) do |stdin, stdout, stderr, _thread|
|
174
|
+
error_log_task = parent.async { stderr.each { |line| PodcastBuddy.whisper_logger.error line } }
|
175
|
+
output_log_task = parent.async { stdout.each { |line| PodcastBuddy.whisper_logger.debug line } }
|
111
176
|
|
112
177
|
while (transcription = stdout.gets)
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
transcription_text = transcription_text.gsub(/\A\["\s?/, "")
|
118
|
-
transcription_text = transcription_text.gsub(/"\]\Z/, "")
|
119
|
-
break if @shutdown_flag
|
120
|
-
|
121
|
-
PodcastBuddy.logger.debug "Filtered: #{transcription_text}"
|
122
|
-
next if transcription_text.to_s.strip.squeeze.empty?
|
123
|
-
|
124
|
-
@full_transcription += transcription_text
|
125
|
-
|
126
|
-
if @listening_for_question
|
127
|
-
@question_transcription += transcription_text
|
128
|
-
PodcastBuddy.logger.info "Heard Question: #{transcription_text}"
|
129
|
-
else
|
130
|
-
@transcription_queue << transcription_text
|
131
|
-
PodcastBuddy.logger.info "Heard: #{transcription_text}"
|
132
|
-
PodcastBuddy.update_transcript(transcription_text)
|
133
|
-
end
|
178
|
+
PodcastBuddy.logger.debug("Shutdown: process_audio_stream...") and break if @shutdown
|
179
|
+
PodcastBuddy.logger.debug("Still listening...")
|
180
|
+
|
181
|
+
last_transcription_task = parent.async { process_transcription(transcription) }
|
134
182
|
end
|
135
183
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
stdin.close
|
140
|
-
stdout.close
|
141
|
-
stderr.close
|
184
|
+
error_log_task.wait
|
185
|
+
output_log_task.wait
|
186
|
+
last_transcription_task.wait
|
142
187
|
end
|
143
188
|
end
|
144
189
|
end
|
145
190
|
|
191
|
+
def process_transcription(line)
|
192
|
+
# Cleanup captured text from whisper
|
193
|
+
transcription_text = (line.scan(/\[[0-9]+.*[0-9]+\]\s?(.*)\n/) || [p])[0].to_s
|
194
|
+
PodcastBuddy.logger.debug "Received transcription: #{transcription_text}" if transcription_text
|
195
|
+
transcription_text = transcription_text.gsub("[BLANK_AUDIO]", "")
|
196
|
+
transcription_text = transcription_text.gsub(/\A\["\s?/, "")
|
197
|
+
transcription_text = transcription_text.gsub(/"\]\Z/, "")
|
198
|
+
|
199
|
+
return if transcription_text.to_s.strip.squeeze.empty?
|
200
|
+
|
201
|
+
@full_transcript += transcription_text
|
202
|
+
|
203
|
+
if @listening_for_question
|
204
|
+
PodcastBuddy.logger.info "Heard Question: #{transcription_text}"
|
205
|
+
@question_transcript << transcription_text
|
206
|
+
else
|
207
|
+
PodcastBuddy.logger.info "Heard: #{transcription_text}"
|
208
|
+
PodcastBuddy.update_transcript(transcription_text)
|
209
|
+
@transcription_queue << transcription_text
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
146
213
|
# Method to generate show notes
|
147
|
-
def generate_show_notes
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
214
|
+
def generate_show_notes
|
215
|
+
return if PodcastBuddy.current_transcript.strip.squeeze.empty?
|
216
|
+
|
217
|
+
PodcastBuddy.logger.info "Generting show notes..."
|
218
|
+
response = PodcastBuddy.openai_client.chat(parameters: {
|
219
|
+
model: "gpt-4o",
|
220
|
+
messages: [
|
221
|
+
{role: "system", content: "You are a kind and helpful podcast assistant helping to take notes for the show, and extract useful information being discussed for listeners."},
|
222
|
+
{role: "user", content: "Transcript:\n---\n#{PodcastBuddy.current_transcript}\n---\n\nTopics:\n---\n#{PodcastBuddy.current_topics}\n---\n\nUse the above transcript and topics to create Show Notes in markdown that outline the discussion. Extract a breif summary that describes the overall conversation, the people involved and their roles, and sentiment of the topics discussed. Follow the summary with a list of helpful links to any libraries, products, or other resources related to the discussion. Cite sources." }
|
223
|
+
],
|
224
|
+
max_tokens: 150
|
225
|
+
})
|
226
|
+
show_notes = response.dig("choices", 0, "message", "content").strip
|
227
|
+
File.open(PodcastBuddy.show_notes_file, "w") do |file|
|
228
|
+
file.puts show_notes
|
152
229
|
end
|
230
|
+
|
231
|
+
PodcastBuddy.logger.info to_human("Show notes saved to: #{PodcastBuddy.show_notes_file}", :success)
|
153
232
|
end
|
154
233
|
|
155
234
|
# Periodically summarize latest transcription
|
156
|
-
def periodic_summarization(
|
157
|
-
|
235
|
+
def periodic_summarization(interval = 15)
|
236
|
+
Sync do
|
158
237
|
loop do
|
159
|
-
break if @
|
238
|
+
PodcastBuddy.logger.debug("Shutdown: periodic_summarization...") and break if @shutdown
|
160
239
|
|
161
240
|
sleep interval
|
162
|
-
summarize_latest
|
241
|
+
summarize_latest
|
163
242
|
rescue => e
|
164
243
|
PodcastBuddy.logger.warn "[summarization] periodic summarization failed: #{e.message}"
|
165
244
|
end
|
166
245
|
end
|
167
246
|
end
|
168
247
|
|
169
|
-
def summarize_latest
|
248
|
+
def summarize_latest
|
170
249
|
latest_transcriptions = []
|
171
250
|
latest_transcriptions << @transcription_queue.pop until @transcription_queue.empty?
|
172
251
|
return if latest_transcriptions.join.strip.squeeze.empty?
|
173
252
|
|
174
253
|
PodcastBuddy.logger.debug "[periodic summarization] Latest transcript: #{latest_transcriptions.join}"
|
175
|
-
|
176
|
-
File.write(PodcastBuddy.discussion_log_file, summary)
|
254
|
+
extract_topics_and_summarize(latest_transcriptions.join)
|
177
255
|
end
|
178
256
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
# Wait for all threads to finish
|
184
|
-
@threads.compact.each(&:join)
|
257
|
+
def wait_for_question_end
|
258
|
+
Sync do |parent|
|
259
|
+
loop do
|
260
|
+
PodcastBuddy.logger.debug("Shutdown: wait_for_question_end...") and break if @shutdown
|
185
261
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
262
|
+
# The wait_for_question_start will signal this
|
263
|
+
sleep 0.1 and next if !@listening_for_question
|
264
|
+
|
265
|
+
input = ""
|
266
|
+
Timeout.timeout(5) do
|
267
|
+
input = gets
|
268
|
+
PodcastBuddy.logger.debug("Input received...") if input.include?("\n")
|
269
|
+
next unless input.to_s.include?("\n")
|
270
|
+
rescue Timeout::Error
|
271
|
+
# Let the loop continue so that it can catch @shutdown periodically, or
|
272
|
+
# capture the signal
|
273
|
+
PodcastBuddy.logger.debug("Input timeout...")
|
274
|
+
next
|
275
|
+
end
|
192
276
|
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
277
|
+
if input.empty?
|
278
|
+
next
|
279
|
+
else
|
280
|
+
PodcastBuddy.logger.info "End of question signal. Generating answer..."
|
281
|
+
@listening_for_question = false
|
282
|
+
answer_question.wait
|
283
|
+
PodcastBuddy.logger.info Rainbow("Press ").blue + Rainbow("Enter").black.bg(:yellow) + Rainbow(" to signal a question start...").blue
|
284
|
+
break
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
199
288
|
end
|
200
289
|
|
201
|
-
|
202
|
-
|
203
|
-
|
290
|
+
def wait_for_question_start
|
291
|
+
Sync do |parent|
|
292
|
+
PodcastBuddy.logger.info Rainbow("Press ").blue + Rainbow("Enter").black.bg(:yellow) + Rainbow(" to signal a question start...").blue
|
293
|
+
loop do
|
294
|
+
PodcastBuddy.logger.debug("Shutdown: wait_for_question...") and break if @shutdown
|
204
295
|
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
296
|
+
PodcastBuddy.logger.debug("Waiting for input...")
|
297
|
+
input = ""
|
298
|
+
Timeout.timeout(5) do
|
299
|
+
input = gets
|
300
|
+
PodcastBuddy.logger.debug("Input received...") if input.include?("\n")
|
301
|
+
@question_transcript.clear
|
302
|
+
@listening_for_question = true if input.include?("\n")
|
303
|
+
rescue Timeout::Error
|
304
|
+
# Let the loop continue so that it can catch @shutdown periodically, or
|
305
|
+
# capture the signal
|
306
|
+
next
|
307
|
+
end
|
308
|
+
|
309
|
+
next unless @listening_for_question
|
310
|
+
|
311
|
+
PodcastBuddy.logger.info to_human("🎙️ Listening for quesiton. Press ", :wait) + to_human("Enter", :input) + to_human(" to signal the end of the question...", :wait)
|
312
|
+
wait_for_question_end
|
313
|
+
end
|
216
314
|
end
|
217
315
|
end
|
218
316
|
|
317
|
+
|
318
|
+
# Handle Ctrl-C to generate show notes
|
319
|
+
Signal.trap("INT") do
|
320
|
+
puts to_human("\nShutting down streams...", :wait)
|
321
|
+
@shutdown = true
|
322
|
+
end
|
323
|
+
|
219
324
|
# Start audio stream processing
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
325
|
+
Async do |task|
|
326
|
+
audio_input_task = task.async { process_audio_stream }
|
327
|
+
periodic_summarization_task = task.async { periodic_summarization(60) }
|
328
|
+
question_listener_task = task.async { wait_for_question_start }
|
329
|
+
|
330
|
+
# Allow recording up to 2 hours, but hard-timeout so that we don't become
|
331
|
+
# zombies.
|
332
|
+
task.with_timeout(60*60*2) do
|
333
|
+
# Wait for shutdown sub-tasks to complete
|
334
|
+
task.yield until @shutdown
|
335
|
+
end
|
336
|
+
|
337
|
+
PodcastBuddy.logger.info "Waiting for all tasks to complete before exiting..."
|
338
|
+
audio_input_task.wait
|
339
|
+
PodcastBuddy.logger.debug "Audio input task complete."
|
340
|
+
question_listener_task.wait
|
341
|
+
PodcastBuddy.logger.debug "Question listener task complete."
|
342
|
+
periodic_summarization_task.wait
|
343
|
+
PodcastBuddy.logger.debug "Periodic Summarization task complete."
|
344
|
+
|
345
|
+
generate_show_notes
|
232
346
|
end
|
@@ -15,12 +15,30 @@ module PodcastBuddy
|
|
15
15
|
system_dependency.install unless system_dependency.installed?
|
16
16
|
end
|
17
17
|
|
18
|
+
def self.resolve_whisper_model(model)
|
19
|
+
return if model_downloaded?(model)
|
20
|
+
download_model(model)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.model_downloaded?(model)
|
24
|
+
File.exist?(File.join(PodcastBuddy.root, "whisper.cpp", "models", "ggml-#{model}.bin"))
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.download_model(model)
|
28
|
+
originating_directory = Dir.pwd
|
29
|
+
Dir.chdir("whisper.cpp")
|
30
|
+
PodcastBuddy.logger.info "Downloading GGML model: #{PodcastBuddy.whisper_model}"
|
31
|
+
PodcastBuddy.logger.info `bash ./models/download-ggml-model.sh #{PodcastBuddy.whisper_model}`
|
32
|
+
ensure
|
33
|
+
Dir.chdir(originating_directory)
|
34
|
+
end
|
35
|
+
|
18
36
|
def installed?
|
19
37
|
PodcastBuddy.logger.info "Checking for system dependency: #{name}..."
|
20
38
|
if name.to_s == "whisper"
|
21
39
|
Dir.exist?("whisper.cpp")
|
22
40
|
else
|
23
|
-
system("brew list #{name}") || system("type -a #{name}")
|
41
|
+
system("brew list -1 #{name} > /dev/null") || system("type -a #{name}")
|
24
42
|
end
|
25
43
|
end
|
26
44
|
|
data/lib/podcast_buddy.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require "net/http"
|
4
2
|
require "uri"
|
5
3
|
require "json"
|
6
4
|
require "open3"
|
7
5
|
require "logger"
|
6
|
+
require "async"
|
7
|
+
require "async/http/faraday"
|
8
|
+
require "rainbow"
|
8
9
|
|
9
10
|
require "bundler/inline"
|
10
11
|
|
@@ -19,6 +20,14 @@ module PodcastBuddy
|
|
19
20
|
Dir.pwd
|
20
21
|
end
|
21
22
|
|
23
|
+
def self.session=(name)
|
24
|
+
@session = "#{root}/tmp/#{name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.session
|
28
|
+
@session ||= "#{root}/tmp"
|
29
|
+
end
|
30
|
+
|
22
31
|
def self.logger
|
23
32
|
@logger ||= Logger.new($stdout, level: Logger::DEBUG)
|
24
33
|
end
|
@@ -27,24 +36,92 @@ module PodcastBuddy
|
|
27
36
|
@logger = logger
|
28
37
|
end
|
29
38
|
|
39
|
+
def self.whisper_model=(model)
|
40
|
+
@whisper_model = model
|
41
|
+
end
|
42
|
+
|
30
43
|
def self.whisper_model
|
31
|
-
"small.en"
|
44
|
+
@whisper_model ||= "small.en"
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.whisper_command
|
48
|
+
"./whisper.cpp/stream -m ./whisper.cpp/models/ggml-#{PodcastBuddy.whisper_model}.bin -t 4 --step 0 --length 5000 --keep 500 --vad-thold 0.60 --audio-ctx 0 --keep-context -c 1"
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.whisper_logger=(file_path)
|
52
|
+
@whisper_logger ||= Logger.new(file_path, "daily")
|
32
53
|
end
|
33
54
|
|
34
55
|
def self.whisper_logger
|
35
|
-
@whisper_logger ||= Logger.new("
|
56
|
+
@whisper_logger ||= Logger.new("#{session}/whisper.log", "daily")
|
36
57
|
end
|
37
58
|
|
38
|
-
def self.
|
39
|
-
@
|
59
|
+
def self.topics_log_file=(log_file_path)
|
60
|
+
@topics_log_file = log_file_path
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.topics_log_file
|
64
|
+
@topics_log_file ||= "#{session}/topics-#{Time.new.strftime("%Y-%m-%d")}.log"
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.summary_log_file=(log_file_path)
|
68
|
+
@summary_log_file = log_file_path
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.summary_log_file
|
72
|
+
@summary_log_file ||= "#{session}/summary-#{Time.new.strftime("%Y-%m-%d")}.log"
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.transcript_file=(file_path)
|
76
|
+
@transcript_file = file_path
|
40
77
|
end
|
41
78
|
|
42
79
|
def self.transcript_file
|
43
|
-
@transcript_file ||= "#{
|
80
|
+
@transcript_file ||= "#{session}/transcript-#{Time.new.strftime("%Y-%m-%d")}.log"
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.show_notes_file=(file_path)
|
84
|
+
@show_notes_file = file_path
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.show_notes_file
|
88
|
+
@show_notes_file ||= "#{session}/show-notes-#{Time.new.strftime("%Y-%m-%d")}.md"
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.current_transcript
|
92
|
+
@current_transcript ||= File.exist?(transcript_file) ? File.read(transcript_file) : ""
|
44
93
|
end
|
45
94
|
|
46
95
|
def self.update_transcript(text)
|
47
96
|
File.open(transcript_file, "a") { |f| f.puts text }
|
97
|
+
current_transcript << text
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.current_summary
|
101
|
+
@current_summary ||= File.exist?(summary_log_file) ? File.read(summary_log_file) : ""
|
102
|
+
end
|
103
|
+
|
104
|
+
def self.update_summary(text)
|
105
|
+
@current_summary = text
|
106
|
+
File.write(summary_log_file, text)
|
107
|
+
@current_summary
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.current_topics
|
111
|
+
@current_topics ||= File.exist?(topics_log_file) ? File.read(topics_log_file) : ""
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.add_to_topics(topics)
|
115
|
+
File.open(topics_log_file, "a") { |f| f.puts topics }
|
116
|
+
current_topics << topics
|
117
|
+
end
|
118
|
+
|
119
|
+
def self.answer_audio_file_path=(file_path)
|
120
|
+
@answer_audio_file_path = file_path
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.answer_audio_file_path
|
124
|
+
@answer_audio_file_path ||= "response.mp3"
|
48
125
|
end
|
49
126
|
|
50
127
|
def self.setup
|
@@ -58,5 +135,16 @@ module PodcastBuddy
|
|
58
135
|
SystemDependency.auto_install!(:git)
|
59
136
|
SystemDependency.auto_install!(:sdl2)
|
60
137
|
SystemDependency.auto_install!(:whisper)
|
138
|
+
SystemDependency.resolve_whisper_model(whisper_model)
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.openai_client=(client)
|
142
|
+
@openai_client = client
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.openai_client
|
146
|
+
raise Error, "Please set an OPENAI_ACCESS_TOKEN environment variable." if ENV["OPENAI_ACCESS_TOKEN"].to_s.strip.squeeze.empty?
|
147
|
+
|
148
|
+
@openai_client ||= OpenAI::Client.new(access_token: ENV["OPENAI_ACCESS_TOKEN"], log_errors: true)
|
61
149
|
end
|
62
150
|
end
|
metadata
CHANGED
@@ -1,15 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: podcast-buddy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Valentino Stoll
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-08-
|
12
|
-
dependencies:
|
11
|
+
date: 2024-08-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: async
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: async-http-faraday
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rainbow
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
13
55
|
description: A simple Ruby command-line tool for dropping in an AI buddy to your podcast.
|
14
56
|
email:
|
15
57
|
- v@codenamev.com
|