SubTimelineFixerHelperEx.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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, processType ProcessType) 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. oneSubAndIsError := false
  62. if processType == ProcessTypeAuto || processType == ProcessTypeBySubFile {
  63. // 自动 || 字幕
  64. // 先尝试获取内置字幕的信息
  65. bok, ffmpegInfo, err = s.ffmpegHelper.ExportFFMPEGInfo(videoFileFullPath, ffmpeg_helper.Subtitle)
  66. if err != nil {
  67. return err
  68. }
  69. if bok == false {
  70. return errors.New("SubTimelineFixerHelperEx.Process.ExportFFMPEGInfo = false Subtitle -- " + videoFileFullPath)
  71. }
  72. // 这个需要提前考虑,如果只有一个内置的字幕,且这个字幕的大小小于 2kb,那么认为这个字幕是有问题的,就直接切换到 audio 校正
  73. if len(ffmpegInfo.SubtitleInfoList) == 1 {
  74. fi, err := os.Stat(ffmpegInfo.SubtitleInfoList[0].FullPath)
  75. if err != nil {
  76. oneSubAndIsError = true
  77. } else {
  78. if fi.Size() <= 2048 {
  79. oneSubAndIsError = true
  80. }
  81. }
  82. }
  83. if processType == ProcessTypeBySubFile && oneSubAndIsError == true {
  84. // 如果是字幕,那么就需要判断下字幕的大小,如果字幕的大小小于 2kb,那么认为这个字幕是有问题的,且是指定用内置字幕矫正,那么就直接错误
  85. return errors.New("SubTimelineFixerHelperEx.Process.ExportFFMPEGInfo Subtitle == 1 and size <= 2048 -- " + videoFileFullPath)
  86. }
  87. if processType == ProcessTypeBySubFile {
  88. // 使用内置的字幕进行时间轴的校正,这里需要考虑一个问题,内置的字幕可能是有问题的(先考虑一种,就是字幕的长度不对,是一小段的)
  89. // 那么就可以比较多个内置字幕的大小选择大的去使用
  90. // 如果有多个内置的字幕,还是要判断下的,选体积最大的那个吧
  91. fileSizes := treemap.NewWith(utils.Int64Comparator)
  92. for index, info := range ffmpegInfo.SubtitleInfoList {
  93. fi, err := os.Stat(info.FullPath)
  94. if err != nil {
  95. fileSizes.Put(0, index)
  96. } else {
  97. fileSizes.Put(fi.Size(), index)
  98. }
  99. }
  100. _, index := fileSizes.Max()
  101. baseSubFPath := ffmpegInfo.SubtitleInfoList[index.(int)].FullPath
  102. bProcess, infoSrc, pipeResultMax, err = s.ProcessBySubFile(baseSubFPath, srcSubFPath)
  103. if err != nil {
  104. return err
  105. }
  106. }
  107. }
  108. if processType == ProcessTypeAuto || processType == ProcessTypeByAudioFile {
  109. // 自动 || 音频
  110. if processType == ProcessTypeAuto && oneSubAndIsError == false {
  111. // 如果是自动,且没有这个问题,那么就不需要再次获取了
  112. } else {
  113. // 如果内置字幕没有,那么就需要尝试获取音频信息
  114. bok, ffmpegInfo, err = s.ffmpegHelper.ExportFFMPEGInfo(videoFileFullPath, ffmpeg_helper.Audio)
  115. if err != nil {
  116. return err
  117. }
  118. if bok == false {
  119. return errors.New("SubTimelineFixerHelperEx.Process.ExportFFMPEGInfo = false Audio -- " + videoFileFullPath)
  120. }
  121. // 使用音频进行时间轴的校正
  122. if len(ffmpegInfo.AudioInfoList) <= 0 {
  123. s.log.Warnln("Can`t find audio info, skip time fix --", videoFileFullPath)
  124. return nil
  125. }
  126. bProcess, infoSrc, pipeResultMax, err = s.ProcessByAudioFile(ffmpegInfo.AudioInfoList[0].FullPath, srcSubFPath)
  127. if err != nil {
  128. return err
  129. }
  130. }
  131. } else if processType == ProcessTypeBySubFile {
  132. // 字幕
  133. } else {
  134. return errors.New("SubTimelineFixerHelperEx.Process processType is error")
  135. }
  136. // 内置的字幕,这里只列举一种格式出来,其实会有一个字幕的 srt 和 ass 两种格式都导出存在
  137. if (ffmpegInfo.SubtitleInfoList == nil || len(ffmpegInfo.SubtitleInfoList) <= 0) && (ffmpegInfo.AudioInfoList == nil || len(ffmpegInfo.AudioInfoList) == 0) {
  138. return errors.New("SubTimelineFixerHelperEx.Process.ExportFFMPEGInfo Can`t Find SubTitle And Audio To Export -- " + videoFileFullPath)
  139. }
  140. // 开始调整字幕时间轴
  141. if bProcess == false || math.Abs(pipeResultMax.GetOffsetTime()) < s.fixerConfig.MinOffset {
  142. s.log.Infoln("Skip TimeLine Fix, MinOffset:", s.fixerConfig.MinOffset, " -- NowOffsetTime:", pipeResultMax.GetOffsetTime(), srcSubFPath)
  143. return nil
  144. }
  145. s.log.Infoln("TimeLine Fix -- Score:", pipeResultMax.Score(), srcSubFPath)
  146. s.log.Infoln("Fix Offset:", pipeResultMax.GetOffsetTime(), srcSubFPath)
  147. err = s.changeTimeLineAndSave(infoSrc, pipeResultMax, srcSubFPath)
  148. if err != nil {
  149. return err
  150. }
  151. s.log.Infoln("BackUp Org SubFile:", pipeResultMax.GetOffsetTime(), srcSubFPath+sub_timeline_fixer.BackUpExt)
  152. return nil
  153. }
  154. func (s *SubTimelineFixerHelperEx) ProcessBySubFileInfo(infoBase *subparser.FileInfo, infoSrc *subparser.FileInfo) (bool, *subparser.FileInfo, sub_timeline_fixer.PipeResult, error) {
  155. // ---------------------------------------------------------------------------------------
  156. // 先在外面排序
  157. infoBase.SortDialogues()
  158. infoSrc.SortDialogues()
  159. pipeResult, err := s.timelineFixPipeLine.CalcOffsetTimeEx(infoBase, infoSrc, nil, false, s.fixerConfig.ThreadCount)
  160. if err != nil {
  161. return false, nil, sub_timeline_fixer.PipeResult{}, err
  162. }
  163. return true, infoSrc, pipeResult, nil
  164. }
  165. func (s *SubTimelineFixerHelperEx) ProcessBySubFile(baseSubFileFPath, srcSubFileFPath string) (bool, *subparser.FileInfo, sub_timeline_fixer.PipeResult, error) {
  166. bFind, infoBase, err := s.subParserHub.DetermineFileTypeFromFile(baseSubFileFPath)
  167. if err != nil {
  168. return false, nil, sub_timeline_fixer.PipeResult{}, err
  169. }
  170. if bFind == false {
  171. s.log.Warnln("ProcessBySubFile.DetermineFileTypeFromFile sub not match --", baseSubFileFPath)
  172. return false, nil, sub_timeline_fixer.PipeResult{}, nil
  173. }
  174. bFind, infoSrc, err := s.subParserHub.DetermineFileTypeFromFile(srcSubFileFPath)
  175. if err != nil {
  176. return false, nil, sub_timeline_fixer.PipeResult{}, err
  177. }
  178. if bFind == false {
  179. s.log.Warnln("ProcessBySubFile.DetermineFileTypeFromFile sub not match --", srcSubFileFPath)
  180. return false, nil, sub_timeline_fixer.PipeResult{}, nil
  181. }
  182. return s.ProcessBySubFileInfo(infoBase, infoSrc)
  183. }
  184. func (s *SubTimelineFixerHelperEx) ProcessByAudioVAD(audioVADInfos []vad.VADInfo, infoSrc *subparser.FileInfo) (bool, *subparser.FileInfo, sub_timeline_fixer.PipeResult, error) {
  185. // ---------------------------------------------------------------------------------------
  186. // 先在外面排序
  187. infoSrc.SortDialogues()
  188. pipeResult, err := s.timelineFixPipeLine.CalcOffsetTimeEx(nil, infoSrc, audioVADInfos, false, s.fixerConfig.ThreadCount)
  189. if err != nil {
  190. return false, nil, sub_timeline_fixer.PipeResult{}, err
  191. }
  192. return true, infoSrc, pipeResult, nil
  193. }
  194. func (s *SubTimelineFixerHelperEx) ProcessByAudioFile(baseAudioFileFPath, srcSubFileFPath string) (bool, *subparser.FileInfo, sub_timeline_fixer.PipeResult, error) {
  195. audioVADInfos, err := vad.GetVADInfoFromAudio(vad.AudioInfo{
  196. FileFullPath: baseAudioFileFPath,
  197. SampleRate: 16000,
  198. BitDepth: 16,
  199. }, true)
  200. if err != nil {
  201. return false, nil, sub_timeline_fixer.PipeResult{}, err
  202. }
  203. bFind, infoSrc, err := s.subParserHub.DetermineFileTypeFromFile(srcSubFileFPath)
  204. if err != nil {
  205. return false, nil, sub_timeline_fixer.PipeResult{}, err
  206. }
  207. if bFind == false {
  208. s.log.Warnln("ProcessByAudioFile.DetermineFileTypeFromFile sub not match --", srcSubFileFPath)
  209. return false, nil, sub_timeline_fixer.PipeResult{}, nil
  210. }
  211. return s.ProcessByAudioVAD(audioVADInfos, infoSrc)
  212. }
  213. func (s *SubTimelineFixerHelperEx) IsVideoCanExportSubtitleAndAudio(videoFileFullPath string) (bool, *ffmpeg_helper.FFMPEGInfo, []vad.VADInfo, *subparser.FileInfo, error) {
  214. // 先尝试获取内置字幕的信息
  215. bok, ffmpegInfo, err := s.ffmpegHelper.ExportFFMPEGInfo(videoFileFullPath, ffmpeg_helper.SubtitleAndAudio)
  216. if err != nil {
  217. return false, nil, nil, nil, err
  218. }
  219. if bok == false {
  220. return false, nil, nil, nil, nil
  221. }
  222. // ---------------------------------------------------------------------------------------
  223. // 音频
  224. if len(ffmpegInfo.AudioInfoList) <= 0 {
  225. return false, nil, nil, nil, nil
  226. }
  227. audioVADInfos, err := vad.GetVADInfoFromAudio(vad.AudioInfo{
  228. FileFullPath: ffmpegInfo.AudioInfoList[0].FullPath,
  229. SampleRate: 16000,
  230. BitDepth: 16,
  231. }, true)
  232. if err != nil {
  233. return false, nil, nil, nil, err
  234. }
  235. // ---------------------------------------------------------------------------------------
  236. // 字幕
  237. if len(ffmpegInfo.SubtitleInfoList) <= 0 {
  238. return false, nil, nil, nil, nil
  239. }
  240. // 使用内置的字幕进行时间轴的校正,这里需要考虑一个问题,内置的字幕可能是有问题的(先考虑一种,就是字幕的长度不对,是一小段的)
  241. // 那么就可以比较多个内置字幕的大小选择大的去使用
  242. // 如果有多个内置的字幕,还是要判断下的,选体积最大的那个吧
  243. fileSizes := treemap.NewWith(utils.Int64Comparator)
  244. for index, info := range ffmpegInfo.SubtitleInfoList {
  245. fi, err := os.Stat(info.FullPath)
  246. if err != nil {
  247. fileSizes.Put(0, index)
  248. } else {
  249. fileSizes.Put(fi.Size(), index)
  250. }
  251. }
  252. _, index := fileSizes.Max()
  253. baseSubFPath := ffmpegInfo.SubtitleInfoList[index.(int)].FullPath
  254. bFind, infoBase, err := s.subParserHub.DetermineFileTypeFromFile(baseSubFPath)
  255. if err != nil {
  256. return false, nil, nil, nil, err
  257. }
  258. if bFind == false {
  259. return false, nil, nil, nil, nil
  260. }
  261. // ---------------------------------------------------------------------------------------
  262. return true, ffmpegInfo, audioVADInfos, infoBase, nil
  263. }
  264. func (s *SubTimelineFixerHelperEx) IsMatchBySubFile(ffmpegInfo *ffmpeg_helper.FFMPEGInfo, audioVADInfos []vad.VADInfo, infoBase *subparser.FileInfo, srcSubFileFPath string, config CompareConfig) (bool, *MatchResult, error) {
  265. bFind, srcBase, err := s.subParserHub.DetermineFileTypeFromFile(srcSubFileFPath)
  266. if err != nil {
  267. return false, nil, err
  268. }
  269. if bFind == false {
  270. return false, nil, nil
  271. }
  272. // ---------------------------------------------------------------------------------------
  273. // 音频
  274. s.log.Infoln("IsMatchBySubFile:", srcSubFileFPath)
  275. bProcess, _, pipeResultMaxAudio, err := s.ProcessByAudioVAD(audioVADInfos, srcBase)
  276. if err != nil {
  277. return false, nil, err
  278. }
  279. if bProcess == false {
  280. return false, nil, nil
  281. }
  282. // ---------------------------------------------------------------------------------------
  283. // 字幕
  284. bProcess, _, pipeResultMaxSub, err := s.ProcessBySubFileInfo(infoBase, srcBase)
  285. if err != nil {
  286. return false, nil, err
  287. }
  288. if bProcess == false {
  289. return false, nil, nil
  290. }
  291. targetSubEndTime := pkg.Time2SecondNumber(srcBase.GetEndTime())
  292. matchResult := &MatchResult{
  293. VideoDuration: ffmpegInfo.Duration,
  294. TargetSubEndTime: targetSubEndTime,
  295. AudioCompareScore: pipeResultMaxAudio.Score(),
  296. AudioCompareOffsetTime: pipeResultMaxAudio.GetOffsetTime(),
  297. SubCompareScore: pipeResultMaxSub.Score(),
  298. SubCompareOffsetTime: pipeResultMaxSub.GetOffsetTime(),
  299. }
  300. // ---------------------------------------------------------------------------------------
  301. // 分数需要大于某个值
  302. if pipeResultMaxAudio.Score() < config.MinScore || pipeResultMaxSub.Score() < config.MinScore {
  303. return false, matchResult, nil
  304. }
  305. // 两种方式获取到的时间轴的偏移量,差值需要在一定范围内
  306. if math.Abs(pipeResultMaxAudio.GetOffsetTime()-pipeResultMaxSub.GetOffsetTime()) > config.OffsetRange {
  307. return false, matchResult, nil
  308. }
  309. // ---------------------------------------------------------------------------------------
  310. // 待判断的字幕的时间长度要小于等于视频的总长度
  311. if targetSubEndTime > ffmpegInfo.Duration {
  312. return false, matchResult, nil
  313. }
  314. // ---------------------------------------------------------------------------------------
  315. // 两个对比字幕的对白数量不能超过 10%
  316. minRage := float64(len(infoBase.Dialogues)) * config.DialoguesDifferencePercentage
  317. if math.Abs(float64(len(srcBase.Dialogues)-len(infoBase.Dialogues))) > minRage {
  318. return false, matchResult, nil
  319. }
  320. return true, matchResult, nil
  321. }
  322. func (s *SubTimelineFixerHelperEx) changeTimeLineAndSave(infoSrc *subparser.FileInfo, pipeResult sub_timeline_fixer.PipeResult, desSubSaveFPath string) error {
  323. /*
  324. 修复的字幕先存放到缓存目录,然后需要把原有的字幕进行“备份”,改名,然后再替换过来
  325. */
  326. subFileName := desSubSaveFPath + sub_timeline_fixer.TmpExt
  327. if pkg.IsFile(subFileName) == true {
  328. err := os.Remove(subFileName)
  329. if err != nil {
  330. return err
  331. }
  332. }
  333. _, err := s.timelineFixPipeLine.FixSubFileTimeline(infoSrc, pipeResult.ScaledFileInfo, pipeResult.GetOffsetTime(), subFileName)
  334. if err != nil {
  335. return err
  336. }
  337. if pkg.IsFile(desSubSaveFPath+sub_timeline_fixer.BackUpExt) == true {
  338. err = os.Remove(desSubSaveFPath + sub_timeline_fixer.BackUpExt)
  339. if err != nil {
  340. return err
  341. }
  342. }
  343. err = os.Rename(desSubSaveFPath, desSubSaveFPath+sub_timeline_fixer.BackUpExt)
  344. if err != nil {
  345. return err
  346. }
  347. err = os.Rename(subFileName, desSubSaveFPath)
  348. if err != nil {
  349. return err
  350. }
  351. return nil
  352. }
  353. type CompareConfig struct {
  354. MinScore float64 // 最低的分数
  355. OffsetRange float64 // 偏移量的范围
  356. DialoguesDifferencePercentage float64 // 两个字幕的对白字幕差异百分比
  357. }
  358. type MatchResult struct {
  359. VideoDuration float64 // 视频的时长
  360. TargetSubEndTime float64 // 目标字幕的结束时间
  361. AudioCompareScore float64 // 音频的对比分数
  362. AudioCompareOffsetTime float64 // 音频的对比偏移量
  363. SubCompareScore float64 // 字幕的对比分数
  364. SubCompareOffsetTime float64 // 字幕的对比偏移量
  365. }
  366. // ProcessType 定义一个处理的参数为枚举类型
  367. type ProcessType int
  368. const (
  369. ProcessTypeBySubFile ProcessType = iota + 1
  370. ProcessTypeByAudioFile
  371. ProcessTypeAuto
  372. )