mirror of
https://github.com/fumiama/ahsai.git
synced 2026-06-05 07:40:23 +08:00
init
This commit is contained in:
52
README.md
52
README.md
@@ -1,2 +1,54 @@
|
||||
# ahsai
|
||||
AH Soft フリーテキスト音声合成 demo API
|
||||
|
||||
## demo
|
||||
Just run go test to hear the voice below
|
||||
|
||||
<audio src='/test.wav' controls><a href='/test.wav'>こんにちは、世界</a></audio>
|
||||
|
||||
```go
|
||||
package ahsai
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAPI(t *testing.T) {
|
||||
s := NewSpeaker()
|
||||
err := s.SetName("東北イタコ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u, err := s.Speak("こんにちは、世界")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(u)
|
||||
err = PlayOgg(u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = SaveOggToWav(u, "test.wav")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## supported speakers
|
||||
- 琴葉葵
|
||||
- 琴葉茜
|
||||
- 紲星あかり
|
||||
- 吉田くん
|
||||
- 東北ずん子
|
||||
- 月読アイ
|
||||
- 月読ショウタ
|
||||
- 民安ともえ
|
||||
- 結月ゆかり
|
||||
- 水奈瀬コウ
|
||||
- 京町セイカ
|
||||
- 東北きりたん
|
||||
- 桜乃そら
|
||||
- 東北イタコ
|
||||
- ついなちゃん標準語
|
||||
- ついなちゃん関西弁
|
||||
- 伊織弓鶴
|
||||
- 音街ウナ
|
||||
85
api.go
Normal file
85
api.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package ahsai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
const api = "https://cloud.ai-j.jp/demo/aitalk2webapi_nop.php?callback=callback&speaker_id=%d&text=%s&ext=ogg&volume=%.1f&speed=%.1f&pitch=%.1f&range=%.1f&anger=%.1f&sadness=%.1f&joy=%.1f&_=%d"
|
||||
|
||||
var (
|
||||
speakers = map[string]uint32{
|
||||
"琴葉葵": 551,
|
||||
"琴葉茜": 552,
|
||||
"紲星あかり": 554,
|
||||
"吉田くん": 1201,
|
||||
"東北ずん子": 1202,
|
||||
"月読アイ": 1203,
|
||||
"月読ショウタ": 1204,
|
||||
"民安ともえ": 1205,
|
||||
"結月ゆかり": 1206,
|
||||
"水奈瀬コウ": 1207,
|
||||
"京町セイカ": 1208,
|
||||
"東北きりたん": 1209,
|
||||
"桜乃そら": 1210,
|
||||
"東北イタコ": 1211,
|
||||
"ついなちゃん標準語": 1212,
|
||||
"ついなちゃん関西弁": 1213,
|
||||
"伊織弓鶴": 1214,
|
||||
"音街ウナ": 2006,
|
||||
}
|
||||
)
|
||||
|
||||
type Speaker struct {
|
||||
id uint32
|
||||
Volume, Speed, Pitch, Range, Anger, Sadness, Joy float32
|
||||
}
|
||||
|
||||
func NewSpeaker() (s Speaker) {
|
||||
s.Volume = 1
|
||||
s.Speed = 1
|
||||
s.Pitch = 1
|
||||
s.Range = 1
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrTextTooLong 文本超过 100 字
|
||||
ErrTextTooLong = errors.New("text too long")
|
||||
// ErrNoSuchSpeaker 查无此人
|
||||
ErrNoSuchSpeaker = errors.New("no such speaker")
|
||||
)
|
||||
|
||||
func (s *Speaker) SetName(name string) error {
|
||||
id, ok := speakers[name]
|
||||
if !ok {
|
||||
return ErrNoSuchSpeaker
|
||||
}
|
||||
s.id = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Speak text, return ogg url
|
||||
func (s *Speaker) Speak(text string) (string, error) {
|
||||
if len([]rune(text)) > 100 {
|
||||
return "", ErrTextTooLong
|
||||
}
|
||||
resp, err := http.Get(fmt.Sprintf(api, s.id, url.QueryEscape(text), s.Volume, s.Speed, s.Pitch, s.Range, s.Anger, s.Sadness, s.Joy, time.Now().UnixMilli()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data = bytes.TrimPrefix(data, []byte(`callback({"url":"`))
|
||||
data = bytes.TrimSuffix(data, []byte(`"})`))
|
||||
data = bytes.ReplaceAll(data, []byte(`\/`), []byte(`/`))
|
||||
return "https:" + string(data), nil
|
||||
}
|
||||
24
api_test.go
Normal file
24
api_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package ahsai
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAPI(t *testing.T) {
|
||||
s := NewSpeaker()
|
||||
err := s.SetName("東北イタコ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
u, err := s.Speak("こんにちは、世界")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(u)
|
||||
err = PlayOgg(u)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = SaveOggToWav(u, "test.wav")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
16
go.mod
Normal file
16
go.mod
Normal file
@@ -0,0 +1,16 @@
|
||||
module github.com/fumiama/ahsai
|
||||
|
||||
go 1.17
|
||||
|
||||
require github.com/faiface/beep v1.1.0
|
||||
|
||||
require (
|
||||
github.com/hajimehoshi/oto v0.7.1 // indirect
|
||||
github.com/jfreymuth/oggvorbis v1.0.1 // indirect
|
||||
github.com/jfreymuth/vorbis v1.0.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 // indirect
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 // indirect
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 // indirect
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect
|
||||
)
|
||||
39
go.sum
Normal file
39
go.sum
Normal file
@@ -0,0 +1,39 @@
|
||||
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
github.com/faiface/beep v1.1.0 h1:A2gWP6xf5Rh7RG/p9/VAW2jRSDEGQm5sbOb38sf5d4c=
|
||||
github.com/faiface/beep v1.1.0/go.mod h1:6I8p6kK2q4opL/eWb+kAkk38ehnTunWeToJB+s51sT4=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs=
|
||||
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
|
||||
github.com/go-audio/wav v1.0.0/go.mod h1:3yoReyQOsiARkvPl3ERCi8JFjihzG6WhjYpZCf5zAWE=
|
||||
github.com/hajimehoshi/go-mp3 v0.3.0/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
|
||||
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
|
||||
github.com/hajimehoshi/oto v0.7.1 h1:I7maFPz5MBCwiutOrz++DLdbr4rTzBsbBuV2VpgU9kk=
|
||||
github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6EepeVGnos=
|
||||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
||||
github.com/jfreymuth/oggvorbis v1.0.1 h1:NT0eXBgE2WHzu6RT/6zcb2H10Kxj6Fm3PccT0LE6bqw=
|
||||
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
|
||||
github.com/jfreymuth/vorbis v1.0.0 h1:SmDf783s82lIjGZi8EGUUaS7YxPHgRj4ZXW/h7rUi7U=
|
||||
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
|
||||
github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s=
|
||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
||||
github.com/mewkiz/flac v1.0.7/go.mod h1:yU74UH277dBUpqxPouHSQIar3G1X/QIclVbFahSd1pU=
|
||||
github.com/mewkiz/pkg v0.0.0-20190919212034-518ade7978e2/go.mod h1:3E2FUC/qYUfM8+r9zAwpeHJzqRVVMIYnpzD/clwWxyA=
|
||||
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=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8 h1:idBdZTd9UioThJp8KpM/rTSinK/ChZFBE43/WtIy8zg=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.0.0-20190220214146-31aff87c08e9/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067 h1:KYGJGHOQy8oSi1fDlSpcZF0+juKwk/hEMv5SiwHogR0=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6 h1:vyLBGJPIl9ZYbcQFM2USFmJBK6KI+t+z6jL0lbwjrnc=
|
||||
golang.org/x/mobile v0.0.0-20190415191353-3e0bab5405d6/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190429190828-d89cdac9e872/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
76
ogg.go
Normal file
76
ogg.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package ahsai
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/faiface/beep"
|
||||
"github.com/faiface/beep/speaker"
|
||||
"github.com/faiface/beep/vorbis"
|
||||
"github.com/faiface/beep/wav"
|
||||
)
|
||||
|
||||
func SaveOggToWav(u, path string) error {
|
||||
resp, err := http.Get(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, format, err := vorbis.Decode(resp.Body)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
tmp := make([][2]float64, 1024)
|
||||
_, _ = s.Stream(tmp)
|
||||
sum := (tmp[0][0] + tmp[0][1]) / 2
|
||||
for sum > 1e-32 || sum < -1e-32 {
|
||||
_, _ = s.Stream(tmp)
|
||||
sum = (tmp[0][0] + tmp[0][1]) / 2
|
||||
for j := 1; j < 1024; j++ {
|
||||
sum += (tmp[j][0] + tmp[j][1]) / 2
|
||||
sum /= 2
|
||||
}
|
||||
}
|
||||
return wav.Encode(f, s, format)
|
||||
}
|
||||
|
||||
func PlayOgg(u string) error {
|
||||
resp, err := http.Get(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, format, err := vorbis.Decode(resp.Body)
|
||||
if err != nil {
|
||||
resp.Body.Close()
|
||||
return err
|
||||
}
|
||||
defer s.Close()
|
||||
tmp := make([][2]float64, 1024)
|
||||
_, _ = s.Stream(tmp)
|
||||
sum := (tmp[0][0] + tmp[0][1]) / 2
|
||||
for sum > 1e-32 || sum < -1e-32 {
|
||||
_, _ = s.Stream(tmp)
|
||||
sum = (tmp[0][0] + tmp[0][1]) / 2
|
||||
for j := 1; j < 1024; j++ {
|
||||
sum += (tmp[j][0] + tmp[j][1]) / 2
|
||||
sum /= 2
|
||||
}
|
||||
}
|
||||
err = speaker.Init(format.SampleRate, format.SampleRate.N(time.Second/32))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
done := make(chan struct{})
|
||||
speaker.Play(beep.Seq(s, beep.Callback(func() {
|
||||
done <- struct{}{}
|
||||
})))
|
||||
<-done
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user