mathpix 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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE +21 -0
  4. data/README.md +171 -0
  5. data/SECURITY.md +137 -0
  6. data/lib/mathpix/balanced_ternary.rb +86 -0
  7. data/lib/mathpix/batch.rb +155 -0
  8. data/lib/mathpix/capture_builder.rb +142 -0
  9. data/lib/mathpix/chemistry.rb +69 -0
  10. data/lib/mathpix/client.rb +439 -0
  11. data/lib/mathpix/configuration.rb +187 -0
  12. data/lib/mathpix/configuration.rb.backup +125 -0
  13. data/lib/mathpix/conversion.rb +257 -0
  14. data/lib/mathpix/document.rb +320 -0
  15. data/lib/mathpix/errors.rb +78 -0
  16. data/lib/mathpix/mcp/auth/oauth_provider.rb +346 -0
  17. data/lib/mathpix/mcp/auth/token_manager.rb +31 -0
  18. data/lib/mathpix/mcp/auth.rb +18 -0
  19. data/lib/mathpix/mcp/base_tool.rb +117 -0
  20. data/lib/mathpix/mcp/elicitations/ambiguity_elicitation.rb +162 -0
  21. data/lib/mathpix/mcp/elicitations/base_elicitation.rb +141 -0
  22. data/lib/mathpix/mcp/elicitations/confidence_elicitation.rb +162 -0
  23. data/lib/mathpix/mcp/elicitations.rb +78 -0
  24. data/lib/mathpix/mcp/middleware/cors_middleware.rb +94 -0
  25. data/lib/mathpix/mcp/middleware/oauth_middleware.rb +72 -0
  26. data/lib/mathpix/mcp/middleware/rate_limiting_middleware.rb +140 -0
  27. data/lib/mathpix/mcp/middleware.rb +13 -0
  28. data/lib/mathpix/mcp/resources/formats_list_resource.rb +113 -0
  29. data/lib/mathpix/mcp/resources/hierarchical_router.rb +237 -0
  30. data/lib/mathpix/mcp/resources/latest_snip_resource.rb +60 -0
  31. data/lib/mathpix/mcp/resources/recent_snips_resource.rb +75 -0
  32. data/lib/mathpix/mcp/resources/snip_stats_resource.rb +78 -0
  33. data/lib/mathpix/mcp/resources.rb +15 -0
  34. data/lib/mathpix/mcp/server.rb +174 -0
  35. data/lib/mathpix/mcp/tools/batch_convert_tool.rb +106 -0
  36. data/lib/mathpix/mcp/tools/check_document_status_tool.rb +66 -0
  37. data/lib/mathpix/mcp/tools/convert_document_tool.rb +90 -0
  38. data/lib/mathpix/mcp/tools/convert_image_tool.rb +91 -0
  39. data/lib/mathpix/mcp/tools/convert_strokes_tool.rb +82 -0
  40. data/lib/mathpix/mcp/tools/get_account_info_tool.rb +57 -0
  41. data/lib/mathpix/mcp/tools/get_usage_tool.rb +62 -0
  42. data/lib/mathpix/mcp/tools/list_formats_tool.rb +81 -0
  43. data/lib/mathpix/mcp/tools/search_results_tool.rb +111 -0
  44. data/lib/mathpix/mcp/transports/http_streaming_transport.rb +622 -0
  45. data/lib/mathpix/mcp/transports/sse_stream_handler.rb +236 -0
  46. data/lib/mathpix/mcp/transports.rb +12 -0
  47. data/lib/mathpix/mcp.rb +52 -0
  48. data/lib/mathpix/result.rb +364 -0
  49. data/lib/mathpix/version.rb +22 -0
  50. data/lib/mathpix.rb +229 -0
  51. metadata +283 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 11a43b1643362801778dbc02b4ec45c3e4cc5bda4dc3b20c191a699c22a67c9d
