anki_generator 1.1.0 → 1.4.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +59 -0
  3. data/.rubocop.yml +79 -0
  4. data/.ruby-version +1 -0
  5. data/.tool-versions +1 -0
  6. data/CHANGELOG.md +173 -0
  7. data/Gemfile +21 -0
  8. data/Makefile +20 -0
  9. data/README.md +160 -32
  10. data/Rakefile +174 -0
  11. data/anki_generator.gemspec +42 -0
  12. data/bin/anki_generator +3 -2
  13. data/docs/CI_SETUP.md +120 -0
  14. data/docs/architecture/current-v1.3.0.architecture.json +315 -0
  15. data/docs/architecture/current-v1.3.0.html +14990 -0
  16. data/docs/architecture/current-v1.3.0.visual-check.json +548 -0
  17. data/docs/architecture/phase3-proposed.architecture.json +310 -0
  18. data/docs/architecture/phase3-proposed.html +15001 -0
  19. data/docs/architecture/phase3-proposed.visual-check.json +548 -0
  20. data/docs/phase3-draft.md +86 -0
  21. data/examples/example_class.rb +13 -0
  22. data/examples/manual_cards.yaml +5 -0
  23. data/examples/study_prompt.txt +3 -0
  24. data/input/input.yaml.example +3 -0
  25. data/lib/anki_generator/anki_connect_client.rb +85 -0
  26. data/lib/anki_generator/apkg_schema.rb +257 -0
  27. data/lib/anki_generator/apkg_writer.rb +149 -0
  28. data/lib/anki_generator/card.rb +83 -0
  29. data/lib/anki_generator/cli.rb +183 -0
  30. data/lib/anki_generator/client_factory.rb +20 -0
  31. data/lib/anki_generator/commands/create_ai_template.rb +39 -0
  32. data/lib/anki_generator/commands/generate_deck.rb +43 -0
  33. data/lib/anki_generator/commands/generate_yaml.rb +63 -0
  34. data/lib/anki_generator/commands/import.rb +75 -0
  35. data/lib/anki_generator/commands/prompt_based.rb +74 -0
  36. data/lib/anki_generator/commands/prompt_to_deck.rb +84 -0
  37. data/lib/anki_generator/commands/push.rb +32 -0
  38. data/lib/anki_generator/commands/serve.rb +99 -0
  39. data/lib/anki_generator/commands/test_api.rb +33 -0
  40. data/lib/anki_generator/deck_builder.rb +184 -0
  41. data/lib/anki_generator/errors.rb +22 -0
  42. data/lib/anki_generator/file_processor.rb +154 -0
  43. data/lib/anki_generator/importers/csv.rb +52 -0
  44. data/lib/anki_generator/importers/markdown.rb +72 -0
  45. data/lib/anki_generator/prompt_builder.rb +77 -0
  46. data/lib/anki_generator/server.rb +98 -0
  47. data/lib/anki_generator/ui.rb +28 -0
  48. data/lib/anki_generator/version.rb +5 -0
  49. data/lib/anki_generator.rb +18 -114
  50. data/prompt.txt +5 -0
  51. metadata +100 -43
  52. data/lib/anki_cli.rb +0 -259
  53. data/lib/file_processor.rb +0 -156
  54. data/lib/openrouter_client.rb +0 -158
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'sinatra/base'
5
+ require_relative 'apkg_exporter'
6
+ require_relative 'client_factory'
7
+ require_relative 'errors'
8
+ require_relative 'importers/markdown'
9
+ require_relative 'importers/yaml'
10
+
11
+ module AnkiGenerator
12
+ # Localhost preview-and-export UI for the gem, as a modular Sinatra app.
13
+ # Thin controllers only: parsing delegates to Importers, export to
14
+ # ApkgExporter, generation to ClientFactory. The page lives in
15
+ # server/public/index.html (plain static HTML + vanilla JS).
16
+ class Server < Sinatra::Base
17
+ set :root, File.dirname(__FILE__)
18
+ set :default_provider, nil
19
+ set :protection, except: :json_csrf
20
+
21
+ get '/' do
22
+ send_file File.expand_path('server/public/index.html', __dir__)
23
+ end
24
+
25
+ post '/parse' do
26
+ payload = json_payload!
27
+ format = payload['format'].to_s
28
+
29
+ cards = case format
30
+ when 'markdown'
31
+ Importers::Markdown.parse(payload['content'].to_s)
32
+ when 'yaml'
33
+ Importers::Yaml.parse(payload['content'].to_s)
34
+ else
35
+ raise ValidationError, "Unknown format '#{format}' (markdown or yaml)"
36
+ end
37
+
38
+ json_response(cards:)
39
+ rescue AnkiGenerator::Error => e
40
+ error_json(e.message)
41
+ end
42
+
43
+ post '/generate' do
44
+ payload = json_payload!
45
+ client = ClientFactory.build(
46
+ provider: payload['provider'] || settings.default_provider,
47
+ model: empty_to_nil(payload['model']),
48
+ api_key: empty_to_nil(payload['api_key'])
49
+ )
50
+
51
+ cards = client.generate_multiple_flashcards(
52
+ topics: [payload['prompt'].to_s],
53
+ difficulty: payload['difficulty'].to_s.empty? ? 'medium' : payload['difficulty'],
54
+ count: (payload['count'] || 10).to_i
55
+ )
56
+
57
+ json_response(cards:)
58
+ rescue AnkiGenerator::Error => e
59
+ error_json(e.message)
60
+ end
61
+
62
+ post '/export' do
63
+ payload = json_payload!
64
+ result = ApkgExporter.export(deck_name: payload['deck_name'].to_s, cards: payload['cards'])
65
+
66
+ content_type result[:content_type]
67
+ headers 'Content-Disposition' => %(attachment; filename="#{result[:filename]}")
68
+ result[:bytes]
69
+ rescue AnkiGenerator::Error => e
70
+ error_json(e.message)
71
+ end
72
+
73
+ private
74
+
75
+ # Parses the request body as JSON once. Malformed payloads halt with 400.
76
+ def json_payload!
77
+ body = request.body.read
78
+ JSON.parse(body.empty? ? '{}' : body)
79
+ rescue JSON::ParserError => e
80
+ halt 400, { 'Content-Type' => 'application/json' }, { error: e.message }.to_json
81
+ end
82
+
83
+ def json_response(payload)
84
+ content_type :json
85
+ payload.to_json
86
+ end
87
+
88
+ def error_json(message, status = 422)
89
+ content_type :json
90
+ status status
91
+ { error: message }.to_json
92
+ end
93
+
94
+ def empty_to_nil(value)
95
+ value.nil? || value.to_s.empty? ? nil : value
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiGenerator
4
+ # Presentation boundary for user-facing output. Commands and utilities emit
5
+ # through a UI instance instead of writing to $stdout directly, which keeps
6
+ # the library quiet when embedded and makes output assertions trivial in tests.
7
+ class UI
8
+ def initialize(io = $stdout)
9
+ @io = io
10
+ end
11
+
12
+ def info(message)
13
+ @io.puts(message)
14
+ end
15
+
16
+ def success(message)
17
+ @io.puts("✅ #{message}")
18
+ end
19
+
20
+ def warn(message)
21
+ @io.puts("⚠️ #{message}")
22
+ end
23
+
24
+ def error(message)
25
+ @io.puts("❌ #{message}")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnkiGenerator
4
+ VERSION = '1.4.0'
5
+ end
@@ -1,116 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'yaml'
4
- require 'anki2'
5
- require_relative 'openrouter_client'
6
-
7
- # AnkiGenerator -> Class
8
- class AnkiGenerator
9
- attr_accessor :deck_file, :cards, :name, :openrouter_client
10
-
11
- def initialize(deck_file:, name:, api_key: nil, model: 'openai/gpt-3.5-turbo')
12
- self.deck_file = deck_file
13
- self.name = name
14
- self.cards = []
15
- self.openrouter_client = OpenRouterClient.new(api_key: api_key, model: model) if api_key || ENV['OPENROUTER_API_KEY']
16
-
17
- fill_cards
18
- end
19
-
20
- def fill_cards
21
- yaml_content = YAML.load_file(deck_file)
22
-
23
- # Handle both old format (array of cards) and new format (with ai_generation config)
24
- if yaml_content.is_a?(Hash) && yaml_content['ai_generation']
25
- process_ai_generation(yaml_content)
26
- else
27
- self.cards = yaml_content.is_a?(Array) ? yaml_content : []
28
- end
29
- end
30
-
31
- def process_ai_generation(config, attachments: nil)
32
- ai_config = config['ai_generation']
33
- existing_cards = config['cards'] || []
34
-
35
- # Generate AI cards if specified and client is available
36
- if ai_config['topics'] && openrouter_client
37
- ai_cards = generate_ai_cards(
38
- topics: ai_config['topics'],
39
- context: ai_config['context'],
40
- difficulty: ai_config['difficulty'] || 'medium',
41
- count: ai_config['count'] || 5,
42
- attachments: attachments
43
- )
44
-
45
- self.cards = existing_cards + ai_cards
46
- else
47
- self.cards = existing_cards
48
- end
49
-
50
- # Save the generated cards back to YAML for future reference
51
- save_generated_cards_to_yaml(config) if ai_config['save_generated'] && openrouter_client
52
- end
53
-
54
- def generate_ai_cards(topics:, context: nil, difficulty: 'medium', count: 5, attachments: nil)
55
- if topics.is_a?(Array) && topics.length > 1
56
- openrouter_client.generate_multiple_flashcards(
57
- topics: topics,
58
- context: context,
59
- difficulty: difficulty,
60
- count: count,
61
- attachments: attachments
62
- )
63
- else
64
- topic = topics.is_a?(Array) ? topics.first : topics
65
- [openrouter_client.generate_flashcard(
66
- topic: topic,
67
- context: context,
68
- difficulty: difficulty,
69
- attachments: attachments
70
- )]
71
- end
72
- end
73
-
74
- def save_generated_cards_to_yaml(original_config)
75
- output_file = deck_file.gsub('.yaml', '_generated.yaml')
76
-
77
- updated_config = original_config.dup
78
- updated_config['cards'] = cards
79
- updated_config['ai_generation']['save_generated'] = false # Prevent recursive generation
80
-
81
- File.open(output_file, 'w') do |file|
82
- file.write(updated_config.to_yaml)
83
- end
84
-
85
- puts "Generated cards saved to: #{output_file}"
86
- end
87
-
88
- def generate_apkg(output_path:)
89
- deck = Anki2.new({ name: name, output_path: output_path })
90
-
91
- cards.each do |card|
92
- deck.add_card(card['front'], card['back'])
93
- end
94
-
95
- deck.save
96
- end
97
-
98
- def add_card(front:, back:)
99
- cards << { 'front' => front, 'back' => back }
100
- end
101
-
102
- def sync_with_existing_deck(existing_yaml_file)
103
- return unless File.exist?(existing_yaml_file)
104
-
105
- existing_cards = YAML.load_file(existing_yaml_file)
106
- existing_cards = existing_cards.is_a?(Array) ? existing_cards : existing_cards['cards'] || []
107
-
108
- # Simple deduplication based on front text
109
- existing_fronts = existing_cards.map { |card| card['front'] }
110
- new_cards = cards.reject { |card| existing_fronts.include?(card['front']) }
111
-
112
- self.cards = existing_cards + new_cards
113
-
114
- puts "Synced #{new_cards.length} new cards with existing deck"
115
- end
116
- end
3
+ require_relative 'anki_generator/version'
4
+ require_relative 'anki_generator/errors'
5
+ require_relative 'anki_generator/ui'
6
+ require_relative 'anki_generator/card'
7
+ require_relative 'anki_generator/apkg_schema'
8
+ require_relative 'anki_generator/apkg_writer'
9
+ require_relative 'anki_generator/file_processor'
10
+ require_relative 'anki_generator/prompt_builder'
11
+ require_relative 'anki_generator/card_schema'
12
+ require_relative 'anki_generator/llm_client'
13
+ require_relative 'anki_generator/client_factory'
14
+ require_relative 'anki_generator/anki_connect_client'
15
+ require_relative 'anki_generator/deck_builder'
16
+ require_relative 'anki_generator/apkg_exporter'
17
+ require_relative 'anki_generator/importers/csv'
18
+ require_relative 'anki_generator/importers/markdown'
19
+ require_relative 'anki_generator/importers/yaml'
20
+ require_relative 'anki_generator/server'
data/prompt.txt ADDED
@@ -0,0 +1,5 @@
1
+ front: What is the main contribution of the Pattern-defeating Quicksort (pdqsort) paper? back: It introduces a new solution to the Dutch national flag problem, providing a worst-case runtime of O(nk) for inputs with k distinct elements and a hybrid sorting algorithm combining quicksort, insertion sort, and heapsort.
2
+
3
+ Following that pattern, structure
4
+
5
+ Generate as many anki cards deeply focused in bioinformatics about the pdf sent. Make as many as you want. be exhaustive
metadata CHANGED
@@ -1,45 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anki_generator
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ceb
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-01 00:00:00.000000000 Z
11
+ date: 2026-10-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: anki2
14
+ name: dotenv
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.1.2
19
+ version: '2.8'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.1.2
26
+ version: '2.8'
27
27
  - !ruby/object:Gem::Dependency
