mistral_translator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +493 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +205 -0
- data/Rakefile +12 -0
- data/examples/basic_usage.rb +135 -0
- data/lib/mistral_translator/client.rb +147 -0
- data/lib/mistral_translator/configuration.rb +39 -0
- data/lib/mistral_translator/errors.rb +53 -0
- data/lib/mistral_translator/locale_helper.rb +99 -0
- data/lib/mistral_translator/logger.rb +55 -0
- data/lib/mistral_translator/prompt_builder.rb +219 -0
- data/lib/mistral_translator/response_parser.rb +125 -0
- data/lib/mistral_translator/summarizer.rb +226 -0
- data/lib/mistral_translator/translator.rb +177 -0
- data/lib/mistral_translator/version.rb +20 -0
- data/lib/mistral_translator.rb +138 -0
- data/sig/mistral_translator.rbs +4 -0
- metadata +150 -0
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "logger"
|
4
|
+
|
5
|
+
module MistralTranslator
|
6
|
+
class Translator
|
7
|
+
DEFAULT_RETRY_COUNT = 3
|
8
|
+
DEFAULT_RETRY_DELAY = 2
|
9
|
+
|
10
|
+
def initialize(client: nil)
|
11
|
+
@client = client || Client.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Traduction simple d'un texte vers une langue
|
15
|
+
def translate(text, from:, to:)
|
16
|
+
validate_inputs!(text, from, to)
|
17
|
+
|
18
|
+
source_locale = LocaleHelper.validate_locale!(from)
|
19
|
+
target_locale = LocaleHelper.validate_locale!(to)
|
20
|
+
|
21
|
+
translate_with_retry(text, source_locale, target_locale)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Traduction vers plusieurs langues
|
25
|
+
def translate_to_multiple(text, from:, to:)
|
26
|
+
validate_translation_inputs!(text, from, to)
|
27
|
+
|
28
|
+
source_locale = LocaleHelper.validate_locale!(from)
|
29
|
+
target_locales = Array(to).map { |locale| LocaleHelper.validate_locale!(locale) }
|
30
|
+
|
31
|
+
results = {}
|
32
|
+
|
33
|
+
target_locales.each_with_index do |target_locale, index|
|
34
|
+
# Délai entre les requêtes, mais pas avant la première
|
35
|
+
sleep(DEFAULT_RETRY_DELAY) if index.positive?
|
36
|
+
results[target_locale] = translate_with_retry(text, source_locale, target_locale)
|
37
|
+
end
|
38
|
+
|
39
|
+
results
|
40
|
+
end
|
41
|
+
|
42
|
+
# Traduction en lot (plusieurs textes vers une langue)
|
43
|
+
def translate_batch(texts, from:, to:)
|
44
|
+
validate_batch_inputs!(texts, from, to)
|
45
|
+
|
46
|
+
source_locale = LocaleHelper.validate_locale!(from)
|
47
|
+
target_locale = LocaleHelper.validate_locale!(to)
|
48
|
+
|
49
|
+
# Pour des lots importants, on peut les découper
|
50
|
+
if texts.length > 10
|
51
|
+
translate_large_batch(texts, source_locale, target_locale)
|
52
|
+
else
|
53
|
+
translate_small_batch(texts, source_locale, target_locale)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Auto-détection de la langue source (utilise l'API pour détecter)
|
58
|
+
def translate_auto(text, to:)
|
59
|
+
target_locale = LocaleHelper.validate_locale!(to)
|
60
|
+
|
61
|
+
# Premier appel pour détecter la langue
|
62
|
+
detection_prompt = build_language_detection_prompt(text)
|
63
|
+
detection_response = @client.complete(detection_prompt)
|
64
|
+
detected_language = parse_language_detection(detection_response)
|
65
|
+
|
66
|
+
# Puis traduction normale
|
67
|
+
translate(text, from: detected_language, to: target_locale)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def translate_with_retry(text, source_locale, target_locale, attempt = 0)
|
73
|
+
prompt = PromptBuilder.translation_prompt(text, source_locale, target_locale)
|
74
|
+
raw_response = @client.complete(prompt)
|
75
|
+
|
76
|
+
result = ResponseParser.parse_translation_response(raw_response)
|
77
|
+
raise EmptyTranslationError if result.nil? || result[:translated].nil?
|
78
|
+
|
79
|
+
result[:translated]
|
80
|
+
rescue EmptyTranslationError, InvalidResponseError => e
|
81
|
+
raise e unless attempt < DEFAULT_RETRY_COUNT
|
82
|
+
|
83
|
+
wait_time = DEFAULT_RETRY_DELAY * (2**attempt) # Backoff exponentiel
|
84
|
+
log_retry(e, attempt + 1, wait_time)
|
85
|
+
sleep(wait_time)
|
86
|
+
translate_with_retry(text, source_locale, target_locale, attempt + 1)
|
87
|
+
rescue RateLimitError => e
|
88
|
+
log_rate_limit_hit(source_locale, target_locale)
|
89
|
+
sleep(DEFAULT_RETRY_DELAY)
|
90
|
+
retry
|
91
|
+
end
|
92
|
+
|
93
|
+
def translate_small_batch(texts, source_locale, target_locale)
|
94
|
+
prompt = PromptBuilder.bulk_translation_prompt(texts, source_locale, target_locale)
|
95
|
+
raw_response = @client.complete(prompt)
|
96
|
+
|
97
|
+
results = ResponseParser.parse_bulk_translation_response(raw_response)
|
98
|
+
|
99
|
+
# Retourner un hash indexé par l'ordre original
|
100
|
+
results.each_with_object({}) do |result, hash|
|
101
|
+
original_index = result[:index] - 1 # L'API retourne 1-indexed
|
102
|
+
hash[original_index] = result[:translated]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def translate_large_batch(texts, source_locale, target_locale)
|
107
|
+
results = {}
|
108
|
+
|
109
|
+
texts.each_slice(10).with_index do |batch, batch_index|
|
110
|
+
sleep(DEFAULT_RETRY_DELAY) if batch_index.positive? # Délai entre les batches
|
111
|
+
|
112
|
+
batch_results = translate_small_batch(batch, source_locale, target_locale)
|
113
|
+
|
114
|
+
# Ajuster les index pour le batch
|
115
|
+
batch_results.each do |local_index, translation|
|
116
|
+
global_index = (batch_index * 10) + local_index
|
117
|
+
results[global_index] = translation
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
results
|
122
|
+
end
|
123
|
+
|
124
|
+
def build_language_detection_prompt(text)
|
125
|
+
PromptBuilder.language_detection_prompt(text)
|
126
|
+
end
|
127
|
+
|
128
|
+
def parse_language_detection(response)
|
129
|
+
json_content = response.match(/\{.*\}/m)&.[](0)
|
130
|
+
return "en" unless json_content # Défaut en anglais si détection échoue
|
131
|
+
|
132
|
+
data = JSON.parse(json_content)
|
133
|
+
detected = data["detected_language"]
|
134
|
+
|
135
|
+
LocaleHelper.locale_supported?(detected) ? detected : "en"
|
136
|
+
rescue JSON::ParserError
|
137
|
+
"en" # Défaut en anglais si parsing échoue
|
138
|
+
end
|
139
|
+
|
140
|
+
def validate_inputs!(text, from, to)
|
141
|
+
raise ArgumentError, "Text cannot be nil or empty" if text.nil? || text.empty?
|
142
|
+
raise ArgumentError, "Source language cannot be nil" if from.nil?
|
143
|
+
raise ArgumentError, "Target language cannot be nil" if to.nil?
|
144
|
+
raise ArgumentError, "Source and target languages cannot be the same" if from == to
|
145
|
+
end
|
146
|
+
|
147
|
+
def validate_translation_inputs!(text, from, to)
|
148
|
+
# Convertir to en array pour la validation
|
149
|
+
target_languages = Array(to)
|
150
|
+
raise ArgumentError, "Target languages cannot be empty" if target_languages.empty?
|
151
|
+
|
152
|
+
validate_inputs!(text, from, target_languages.first)
|
153
|
+
end
|
154
|
+
|
155
|
+
def validate_batch_inputs!(texts, from, to)
|
156
|
+
raise ArgumentError, "Texts array cannot be nil or empty" if texts.nil? || texts.empty?
|
157
|
+
raise ArgumentError, "Source language cannot be nil" if from.nil?
|
158
|
+
raise ArgumentError, "Target language cannot be nil" if to.nil?
|
159
|
+
|
160
|
+
texts.each_with_index do |text, index|
|
161
|
+
raise ArgumentError, "Text at index #{index} cannot be nil or empty" if text.nil? || text.empty?
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def log_retry(error, attempt, wait_time)
|
166
|
+
message = "#{error.class.name}: #{error.message}. Retry #{attempt}/#{DEFAULT_RETRY_COUNT} in #{wait_time}s"
|
167
|
+
# Log une seule fois par type d'erreur pour éviter le spam
|
168
|
+
Logger.warn_once(message, key: "retry_#{error.class.name}", sensitive: false, ttl: 120)
|
169
|
+
end
|
170
|
+
|
171
|
+
def log_rate_limit_hit(source, target)
|
172
|
+
message = "Rate limit hit for translation #{source} -> #{target}, retrying..."
|
173
|
+
# Log une seule fois par paire de langues
|
174
|
+
Logger.warn_once(message, key: "rate_limit_#{source}_#{target}", sensitive: false, ttl: 300)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MistralTranslator
|
4
|
+
VERSION = "0.1.0"
|
5
|
+
|
6
|
+
# Informations additionnelles sur la gem
|
7
|
+
API_VERSION = "v1"
|
8
|
+
SUPPORTED_MODEL = "mistral-small"
|
9
|
+
|
10
|
+
# Méthode pour obtenir des informations complètes sur la version
|
11
|
+
def self.version_info
|
12
|
+
{
|
13
|
+
gem_version: VERSION,
|
14
|
+
api_version: API_VERSION,
|
15
|
+
supported_model: SUPPORTED_MODEL,
|
16
|
+
ruby_version: RUBY_VERSION,
|
17
|
+
platform: RUBY_PLATFORM
|
18
|
+
}
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "mistral_translator/version"
|
4
|
+
require_relative "mistral_translator/errors"
|
5
|
+
require_relative "mistral_translator/configuration"
|
6
|
+
require_relative "mistral_translator/locale_helper"
|
7
|
+
require_relative "mistral_translator/prompt_builder"
|
8
|
+
require_relative "mistral_translator/response_parser"
|
9
|
+
require_relative "mistral_translator/client"
|
10
|
+
require_relative "mistral_translator/translator"
|
11
|
+
require_relative "mistral_translator/summarizer"
|
12
|
+
|
13
|
+
module MistralTranslator
|
14
|
+
class << self
|
15
|
+
# Méthodes de convenance pour accès direct
|
16
|
+
def translate(text, from:, to:)
|
17
|
+
translator.translate(text, from: from, to: to)
|
18
|
+
end
|
19
|
+
|
20
|
+
def translate_to_multiple(text, from:, to:)
|
21
|
+
translator.translate_to_multiple(text, from: from, to: to)
|
22
|
+
end
|
23
|
+
|
24
|
+
def translate_batch(texts, from:, to:)
|
25
|
+
translator.translate_batch(texts, from: from, to: to)
|
26
|
+
end
|
27
|
+
|
28
|
+
def translate_auto(text, to:)
|
29
|
+
translator.translate_auto(text, to: to)
|
30
|
+
end
|
31
|
+
|
32
|
+
def summarize(text, language: "fr", max_words: 250)
|
33
|
+
summarizer.summarize(text, language: language, max_words: max_words)
|
34
|
+
end
|
35
|
+
|
36
|
+
def summarize_and_translate(text, from:, to:, max_words: 250)
|
37
|
+
summarizer.summarize_and_translate(text, from: from, to: to, max_words: max_words)
|
38
|
+
end
|
39
|
+
|
40
|
+
def summarize_to_multiple(text, languages:, max_words: 250)
|
41
|
+
summarizer.summarize_to_multiple(text, languages: languages, max_words: max_words)
|
42
|
+
end
|
43
|
+
|
44
|
+
def summarize_tiered(text, language: "fr", short: 50, medium: 150, long: 300)
|
45
|
+
summarizer.summarize_tiered(text, language: language, short: short, medium: medium, long: long)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Méthodes utilitaires
|
49
|
+
def supported_languages
|
50
|
+
LocaleHelper.supported_languages_list
|
51
|
+
end
|
52
|
+
|
53
|
+
def supported_locales
|
54
|
+
LocaleHelper.supported_locales
|
55
|
+
end
|
56
|
+
|
57
|
+
def locale_supported?(locale)
|
58
|
+
LocaleHelper.locale_supported?(locale)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Configuration
|
62
|
+
def configure
|
63
|
+
yield(configuration) if block_given?
|
64
|
+
end
|
65
|
+
|
66
|
+
def configuration
|
67
|
+
@configuration ||= Configuration.new
|
68
|
+
end
|
69
|
+
|
70
|
+
def reset_configuration!
|
71
|
+
@configuration = Configuration.new
|
72
|
+
@translator = nil
|
73
|
+
@summarizer = nil
|
74
|
+
@client = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
# Version info
|
78
|
+
def version
|
79
|
+
VERSION
|
80
|
+
end
|
81
|
+
|
82
|
+
# Health check
|
83
|
+
def health_check
|
84
|
+
client.complete("Hello", max_tokens: 10)
|
85
|
+
{ status: :ok, message: "API connection successful" }
|
86
|
+
rescue AuthenticationError
|
87
|
+
{ status: :error, message: "Authentication failed - check your API key" }
|
88
|
+
rescue ApiError => e
|
89
|
+
{ status: :error, message: "API error: #{e.message}" }
|
90
|
+
rescue StandardError => e
|
91
|
+
{ status: :error, message: "Unexpected error: #{e.message}" }
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def translator
|
97
|
+
@translator ||= Translator.new(client: client)
|
98
|
+
end
|
99
|
+
|
100
|
+
def summarizer
|
101
|
+
@summarizer ||= Summarizer.new(client: client)
|
102
|
+
end
|
103
|
+
|
104
|
+
def client
|
105
|
+
@client ||= Client.new
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Alias pour compatibilité
|
110
|
+
module Convenience
|
111
|
+
def self.included(base)
|
112
|
+
base.extend(ClassMethods)
|
113
|
+
end
|
114
|
+
|
115
|
+
module ClassMethods
|
116
|
+
def mistral_translate(text, from:, to:)
|
117
|
+
MistralTranslator.translate(text, from: from, to: to)
|
118
|
+
end
|
119
|
+
|
120
|
+
def mistral_summarize(text, language: "fr", max_words: 250)
|
121
|
+
MistralTranslator.summarize(text, language: language, max_words: max_words)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Extensions optionnelles pour String
|
128
|
+
if ENV["MISTRAL_TRANSLATOR_EXTEND_STRING"]
|
129
|
+
class String
|
130
|
+
def mistral_translate(from:, to:)
|
131
|
+
MistralTranslator.translate(self, from: from, to: to)
|
132
|
+
end
|
133
|
+
|
134
|
+
def mistral_summarize(language: "fr", max_words: 250)
|
135
|
+
MistralTranslator.summarize(self, language: language, max_words: max_words)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
metadata
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mistral_translator
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Peyochanchan
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: net-http
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0.4'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0.4'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rake
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '13.0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '13.0'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: rspec
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.0'
|
47
|
+
type: :development
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rubocop
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '1.21'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '1.21'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: vcr
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '6.2'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '6.2'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: webmock
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '3.18'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '3.18'
|
96
|
+
description: Allows translating text into different languages and generating summaries
|
97
|
+
using the MistralAI API
|
98
|
+
email:
|
99
|
+
- cameleon24@outlook.fr
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".rspec"
|
105
|
+
- ".rubocop.yml"
|
106
|
+
- ".ruby-version"
|
107
|
+
- CHANGELOG.md
|
108
|
+
- CODE_OF_CONDUCT.md
|
109
|
+
- LICENSE.txt
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- examples/basic_usage.rb
|
113
|
+
- lib/mistral_translator.rb
|
114
|
+
- lib/mistral_translator/client.rb
|
115
|
+
- lib/mistral_translator/configuration.rb
|
116
|
+
- lib/mistral_translator/errors.rb
|
117
|
+
- lib/mistral_translator/locale_helper.rb
|
118
|
+
- lib/mistral_translator/logger.rb
|
119
|
+
- lib/mistral_translator/prompt_builder.rb
|
120
|
+
- lib/mistral_translator/response_parser.rb
|
121
|
+
- lib/mistral_translator/summarizer.rb
|
122
|
+
- lib/mistral_translator/translator.rb
|
123
|
+
- lib/mistral_translator/version.rb
|
124
|
+
- sig/mistral_translator.rbs
|
125
|
+
homepage: https://github.com/Peyochanchan/mistral_translator
|
126
|
+
licenses:
|
127
|
+
- MIT
|
128
|
+
metadata:
|
129
|
+
allowed_push_host: https://rubygems.org
|
130
|
+
homepage_uri: https://github.com/Peyochanchan/mistral_translator
|
131
|
+
changelog_uri: https://github.com/Peyochanchan/mistral_translator/blob/main/CHANGELOG.md
|
132
|
+
rubygems_mfa_required: 'true'
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: 3.2.0
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubygems_version: 3.7.1
|
148
|
+
specification_version: 4
|
149
|
+
summary: Gem to translate and summarize text with Mistral API
|
150
|
+
test_files: []
|