4
+ data.tar.gz: 80b902ab983d1a3c1d3a8483646c861edfb77d5ad8b573a66b60d7a0b82cea33
5
+ SHA512:
6
+ metadata.gz: 5b7aa48fd8fd1c983412221f9cf4c06df4a4bb52bd2c217da9125c39f8ebec27755b830457ce6469ee80bba102935987bd686d53a9bfab92ccae30e9264b67d9
7
+ data.tar.gz: 74aa5937731cb5661d7fecd8354617e6548ddb7d09dd951b2d5559ac9eb9005f620cd12f76c7fe883c77d7f06351f4146c9b5a3128474d715e67c4e3c4a12346
data/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2025-10-13
9
+
10
+ ### Added
11
+ - Initial release of Mathpix Ruby client
12
+ - Core API client with fluent builder pattern
13
+ - Image OCR support via `/v3/text` endpoint
14
+ - PDF conversion support via `/v3/pdf` endpoint (async)
15
+ - Batch processing capabilities with parallel execution
16
+ - Comprehensive BDD test suite (15+ feature files)
17
+ - Security-first design:
18
+ - HTTPS enforcement
19
+ - Path traversal protection
20
+ - File size limits (10MB default)
21
+ - Private IP blocking
22
+ - Configuration management with validation
23
+ - Multiple output formats:
24
+ - LaTeX (styled and standard)
25
+ - MathML
26
+ - AsciiMath
27
+ - Markdown (MMD)
28
+ - Text
29
+ - SMILES (chemistry)
30
+ - Balanced ternary encoding utilities (seed 1069)
31
+ - Result value objects with domain-specific accessors
32
+ - Comprehensive error handling hierarchy
33
+ - MCP (Model Context Protocol) server integration
34
+ - YARD documentation inline
35
+
36
+ ### Dependencies
37
+ - Ruby >= 2.7.0
38
+ - faraday ~> 2.0 (HTTP client)
39
+ - concurrent-ruby ~> 1.2 (parallelism)
40
+ - mcp ~> 0.1.0 (MCP protocol)
41
+ - rack ~> 3.0 (MCP server)
42
+ - puma ~> 6.0 (MCP server)
43
+
44
+ ### Development
45
+ - cucumber ~> 9.0 (BDD testing)
46
+ - rspec ~> 3.12 (unit testing)
47
+ - webmock ~> 3.18 (HTTP mocking)
48
+ - vcr ~> 6.1 (HTTP recording)
49
+ - rubocop ~> 1.50 (linting)
50
+ - bundler-audit ~> 0.9 (security scanning)
51
+
52
+ [0.1.0]: https://github.com/teglonlabs/mathpix-mcp-server/releases/tag/v0.1.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Barton Rhodes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # Mathpix Ruby Client
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/mathpix.svg)](https://badge.fury.io/rb/mathpix)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ Transform mathematical images to LaTeX, chemistry structures to SMILES, and documents to markdown with security-first design. The geodesic path to mathematical OCR in Ruby.
7
+
8
+ ## Features
9
+
10
+ - 🔒 **Security First**: HTTPS enforcement, path traversal protection, file size limits
11
+ - 🎯 **Fluent API**: Builder pattern for elegant, chainable operations
12
+ - ⚡ **Batch Processing**: Parallel execution with callback hooks
13
+ - 📊 **Multiple Formats**: LaTeX, MathML, AsciiMath, Markdown, SMILES
14
+ - 🧪 **BDD Tested**: 15+ Cucumber feature files with comprehensive coverage
15
+ - 🔌 **MCP Integration**: Full Model Context Protocol server support
16
+ - 🎲 **Balanced Ternary**: Seed 1069 encoding utilities
17
+
18
+ ## Installation
19
+
20
+ Add to your Gemfile:
21
+
22
+ ```ruby
23
+ gem 'mathpix'
24
+ ```
25
+
26
+ Or install directly:
27
+
28
+ ```bash
29
+ gem install mathpix
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ### Configuration
35
+
36
+ ```ruby
37
+ require 'mathpix'
38
+
39
+ Mathpix.configure do |config|
40
+ config.app_id = ENV['MATHPIX_APP_ID']
41
+ config.app_key = ENV['MATHPIX_APP_KEY']
42
+ end
43
+ ```
44
+
45
+ Or use `.env` file:
46
+
47
+ ```env
48
+ MATHPIX_APP_ID=your_app_id
49
+ MATHPIX_APP_KEY=your_app_key
50
+ ```
51
+
52
+ ### Basic Usage
53
+
54
+ #### Image to LaTeX
55
+
56
+ ```ruby
57
+ # Single image conversion
58
+ result = Mathpix.from('equation.png')
59
+ .with_formats(:latex_styled, :text)
60
+ .capture
61
+
62
+ puts result.latex # => "3x^{2} + 5x - 7 = 0"
63
+ puts result.confidence # => 0.99
64
+ ```
65
+
66
+ #### URL to LaTeX
67
+
68
+ ```ruby
69
+ # Convert from URL
70
+ result = Mathpix.from_url('https://example.com/math.png')
71
+ .with_formats(:latex_styled, :mathml)
72
+ .capture
73
+
74
+ puts result.latex
75
+ puts result.mathml
76
+ ```
77
+
78
+ #### Chemistry (SMILES)
79
+
80
+ ```ruby
81
+ # Chemistry structure recognition
82
+ result = Mathpix.from('molecule.png')
83
+ .with_formats(:smiles)
84
+ .capture
85
+
86
+ puts result.smiles # => "CC(C)CCO"
87
+ ```
88
+
89
+ ### PDF Conversion
90
+
91
+ ```ruby
92
+ # Async PDF to Markdown
93
+ pdf_job = Mathpix.pdf('research_paper.pdf')
94
+ .to_markdown
95
+ .async
96
+ .start
97
+
98
+ # Poll for completion
99
+ until pdf_job.complete?
100
+ sleep 2
101
+ pdf_job.refresh
102
+ end
103
+
104
+ puts pdf_job.markdown
105
+ ```
106
+
107
+ ### Batch Processing
108
+
109
+ ```ruby
110
+ images = Dir['equations/*.png']
111
+
112
+ results = Mathpix.batch(images) do |batch|
113
+ batch.formats :latex_styled, :text
114
+ batch.confidence_threshold 0.85
115
+ batch.max_threads 5
116
+
117
+ batch.on_each do |result|
118
+ puts "✓ #{result.latex}"
119
+ end
120
+
121
+ batch.on_error do |error, path|
122
+ puts "✗ Failed: #{path} - #{error.message}"
123
+ end
124
+ end.process
125
+
126
+ puts "Success rate: #{results.success_rate}"
127
+ puts "High confidence: #{results.confident(0.9).count}"
128
+ ```
129
+
130
+ ## Error Handling
131
+
132
+ ```ruby
133
+ begin
134
+ result = Mathpix.from('image.png').capture
135
+ rescue Mathpix::AuthenticationError => e
136
+ puts "Invalid credentials: #{e.message}"
137
+ rescue Mathpix::ValidationError => e
138
+ puts "Invalid input: #{e.message}"
139
+ rescue Mathpix::RateLimitError => e
140
+ puts "Rate limit exceeded. Retry after: #{e.retry_after}"
141
+ rescue Mathpix::SecurityError => e
142
+ puts "Security violation: #{e.message}"
143
+ rescue Mathpix::APIError => e
144
+ puts "API error: #{e.message}"
145
+ end
146
+ ```
147
+
148
+ ## Documentation
149
+
150
+ - [API Reference](https://docs.mathpix.com)
151
+ - [Changelog](CHANGELOG.md)
152
+ - [Security Policy](SECURITY.md)
153
+
154
+ ## License
155
+
156
+ MIT License - see [LICENSE](LICENSE) for details.
157
+
158
+ ## Credits
159
+
160
+ - Developed by [TegLon Labs](https://github.com/teglonlabs)
161
+ - Mathpix API by [Mathpix](https://mathpix.com)
162
+ - Balanced ternary seed: 1069
163
+
164
+ ## Support
165
+
166
+ - GitHub Issues: https://github.com/teglonlabs/mathpix-mcp-server/issues
167
+ - Email: ies@prototypesf.org
168
+
169
+ ---
170
+
171
+ **The geodesic path to mathematical OCR in Ruby** ∎
data/SECURITY.md ADDED
@@ -0,0 +1,137 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------ |
7
+ | 0.1.x | :white_check_mark: |
8
+
9
+ ## Reporting a Vulnerability
10
+
11
+ We take security vulnerabilities seriously. If you discover a security issue, please report it by emailing **ies@prototypesf.org** with the subject line "Mathpix Gem Security Vulnerability".
12
+
13
+ ### What to Include
14
+
15
+ Please include the following information in your report:
16
+
17
+ - Description of the vulnerability
18
+ - Steps to reproduce the issue
19
+ - Potential impact
20
+ - Suggested fix (if available)
21
+
22
+ ### Response Timeline
23
+
24
+ - **Initial Response**: Within 48 hours of report
25
+ - **Status Update**: Within 7 days with assessment
26
+ - **Fix Timeline**: Critical issues within 14 days, others within 30 days
27
+ - **Disclosure**: Coordinated disclosure after fix is available
28
+
29
+ ## Security Features
30
+
31
+ This gem implements several security measures:
32
+
33
+ ### HTTPS Enforcement
34
+ - All API requests forced to HTTPS
35
+ - HTTP URLs automatically upgraded
36
+ - Private IP addresses blocked
37
+
38
+ ### Path Traversal Protection
39
+ - File paths expanded and validated
40
+ - Maximum path length enforced (4096 characters)
41
+ - Directory traversal attempts rejected
42
+
43
+ ### File Size Limits
44
+ - Default maximum: 10MB per file
45
+ - Configurable via `Mathpix::Configuration`
46
+ - Prevents resource exhaustion attacks
47
+
48
+ ### Rate Limiting
49
+ - Default: 60 requests per minute
50
+ - Burst capacity: 10 requests
51
+ - Configurable per-client
52
+
53
+ ### Input Validation
54
+ - URL format validation
55
+ - File type verification
56
+ - Confidence threshold validation
57
+
58
+ ## Known Security Considerations
59
+
60
+ ### API Credentials
61
+ - Store credentials in environment variables or secure vaults
62
+ - Never commit credentials to version control
63
+ - Use `.env` files with `.gitignore` protection
64
+
65
+ ### File Operations
66
+ - Be cautious with user-supplied file paths
67
+ - Validate file types before processing
68
+ - Monitor disk space usage for large batches
69
+
70
+ ### Network Security
71
+ - API requests go to `api.mathpix.com` (official endpoint)
72
+ - TLS 1.2+ required for all connections
73
+ - Certificate validation enforced
74
+
75
+ ## Security Best Practices
76
+
77
+ ### Configuration
78
+ ```ruby
79
+ Mathpix.configure do |config|
80
+ config.app_id = ENV['MATHPIX_APP_ID']
81
+ config.app_key = ENV['MATHPIX_APP_KEY']
82
+ config.max_file_size_mb = 5 # Restrict file size
83
+ config.https_only = true # Enforce HTTPS
84
+ end
85
+ ```
86
+
87
+ ### Batch Processing
88
+ ```ruby
89
+ # Limit concurrent threads
90
+ Mathpix.batch(files, max_threads: 3) do |batch|
91
+ batch.on_error { |err, path| log_security_event(err, path) }
92
+ end
93
+ ```
94
+
95
+ ### Error Handling
96
+ ```ruby
97
+ begin
98
+ result = Mathpix.from(user_path).capture
99
+ rescue Mathpix::SecurityError => e
100
+ # Handle security violations
101
+ logger.warn("Security violation: #{e.message}")
102
+ rescue Mathpix::ValidationError => e
103
+ # Handle validation failures
104
+ logger.error("Validation failed: #{e.message}")
105
+ end
106
+ ```
107
+
108
+ ## Dependency Security
109
+
110
+ We use the following tools to maintain dependency security:
111
+
112
+ - **bundler-audit**: Scans for known vulnerabilities
113
+ - **brakeman**: Static analysis for security issues
114
+ - **Dependabot**: Automated dependency updates
115
+
116
+ Run security scan:
117
+ ```bash
118
+ bundle audit check --update
119
+ ```
120
+
121
+ ## MFA Requirement
122
+
123
+ This gem requires Multi-Factor Authentication (MFA) for RubyGems publishing as indicated in the gemspec:
124
+
125
+ ```ruby
126
+ spec.metadata['rubygems_mfa_required'] = 'true'
127
+ ```
128
+
129
+ ## Contact
130
+
131
+ For security-related questions or concerns:
132
+ - Email: ies@prototypesf.org
133
+ - GitHub Issues: https://github.com/teglonlabs/mathpix-mcp-server/issues (for non-sensitive issues)
134
+
135
+ ## Acknowledgments
136
+
137
+ We appreciate responsible disclosure and will acknowledge security researchers who report vulnerabilities (with permission).
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mathpix
4
+ # Balanced Ternary support for Seed 1069
5
+ # The geodesic path: mathematical precision, test determinism
6
+ module BalancedTernary
7
+ # Seed 1069 balanced ternary pattern
8
+ # Mathematical verification:
9
+ # 1×3⁶ - 1×3⁵ - 1×3⁴ + 1×3³ + 1×3² + 1×3¹ + 1×3⁰
10
+ # = 729 - 243 - 81 + 27 + 9 + 3 + 1
11
+ # = 1069 ✓
12
+ SEED_1069_PATTERN = [+1, -1, -1, +1, +1, +1, +1].freeze
13
+
14
+ class << self
15
+ # Get the pattern for seed 1069
16
+ # @return [Array<Integer>]
17
+ def pattern
18
+ SEED_1069_PATTERN
19
+ end
20
+
21
+ # Generate confidence sequence following balanced ternary pattern
22
+ #
23
+ # @param base [Float] base confidence value
24
+ # @param delta [Float] variation per trit
25
+ # @return [Array<Float>] confidence values
26
+ # @example
27
+ # Mathpix::BalancedTernary.confidence_sequence(base: 0.92, delta: 0.02)
28
+ # # => [0.94, 0.90, 0.90, 0.94, 0.94, 0.94, 0.94]
29
+ def confidence_sequence(base: 0.92, delta: 0.02)
30
+ pattern.map { |trit| base + (trit * delta) }
31
+ end
32
+
33
+ # Generate values with balanced ternary pattern
34
+ #
35
+ # @param count [Integer] number of values to generate
36
+ # @param seed [Integer] random seed
37
+ # @yield [trit, index] block receives trit value and index
38
+ # @return [Array]
39
+ # @example
40
+ # Mathpix::BalancedTernary.generate_with_pattern(10, seed: 1069) do |trit, i|
41
+ # puts "Position #{i}: trit=#{trit}"
42
+ # generate_test_data(trit)
43
+ # end
44
+ def generate_with_pattern(count, seed: 1069)
45
+ srand(seed)
46
+ Array.new(count) do |i|
47
+ trit = pattern[i % pattern.length]
48
+ yield(trit, i) if block_given?
49
+ end
50
+ end
51
+
52
+ # Convert balanced ternary to decimal
53
+ #
54
+ # @param trits [Array<Integer>] balanced ternary digits
55
+ # @return [Integer] decimal value
56
+ # @example
57
+ # Mathpix::BalancedTernary.to_decimal([+1, -1, -1, +1, +1, +1, +1])
58
+ # # => 1069
59
+ def to_decimal(trits)
60
+ trits.reverse.each_with_index.sum { |trit, i| trit * (3**i) }
61
+ end
62
+
63
+ # Verify seed 1069 pattern
64
+ #
65
+ # @return [Boolean] true if pattern converts to 1069
66
+ def verify_seed_1069
67
+ to_decimal(pattern) == 1069
68
+ end
69
+
70
+ # Pattern interpretation
71
+ #
72
+ # @return [Hash] semantic meaning of pattern
73
+ def pattern_semantics
74
+ {
75
+ position_0: { trit: +1, meaning: 'Positive start (high confidence)' },
76
+ position_1: { trit: -1, meaning: 'Descent begins (complexity)' },
77
+ position_2: { trit: -1, meaning: 'Continued descent (adjustment)' },
78
+ position_3: { trit: +1, meaning: 'Recovery point (stabilization)' },
79
+ position_4: { trit: +1, meaning: 'Convergence (mastery)' },
80
+ position_5: { trit: +1, meaning: 'Stable convergence' },
81
+ position_6: { trit: +1, meaning: 'Final stability' }
82
+ }
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mathpix
4
+ # Batch processing builder
5
+ # The geodesic path: efficient, parallel, callback-driven
6
+ class Batch
7
+ attr_reader :client, :image_paths, :options, :callbacks
8
+
9
+ def initialize(client, image_paths)
10
+ @client = client
11
+ @image_paths = Array(image_paths)
12
+ @options = {}
13
+ @callbacks = {}
14
+ end
15
+
16
+ # Set output formats
17
+ # @param formats [Array<Symbol>] format names
18
+ # @return [self]
19
+ def formats(*formats)
20
+ @options[:formats] = formats.flatten
21
+ self
22
+ end
23
+
24
+ # Set confidence threshold
25
+ # @param threshold [Float] minimum confidence
26
+ # @return [self]
27
+ def confidence_threshold(threshold)
28
+ @options[:confidence_threshold] = threshold
29
+ self
30
+ end
31
+
32
+ # Enable chemistry mode
33
+ # @return [self]
34
+ def chemistry_mode
35
+ @options[:chemistry] = true
36
+ @options[:include_smiles] = true
37
+ self
38
+ end
39
+
40
+ # Set preprocessing options
41
+ # @param options [Hash] preprocessing settings
42
+ # @return [self]
43
+ def preprocessing(**options)
44
+ @options[:preprocessing] = options
45
+ self
46
+ end
47
+
48
+ # Process asynchronously
49
+ # @return [self]
50
+ def async
51
+ @options[:async] = true
52
+ self
53
+ end
54
+
55
+ # Callback for each result
56
+ # @yield [Result] individual result
57
+ # @return [self]
58
+ def on_each(&block)
59
+ @callbacks[:each] = block
60
+ self
61
+ end
62
+
63
+ # Callback on completion
64
+ # @yield [Array<Result>] all results
65
+ # @return [self]
66
+ def on_complete(&block)
67
+ @callbacks[:complete] = block
68
+ self
69
+ end
70
+
71
+ # Callback on error
72
+ # @yield [Error, String] error and image path
73
+ # @return [self]
74
+ def on_error(&block)
75
+ @callbacks[:error] = block
76
+ self
77
+ end
78
+
79
+ # Execute batch processing
80
+ # @return [BatchResult]
81
+ def process
82
+ results = []
83
+ errors = []
84
+
85
+ image_paths.each do |path|
86
+ begin
87
+ result = client.snap(path, **options)
88
+ results << result
89
+ callbacks[:each]&.call(result)
90
+ rescue StandardError => e
91
+ errors << { path: path, error: e }
92
+ callbacks[:error]&.call(e, path)
93
+ end
94
+ end
95
+
96
+ batch_result = BatchResult.new(results, errors)
97
+ callbacks[:complete]&.call(batch_result)
98
+
99
+ batch_result
100
+ end
101
+
102
+ alias call process
103
+ alias run process
104
+ end
105
+
106
+ # Batch Result container
107
+ class BatchResult
108
+ attr_reader :results, :errors
109
+
110
+ def initialize(results, errors = [])
111
+ @results = results
112
+ @errors = errors
113
+ end
114
+
115
+ def total
116
+ results.length + errors.length
117
+ end
118
+
119
+ def successful
120
+ results.length
121
+ end
122
+
123
+ def failed
124
+ errors.length
125
+ end
126
+
127
+ def success_rate
128
+ return 1.0 if total.zero?
129
+ successful.to_f / total
130
+ end
131
+
132
+ # Iterate over successful results
133
+ # @yield [Result]
134
+ def each(&block)
135
+ results.each(&block)
136
+ end
137
+
138
+ # Get all LaTeX
139
+ # @return [Array<String>]
140
+ def all_latex
141
+ results.map(&:latex).compact
142
+ end
143
+
144
+ # Get all with confidence above threshold
145
+ # @param threshold [Float]
146
+ # @return [Array<Result>]
147
+ def confident(threshold = 0.90)
148
+ results.select { |r| r.confidence >= threshold }
149
+ end
150
+
151
+ def inspect
152
+ "#<Mathpix::BatchResult total=#{total} successful=#{successful} failed=#{failed}>"
153
+ end
154
+ end
155
+ end