meeting-buddy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []