v/GTT
1
0
mirror of https://github.com/eeeXun/GTT.git synced 2025-05-19 09:10:42 -07:00

feat(translator): add BingTranslate (#16)

This commit is contained in:
Xun 2023-04-22 00:17:18 +08:00 committed by GitHub
parent 7773bb3e8f
commit e0c5569adf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 595 additions and 4 deletions

View File

@ -5,6 +5,7 @@ Google Translate TUI (Originally)
Supported Translator:
[`ApertiumTranslate`](https://www.apertium.org/),
[`ArgosTranslate`](https://translate.argosopentech.com/),
[`BingTranslate`](https://www.bing.com/translator),
[`GoogleTranslate`](https://translate.google.com/),
[`ReversoTranslate`](https://www.reverso.net/text-translation)
@ -48,6 +49,7 @@ See available languages on:
- [Apertium Translate](https://www.apertium.org/) for `ApertiumTranslate`
- [argosopentech/argos-translate](https://github.com/argosopentech/argos-translate#supported-languages) for `ArgosTranslate`
- [Bing language-support](https://learn.microsoft.com/en-us/azure/cognitive-services/translator/language-support#translation) for `BingTranslate`
- [Google Language support](https://cloud.google.com/translate/docs/languages) for `GoogleTranslate`
- [Reverso Translation](https://www.reverso.net/text-translation) for `ReversoTranslate`

View File

@ -22,6 +22,8 @@ func configInit() {
"destination.language.apertiumtranslate": "English",
"source.language.argostranslate": "English",
"destination.language.argostranslate": "English",
"source.language.bingtranslate": "English",
"destination.language.bingtranslate": "English",
"source.language.googletranslate": "English",
"destination.language.googletranslate": "English",
"source.language.reversotranslate": "English",

View File

@ -0,0 +1,320 @@
package bingtranslate
var (
lang = []string{
"Afrikaans",
"Albanian",
"Amharic",
"Auto",
"Arabic",
"Armenian",
"Assamese",
"Azerbaijani",
"Bangla",
"Bashkir",
"Basque",
"Bosnian",
"Bulgarian",
"Cantonese (Traditional)",
"Catalan",
"Chinese (Literary)",
"Chinese Simplified",
"Chinese Traditional",
"Croatian",
"Czech",
"Danish",
"Dari",
"Divehi",
"Dutch",
"English",
"Estonian",
"Faroese",
"Fijian",
"Filipino",
"Finnish",
"French",
"French (Canada)",
"Galician",
"Ganda",
"Georgian",
"German",
"Greek",
"Gujarati",
"Haitian Creole",
"Hausa",
"Hebrew",
"Hindi",
"Hmong Daw",
"Hungarian",
"Icelandic",
"Igbo",
"Indonesian",
"Inuinnaqtun",
"Inuktitut",
"Inuktitut (Latin)",
"Irish",
"Italian",
"Japanese",
"Kannada",
"Kazakh",
"Khmer",
"Kinyarwanda",
"Klingon (Latin)",
"Korean",
"Kurdish (Central)",
"Kurdish (Northern)",
"Kyrgyz",
"Lao",
"Latvian",
"Lingala",
"Lithuanian",
"Macedonian",
"Malagasy",
"Malay",
"Malayalam",
"Maltese",
"Marathi",
"Mongolian (Cyrillic)",
"Mongolian (Traditional)",
"Myanmar (Burmese)",
"Māori",
"Nepali",
"Norwegian",
"Nyanja",
"Odia",
"Pashto",
"Persian",
"Polish",
"Portuguese (Brazil)",
"Portuguese (Portugal)",
"Punjabi",
"Querétaro Otomi",
"Romanian",
"Rundi",
"Russian",
"Samoan",
"Serbian (Cyrillic)",
"Serbian (Latin)",
"Sesotho",
"Sesotho sa Leboa",
"Setswana",
"Shona",
"Slovak",
"Slovenian",
"Somali",
"Spanish",
"Swahili",
"Swedish",
"Tahitian",
"Tamil",
"Tatar",
"Telugu",
"Thai",
"Tibetan",
"Tigrinya",
"Tongan",
"Turkish",
"Turkmen",
"Ukrainian",
"Urdu",
"Uyghur",
"Uzbek (Latin)",
"Vietnamese",
"Welsh",
"Xhosa",
"Yoruba",
"Yucatec Maya",
"Zulu",
}
langCode = map[string]string{
"Afrikaans": "af",
"Albanian": "sq",
"Amharic": "am",
"Auto": "auto-detect",
"Arabic": "ar",
"Armenian": "hy",
"Assamese": "as",
"Azerbaijani": "az",
"Bangla": "bn",
"Bashkir": "ba",
"Basque": "eu",
"Bosnian": "bs",
"Bulgarian": "bg",
"Cantonese (Traditional)": "yue",
"Catalan": "ca",
"Chinese (Literary)": "lzh",
"Chinese Simplified": "zh-Hans",
"Chinese Traditional": "zh-Hant",
"Croatian": "hr",
"Czech": "cs",
"Danish": "da",
"Dari": "prs",
"Divehi": "dv",
"Dutch": "nl",
"English": "en",
"Estonian": "et",
"Faroese": "fo",
"Fijian": "fj",
"Filipino": "fil",
"Finnish": "fi",
"French": "fr",
"French (Canada)": "fr-CA",
"Galician": "gl",
"Ganda": "lug",
"Georgian": "ka",
"German": "de",
"Greek": "el",
"Gujarati": "gu",
"Haitian Creole": "ht",
"Hausa": "ha",
"Hebrew": "he",
"Hindi": "hi",
"Hmong Daw": "mww",
"Hungarian": "hu",
"Icelandic": "is",
"Igbo": "ig",
"Indonesian": "id",
"Inuinnaqtun": "ikt",
"Inuktitut": "iu",
"Inuktitut (Latin)": "iu-Latn",
"Irish": "ga",
"Italian": "it",
"Japanese": "ja",
"Kannada": "kn",
"Kazakh": "kk",
"Khmer": "km",
"Kinyarwanda": "rw",
"Klingon (Latin)": "tlh-Latn",
"Korean": "ko",
"Kurdish (Central)": "ku",
"Kurdish (Northern)": "kmr",
"Kyrgyz": "ky",
"Lao": "lo",
"Latvian": "lv",
"Lingala": "ln",
"Lithuanian": "lt",
"Macedonian": "mk",
"Malagasy": "mg",
"Malay": "ms",
"Malayalam": "ml",
"Maltese": "mt",
"Marathi": "mr",
"Mongolian (Cyrillic)": "mn-Cyrl",
"Mongolian (Traditional)": "mn-Mong",
"Myanmar (Burmese)": "my",
"Māori": "mi",
"Nepali": "ne",
"Norwegian": "nb",
"Nyanja": "nya",
"Odia": "or",
"Pashto": "ps",
"Persian": "fa",
"Polish": "pl",
"Portuguese (Brazil)": "pt",
"Portuguese (Portugal)": "pt-PT",
"Punjabi": "pa",
"Querétaro Otomi": "otq",
"Romanian": "ro",
"Rundi": "run",
"Russian": "ru",
"Samoan": "sm",
"Serbian (Cyrillic)": "sr-Cyrl",
"Serbian (Latin)": "sr-Latn",
"Sesotho": "st",
"Sesotho sa Leboa": "nso",
"Setswana": "tn",
"Shona": "sn",
"Slovak": "sk",
"Slovenian": "sl",
"Somali": "so",
"Spanish": "es",
"Swahili": "sw",
"Swedish": "sv",
"Tahitian": "ty",
"Tamil": "ta",
"Tatar": "tt",
"Telugu": "te",
"Thai": "th",
"Tibetan": "bo",
"Tigrinya": "ti",
"Tongan": "to",
"Turkish": "tr",
"Turkmen": "tk",
"Ukrainian": "uk",
"Urdu": "ur",
"Uyghur": "ug",
"Uzbek (Latin)": "uz",
"Vietnamese": "vi",
"Welsh": "cy",
"Xhosa": "xh",
"Yoruba": "yo",
"Yucatec Maya": "yua",
"Zulu": "zu",
}
voiceName = map[string]string{
"Afrikaans": "af-ZA-AdriNeural",
"Amharic": "am-ET-MekdesNeural",
"Arabic": "ar-SA-HamedNeural",
"Bangla": "bn-IN-TanishaaNeural",
"Bulgarian": "bg-BG-BorislavNeural",
"Cantonese (Traditional)": "zh-HK-HiuGaaiNeural",
"Catalan": "ca-ES-JoanaNeural",
"Chinese Simplified": "zh-CN-XiaoxiaoNeural",
"Chinese Traditional": "zh-CN-XiaoxiaoNeural",
"Croatian": "hr-HR-SreckoNeural",
"Czech": "cs-CZ-AntoninNeural",
"Danish": "da-DK-ChristelNeural",
"Dutch": "nl-NL-ColetteNeural",
"English": "en-US-AriaNeural",
"Estonian": "et-EE-AnuNeural",
"Finnish": "fi-FI-NooraNeural",
"French": "fr-FR-DeniseNeural",
"French (Canada)": "fr-CA-SylvieNeural",
"German": "de-DE-KatjaNeural",
"Greek": "el-GR-NestorasNeural",
"Gujarati": "gu-IN-DhwaniNeural",
"Hebrew": "he-IL-AvriNeural",
"Hindi": "hi-IN-SwaraNeural",
"Hungarian": "hu-HU-TamasNeural",
"Icelandic": "is-IS-GudrunNeural",
"Indonesian": "id-ID-ArdiNeural",
"Irish": "ga-IE-OrlaNeural",
"Italian": "it-IT-DiegoNeural",
"Japanese": "ja-JP-NanamiNeural",
"Kannada": "kn-IN-SapnaNeural",
"Kazakh": "kk-KZ-AigulNeural",
"Khmer": "km-KH-SreymomNeural",
"Korean": "ko-KR-SunHiNeural",
"Lao": "lo-LA-KeomanyNeural",
"Latvian": "lv-LV-EveritaNeural",
"Lithuanian": "lt-LT-OnaNeural",
"Macedonian": "mk-MK-MarijaNeural",
"Malay": "ms-MY-OsmanNeural",
"Malayalam": "ml-IN-SobhanaNeural",
"Maltese": "mt-MT-GraceNeural",
"Marathi": "mr-IN-AarohiNeural",
"Myanmar (Burmese)": "my-MM-NilarNeural",
"Norwegian": "nb-NO-PernilleNeural",
"Pashto": "ps-AF-LatifaNeural",
"Persian": "fa-IR-DilaraNeural",
"Polish": "pl-PL-ZofiaNeural",
"Portuguese (Brazil)": "pt-BR-FranciscaNeural",
"Portuguese (Portugal)": "pt-PT-FernandaNeural",
"Romanian": "ro-RO-EmilNeural",
"Russian": "ru-RU-DariyaNeural",
"Serbian (Cyrillic)": "sr-RS-SophieNeural",
"Slovak": "sk-SK-LukasNeural",
"Slovenian": "sl-SI-RokNeural",
"Spanish": "es-ES-ElviraNeural",
"Swedish": "sv-SE-SofieNeural",
"Tamil": "ta-IN-PallaviNeural",
"Telugu": "te-IN-ShrutiNeural",
"Thai": "th-TH-NiwatNeural",
"Turkish": "tr-TR-EmelNeural",
"Ukrainian": "uk-UA-PolinaNeural",
"Urdu": "ur-IN-GulNeural",
"Uzbek (Latin)": "uz-UZ-MadinaNeural",
"Vietnamese": "vi-VN-NamMinhNeural",
"Welsh": "cy-GB-NiaNeural",
}
)

View File

@ -0,0 +1,214 @@
package bingtranslate
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/eeeXun/gtt/internal/translate/core"
"github.com/hajimehoshi/go-mp3"
"github.com/hajimehoshi/oto/v2"
)
const (
setUpURL = "https://www.bing.com/translator"
textURL = "https://www.bing.com/ttranslatev3?IG=%s&IID=%s"
posURL = "https://www.bing.com/tlookupv3?IG=%s&IID=%s"
ttsURL = "https://www.bing.com/tfettts?IG=%s&IID=%s"
ttsSSML = "<speak version='1.0' xml:lang='%[1]s'><voice xml:lang='%[1]s' xml:gender='Female' name='%s'><prosody rate='-20.00%%'>%s</prosody></voice></speak>"
)
type BingTranslate struct {
*core.Language
*core.TTSLock
core.EngineName
}
type setUpData struct {
ig string
iid string
key string
token string
}
func NewBingTranslate() *BingTranslate {
return &BingTranslate{
Language: core.NewLanguage(),
TTSLock: core.NewTTSLock(),
EngineName: core.NewEngineName("BingTranslate"),
}
}
func (t *BingTranslate) GetAllLang() []string {
return lang
}
func (t *BingTranslate) setUp() (*setUpData, error) {
var data setUpData
res, err := http.Get(setUpURL)
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
bodyStr := string(body)
igData := regexp.MustCompile(`IG:"([^"]+)"`).FindStringSubmatch(bodyStr)
if len(igData) < 2 {
return nil, errors.New(t.GetEngineName() + " IG not found")
}
data.ig = igData[1]
iidData := regexp.MustCompile(`data-iid="([^"]+)`).FindStringSubmatch(bodyStr)
if len(iidData) < 2 {
return nil, errors.New(t.GetEngineName() + " IID not found")
}
data.iid = iidData[1]
params := regexp.MustCompile(`params_AbusePreventionHelper = ([^;]+);`).FindStringSubmatch(bodyStr)
if len(params) < 2 {
return nil, errors.New(t.GetEngineName() + " Key and Token not found")
}
paramsStr := strings.Split(params[1][1:len(params[1])-1], ",")
data.key = paramsStr[0]
data.token = paramsStr[1][1 : len(paramsStr[1])-1]
return &data, nil
}
func (t *BingTranslate) Translate(message string) (translation, definition, partOfSpeech string, err error) {
var data []interface{}
initData, err := t.setUp()
if err != nil {
return "", "", "", err
}
userData := url.Values{
"fromLang": {langCode[t.GetSrcLang()]},
"to": {langCode[t.GetDstLang()]},
"text": {message},
"key": {initData.key},
"token": {initData.token},
}
req, err := http.NewRequest("POST",
fmt.Sprintf(textURL, initData.ig, initData.iid),
strings.NewReader(userData.Encode()),
)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("User-Agent", core.UserAgent)
res, err := http.DefaultClient.Do(req)
if err != nil {
return "", "", "", err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", "", "", err
}
if err = json.Unmarshal(body, &data); err != nil {
return "", "", "", err
}
if len(data) <= 0 {
return "", "", "", errors.New("Translation not found")
}
// translation
translation = fmt.Sprintf("%v",
data[0].(map[string]interface{})["translations"].([]interface{})[0].(map[string]interface{})["text"])
// request part of speech
userData.Del("fromLang")
userData.Add("from", langCode[t.GetSrcLang()])
req, err = http.NewRequest("POST",
fmt.Sprintf(posURL, initData.ig, initData.iid),
strings.NewReader(userData.Encode()),
)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("User-Agent", core.UserAgent)
res, err = http.DefaultClient.Do(req)
if err != nil {
return "", "", "", err
}
body, err = ioutil.ReadAll(res.Body)
if err != nil {
return "", "", "", err
}
// Bing will return the request with list when success.
// Otherwises, it would return map. Then the following err would not be nil.
if err = json.Unmarshal(body, &data); err == nil {
poses := make(posSet)
for _, pos := range data[0].(map[string]interface{})["translations"].([]interface{}) {
pos := pos.(map[string]interface{})
var words posWords
words.target = pos["displayTarget"].(string)
for _, backTranslation := range pos["backTranslations"].([]interface{}) {
backTranslation := backTranslation.(map[string]interface{})
words.add(backTranslation["displayText"].(string))
}
poses.add(pos["posTag"].(string), words)
}
partOfSpeech = poses.format()
}
return translation, definition, partOfSpeech, nil
}
func (t *BingTranslate) PlayTTS(lang, message string) error {
defer t.ReleaseLock()
name, ok := voiceName[lang]
if !ok {
return errors.New(t.GetEngineName() + " does not support text to speech of " + lang)
}
initData, err := t.setUp()
if err != nil {
return err
}
userData := url.Values{
// lang='%s' in ssml should be xx-XX, e.g. en-US
// But xx also works, e.g. en
// So don't do extra work to get xx-XX
"ssml": {fmt.Sprintf(ttsSSML, langCode[lang], name, message)},
"key": {initData.key},
"token": {initData.token},
}
req, err := http.NewRequest("POST",
fmt.Sprintf(ttsURL, initData.ig, initData.iid),
strings.NewReader(userData.Encode()),
)
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("User-Agent", core.UserAgent)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
decoder, err := mp3.NewDecoder(res.Body)
if err != nil {
return err
}
otoCtx, readyChan, err := oto.NewContext(decoder.SampleRate(), 2, 2)
if err != nil {
return err
}
<-readyChan
player := otoCtx.NewPlayer(decoder)
player.Play()
for player.IsPlaying() {
if t.IsStopped() {
return nil
}
time.Sleep(time.Millisecond)
}
if err = player.Close(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,40 @@
package bingtranslate
import (
"fmt"
)
type posWords struct {
target string
backTargets []string
}
func (t *posWords) add(s string) {
t.backTargets = append(t.backTargets, s)
}
type posSet map[string][]posWords
func (set posSet) add(tag string, words posWords) {
set[tag] = append(set[tag], words)
}
func (set posSet) format() (s string) {
for tag := range set {
s += fmt.Sprintf("[%s]\n", tag)
for _, words := range set[tag] {
s += fmt.Sprintf("\t%s:", words.target)
firstWord := true
for _, backTarget := range words.backTargets {
if firstWord {
s += fmt.Sprintf(" %s", backTarget)
firstWord = false
} else {
s += fmt.Sprintf(", %s", backTarget)
}
}
s += "\n"
}
}
return s
}

View File

@ -0,0 +1,5 @@
package core
const (
UserAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
)

View File

@ -19,7 +19,6 @@ import (
const (
textURL = "https://api.reverso.net/translate/v1/translation"
ttsURL = "https://voice.reverso.net/RestPronunciation.svc/v1/output=json/GetVoiceStream/voiceName=%s?voiceSpeed=80&inputText=%s"
userAgent = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36"
)
type ReversoTranslate struct {
@ -65,7 +64,7 @@ func (t *ReversoTranslate) Translate(message string) (translation, definition, p
textURL,
bytes.NewBuffer([]byte(userData)))
req.Header.Add("Content-Type", "application/json")
req.Header.Add("User-Agent", userAgent)
req.Header.Add("User-Agent", core.UserAgent)
res, err := http.DefaultClient.Do(req)
if err != nil {
return "", "", "", err
@ -124,7 +123,7 @@ func (t *ReversoTranslate) PlayTTS(lang, message string) error {
base64.StdEncoding.EncodeToString([]byte(message)),
)
req, _ := http.NewRequest("GET", urlStr, nil)
req.Header.Add("User-Agent", userAgent)
req.Header.Add("User-Agent", core.UserAgent)
res, err := http.DefaultClient.Do(req)
if err != nil {
return err

View File

@ -3,12 +3,19 @@ package translate
import (
"github.com/eeeXun/gtt/internal/translate/apertiumtranslate"
"github.com/eeeXun/gtt/internal/translate/argostranslate"
"github.com/eeeXun/gtt/internal/translate/bingtranslate"
"github.com/eeeXun/gtt/internal/translate/googletranslate"
"github.com/eeeXun/gtt/internal/translate/reversotranslate"
)
var (
AllTranslator = []string{"ApertiumTranslate", "ArgosTranslate", "GoogleTranslate", "ReversoTranslate"}
AllTranslator = []string{
"ApertiumTranslate",
"BingTranslate",
"ArgosTranslate",
"GoogleTranslate",
"ReversoTranslate",
}
)
type Translator interface {
@ -57,6 +64,8 @@ func NewTranslator(name string) Translator {
translator = apertiumtranslate.NewApertiumTranslate()
case "ArgosTranslate":
translator = argostranslate.NewArgosTranslate()
case "BingTranslate":
translator = bingtranslate.NewBingTranslate()
case "GoogleTranslate":
translator = googletranslate.NewGoogleTranslate()
case "ReversoTranslate":