translation_api 0.1.2 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 370f25cdc53d4030c0928031a07c102a659bd73e5896d5a03e6e094981ad76b7
4
- data.tar.gz: 676027c41756226cfe1ae91fb4a7f56edc5df58db1fa0a3a9299cec729247b8e
3
+ metadata.gz: c9c1c9798882d3591828278748d1aa763227e85a2c37be0d7583859c78fc0013
4
+ data.tar.gz: ac5378596044a829e4f653e258ade5074e023b2ca0d3ccb1a55b603c8e707c5d
5
5
  SHA512:
6
- metadata.gz: 4b3bd44c26896be44baa467a1cfe7a1dbd7b83ba30ea6dc0fdafb9e0a1001296a34724a5e243902e05435f431285f0aa8a5e9af2bf0fa4e653129d9db432ca4b
7
- data.tar.gz: cdcbf2fd3bf73d6bcddb51488c1123d4ed79b76d137b6e64a8a05b183487a789340db5dffe6c1a215a7064c2bf3f79cc92c6f4146a35970cfb6560aeced046bd
6
+ metadata.gz: 94379f1ffbd3741c72fc87289b7887b434eb65e3bb8fb55438a98fdede95e4220f473aae4f56aeed5a126099d95fdc7abde0d2b9fd05f53d8ec7286bb03af54a
7
+ data.tar.gz: 17d351d60dc25125577f5a9c8a43490e86a91a8d77420d795131c17de6c7b1a33e3f9050c5d836272fd87ec825846a25379fd44d42f1725580bde08b4fe5c06a
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 3.0
2
+ TargetRubyVersion: 3.4
3
3
  NewCops: enable
4
4
 
5
5
  Style/StringLiterals:
@@ -16,7 +16,7 @@ Style/ParallelAssignment:
16
16
 
17
17
  Metrics/MethodLength:
18
18
  CountComments: true
19
- Max: 20
19
+ Max: 30
20
20
 
21
21
  Lint/ScriptPermission:
22
22
  Enabled: false
@@ -29,4 +29,4 @@ RSpec/ExampleLength:
29
29
 
30
30
  require:
31
31
  - rubocop-rake
32
- - rubocop-rspec
32
+ - rubocop-rspec
data/CHANGELOG.md CHANGED
@@ -29,4 +29,13 @@
29
29
  ## [0.1.2] - 2025-03-31
30
30
 
31
31
  - deeplを使用可能に
32
- - gemspecにdeepl-rbを書き忘れていたのを修正
32
+ - gemspecにdeepl-rbを書き忘れていたのを修正
33
+
34
+ ## [0.2.0] - 2025-07-26
35
+
36
+ - いろいろバージョン更新
37
+ - OpenAIのモデル追加
38
+
39
+ ## [1.0.0] - 2025-11-10
40
+ - 全体的な設計を刷新
41
+ - よりシンプルなインターフェースに変更
data/README.md CHANGED
@@ -7,14 +7,14 @@ Requires api key.
7
7
 
8
8
  1. `touch .env`
9
9
  2. Add `OPENAI_API_KEY=YOUR_API_KEY`
10
- 3. Optional: `ENV["OPENAI_MODEL"]`(default: gpt-4o-mini)
11
- 4. `TranslationAPI::Mediator.new.translate("text")`
10
+ 3. Optional: `ENV["OPENAI_MODEL"]`
11
+ 4. `TranslationAPI.translate("text")`
12
12
 
13
- ### Init Options
13
+ ### Configuration Options
14
14
 
15
15
  * output_logs (default: true)
16
16
  * language (default: "japanese")
17
- * agent (default: :openai)
17
+ * provider (default: :openai)
18
18
  * except_words (default: [])
19
19
 
20
20
  ### Output
@@ -26,3 +26,14 @@ Requires api key.
26
26
  ## Example
27
27
 
28
28
  Exec `ruby example.rb "text"`
29
+
30
+ ```ruby
31
+ TranslationAPI.configure do |config|
32
+ config.language = "english"
33
+ config.provider = :deepl
34
+ config.output_logs = false
35
+ config.except_words = %w[hoge fuga]
36
+ end
37
+
38
+ TranslationAPI.translate("text")
39
+ ```
data/example.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/translation_api/mediator"
3
+ require_relative "lib/translation_api"
4
4
 
5
5
  if ARGV.empty?
6
6
  puts "引数が必要です: ruby example.rb \"text\""
