v/GTT
1
0
mirror of https://github.com/eeeXun/GTT.git synced 2025-05-19 01:00:20 -07:00
GTT/internal/translate/bing/translator.go
2023-05-19 19:15:08 +08:00

218 lines
5.6 KiB
Go

package bing
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 Translator struct {
*core.APIKey
*core.Language
*core.TTSLock
core.EngineName
}
type setUpData struct {
ig string
iid string
key string
token string
}
func NewTranslator() *Translator {
return &Translator{
APIKey: new(core.APIKey),
Language: new(core.Language),
TTSLock: core.NewTTSLock(),
EngineName: core.NewEngineName("Bing"),
}
}
func (t *Translator) GetAllLang() []string {
return lang
}
func (t *Translator) setUp() (*setUpData, error) {
data := new(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 *Translator) Translate(message string) (translation *core.Translation, err error) {
translation = new(core.Translation)
var data []interface{}
initData, err := t.setUp()
if err != nil {
return nil, err
}
userData := url.Values{
"fromLang": {langCode[t.GetSrcLang()]},
"to": {langCode[t.GetDstLang()]},
"text": {message},
"key": {initData.key},
"token": {initData.token},
}
req, err := http.NewRequest(http.MethodPost,
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 nil, err
}
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if err = json.Unmarshal(body, &data); err != nil {
return nil, err
}
if len(data) <= 0 {
return nil, errors.New("Translation not found")
}
// translation
translation.TEXT =
data[0].(map[string]interface{})["translations"].([]interface{})[0].(map[string]interface{})["text"].(string)
// request part of speech
userData.Del("fromLang")
userData.Add("from", langCode[t.GetSrcLang()])
req, err = http.NewRequest(http.MethodPost,
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 nil, err
}
body, err = ioutil.ReadAll(res.Body)
if err != nil {
return nil, 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)
}
translation.POS = poses.format()
}
return translation, nil
}
func (t *Translator) 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(http.MethodPost,
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
}