使用GARbro 提取游戏的
- s文件放入到script目录中
- ogg文件文件放入 voice 中
package main | |
import ( | |
"fmt" | |
"github.com/faiface/beep" | |
"github.com/faiface/beep/vorbis" | |
"github.com/faiface/beep/wav" | |
"golang.org/x/text/encoding/japanese" | |
"golang.org/x/text/transform" | |
"io" | |
"log" | |
"math/rand" | |
"os" | |
"path/filepath" | |
"regexp" | |
"strconv" | |
"strings" | |
) | |
func main() { | |
if len(os.Args) < 2 { | |
log.Println("请输入角色名, 多个角色用 _ 分割") | |
return | |
} | |
characterName := os.Args[1] | |
characterId := -1 | |
if len(os.Args) > 2 { | |
characterId, _ = strconv.Atoi(os.Args[2]) | |
} | |
// 当前目录下面创建DUMMY_WAV文件夹 | |
pwd, _ := os.Getwd() | |
// 当前文件夹新建 output 文件夹 | |
outputDir := filepath.Join(pwd, "output") | |
if _, err := os.Stat(outputDir); os.IsNotExist(err) { | |
_ = os.Mkdir(outputDir, 0755) | |
} | |
dummyWavDir := filepath.Join(outputDir, "DUMMY_WAV") | |
if _, err := os.Stat(dummyWavDir); os.IsNotExist(err) { | |
_ = os.Mkdir(dummyWavDir, 0755) | |
} | |
scripts := getScripts(characterName) | |
log.Println("共有", len(scripts), "条台词, 现在进行音频转换并且写入文件列表...") | |
hashMap := make(map[string]bool) // 去除重复的台词 | |
trainFilelist := make([]string, 0) | |
testFilelist := make([]string, 0) | |
for _, script := range scripts { | |
if _, ok := hashMap[script.Text]; ok { | |
continue | |
} | |
hashMap[script.Text] = true | |
oggFileName := filepath.Join(pwd, "voice", script.Voice+".ogg") | |
wavFileName := filepath.Join(dummyWavDir, script.Voice+".wav") | |
if err := oggToWav(oggFileName, wavFileName); err != nil { | |
log.Println(err) | |
continue | |
} | |
var fileLine string | |
if characterId == -1 { | |
fileLine = fmt.Sprintf("DUMMY_WAV/%s.wav|%s", script.Voice, script.Text) | |
} else { | |
fileLine = fmt.Sprintf("DUMMY_WAV/%s.wav|%d|%s", script.Voice, characterId, script.Text) | |
} | |
// 随机80%的数据用于训练, 20%的数据用于测试 | |
randNum := rand.Intn(100) | |
if randNum < 80 { | |
trainFilelist = append(trainFilelist, fileLine) | |
} else { | |
testFilelist = append(testFilelist, fileLine) | |
} | |
} | |
if err := saveToFile(filepath.Join(outputDir, characterName+"_train_filelist.txt"), strings.Join(trainFilelist, "\n")); err != nil { | |
panic(err) | |
} | |
if err := saveToFile(filepath.Join(outputDir, characterName+"_test_filelist.txt"), strings.Join(testFilelist, "\n")); err != nil { | |
panic(err) | |
} | |
} | |
// Script 台词 | |
type Script struct { | |
Text string | |
Voice string | |
} | |
// getScripts 获取台词 | |
// 遍历 script 文件夹下的s文件, 获取对应角色的所有台词 | |
func getScripts(characterName string) (ret []Script) { | |
ret = make([]Script, 0) | |
// 遍历 script 文件夹下的s文件 | |
pwd, _ := os.Getwd() | |
scriptDir := filepath.Join(pwd, "script") | |
charactersName := strings.Split(characterName, "_") | |
err := filepath.Walk(scriptDir, func(path string, info os.FileInfo, err error) error { | |
if err != nil || info.IsDir() { | |
return nil | |
} | |
if !strings.HasSuffix(info.Name(), ".s") { | |
return nil | |
} | |
// 打开文件 | |
f, err := os.Open(path) | |
if err != nil { | |
return err | |
} | |
log.Println("open file: ", path) | |
defer f.Close() | |
// 编码转换 从JIS转换为UTF-8 | |
reader := transform.NewReader(f, japanese.ShiftJIS.NewDecoder()) | |
content, err := io.ReadAll(reader) | |
if err != nil { | |
return err | |
} | |
// 用正则表达式匹配出所有的台词 | |
// %v_yuu0182 | |
// 【大蔵遊星】 | |
// 「これでいい……?」 | |
// ^message,show:true | |
// ^music01,file:BGM42 | |
re := regexp.MustCompile(`%(.+?)\s+【(.+?)】\s+「([^」]+)」`) | |
matches := re.FindAllStringSubmatch(string(content), -1) | |
for _, match := range matches { | |
if len(match) < 3 { | |
continue | |
} | |
if strContains(match[2], charactersName) { | |
s := Script{ | |
Text: DBC2SBC(strings.TrimSpace(match[3])), | |
Voice: strings.TrimSpace(match[1]), | |
} | |
if s.Voice == "" { | |
continue | |
} | |
// 如果 去掉 …… 后的台词为空, 则不添加 | |
if strings.TrimSpace(strings.ReplaceAll(s.Text, "……", "")) == "" { | |
continue | |
} | |
ret = append(ret, s) | |
fmt.Printf("%s: %s\n", s.Text, s.Voice) | |
} | |
} | |
return nil | |
}) | |
if err != nil { | |
log.Println(err) | |
return | |
} | |
return | |
} | |
func strContains(str string, substr []string) bool { | |
str = strings.TrimSpace(str) | |
for _, s := range substr { | |
if str == s { | |
return true | |
} | |
} | |
return false | |
} | |
func DBC2SBC(s string) string { | |
var strLst []string | |
for _, i := range s { | |
insideCode := i | |
if insideCode == 12288 { | |
insideCode = 32 | |
} else { | |
insideCode -= 65248 | |
} | |
if insideCode < 32 || insideCode > 126 { | |
strLst = append(strLst, string(i)) | |
} else { | |
strLst = append(strLst, string(insideCode)) | |
} | |
} | |
return strings.Join(strLst, "") | |
} | |
func oggToWav(oggFileName, wavFileName string) (err error) { | |
// 如果文件已经存在, 则不再转换 | |
if _, err := os.Stat(wavFileName); err == nil { | |
return nil | |
} | |
f, err := os.Open(oggFileName) | |
if err != nil { | |
return err | |
} | |
defer f.Close() | |
s, format, err := vorbis.Decode(f) | |
if err != nil { | |
return err | |
} | |
defer s.Close() | |
sr := beep.SampleRate(22050) | |
// 保存为 wav 文件 | |
f, err = os.Create(wavFileName) | |
if err != nil { | |
return err | |
} | |
format.SampleRate = sr | |
format.NumChannels = 1 | |
if err = wav.Encode(f, s, format); err != nil { | |
return err | |
} | |
return | |
} | |
func saveToFile(fileName string, content string) error { | |
f, err := os.Create(fileName) | |
if err != nil { | |
return err | |
} | |
defer f.Close() | |
_, _ = io.WriteString(f, content) | |
return nil | |
} |