word_smith 0.1.4
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/ws +5 -0
- data/lib/command_runner/index.rb +156 -0
- data/lib/command_runner/translation.rb +123 -0
- data/lib/config.rb +5 -0
- data/lib/helpers/logo_visualizer.rb +14 -0
- data/lib/helpers/str.rb +17 -0
- data/lib/index.rb +34 -0
- data/lib/migrations/1_words.rb +26 -0
- data/lib/migrations/index.rb +25 -0
- data/lib/models/word.rb +134 -0
- data/lib/services/db.rb +24 -0
- data/lib/services/logger.rb +22 -0
- data/lib/services/open_a_i.rb +173 -0
- data/version.rb +3 -0
- metadata +57 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 7dc1f6956a9ee72a61859869da8f0413fe26fdc42ad306e68dbce28f184cbb55
|
|
4
|
+
data.tar.gz: 1c8563f63c8ed9febfb1a332d4fbf69400642590ed86b658675c233a58c4c4c9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4888cd0c6ebabdd9ba9416aa6d4a8decc4647b89db7d2c5edcbb404b0670e773ed51717821c597d648a058d8b01bb3b53006f30f87405d9e3a9ba7cd12f48de9
|
|
7
|
+
data.tar.gz: 490683d03531c59041cecd173bf0b1b03c69136debd215e6b6bf2f022df3c2e87b8cddeeffbf508b0e8449994b6c9eaf706a5fb9786a888d2f5e5d7a8447da01
|
data/bin/ws
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require_relative '../../version'
|
|
6
|
+
require_relative '../services/open_a_i'
|
|
7
|
+
require_relative '../helpers/str'
|
|
8
|
+
require_relative 'translation'
|
|
9
|
+
require 'sorbet-runtime'
|
|
10
|
+
|
|
11
|
+
module WordSmith
|
|
12
|
+
module CommandRunner
|
|
13
|
+
class ArgumentError < StandardError; end
|
|
14
|
+
class BadPermutationOfArgumentsError < ArgumentError; end
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
extend T::Sig
|
|
18
|
+
|
|
19
|
+
EXECUTABLE_NAME = 'ws'
|
|
20
|
+
OPENAI_API_KEY_COMMAND = '--set-openai-api-key'
|
|
21
|
+
OPENAI_ORG_ID_COMMAND = '--set-openai-org-id'
|
|
22
|
+
|
|
23
|
+
class Options < T::Struct
|
|
24
|
+
prop :no_cache, T::Boolean
|
|
25
|
+
prop :file_path, T.nilable(String)
|
|
26
|
+
prop :target_language, T.nilable(String)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { params(args: T::Array[String]).void }
|
|
30
|
+
def run(args)
|
|
31
|
+
args_clone = args.clone
|
|
32
|
+
options = Options.new(no_cache: false, file_path: nil, target_language: nil)
|
|
33
|
+
|
|
34
|
+
parser = OptionParser.new do |opts|
|
|
35
|
+
opts.banner = "Usage: #{EXECUTABLE_NAME} [word] [options...]"
|
|
36
|
+
|
|
37
|
+
opts.on('-f', '--file [FILE_PATH]', 'Read words from a file') do |file_path|
|
|
38
|
+
options.file_path = file_path
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
opts.on('--target-language [LANGUAGE_CODE]',
|
|
42
|
+
'If you want to translate the word to a specific language, specify the language. e.g: Spanish') do |target_language|
|
|
43
|
+
options.target_language = target_language
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
opts.on("#{OPENAI_API_KEY_COMMAND} [key]", 'Set the OpenAI API key') do |key|
|
|
47
|
+
store_open_a_i_api_key(key)
|
|
48
|
+
|
|
49
|
+
exit
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
opts.on("#{OPENAI_ORG_ID_COMMAND} [key]", 'Set the OpenAI Org ID') do |key|
|
|
53
|
+
store_open_a_i_org_id(key)
|
|
54
|
+
|
|
55
|
+
exit
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
opts.on('--no-cache', 'Translate word without using cache') do
|
|
59
|
+
options.no_cache = true
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
opts.on_tail('-h', '--help', 'Show help') do
|
|
63
|
+
print_help(opts)
|
|
64
|
+
|
|
65
|
+
exit
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
opts.on_tail('-v', '--version', 'Show version') do
|
|
69
|
+
print_version
|
|
70
|
+
|
|
71
|
+
exit
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
parser.parse!(args_clone)
|
|
76
|
+
|
|
77
|
+
input_text = args_clone.join(' ').chomp
|
|
78
|
+
|
|
79
|
+
if !input_text.empty? && !options.file_path.nil?
|
|
80
|
+
raise BadPermutationOfArgumentsError, 'Both word and file path cannot be provided'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
raise ArgumentError, 'No word or file path provided' if input_text.empty? && options.file_path.nil?
|
|
84
|
+
|
|
85
|
+
translation_options = {
|
|
86
|
+
no_cache: options.no_cache,
|
|
87
|
+
target_language: options.target_language
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
unless options.file_path.nil?
|
|
91
|
+
raise ArgumentError, "File not found: #{options.file_path}" unless File.exist?(options.file_path)
|
|
92
|
+
|
|
93
|
+
File.readlines(T.must(options.file_path)).each_with_index do |line, index|
|
|
94
|
+
puts Rainbow('-' * 60).yellow.bright unless index.zero?
|
|
95
|
+
|
|
96
|
+
Translation.run(line, translation_options)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Translation.run(input_text, translation_options)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
sig { params(opts: OptionParser).void }
|
|
108
|
+
def print_help(opts)
|
|
109
|
+
puts Helpers::Str.lstr_every_line("
|
|
110
|
+
Translate words, phrases and words in context of a sentence.
|
|
111
|
+
|
|
112
|
+
-> To translate a word or a phrase, just run:
|
|
113
|
+
#{Rainbow(EXECUTABLE_NAME + ' [word/phrase]').blue.bold}
|
|
114
|
+
|
|
115
|
+
-> To translate a word in context of a sentence, you should write the sentence and wrap the word in slashes:
|
|
116
|
+
#{Rainbow(EXECUTABLE_NAME + ' a /random/ sentence').blue.bold}
|
|
117
|
+
In this example, the word 'random' will be translated in context of the sentence.
|
|
118
|
+
|
|
119
|
+
#{Rainbow('----------------------------------------').yellow.bright}
|
|
120
|
+
")
|
|
121
|
+
|
|
122
|
+
puts '', opts
|
|
123
|
+
|
|
124
|
+
return unless WordSmith::Services::OpenAI.api_key.nil?
|
|
125
|
+
|
|
126
|
+
return unless WordSmith::Services::OpenAI.api_key.nil?
|
|
127
|
+
|
|
128
|
+
open_a_i_message = T.let("
|
|
129
|
+
To use OpenAI, you need to set an API key and Org ID.
|
|
130
|
+
You can set the API key using '#{EXECUTABLE_NAME} #{OPENAI_API_KEY_COMMAND} <key>'
|
|
131
|
+
You can set the Org ID using '#{EXECUTABLE_NAME} #{OPENAI_ORG_ID_COMMAND} <key>'
|
|
132
|
+
", String)
|
|
133
|
+
puts '', WordSmith::Helpers::Str.lstr_every_line(open_a_i_message)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
sig { void }
|
|
137
|
+
def print_version
|
|
138
|
+
puts "Version #{WordSmith::VERSION}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
sig { params(key: String).void }
|
|
142
|
+
def store_open_a_i_api_key(key)
|
|
143
|
+
WordSmith::Services::OpenAI.store_api_key(key)
|
|
144
|
+
|
|
145
|
+
puts 'OpenAI API key set!'
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
sig { params(key: String).void }
|
|
149
|
+
def store_open_a_i_org_id(key)
|
|
150
|
+
WordSmith::Services::OpenAI.store_org_id(key)
|
|
151
|
+
|
|
152
|
+
puts 'OpenAI Org ID set!'
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require_relative '../services/open_a_i'
|
|
6
|
+
require_relative '../models/word'
|
|
7
|
+
require_relative '../services/logger'
|
|
8
|
+
|
|
9
|
+
module WordSmith
|
|
10
|
+
module CommandRunner
|
|
11
|
+
module Translation
|
|
12
|
+
class << self
|
|
13
|
+
extend T::Sig
|
|
14
|
+
|
|
15
|
+
sig { params(input_text: String, options: { no_cache: T::Boolean, target_language: T.nilable(String) }).void }
|
|
16
|
+
def run(input_text, options)
|
|
17
|
+
is_contextual_translation = input_text.match?(%r{/[a-zA-Z]+/})
|
|
18
|
+
|
|
19
|
+
if is_contextual_translation
|
|
20
|
+
sentence = input_text.gsub(%r{/([a-zA-Z]+)/}, '\1')
|
|
21
|
+
|
|
22
|
+
literal_word_match = input_text.match(%r{/([a-zA-Z]+)/})
|
|
23
|
+
raise "Invalid word: #{input_text}" if literal_word_match.nil?
|
|
24
|
+
|
|
25
|
+
word = T.cast(literal_word_match.captures[0], String)
|
|
26
|
+
|
|
27
|
+
result = find_or_create_contextual_translation(word, sentence, **options)
|
|
28
|
+
|
|
29
|
+
puts '', "In context of \"#{Rainbow(sentence).blue.bold}\":"
|
|
30
|
+
|
|
31
|
+
print_common_parts(result)
|
|
32
|
+
|
|
33
|
+
return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
word = input_text.chomp
|
|
37
|
+
|
|
38
|
+
result = find_or_create_word_translation(word, **options)
|
|
39
|
+
|
|
40
|
+
print_common_parts(result)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
sig do
|
|
46
|
+
params(word: String, no_cache: T::Boolean, target_language: T.nilable(String)).returns(Models::Word)
|
|
47
|
+
end
|
|
48
|
+
def find_or_create_word_translation(word, no_cache: false, target_language: nil)
|
|
49
|
+
existing_word = Models::Word.find_by_word(word)
|
|
50
|
+
|
|
51
|
+
unless no_cache || existing_word.nil?
|
|
52
|
+
Services::Logger.debug_log("Found existing word: #{existing_word.word}")
|
|
53
|
+
|
|
54
|
+
return existing_word
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
Services::Logger.debug_log("Translating the word: #{word}")
|
|
58
|
+
|
|
59
|
+
result = Services::OpenAI.new.translate(text: word, target_language: target_language)
|
|
60
|
+
|
|
61
|
+
if existing_word.nil?
|
|
62
|
+
Services::Logger.debug_log("Creating new word: #{word}")
|
|
63
|
+
|
|
64
|
+
return Models::Word.create(word: word, pronunciation: result[:pronunciation], meaning: result[:meaning],
|
|
65
|
+
example: result[:example], target_language: target_language,
|
|
66
|
+
translation_to_target_language: result[:translation_to_target_language])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
Services::Logger.debug_log("Updating existing word: #{word}")
|
|
70
|
+
|
|
71
|
+
existing_word.update(word: word, pronunciation: result[:pronunciation], meaning: result[:meaning],
|
|
72
|
+
example: result[:example], target_language: target_language,
|
|
73
|
+
translation_to_target_language: result[:translation_to_target_language])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sig do
|
|
77
|
+
params(word: String, sentence: String, no_cache: T::Boolean,
|
|
78
|
+
target_language: T.nilable(String)).returns(Models::Word)
|
|
79
|
+
end
|
|
80
|
+
def find_or_create_contextual_translation(word, sentence, no_cache: false, target_language: nil)
|
|
81
|
+
existing_word = Models::Word.find_by_word(word)
|
|
82
|
+
|
|
83
|
+
unless no_cache || existing_word.nil?
|
|
84
|
+
Services::Logger.debug_log("Found existing word: #{existing_word.word}")
|
|
85
|
+
|
|
86
|
+
return existing_word
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
Services::Logger.debug_log("Translating the word: #{word}")
|
|
90
|
+
|
|
91
|
+
result = Services::OpenAI.new.translate_in_context_of_sentence(word: word, sentence: sentence,
|
|
92
|
+
target_language: target_language)
|
|
93
|
+
|
|
94
|
+
if existing_word.nil?
|
|
95
|
+
Services::Logger.debug_log("Creating new word: #{word}")
|
|
96
|
+
|
|
97
|
+
return Models::Word.create(word: word, pronunciation: result[:pronunciation], meaning: result[:meaning],
|
|
98
|
+
example: result[:example], target_language: target_language,
|
|
99
|
+
translation_to_target_language: result[:translation_to_target_language])
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
Services::Logger.debug_log("Updating existing word: #{word}")
|
|
103
|
+
|
|
104
|
+
existing_word.update(word: word, pronunciation: result[:pronunciation], meaning: result[:meaning],
|
|
105
|
+
example: result[:example], target_language: target_language,
|
|
106
|
+
translation_to_target_language: result[:translation_to_target_language])
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
sig { params(word: Models::Word).void }
|
|
110
|
+
def print_common_parts(word)
|
|
111
|
+
puts '', "#{Rainbow(word.word).green.bold}#{Rainbow(" (#{word.pronunciation})").blue}", "\n"
|
|
112
|
+
puts " #{Rainbow('Meaning:').bold}#{Rainbow(" #{word.meaning}").blue}", "\n"
|
|
113
|
+
puts " #{Rainbow('Example:').bold}#{Rainbow(" #{word.example}").blue}", "\n"
|
|
114
|
+
|
|
115
|
+
return if word.translation_to_target_language.nil?
|
|
116
|
+
|
|
117
|
+
puts " #{Rainbow('Translation to target language:').bold}#{Rainbow(" #{word.translation_to_target_language}").blue}",
|
|
118
|
+
"\n"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
data/lib/config.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module WordSmith
|
|
2
|
+
module Helpers
|
|
3
|
+
module LogoVisualizer
|
|
4
|
+
def self.draw
|
|
5
|
+
puts Rainbow("
|
|
6
|
+
▗▖ ▗▖ ▗▄▖ ▗▄▄▖ ▗▄▄▄ ▗▄▄▖▗▖ ▗▖▗▄▄▄▖▗▄▄▄▖▗▖ ▗▖
|
|
7
|
+
▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌▐▌ █ ▐▌ ▐▛▚▞▜▌ █ █ ▐▌ ▐▌
|
|
8
|
+
▐▌ ▐▌▐▌ ▐▌▐▛▀▚▖▐▌ █ ▝▀▚▖▐▌ ▐▌ █ █ ▐▛▀▜▌
|
|
9
|
+
▐▙█▟▌▝▚▄▞▘▐▌ ▐▌▐▙▄▄▀ ▗▄▄▞▘▐▌ ▐▌▗▄█▄▖ █ ▐▌ ▐▌
|
|
10
|
+
").blue.bold
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/helpers/str.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
module WordSmith
|
|
7
|
+
module Helpers
|
|
8
|
+
module Str
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(string: String).returns(String) }
|
|
12
|
+
def self.lstr_every_line(string)
|
|
13
|
+
string.split("\n").map(&:lstrip).join("\n")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/index.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
|
|
3
|
+
require_relative 'command_runner/index'
|
|
4
|
+
require_relative 'services/logger'
|
|
5
|
+
require_relative 'migrations/index'
|
|
6
|
+
require_relative 'helpers/logo_visualizer'
|
|
7
|
+
module WordSmith
|
|
8
|
+
class << self
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(args: T::Array[String]).void }
|
|
12
|
+
def run(args)
|
|
13
|
+
Helpers::LogoVisualizer.draw
|
|
14
|
+
|
|
15
|
+
run_migrations
|
|
16
|
+
|
|
17
|
+
CommandRunner.run(args)
|
|
18
|
+
rescue CommandRunner::ArgumentError => e
|
|
19
|
+
puts e.message
|
|
20
|
+
|
|
21
|
+
exit 1
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def run_migrations
|
|
27
|
+
Services::Logger.debug_log('Running migrations...')
|
|
28
|
+
|
|
29
|
+
Migrations.run
|
|
30
|
+
|
|
31
|
+
Services::Logger.debug_log('Migrations complete.')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../services/db'
|
|
5
|
+
|
|
6
|
+
module WordSmith
|
|
7
|
+
module Migrations
|
|
8
|
+
class Words
|
|
9
|
+
def self.up
|
|
10
|
+
Services::DB.instance.execute <<-SQL
|
|
11
|
+
CREATE TABLE IF NOT EXISTS words (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
13
|
+
word TEXT NOT NULL,
|
|
14
|
+
pronunciation TEXT NOT NULL,
|
|
15
|
+
meaning TEXT NOT NULL,
|
|
16
|
+
example TEXT NOT NULL,
|
|
17
|
+
context TEXT DEFAULT NULL,
|
|
18
|
+
target_language TEXT DEFAULT NULL,
|
|
19
|
+
translation_to_target_language TEXT DEFAULT NULL,
|
|
20
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
21
|
+
)
|
|
22
|
+
SQL
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
|
|
6
|
+
module WordSmith
|
|
7
|
+
module Migrations
|
|
8
|
+
class << self
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { void }
|
|
12
|
+
def run
|
|
13
|
+
Dir.glob(File.join(__dir__, './*.rb')).each do |file| # rubocop:disable Lint/NonDeterministicRequireOrder
|
|
14
|
+
next if file == __FILE__
|
|
15
|
+
|
|
16
|
+
require_relative file
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
WordSmith::Migrations.constants.each do |constant|
|
|
20
|
+
WordSmith::Migrations.const_get(constant).up
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/models/word.rb
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../services/db'
|
|
5
|
+
require 'sorbet-runtime'
|
|
6
|
+
|
|
7
|
+
module WordSmith
|
|
8
|
+
module Models
|
|
9
|
+
class Word
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { returns(Integer) }
|
|
13
|
+
attr_reader :id
|
|
14
|
+
|
|
15
|
+
sig { returns(String) }
|
|
16
|
+
attr_reader :word
|
|
17
|
+
|
|
18
|
+
sig { returns(String) }
|
|
19
|
+
attr_reader :pronunciation
|
|
20
|
+
|
|
21
|
+
sig { returns(String) }
|
|
22
|
+
attr_reader :meaning
|
|
23
|
+
|
|
24
|
+
sig { returns(String) }
|
|
25
|
+
attr_reader :example
|
|
26
|
+
|
|
27
|
+
sig { returns(T.nilable(String)) }
|
|
28
|
+
attr_reader :context
|
|
29
|
+
|
|
30
|
+
sig { returns(T.nilable(String)) }
|
|
31
|
+
attr_reader :target_language
|
|
32
|
+
|
|
33
|
+
sig { returns(T.nilable(String)) }
|
|
34
|
+
attr_reader :translation_to_target_language
|
|
35
|
+
|
|
36
|
+
sig do
|
|
37
|
+
params(id: Integer, word: String, pronunciation: String, meaning: String, example: String,
|
|
38
|
+
context: T.nilable(String), target_language: T.nilable(String),
|
|
39
|
+
translation_to_target_language: T.nilable(String)).void
|
|
40
|
+
end
|
|
41
|
+
def initialize(
|
|
42
|
+
id:,
|
|
43
|
+
word:,
|
|
44
|
+
pronunciation:,
|
|
45
|
+
meaning:,
|
|
46
|
+
example:,
|
|
47
|
+
context: nil,
|
|
48
|
+
target_language: nil,
|
|
49
|
+
translation_to_target_language: nil
|
|
50
|
+
)
|
|
51
|
+
@id = id
|
|
52
|
+
@word = word
|
|
53
|
+
@pronunciation = pronunciation
|
|
54
|
+
@meaning = meaning
|
|
55
|
+
@example = example
|
|
56
|
+
@context = context
|
|
57
|
+
@target_language = target_language
|
|
58
|
+
@translation_to_target_language = translation_to_target_language
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sig { void }
|
|
62
|
+
def delete
|
|
63
|
+
Services::DB.instance.execute('DELETE FROM words WHERE id = ?', [@id])
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
sig do
|
|
67
|
+
params(word: String, pronunciation: String, meaning: String, example: String,
|
|
68
|
+
context: T.nilable(String), target_language: T.nilable(String),
|
|
69
|
+
translation_to_target_language: T.nilable(String)).returns(Word)
|
|
70
|
+
end
|
|
71
|
+
def update(word:, pronunciation:, meaning:, example:, context: nil, target_language: nil,
|
|
72
|
+
translation_to_target_language: nil)
|
|
73
|
+
result = Services::DB.instance.execute('UPDATE words SET word = ?, pronunciation = ?, meaning = ?, example = ?, context = ?, target_language = ?, translation_to_target_language = ? WHERE id = ? RETURNING *',
|
|
74
|
+
[word, pronunciation, meaning, example, context, target_language,
|
|
75
|
+
translation_to_target_language, @id]).first
|
|
76
|
+
|
|
77
|
+
@word = result[1]
|
|
78
|
+
@pronunciation = result[2]
|
|
79
|
+
@meaning = result[3]
|
|
80
|
+
@example = result[4]
|
|
81
|
+
@context = result[5]
|
|
82
|
+
@target_language = result[6]
|
|
83
|
+
@translation_to_target_language = result[7]
|
|
84
|
+
|
|
85
|
+
self
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
class << self
|
|
89
|
+
extend T::Sig
|
|
90
|
+
|
|
91
|
+
sig { returns(T::Array[Word]) }
|
|
92
|
+
def all
|
|
93
|
+
Services::DB.instance.execute('SELECT id, word, pronunciation, meaning, example, context, target_language, translation_to_target_language FROM words').map do |row|
|
|
94
|
+
new(id: row[0], word: row[1], pronunciation: row[2], meaning: row[3], example: row[4], context: row[5],
|
|
95
|
+
target_language: row[6], translation_to_target_language: row[7])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig do
|
|
100
|
+
params(word: String, pronunciation: String, meaning: String, example: String,
|
|
101
|
+
context: T.nilable(String), target_language: T.nilable(String),
|
|
102
|
+
translation_to_target_language: T.nilable(String)).returns(Word)
|
|
103
|
+
end
|
|
104
|
+
def create(word:, pronunciation:, meaning:, example:, context: nil, target_language: nil,
|
|
105
|
+
translation_to_target_language: nil)
|
|
106
|
+
result = Services::DB.instance.execute('INSERT INTO words (word, pronunciation, meaning, example, context, target_language, translation_to_target_language) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *',
|
|
107
|
+
[word, pronunciation, meaning, example, context, target_language,
|
|
108
|
+
translation_to_target_language]).first
|
|
109
|
+
|
|
110
|
+
new(id: result[0], word: result[1], pronunciation: result[2], meaning: result[3], example: result[4],
|
|
111
|
+
context: result[5], target_language: result[6], translation_to_target_language: result[7])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
sig { params(id: Integer).returns(Word) }
|
|
115
|
+
def find(id)
|
|
116
|
+
Services::DB.instance.execute('SELECT id, word, pronunciation, meaning, example, context, target_language, translation_to_target_language FROM words WHERE id = ?',
|
|
117
|
+
[id]).map do |row|
|
|
118
|
+
new(id: row[0], word: row[1], pronunciation: row[2], meaning: row[3], example: row[4], context: row[5],
|
|
119
|
+
target_language: row[6], translation_to_target_language: row[7])
|
|
120
|
+
end.first
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
sig { params(word: String).returns(T.nilable(Word)) }
|
|
124
|
+
def find_by_word(word)
|
|
125
|
+
Services::DB.instance.execute('SELECT id, word, pronunciation, meaning, example, context, target_language, translation_to_target_language FROM words WHERE word = ?',
|
|
126
|
+
[word]).map do |row|
|
|
127
|
+
new(id: row[0], word: row[1], pronunciation: row[2], meaning: row[3], example: row[4], context: row[5],
|
|
128
|
+
target_language: row[6], translation_to_target_language: row[7])
|
|
129
|
+
end.first
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
data/lib/services/db.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sqlite3'
|
|
5
|
+
require 'sorbet-runtime'
|
|
6
|
+
require 'singleton'
|
|
7
|
+
|
|
8
|
+
module WordSmith
|
|
9
|
+
module Services
|
|
10
|
+
class DB < SQLite3::Database
|
|
11
|
+
extend T::Sig
|
|
12
|
+
include Singleton
|
|
13
|
+
|
|
14
|
+
DB_FILE = File.join(File.dirname(__FILE__), '../..', 'db', 'storage.db')
|
|
15
|
+
|
|
16
|
+
sig { void }
|
|
17
|
+
def initialize
|
|
18
|
+
Dir.mkdir(File.dirname(DB_FILE)) unless Dir.exist?(File.dirname(DB_FILE))
|
|
19
|
+
|
|
20
|
+
@db = super(DB_FILE)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require 'rainbow'
|
|
6
|
+
|
|
7
|
+
module WordSmith
|
|
8
|
+
module Services
|
|
9
|
+
module Logger
|
|
10
|
+
class << self
|
|
11
|
+
extend T::Sig
|
|
12
|
+
|
|
13
|
+
sig { params(message: String).void }
|
|
14
|
+
def debug_log(message)
|
|
15
|
+
return unless Config::DEBUG_MODE
|
|
16
|
+
|
|
17
|
+
puts Rainbow('DEBUG:').white.bg(:yellow) + " #{message}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'sorbet-runtime'
|
|
5
|
+
require 'openai'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
require_relative '../config'
|
|
9
|
+
|
|
10
|
+
module WordSmith
|
|
11
|
+
module Services
|
|
12
|
+
class OpenAI
|
|
13
|
+
class << self
|
|
14
|
+
extend T::Sig
|
|
15
|
+
|
|
16
|
+
# Storage methods
|
|
17
|
+
|
|
18
|
+
OPEN_AI_API_KEY_FILE = File.join(File.dirname(__FILE__), '../../', '.openai_api_key')
|
|
19
|
+
OPEN_AI_ORG_ID_FILE = File.join(File.dirname(__FILE__), '../../', '.openai_org_id')
|
|
20
|
+
|
|
21
|
+
sig { params(key: String).void }
|
|
22
|
+
def store_api_key(key)
|
|
23
|
+
File.write(OPEN_AI_API_KEY_FILE, key)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { returns(T.nilable(String)) }
|
|
27
|
+
def api_key
|
|
28
|
+
return nil unless File.exist?(OPEN_AI_API_KEY_FILE)
|
|
29
|
+
|
|
30
|
+
File.read(OPEN_AI_API_KEY_FILE)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
sig { params(key: String).void }
|
|
34
|
+
def store_org_id(key)
|
|
35
|
+
File.write(OPEN_AI_ORG_ID_FILE, key)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { returns(T.nilable(String)) }
|
|
39
|
+
def org_id
|
|
40
|
+
return nil unless File.exist?(OPEN_AI_ORG_ID_FILE)
|
|
41
|
+
|
|
42
|
+
File.read(OPEN_AI_ORG_ID_FILE)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
extend T::Sig
|
|
47
|
+
|
|
48
|
+
class OpenAIKeyNotSetError < StandardError; end
|
|
49
|
+
class OpenAIOrgIDNotSetError < StandardError; end
|
|
50
|
+
|
|
51
|
+
def initialize
|
|
52
|
+
raise OpenAIKeyNotSetError if OpenAI.api_key.nil?
|
|
53
|
+
raise OpenAIOrgIDNotSetError if OpenAI.org_id.nil?
|
|
54
|
+
|
|
55
|
+
::OpenAI.configure do |config|
|
|
56
|
+
config.access_token = OpenAI.api_key
|
|
57
|
+
config.organization_id = OpenAI.org_id
|
|
58
|
+
config.log_errors = Config::DEBUG_MODE
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
@client = ::OpenAI::Client.new
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
sig do
|
|
65
|
+
params(text: String, target_language: T.nilable(String)).returns({ word: String, pronunciation: String,
|
|
66
|
+
meaning: String, example: String,
|
|
67
|
+
translation_to_target_language: T.nilable(String) })
|
|
68
|
+
end
|
|
69
|
+
def translate(text:, target_language:)
|
|
70
|
+
response = @client.chat(
|
|
71
|
+
parameters: {
|
|
72
|
+
model: 'gpt-4o-mini',
|
|
73
|
+
response_format: { type: 'json_schema',
|
|
74
|
+
json_schema: {
|
|
75
|
+
name: 'Translation',
|
|
76
|
+
strict: true,
|
|
77
|
+
schema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
word: { type: 'string' },
|
|
81
|
+
pronunciation: { type: 'string' },
|
|
82
|
+
meaning: { type: 'string' },
|
|
83
|
+
example: { type: 'string' },
|
|
84
|
+
translation_to_target_language: { type: %w[string null] }
|
|
85
|
+
},
|
|
86
|
+
additionalProperties: false,
|
|
87
|
+
required: %w[word pronunciation meaning example translation_to_target_language]
|
|
88
|
+
}
|
|
89
|
+
} },
|
|
90
|
+
messages: [
|
|
91
|
+
{
|
|
92
|
+
role: 'system', content: Helpers::Str.lstr_every_line("
|
|
93
|
+
You are a great translator. Give the user the meaning of the word in English.
|
|
94
|
+
Also give the user an example of the sentence using the word.
|
|
95
|
+
#{get_target_language_prompt(target_language)}
|
|
96
|
+
Do not hallucinate.
|
|
97
|
+
Be to the point and concise without an
|
|
98
|
+
")
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
role: 'user', content: text
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
temperature: 0.7
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
JSON.parse(response.dig('choices', 0, 'message', 'content'), { symbolize_names: true })
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
sig do
|
|
112
|
+
params(sentence: String,
|
|
113
|
+
word: String,
|
|
114
|
+
target_language: T.nilable(String)).returns({ word: String, pronunciation: String, meaning: String,
|
|
115
|
+
example: String })
|
|
116
|
+
end
|
|
117
|
+
def translate_in_context_of_sentence(sentence:, word:, target_language:)
|
|
118
|
+
response = @client.chat(
|
|
119
|
+
parameters: {
|
|
120
|
+
model: 'gpt-4o-mini',
|
|
121
|
+
response_format: { type: 'json_schema',
|
|
122
|
+
json_schema: {
|
|
123
|
+
name: 'Translation',
|
|
124
|
+
strict: true,
|
|
125
|
+
schema: {
|
|
126
|
+
type: 'object',
|
|
127
|
+
properties: {
|
|
128
|
+
word: { type: 'string' },
|
|
129
|
+
pronunciation: { type: 'string' },
|
|
130
|
+
meaning: { type: 'string' },
|
|
131
|
+
example: { type: 'string' },
|
|
132
|
+
translation_to_target_language: { type: %w[string null] }
|
|
133
|
+
},
|
|
134
|
+
additionalProperties: false,
|
|
135
|
+
required: %w[word pronunciation meaning example]
|
|
136
|
+
}
|
|
137
|
+
} },
|
|
138
|
+
messages: [
|
|
139
|
+
{
|
|
140
|
+
role: 'system', content: Helpers::Str.lstr_every_line("
|
|
141
|
+
You are a great translator. Give the user the meaning of the word in context of the sentence in English.
|
|
142
|
+
Also give the user an example of the sentence using the meaning of the word in that context.
|
|
143
|
+
#{get_target_language_prompt(target_language)}
|
|
144
|
+
Do not hallucinate.
|
|
145
|
+
Be to the point and concise without an
|
|
146
|
+
")
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
role: 'user', content: Helpers::Str.lstr_every_line("
|
|
150
|
+
Sentence: #{sentence}
|
|
151
|
+
Word: #{word}
|
|
152
|
+
")
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
temperature: 0.7
|
|
156
|
+
}
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
JSON.parse(response.dig('choices', 0, 'message', 'content'), { symbolize_names: true })
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
private
|
|
163
|
+
|
|
164
|
+
def get_target_language_prompt(target_language)
|
|
165
|
+
return '' if target_language.nil?
|
|
166
|
+
|
|
167
|
+
Helpers::Str.lstr_every_line("
|
|
168
|
+
Also, please give me the literal translation of the word in #{target_language} language.
|
|
169
|
+
")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
data/version.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: word_smith
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Pouya Mozaffar Magham
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2024-09-13 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Command-line tool for quick and easy English word lookup.
|
|
14
|
+
email: pouya.mozafar@gmail.com
|
|
15
|
+
executables:
|
|
16
|
+
- ws
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- bin/ws
|
|
21
|
+
- lib/command_runner/index.rb
|
|
22
|
+
- lib/command_runner/translation.rb
|
|
23
|
+
- lib/config.rb
|
|
24
|
+
- lib/helpers/logo_visualizer.rb
|
|
25
|
+
- lib/helpers/str.rb
|
|
26
|
+
- lib/index.rb
|
|
27
|
+
- lib/migrations/1_words.rb
|
|
28
|
+
- lib/migrations/index.rb
|
|
29
|
+
- lib/models/word.rb
|
|
30
|
+
- lib/services/db.rb
|
|
31
|
+
- lib/services/logger.rb
|
|
32
|
+
- lib/services/open_a_i.rb
|
|
33
|
+
- version.rb
|
|
34
|
+
homepage: https://github.com/pmzi/WordSmith
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata: {}
|
|
38
|
+
post_install_message:
|
|
39
|
+
rdoc_options: []
|
|
40
|
+
require_paths:
|
|
41
|
+
- lib
|
|
42
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
48
|
+
requirements:
|
|
49
|
+
- - ">="
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '0'
|
|
52
|
+
requirements: []
|
|
53
|
+
rubygems_version: 3.5.11
|
|
54
|
+
signing_key:
|
|
55
|
+
specification_version: 4
|
|
56
|
+
summary: MR Word Smith!
|
|
57
|
+
test_files: []
|