28
- name: thor
28
+ name: json
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.2'
33
+ version: '2.0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.2'
40
+ version: '2.0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: faraday
42
+ name: ruby_llm
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
@@ -53,80 +53,93 @@ dependencies:
53
53
  - !ruby/object:Gem::Version
54
54
  version: '2.0'
55
55
  - !ruby/object:Gem::Dependency
56
- name: json
56
+ name: rubyzip
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '2.0'
61
+ version: '2.3'
62
+ - - "<"
63
+ - !ruby/object:Gem::Version
64
+ version: '3.0'
62
65
  type: :runtime
63
66
  prerelease: false
64
67
  version_requirements: !ruby/object:Gem::Requirement
65
68
  requirements:
66
- - - "~>"
69
+ - - ">="
67
70
  - !ruby/object:Gem::Version
68
- version: '2.0'
71
+ version: '2.3'
72
+ - - "<"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.0'
69
75
  - !ruby/object:Gem::Dependency
70
- name: dotenv
76
+ name: sinatra
71
77
  requirement: !ruby/object:Gem::Requirement
72
78
  requirements:
73
79
  - - "~>"
74
80
  - !ruby/object:Gem::Version
75
- version: '2.8'
81
+ version: '4.0'
76
82
  type: :runtime
77
83
  prerelease: false
