summarize-meeting 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/bin/summarize-meeting +65 -0
  3. data/lib/ai.rb +26 -0
  4. data/lib/meeting.rb +130 -0
  5. metadata +158 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3b6ec76b5b3462260a5ea23a4768000ac6ff34de4cb7436cb9a612e9ed18c526
4
+ data.tar.gz: 1650a37e387b5917a4b651caadf2b755b9370ae5a9298ce2c61d29889f89b1e4
5
+ SHA512:
6
+ metadata.gz: ee9f26149f5bc591103d49cf6cbf3a6706a3a267aa48db8c4c13acea6ca2f904e83e6c35604f25323a39f18f2f8a7d0db0813190d7ffbeda5137fb22438429b0
7
+ data.tar.gz: 2671cd220684d611ac5681d7f734732c0841b23cf715674923c23e5cee0bc3d6e6cfa84834038db405889c43378b536a99f37dd71569c94c6ced26c05d032c73
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "optparse"
4
+ require "dotenv/load"
5
+
6
+ require_relative "../lib/meeting"
7
+
8
+ def main
9
+ options = {}
10
+
11
+ OptionParser.new do |opts|
12
+ opts.banner = "Usage: summarize-meeting.rb [options] input-file"
13
+
14
+ opts.on("-h", "--help", "Prints this help") do
15
+ puts opts
16
+ exit
17
+ end
18
+
19
+ opts.on("-o", "--output-file FILE", "The file to write the summary to") do |file|
20
+ options[:output_file] = file
21
+ end
22
+
23
+ if ENV["OPENAI_KEY"]
24
+ options[:openai_key] = ENV["OPENAI_KEY"]
25
+ end
26
+
27
+ opts.on("-k", "--openai-key KEY", "The OpenAI API key to use") do |key|
28
+ options[:openai_key] = key
29
+ end
30
+
31
+ if ENV["OPENAI_ORG"]
32
+ options[:openai_org] = ENV["OPENAI_ORG"]
33
+ end
34
+
35
+ opts.on("-g", "--openai-org ORG", "The OpenAI organization ID to use") do |org|
36
+ options[:openai_org] = org
37
+ end
38
+ end.parse!
39
+
40
+ Ai.access_token = options[:openai_key] if options[:openai_key]
41
+ Ai.organization_id = options[:openai_org] if options[:openai_org]
42
+
43
+ if ARGV.length != 1
44
+ puts "Error: You must specify a transcript file to summarize."
45
+ exit 1
46
+ end
47
+
48
+ transcript_file = ARGV[0]
49
+ transcript = File.read(transcript_file)
50
+
51
+ meeting = Meeting.new(transcript)
52
+ summary = meeting.summarize
53
+ summary_file_name = if options[:output_file]
54
+ options[:output_file]
55
+ else
56
+ transcript_file_basename = File.basename(transcript_file, ".*")
57
+ summary_file_name = "#{transcript_file_basename}-summary.txt"
58
+ end
59
+
60
+ File.write(summary_file_name, summary)
61
+ end
62
+
63
+ if __FILE__ == $0
64
+ main
65
+ end
data/lib/ai.rb ADDED
@@ -0,0 +1,26 @@
1
+ require "openai"
2
+
3
+ module Ai
4
+ @@access_token = ENV["OPENAI_KEY"]
5
+ @@organization_id = ENV["OPENAI_ORG"]
6
+
7
+ def self.client
8
+ OpenAI::Client.new(access_token: access_token, organization_id: organization_id)
9
+ end
10
+
11
+ def self.access_token
12
+ @@access_token
13
+ end
14
+
15
+ def self.organization_id
16
+ @@organization_id
17
+ end
18
+
19
+ def self.access_token=(token)
20
+ @@access_token = token
21
+ end
22
+
23
+ def self.organization_id=(id)
24
+ @@organization_id = id
25
+ end
26
+ end
data/lib/meeting.rb ADDED
@@ -0,0 +1,130 @@
1
+ require "json"
2
+ require "mustache"
3
+ require "openai"
4
+
5
+ require_relative "./ai"
6
+
7
+ class Meeting
8
+ LINE_SUMMARY_PROMPT_TEMPLATE = [
9
+ {
10
+ role: "system",
11
+ content: "You are an assistant summarizing a meeting.",
12
+ },
13
+ {
14
+ role: "system",
15
+ content: "The transcript of the meeting is split into {{chunkCount}} chunks. This is the {{chunkIndex}} chunk.",
16
+ },
17
+ {
18
+ role: "assistant",
19
+ content: "Please provide me with the next chunk of the transcript.",
20
+ },
21
+ {
22
+ role: "user",
23
+ content: "{{chunk}}",
24
+ }
25
+ ]
26
+
27
+ CONSOLIDATED_SUMMARY_PROMPT_TEMPLATE = [
28
+ {
29
+ role: "system",
30
+ content: "You are an assistant summarizing a meeting.",
31
+ },
32
+ {
33
+ role: "system",
34
+ content: "Notes about the meeting have been compiled.",
35
+ },
36
+ {
37
+ role: "system",
38
+ content: <<~CONTENT
39
+ Your job is to write a thorough summary of the meeting.
40
+ The summary should start with a brief overview of the meeting.
41
+ The summary should be detailed and should extract any action items that were discussed.
42
+ The summary should be organized into sections with headings and bullet points.
43
+ The summary should include a list of attendees.
44
+ The order of the sections should be overview, attendees, action items, and detailed notes by topic.
45
+ CONTENT
46
+ },
47
+ {
48
+ role: "assistant",
49
+ content: "Please provide me with notes from the meeting.",
50
+ },
51
+ {
52
+ role: "user",
53
+ content: "{{notes}}",
54
+ }
55
+ ]
56
+
57
+ def initialize(transcript)
58
+ @transcript = transcript
59
+ end
60
+
61
+ attr_reader :transcript
62
+
63
+ def summarize
64
+
65
+ # Step 1. Split the transcript into lines.
66
+ lines = transcript.split("\n")
67
+
68
+ # Step 2. Calculate the maximum chunk size in words.
69
+ max_total_tokens = 4000
70
+ response_token_reserve = 500
71
+ template_tokens = LINE_SUMMARY_PROMPT_TEMPLATE.map { |line| line[:content].split.size }.sum
72
+ max_chunk_tokens = max_total_tokens - response_token_reserve - template_tokens
73
+ words_per_token = 0.7
74
+ max_chunk_word_count = max_chunk_tokens * words_per_token
75
+
76
+ # Step 3. Split the transcript into equally sized chunks.
77
+ chunks = split_lines_into_equal_size_chunks(lines, max_chunk_word_count)
78
+
79
+ # Step 4. Summarize each chunk.
80
+ previous_chunks_summary = ""
81
+ chunks.each_with_index do |chunk, chunk_index|
82
+ chunk_summary = summarize_chunk(chunk, chunk_index, chunks.size, previous_chunks_summary)
83
+ previous_chunks_summary += chunk_summary
84
+ end
85
+
86
+ # Step 5. Write a consolidated summary.
87
+ consolidated_template = CONSOLIDATED_SUMMARY_PROMPT_TEMPLATE
88
+ prompt = Mustache.render(consolidated_template.to_json, { notes: previous_chunks_summary.to_json })
89
+ messages = JSON.parse(prompt)
90
+ response = Ai.client.chat(
91
+ parameters: {
92
+ model: "gpt-3.5-turbo",
93
+ messages: messages,
94
+ }
95
+ )
96
+ response.dig("choices", 0, "message", "content")
97
+ end
98
+
99
+ def summarize_chunk(chunk, chunk_index, chunk_count, previous_chunks_summary)
100
+ template = LINE_SUMMARY_PROMPT_TEMPLATE
101
+ prompt = Mustache.render(template.to_json, { chunkCount: chunk_count, chunkIndex: chunk_index + 1, chunk: chunk.join("\n").to_json })
102
+ messages = JSON.parse(prompt)
103
+
104
+ response = Ai.client.chat(
105
+ parameters: {
106
+ model: "gpt-3.5-turbo",
107
+ messages: messages,
108
+ }
109
+ )
110
+ response.dig("choices", 0, "message", "content")
111
+ end
112
+
113
+ def split_lines_into_equal_size_chunks(lines, max_chunk_word_count)
114
+ chunks = []
115
+ chunk = []
116
+ chunk_word_count = 0
117
+ lines.each do |line|
118
+ line_word_count = line.split.size
119
+ if chunk_word_count + line_word_count > max_chunk_word_count
120
+ chunks << chunk
121
+ chunk = []
122
+ chunk_word_count = 0
123
+ end
124
+ chunk << line
125
+ chunk_word_count += line_word_count
126
+ end
127
+ chunks << chunk
128
+ chunks
129
+ end
130
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: summarize-meeting
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sean Devine
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-03-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: optparse
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: dotenv
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: mustache
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
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: guard-rspec
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: vcr
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: A command line utility that summarizes a meeting using generative language
126
+ models.
127
+ email: sean-devine@x-b-e.com
128
+ executables:
129
+ - summarize-meeting
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - bin/summarize-meeting
134
+ - lib/ai.rb
135
+ - lib/meeting.rb
136
+ homepage:
137
+ licenses: []
138
+ metadata: {}
139
+ post_install_message:
140
+ rdoc_options: []
141
+ require_paths:
142
+ - "."
143
+ required_ruby_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubygems_version: 3.1.4
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: A command line utility that summarizes a meeting
158
+ test_files: []