見出し画像

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(&params); 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の運営にも貢献できるのでよろしくお願います。