meeting-buddy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2024-12-20
4
+
5
+ - Initial release
@@ -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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "standard/rake"
9
+
10
+ task default: %i[spec standard]
data/exe/meeting-buddy ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ load File.expand_path("meeting_buddy", __dir__)
data/exe/meeting_buddy ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/meeting_buddy"
5
+
6
+ MeetingBuddy::CLI.new(ARGV).run
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MeetingBuddy
4
+ VERSION = "0.1.0"
5
+ 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
@@ -0,0 +1,3 @@
1
+ module MeetingBuddy
2
+ VERSION: String
3
+ 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: []