VOICEVOXに動画マニュアルのナレーションを行わせるプログラムが楽しくなってきた
今朝は4時から開発開始です。
ワールドカップはお休みですが昨日から始めたVICEVOX
に動画マニュアルのナレーションを行わせるプログラムの開発が楽しくなってきたためです。
昨日は、
の記事のCLIプログラム
を試してみました。
今朝は、このプログラムを参考にして台本のテキストファイルを読み込んで
会話するプログラムを作ってみました。
台本は、
#玄野武宏,ノーマル
こんにちは、ひまりさん
#冥鳴ひまり,ノーマル
こんにちは、たけひろさん
#WhiteCUL,びえーん
TWSNMPは最高です。
#九州そら,ささやき
それは良かった。
#四国めたん,セクシー
がんばれTWSNMP!
のような感じです。スピーカーとスタイル(喋り方)を#で切り替えてセリフを言ってもらう感じです。今朝作ったプログラム
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"github.com/hajimehoshi/oto"
)
type Params struct {
AccentPhrases []AccentPhrases `json:"accent_phrases"`
SpeedScale float64 `json:"speedScale"`
PitchScale float64 `json:"pitchScale"`
IntonationScale float64 `json:"intonationScale"`
VolumeScale float64 `json:"volumeScale"`
PrePhonemeLength float64 `json:"prePhonemeLength"`
PostPhonemeLength float64 `json:"postPhonemeLength"`
OutputSamplingRate int `json:"outputSamplingRate"`
OutputStereo bool `json:"outputStereo"`
Kana string `json:"kana"`
}
type Mora struct {
Text string `json:"text"`
Consonant *string `json:"consonant"`
ConsonantLength *float64 `json:"consonant_length"`
Vowel string `json:"vowel"`
VowelLength float64 `json:"vowel_length"`
Pitch float64 `json:"pitch"`
}
type AccentPhrases struct {
Moras []Mora `json:"moras"`
Accent int `json:"accent"`
PauseMora *Mora `json:"pause_mora"`
IsInterrogative bool `json:"is_interrogative"`
}
type Speaker struct {
Name string `json:"name"`
SpeakerUUID string `json:"speaker_uuid"`
Styles []Styles `json:"styles"`
Version string `json:"version"`
}
type Styles struct {
ID int `json:"id"`
Name string `json:"name"`
}
var speakers = []Speaker{}
var url = "http://localhost:50021"
var script = ""
var list = false
var play = false
func getSpeakers() {
resp, err := http.Get(url + "/speakers")
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&speakers); err != nil {
log.Fatal(err)
}
}
func getQuery(id int, text string) (*Params, error) {
req, err := http.NewRequest("POST", url+"/audio_query", nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Add("speaker", strconv.Itoa(id))
q.Add("text", text)
req.URL.RawQuery = q.Encode()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var params *Params
if err := json.NewDecoder(resp.Body).Decode(¶ms); err != nil {
return nil, err
}
return params, nil
}
func synth(id int, params *Params) ([]byte, error) {
b, err := json.MarshalIndent(params, "", " ")
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", url+"/synthesis", bytes.NewReader(b))
if err != nil {
return nil, err
}
req.Header.Add("Accept", "audio/wav")
req.Header.Add("Content-Type", "application/json")
q := req.URL.Query()
q.Add("speaker", strconv.Itoa(id))
req.URL.RawQuery = q.Encode()
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
buff := bytes.NewBuffer(nil)
if _, err := io.Copy(buff, resp.Body); err != nil {
return nil, err
}
return buff.Bytes(), nil
}
func playback(params *Params, b []byte) error {
ch := 1
if params.OutputStereo {
ch = 2
}
ctx, err := oto.NewContext(params.OutputSamplingRate, ch, 2, 3200)
if err != nil {
return err
}
defer ctx.Close()
p := ctx.NewPlayer()
if _, err := io.Copy(p, bytes.NewReader(b)); err != nil {
return err
}
if err := p.Close(); err != nil {
return err
}
return nil
}
func main() {
flag.StringVar(&url, "url", "http://localhost:50021", "api url")
flag.StringVar(&script, "s", "", "input script(txt or pptx")
flag.BoolVar(&list, "l", false, "list speaker")
flag.BoolVar(&play, "p", false, "play")
flag.Parse()
getSpeakers()
if list {
showSpeakers()
return
}
st := time.Now()
playScript(script)
log.Printf("time=%v", time.Since(st))
}
type config struct {
speaker int
style int
speed float64
intonation float64
volume float64
pitch float64
}
func playScript(file string) {
lines, err := readScript(file)
if err != nil {
log.Fatalf("readScript err=%v", err)
}
cfg := getConfig("")
for i, l := range lines {
l = strings.TrimSpace(l)
log.Printf("%d %s\n", i, l)
if strings.HasPrefix(l, "#") {
cfg = getConfig(l)
} else if strings.HasPrefix(l, "$") {
// Page
} else if l != "" {
if err := speak(cfg, l); err != nil {
log.Println(err)
}
}
}
}
func speak(cfg config, l string) error {
spk := speakers[cfg.speaker]
spkID := spk.Styles[cfg.style].ID
params, err := getQuery(spkID, l)
if err != nil {
log.Fatal(err)
}
params.SpeedScale = cfg.speed
params.PitchScale = cfg.pitch
params.IntonationScale = cfg.intonation
params.VolumeScale = cfg.volume
b, err := synth(spkID, params)
if err != nil {
return err
}
if play {
return playback(params, b[44:])
}
return nil
}
func getConfig(l string) config {
ret := config{
speaker: 0,
style: 0,
speed: 1.0,
intonation: 1.0,
volume: 1.0,
pitch: 0.0,
}
l = strings.ReplaceAll(l, "#", "")
p := strings.Split(l, ",")
if len(p) < 2 {
return ret
}
speaker, style, err := findSpeaker(strings.TrimSpace(p[0]), strings.TrimSpace(p[1]))
if err != nil {
log.Println(err)
return ret
}
ret.speaker = speaker
ret.style = style
if len(p) < 6 {
return ret
}
if v, err := strconv.ParseFloat(p[2], 64); err == nil {
ret.speed = v
}
if v, err := strconv.ParseFloat(p[3], 64); err == nil {
ret.intonation = v
}
if v, err := strconv.ParseFloat(p[4], 64); err == nil {
ret.volume = v
}
if v, err := strconv.ParseFloat(p[5], 64); err == nil {
ret.pitch = v
}
return ret
}
func showSpeakers() {
for _, s := range speakers {
for _, t := range s.Styles {
fmt.Printf("%s,%s\n", s.Name, t.Name)
}
}
}
func findSpeaker(name, style string) (int, int, error) {
for i, s := range speakers {
if name == s.Name {
for j, t := range s.Styles {
if style == t.Name {
return i, j, nil
}
}
}
}
return -1, -1, fmt.Errorf("speaker not found name=%s style=%s", name, style)
}
func readScript(filename string) ([]string, error) {
b, err := os.ReadFile(filename)
if err != nil {
return []string{}, err
}
return strings.Split(string(b), "\n"), nil
}
に読み込ませると会話しているようになります。
実際に動くとなんだか面白くなってきました。
この会話を音声ファイルにつなぎ合わせて出力できれば、動作マニュアルのナレーションに組み込めます。
そのためには音声(WAV)ファイルの処理をGO言語でおこなうパッケージの学習が必要です。
なんとかできそうですが、空白で間合いを開けたいとか、台本はパワーポイントのファイルから読みたいとか、いろいろ欲がでてきました。
明日に続く
開発のための諸経費(機材、Appleの開発者、サーバー運用)に利用します。 ソフトウェアのマニュアルをnoteの記事で提供しています。 サポートによりnoteの運営にも貢献できるのでよろしくお願います。