使用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 | |
| } |