78
84
  version_requirements: !ruby/object:Gem::Requirement
79
85
  requirements:
80
86
  - - "~>"
81
87
  - !ruby/object:Gem::Version
82
- version: '2.8'
88
+ version: '4.0'
83
89
  - !ruby/object:Gem::Dependency
84
- name: minitest
90
+ name: sqlite3
85
91
  requirement: !ruby/object:Gem::Requirement
86
92
  requirements:
87
- - - "~>"
93
+ - - ">="
88
94
  - !ruby/object:Gem::Version
89
- version: '5.25'
90
- type: :development
95
+ version: '1.6'
96
+ - - "<"
97
+ - !ruby/object:Gem::Version
98
+ version: '3.0'
99
+ type: :runtime
91
100
  prerelease: false
92
101
  version_requirements: !ruby/object:Gem::Requirement
93
102
  requirements:
94
- - - "~>"
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '1.6'
106
+ - - "<"
95
107
  - !ruby/object:Gem::Version
96
- version: '5.25'
108
+ version: '3.0'
97
109
  - !ruby/object:Gem::Dependency
98
- name: rake
110
+ name: thor
99
111
  requirement: !ruby/object:Gem::Requirement
100
112
  requirements:
101
113
  - - "~>"
102
114
  - !ruby/object:Gem::Version
