jekyll-chatgpt-translate 0.2.0 → 0.3.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: bef852600359023f88efcea920702d0bbb44ae263e0493c77e10989df3b3e311
4
- data.tar.gz: 5b8fc42cc02249b143396487d0ea0123e276362369b35a7a869c3291a3b9dea0
3
+ metadata.gz: a33656cc2e5c82d51feb6cb2900c339cce6f210bbe9615dd19756f457ace47c7
4
+ data.tar.gz: 2867d144dfa9e867a6bbecce340a1d77eb908a54c9790fbef9f7985223861250
5
5
  SHA512:
6
- metadata.gz: 961104d1f55fdceaf47dae3bf5c739d871987cd0f31ae1203e311eec12fe018c1726ff47e0eb31844406197bf51b755295ca9d2befec4ea4d7e596fa7bf5cf41
7
- data.tar.gz: 543b382769527466b275c0d03d01590e745cdc7a8df51849a6edf9cf711441d38a708198a2ea4a0342b3675ecbfd3de39cdb84211156dcaebcc0cfa52171920f
6
+ metadata.gz: 370cf4f063e237ab00af98e0212b738211d33f868e24c89547a4b378f867389ff46c6dfce39ac90553e25e448751490b8c465382d5a6ffdefb6aa8bca59b6826
7
+ data.tar.gz: beafec1d47ac6b1f731d9557de276c7ce4d9b9223a6a19e3791e7b9555cf2d3950501e99d77bc92c54501a0c8b8d4ba21710cb35102ad4a932430330c32b6795
data/Gemfile CHANGED
@@ -11,6 +11,7 @@ gem 'kramdown-parser-gfm', '~>1.1', require: false
11
11
  gem 'minitest', '~>6.0', require: false
12
12
  gem 'minitest-reporters', '~>1.7', require: false
13
13
  gem 'rake', '~>13.2', require: false
14
+ gem 'rdoc', '~>7.0', require: false
14
15
  gem 'rubocop', '~>1.64', require: false
15
16
  gem 'rubocop-minitest', '~>0.38', require: false
16
17
  gem 'rubocop-performance', '>1.26', require: false
