summarize-meeting 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 +7 -0
- data/bin/summarize-meeting +65 -0
- data/lib/ai.rb +26 -0
- data/lib/meeting.rb +130 -0
- 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: []
|