103
- version: '13.0'
104
- type: :development
115
+ version: '1.2'
116
+ type: :runtime
105
117
  prerelease: false
106
118
  version_requirements: !ruby/object:Gem::Requirement
107
119
  requirements:
108
120
  - - "~>"
109
121
  - !ruby/object:Gem::Version
110
- version: '13.0'
122
+ version: '1.2'
111
123
  - !ruby/object:Gem::Dependency
112
- name: rubocop
124
+ name: webrick
113
125
  requirement: !ruby/object:Gem::Requirement
114
126
  requirements:
115
- - - "~>"
127
+ - - ">="
116
128
  - !ruby/object:Gem::Version
117
- version: '1.0'
118
- type: :development
129
+ version: '1.8'
130
+ type: :runtime
119
131
  prerelease: false
120
132
  version_requirements: !ruby/object:Gem::Requirement
121
133
  requirements:
122
- - - "~>"
134
+ - - ">="
123
135
  - !ruby/object:Gem::Version
124
- version: '1.0'
125
- description: A powerful command-line tool that generates Anki flashcard decks (.apkg)
126
- from YAML files, direct prompts, or file attachments. Features AI-powered content
127
- generation using OpenRouter API with support for multiple models (GPT, Claude, Llama),
128
- file and directory attachment processing for context-aware generation, prompt file
129
- support, intelligent content filtering, and flexible deck management with sync capabilities.
136
+ version: '1.8'
137
+ description: A command-line tool that generates Anki flashcard decks (.apkg) from
138
+ YAML files, direct prompts, or file attachments. Features AI-powered content generation
139
+ via the provider-agnostic ruby_llm gem (Gemini, OpenAI, Anthropic, OpenRouter, Ollama,
140
+ and more), file and directory attachment processing for context-aware generation,
141
+ prompt file support, intelligent content filtering, and flexible deck management
142
+ with sync capabilities.
130
143
  email:
