diff --git a/go.mod b/go.mod index 2d877826e8e..f8a196116e1 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/lib/pq v1.10.9 github.com/prometheus/client_golang v1.20.5 + github.com/sashabaranov/go-openai v1.32.5 github.com/tdewolff/minify/v2 v2.21.1 github.com/yuin/goldmark v1.7.8 golang.org/x/crypto v0.28.0 diff --git a/go.sum b/go.sum index 2c5936eebf4..012ecfd813c 100644 --- a/go.sum +++ b/go.sum @@ -52,6 +52,8 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/sashabaranov/go-openai v1.32.5 h1:/eNVa8KzlE7mJdKPZDj6886MUzZQjoVHyn0sLvIt5qA= +github.com/sashabaranov/go-openai v1.32.5/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/tdewolff/minify/v2 v2.21.1 h1:AAf5iltw6+KlUvjRNPAPrANIXl3XEJNBBzuZom5iCAM= diff --git a/internal/config/options.go b/internal/config/options.go index 303c8ff3d67..084f0cc0f66 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -89,6 +89,8 @@ const ( defaultWatchdog = true defaultInvidiousInstance = "yewtu.be" defaultWebAuthn = false + defaultChatGTPToken = "" + defaultChatGTPBaseURL = "https://api.openai.com/v1" ) var defaultHTTPClientUserAgent = "Mozilla/5.0 (compatible; Miniflux/" + version.Version + "; +https://miniflux.app)" @@ -177,6 +179,8 @@ type Options struct { invidiousInstance string mediaProxyPrivateKey []byte webAuthn bool + chatgptUrl string + chatgptToken string } // NewOptions returns Options with default values. @@ -256,6 +260,8 @@ func NewOptions() *Options { invidiousInstance: defaultInvidiousInstance, mediaProxyPrivateKey: crypto.GenerateRandomBytes(16), webAuthn: defaultWebAuthn, + chatgptUrl: defaultChatGTPBaseURL, + chatgptToken: defaultChatGTPToken, } } @@ -654,6 +660,13 @@ func (o *Options) FilterEntryMaxAgeDays() int { return o.filterEntryMaxAgeDays } +func (o *Options) GetChatGPTToken() string { + return o.chatgptToken +} +func (o *Options) GetChatGPTUrl() string { + return o.chatgptUrl +} + // SortedOptions returns options as a list of key value pairs, sorted by keys. func (o *Options) SortedOptions(redactSecret bool) []*Option { var keyValues = map[string]interface{}{ diff --git a/internal/config/parser.go b/internal/config/parser.go index b443bae0ef0..827f0b1ef45 100644 --- a/internal/config/parser.go +++ b/internal/config/parser.go @@ -279,6 +279,10 @@ func (p *Parser) parseLines(lines []string) (err error) { p.opts.invidiousInstance = parseString(value, defaultInvidiousInstance) case "WEBAUTHN": p.opts.webAuthn = parseBool(value, defaultWebAuthn) + case "CHAT_GTP_URL": + p.opts.chatgptUrl = parseString(value, defaultChatGTPBaseURL) + case "CHAT_GTP_TOKEN": + p.opts.chatgptToken = parseString(value, defaultChatGTPToken) } } diff --git a/internal/reader/rewrite/rewriter.go b/internal/reader/rewrite/rewriter.go index b577f687517..c961bf1b393 100644 --- a/internal/reader/rewrite/rewriter.go +++ b/internal/reader/rewrite/rewriter.go @@ -4,7 +4,10 @@ package rewrite // import "miniflux.app/v2/internal/reader/rewrite" import ( + "context" + "fmt" "log/slog" + "miniflux.app/v2/internal/config" "strconv" "strings" "text/scanner" @@ -12,6 +15,7 @@ import ( "miniflux.app/v2/internal/model" "miniflux.app/v2/internal/urllib" + openai "github.com/sashabaranov/go-openai" "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -69,6 +73,12 @@ func (rule rule) applyRule(entryURL string, entry *model.Entry) { slog.String("entry_url", entryURL), ) } + case "translators_title": + if len(rule.args) == 1 { + entry.Title = translatorsTitle(entry.Title, rule.args[0]) + } else { + entry.Title = translatorsTitle(entry.Title, "zh-cn") + } case "remove": // Format: remove("#selector > .element, .another") if len(rule.args) >= 1 { @@ -149,3 +159,31 @@ func getPredefinedRewriteRules(entryURL string) string { return "" } + +func translatorsTitle(title string, targetLanguage string) string { + gptConfig := openai.DefaultConfig(config.Opts.GetChatGPTToken()) + gptConfig.BaseURL = config.Opts.GetChatGPTUrl() + client := openai.NewClientWithConfig(gptConfig) + response, err := client.CreateChatCompletion( + context.Background(), + openai.ChatCompletionRequest{ + Model: openai.GPT4oMini, + Messages: []openai.ChatCompletionMessage{ + { + Role: openai.ChatMessageRoleSystem, + Content: fmt.Sprintf(` +You are a super translator and also a news anchor, proficient in various languages. I need you to help me translate the following news headlines into [%s], keeping the original meaning as much as possible. Proper nouns and abbreviations can be left untranslated. Do not say anything outside of the translation; serious penalties will apply if you do.`, targetLanguage), + }, + { + Role: openai.ChatMessageRoleUser, + Content: title, + }, + }, + }) + if err != nil { + slog.Error("Cannot translate title", err) + return title + } + slog.Debug("Translated title", slog.String("title", title), slog.String("translated_title", response.Choices[0].Message.Content)) + return response.Choices[0].Message.Content +}