@@ -8,12 +8,11 @@ if ARGV.empty?
8
8
  end
9
9
 
10
10
  text = ARGV.join(" ")
11
- translator =
12
- TranslationAPI::Mediator.new(
13
- output_logs: true,
14
- language: "japanese",
15
- agent: :deepl,
16
- except_words: %w[hoge fuga]
17
- )
18
- translated_text = translator.translate(text)
11
+ TranslationAPI.configure do |config|
12
+ config.language = "english"
13
+ config.provider = :deepl
14
+ config.except_words = %w[hoge fuga]
15
+ end
16
+
17
+ translated_text = TranslationAPI.translate(text)
19
18
  p translated_text
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+
5
+ class TranslationAPI
6
+ class Config
7
+ include Singleton
8
+
9
+ attr_accessor :language, :provider, :output_logs, :except_words, :deepl_pro
10
+
11
+ def self.configure(&block)
12
+ block.call(instance)
13
+ end
14
+
15
+ def initialize
16
+ @language = "japanese"
17
+ @provider = :openai
18
+ @output_logs = true
19
+ @except_words = []
20
+ @deepl_pro = false
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dotenv"
4
+ require "deepl"
5
+
6
+ class TranslationAPI
7
+ module Provider
8
+ class DeepL
9
+ SYSTEM_PROMPT_BASE = <<~TEXT
10
+ Keep symbols
11
+ TEXT
12
+
13
+ API_KEY_ERROR_MESSAGE = "API key is not found."
14
+
15
+ LANGUAGE_UNSUPPORTED_MESSAGE = "This language is unsupported by DeepL."
16
+
17
+ def initialize(pro:, except_words: [], language: "japanese")
18
+ @pro = pro
19
+ @language = language
20
+
21
+ setup_deepl_config!
22
+ validate_supported_language!
23
+
24
+ @system_content = SYSTEM_PROMPT_BASE + except_option_text(except_words)
25
+ @language = supported_languages[language.to_sym]
26
+ end
27
+
28
+ def translate(text)
29
+ return text if text.strip.empty?
30
+
31
+ ::DeepL.translate(text, nil, @language, context: @system_content).text
32
+ end
33
+
34
+ private
35
+
36
+ def setup_deepl_config!
37
+ validate_api_key!
38
+
39
+ ::DeepL.configure do |config|
40
+ config.auth_key = ENV["DEEPL_API_KEY"] || ENV["DEEPL_AUTH_KEY"]
41
+ config.host = @pro ? "https://api.deepl.com" : "https://api-free.deepl.com"
42
+ end
43
+ end
44
+
45
+ def validate_api_key!
46
+ raise API_KEY_ERROR_MESSAGE unless ENV["DEEPL_API_KEY"] || ENV["DEEPL_AUTH_KEY"]
47
+ end
48
+
49
+ def validate_supported_language!
50
+ raise LANGUAGE_UNSUPPORTED_MESSAGE unless supported_language?
51
+ end
52
+
53
+ def supported_languages
54
+ @supported_languages ||=
55
+ ::DeepL.languages.to_h { [it.name.downcase.to_sym, it.code] }
56
+ end
57
+
58
+ def supported_language?
59
+ supported_languages.key?(@language.to_sym)
60
+ end
61
+
62
+ def except_option_text(except_words)
63
+ return "" if except_words.empty?
64
+
65
+ <<~TEXT
66
+ Words listed next are not translated: [#{except_words.join(", ")}]
67
+ TEXT
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TranslationAPI
4
+ module Provider
5
+ class OpenAI
6
+ class Cost
7
+ BASE_MODEL_NAME = "gpt-5"
8
+ ONE_MILLION = 1_000_000
9
+ BASE_MODEL_COST = 1.25 / ONE_MILLION
10
+
11
+ def initialize(provider)
12
+ @provider = provider
13
+ end
14
+
15
+ def input_cost(used_tokens)
16
+ calculate_cost(used_tokens, :input)
17
+ end
18
+
19
+ def output_cost(used_tokens)
20
+ calculate_cost(used_tokens, :output)
21
+ end
22
+
23
+ private
24
+
25
+ def calculate_cost(used_tokens, type)
26
+ used_tokens * token_rates[@provider.using_model][type]
27
+ end
28
+
29
+ def token_rates
30
+ normal_models = base_model.merge(mini_model).merge(nano_model)
31
+ normal_models.merge(other_models)
32
+ end
33
+
34
+ def base_model
35
+ {
36
+ BASE_MODEL_NAME => {
37
+ input: BASE_MODEL_COST,
38
+ output: BASE_MODEL_COST * normal_io_ratio[:output]
39
+ }
40
+ }
41
+ end
42
+
43
+ def mini_model
44
+ {
45
+ "#{BASE_MODEL_NAME}-mini" => {
46
+ input: BASE_MODEL_COST / normal_cost_diff_ratio,
47
+ output: (BASE_MODEL_COST * normal_io_ratio[:output]) / normal_cost_diff_ratio
48
+ }
49
+ }
50
+ end
51
+
52
+ def nano_model
53
+ mini_model_cost = mini_model.values[0][:input]
54
+
55
+ {
56
+ "#{BASE_MODEL_NAME}-nano" => {
57
+ input: mini_model_cost / normal_cost_diff_ratio,
58
+ output: (mini_model_cost * normal_io_ratio[:output]) / normal_cost_diff_ratio
59
+ }
60
+ }
61
+ end
62
+
63
+ def other_models
64
+ {
65
+ "#{BASE_MODEL_NAME}-chat-latest" => {
66
+ input: 1.25 / ONE_MILLION,
67
+ output: (1.25 * normal_io_ratio[:output]) / ONE_MILLION
68
+ }
69
+ }
70
+ end
71
+
72
+ def normal_io_ratio
73
+ {
74
+ input: 1.0,
75
+ output: 8.0
76
+ }
77
+ end
78
+
79
+ def normal_cost_diff_ratio
80
+ 5.0
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require_relative "cost"
5
+
6
+ class TranslationAPI
7
+ module Provider
8
+ class OpenAI
9
+ class Log
10
+ def initialize(provider)
11
+ @provider = provider
12
+ @cost = Cost.new(@provider)
13
+ end
14
+
15
+ def write
16
+ write_translated_text
17
+ write_used_tokens
18
+ write_total_cost
19
+ end
20
+
21
+ private
22
+
23
+ def write_translated_text
24
+ log_file_path = text_path("translated_text.txt")
25
+
26
+ File.open(log_file_path, "a") do |file|
27
+ file.puts(@provider.translated_text)
28
+ end
29
+ end
30
+
31
+ def write_used_tokens
32
+ log_file_path = text_path("tokens.txt")
33
+
34
+ existing_input_tokens, existing_output_tokens = read_existing_tokens
35
+ tokens => { input_tokens:, output_tokens: }
36
+
37
+ total_input_tokens = existing_input_tokens + input_tokens
38
+ total_output_tokens = existing_output_tokens + output_tokens
39
+
40
+ File.open(log_file_path, "w") do |file|
41
+ file.puts("input: #{total_input_tokens}")
42
+ file.puts("output: #{total_output_tokens}")
43
+ end
44
+ end
45
+
46
+ def read_existing_tokens
47
+ log_file_path = text_path("tokens.txt")
48
+ input_tokens, output_tokens = 0, 0
49
+
50
+ if File.exist?(log_file_path)
51
+ File.readlines(log_file_path).each do |line|
52
+ tokens = line.split(":").last.strip.to_i
53
+ input_tokens = tokens if line.start_with?("input:")
54
+ output_tokens = tokens if line.start_with?("output:")
55
+ end
56
+ end
57
+
58
+ [input_tokens, output_tokens]
59
+ end
60
+
61
+ def write_total_cost
62
+ log_file_path = text_path("cost.txt")
63
+ tokens => { input_tokens:, output_tokens: }
64
+
65
+ this_cost = @cost.input_cost(input_tokens) + @cost.output_cost(output_tokens)
66
+ total_cost = this_cost + existing_cost
67
+
68
+ File.open(log_file_path, "w") do |file|
69
+ file.puts(format_cost(total_cost))
70
+ end
71
+ end
72
+
73
+ def format_cost(cost)
74
+ "$#{format("%.8f", cost)}"
75
+ end
76
+
77
+ def existing_cost
78
+ log_file_path = text_path("cost.txt")
79
+
80
+ File.exist?(log_file_path) ? File.read(log_file_path).gsub("$", "").to_f : 0.0
81
+ end
82
+
83
+ def tokens
84
+ {
85
+ input_tokens: @provider.dig_used_tokens(type: :input),
86
+ output_tokens: @provider.dig_used_tokens(type: :output)
87
+ }
88
+ end
89
+
90
+ def text_path(under_logs_path)
91
+ output_dir = "translator_logs/openai"
92
+ FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir)
93
+
94
+ File.join(output_dir, under_logs_path)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openai"
4
+ require_relative "openai/log"
5
+
6
+ class TranslationAPI
7
+ module Provider
8
+ class OpenAI
9
+ SYSTEM_PROMPT_BASE = <<~TEXT
10
+ Translate only.
11
+ Return result only, no extra info
12
+ Keep symbols
13
+ TEXT
14
+
15
+ API_KEY_ERROR_MESSAGE = "API key is not found."
16
+
17
+ MODEL_ERROR_MESSAGE =
18
+ "Specified model is not supported. Please check the model name."
19
+
20
+ def initialize(output_logs:, except_words:, language:)
21
+ validate_api_key!
22
+
23
+ @client = init_client
24
+ @output_logs = output_logs
25
+ @system_prompt = SYSTEM_PROMPT_BASE + except_option_text(except_words)
26
+ @user_prompt = user_prompt_text(language)
27
+ end
28
+
29
+ def translate(text)
30
+ return text if text.strip.empty?
31
+
32
+ @response = chat_to_api(text)
33
+ Log.new(self).write if @output_logs
34
+
35
+ translated_text
36
+ end
37
+
38
+ def translated_text
39
+ @response["choices"][0]["message"]["content"]
40
+ end
41
+
42
+ def using_model
43
+ ENV["OPENAI_MODEL"] || "gpt-5-mini"
44
+ end
45
+
46
+ def dig_used_tokens(type:)
47
+ case type
48
+ when :input
49
+ @response["usage"]["prompt_tokens"]
50
+ when :output
51
+ @response["usage"]["completion_tokens"]
52
+ else
53
+ raise ArgumentError, "Invalid token type: #{type}"
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ def validate_api_key!
60
+ raise API_KEY_ERROR_MESSAGE unless ENV["OPENAI_API_KEY"]
61
+ end
62
+
63
+ def init_client
64
+ ::OpenAI::Client.new(
65
+ access_token: ENV["OPENAI_API_KEY"],
66
+ log_errors: true
67
+ )
68
+ end
69
+
70
+ def chat_to_api(text)
71
+ @client.chat(
72
+ parameters: {
73
+ model: using_model,
74
+ messages: [
75
+ { role: "system", content: @system_prompt },
76
+ { role: "user", content: @user_prompt + text }
77
+ ]
78
+ }
79
+ )
80
+ end
81
+
82
+ def except_option_text(except_words)
83
+ return "" if except_words.empty?
84
+
85
+ <<~TEXT
86
+ Words listed next are not translated: [#{except_words.join(", ")}]
87
+ TEXT
88
+ end
89
+
90
+ def user_prompt_text(language)
91
+ <<~TEXT
92
+ Please translate this text to #{language}:
93
+ TEXT
94
+ end
95
+ end
96
+ end
97
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module TranslationAPI
4
- VERSION = "0.1.2"
3
+ class TranslationAPI
4
+ VERSION = "1.0.0"
5
5
  end
@@ -1,6 +1,66 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "dotenv"
3
4
  require_relative "translation_api/version"
4
- require_relative "translation_api/mediator"
5
+ require_relative "translation_api/config"
6
+ require_relative "translation_api/provider/openai"
7
+ require_relative "translation_api/provider/deepl"
5
8
 
6
- module TranslationAPI; end
9
+ class TranslationAPI
10
+ UNSUPPORTED_PROVIDER_MESSAGE = "This provider is unsupported."
11
+
12
+ Dotenv.load
13
+
14
+ def self.config
15
+ Config.instance
16
+ end
17
+
18
+ def self.configure(&)
19
+ Config.configure(&)
20
+ end
21
+
22
+ def self.translate(text, **)
23
+ new(**).translate(text)
24
+ end
25
+
26
+ def initialize(
27
+ language: config.language,
28
+ provider: config.provider,
29
+ output_logs: config.output_logs,
30
+ except_words: config.except_words
31
+ )
32
+ @language = language
33
+ @output_logs = output_logs
34
+ @except_words = except_words
35
+ @provider = init_provider(provider)
36
+ end
37
+
38
+ def config
39
+ self.class.config
40
+ end
41
+
42
+ def translate(text)
43
+ @provider.translate(text)
44
+ end
45
+
46
+ private
47
+
48
+ def init_provider(provider)
49
+ case provider
50
+ when :openai
51
+ Provider::OpenAI.new(
52
+ output_logs: @output_logs,
53
+ except_words: @except_words,
54
+ language: @language
55
+ )
56
+ when :deepl
57
+ Provider::DeepL.new(
58
+ pro: config.deepl_pro,
59
+ except_words: @except_words,
60
+ language: @language
61
+ )
62
+ else
63
+ raise UNSUPPORTED_PROVIDER_MESSAGE
64
+ end
65
+ end
66
+ end
metadata CHANGED
@@ -1,56 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: translation_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - milkeclair
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-03-30 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: deepl-rb
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
- - - "~>"
16
+ - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: 2.5.3
18
+ version: '0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
- - - "~>"
23
+ - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: 2.5.3
25
+ version: '0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: dotenv
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 3.1.4
32
+ version: '0'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 3.1.4
39
+ version: '0'
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: ruby-openai
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "~>"
44
+ - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 7.1.0
46
+ version: '0'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - "~>"
51
+ - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 7.1.0
53
+ version: '0'
54
54
  description: translate
55
55
  email:
56
56
  - milkeclair.noreply@gmail.com
@@ -67,14 +67,13 @@ files:
67
67
  - example.rb
68
68
  - how_to_publish.txt
69
69
  - lib/translation_api.rb
70
- - lib/translation_api/calculator.rb
71
- - lib/translation_api/deepl.rb
72
- - lib/translation_api/mediator.rb
73
- - lib/translation_api/openai.rb
70
+ - lib/translation_api/config.rb
71
+ - lib/translation_api/provider/deepl.rb
72
+ - lib/translation_api/provider/openai.rb
73
+ - lib/translation_api/provider/openai/cost.rb
74
+ - lib/translation_api/provider/openai/log.rb
74
75
  - lib/translation_api/version.rb
75
- - lib/translation_api/writer.rb
76
76
  - rake_helper.rb
77
- - sig/translation_api.rbs
78
77
  homepage: https://github.com/milkeclair/translation_api
79
78
  licenses:
80
79
  - MIT
@@ -90,14 +89,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
90
89
  requirements:
91
90
  - - ">="
92
91
  - !ruby/object:Gem::Version
93
- version: 3.0.0
92
+ version: 3.4.1
94
93
  required_rubygems_version: !ruby/object:Gem::Requirement
95
94
  requirements:
96
95
  - - ">="
97
96
  - !ruby/object:Gem::Version
98
97
  version: '0'
99
98
  requirements: []
100
- rubygems_version: 3.6.2
99
+ rubygems_version: 3.6.9
101
100
  specification_version: 4
102
101
  summary: translate
103
102
  test_files: []
@@ -1,66 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "writer"
4
-
5
- module TranslationAPI
6
- class Calculator
7
- class ArgumentError < StandardError; end
8
- MODEL_ERROR_MESSAGE =
9
- "設定に無いモデルです。.envを確認してください。"
10
-
11
- # トークン数から利用料金を計算する
12
- #
13
- # @param [Integer] used_tokens 使用したトークン数
14
- # @param [String] token_type トークンの種類
15
- # @return [Float] 利用料金
16
- def self.calc_total_cost(used_tokens, token_type)
17
- model = ENV["OPENAI_MODEL"] || "gpt-4o-mini"
18
- rate = get_token_rate(model, token_type)
19
- used_tokens * rate
20
- end
21
-
22
- # モデルとトークンの種類からトークンの単価を取得する
23
- # モデルが設定に無い場合はエラーを投げる
24
- #
25
- # @param [String] model モデル名
26
- # @param [String] token_type トークンの種類
27
- # @return [Float] トークンの単価
28
- def self.get_token_rate(model, token_type)
29
- token_rate = token_rate_hash
30
- validate_model(model, token_rate)
31
- token_rate[model][token_type.to_sym]
32
- end
33
-
34
- # トークン単価のハッシュを返す
35
- #
36
- # @return [Hash] トークン単価のハッシュ
37
- def self.token_rate_hash
38
- one_million = 1_000_000
39
- {
40
- "gpt-4o" => {
41
- input: 5.0 / one_million,
42
- output: 15.0 / one_million
43
- },
44
- "gpt-4o-2024-08-06" => {
45
- input: 2.5 / one_million,
46
- output: 10.0 / one_million
47
- },
48
- "gpt-4o-mini" => {
49
- input: 0.15 / one_million,
50
- output: 0.6 / one_million
51
- }
52
- }
53
- end
54
-
55
- # モデルが存在するかどうかを確認する
56
- #
57
- # @param [String] model モデル名
58
- # @param [Hash] token_rate トークンレートのハッシュ
59
- # @raise [Calculator::ArgumentError] モデルが存在しない場合
60
- def self.validate_model(model, token_rate)
61
- return if token_rate.key?(model)
62
-
63
- raise Calculator::ArgumentError, MODEL_ERROR_MESSAGE
64
- end
65
- end
66
- end
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "dotenv"
4
- require "deepl"
5
-
6
- module TranslationAPI
7
- class DeepL
8
- SYSTEM_CONTENT_BASE = <<~TEXT
9
- Keep symbols
10
- TEXT
11
-
12
- def initialize(output_logs: true, except_words: [], language: "japanese", pro: false)
13
- Dotenv.load
14
- setup_deepl_config!(pro: pro)
15
- @supported_languages = fetch_supported_languages
16
- validate_supported!(language)
17
- @system_content = SYSTEM_CONTENT_BASE + except_option_text(except_words)
18
- @language = @supported_languages[language.to_sym]
19
- end
20
-
21
- def translate(text)
22
- return text if text.strip.empty?
23
-
24
- ::DeepL.translate(text, nil, @language, context: @system_content).text
25
- end
26
-
27
- private
28
-
29
- def setup_deepl_config!(pro:)
30
- validate_api_key!
31
-
32
- ::DeepL.configure do |config|
33
- config.auth_key = ENV["DEEPL_API_KEY"] || ENV["DEEPL_AUTH_KEY"]
34
- config.host = pro ? "https://api.deepl.com" : "https://api-free.deepl.com"
35
- end
36
- end
37
-
38
- def validate_supported!(lang)
39
- raise "This language is unsupported by DeepL" unless supported?(lang)
40
- end
41
-
42
- def validate_api_key!
43
- raise "API key is not found" unless ENV["DEEPL_API_KEY"] || ENV["DEEPL_AUTH_KEY"]
44
- end
45
-
46
- def fetch_supported_languages
47
- ::DeepL.languages.to_h { |lang| [lang.name.downcase.to_sym, lang.code] }
48
- end
49
-
50
- def supported?(lang)
51
- @supported_languages.key?(lang.to_sym)
52
- end
53
-
54
- def except_option_text(except_words)
55
- return "" if except_words.empty?
56
-
57
- <<~TEXT
58
- Words listed next are not translated: [#{except_words.join(", ")}]
59
- TEXT
60
- end
61
- end
62
- end
@@ -1,64 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "writer"
4
- require_relative "openai"
5
- require_relative "deepl"
6
-
7
- module TranslationAPI
8
- class Mediator
9
- # @param [Boolean] output_logs ログを出力するかどうか
10
- # @param [String] language 翻訳先の言語
11
- # @param [Symbol] agent 翻訳エージェント
12
- # @param [Array<String>] except_words 除外する単語のリスト
13
- # @return [TranslationAPI::Mediator]
14
- def initialize(
15
- output_logs: true, language: "japanese", agent: :openai, except_words: []
16
- )
17
- @output_logs = output_logs
18
- @language = language
19
- @agent = agent
20
- @except_words = except_words
21
- end
22
-
23
- # テキストを翻訳する
24
- #
25
- # @param [String] text 翻訳するテキスト
26
- # @return [String] 翻訳されたテキスト
27
- def translate(text)
28
- agent = init_agent
29
- agent.translate(text)
30
- end
31
-
32
- # エージェントのインスタンスを初期化する
33
- #
34
- # @return [Object] 翻訳エージェントのインスタンス
35
- def init_agent
36
- agent_class.new(
37
- output_logs: @output_logs, except_words: @except_words, language: @language
38
- )
39
- end
40
-
41
- # エージェントのクラスを返す
42
- #
43
- # @return [Class] エージェントのクラス
44
- def agent_class
45
- case @agent
46
- when :openai
47
- OpenAI
48
- when :deepl
49
- DeepL
50
- else
51
- class_name = camelize(@agent.to_s)
52
- Object.const_get("TranslationAPI::#{class_name}")
53
- end
54
- end
55
-
56
- # スネークケースの文字列をキャメルケースに変換する
57
- #
58
- # @param [String] str スネークケースの文字列
59
- # @return [String] キャメルケースの文字列
60
- def camelize(str)
61
- str.split("_").map(&:capitalize).join
62
- end
63
- end
64
- end
@@ -1,102 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "dotenv"
4
- require "openai"
5
- require_relative "writer"
6
-
7
- module TranslationAPI
8
- class OpenAI
9
- SYSTEM_CONTENT_BASE = <<~TEXT
10
- Translate only.
11
- Return result only, no extra info
12
- Keep symbols
13
- TEXT
14
-
15
- # OpenAI APIを使用してテキストを翻訳する
16
- #
17
- # @param [Boolean] output_logs ログを出力するかどうか
18
- # @param [Array<String>] except_words 除外する単語のリスト
19
- # @param [String] language 翻訳先の言語
20
- # @return [void]
21
- def initialize(output_logs: true, except_words: [], language: "japanese")
22
- # 環境変数の読み込み
23
- Dotenv.load
24
- raise "API key is not found" unless ENV["OPENAI_API_KEY"]
25
-
26
- @client = ::OpenAI::Client.new(
27
- access_token: ENV["OPENAI_API_KEY"],
28
- log_errors: true # 好み
29
- )
30
- @output_logs = output_logs
31
- @system_content = SYSTEM_CONTENT_BASE + except_option_text(except_words)
32
- @language = language
33
- end
34
-
35
- # テキストを日本語に翻訳し、結果をファイルに書き込む
36
- #
37
- # @param [String] text 翻訳するテキスト
38
- # @return [void]
39
- def translate(text)
40
- # 空白文字は翻訳する必要がない
41
- return text if text.strip.empty?
42
-
43
- response = chat_to_api(text)
44
- Writer.write_logs(self, response) if @output_logs
45
-
46
- response["choices"][0]["message"]["content"]
47
- end
48
-
49
- # レスポンスから使用したトークン数を取得する
50
- #
51
- # @param [Hash] response OpenAI APIからのレスポンス
52
- # @param [String] token_type トークンの種類 (input or output)
53
- # @return [Integer] 使用したトークン数
54
- def self.dig_used_tokens(response, token_type)
55
- if token_type == "input"
56
- response["usage"]["prompt_tokens"]
57
- elsif token_type == "output"
58
- response["usage"]["completion_tokens"]
59
- end
60
- end
61
-
62
- private
63
-
64
- # OpenAI APIにテキストを送信し、翻訳結果を取得する
65
- #
66
- # @param [String] text 翻訳するテキスト
67
- # @return [Hash] OpenAI APIからのレスポンス
68
- def chat_to_api(text)
69
- @client.chat(
70
- parameters: {
71
- model: ENV["OPENAI_MODEL"] || "gpt-4o-mini",
72
- messages: [
73
- { role: "system", content: @system_content },
74
- { role: "user", content: user_prompt_text(text) }
75
- ]
76
- }
77
- )
78
- end
79
-
80
- # 除外する単語を指定するプロンプト
81
- #
82
- # @param [Array<String>] except_words 除外する単語のリスト
83
- # @return [String] 除外する単語を指定するテキスト
84
- def except_option_text(except_words)
85
- return "" if except_words.empty?
86
-
87
- <<~TEXT
88
- Words listed next are not translated: [#{except_words.join(", ")}]
89
- TEXT
90
- end
91
-
92
- # ユーザー入力のプロンプト
93
- #
94
- # @param [String] text テキスト
95
- # @return [String] ユーザー入力のプロンプト
96
- def user_prompt_text(text)
97
- <<~TEXT
98
- Please translate this text to #{@language}: #{text}
99
- TEXT
100
- end
101
- end
102
- end
@@ -1,127 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "fileutils"
4
- require_relative "calculator"
5
- require_relative "openai"
6
-
7
- module TranslationAPI
8
- class Writer
9
- @agent = nil
10
-
11
- # ログをファイルに書き込む
12
- #
13
- # @param [Object] agent 翻訳エージェントのインスタンス
14
- # @param [Hash] response 翻訳した結果
15
- # @return [void]
16
- def self.write_logs(agent, response)
17
- # 例: "Hoge::Fuga" => "fuga"
18
- @agent = agent.class.to_s.split("::").last.downcase
19
- handle_agent(response)
20
- end
21
-
22
- # エージェントに対応したログ書き込み用メソッドを呼び出す
23
- #
24
- # @param [Hash] response 翻訳した結果
25
- # @return [void]
26
- def self.handle_agent(response)
27
- method_name = "write_#{@agent}_logs"
28
- send(method_name, response)
29
- end
30
-
31
- # OpenAIのログをファイルに書き込む
32
- #
33
- # @param [Hash] response OpenAI APIからのレスポンス
34
- # @return [void]
35
- def self.write_openai_logs(response)
36
- input_tokens = OpenAI.dig_used_tokens(response, "input")
37
- output_tokens = OpenAI.dig_used_tokens(response, "output")
38
-
39
- write_translated_text(response["choices"][0]["message"]["content"])
40
- write_used_tokens(input_tokens, output_tokens)
41
- write_total_cost(input_tokens, output_tokens)
42
- end
43
-
44
- # 出力先のテキストファイルのパスを返す
45
- # example.rbから見たパスで指定している
46
- #
47
- # @param [String] under_logs_path translator_logsディレクトリ配下のパス
48
- # @return [String] 出力先のテキストファイルのパス
49
- def self.text_path(under_logs_path)
50
- output_dir = "translator_logs/#{@agent}"
51
- FileUtils.mkdir_p(output_dir) unless File.directory?(output_dir)
52
- File.join(output_dir, under_logs_path)
53
- end
54
-
55
- # 翻訳されたテキストをファイルに書き込み、ターミナルに出力する
56
- # テキストはファイルの末尾に追記される
57
- #
58
- # @param [Hash] translated_text 翻訳されたテキスト
59
- # @return [void]
60
- def self.write_translated_text(translated_text)
61
- log_file_path = text_path("translated_text.txt")
62
- File.open(log_file_path, "a") do |file|
63
- file.puts(translated_text)
64
- end
65
- end
66
-
67
- # 使用したトークン数をファイルに書き込む
68
- # ファイルのテキストは上書きされる
69
- #
70
- # @param [Integer] input_tokens 入力トークン数
71
- # @param [Integer] output_tokens 出力トークン数
72
- # @return [void]
73
- def self.write_used_tokens(input_tokens, output_tokens)
74
- log_file_path = text_path("tokens.txt")
75
- existing_input_tokens, existing_output_tokens = read_existing_tokens(log_file_path)
76
-
77
- total_input_tokens = existing_input_tokens + input_tokens
78
- total_output_tokens = existing_output_tokens + output_tokens
79
-
80
- File.open(log_file_path, "w") do |file|
81
- file.puts("input: #{total_input_tokens}")
82
- file.puts("output: #{total_output_tokens}")
83
- end
84
- end
85
-
86
- # ファイルにあるトークン数を読み込む
87
- #
88
- # @param [String] log_file_path トークン数が書かれたファイルのパス
89
- # @return [Array<Integer>] 入力トークン数と出力トークン数
90
- def self.read_existing_tokens(log_file_path)
91
- existing_input_tokens, existing_output_tokens = 0, 0
92
-
93
- if File.exist?(log_file_path)
94
- File.readlines(log_file_path).each do |line|
95
- existing_input_tokens = line.split(":").last.strip.to_i if line.start_with?("input:")
96
- existing_output_tokens = line.split(":").last.strip.to_i if line.start_with?("output:")
97
- end
98
- end
99
-
100
- [existing_input_tokens, existing_output_tokens]
101
- end
102
-
103
- # トークン数から利用料金を計算し、ファイルにある合計金額に加算して書き込む
104
- # ファイルのテキストは上書きされる
105
- #
106
- # @param [Integer] input_tokens 入力トークン数
107
- # @param [Integer] output_tokens 出力トークン数
108
- # @return [void]
109
- def self.write_total_cost(input_tokens, output_tokens)
110
- log_file_path = text_path("cost.txt")
111
- this_cost =
112
- Calculator.calc_total_cost(input_tokens, "input") + Calculator.calc_total_cost(output_tokens, "output")
113
- existing_cost =
114
- if File.exist?(log_file_path)
115
- File.read(log_file_path).gsub("$", "").to_f
116
- else
117
- 0.0
118
- end
119
- total_cost = this_cost + existing_cost
120
-
121
- File.open(log_file_path, "w") do |file|
122
- # 小数点以下8桁まで表示
123
- file.puts("$#{format("%.8f", total_cost)}")
124
- end
125
- end
126
- end
127
- end
@@ -1,4 +0,0 @@
1
- module TranslationAPI
2
- VERSION: String
3
- # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
- end