SubTimelineFixerHelperEx.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. package sub_timeline_fixer
  2. import (
  3. "errors"
  4. "math"
  5. "os"
  6. "github.com/ChineseSubFinder/ChineseSubFinder/pkg"
  7. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/types/subparser"
  8. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/ffmpeg_helper"
  9. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/logic/sub_parser/ass"
  10. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/logic/sub_parser/srt"
  11. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/settings"
  12. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/sub_parser_hub"
  13. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/sub_timeline_fixer"
  14. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/vad"
  15. "github.com/emirpasic/gods/maps/treemap"
  16. "github.com/emirpasic/gods/utils"
  17. "github.com/sirupsen/logrus"
  18. )
  19. type SubTimelineFixerHelperEx struct {
  20. log *logrus.Logger
  21. ffmpegHelper *ffmpeg_helper.FFMPEGHelper
  22. subParserHub *sub_parser_hub.SubParserHub
  23. timelineFixPipeLine *sub_timeline_fixer.Pipeline
  24. fixerConfig settings.TimelineFixerSettings
  25. needDownloadFFMPeg bool
  26. }
  27. func NewSubTimelineFixerHelperEx(log *logrus.Logger, fixerConfig settings.TimelineFixerSettings) *SubTimelineFixerHelperEx {
  28. fixerConfig.Check()
  29. return &SubTimelineFixerHelperEx{
  30. log: log,
  31. ffmpegHelper: ffmpeg_helper.NewFFMPEGHelper(log),
  32. subParserHub: sub_parser_hub.NewSubParserHub(log, ass.NewParser(log), srt.NewParser(log)),
  33. timelineFixPipeLine: sub_timeline_fixer.NewPipeline(fixerConfig.MaxOffsetTime),
  34. fixerConfig: fixerConfig,
  35. needDownloadFFMPeg: false,
  36. }
  37. }
  38. // Check 是否安装了 ffmpeg 和 ffprobe
  39. func (s *SubTimelineFixerHelperEx) Check() bool {
  40. version, err := s.ffmpegHelper.Version()
  41. if err != nil {
  42. s.needDownloadFFMPeg = false
  43. s.log.Errorln("Need Install ffmpeg and ffprobe !")
  44. return false
  45. }
  46. s.needDownloadFFMPeg = true
  47. s.log.Infoln(version)
  48. return true
  49. }
  50. func (s *SubTimelineFixerHelperEx) Process(videoFileFullPath, srcSubFPath string) error {
  51. if s.needDownloadFFMPeg == false {
  52. s.log.Errorln("Need Install ffmpeg and ffprobe, Can't Do TimeLine Fix")
  53. return nil
  54. }
  55. var infoSrc *subparser.FileInfo
  56. var pipeResultMax sub_timeline_fixer.PipeResult
  57. bProcess := false
  58. bok := false
  59. var ffmpegInfo *ffmpeg_helper.FFMPEGInfo
  60. var err error
  61. // 先尝试获取内置字幕的信息
  62. bok, ffmpegInfo, err = s.ffmpegHelper.ExportFFMPEGInfo(videoFileFullPath, ffmpeg_helper.Subtitle)
  63. if err != nil {
  64. return err
  65. }
  66. if bok == false {
  67. return errors.New("SubTimelineFixerHelperEx.Process.ExportFFMPEGInfo = false Subtitle -- " + videoFileFullPath)
  68. }
  69. // 这个需要提前考虑,如果只有一个内置的字幕,且这个字幕的大小小于 2kb,那么认为这个字幕是有问题的,就直接切换到 audio 校正
  70. oneSubAndIsError := false
  71. if len(ffmpegInfo.SubtitleInfoList) == 1 {
  72. fi, err := os.Stat(ffmpegInfo.SubtitleInfoList[0].FullPath)
  73. if err != nil {
  74. oneSubAndIsError = true
  75. } else {
  76. if fi.Size() <= 2048 {
  77. oneSubAndIsError = true
  78. }
  79. }
  80. }
  81. // 内置的字幕,这里只列举一种格式出来,其实会有一个字幕的 srt 和 ass 两种格式都导出存在
  82. if ffmpegInfo.SubtitleInfoList == nil || len(ffmpegInfo.SubtitleInfoList) <= 0 || oneSubAndIsError == true {
  83. if ffmpegInfo.AudioInfoList == nil || len(ffmpegInfo.AudioInfoList) == 0 {
  84. return errors.New("SubTimelineFixerHelperEx.Process.ExportFFMPEGInfo Can`t Find SubTitle And Audio To Export -- " + videoFileFullPath)
  85. }
  86. // 如果内置字幕没有,那么就需要尝试获取音频信息
  87. bok, ffmpegInfo, err = s.ffmpegHelper.ExportFFMPEGInfo(videoFileFullPath, ffmpeg_helper.Audio)
  88. if err != nil {
  89. return err
  90. }
  91. if bok == false {
  92. return errors.New("SubTimelineFixerHelperEx.Process.ExportFFMPEGInfo = false Audio -- " + videoFileFullPath)
  93. }
  94. // 使用音频进行时间轴的校正
  95. if len(ffmpegInfo.AudioInfoList) <= 0 {
  96. s.log.Warnln("Can`t find audio info, skip time fix --", videoFileFullPath)
  97. return nil
  98. }
  99. bProcess, infoSrc, pipeResultMax, err = s.ProcessByAudioFile(ffmpegInfo.AudioInfoList[0].FullPath, srcSubFPath)
  100. if err != nil {
  101. return err
  102. }
  103. } else {
  104. // 使用内置的字幕进行时间轴的校正,这里需要考虑一个问题,内置的字幕可能是有问题的(先考虑一种,就是字幕的长度不对,是一小段的)
  105. // 那么就可以比较多个内置字幕的大小选择大的去使用
  106. // 如果有多个内置的字幕,还是要判断下的,选体积最大的那个吧
  107. fileSizes := treemap.NewWith(utils.Int64Comparator)
  108. for index, info := range ffmpegInfo.SubtitleInfoList {
  109. fi, err := os.Stat(info.FullPath)
  110. if err != nil {
  111. fileSizes.Put(0, index)
  112. } else {
  113. fileSizes.Put(fi.Size(), index)
  114. }
  115. }
  116. _, index := fileSizes.Max()
  117. baseSubFPath := ffmpegInfo.SubtitleInfoList[index.(int)].FullPath
  118. bProcess, infoSrc, pipeResultMax, err = s.ProcessBySubFile(baseSubFPath, srcSubFPath)
  119. if err != nil {
  120. return err
  121. }
  122. }
  123. // 开始调整字幕时间轴
  124. if bProcess == false || math.Abs(pipeResultMax.GetOffsetTime()) < s.fixerConfig.MinOffset {
  125. s.log.Infoln("Skip TimeLine Fix -- OffsetTime:", pipeResultMax.GetOffsetTime(), srcSubFPath)
  126. return nil
  127. }
  128. err = s.changeTimeLineAndSave(infoSrc, pipeResultMax, srcSubFPath)
  129. if err != nil {
  130. return err
  131. }
  132. s.log.Infoln("TimeLine Fix -- Score:", pipeResultMax.Score, srcSubFPath)
  133. s.log.Infoln("Fix Offset:", pipeResultMax.GetOffsetTime(), srcSubFPath)
  134. s.log.Infoln("BackUp Org SubFile:", pipeResultMax.GetOffsetTime(), srcSubFPath+sub_timeline_fixer.BackUpExt)
  135. return nil
  136. }
  137. func (s *SubTimelineFixerHelperEx) ProcessBySubFileInfo(infoBase *subparser.FileInfo, infoSrc *subparser.FileInfo) (bool, *subparser.FileInfo, sub_timeline_fixer.PipeResult, error) {
  138. // ---------------------------------------------------------------------------------------
  139. pipeResult, err := s.timelineFixPipeLine.CalcOffsetTime(infoBase, infoSrc, nil, false)
  140. if err != nil {
  141. return false, nil, sub_timeline_fixer.PipeResult{}, err
  142. }
  143. return true, infoSrc, pipeResult, nil
  144. }
  145. func (s *SubTimelineFixerHelperEx) ProcessBySubFile(baseSubFileFPath, srcSubFileFPath string) (bool, *subparser.FileInfo, sub_timeline_fixer.PipeResult, error) {
  146. bFind, infoBase, err := s.subParserHub.DetermineFileTypeFromFile(baseSubFileFPath)
  147. if err != nil {
  148. return false, nil, sub_timeline_fixer.PipeResult{}, err
  149. }
  150. if bFind == false {
  151. s.log.Warnln("ProcessBySubFile.DetermineFileTypeFromFile sub not match --", baseSubFileFPath)
  152. return false, nil, sub_timeline_fixer.PipeResult{}, nil
  153. }
  154. bFind, infoSrc, err := s.subParserHub.DetermineFileTypeFromFile(srcSubFileFPath)
  155. if err != nil {
  156. return false, nil, sub_timeline_fixer.PipeResult{}, err
  157. }
  158. if bFind == false {
  159. s.log.Warnln("ProcessBySubFile.DetermineFileTypeFromFile sub not match --", srcSubFileFPath)
  160. return false, nil, sub_timeline_fixer.PipeResult{}, nil
  161. }
  162. return s.ProcessBySubFileInfo(infoBase, infoSrc)
  163. }
  164. func (s *SubTimelineFixerHelperEx) ProcessByAudioVAD(audioVADInfos []vad.VADInfo, infoSrc *subparser.FileInfo) (bool, *subparser.FileInfo, sub_timeline_fixer.PipeResult, error) {
  165. // ---------------------------------------------------------------------------------------
  166. pipeResult, err := s.timelineFixPipeLine.CalcOffsetTime(nil, infoSrc, audioVADInfos, false)
  167. if err != nil {
  168. return false, nil, sub_timeline_fixer.PipeResult{}, err
  169. }
  170. return true, infoSrc, pipeResult, nil
  171. }
  172. func (s *SubTimelineFixerHelperEx) ProcessByAudioFile(baseAudioFileFPath, srcSubFileFPath string) (bool, *subparser.FileInfo, sub_timeline_fixer.PipeResult, error) {
  173. audioVADInfos, err := vad.GetVADInfoFromAudio(vad.AudioInfo{
  174. FileFullPath: baseAudioFileFPath,
  175. SampleRate: 16000,
  176. BitDepth: 16,
  177. }, true)
  178. if err != nil {
  179. return false, nil, sub_timeline_fixer.PipeResult{}, err
  180. }
  181. bFind, infoSrc, err := s.subParserHub.DetermineFileTypeFromFile(srcSubFileFPath)
  182. if err != nil {
  183. return false, nil, sub_timeline_fixer.PipeResult{}, err
  184. }
  185. if bFind == false {
  186. s.log.Warnln("ProcessByAudioFile.DetermineFileTypeFromFile sub not match --", srcSubFileFPath)
  187. return false, nil, sub_timeline_fixer.PipeResult{}, nil
  188. }
  189. return s.ProcessByAudioVAD(audioVADInfos, infoSrc)
  190. }
  191. func (s *SubTimelineFixerHelperEx) IsVideoCanExportSubtitleAndAudio(videoFileFullPath string) (bool, *ffmpeg_helper.FFMPEGInfo, []vad.VADInfo, *subparser.FileInfo, error) {
  192. // 先尝试获取内置字幕的信息
  193. bok, ffmpegInfo, err := s.ffmpegHelper.ExportFFMPEGInfo(videoFileFullPath, ffmpeg_helper.SubtitleAndAudio)
  194. if err != nil {
  195. return false, nil, nil, nil, err
  196. }
  197. if bok == false {
  198. return false, nil, nil, nil, nil
  199. }
  200. // ---------------------------------------------------------------------------------------
  201. // 音频
  202. if len(ffmpegInfo.AudioInfoList) <= 0 {
  203. return false, nil, nil, nil, nil
  204. }
  205. audioVADInfos, err := vad.GetVADInfoFromAudio(vad.AudioInfo{
  206. FileFullPath: ffmpegInfo.AudioInfoList[0].FullPath,
  207. SampleRate: 16000,
  208. BitDepth: 16,
  209. }, true)
  210. if err != nil {
  211. return false, nil, nil, nil, err
  212. }
  213. // ---------------------------------------------------------------------------------------
  214. // 字幕
  215. if len(ffmpegInfo.SubtitleInfoList) <= 0 {
  216. return false, nil, nil, nil, nil
  217. }
  218. // 使用内置的字幕进行时间轴的校正,这里需要考虑一个问题,内置的字幕可能是有问题的(先考虑一种,就是字幕的长度不对,是一小段的)
  219. // 那么就可以比较多个内置字幕的大小选择大的去使用
  220. // 如果有多个内置的字幕,还是要判断下的,选体积最大的那个吧
  221. fileSizes := treemap.NewWith(utils.Int64Comparator)
  222. for index, info := range ffmpegInfo.SubtitleInfoList {
  223. fi, err := os.Stat(info.FullPath)
  224. if err != nil {
  225. fileSizes.Put(0, index)
  226. } else {
  227. fileSizes.Put(fi.Size(), index)
  228. }
  229. }
  230. _, index := fileSizes.Max()
  231. baseSubFPath := ffmpegInfo.SubtitleInfoList[index.(int)].FullPath
  232. bFind, infoBase, err := s.subParserHub.DetermineFileTypeFromFile(baseSubFPath)
  233. if err != nil {
  234. return false, nil, nil, nil, err
  235. }
  236. if bFind == false {
  237. return false, nil, nil, nil, nil
  238. }
  239. // ---------------------------------------------------------------------------------------
  240. return true, ffmpegInfo, audioVADInfos, infoBase, nil
  241. }
  242. func (s *SubTimelineFixerHelperEx) IsMatchBySubFile(ffmpegInfo *ffmpeg_helper.FFMPEGInfo, audioVADInfos []vad.VADInfo, infoBase *subparser.FileInfo, srcSubFileFPath string, config CompareConfig) (bool, *MatchResult, error) {
  243. bFind, srcBase, err := s.subParserHub.DetermineFileTypeFromFile(srcSubFileFPath)
  244. if err != nil {
  245. return false, nil, err
  246. }
  247. if bFind == false {
  248. return false, nil, nil
  249. }
  250. // ---------------------------------------------------------------------------------------
  251. // 音频
  252. s.log.Infoln("IsMatchBySubFile:", srcSubFileFPath)
  253. bProcess, _, pipeResultMaxAudio, err := s.ProcessByAudioVAD(audioVADInfos, srcBase)
  254. if err != nil {
  255. return false, nil, err
  256. }
  257. if bProcess == false {
  258. return false, nil, nil
  259. }
  260. // ---------------------------------------------------------------------------------------
  261. // 字幕
  262. bProcess, _, pipeResultMaxSub, err := s.ProcessBySubFileInfo(infoBase, srcBase)
  263. if err != nil {
  264. return false, nil, err
  265. }
  266. if bProcess == false {
  267. return false, nil, nil
  268. }
  269. targetSubEndTime := pkg.Time2SecondNumber(srcBase.GetEndTime())
  270. matchResult := &MatchResult{
  271. VideoDuration: ffmpegInfo.Duration,
  272. TargetSubEndTime: targetSubEndTime,
  273. AudioCompareScore: pipeResultMaxAudio.Score,
  274. AudioCompareOffsetTime: pipeResultMaxAudio.GetOffsetTime(),
  275. SubCompareScore: pipeResultMaxSub.Score,
  276. SubCompareOffsetTime: pipeResultMaxSub.GetOffsetTime(),
  277. }
  278. // ---------------------------------------------------------------------------------------
  279. // 分数需要大于某个值
  280. if pipeResultMaxAudio.Score < config.MinScore || pipeResultMaxSub.Score < config.MinScore {
  281. return false, matchResult, nil
  282. }
  283. // 两种方式获取到的时间轴的偏移量,差值需要在一定范围内
  284. if math.Abs(pipeResultMaxAudio.GetOffsetTime()-pipeResultMaxSub.GetOffsetTime()) > config.OffsetRange {
  285. return false, matchResult, nil
  286. }
  287. // ---------------------------------------------------------------------------------------
  288. // 待判断的字幕的时间长度要小于等于视频的总长度
  289. if targetSubEndTime > ffmpegInfo.Duration {
  290. return false, matchResult, nil
  291. }
  292. // ---------------------------------------------------------------------------------------
  293. // 两个对比字幕的对白数量不能超过 10%
  294. minRage := float64(len(infoBase.Dialogues)) * config.DialoguesDifferencePercentage
  295. if math.Abs(float64(len(srcBase.Dialogues)-len(infoBase.Dialogues))) > minRage {
  296. return false, matchResult, nil
  297. }
  298. return true, matchResult, nil
  299. }
  300. func (s *SubTimelineFixerHelperEx) changeTimeLineAndSave(infoSrc *subparser.FileInfo, pipeResult sub_timeline_fixer.PipeResult, desSubSaveFPath string) error {
  301. /*
  302. 修复的字幕先存放到缓存目录,然后需要把原有的字幕进行“备份”,改名,然后再替换过来
  303. */
  304. subFileName := desSubSaveFPath + sub_timeline_fixer.TmpExt
  305. if pkg.IsFile(subFileName) == true {
  306. err := os.Remove(subFileName)
  307. if err != nil {
  308. return err
  309. }
  310. }
  311. _, err := s.timelineFixPipeLine.FixSubFileTimeline(infoSrc, pipeResult.ScaledFileInfo, pipeResult.GetOffsetTime(), subFileName)
  312. if err != nil {
  313. return err
  314. }
  315. if pkg.IsFile(desSubSaveFPath+sub_timeline_fixer.BackUpExt) == true {
  316. err = os.Remove(desSubSaveFPath + sub_timeline_fixer.BackUpExt)
  317. if err != nil {
  318. return err
  319. }
  320. }
  321. err = os.Rename(desSubSaveFPath, desSubSaveFPath+sub_timeline_fixer.BackUpExt)
  322. if err != nil {
  323. return err
  324. }
  325. err = os.Rename(subFileName, desSubSaveFPath)
  326. if err != nil {
  327. return err
  328. }
  329. return nil
  330. }
  331. type CompareConfig struct {
  332. MinScore float64 // 最低的分数
  333. OffsetRange float64 // 偏移量的范围
  334. DialoguesDifferencePercentage float64 // 两个字幕的对白字幕差异百分比
  335. }
  336. type MatchResult struct {
  337. VideoDuration float64 // 视频的时长
  338. TargetSubEndTime float64 // 目标字幕的结束时间
  339. AudioCompareScore float64 // 音频的对比分数
  340. AudioCompareOffsetTime float64 // 音频的对比偏移量
  341. SubCompareScore float64 // 字幕的对比分数
  342. SubCompareOffsetTime float64 // 字幕的对比偏移量
  343. }