meeting-buddy 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +68 -0
- data/Rakefile +10 -0
- data/exe/meeting-buddy +4 -0
- data/exe/meeting_buddy +6 -0
- data/lib/meeting_buddy/cli.rb +83 -0
- data/lib/meeting_buddy/configuration.rb +34 -0
- data/lib/meeting_buddy/listener.rb +77 -0
- data/lib/meeting_buddy/meeting_signal.rb +30 -0
- data/lib/meeting_buddy/session.rb +73 -0
- data/lib/meeting_buddy/system_dependency.rb +67 -0
- data/lib/meeting_buddy/transcriber.rb +59 -0
- data/lib/meeting_buddy/version.rb +5 -0
- data/lib/meeting_buddy.rb +78 -0
- data/sig/meeting_buddy.rbs +3 -0
- metadata +124 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 54a467b53e070e625d4bfaf751387bf0d7c13f6f92f16fde8b8e9c1e31812173
|
4
|
+
data.tar.gz: d51557d5b60bd8b64e08f017ecd365ece6ed69ff1f18c666da3b97b376d74fcf
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cbb4bb3d8aa47d0ff1050d164052460b06ee596b702a14071832dc4574e8c455b757675767d7646afd6806b3dc8fa93176c25e0f5d4532de2009f62f26c2246d
|
7
|
+
data.tar.gz: 71d90de2ea922426498fabf5978d6acbce5583ecb2c5931972ed0d97dff1ca695b38c19ac50cc5faa2a132ce57abe413cb2e80278db5036bf9ecac0ea66cf03b
|
data/.rspec
ADDED
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
2
|
+
|
3
|
+
## Our Pledge
|
4
|
+
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
9
|
+
nationality, personal appearance, race, caste, color, religion, or sexual
|
10
|
+
identity and orientation.
|
11
|
+
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
13
|
+
diverse, inclusive, and healthy community.
|
14
|
+
|
15
|
+
## Our Standards
|
16
|
+
|
17
|
+
Examples of behavior that contributes to a positive environment for our
|
18
|
+
community include:
|
19
|
+
|
20
|
+
* Demonstrating empathy and kindness toward other people
|
21
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
22
|
+
* Giving and gracefully accepting constructive feedback
|
23
|
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
24
|
+
and learning from the experience
|
25
|
+
* Focusing on what is best not just for us as individuals, but for the overall
|
26
|
+
community
|
27
|
+
|
28
|
+
Examples of unacceptable behavior include:
|
29
|
+
|
30
|
+
* The use of sexualized language or imagery, and sexual attention or advances of
|
31
|
+
any kind
|
32
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
33
|
+
* Public or private harassment
|
34
|
+
* Publishing others' private information, such as a physical or email address,
|
35
|
+
without their explicit permission
|
36
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
37
|
+
professional setting
|
38
|
+
|
39
|
+
## Enforcement Responsibilities
|
40
|
+
|
41
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
42
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
43
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
44
|
+
or harmful.
|
45
|
+
|
46
|
+
Community leaders have the right and responsibility to remove, edit, or reject
|
47
|
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
48
|
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
49
|
+
decisions when appropriate.
|
50
|
+
|
51
|
+
## Scope
|
52
|
+
|
53
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
54
|
+
an individual is officially representing the community in public spaces.
|
55
|
+
Examples of representing our community include using an official email address,
|
56
|
+
posting via an official social media account, or acting as an appointed
|
57
|
+
representative at an online or offline event.
|
58
|
+
|
59
|
+
## Enforcement
|
60
|
+
|
61
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
62
|
+
reported to the community leaders responsible for enforcement at
|
63
|
+
[INSERT CONTACT METHOD].
|
64
|
+
All complaints will be reviewed and investigated promptly and fairly.
|
65
|
+
|
66
|
+
All community leaders are obligated to respect the privacy and security of the
|
67
|
+
reporter of any incident.
|
68
|
+
|
69
|
+
## Enforcement Guidelines
|
70
|
+
|
71
|
+
Community leaders will follow these Community Impact Guidelines in determining
|
72
|
+
the consequences for any action they deem in violation of this Code of Conduct:
|
73
|
+
|
74
|
+
### 1. Correction
|
75
|
+
|
76
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
77
|
+
unprofessional or unwelcome in the community.
|
78
|
+
|
79
|
+
**Consequence**: A private, written warning from community leaders, providing
|
80
|
+
clarity around the nature of the violation and an explanation of why the
|
81
|
+
behavior was inappropriate. A public apology may be requested.
|
82
|
+
|
83
|
+
### 2. Warning
|
84
|
+
|
85
|
+
**Community Impact**: A violation through a single incident or series of
|
86
|
+
actions.
|
87
|
+
|
88
|
+
**Consequence**: A warning with consequences for continued behavior. No
|
89
|
+
interaction with the people involved, including unsolicited interaction with
|
90
|
+
those enforcing the Code of Conduct, for a specified period of time. This
|
91
|
+
includes avoiding interactions in community spaces as well as external channels
|
92
|
+
like social media. Violating these terms may lead to a temporary or permanent
|
93
|
+
ban.
|
94
|
+
|
95
|
+
### 3. Temporary Ban
|
96
|
+
|
97
|
+
**Community Impact**: A serious violation of community standards, including
|
98
|
+
sustained inappropriate behavior.
|
99
|
+
|
100
|
+
**Consequence**: A temporary ban from any sort of interaction or public
|
101
|
+
communication with the community for a specified period of time. No public or
|
102
|
+
private interaction with the people involved, including unsolicited interaction
|
103
|
+
with those enforcing the Code of Conduct, is allowed during this period.
|
104
|
+
Violating these terms may lead to a permanent ban.
|
105
|
+
|
106
|
+
### 4. Permanent Ban
|
107
|
+
|
108
|
+
**Community Impact**: Demonstrating a pattern of violation of community
|
109
|
+
standards, including sustained inappropriate behavior, harassment of an
|
110
|
+
individual, or aggression toward or disparagement of classes of individuals.
|
111
|
+
|
112
|
+
**Consequence**: A permanent ban from any sort of public interaction within the
|
113
|
+
community.
|
114
|
+
|
115
|
+
## Attribution
|
116
|
+
|
117
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
118
|
+
version 2.1, available at
|
119
|
+
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
|
120
|
+
|
121
|
+
Community Impact Guidelines were inspired by
|
122
|
+
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
123
|
+
|
124
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
125
|
+
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
|
126
|
+
[https://www.contributor-covenant.org/translations][translations].
|
127
|
+
|
128
|
+
[homepage]: https://www.contributor-covenant.org
|
129
|
+
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
|
130
|
+
[Mozilla CoC]: https://github.com/mozilla/diversity
|
131
|
+
[FAQ]: https://www.contributor-covenant.org/faq
|
132
|
+
[translations]: https://www.contributor-covenant.org/translations
|
data/README.md
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# MeetingBuddy
|
2
|
+
|
3
|
+
MeetingBuddy is a Ruby framework for real-time audio transcription with event handling. It provides core functionality for applications requiring live speech-to-text conversion and processing.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
gem install meeting-buddy
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Basic usage with a custom handler:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
require 'meeting_buddy'
|
17
|
+
|
18
|
+
class MyHandler < MeetingBuddy::Handler
|
19
|
+
def on_transcription(text)
|
20
|
+
puts "Transcribed: #{text}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
session = MeetingBuddy::Session.new(
|
25
|
+
name: "my-meeting",
|
26
|
+
handlers: [MyHandler.new]
|
27
|
+
)
|
28
|
+
|
29
|
+
session.start
|
30
|
+
```
|
31
|
+
|
32
|
+
### Configuration
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
MeetingBuddy.configure do |config|
|
36
|
+
config.whisper_model = "small.en" # Choose whisper model
|
37
|
+
config.root = "path/to/files" # Set root directory
|
38
|
+
config.logger = Logger.new($stdout, level: Logger::DEBUG)
|
39
|
+
end
|
40
|
+
```
|
41
|
+
|
42
|
+
### Requirements
|
43
|
+
|
44
|
+
1. `git` (for whisper.cpp setup)
|
45
|
+
2. `sdl2` (for audio input)
|
46
|
+
3. OpenAI token in `OPENAI_ACCESS_TOKEN` env var
|
47
|
+
4. MacOS (currently supported platform)
|
48
|
+
|
49
|
+
## Development
|
50
|
+
|
51
|
+
After checking out the repo:
|
52
|
+
|
53
|
+
```bash
|
54
|
+
bin/setup
|
55
|
+
rake spec
|
56
|
+
```
|
57
|
+
|
58
|
+
## Contributing
|
59
|
+
|
60
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/codenamev/meeting-buddy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/codenamev/meeting-buddy/blob/main/CODE_OF_CONDUCT.md).
|
61
|
+
|
62
|
+
## License
|
63
|
+
|
64
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
65
|
+
|
66
|
+
## Code of Conduct
|
67
|
+
|
68
|
+
Everyone interacting in the Meeting::Buddy project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/codenamev/meeting-buddy/blob/main/CODE_OF_CONDUCT.md).
|
data/Rakefile
ADDED
data/exe/meeting-buddy
ADDED
data/exe/meeting_buddy
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
require "rainbow"
|
5
|
+
|
6
|
+
module MeetingBuddy
|
7
|
+
# Command Line Interface for MeetingBuddy
|
8
|
+
#
|
9
|
+
# Handles command line argument parsing, session management, and audio processing
|
10
|
+
# for meeting recording and AI interaction.
|
11
|
+
#
|
12
|
+
# @example Basic usage
|
13
|
+
# cli = MeetingBuddy::CLI.new(ARGV)
|
14
|
+
# cli.run
|
15
|
+
#
|
16
|
+
# @example With options
|
17
|
+
# cli = MeetingBuddy::CLI.new(["--debug", "-n", "my-meeting"])
|
18
|
+
# cli.run
|
19
|
+
class CLI
|
20
|
+
def initialize(argv)
|
21
|
+
@options = parse_options(argv)
|
22
|
+
end
|
23
|
+
|
24
|
+
def run
|
25
|
+
configure
|
26
|
+
setup_dependencies
|
27
|
+
start_session
|
28
|
+
rescue Interrupt
|
29
|
+
handle_shutdown
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def parse_options(argv)
|
35
|
+
options = {}
|
36
|
+
OptionParser.new do |opts|
|
37
|
+
opts.banner = "Usage: meeting_buddy [options]"
|
38
|
+
|
39
|
+
opts.on("--debug", "Run in debug mode") do |v|
|
40
|
+
options[:debug] = v
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("-w", "--whisper MODEL", "Use specific whisper model (default: small.en)") do |v|
|
44
|
+
options[:whisper_model] = v
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on("-n", "--name NAME", "A name for the session to label all log files") do |v|
|
48
|
+
options[:name] = v
|
49
|
+
end
|
50
|
+
end.parse!(argv)
|
51
|
+
options
|
52
|
+
end
|
53
|
+
|
54
|
+
def configure
|
55
|
+
MeetingBuddy.configure do |config|
|
56
|
+
config.whisper_model = @options[:whisper_model] if @options[:whisper_model]
|
57
|
+
config.logger.level = @options[:debug] ? Logger::DEBUG : Logger::INFO
|
58
|
+
config.logger.formatter = proc do |severity, datetime, progname, msg|
|
59
|
+
(severity.to_s == "INFO") ? "#{msg}\n" : "[#{severity}] #{msg}\n"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def setup_dependencies
|
65
|
+
MeetingBuddy.logger.info "Setting up dependencies..."
|
66
|
+
MeetingBuddy.setup
|
67
|
+
MeetingBuddy.logger.info "Setup complete."
|
68
|
+
MeetingBuddy.openai_client
|
69
|
+
end
|
70
|
+
|
71
|
+
def start_session
|
72
|
+
MeetingBuddy.start_session(name: @options[:name])
|
73
|
+
MeetingBuddy.logger.info MeetingBuddy.to_human("Using whisper model: #{MeetingBuddy.config.whisper_model}", :info)
|
74
|
+
MeetingBuddy.logger.info MeetingBuddy.to_human("Starting session in: #{MeetingBuddy.session.base_path}", :info)
|
75
|
+
MeetingBuddy.session.start
|
76
|
+
end
|
77
|
+
|
78
|
+
def handle_shutdown
|
79
|
+
MeetingBuddy.logger.info MeetingBuddy.to_human("\nShutting down streams...", :wait)
|
80
|
+
MeetingBuddy.session.stop
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MeetingBuddy
|
4
|
+
# Manages configuration for MeetingBuddy
|
5
|
+
class Configuration
|
6
|
+
attr_accessor :whisper_model
|
7
|
+
attr_writer :openai_client, :logger, :whisper_logger
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@logger = Logger.new($stdout, level: Logger::INFO)
|
11
|
+
@whisper_model = "small.en-q5_1"
|
12
|
+
end
|
13
|
+
|
14
|
+
def logger
|
15
|
+
@logger ||= Logger.new($stdout, level: Logger::INFO)
|
16
|
+
end
|
17
|
+
|
18
|
+
def whisper_logger
|
19
|
+
@whisper_logger ||= Logger.new(MeetingBuddy.session.whisper_log, level: Logger::INFO)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [OpenAI::Client]
|
23
|
+
def openai_client
|
24
|
+
raise Error, "Please set an OPENAI_ACCESS_TOKEN environment variable." if ENV["OPENAI_ACCESS_TOKEN"].to_s.strip.empty?
|
25
|
+
|
26
|
+
@openai_client ||= OpenAI::Client.new(access_token: ENV["OPENAI_ACCESS_TOKEN"], log_errors: true)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [String]
|
30
|
+
def whisper_command
|
31
|
+
"#{MeetingBuddy.cache_dir}/whisper.cpp/build/bin/stream -m #{MeetingBuddy.cache_dir}/whisper.cpp/models/ggml-#{whisper_model}.bin -t 8 --step 0 --length 5000 --keep 500 --vad-thold 0.75 --audio-ctx 0 --keep-context -c 1 -l en"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "open3"
|
4
|
+
|
5
|
+
module MeetingBuddy
|
6
|
+
class Listener
|
7
|
+
def initialize(transcriber:, signal:)
|
8
|
+
@transcriber = transcriber
|
9
|
+
@signal = signal
|
10
|
+
@shutdown = false
|
11
|
+
@announce_hearing = true
|
12
|
+
@whisper_logger = MeetingBuddy.whisper_logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
Sync do |parent|
|
17
|
+
Open3.popen3(MeetingBuddy.config.whisper_command) do |_stdin, stdout, stderr, _thread|
|
18
|
+
error_task = parent.async do
|
19
|
+
log_errors(stderr)
|
20
|
+
rescue IOError => e
|
21
|
+
MeetingBuddy.logger.debug "Error stream closed: #{e.message}"
|
22
|
+
end
|
23
|
+
output_task = parent.async do
|
24
|
+
log_output(stdout)
|
25
|
+
rescue IOError => e
|
26
|
+
MeetingBuddy.config.logger.debug "Output stream closed: #{e.message}"
|
27
|
+
end
|
28
|
+
|
29
|
+
MeetingBuddy.logger.info "Listening..."
|
30
|
+
|
31
|
+
while (line = stdout.gets)
|
32
|
+
break if @shutdown
|
33
|
+
MeetingBuddy.logger.debug("Shutdown: process_audio_stream...") and break if @shutdown
|
34
|
+
begin
|
35
|
+
process_transcription(line)
|
36
|
+
rescue IOError => e
|
37
|
+
MeetingBuddy.config.logger.debug "Main output stream closed: #{e.message}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
error_task.wait
|
42
|
+
output_task.wait
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Stop the listening process
|
48
|
+
def stop
|
49
|
+
@shutdown = true
|
50
|
+
end
|
51
|
+
|
52
|
+
def announce_what_you_hear!
|
53
|
+
@announce_hearing = true
|
54
|
+
end
|
55
|
+
|
56
|
+
def suppress_what_you_hear!
|
57
|
+
@announce_hearing = false
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def process_transcription(line)
|
63
|
+
transcribed_line = @transcriber.process(line)
|
64
|
+
return if transcribed_line.text.empty?
|
65
|
+
MeetingBuddy.logger.info "Heard: #{transcribed_line.text}" if @announce_hearing
|
66
|
+
@signal.trigger({text: transcribed_line.text, timestamp: transcribed_line.timestamp})
|
67
|
+
end
|
68
|
+
|
69
|
+
def log_errors(stderr)
|
70
|
+
stderr.each { |line| @whisper_logger.error(line) }
|
71
|
+
end
|
72
|
+
|
73
|
+
def log_output(stdout)
|
74
|
+
stdout.each { |line| @whisper_logger.debug(line) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MeetingBuddy
|
4
|
+
class MeetingSignal
|
5
|
+
def initialize
|
6
|
+
@listeners = []
|
7
|
+
@queue = Queue.new
|
8
|
+
start_listener_thread
|
9
|
+
end
|
10
|
+
|
11
|
+
def subscribe(&block)
|
12
|
+
@listeners << block
|
13
|
+
end
|
14
|
+
|
15
|
+
def trigger(data = nil)
|
16
|
+
@queue << data
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def start_listener_thread
|
22
|
+
Thread.new do
|
23
|
+
loop do
|
24
|
+
data = @queue.pop
|
25
|
+
@listeners.each { |listener| listener.call(data) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
|
5
|
+
module MeetingBuddy
|
6
|
+
# Manages session-specific data for MeetingBuddy
|
7
|
+
class Session
|
8
|
+
attr_reader :name, :base_path, :handlers
|
9
|
+
|
10
|
+
def initialize(name: nil, handlers: [])
|
11
|
+
@name = name || Time.now.strftime("%Y-%m-%d_%H-%M-%S")
|
12
|
+
@base_path = File.join(MeetingBuddy.cache_dir, "sessions", @name)
|
13
|
+
@handlers = handlers
|
14
|
+
@signal = MeetingSignal.new
|
15
|
+
setup_transcriber
|
16
|
+
FileUtils.mkdir_p base_path
|
17
|
+
FileUtils.touch(transcript_log)
|
18
|
+
end
|
19
|
+
|
20
|
+
def start
|
21
|
+
Sync do |task|
|
22
|
+
listener_task = task.async { start_listener }
|
23
|
+
@tasks = [
|
24
|
+
{name: "Listener", task: listener_task}
|
25
|
+
]
|
26
|
+
task.yield until @shutdown
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def stop
|
31
|
+
@shutdown = true
|
32
|
+
@tasks&.each do |task_info|
|
33
|
+
MeetingBuddy.config.logger.info "Stopping #{task_info[:name]}..."
|
34
|
+
task_info[:task].wait
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def transcript_log
|
39
|
+
File.join(@base_path, "transcript.log")
|
40
|
+
end
|
41
|
+
|
42
|
+
def whisper_log
|
43
|
+
File.join(@base_path, "whisper.log")
|
44
|
+
end
|
45
|
+
|
46
|
+
def current_transcript
|
47
|
+
File.exist?(transcript_log) ? File.read(transcript_log) : ""
|
48
|
+
end
|
49
|
+
|
50
|
+
def update_transcript(text)
|
51
|
+
File.open(transcript_log, "a") { |f| f.puts text }
|
52
|
+
@handlers.each { |h| h.on_transcription(text) }
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def setup_transcriber
|
58
|
+
@transcriber = Transcriber.new
|
59
|
+
@signal.subscribe { |data| handle_transcription(data) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def handle_transcription(data)
|
63
|
+
return if data[:text].to_s.empty?
|
64
|
+
|
65
|
+
update_transcript(data[:text])
|
66
|
+
end
|
67
|
+
|
68
|
+
def start_listener
|
69
|
+
@listener = Listener.new(transcriber: @transcriber, signal: @signal)
|
70
|
+
@listener.start
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MeetingBuddy
|
4
|
+
class SystemDependency < Struct.new(:name, keyword_init: true)
|
5
|
+
WHISPER_CPP_VERSION = "v1.7.3"
|
6
|
+
|
7
|
+
attr_accessor :name
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def auto_install!(name)
|
11
|
+
system_dependency = new(name: name)
|
12
|
+
system_dependency.install unless system_dependency.installed?
|
13
|
+
end
|
14
|
+
|
15
|
+
def resolve_whisper_model(model)
|
16
|
+
return if model_downloaded?(model)
|
17
|
+
download_model(model)
|
18
|
+
end
|
19
|
+
|
20
|
+
def model_downloaded?(model)
|
21
|
+
File.exist?(File.join(MeetingBuddy.cache_dir, "whisper.cpp", "models", "ggml-#{model}.bin"))
|
22
|
+
end
|
23
|
+
|
24
|
+
def download_model(model)
|
25
|
+
Dir.chdir("#{MeetingBuddy.cache_dir}/whisper.cpp") do
|
26
|
+
MeetingBuddy.logger.info "Downloading GGML model: #{MeetingBuddy.whisper_model}"
|
27
|
+
MeetingBuddy.logger.info `bash ./models/download-ggml-model.sh #{MeetingBuddy.whisper_model}`
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(name:)
|
33
|
+
@name = name
|
34
|
+
end
|
35
|
+
|
36
|
+
def installed?
|
37
|
+
MeetingBuddy.logger.info "Checking for system dependency: #{name}..."
|
38
|
+
if name.to_s == "whisper"
|
39
|
+
Dir.exist?("#{MeetingBuddy.cache_dir}/whisper.cpp")
|
40
|
+
else
|
41
|
+
system("brew list -1 #{name} > /dev/null") || system("type -a #{name}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def install
|
46
|
+
return install_whisper if name.to_s == "whisper"
|
47
|
+
|
48
|
+
MeetingBuddy.logger.info "Installing #{name}..."
|
49
|
+
`brew list #{name} || brew install #{name}`
|
50
|
+
end
|
51
|
+
|
52
|
+
def install_whisper
|
53
|
+
MeetingBuddy.logger.info "Installing whisper.cpp..."
|
54
|
+
Dir.chdir(MeetingBuddy.cache_dir) do
|
55
|
+
MeetingBuddy.logger.info "Setting up whipser.cpp in #{MeetingBuddy.cache_dir}/whipser.cpp"
|
56
|
+
MeetingBuddy.logger.info `git clone https://github.com/ggerganov/whisper.cpp`
|
57
|
+
Dir.chdir("whisper.cpp") do
|
58
|
+
MeetingBuddy.logger.info `git checkout #{WHISPER_CPP_VERSION}`
|
59
|
+
MeetingBuddy.logger.info "Downloading GGML model: #{MeetingBuddy.whisper_model}"
|
60
|
+
MeetingBuddy.logger.info `bash #{MeetingBuddy.cache_dir}/whisper.cpp/models/download-ggml-model.sh #{MeetingBuddy.whisper_model}`
|
61
|
+
MeetingBuddy.logger.info "Building whipser.cpp with streaming support..."
|
62
|
+
MeetingBuddy.logger.info `cmake -B build -DWHISPER_SDL2=ON && cmake --build build --config Release`
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: false
|
2
|
+
|
3
|
+
module MeetingBuddy
|
4
|
+
# Manages the transcript of the meeting
|
5
|
+
class Transcriber
|
6
|
+
# @return [String] full transcript of the meeting
|
7
|
+
attr_reader :full_transcript
|
8
|
+
|
9
|
+
class Line < Struct.new(:text, :timestamp, keyword_init: true); end
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@full_transcript = ""
|
13
|
+
@last_timestamp = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
# Process new transcription text
|
17
|
+
# @param line [String] raw transcription line from whisper
|
18
|
+
# @return [String] cleaned transcription text
|
19
|
+
def process(line)
|
20
|
+
timestamp, text = parse_line(line)
|
21
|
+
return Line.new(text: "", timestamp: Time.now) if text.empty?
|
22
|
+
|
23
|
+
@full_transcript += text
|
24
|
+
@last_timestamp = timestamp
|
25
|
+
|
26
|
+
Line.new(text: format_transcription(text), timestamp: @last_timestamp)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get the latest portion of the transcript
|
30
|
+
# @param limit [Integer] number of characters to return
|
31
|
+
# @return [String] latest portion of the transcript
|
32
|
+
def latest(limit = 200)
|
33
|
+
@full_transcript[[@full_transcript.length - limit, 0].max, limit] || raise(ArgumentError, "negative limit")
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def format_transcription(text)
|
39
|
+
text.strip + " "
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def parse_line(line)
|
45
|
+
timestamp, text = nil, ""
|
46
|
+
match = line.match(/\[.*?(\d{2}:\d{2}:\d{2}\.\d{3}).*?\]\s{1,3}(.+)/)
|
47
|
+
timestamp, text = [match[1].to_i, match[2]] if match
|
48
|
+
text.gsub!(/(\[BLANK_AUDIO\]|\A\["\s?|"\]\Z)/, "")&.strip
|
49
|
+
text.concat(" ") if text.match?(/[^\w\s]\Z/)
|
50
|
+
[timestamp, text]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Timestamps are formatted as 'h:m:s'
|
54
|
+
def time_to_seconds(time_str)
|
55
|
+
h, m, s = time_str.split(":").map(&:to_f)
|
56
|
+
(h * 3600) + (m * 60) + s
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "async"
|
5
|
+
require "async/http/faraday"
|
6
|
+
require "rainbow"
|
7
|
+
require "openai"
|
8
|
+
|
9
|
+
require_relative "meeting_buddy/version"
|
10
|
+
require_relative "meeting_buddy/configuration"
|
11
|
+
require_relative "meeting_buddy/listener"
|
12
|
+
require_relative "meeting_buddy/meeting_signal"
|
13
|
+
require_relative "meeting_buddy/session"
|
14
|
+
require_relative "meeting_buddy/system_dependency"
|
15
|
+
require_relative "meeting_buddy/transcriber"
|
16
|
+
require_relative "meeting_buddy/cli"
|
17
|
+
|
18
|
+
module MeetingBuddy
|
19
|
+
class Error < StandardError; end
|
20
|
+
|
21
|
+
class << self
|
22
|
+
attr_accessor :session
|
23
|
+
|
24
|
+
extend Forwardable
|
25
|
+
def_delegators :config,
|
26
|
+
:logger,
|
27
|
+
:logger=,
|
28
|
+
:whisper_command,
|
29
|
+
:whisper_model,
|
30
|
+
:whisper_logger,
|
31
|
+
:whisper_logger=,
|
32
|
+
:openai_client,
|
33
|
+
:openai_client=
|
34
|
+
|
35
|
+
def config
|
36
|
+
@config ||= Configuration.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def configure
|
40
|
+
@config = Configuration.new
|
41
|
+
yield(@config) if block_given?
|
42
|
+
end
|
43
|
+
|
44
|
+
def start_session(name: nil)
|
45
|
+
@session = Session.new(name: name)
|
46
|
+
end
|
47
|
+
|
48
|
+
def cache_dir
|
49
|
+
@cache_dir ||= "#{ENV["HOME"]}/.buddy"
|
50
|
+
end
|
51
|
+
|
52
|
+
def setup
|
53
|
+
Dir.mkdir cache_dir unless Dir.exist?(cache_dir)
|
54
|
+
SystemDependency.auto_install!(:git)
|
55
|
+
SystemDependency.auto_install!(:sdl2)
|
56
|
+
SystemDependency.auto_install!(:whisper)
|
57
|
+
SystemDependency.auto_install!(:bat)
|
58
|
+
SystemDependency.resolve_whisper_model(whisper_model)
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_human(text, label = :info)
|
62
|
+
case label.to_sym
|
63
|
+
when :info
|
64
|
+
Rainbow(text).blue
|
65
|
+
when :wait
|
66
|
+
Rainbow(text).yellow
|
67
|
+
when :input
|
68
|
+
Rainbow(text).black.bg(:yellow)
|
69
|
+
when :success
|
70
|
+
Rainbow(text).green
|
71
|
+
else
|
72
|
+
text
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
configure
|
78
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: meeting-buddy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Valentino Stoll
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-20 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: ruby-openai
|
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'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rainbow
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: A simple Ruby command-line framework for dropping an AI buddy into your
|
70
|
+
meeting.
|
71
|
+
email:
|
72
|
+
- v@codenamev.com
|
73
|
+
executables:
|
74
|
+
- meeting-buddy
|
75
|
+
- meeting_buddy
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- ".rspec"
|
80
|
+
- ".standard.yml"
|
81
|
+
- CHANGELOG.md
|
82
|
+
- CODE_OF_CONDUCT.md
|
83
|
+
- README.md
|
84
|
+
- Rakefile
|
85
|
+
- exe/meeting-buddy
|
86
|
+
- exe/meeting_buddy
|
87
|
+
- lib/meeting_buddy.rb
|
88
|
+
- lib/meeting_buddy/cli.rb
|
89
|
+
- lib/meeting_buddy/configuration.rb
|
90
|
+
- lib/meeting_buddy/listener.rb
|
91
|
+
- lib/meeting_buddy/meeting_signal.rb
|
92
|
+
- lib/meeting_buddy/session.rb
|
93
|
+
- lib/meeting_buddy/system_dependency.rb
|
94
|
+
- lib/meeting_buddy/transcriber.rb
|
95
|
+
- lib/meeting_buddy/version.rb
|
96
|
+
- sig/meeting_buddy.rbs
|
97
|
+
homepage: https://github.com/codenamev/meeting-buddy
|
98
|
+
licenses:
|
99
|
+
- MIT
|
100
|
+
metadata:
|
101
|
+
allowed_push_host: https://rubygems.org
|
102
|
+
homepage_uri: https://github.com/codenamev/meeting-buddy
|
103
|
+
source_code_uri: https://github.com/codenamev/meeting-buddy
|
104
|
+
changelog_uri: https://github.com/codenamev/meeting-buddy/tree/main/CHANGELOG.md
|
105
|
+
post_install_message:
|
106
|
+
rdoc_options: []
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: 3.0.0
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
requirements: []
|
120
|
+
rubygems_version: 3.3.26
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: A simple Ruby command-line framework for dropping an AI buddy into your meeting.
|
124
|
+
test_files: []
|