data/README.md CHANGED
@@ -6,17 +6,18 @@
6
6
  [![Gem Version](https://badge.fury.io/rb/jekyll-chatgpt-translate.svg)](https://badge.fury.io/rb/jekyll-chatgpt-translate)
7
7
 
8
8
  If you have a [Jekyll](https://jekyllrb.com/) static site,
9
- this plugin may help you automatically
10
- translate its pages to another language, through
11
- [ChatGPT](https://chat.openai.com/). See how it
12
- works for [my blog](https://github.com/yegor256/ru.yegor256.com),
13
- for example [this page](https://ru.yegor256.com/2023-08-13-dictators.html)
14
- is translated to
15
- [English](https://www.yegor256.com/en/2023/08/13/dictators.html).
16
-
17
- Install it first (you need
18
- [Ruby 3+](https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-released/)
19
- and [Jekyll 3+](https://jekyllrb.com/)):
9
+ this plugin may help you automatically
10
+ translate its pages to another language, through
11
+ [ChatGPT](https://chat.openai.com/).
12
+ See how it works for [my blog](https://github.com/yegor256/ru.yegor256.com),
13
+ for example [this page](https://ru.yegor256.com/2023-08-13-dictators.html)
14
+ is translated to
15
+ [English](https://www.yegor256.com/en/2023/08/13/dictators.html).
16
+
17
+ Install it first
18
+ (you need
19
+ [Ruby 3+](https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-released/)
20
+ and [Jekyll 3+](https://jekyllrb.com/)):
20
21
 
21
22
  ```bash
22
23
  gem install jekyll-chatgpt-translate
@@ -49,29 +50,28 @@ where the layout for Chinese is `_layout/chinese-translated.html` and for
49
50
  French is `_layout/translated.html` (you must have these files).
50
51
 
51
52
  OpenAI API KEY must be set in the `OPENAI_API_KEY` environment variable,
52
- otherwise
53
- the plugin will not do any translation and won't generate translated pages.
54
- You can get your key
55
- [here][open-ai].
53
+ otherwise the plugin will not do any translation
54
+ and won't generate translated pages.
55
+ You can get your key go to [OpenAI][open-ai].
56
56
 
57
57
  OpenAI API base URL can be customized by the `OPENAI_API_BASE`
58
- environment variable.
58
+ environment variable.
59
59
  If this variable is not set, the default value is `https://api.openai.com/`.
60
60
 
61
61
  Inside the original page you can use `{{ page.chatgpt-translate.urls[XX] }}`
62
- in order to render the URL
63
- of the translated page, where `XX` is the [ISO-639-1][iso-639]
64
- code of the target language.
62
+ in order to render the URL
63
+ of the translated page, where `XX` is the [ISO-639-1][iso-639]
64
+ code of the target language.
65
65
  Inside the translated page you can use
66
- `{{ page.chatgpt-translate.original-url }}` in order
67
- to get the URL of the page that was translated.
66
+ `{{ page.chatgpt-translate.original-url }}` in order
67
+ to get the URL of the page that was translated.
68
68
 
69
69
  You can also use `{{ page.chatgpt-translate.model }}`
70
- inside both the original page and the translated one,
71
- to refer to the model of ChatGPT.
70
+ inside both the original page and the translated one,
71
+ to refer to the model of ChatGPT.
72
72
  The presence of `{{ page.chatgpt-translate }}` means that the
73
- page was translated or the translated HTML was downloaded
74
- and placed into the `_site` directory.
73
+ page was translated or the translated HTML was downloaded
74
+ and placed into the `_site` directory.
75
75
 
76
76
  ## Options
77
77
 
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
9
9
  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
10
10
  s.required_ruby_version = '>= 3.0'
11
11
  s.name = 'jekyll-chatgpt-translate'
12
- s.version = '0.2.0'
12
+ s.version = '0.3.0'
13
13
  s.license = 'MIT'
14
14
  s.summary = 'Translate Jekyll Pages Through ChatGPT'
15
15
  s.description = [
@@ -24,10 +24,11 @@ Gem::Specification.new do |s|
24
24
  s.rdoc_options = ['--charset=UTF-8']
25
25
  s.extra_rdoc_files = %w[README.md LICENSE.txt]
26
26
  s.add_dependency 'base64', '~>0.3'
27
+ s.add_dependency 'elapsed', '~>0.2'
27
28
  s.add_dependency 'humanize', '~>3.1'
28
29
  s.add_dependency 'iri', '~>0.11'
29
30
  s.add_dependency 'iso-639', '~>0.3'
30
- s.add_dependency 'jekyll', '~>3.10'
31
+ s.add_dependency 'jekyll', '>= 3.0', '< 5.0'
31
32
  s.add_dependency 'json', '~>2.18'
32
33
  s.add_dependency 'redcarpet', '~>3.6'
33
34
  s.add_dependency 'ruby-openai', '~>8.3'
@@ -3,10 +3,11 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2023-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
+ require 'elapsed'
7
+ require 'iso-639'
6
8
  require 'jekyll'
7
9
  require 'json'
8
10
  require 'openai'
9
- require 'iso-639'
10
11
  require 'tiktoken_ruby'
11
12
  require_relative 'pars'
12
13
  require_relative 'prompt'
@@ -115,40 +116,39 @@ class GptTranslate::ChatGPT
115
116
  @@models_printed = true
116
117
  end
117
118
  prompt = GptTranslate::Prompt.new(par, @source, @target).to_s
118
- start = Time.now
119
119
  answer = nil
120
120
  attempt = 0
121
- begin
122
- response = @client.chat(
123
- parameters: {
124
- model: @model,
125
- messages: [{ role: 'user', content: prompt }],
126
- temperature: 0.7
127
- }
128
- )
129
- json = response.is_a?(Hash) ? response : JSON.parse(response)
130
- answer = json.dig('choices', 0, 'message', 'content')
131
- if answer.nil?
132
- Jekyll.logger.error("No content returned by ChatGPT: #{response}")
133
- raise 'No content returned by ChatGPT'
134
- end
135
- Jekyll.logger.debug("ChatGPT prompt: #{prompt.inspect}, ChatGPT answer: #{answer.inspect}")
136
- rescue StandardError => e
137
- attempt += 1
138
- if attempt < 4
139
- Jekyll.logger.error(
140
- "ChatGPT failed to answer to #{prompt.inspect}" \
141
- "(attempt no.#{attempt}): #{e.message.inspect}"
121
+ elapsed(Jekyll.logger) do
122
+ begin
123
+ response = @client.chat(
124
+ parameters: {
125
+ model: @model,
126
+ messages: [{ role: 'user', content: prompt }],
127
+ temperature: 0.7
128
+ }
142
129
  )
143
- retry
130
+ json = response.is_a?(Hash) ? response : JSON.parse(response)
131
+ answer = json.dig('choices', 0, 'message', 'content')
132
+ if answer.nil?
133
+ Jekyll.logger.error("No content returned by ChatGPT: #{response}")
134
+ raise 'No content returned by ChatGPT'
135
+ end
136
+ Jekyll.logger.debug("ChatGPT prompt: #{prompt.inspect}, ChatGPT answer: #{answer.inspect}")
137
+ rescue StandardError => e
138
+ attempt += 1
139
+ if attempt < 4
140
+ Jekyll.logger.error(
141
+ "ChatGPT failed to answer to #{prompt.inspect}" \
142
+ "(attempt no.#{attempt}): #{e.message.inspect}"
143
+ )
144
+ retry
145
+ end
146
+ raise e
144
147
  end
145
- raise e
148
+ throw :"Translated #{par.split.count} #{@source.upcase} words \
149
+ to #{answer.split.count} #{@target.upcase} words \
150
+ through #{@model}: #{"#{par[0..24]}...".inspect}"
146
151
  end
147
- Jekyll.logger.info(
148
- "Translated #{par.split.count} #{@source.upcase} words " \
149
- "to #{answer.split.count} #{@target.upcase} words " \
150
- "through #{@model} in #{(Time.now - start).round(2)}s: #{"#{par[0..24]}...".inspect}"
151
- )
152
152
  answer
153
153
  end
154
154
  end
@@ -3,8 +3,9 @@
3
3
  # SPDX-FileCopyrightText: Copyright (c) 2023-2026 Yegor Bugayenko
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
- require 'jekyll'
6
+ require 'elapsed'
7
7
  require 'fileutils'
8
+ require 'jekyll'
8
9
  require 'json'
9
10
  require_relative 'chatgpt'
10
11
  require_relative 'permalink'
@@ -29,7 +30,7 @@ class GptTranslate::Generator < Jekyll::Generator
29
30
  Jekyll.logger.info("jekyll-chatgpt-translate #{GptTranslate::VERSION} skipped, due to the --offline option")
30
31
  return
31
32
  end
32
- Jekyll.logger.info("jekyll-chatgpt-translate #{GptTranslate::VERSION} starting...")
33
+ Jekyll.logger.info("jekyll-chatgpt-translate #{GptTranslate::VERSION} starts (no --offline option)...")
33
34
  config ||= site.config['chatgpt-translate'] || {}
34
35
  home = config['tmpdir'] || '_chatgpt-translate'
35
36
  key = api_key(config)
@@ -41,101 +42,100 @@ class GptTranslate::Generator < Jekyll::Generator
41
42
  version = config['version'] || GptTranslate::VERSION
42
43
  threshold = config['threshold'] || 1024
43
44
  min_chars = config['min_chars'] || 128
44
- start = Time.now
45
45
  translated = 0
46
46
  copied = 0
47
47
  model = config['model'] || 'gpt-3.5-turbo'
48
48
  marker = "Translated by ChatGPT #{model}#{"/#{version}" unless version.empty?}"
49
- site.posts.docs.shuffle.each_with_index do |doc, pos|
50
- plain = GptTranslate::Plain.new(doc.content).to_s
51
- layout = doc['layout']
52
- config['targets'].each do |target|
53
- pstart = Time.now
54
- link = GptTranslate::Permalink.new(doc, target['permalink']).to_path
55
- lang = target['language']
56
- raise 'Language must be defined for each target' if target.nil?
57
- only = target['only']
58
- if !only.nil? && layout != only
59
- Jekyll.logger.debug("Not translating #{link.inspect}, b/c 'only' set to '#{only}'")
60
- next
61
- end
62
- path = File.join(home, lang, doc.basename.gsub(/\.md$/, "-#{lang}.md"))
63
- FileUtils.mkdir_p(File.dirname(path))
64
- File.write(
65
- path,
66
- [
67
- '---',
68
- "layout: #{target['layout'] || layout}",
69
- "title: #{doc['title'].to_json}",
70
- "description: #{doc['description'].to_json}",
71
- "permalink: #{link.to_json}",
72
- 'chatgpt-translate:',
73
- " original-url: #{doc.url.to_json}",
74
- " language: #{lang.to_json}",
75
- " model: #{model.to_json}",
76
- '---'
77
- ].join("\n")
78
- )
79
- html = config['no_download'].nil? ? GptTranslate::Ping.new(site, link).download : nil
80
- needed = false
81
- added = false
82
- if html.nil?
83
- Jekyll.logger.info("The page is absent, need to translate #{link.inspect}")
84
- needed = true
85
- else
86
- copied += 1
87
- site.static_files << DownloadedFile.new(site, link, html)
88
- added = true
89
- if version.empty?
90
- Jekyll.logger.info("Re-translation not required, since version is empty: #{link.inspect}")
91
- elsif html.include?(marker)
92
- Jekyll.logger.info("No need to translate, the page exists at \
93
- #{link.inspect} (#{html.split.count} words)")
94
- else
95
- Jekyll.logger.info("Re-translation required for #{link.inspect}")
96
- needed = true
49
+ elapsed(Jekyll.logger) do
50
+ site.posts.docs.shuffle.each_with_index do |doc, pos|
51
+ plain = GptTranslate::Plain.new(doc.content).to_s
52
+ layout = doc['layout']
53
+ config['targets'].each do |target|
54
+ link = GptTranslate::Permalink.new(doc, target['permalink']).to_path
55
+ lang = target['language']
56
+ raise 'Language must be defined for each target' if target.nil?
57
+ only = target['only']
58
+ if !only.nil? && layout != only
59
+ Jekyll.logger.debug("Not translating #{link.inspect}, b/c 'only' set to '#{only}'")
60
+ next
97
61
  end
98
- end
99
- if translated >= threshold
100
- Jekyll.logger.info("Page ##{pos} is ignored, we are over the threshold of #{threshold}: #{link}")
101
- elsif needed
102
- gpt = GptTranslate::ChatGPT.new(
103
- key,
104
- model,
105
- target['source'] || config['source'] || 'en',
106
- lang
107
- )
108
- foreign = gpt.translate(
109
- plain,
110
- min: min_chars,
111
- window_length: (config['window_length'] || '2048').to_i
112
- )
62
+ path = File.join(home, lang, doc.basename.gsub(/\.md$/, "-#{lang}.md"))
63
+ FileUtils.mkdir_p(File.dirname(path))
113
64
  File.write(
114
65
  path,
115
66
  [
116
- '',
117
- foreign,
118
- '',
119
- "#{marker} on #{Time.now.strftime('%Y-%m-%d at %H:%M')}\n{: .jekyll-chatgpt-translate}"
120
- ].join("\n"),
121
- mode: 'a+'
67
+ '---',
68
+ "layout: #{target['layout'] || layout}",
69
+ "title: #{doc['title'].to_json}",
70
+ "description: #{doc['description'].to_json}",
71
+ "permalink: #{link.to_json}",
72
+ 'chatgpt-translate:',
73
+ " original-url: #{doc.url.to_json}",
74
+ " language: #{lang.to_json}",
75
+ " model: #{model.to_json}",
76
+ '---'
77
+ ].join("\n")
122
78
  )
123
- site.pages << Jekyll::Page.new(site, site.source, File.dirname(path), File.basename(path))
124
- site.static_files.delete_if { |f| f.is_a?(DownloadedFile) && f.link == link }
125
- added = true
126
- translated += 1
127
- Jekyll.logger.info("Translated via ChatGPT \
128
- in #{(Time.now - pstart).round(2)}s: #{path} (#{File.size(path)} bytes)")
79
+ html = config['no_download'].nil? ? GptTranslate::Ping.new(site, link).download : nil
80
+ needed = false
81
+ added = false
82
+ if html.nil?
83
+ Jekyll.logger.info("The page is absent, need to translate #{link.inspect}")
84
+ needed = true
85
+ else
86
+ copied += 1
87
+ site.static_files << DownloadedFile.new(site, link, html)
88
+ added = true
89
+ if version.empty?
90
+ Jekyll.logger.info("Re-translation not required, since version is empty: #{link.inspect}")
91
+ elsif html.include?(marker)
92
+ Jekyll.logger.info("The page exists at #{link.inspect} (#{html.split.count} words)")
93
+ else
94
+ Jekyll.logger.info("Re-translation required for #{link.inspect}")
95
+ needed = true
96
+ end
97
+ end
98
+ if translated >= threshold
99
+ Jekyll.logger.info("Page ##{pos} is ignored, we are over the threshold of #{threshold}: #{link}")
100
+ elsif needed
101
+ elapsed(Jekyll.logger) do
102
+ gpt = GptTranslate::ChatGPT.new(
103
+ key,
104
+ model,
105
+ target['source'] || config['source'] || 'en',
106
+ lang
107
+ )
108
+ foreign = gpt.translate(
109
+ plain,
110
+ min: min_chars,
111
+ window_length: (config['window_length'] || '2048').to_i
112
+ )
113
+ File.write(
114
+ path,
115
+ [
116
+ '',
117
+ foreign,
118
+ '',
119
+ "#{marker} on #{Time.now.strftime('%Y-%m-%d at %H:%M')}\n{: .jekyll-chatgpt-translate}"
120
+ ].join("\n"),
121
+ mode: 'a+'
122
+ )
123
+ site.pages << Jekyll::Page.new(site, site.source, File.dirname(path), File.basename(path))
124
+ site.static_files.delete_if { |f| f.is_a?(DownloadedFile) && f.link == link }
125
+ added = true
126
+ translated += 1
127
+ throw :"Translated via ChatGPT #{path} (#{File.size(path)} bytes)"
128
+ end
129
+ end
130
+ next unless added
131
+ doc.data['chatgpt-translate'] ||= {}
132
+ doc.data['chatgpt-translate']['model'] ||= model
133
+ doc.data['chatgpt-translate']['urls'] ||= {}
134
+ doc.data['chatgpt-translate']['urls'][lang] = link
129
135
  end
130
- next unless added
131
- doc.data['chatgpt-translate'] ||= {}
132
- doc.data['chatgpt-translate']['model'] ||= model
133
- doc.data['chatgpt-translate']['urls'] ||= {}
134
- doc.data['chatgpt-translate']['urls'][lang] = link
135
136
  end
137
+ throw :"#{translated} page(s) translated and #{copied} page(s) copied"
136
138
  end
137
- Jekyll.logger.info("jekyll-chatgpt-translate #{GptTranslate::VERSION}: \
138
- #{translated} pages translated and #{copied} pages copied in #{(Time.now - start).round(2)}s")
139
139
  end
140
140
 
141
141
  # The file we just downloaded.
@@ -10,7 +10,7 @@ require_relative 'version'
10
10
 
11
11
  # see https://stackoverflow.com/a/6048451/187141
12
12
  require 'openssl'
13
- OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE unless defined?(OpenSSL::SSL::VERIFY_PEER)
13
+ OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
14
14
 
15
15
  # The module we are in.
16
16
  module GptTranslate; end
@@ -4,5 +4,5 @@
4
4
  # SPDX-License-Identifier: MIT
5
5
 
6
6
  module GptTranslate
7
- VERSION = '0.2.0'
7
+ VERSION = '0.3.0'
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-chatgpt-translate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yegor Bugayenko
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: elapsed
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.2'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: humanize
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -70,16 +84,22 @@ dependencies:
70
84
  name: jekyll
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
- - - "~>"
87
+ - - ">="
74
88
  - !ruby/object:Gem::Version
75
- version: '3.10'
89
+ version: '3.0'
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: '5.0'
76
93
  type: :runtime
77
94
  prerelease: false
78
95
  version_requirements: !ruby/object:Gem::Requirement
79
96
  requirements:
80
- - - "~>"
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '3.0'
100
+ - - "<"
81
101
  - !ruby/object:Gem::Version
82
- version: '3.10'
102
+ version: '5.0'
83
103
  - !ruby/object:Gem::Dependency
84
104
  name: json
85
105
  requirement: !ruby/object:Gem::Requirement