123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- package sub_timeline_fixer
- import (
- "errors"
- "fmt"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg"
- "os"
- "sort"
- "strings"
- "time"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/types/subparser"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/gss"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/sub_helper"
- "github.com/ChineseSubFinder/ChineseSubFinder/pkg/vad"
- "github.com/huandu/go-clone"
- )
- type Pipeline struct {
- MaxOffsetSeconds int
- framerateRatios []float64
- }
- func NewPipeline(maxOffsetSeconds int) *Pipeline {
- return &Pipeline{
- MaxOffsetSeconds: maxOffsetSeconds,
- framerateRatios: make([]float64, 0),
- }
- }
- func (p Pipeline) CalcOffsetTime(infoBase, infoSrc *subparser.FileInfo, audioVadList []vad.VADInfo, useGSS bool) (PipeResult, error) {
- baseVADInfo := make([]float64, 0)
- useSubtitleOrAudioAsBase := false
- // 排序
- infoSrc.SortDialogues()
- if infoBase == nil && audioVadList != nil {
- baseVADInfo = vad.GetFloatSlice(audioVadList)
- useSubtitleOrAudioAsBase = true
- } else if infoBase != nil {
- useSubtitleOrAudioAsBase = false
- // 排序
- infoBase.SortDialogues()
- // 解析处 VAD 信息
- baseUnitNew, err := sub_helper.GetVADInfoFeatureFromSubNew(infoBase, 0)
- if err != nil {
- return PipeResult{}, err
- }
- baseVADInfo = baseUnitNew.GetVADFloatSlice()
- } else {
- return PipeResult{}, errors.New("FixTimeline input is error")
- }
- pipeResults := make([]PipeResult, 0)
- /*
- 这里复现 ffsubsync 的思路
- 1. 首先由 getFramerateRatios2Try 得到多个帧数比率的数值,理论上有以下 7 个值:
- 将 frameRateRatio = 1.0 插入到 framerateRatios 这个队列的首位
- [0] 1.0
- [1] 1.001001001001001
- [2] 1.0427093760427095
- [3] 1.0416666666666667
- [4] 0.9989999999999999
- [5] 0.9590399999999999
- [6] 0.96
- 得到一个 framerateRatios 列表
- 2. 计算 base 字幕的 num_frames,以及 frameRateRatio = 1.0 时 src 字幕的 num_frames
- 推断 frame ratio 比率是多少,得到一个,inferred_framerate_ratio_from_length = base / src
- 把这个值插入到 framerateRatios 的尾部也就是第八个元素
- 3. 使用上述的 framerateRatios 作为传入参数,开始 FFT 模块的 fit 计算,得到(分数、偏移)信息,选择分数最大的作为匹配的结论
- */
- // 1.
- framerateRatios := make([]float64, 0)
- framerateRatios = p.getFramerateRatios2Try()
- // 2.
- if useSubtitleOrAudioAsBase == false {
- inferredFramerateRatioFromLength := float64(infoBase.GetNumFrames()) / float64(infoSrc.GetNumFrames())
- framerateRatios = append(framerateRatios, inferredFramerateRatioFromLength)
- }
- // 3.
- fffAligner := NewFFTAligner(p.MaxOffsetSeconds, SampleRate)
- // 需要在这个偏移之下
- maxOffsetSamples := p.MaxOffsetSeconds * SampleRate
- if maxOffsetSamples < 0 {
- maxOffsetSamples = -maxOffsetSamples
- }
- for _, framerateRatio := range framerateRatios {
- /*
- ffsubsync 的 pipeline 有这三个步骤
- 1. parse 解析字幕
- 2. scale 根据帧数比率调整时间轴
- 3. speech_extract 从字幕转换为 VAD 的语音检测信息
- */
- // 外部传入
- // 1. parse 解析字幕
- tmpInfoSrc := clone.Clone(infoSrc).(*subparser.FileInfo)
- // 2. scale 根据帧数比率调整时间轴
- err := tmpInfoSrc.ChangeDialoguesTimeByFramerateRatio(framerateRatio)
- if err != nil {
- // 还原
- println("ChangeDialoguesTimeByFramerateRatio", err)
- tmpInfoSrc = clone.Clone(infoSrc).(*subparser.FileInfo)
- }
- // 3. speech_extract 从字幕转换为 VAD 的语音检测信息
- tmpSrcInfoUnit, err := sub_helper.GetVADInfoFeatureFromSubNew(tmpInfoSrc, 0)
- if err != nil {
- return PipeResult{}, err
- }
- bestOffset, score := fffAligner.Fit(baseVADInfo, tmpSrcInfoUnit.GetVADFloatSlice())
- pipeResult := PipeResult{
- Score: score,
- BestOffset: bestOffset,
- ScaleFactor: framerateRatio,
- ScaledFileInfo: tmpInfoSrc,
- }
- pipeResults = append(pipeResults, pipeResult)
- }
- if useGSS == true {
- // 最后一个才需要额外使用 GSS
- // 使用 GSS
- optFunc := func(framerateRatio float64, isLastIter bool) float64 {
- // 1. parse 解析字幕
- tmpInfoSrc := clone.Clone(infoSrc).(*subparser.FileInfo)
- // 2. scale 根据帧数比率调整时间轴
- err := tmpInfoSrc.ChangeDialoguesTimeByFramerateRatio(framerateRatio)
- if err != nil {
- // 还原
- println("ChangeDialoguesTimeByFramerateRatio", err)
- tmpInfoSrc = clone.Clone(infoSrc).(*subparser.FileInfo)
- }
- // 3. speech_extract 从字幕转换为 VAD 的语音检测信息
- tmpSrcInfoUnit, err := sub_helper.GetVADInfoFeatureFromSubNew(tmpInfoSrc, 0)
- if err != nil {
- return 0
- }
- // 然后进行 base 与 src 匹配计算,将每一次变动 framerateRatio 计算得到的 偏移值和分数进行记录
- bestOffset, score := fffAligner.Fit(baseVADInfo, tmpSrcInfoUnit.GetVADFloatSlice())
- println(fmt.Sprintf("got score %.0f (offset %d) for ratio %.3f", score, bestOffset, framerateRatio))
- // 放到外部的存储中
- if isLastIter == true {
- pipeResult := PipeResult{
- Score: score,
- BestOffset: bestOffset,
- ScaleFactor: framerateRatio,
- ScaledFileInfo: tmpInfoSrc,
- }
- pipeResults = append(pipeResults, pipeResult)
- }
- return -score
- }
- gss.Gss(optFunc, MinFramerateRatio, MaxFramerateRatio, 1e-4, nil)
- }
- // 先进行过滤
- filterPipeResults := make([]PipeResult, 0)
- for _, result := range pipeResults {
- if result.BestOffset < maxOffsetSamples {
- filterPipeResults = append(filterPipeResults, result)
- }
- }
- if len(filterPipeResults) <= 0 {
- return PipeResult{}, errors.New(fmt.Sprintf("AutoFixTimeline failed; you can set 'MaxOffSetTime' > %d", p.MaxOffsetSeconds) +
- fmt.Sprintf(" Or this two subtiles are not fited to this video!"))
- }
- // 从得到的结果里面找到分数最高的
- sort.Sort(PipeResults(filterPipeResults))
- maxPipeResult := filterPipeResults[len(filterPipeResults)-1]
- return maxPipeResult, nil
- }
- // FixSubFileTimeline 这里传入的 scaledInfoSrc 是从 pipeResults 筛选出来的最大分数的 FileInfo
- // infoSrc 是从源文件读取出来的,这样才能正确匹配 Content 中的时间戳
- func (p Pipeline) FixSubFileTimeline(infoSrc, scaledInfoSrc *subparser.FileInfo, inOffsetTime float64, desSaveSubFileFullPath string) (string, error) {
- /*
- 从解析的实例中,正常来说是可以匹配出所有的 Dialogue 对话的 Start 和 End time 的信息
- 然后找到对应的字幕的文件,进行文件内容的替换来做时间轴的校正
- */
- // 偏移时间
- offsetTime := time.Duration(inOffsetTime*1000) * time.Millisecond
- fixContent := scaledInfoSrc.Content
- /*
- 这里进行时间转字符串的时候有一点比较特殊
- 正常来说输出的格式是类似 15:04:05.00
- 那么有个问题,字幕的时间格式是 0:00:12.00, 小时,是个数,除非有跨度到 20 小时的视频,不然小时就应该是个数
- 这就需要一个额外的函数去处理这些情况
- */
- timeFormat := scaledInfoSrc.GetTimeFormat()
- // 如果两个解析出来的对白数量不一致,那么肯定是无法进行下面的匹配的,理论上应该没得问题
- if len(scaledInfoSrc.Dialogues) != len(infoSrc.Dialogues) {
- return "", errors.New("FixSubFileTimeline Not The Same Len: scaledInfoSrc.Dialogues and infoSrc.Dialogues")
- }
- contentReplaceOffsetAll := -1
- for index, scaledSrcOneDialogue := range scaledInfoSrc.Dialogues {
- timeStart, err := pkg.ParseTime(scaledSrcOneDialogue.StartTime)
- if err != nil {
- return "", err
- }
- timeEnd, err := pkg.ParseTime(scaledSrcOneDialogue.EndTime)
- if err != nil {
- return "", err
- }
- fixTimeStart := timeStart.Add(offsetTime)
- fixTimeEnd := timeEnd.Add(offsetTime)
- /*
- 这里有一个梗(之前没有考虑到),理论上这样的替换应该匹配到一句话(正确的那一句),但是有一定几率
- 会把上面修复完的对白时间也算进去替换(匹配上了两句话),导致时间轴无形中被错误延长了
- 那么就需要一个 contentReplaceOffsetAll 去记录现在进行到整个字幕那个偏移未知的替换操作了
- 并不是说一个字幕中不能出现多个一样的“时间字符串”,也就是如果使用 Find 去查找应该也是一定 >= 1 的结果
- 所以才需要 contentReplaceOffsetAll 来记录替换的偏移位置,每次只能变大,而不是变小
- */
- orgStartTimeString := infoSrc.Dialogues[index].StartTime
- orgEndTimeString := infoSrc.Dialogues[index].EndTime
- // contentReplaceOffsetAll 为 -1 的时候那么第一次搜索得到的就一定是可以替换的
- if contentReplaceOffsetAll == -1 {
- contentReplaceOffsetAll = 0
- }
- contentReplaceOffsetNow := strings.Index(fixContent[contentReplaceOffsetAll:], orgStartTimeString)
- if contentReplaceOffsetNow == -1 {
- // 说明没找到,就跳过,虽然理论上不应该会出现
- continue
- }
- contentReplaceOffsetAll += contentReplaceOffsetNow
- fixContent = fixContent[:contentReplaceOffsetAll] + strings.Replace(fixContent[contentReplaceOffsetAll:], orgStartTimeString, pkg.Time2SubTimeString(fixTimeStart, timeFormat), 1)
- // contentReplaceOffsetAll 为 -1 的时候那么第一次搜索得到的就一定是可以替换的
- if contentReplaceOffsetAll == -1 {
- contentReplaceOffsetAll = 0
- }
- contentReplaceOffsetNow = strings.Index(fixContent[contentReplaceOffsetAll:], orgEndTimeString)
- if contentReplaceOffsetNow == -1 {
- // 说明没找到,就跳过,虽然理论上不应该会出现
- continue
- }
- contentReplaceOffsetAll += contentReplaceOffsetNow
- fixContent = fixContent[:contentReplaceOffsetAll] + strings.Replace(fixContent[contentReplaceOffsetAll:], orgEndTimeString, pkg.Time2SubTimeString(fixTimeEnd, timeFormat), 1)
- }
- dstFile, err := os.Create(desSaveSubFileFullPath)
- if err != nil {
- return "", err
- }
- defer func() {
- _ = dstFile.Close()
- }()
- _, err = dstFile.WriteString(fixContent)
- if err != nil {
- return "", err
- }
- return fixContent, nil
- }
- func (p *Pipeline) getFramerateRatios2Try() []float64 {
- if len(p.framerateRatios) > 0 {
- return p.framerateRatios
- }
- p.framerateRatios = append(p.framerateRatios, 1.0)
- p.framerateRatios = append(p.framerateRatios, FramerateRatios...)
- for i := 0; i < len(FramerateRatios); i++ {
- p.framerateRatios = append(p.framerateRatios, 1.0/FramerateRatios[i])
- }
- return p.framerateRatios
- }
- var FramerateRatios = []float64{24. / 23.976, 25. / 23.976, 25. / 24.}
- const MinFramerateRatio = 0.9
- const MaxFramerateRatio = 1.1
- const DefaultMaxOffsetSeconds = 120
- const SampleRate = 100
- type PipeResult struct {
- Score float64
- BestOffset int
- ScaleFactor float64
- ScaledFileInfo *subparser.FileInfo
- }
- // GetOffsetTime 从偏移得到偏移时间
- func (p PipeResult) GetOffsetTime() float64 {
- return float64(p.BestOffset) / 100.0
- }
- type PipeResults []PipeResult
- func (d PipeResults) Len() int {
- return len(d)
- }
- func (d PipeResults) Swap(i, j int) {
- d[i], d[j] = d[j], d[i]
- }
- func (d PipeResults) Less(i, j int) bool {
- return d[i].Score < d[j].Score
- }
|