pipeline.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. package sub_timeline_fixer
  2. import (
  3. "errors"
  4. "fmt"
  5. "github.com/ChineseSubFinder/ChineseSubFinder/pkg"
  6. "os"
  7. "sort"
  8. "strings"
  9. "time"
  10. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/types/subparser"
  11. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/gss"
  12. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/sub_helper"
  13. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/vad"
  14. "github.com/huandu/go-clone"
  15. )
  16. type Pipeline struct {
  17. MaxOffsetSeconds int
  18. framerateRatios []float64
  19. }
  20. func NewPipeline(maxOffsetSeconds int) *Pipeline {
  21. return &Pipeline{
  22. MaxOffsetSeconds: maxOffsetSeconds,
  23. framerateRatios: make([]float64, 0),
  24. }
  25. }
  26. func (p Pipeline) CalcOffsetTime(infoBase, infoSrc *subparser.FileInfo, audioVadList []vad.VADInfo, useGSS bool) (PipeResult, error) {
  27. baseVADInfo := make([]float64, 0)
  28. useSubtitleOrAudioAsBase := false
  29. // 排序
  30. infoSrc.SortDialogues()
  31. if infoBase == nil && audioVadList != nil {
  32. baseVADInfo = vad.GetFloatSlice(audioVadList)
  33. useSubtitleOrAudioAsBase = true
  34. } else if infoBase != nil {
  35. useSubtitleOrAudioAsBase = false
  36. // 排序
  37. infoBase.SortDialogues()
  38. // 解析处 VAD 信息
  39. baseUnitNew, err := sub_helper.GetVADInfoFeatureFromSubNew(infoBase, 0)
  40. if err != nil {
  41. return PipeResult{}, err
  42. }
  43. baseVADInfo = baseUnitNew.GetVADFloatSlice()
  44. } else {
  45. return PipeResult{}, errors.New("FixTimeline input is error")
  46. }
  47. pipeResults := make([]PipeResult, 0)
  48. /*
  49. 这里复现 ffsubsync 的思路
  50. 1. 首先由 getFramerateRatios2Try 得到多个帧数比率的数值,理论上有以下 7 个值:
  51. 将 frameRateRatio = 1.0 插入到 framerateRatios 这个队列的首位
  52. [0] 1.0
  53. [1] 1.001001001001001
  54. [2] 1.0427093760427095
  55. [3] 1.0416666666666667
  56. [4] 0.9989999999999999
  57. [5] 0.9590399999999999
  58. [6] 0.96
  59. 得到一个 framerateRatios 列表
  60. 2. 计算 base 字幕的 num_frames,以及 frameRateRatio = 1.0 时 src 字幕的 num_frames
  61. 推断 frame ratio 比率是多少,得到一个,inferred_framerate_ratio_from_length = base / src
  62. 把这个值插入到 framerateRatios 的尾部也就是第八个元素
  63. 3. 使用上述的 framerateRatios 作为传入参数,开始 FFT 模块的 fit 计算,得到(分数、偏移)信息,选择分数最大的作为匹配的结论
  64. */
  65. // 1.
  66. framerateRatios := make([]float64, 0)
  67. framerateRatios = p.getFramerateRatios2Try()
  68. // 2.
  69. if useSubtitleOrAudioAsBase == false {
  70. inferredFramerateRatioFromLength := float64(infoBase.GetNumFrames()) / float64(infoSrc.GetNumFrames())
  71. framerateRatios = append(framerateRatios, inferredFramerateRatioFromLength)
  72. }
  73. // 3.
  74. fffAligner := NewFFTAligner(p.MaxOffsetSeconds, SampleRate)
  75. // 需要在这个偏移之下
  76. maxOffsetSamples := p.MaxOffsetSeconds * SampleRate
  77. if maxOffsetSamples < 0 {
  78. maxOffsetSamples = -maxOffsetSamples
  79. }
  80. for _, framerateRatio := range framerateRatios {
  81. /*
  82. ffsubsync 的 pipeline 有这三个步骤
  83. 1. parse 解析字幕
  84. 2. scale 根据帧数比率调整时间轴
  85. 3. speech_extract 从字幕转换为 VAD 的语音检测信息
  86. */
  87. // 外部传入
  88. // 1. parse 解析字幕
  89. tmpInfoSrc := clone.Clone(infoSrc).(*subparser.FileInfo)
  90. // 2. scale 根据帧数比率调整时间轴
  91. err := tmpInfoSrc.ChangeDialoguesTimeByFramerateRatio(framerateRatio)
  92. if err != nil {
  93. // 还原
  94. println("ChangeDialoguesTimeByFramerateRatio", err)
  95. tmpInfoSrc = clone.Clone(infoSrc).(*subparser.FileInfo)
  96. }
  97. // 3. speech_extract 从字幕转换为 VAD 的语音检测信息
  98. tmpSrcInfoUnit, err := sub_helper.GetVADInfoFeatureFromSubNew(tmpInfoSrc, 0)
  99. if err != nil {
  100. return PipeResult{}, err
  101. }
  102. bestOffset, score := fffAligner.Fit(baseVADInfo, tmpSrcInfoUnit.GetVADFloatSlice())
  103. pipeResult := PipeResult{
  104. Score: score,
  105. BestOffset: bestOffset,
  106. ScaleFactor: framerateRatio,
  107. ScaledFileInfo: tmpInfoSrc,
  108. }
  109. pipeResults = append(pipeResults, pipeResult)
  110. }
  111. if useGSS == true {
  112. // 最后一个才需要额外使用 GSS
  113. // 使用 GSS
  114. optFunc := func(framerateRatio float64, isLastIter bool) float64 {
  115. // 1. parse 解析字幕
  116. tmpInfoSrc := clone.Clone(infoSrc).(*subparser.FileInfo)
  117. // 2. scale 根据帧数比率调整时间轴
  118. err := tmpInfoSrc.ChangeDialoguesTimeByFramerateRatio(framerateRatio)
  119. if err != nil {
  120. // 还原
  121. println("ChangeDialoguesTimeByFramerateRatio", err)
  122. tmpInfoSrc = clone.Clone(infoSrc).(*subparser.FileInfo)
  123. }
  124. // 3. speech_extract 从字幕转换为 VAD 的语音检测信息
  125. tmpSrcInfoUnit, err := sub_helper.GetVADInfoFeatureFromSubNew(tmpInfoSrc, 0)
  126. if err != nil {
  127. return 0
  128. }
  129. // 然后进行 base 与 src 匹配计算,将每一次变动 framerateRatio 计算得到的 偏移值和分数进行记录
  130. bestOffset, score := fffAligner.Fit(baseVADInfo, tmpSrcInfoUnit.GetVADFloatSlice())
  131. println(fmt.Sprintf("got score %.0f (offset %d) for ratio %.3f", score, bestOffset, framerateRatio))
  132. // 放到外部的存储中
  133. if isLastIter == true {
  134. pipeResult := PipeResult{
  135. Score: score,
  136. BestOffset: bestOffset,
  137. ScaleFactor: framerateRatio,
  138. ScaledFileInfo: tmpInfoSrc,
  139. }
  140. pipeResults = append(pipeResults, pipeResult)
  141. }
  142. return -score
  143. }
  144. gss.Gss(optFunc, MinFramerateRatio, MaxFramerateRatio, 1e-4, nil)
  145. }
  146. // 先进行过滤
  147. filterPipeResults := make([]PipeResult, 0)
  148. for _, result := range pipeResults {
  149. if result.BestOffset < maxOffsetSamples {
  150. filterPipeResults = append(filterPipeResults, result)
  151. }
  152. }
  153. if len(filterPipeResults) <= 0 {
  154. return PipeResult{}, errors.New(fmt.Sprintf("AutoFixTimeline failed; you can set 'MaxOffSetTime' > %d", p.MaxOffsetSeconds) +
  155. fmt.Sprintf(" Or this two subtiles are not fited to this video!"))
  156. }
  157. // 从得到的结果里面找到分数最高的
  158. sort.Sort(PipeResults(filterPipeResults))
  159. maxPipeResult := filterPipeResults[len(filterPipeResults)-1]
  160. return maxPipeResult, nil
  161. }
  162. // FixSubFileTimeline 这里传入的 scaledInfoSrc 是从 pipeResults 筛选出来的最大分数的 FileInfo
  163. // infoSrc 是从源文件读取出来的,这样才能正确匹配 Content 中的时间戳
  164. func (p Pipeline) FixSubFileTimeline(infoSrc, scaledInfoSrc *subparser.FileInfo, inOffsetTime float64, desSaveSubFileFullPath string) (string, error) {
  165. /*
  166. 从解析的实例中,正常来说是可以匹配出所有的 Dialogue 对话的 Start 和 End time 的信息
  167. 然后找到对应的字幕的文件,进行文件内容的替换来做时间轴的校正
  168. */
  169. // 偏移时间
  170. offsetTime := time.Duration(inOffsetTime*1000) * time.Millisecond
  171. fixContent := scaledInfoSrc.Content
  172. /*
  173. 这里进行时间转字符串的时候有一点比较特殊
  174. 正常来说输出的格式是类似 15:04:05.00
  175. 那么有个问题,字幕的时间格式是 0:00:12.00, 小时,是个数,除非有跨度到 20 小时的视频,不然小时就应该是个数
  176. 这就需要一个额外的函数去处理这些情况
  177. */
  178. timeFormat := scaledInfoSrc.GetTimeFormat()
  179. // 如果两个解析出来的对白数量不一致,那么肯定是无法进行下面的匹配的,理论上应该没得问题
  180. if len(scaledInfoSrc.Dialogues) != len(infoSrc.Dialogues) {
  181. return "", errors.New("FixSubFileTimeline Not The Same Len: scaledInfoSrc.Dialogues and infoSrc.Dialogues")
  182. }
  183. contentReplaceOffsetAll := -1
  184. for index, scaledSrcOneDialogue := range scaledInfoSrc.Dialogues {
  185. timeStart, err := pkg.ParseTime(scaledSrcOneDialogue.StartTime)
  186. if err != nil {
  187. return "", err
  188. }
  189. timeEnd, err := pkg.ParseTime(scaledSrcOneDialogue.EndTime)
  190. if err != nil {
  191. return "", err
  192. }
  193. fixTimeStart := timeStart.Add(offsetTime)
  194. fixTimeEnd := timeEnd.Add(offsetTime)
  195. /*
  196. 这里有一个梗(之前没有考虑到),理论上这样的替换应该匹配到一句话(正确的那一句),但是有一定几率
  197. 会把上面修复完的对白时间也算进去替换(匹配上了两句话),导致时间轴无形中被错误延长了
  198. 那么就需要一个 contentReplaceOffsetAll 去记录现在进行到整个字幕那个偏移未知的替换操作了
  199. 并不是说一个字幕中不能出现多个一样的“时间字符串”,也就是如果使用 Find 去查找应该也是一定 >= 1 的结果
  200. 所以才需要 contentReplaceOffsetAll 来记录替换的偏移位置,每次只能变大,而不是变小
  201. */
  202. orgStartTimeString := infoSrc.Dialogues[index].StartTime
  203. orgEndTimeString := infoSrc.Dialogues[index].EndTime
  204. // contentReplaceOffsetAll 为 -1 的时候那么第一次搜索得到的就一定是可以替换的
  205. if contentReplaceOffsetAll == -1 {
  206. contentReplaceOffsetAll = 0
  207. }
  208. contentReplaceOffsetNow := strings.Index(fixContent[contentReplaceOffsetAll:], orgStartTimeString)
  209. if contentReplaceOffsetNow == -1 {
  210. // 说明没找到,就跳过,虽然理论上不应该会出现
  211. continue
  212. }
  213. contentReplaceOffsetAll += contentReplaceOffsetNow
  214. fixContent = fixContent[:contentReplaceOffsetAll] + strings.Replace(fixContent[contentReplaceOffsetAll:], orgStartTimeString, pkg.Time2SubTimeString(fixTimeStart, timeFormat), 1)
  215. // contentReplaceOffsetAll 为 -1 的时候那么第一次搜索得到的就一定是可以替换的
  216. if contentReplaceOffsetAll == -1 {
  217. contentReplaceOffsetAll = 0
  218. }
  219. contentReplaceOffsetNow = strings.Index(fixContent[contentReplaceOffsetAll:], orgEndTimeString)
  220. if contentReplaceOffsetNow == -1 {
  221. // 说明没找到,就跳过,虽然理论上不应该会出现
  222. continue
  223. }
  224. contentReplaceOffsetAll += contentReplaceOffsetNow
  225. fixContent = fixContent[:contentReplaceOffsetAll] + strings.Replace(fixContent[contentReplaceOffsetAll:], orgEndTimeString, pkg.Time2SubTimeString(fixTimeEnd, timeFormat), 1)
  226. }
  227. dstFile, err := os.Create(desSaveSubFileFullPath)
  228. if err != nil {
  229. return "", err
  230. }
  231. defer func() {
  232. _ = dstFile.Close()
  233. }()
  234. _, err = dstFile.WriteString(fixContent)
  235. if err != nil {
  236. return "", err
  237. }
  238. return fixContent, nil
  239. }
  240. func (p *Pipeline) getFramerateRatios2Try() []float64 {
  241. if len(p.framerateRatios) > 0 {
  242. return p.framerateRatios
  243. }
  244. p.framerateRatios = append(p.framerateRatios, 1.0)
  245. p.framerateRatios = append(p.framerateRatios, FramerateRatios...)
  246. for i := 0; i < len(FramerateRatios); i++ {
  247. p.framerateRatios = append(p.framerateRatios, 1.0/FramerateRatios[i])
  248. }
  249. return p.framerateRatios
  250. }
  251. var FramerateRatios = []float64{24. / 23.976, 25. / 23.976, 25. / 24.}
  252. const MinFramerateRatio = 0.9
  253. const MaxFramerateRatio = 1.1
  254. const DefaultMaxOffsetSeconds = 120
  255. const SampleRate = 100
  256. type PipeResult struct {
  257. Score float64
  258. BestOffset int
  259. ScaleFactor float64
  260. ScaledFileInfo *subparser.FileInfo
  261. }
  262. // GetOffsetTime 从偏移得到偏移时间
  263. func (p PipeResult) GetOffsetTime() float64 {
  264. return float64(p.BestOffset) / 100.0
  265. }
  266. type PipeResults []PipeResult
  267. func (d PipeResults) Len() int {
  268. return len(d)
  269. }
  270. func (d PipeResults) Swap(i, j int) {
  271. d[i], d[j] = d[j], d[i]
  272. }
  273. func (d PipeResults) Less(i, j int) bool {
  274. return d[i].Score < d[j].Score
  275. }