llm-spell 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: 51c9111bd6a08d766b059d39518f0678021dd1e7b67fb18c9693681a9d6c4dce
4
+ data.tar.gz: 82298f5be8162f8eb29d031e55974fc566cc586e02d34fa9eead3502a657400a
5
+ SHA512:
6
+ metadata.gz: 379205906480b491d1436d123a07c5ade826e55be38a0b3681d5deee38107ae80aa8a87e112e817c17ec676019951c9403d45296cdfae4df319384720daff121
7
+ data.tar.gz: 3d96bc466a4694502406226d4178f8da11eee55100cfb4b59159e89c5b32fa22f7dae0ce79957183a0c6a7b496182637172854c51093d17e43eefacf7dacde64
data/README.md ADDED
@@ -0,0 +1,103 @@
1
+ ## About
2
+
3
+ llm-spell is both a library and command-line utility that
4
+ corrects spelling mistakes using a Large Language Model (LLM).
5
+ It is powered by [llm.rb](https://github.com/llmrb/llm).
6
+
7
+ ## Motivation
8
+
9
+ This project was born while I was working on the documentation
10
+ for a friend's open source project. After realizing how much
11
+ manual effort was involved with traditional spell checkers I
12
+ decided to see if I could leverage LLMs to make the process
13
+ easier and also faster.
14
+
15
+ Compared to traditional spell checkers like `aspell` and `hunspell`,
16
+ `llm-spell` provides significantly more accurate suggestions with
17
+ far fewer false positives – eliminating the need to manually
18
+ ignore irrelevant corrections, and that often reduces the overall
19
+ time spent on correcting spelling mistakes.
20
+
21
+ I would call the experiment a success but I also realize this
22
+ approach is not for everyone, or every situation. For example,
23
+ my friend preferred to not use AI for this and instead we opted
24
+ to stick with `hunspell` – even though it meant more
25
+ manual work.
26
+
27
+ ## Features
28
+
29
+ - ✨ **LLM-powered corrections** – smarter spelling fixes than traditional tools
30
+ - 🤖 **Fewer false positives** – avoids flagging uncommon but valid words
31
+ - 🌐 **Broad provider support** – use OpenAI, Gemini, or xAI (Grok) out of the box
32
+ - 💻 **Offline ready** – run locally with Ollama and LlamaCpp, no cloud required
33
+ - 🔒 **Privacy** – keep sensitive text local with offline models
34
+ - 🛠️ **Easy to use** – provides an easy to use library and command-line utility
35
+
36
+ ## Library
37
+
38
+ ```ruby
39
+ #!/usr/bin/env ruby
40
+ require "llm"
41
+ require "llm/spell"
42
+
43
+ ##
44
+ # Text
45
+ llm = LLM.openai(key: ENV["OPENAI_SECRET"])
46
+ text = LLM::Spell::Text.new("Ths is a smple txt with sme speling erors.", llm)
47
+ print "mistakes: ", text.mistakes, "\n"
48
+ print "corrections: ", text.corrections, "\n"
49
+
50
+ ##
51
+ # PDF
52
+ llm = LLM.openai(key: ENV["OPENAI_SECRET"])
53
+ file = LLM::Spell::Document.new("typos.pdf", llm)
54
+ print "mistakes: ", file.mistakes, "\n"
55
+ print "corrections: ", file.corrections, "\n"
56
+ ```
57
+
58
+ ## CLI
59
+
60
+ #### Configuration
61
+
62
+ The command line interface can be configured through the configuration file
63
+ located at `$XDG_CONFIG_HOME/llm-spell.yml` or `~/.config/llm-spell.yml`. It
64
+ is also possible to provide the configuration at the command-line, but usually
65
+ it's more convenient to use the configuration file:
66
+
67
+ ```yaml
68
+ # ~/.config/llm-spell.yml
69
+ openai:
70
+ key: YOURKEY
71
+ gemini:
72
+ key: YOURKEY
73
+ xai:
74
+ key: YOURKEY
75
+ ollama:
76
+ host: localhost
77
+ llamacpp:
78
+ host: localhost
79
+ ```
80
+
81
+ #### Usage
82
+
83
+ ```sh
84
+ Usage: llm-spell [OPTIONS]
85
+ -p, --provider NAME Required. Options: gemini, openai, xai, ollama or llamacpp.
86
+ -f, --file FILE Required. The file to check.
87
+ -k, --key [KEY] Optional. Required by gemini, openai, and xai.
88
+ -v, --version Optional. Print the version and exit.
89
+ ```
90
+
91
+ ## Demo
92
+
93
+ <details>
94
+ <summary>Start demo</summary>
95
+ <img src="share/llm-spell/demo.gif" alt="Demo of llm-spell in action" />
96
+ </details>
97
+
98
+ ## License
99
+
100
+ [BSD Zero Clause](https://choosealicense.com/licenses/0bsd/)
101
+ <br>
102
+ See [LICENSE](./LICENSE)
103
+
data/bin/llm-spell ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ def wait
5
+ Process.wait
6
+ rescue Interrupt
7
+ retry
8
+ end
9
+
10
+ def libexec
11
+ File.realpath File.join(__dir__, "..", "libexec", "llm-spell")
12
+ end
13
+
14
+ def main(argv)
15
+ Process.spawn File.join(libexec, "check"), *ARGV[0..]
16
+ Process.wait
17
+ rescue Interrupt
18
+ wait
19
+ end
20
+ main(ARGV)
21
+ exit $?&.exitstatus || 1
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Spell
4
+ class CLI
5
+ attr_reader :text, :content
6
+
7
+ def initialize(text)
8
+ @text = text
9
+ end
10
+
11
+ def start
12
+ say "please wait"
13
+ if text.mistakes.empty?
14
+ say "no mistakes found"
15
+ else
16
+ text.mistakes.each.with_index do |mistake, i|
17
+ correction = text.corrections[i]
18
+ print "#{mistake} => #{correction}", "\n"
19
+ print "Replace? (y/N): "
20
+ res = $stdin.gets
21
+ @text.to_s.gsub!(mistake, correction) if res&.strip&.downcase == "y"
22
+ print "\n"
23
+ end
24
+ end
25
+ @text
26
+ end
27
+
28
+ private
29
+
30
+ def say(*messages)
31
+ print "llm-spell: ", *messages, "\n"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Spell
4
+ ##
5
+ # The {LLM::Spell::Document LLM::Spell::Document} class can analyze a given
6
+ # text file or document, and return any spelling mistakes that were
7
+ # found &ndash; along with suggested corrections.
8
+ class Document
9
+ include LLM::Spell::Engine
10
+
11
+ ##
12
+ # @param [File] input
13
+ # The file to be analyzed
14
+ # @param [LLM::Provider] provider
15
+ # An instance of LLM::Provider
16
+ # @return [LLM::Spell::Document]
17
+ def initialize(input, llm)
18
+ @input = input
19
+ @llm = llm
20
+ @bot = LLM::Bot.new(llm, schema:)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Spell
4
+ module Engine
5
+ ##
6
+ # @return [Array<String>]
7
+ # An array of unique spelling mistakes found in the text
8
+ def mistakes
9
+ response["mistakes"].uniq
10
+ end
11
+
12
+ ##
13
+ # @return [Array<String>]
14
+ # An array of unique corrections corresponding to the mistakes
15
+ def corrections
16
+ response["corrections"].uniq
17
+ end
18
+
19
+ ##
20
+ # @return [String]
21
+ def inspect = "#<#{self.class}:0x#{object_id.to_s(16)} " \
22
+ "mistakes=#{mistakes.size} corrections=#{corrections.size}>"
23
+
24
+ private
25
+
26
+ attr_reader :input, :llm, :bot
27
+
28
+ def prompt
29
+ <<~PROMPT
30
+ Your task is to find all spelling mistakes in the user's input and provide corrections.
31
+ Return a JSON object with two arrays:
32
+ - "mistakes": a list of all detected spelling mistakes.
33
+ - "corrections": a list of corrections, where each correction is at the same index as its corresponding mistake in the "mistakes" array.
34
+ Make sure that each mistake and its correction appear at the same index in their respective arrays.
35
+ PROMPT
36
+ end
37
+
38
+ def schema
39
+ llm.schema.object(
40
+ mistakes: llm.schema.array(llm.schema.string.required),
41
+ corrections: llm.schema.array(llm.schema.string.required)
42
+ )
43
+ end
44
+
45
+ def response
46
+ @response ||= begin
47
+ input = (LLM::Spell::Document === self) ? File.open(@input, "rb") : @input
48
+ bot.chat prompt, role: :user
49
+ bot.chat input, role: :user
50
+ bot.messages.find(&:assistant?).content!
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LLM::Spell
4
+ ##
5
+ # The {LLM::Spell::Text LLM::Spell::Text} class can analyze a given
6
+ # piece of text and return any spelling mistakes it found &ndash; along
7
+ # with suggested corrections.
8
+ class Text
9
+ include LLM::Spell::Engine
10
+
11
+ ##
12
+ # @param [String] text
13
+ # The contents of the Text
14
+ # @param [LLM::Provider] provider
15
+ # An instance of LLM::Provider
16
+ def initialize(input, llm)
17
+ @input = input
18
+ @llm = llm
19
+ @bot = LLM::Bot.new(llm, schema:)
20
+ end
21
+
22
+ def to_s = input
23
+ def to_str = input
24
+ end
25
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ end unless defined?(LLM)
5
+
6
+ class LLM::Spell
7
+ VERSION = "0.1.0"
8
+ end
data/lib/llm/spell.rb ADDED
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LLM
4
+ end unless defined?(LLM)
5
+
6
+ class LLM::Spell
7
+ require "llm"
8
+ require "yaml"
9
+ require_relative "spell/version"
10
+ require_relative "spell/engine"
11
+ require_relative "spell/document"
12
+ require_relative "spell/text"
13
+ require_relative "spell/cli"
14
+
15
+ ##
16
+ # The superclass of all LLM::Spell errors
17
+ Error = Class.new(RuntimeError)
18
+
19
+ ##
20
+ # @param [Hash] options
21
+ # @return [LLM::Spell]
22
+ def initialize(options)
23
+ @options = options
24
+ @text = Text.new(File.read(@options[:file]), llm)
25
+ end
26
+
27
+ ##
28
+ # Run in interactive mode
29
+ # @return [void]
30
+ def interactive
31
+ if mime_types.include?(LLM::Mime[file])
32
+ File.write file, CLI.new(@text).start
33
+ else
34
+ raise Error, "In interactive mode, the following mime types " \
35
+ "are supported: #{mime_types.join(', ')}"
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def options
42
+ if @options[:key]
43
+ {key: @options[:key]}
44
+ elsif File.readable?(config_file)
45
+ config[provider.to_s].transform_keys(&:to_sym)
46
+ else
47
+ raise Error, "No API key available"
48
+ end
49
+ end
50
+
51
+ def llm = @llm ||= LLM.method(provider).call(**options)
52
+ def provider = @options[:provider]
53
+ def config_dir = ENV["XDG_CONFIG_HOME"] || File.join(Dir.home, ".config")
54
+ def config_file = File.join(config_dir, "llm-spell.yml")
55
+ def config = @config ||= YAML.load_file(config_file)
56
+ def file = @options[:file]
57
+ def mime_types = ["text/plain", "text/markdown"]
58
+ end
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ def main(argv)
5
+ require "optparse"
6
+ require_relative "../../lib/llm/spell"
7
+ if argv.include?("-v") || argv.include?("--version")
8
+ puts LLM::Spell::VERSION
9
+ else
10
+ options = {}
11
+ option_parser.parse(argv, into: options)
12
+ if argv.empty? || options[:file].nil? || options[:provider].nil?
13
+ warn option_parser.help
14
+ throw(:exit, 1)
15
+ else
16
+ LLM::Spell.new(options).interactive
17
+ end
18
+ end
19
+ end
20
+
21
+ def option_parser
22
+ OptionParser.new do |o|
23
+ o.banner = "Usage: llm-spell [OPTIONS]"
24
+ o.on("-p PROVIDER", "--provider NAME", "Required. Options: gemini, openai, xai, ollama or llamacpp.", String)
25
+ o.on("-f FILE", "--file FILE", "Required. The file to check.", String)
26
+ o.on("-k [KEY]", "--key [KEY]", "Optional. Required by gemini, openai, and xai.", String)
27
+ o.on("-v", "--version", "Optional. Print the version and exit.")
28
+ end
29
+ end
30
+
31
+ excode = catch(:exit) do
32
+ main(ARGV)
33
+ 0
34
+ rescue => ex
35
+ warn "#{ex.class}: #{ex.message}"
36
+ warn ex.backtrace[0..5].join("\n")
37
+ 1
38
+ end
39
+ exit excode
data/llm-spell.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/llm/spell/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "llm-spell"
7
+ spec.version = LLM::Spell::VERSION
8
+ spec.authors = ["Antar Azri", "0x1eef"]
9
+ spec.email = ["azantar@proton.me", "0x1eef@proton.me"]
10
+
11
+ spec.summary = "llm-spell is a command-line utility that " \
12
+ "can correct spelling mistakes with the help " \
13
+ "of a Large Language Model (LLM). Compared to " \
14
+ "traditional spell checkers like `aspell` and `hunspell`, " \
15
+ "llm-spell often produces fewer false positives and more " \
16
+ "accurate suggestions."
17
+ spec.description = spec.summary
18
+ spec.homepage = "https://github.com/llmrb/llm-spell"
19
+ spec.license = "0BSD"
20
+ spec.required_ruby_version = ">= 3.2"
21
+
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = spec.homepage
24
+
25
+ spec.files = Dir[
26
+ "README.md", "LICENSE",
27
+ "lib/*.rb", "lib/**/*.rb",
28
+ "libexec/*", "libexec/**/*",
29
+ "bin/*", "llm-spell.gemspec"
30
+ ]
31
+ spec.require_paths = ["lib"]
32
+ spec.executables = ["llm-spell"]
33
+ spec.add_dependency "llm.rb", "~> 0.14"
34
+ spec.add_development_dependency "webmock", "~> 3.24.0"
35
+ spec.add_development_dependency "yard", "~> 0.9.37"
36
+ spec.add_development_dependency "kramdown", "~> 2.4"
37
+ spec.add_development_dependency "webrick", "~> 1.8"
38
+ spec.add_development_dependency "test-cmd.rb", "~> 0.12.0"
39
+ spec.add_development_dependency "rake", "~> 13.0"
40
+ spec.add_development_dependency "rspec", "~> 3.0"
41
+ spec.add_development_dependency "standard", "~> 1.40"
42
+ spec.add_development_dependency "vcr", "~> 6.0"
43
+ spec.add_development_dependency "dotenv", "~> 2.8"
44
+ end
metadata ADDED
@@ -0,0 +1,215 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: llm-spell
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Antar Azri
8
+ - '0x1eef'
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: llm.rb
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.14'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: webmock
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.24.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.24.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: yard
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.9.37
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.9.37
55
+ - !ruby/object:Gem::Dependency
56
+ name: kramdown
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: webrick
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.8'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.8'
83
+ - !ruby/object:Gem::Dependency
84
+ name: test-cmd.rb
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.12.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.12.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '13.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '13.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: standard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.40'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.40'
139
+ - !ruby/object:Gem::Dependency
140
+ name: vcr
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '6.0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '6.0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: dotenv
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '2.8'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '2.8'
167
+ description: llm-spell is a command-line utility that can correct spelling mistakes
168
+ with the help of a Large Language Model (LLM). Compared to traditional spell checkers
169
+ like `aspell` and `hunspell`, llm-spell often produces fewer false positives and
170
+ more accurate suggestions.
171
+ email:
172
+ - azantar@proton.me
173
+ - 0x1eef@proton.me
174
+ executables:
175
+ - llm-spell
176
+ extensions: []
177
+ extra_rdoc_files: []
178
+ files:
179
+ - README.md
180
+ - bin/llm-spell
181
+ - lib/llm/spell.rb
182
+ - lib/llm/spell/cli.rb
183
+ - lib/llm/spell/document.rb
184
+ - lib/llm/spell/engine.rb
185
+ - lib/llm/spell/text.rb
186
+ - lib/llm/spell/version.rb
187
+ - libexec/llm-spell/check
188
+ - llm-spell.gemspec
189
+ homepage: https://github.com/llmrb/llm-spell
190
+ licenses:
191
+ - 0BSD
192
+ metadata:
193
+ homepage_uri: https://github.com/llmrb/llm-spell
194
+ source_code_uri: https://github.com/llmrb/llm-spell
195
+ rdoc_options: []
196
+ require_paths:
197
+ - lib
198
+ required_ruby_version: !ruby/object:Gem::Requirement
199
+ requirements:
200
+ - - ">="
201
+ - !ruby/object:Gem::Version
202
+ version: '3.2'
203
+ required_rubygems_version: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '0'
208
+ requirements: []
209
+ rubygems_version: 3.6.9
210
+ specification_version: 4
211
+ summary: llm-spell is a command-line utility that can correct spelling mistakes with
212
+ the help of a Large Language Model (LLM). Compared to traditional spell checkers
213
+ like `aspell` and `hunspell`, llm-spell often produces fewer false positives and
214
+ more accurate suggestions.
215
+ test_files: []