Improved TTS

This commit is contained in:
Jan-Lukas Else 2021-09-07 22:16:28 +02:00
parent d48f4f556a
commit 74ea0f5576
16 changed files with 451 additions and 11 deletions

View File

@ -69,6 +69,23 @@ func (a *goBlog) serveEditorPost(w http.ResponseWriter, r *http.Request) {
a.editorMicropubPost(w, req, false)
case "upload":
a.editorMicropubPost(w, r, true)
case "tts":
parsedURL, err := url.Parse(r.FormValue("url"))
if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
post, err := a.getPost(parsedURL.Path)
if err != nil {
a.serveError(w, r, err.Error(), http.StatusBadRequest)
return
}
if err = a.createPostTTSAudio(post); err != nil {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, post.Path, http.StatusFound)
return
default:
a.serveError(w, r, "Unknown editoraction", http.StatusBadRequest)
}

9
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/cretz/bine v0.2.0
github.com/dchest/captcha v0.0.0-20200903113550-03f5f0333e1f
github.com/dgraph-io/ristretto v0.1.0
github.com/dmulholl/mp3lib v1.0.0
github.com/elnormous/contenttype v1.0.0
github.com/go-chi/chi/v5 v5.0.4
github.com/go-fed/httpsig v1.1.0
@ -43,7 +44,7 @@ require (
// master
github.com/yuin/goldmark-emoji v1.0.2-0.20210607094911-0487583eca38
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
golang.org/x/net v0.0.0-20210825183410-e898025ed96a
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
willnorris.com/go/microformats v1.1.1
@ -65,7 +66,7 @@ require (
github.com/lestrrat-go/strftime v1.0.5 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mitchellh/mapstructure v1.4.1 // indirect
github.com/pelletier/go-toml v1.9.3 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/snabb/diagio v1.0.0 // indirect
@ -74,8 +75,8 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/tdewolff/parse/v2 v2.5.19 // indirect
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b // indirect
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 // indirect
golang.org/x/text v0.3.7 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/ini.v1 v1.63.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

16
go.sum
View File

@ -95,6 +95,8 @@ github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/Lu
github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA=
github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dmulholl/mp3lib v1.0.0 h1:PZq24kJBIk5zIxi/t6Qp8/EOAbAqThyrUCpkUKLBeWQ=
github.com/dmulholl/mp3lib v1.0.0/go.mod h1:4RoA+iht/khfwxmH1ieoxZTzYVbb0am/zdvFkyGRr6I=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/elnormous/contenttype v1.0.0 h1:cTLou7K7uQMsPEmRiTJosAznsPcYuoBmXMrFAf86t2A=
@ -290,8 +292,9 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@ -459,8 +462,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a h1:bRuuGXV8wwSdGTB+CtJf+FjgO1APK1CoO39T4BN/XBw=
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg=
golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -529,8 +532,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b h1:S7hKs0Flbq0bbc9xgYt4stIEG1zNDFqyrPwAX2Wj/sE=
golang.org/x/sys v0.0.0-20210902050250-f475640dd07b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34 h1:GkvMjFtXUmahfDtashnc1mnrCtuBVcwse5QV2lUk/tI=
golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -708,8 +711,9 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.63.0 h1:2t0h8NA59dpVQpa5Yh8cIcR6nHAeBIEk0zlLVqfw4N4=
gopkg.in/ini.v1 v1.63.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -86,6 +86,9 @@ func (a *goBlog) safeRenderMarkdownAsHTML(source string) template.HTML {
}
func (a *goBlog) renderText(s string) string {
if s == "" {
return ""
}
h, err := a.renderMarkdown(s, false)
if err != nil {
return ""
@ -94,6 +97,9 @@ func (a *goBlog) renderText(s string) string {
}
func (a *goBlog) renderMdTitle(s string) string {
if s == "" {
return ""
}
var buffer bytes.Buffer
err := a.titleMd.Convert([]byte(s), &buffer)
if err != nil {

View File

@ -255,6 +255,7 @@ details summary {
nav,
#post-actions,
#tts,
#related,
#interactions,
#posteditactions,

136
pkgs/mp3merge/mp3merge.go Normal file
View File

@ -0,0 +1,136 @@
package mp3merge
import (
"errors"
"io"
"os"
"path/filepath"
"github.com/dmulholl/mp3lib"
"github.com/thoas/go-funk"
)
// Inspired by https://github.com/dmulholl/mp3cat/blob/2ec1e4fe4d995ebd41bf1887b3cab8e2a569b3d4/mp3cat.go
// Merge multiple mp3 files into one file
func MergeMP3(out string, in []string) error {
var totalFrames, totalBytes uint32
var firstBitRate int
var isVBR bool
// Check if output file is included in input files
if funk.ContainsString(in, out) {
return errors.New("the list of input files includes the output file")
}
// Create the output file.
if err := os.MkdirAll(filepath.Dir(out), os.ModePerm); err != nil {
return err
}
outfile, err := os.Create(out)
if err != nil {
return err
}
// Loop over the input files and append their MP3 frames to the output file.
for _, inpath := range in {
infile, err := os.Open(inpath)
if err != nil {
return err
}
isFirstFrame := true
for {
// Read the next frame from the input file.
frame := mp3lib.NextFrame(infile)
if frame == nil {
break
}
// Skip the first frame if it's a VBR header.
if isFirstFrame {
isFirstFrame = false
if mp3lib.IsXingHeader(frame) || mp3lib.IsVbriHeader(frame) {
continue
}
}
// If we detect more than one bitrate we'll need to add a VBR
// header to the output file.
if firstBitRate == 0 {
firstBitRate = frame.BitRate
} else if frame.BitRate != firstBitRate {
isVBR = true
}
// Write the frame to the output file.
_, err := outfile.Write(frame.RawBytes)
if err != nil {
return err
}
totalFrames += 1
totalBytes += uint32(len(frame.RawBytes))
}
_ = infile.Close()
}
_ = outfile.Close()
// If we detected multiple bitrates, prepend a VBR header to the file.
if isVBR {
err = addXingHeader(out, totalFrames, totalBytes)
if err != nil {
return err
}
}
return nil
}
// Prepend an Xing VBR header to the specified MP3 file.
func addXingHeader(filepath string, totalFrames, totalBytes uint32) error {
tmpSuffix := ".mp3merge.tmp"
outputFile, err := os.Create(filepath + tmpSuffix)
if err != nil {
return err
}
inputFile, err := os.Open(filepath)
if err != nil {
return err
}
xingHeader := mp3lib.NewXingHeader(totalFrames, totalBytes)
_, err = outputFile.Write(xingHeader.RawBytes)
if err != nil {
return err
}
_, err = io.Copy(outputFile, inputFile)
if err != nil {
return err
}
_ = outputFile.Close()
_ = inputFile.Close()
err = os.Remove(filepath)
if err != nil {
return err
}
err = os.Rename(filepath+tmpSuffix, filepath)
if err != nil {
return err
}
return nil
}

View File

@ -232,6 +232,36 @@ func (a *goBlog) deletePostFromDb(path string) (*post, error) {
return p, nil
}
func (db *database) replacePostParam(path, param string, values []string) error {
// Lock post creation
db.pcm.Lock()
defer db.pcm.Unlock()
// Build SQL
var sqlBuilder strings.Builder
var sqlArgs = []interface{}{dbNoCache}
// Start transaction
sqlBuilder.WriteString("begin;")
// Delete old post
sqlBuilder.WriteString("delete from post_parameters where path = ? and parameter = ?;")
sqlArgs = append(sqlArgs, path, param)
// Insert new post parameters
for _, value := range values {
if value != "" {
sqlBuilder.WriteString("insert into post_parameters (path, parameter, value) values (?, ?, ?);")
sqlArgs = append(sqlArgs, path, param, value)
}
}
// Commit transaction
sqlBuilder.WriteString("commit;")
// Execute
if _, err := db.exec(sqlBuilder.String(), sqlArgs...); err != nil {
return err
}
// Update FTS index
db.rebuildFTSIndex()
return nil
}
type postsRequestConfig struct {
search string
blog string

View File

@ -242,3 +242,7 @@ func (p *post) Old() bool {
}
return pubDate.AddDate(1, 0, 0).Before(time.Now())
}
func (p *post) TTS() string {
return p.firstParameter("tts")
}

View File

@ -213,6 +213,7 @@ details summary > *:first-child {
nav,
#post-actions,
#tts,
#related,
#interactions,
#posteditactions,

View File

@ -5,9 +5,9 @@
let speech = window.speechSynthesis
if (speech) {
speakButton.classList.remove('hide')
speakButton.onclick = startSpeak
speakButton.textContent = speakButton.dataset.speak
speakButton.classList.remove('hide')
}
function query(selector) {

View File

@ -0,0 +1,36 @@
(function () {
let speakButton = query('#speakBtn')
let ttsAudio = query('#tts-audio')
let init = false
speakButton.textContent = speakButton.dataset.speak
speakButton.classList.remove('hide')
speakButton.addEventListener('click', function () {
if (!init) {
init = true
query('#tts').classList.remove('hide')
ttsAudio.play()
} else {
togglePlay()
}
})
let isPlaying = false
function togglePlay() {
isPlaying ? ttsAudio.pause() : ttsAudio.play()
}
ttsAudio.onplaying = function() {
isPlaying = true
speakButton.textContent = speakButton.dataset.stopspeak
}
ttsAudio.onpause = function() {
isPlaying = false
speakButton.textContent = speakButton.dataset.speak
}
function query(selector) {
return document.querySelector(selector)
}
})()

View File

@ -33,6 +33,11 @@
<input type="hidden" name="url" value="{{ .Canonical }}">
<input type="submit" value="{{ string .Blog.Lang "delete" }}" class="confirm" data-confirmmessage="{{ string .Blog.Lang "confirmdelete" }}">
</form>
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}">
<input type="hidden" name="editoraction" value="tts">
<input type="hidden" name="url" value="{{ .Canonical }}">
<input type="submit" value="{{ string .Blog.Lang "gentts" }}">
</form>
<script defer src="{{ asset "js/formconfirm.js" }}"></script>
</div>
{{ end }}

View File

@ -4,6 +4,15 @@
<a id="translateBtn" href="https://translate.google.com/translate?u={{ absolute .Data.Path }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "translate" }}</a>&nbsp;
<script defer src="{{ asset "js/translate.js" }}"></script>
<button id="speakBtn" class="hide" data-speak="{{ string .Blog.Lang "speak" }}" data-stopspeak="{{ string .Blog.Lang "stopspeak" }}"></button>
{{ if .Data.TTS }}
<script defer src="{{ asset "js/tts.js" }}"></script>
{{ else }}
<script defer src="{{ asset "js/speak.js" }}"></script>
{{ end }}
</div>
{{ if .Data.TTS }}
<div class="p hide" id="tts">
<audio controls preload=none id="tts-audio"><source src="{{ .Data.TTS }}"/></audio>
</div>
{{ end }}
{{ end }}

View File

@ -17,6 +17,7 @@ editor: "Editor"
editorpostdesc: "Leere Parameter werden automatisch entfernt. Mehr mögliche Parameter: %s. Mögliche Zustände für `%s`: %s."
emailopt: "E-Mail (optional)"
fileuses: "Datei-Verwendungen"
gentts: "Text-To-Speech-Audio erzeugen"
interactions: "Interaktionen & Kommentare"
interactionslabel: "Hast du eine Antwort hierzu veröffentlicht? Füge hier die URL ein."
likeof: "Gefällt mir von"

View File

@ -21,6 +21,7 @@ editorpostdesc: "Empty parameters are removed automatically. More possible param
emailopt: "Email (optional)"
feed: "Feed"
fileuses: "file uses"
gentts: "Generate Text-To-Speech audio"
indieauth: "IndieAuth"
interactions: "Interactions & Comments"
interactionslabel: "Have you published a response to this? Paste the URL here."

188
tts.go Normal file
View File

@ -0,0 +1,188 @@
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"path/filepath"
"strings"
"sync"
"unicode"
"go.goblog.app/app/pkgs/mp3merge"
)
func (a *goBlog) createPostTTSAudio(p *post) error {
// Get required values
lang := a.cfg.Blogs[p.Blog].Lang
if lang == "" {
lang = "en"
}
text := a.renderMdTitle(p.Title()) + "\n\n" + cleanHTMLText(string(a.postHtml(p, false)))
// Generate audio file
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
return err
}
defer func() {
_ = os.RemoveAll(tmpDir)
}()
outputFileName := filepath.Join(tmpDir, "audio.mp3")
err = a.createTTSAudio(lang, text, outputFileName)
if err != nil {
return err
}
// Save new audio file
file, err := os.Open(outputFileName)
if err != nil {
return err
}
fileHash, err := getSHA256(file)
if err != nil {
return err
}
loc, err := a.saveMediaFile(fileHash+".mp3", file)
if err != nil {
return err
}
// Set post parameter
if loc != "" {
err = a.db.replacePostParam(p.Path, "tts", []string{loc})
if err != nil {
return err
}
}
return nil
}
func (a *goBlog) createTTSAudio(lang, text, outputFile string) error {
// Create temporary directory
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
return err
}
defer func() {
_ = os.RemoveAll(tmpDir)
}()
// Split text
textParts := []string{}
var textPartBuilder strings.Builder
textRunes := []rune(text)
for i, r := range textRunes {
textPartBuilder.WriteRune(r)
newText := false
if strings.ContainsRune(",.:!?)", r) && i+1 < len(textRunes) && unicode.IsSpace(textRunes[i+1]) {
newText = true
} else if r == '\n' {
newText = true
} else if textPartBuilder.Len() > 500 && unicode.IsSpace(r) {
newText = true
}
if newText {
textParts = append(textParts, textPartBuilder.String())
textPartBuilder.Reset()
}
}
textParts = append(textParts, textPartBuilder.String())
// Start request for every text part
allFiles := []string{}
var wg sync.WaitGroup
var ttsErr error
ctx, cancel := context.WithCancel(context.Background())
for _, s := range textParts {
s := strings.TrimSpace(s)
if s == "" {
continue
}
fileName := filepath.Join(tmpDir, generateRandomString(10)+".mp3")
allFiles = append(allFiles, fileName)
wg.Add(1)
go func() {
defer wg.Done()
err := a.downloadTTSAudio(ctx, lang, s, fileName)
if err != nil && ttsErr == nil {
ttsErr = err
cancel()
}
}()
}
wg.Wait()
cancel()
if ttsErr != nil {
return ttsErr
}
// Merge MP3s
if err = mp3merge.MergeMP3(outputFile, allFiles); err != nil {
return err
}
return nil
}
func (a *goBlog) downloadTTSAudio(ctx context.Context, lang, text, outputFile string) error {
// Check parameters
if lang == "" {
return errors.New("language not provided")
}
if text == "" {
return errors.New("empty text")
}
if outputFile == "" {
return errors.New("output file not provided")
}
// Encode params
ttsUrlVals := url.Values{}
ttsUrlVals.Set("client", "tw-ob")
ttsUrlVals.Set("tl", lang)
ttsUrlVals.Set("q", strings.TrimSpace(text))
// Create request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://translate.google.com/translate_tts?"+ttsUrlVals.Encode(), nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0")
// Do request
res, err := a.httpClient.Do(req)
if err != nil {
return err
}
defer func() {
_, _ = io.Copy(io.Discard, res.Body)
_ = res.Body.Close()
}()
if res.StatusCode != http.StatusOK {
return fmt.Errorf("TTS: got status: %s, text: %s", res.Status, text)
}
// Save response
if err = os.MkdirAll(path.Dir(outputFile), os.ModePerm); err != nil {
return err
}
out, err := os.Create(outputFile)
if err != nil {
return err
}
defer func() {
_ = out.Close()
}()
if _, err = io.Copy(out, res.Body); err != nil {
return err
}
return nil
}