131
144
  - ceeb.developer@gmail.com
132
145
  executables:
@@ -134,12 +147,55 @@ executables:
134
147
  extensions: []
135
148
  extra_rdoc_files: []
136
149
  files:
150
+ - ".gitignore"
151
+ - ".rubocop.yml"
152
+ - ".ruby-version"
153
+ - ".tool-versions"
154
+ - CHANGELOG.md
155
+ - Gemfile
156
+ - Makefile
137
157
  - README.md
158
+ - Rakefile
159
+ - anki_generator.gemspec
138
160
  - bin/anki_generator
139
- - lib/anki_cli.rb
161
+ - docs/CI_SETUP.md
162
+ - docs/architecture/current-v1.3.0.architecture.json
163
+ - docs/architecture/current-v1.3.0.html
164
+ - docs/architecture/current-v1.3.0.visual-check.json
165
+ - docs/architecture/phase3-proposed.architecture.json
166
+ - docs/architecture/phase3-proposed.html
167
+ - docs/architecture/phase3-proposed.visual-check.json
168
+ - docs/phase3-draft.md
169
+ - examples/example_class.rb
170
+ - examples/manual_cards.yaml
171
+ - examples/study_prompt.txt
172
+ - input/input.yaml.example
140
173
  - lib/anki_generator.rb
141
- - lib/file_processor.rb
142
- - lib/openrouter_client.rb
174
+ - lib/anki_generator/anki_connect_client.rb
175
+ - lib/anki_generator/apkg_schema.rb
176
+ - lib/anki_generator/apkg_writer.rb
177
+ - lib/anki_generator/card.rb
178
+ - lib/anki_generator/cli.rb
179
+ - lib/anki_generator/client_factory.rb
180
+ - lib/anki_generator/commands/create_ai_template.rb
181
+ - lib/anki_generator/commands/generate_deck.rb
182
+ - lib/anki_generator/commands/generate_yaml.rb
183
+ - lib/anki_generator/commands/import.rb
184
+ - lib/anki_generator/commands/prompt_based.rb
185
+ - lib/anki_generator/commands/prompt_to_deck.rb
186
+ - lib/anki_generator/commands/push.rb
187
+ - lib/anki_generator/commands/serve.rb
188
+ - lib/anki_generator/commands/test_api.rb
189
+ - lib/anki_generator/deck_builder.rb
190
+ - lib/anki_generator/errors.rb
191
+ - lib/anki_generator/file_processor.rb
192
+ - lib/anki_generator/importers/csv.rb
193
+ - lib/anki_generator/importers/markdown.rb
194
+ - lib/anki_generator/prompt_builder.rb
195
+ - lib/anki_generator/server.rb
196
+ - lib/anki_generator/ui.rb
197
+ - lib/anki_generator/version.rb
198
+ - prompt.txt
143
199
  homepage: https://github.com/pinkfloydsito/anki_generator
144
200
  licenses:
145
201
  - MIT
@@ -149,6 +205,7 @@ metadata:
149
205
  changelog_uri: https://github.com/pinkfloydsito/anki_generator/blob/main/CHANGELOG.md
150
206
  bug_tracker_uri: https://github.com/pinkfloydsito/anki_generator/issues
151
207
  documentation_uri: https://github.com/pinkfloydsito/anki_generator/blob/main/README.md
208
+ rubygems_mfa_required: 'true'
152
209
  post_install_message:
153
210
  rdoc_options: []
154
211
  require_paths:
@@ -157,14 +214,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
157
214
  requirements:
158
215
  - - ">="
159
216
  - !ruby/object:Gem::Version
160
- version: 2.7.0
217
+ version: 3.1.0
161
218
  required_rubygems_version: !ruby/object:Gem::Requirement
162
219
  requirements:
163
220
  - - ">="
164
221
  - !ruby/object:Gem::Version
165
222
  version: '0'
166
223
  requirements: []
167
- rubygems_version: 3.3.7
224
+ rubygems_version: 3.5.22
168
225
  signing_key:
169
226
  specification_version: 4
170
227
  summary: AI-powered Anki flashcard generator with file attachment support