sub_helper.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. package sub_helper
  2. import (
  3. "errors"
  4. "math"
  5. "os"
  6. "path/filepath"
  7. "strconv"
  8. "strings"
  9. "time"
  10. "github.com/ChineseSubFinder/ChineseSubFinder/pkg"
  11. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/types/common"
  12. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/types/subparser"
  13. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/types/supplier"
  14. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/archive_helper"
  15. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/decode"
  16. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/filter"
  17. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/language"
  18. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/regex_things"
  19. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/sub_parser_hub"
  20. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/vad"
  21. "github.com/sirupsen/logrus"
  22. )
  23. // OrganizeDlSubFiles 需要从汇总来是网站字幕中,解压对应的压缩包中的字幕出来
  24. func OrganizeDlSubFiles(log *logrus.Logger, tmpFolderName string, subInfos []supplier.SubInfo, isMovie bool) (map[string][]string, error) {
  25. // 缓存列表,整理后的字幕列表
  26. // SxEx - []string 字幕的路径
  27. var siteSubInfoDict = make(map[string][]string)
  28. tmpFolderFullPath, err := pkg.GetTmpFolderByName(tmpFolderName)
  29. if err != nil {
  30. return nil, err
  31. }
  32. // 把后缀名给改好
  33. ChangeVideoExt2SubExt(subInfos)
  34. // 第三方的解压库,首先不支持 io.Reader 的操作,也就是得缓存到本地硬盘再读取解压
  35. // 且使用 walk 会无法解压 rar,得指定具体的实例,太麻烦了,直接用通用的接口得了,就是得都缓存下来再判断
  36. // 基于以上两点,写了一堆啰嗦的逻辑···
  37. for i := range subInfos {
  38. // 先存下来,保存是时候需要前缀,前缀就是从那个网站下载来的
  39. nowFileSaveFullPath := filepath.Join(tmpFolderFullPath, GetFrontNameAndOrgName(log, &subInfos[i]))
  40. err = pkg.WriteFile(nowFileSaveFullPath, subInfos[i].Data)
  41. if err != nil {
  42. log.Errorln("getFrontNameAndOrgName - WriteFile", nowFileSaveFullPath, "FromWhere Name TopN", subInfos[i].FromWhere, subInfos[i].Name, subInfos[i].TopN, err)
  43. continue
  44. }
  45. nowExt := strings.ToLower(subInfos[i].Ext)
  46. epsKey := pkg.GetEpisodeKeyName(subInfos[i].Season, subInfos[i].Episode)
  47. _, ok := siteSubInfoDict[epsKey]
  48. if ok == false {
  49. // 不存在则实例化
  50. siteSubInfoDict[epsKey] = make([]string, 0)
  51. }
  52. if nowExt != ".zip" && nowExt != ".tar" && nowExt != ".rar" && nowExt != ".7z" {
  53. // 是否是受支持的字幕类型
  54. if sub_parser_hub.IsSubExtWanted(nowExt) == false {
  55. log.Debugln("OrganizeDlSubFiles -> IsSubExtWanted == false", "Name:", subInfos[i].Name, "FileUrl:", subInfos[i].FileUrl)
  56. continue
  57. }
  58. // 加入缓存列表
  59. siteSubInfoDict[epsKey] = append(siteSubInfoDict[epsKey], nowFileSaveFullPath)
  60. } else {
  61. // 那么就是需要解压的文件了
  62. // 解压,给一个单独的文件夹
  63. unzipTmpFolder := filepath.Join(tmpFolderFullPath, subInfos[i].FromWhere)
  64. err = os.MkdirAll(unzipTmpFolder, os.ModePerm)
  65. if err != nil {
  66. return nil, err
  67. }
  68. err = archive_helper.UnArchiveFileEx(nowFileSaveFullPath, unzipTmpFolder)
  69. // 解压完成后,遍历受支持的字幕列表,加入缓存列表
  70. if err != nil {
  71. log.Errorln("archiver.UnArchive", subInfos[i].FromWhere, subInfos[i].Name, subInfos[i].TopN, err)
  72. continue
  73. }
  74. // 搜索这个目录下的所有符合字幕格式的文件
  75. subFileFullPaths, err := SearchMatchedSubFileByDir(log, unzipTmpFolder)
  76. if err != nil {
  77. log.Errorln("searchMatchedSubFile", subInfos[i].FromWhere, subInfos[i].Name, subInfos[i].TopN, err)
  78. continue
  79. }
  80. // 这里需要给这些下载到的文件进行改名,加是从那个网站来的前缀,后续好查找
  81. for _, fileFullPath := range subFileFullPaths {
  82. if isMovie == false {
  83. // 连续剧的情况
  84. // 从解压的文件名称推断 Season 和 Episode 信息
  85. _, nowSeason, nowEps, err := decode.GetSeasonAndEpisodeFromSubFileName(filepath.Base(fileFullPath))
  86. if err != nil {
  87. continue
  88. }
  89. newSubName := AddFrontName(subInfos[i], filepath.Base(fileFullPath))
  90. newSubNameFullPath := filepath.Join(tmpFolderFullPath, newSubName)
  91. // 改名
  92. err = os.Rename(fileFullPath, newSubNameFullPath)
  93. if err != nil {
  94. log.Errorln("os.Rename", subInfos[i].FromWhere, subInfos[i].Name, subInfos[i].TopN, err)
  95. continue
  96. }
  97. // 加入缓存列表
  98. // 根据当前字幕的信息来构建 key
  99. SEPKey := pkg.GetEpisodeKeyName(nowSeason, nowEps)
  100. _, ok = siteSubInfoDict[SEPKey]
  101. if ok == false {
  102. siteSubInfoDict[SEPKey] = make([]string, 0)
  103. }
  104. siteSubInfoDict[SEPKey] = append(siteSubInfoDict[SEPKey], newSubNameFullPath)
  105. } else {
  106. // 电影的情况
  107. newSubName := AddFrontName(subInfos[i], filepath.Base(fileFullPath))
  108. newSubNameFullPath := filepath.Join(tmpFolderFullPath, newSubName)
  109. // 改名
  110. err = os.Rename(fileFullPath, newSubNameFullPath)
  111. if err != nil {
  112. log.Errorln("os.Rename", subInfos[i].FromWhere, subInfos[i].Name, subInfos[i].TopN, err)
  113. continue
  114. }
  115. // 加入缓存列表
  116. siteSubInfoDict[epsKey] = append(siteSubInfoDict[epsKey], newSubNameFullPath)
  117. }
  118. }
  119. }
  120. }
  121. return siteSubInfoDict, nil
  122. }
  123. // ChangeVideoExt2SubExt 检测 Name,如果是视频的后缀名就改为字幕的后缀名
  124. func ChangeVideoExt2SubExt(subInfos []supplier.SubInfo) {
  125. for x, info := range subInfos {
  126. tmpSubFileName := info.Name
  127. // 如果后缀名是下载字幕目标的后缀名 或者 是压缩包格式的,则跳过
  128. if strings.Contains(tmpSubFileName, info.Ext) == true || archive_helper.IsWantedArchiveExtName(tmpSubFileName) == true {
  129. } else {
  130. subInfos[x].Name = tmpSubFileName + info.Ext
  131. }
  132. }
  133. }
  134. // SelectChineseBestBilingualSubtitle 找到合适的双语中文字幕,简体->繁体,以及 字幕类型的优先级选择
  135. func SelectChineseBestBilingualSubtitle(subs []subparser.FileInfo, subTypePriority int) *subparser.FileInfo {
  136. // 先傻一点实现优先双语的,之前的写法有 bug
  137. for _, info := range subs {
  138. // 找到了中文字幕
  139. if language.HasChineseLang(info.Lang) == true {
  140. // 字幕的优先级 0 - 原样, 1 - srt , 2 - ass/ssa
  141. if subTypePriority == 1 {
  142. // 1 - srt
  143. if strings.ToLower(info.Ext) == common.SubExtSRT {
  144. // 优先双语
  145. if language.IsBilingualSubtitle(info.Lang) == true {
  146. return &info
  147. }
  148. }
  149. } else if subTypePriority == 2 {
  150. // 2 - ass/ssa
  151. if strings.ToLower(info.Ext) == common.SubExtASS || strings.ToLower(info.Ext) == common.SubExtSSA {
  152. // 优先双语
  153. if language.IsBilingualSubtitle(info.Lang) == true {
  154. return &info
  155. }
  156. }
  157. } else {
  158. // 优先双语
  159. if language.IsBilingualSubtitle(info.Lang) == true {
  160. return &info
  161. }
  162. }
  163. }
  164. }
  165. return nil
  166. }
  167. // SelectChineseBestSubtitle 找到合适的中文字幕,简体->繁体,以及 字幕类型的优先级选择
  168. func SelectChineseBestSubtitle(subs []subparser.FileInfo, subTypePriority int) *subparser.FileInfo {
  169. // 先傻一点实现优先双语的,之前的写法有 bug
  170. for _, info := range subs {
  171. // 找到了中文字幕
  172. if language.HasChineseLang(info.Lang) == true {
  173. // 字幕的优先级 0 - 原样, 1 - srt , 2 - ass/ssa
  174. if subTypePriority == 1 {
  175. // 1 - srt
  176. if strings.ToLower(info.Ext) == common.SubExtSRT {
  177. return &info
  178. }
  179. } else if subTypePriority == 2 {
  180. // 2 - ass/ssa
  181. if strings.ToLower(info.Ext) == common.SubExtASS || strings.ToLower(info.Ext) == common.SubExtSSA {
  182. return &info
  183. }
  184. } else {
  185. return &info
  186. }
  187. }
  188. }
  189. return nil
  190. }
  191. // GetFrontNameAndOrgName 返回的名称包含,那个网站下载的,这个网站中排名第几,文件名
  192. func GetFrontNameAndOrgName(log *logrus.Logger, info *supplier.SubInfo) string {
  193. infoName := ""
  194. fileName, err := decode.GetVideoInfoFromFileName(info.Name)
  195. if err != nil {
  196. log.Warnln("", err)
  197. // 替换特殊字符
  198. infoName = pkg.ReplaceSpecString(info.Name, "x")
  199. } else {
  200. infoName = fileName.Title + "_S" + strconv.Itoa(fileName.Season) + "E" + strconv.Itoa(fileName.Episode) + filepath.Ext(info.Name)
  201. }
  202. if len(infoName) < 1 {
  203. infoName = pkg.RandStringBytesMaskImprSrcSB(10) + filepath.Ext(info.Name)
  204. }
  205. info.Name = infoName
  206. return "[" + info.FromWhere + "]_" + strconv.FormatInt(info.TopN, 10) + "_" + infoName
  207. }
  208. // AddFrontName 添加文件的前缀
  209. func AddFrontName(info supplier.SubInfo, orgName string) string {
  210. return "[" + info.FromWhere + "]_" + strconv.FormatInt(info.TopN, 10) + "_" + orgName
  211. }
  212. // SearchMatchedSubFileByDir 搜索符合后缀名的视频文件,排除 Sub_SxE0 这样的文件夹中的文件
  213. func SearchMatchedSubFileByDir(log *logrus.Logger, dir string) ([]string, error) {
  214. // 这里有个梗,会出现 __MACOSX 这类文件夹,那么里面会有一样的文件,需要用文件大小排除一下,至少大于 1 kb 吧
  215. var fileFullPathList = make([]string, 0)
  216. pathSep := string(os.PathSeparator)
  217. files, err := os.ReadDir(dir)
  218. if err != nil {
  219. return nil, err
  220. }
  221. for _, curFile := range files {
  222. fullPath := dir + pathSep + curFile.Name()
  223. if pkg.IsDir(fullPath) == true {
  224. // 需要排除 Sub_S1E0、Sub_S2E0 这样的整季的字幕文件夹,这里仅仅是缓存,不会被加载的
  225. matched := regex_things.RegOneSeasonSubFolderNameMatch.FindAllStringSubmatch(curFile.Name(), -1)
  226. if matched != nil && len(matched) > 0 {
  227. continue
  228. }
  229. // 内层的错误就无视了
  230. oneList, _ := SearchMatchedSubFileByDir(log, fullPath)
  231. if oneList != nil {
  232. fileFullPathList = append(fileFullPathList, oneList...)
  233. }
  234. } else {
  235. // 这里就是文件了
  236. if filter.SkipFileInfo(log, curFile, fullPath) == true {
  237. continue
  238. }
  239. if sub_parser_hub.IsSubExtWanted(filepath.Ext(curFile.Name())) == true {
  240. fileFullPathList = append(fileFullPathList, fullPath)
  241. }
  242. }
  243. }
  244. return fileFullPathList, nil
  245. }
  246. // SearchMatchedSubFileByOneVideo 搜索这个视频当前目录下匹配的字幕
  247. func SearchMatchedSubFileByOneVideo(l *logrus.Logger, oneVideoFullPath string) ([]string, error) {
  248. dir := filepath.Dir(oneVideoFullPath)
  249. fileName := filepath.Base(oneVideoFullPath)
  250. fileName = strings.ToLower(fileName)
  251. fileName = strings.ReplaceAll(fileName, filepath.Ext(fileName), "")
  252. pathSep := string(os.PathSeparator)
  253. files, err := os.ReadDir(dir)
  254. if err != nil {
  255. return nil, err
  256. }
  257. var matchedSubs = make([]string, 0)
  258. for _, curFile := range files {
  259. if curFile.IsDir() {
  260. continue
  261. }
  262. // 这里就是文件了
  263. oldPath := dir + pathSep + curFile.Name()
  264. if filter.SkipFileInfo(l, curFile, oldPath) == true {
  265. continue
  266. }
  267. // 判断的时候用小写的,后续重命名的时候用原有的名称
  268. nowFileName := strings.ToLower(curFile.Name())
  269. // 后缀名得对
  270. if sub_parser_hub.IsSubExtWanted(filepath.Ext(nowFileName)) == false {
  271. continue
  272. }
  273. // 字幕文件名应该包含 视频文件名(无后缀)
  274. if strings.HasPrefix(nowFileName, fileName) == false {
  275. continue
  276. }
  277. matchedSubs = append(matchedSubs, oldPath)
  278. }
  279. return matchedSubs, nil
  280. }
  281. // SearchVideoMatchSubFileAndRemoveExtMark 找到找个视频目录下相匹配的字幕,同时去除这些字幕中 .default 或者 .forced 的标记。注意这两个标记不应该同时出现,否则无法正确去除
  282. func SearchVideoMatchSubFileAndRemoveExtMark(l *logrus.Logger, oneVideoFullPath string) error {
  283. dir := filepath.Dir(oneVideoFullPath)
  284. fileName := filepath.Base(oneVideoFullPath)
  285. fileName = strings.ToLower(fileName)
  286. fileName = strings.ReplaceAll(fileName, filepath.Ext(fileName), "")
  287. pathSep := string(os.PathSeparator)
  288. files, err := os.ReadDir(dir)
  289. if err != nil {
  290. return err
  291. }
  292. for _, curFile := range files {
  293. if curFile.IsDir() {
  294. continue
  295. } else {
  296. // 这里就是文件了
  297. oldPath := dir + pathSep + curFile.Name()
  298. if filter.SkipFileInfo(l, curFile, oldPath) == true {
  299. continue
  300. }
  301. // 判断的时候用小写的,后续重命名的时候用原有的名称
  302. nowFileName := strings.ToLower(curFile.Name())
  303. // 后缀名得对
  304. if sub_parser_hub.IsSubExtWanted(filepath.Ext(nowFileName)) == false {
  305. continue
  306. }
  307. // 字幕文件名应该包含 视频文件名(无后缀)
  308. if strings.HasPrefix(nowFileName, fileName) == false {
  309. continue
  310. }
  311. if strings.Contains(nowFileName, subparser.Sub_Ext_Mark_Default+".") == true {
  312. // 得包含 .default. 找个关键词
  313. // 去除 .default.
  314. newPath := dir + pathSep + strings.ReplaceAll(curFile.Name(), subparser.Sub_Ext_Mark_Default+".", ".")
  315. err = os.Rename(oldPath, newPath)
  316. if err != nil {
  317. return err
  318. }
  319. } else if strings.Contains(nowFileName, subparser.Sub_Ext_Mark_Forced+".") == true {
  320. // 得包含 .forced. 找个关键词
  321. oldPath := dir + pathSep + curFile.Name()
  322. newPath := dir + pathSep + strings.ReplaceAll(curFile.Name(), subparser.Sub_Ext_Mark_Forced+".", ".")
  323. err = os.Rename(oldPath, newPath)
  324. if err != nil {
  325. return err
  326. }
  327. } else {
  328. continue
  329. }
  330. }
  331. }
  332. return nil
  333. }
  334. // DeleteOneSeasonSubCacheFolder 删除一个连续剧中的所有一季字幕的缓存文件夹
  335. func DeleteOneSeasonSubCacheFolder(seriesDir string) error {
  336. debugFolderByName, err := pkg.GetDebugFolderByName([]string{filepath.Base(seriesDir)})
  337. if err != nil {
  338. return err
  339. }
  340. files, err := os.ReadDir(debugFolderByName)
  341. if err != nil {
  342. return err
  343. }
  344. pathSep := string(os.PathSeparator)
  345. for _, curFile := range files {
  346. if curFile.IsDir() == true {
  347. matched := regex_things.RegOneSeasonSubFolderNameMatch.FindAllStringSubmatch(curFile.Name(), -1)
  348. if matched == nil || len(matched) < 1 {
  349. continue
  350. }
  351. fullPath := debugFolderByName + pathSep + curFile.Name()
  352. err = os.RemoveAll(fullPath)
  353. if err != nil {
  354. return err
  355. }
  356. }
  357. }
  358. return nil
  359. }
  360. /*
  361. 只针对英文字幕进行合并分散的 DialoguesFilter
  362. 会遇到这样的字幕,如下0
  363. 2line-The Card Counter (2021) WEBDL-1080p.chinese(inside).ass
  364. 它的对白一句话分了两个 dialogue 去做。这样做后续字幕时间轴校正就会遇到问题,因为只有一半,匹配占比会很低
  365. (每一个 Dialogue 的首字母需要分析,大写和小写的占比是多少,统计一下,正常的,和上述特殊的)
  366. 那么,就需要额外的逻辑去对 DialoguesFilterEx 进行额外的推断
  367. 暂时考虑的方案是,英文对白每一句的开头应该是英文大写字幕,如果是小写字幕,就应该与上语句合并,且每一句的字符长度有大于一定才触发
  368. */
  369. func MergeMultiDialogue4EngSubtitle(inSubParser *subparser.FileInfo) {
  370. merger := NewDialogueMerger()
  371. for _, dialogueEx := range inSubParser.DialoguesFilterEx {
  372. merger.Add(dialogueEx)
  373. }
  374. inSubParser.DialoguesFilterEx = merger.Get()
  375. }
  376. // GetVADInfoFeatureFromSub 跟下面的 GetVADInfoFeatureFromSubNeedOffsetTimeWillInsert 函数功能一致
  377. func GetVADInfoFeatureFromSub(fileInfo *subparser.FileInfo, frontAndEndPer float64, subUnitMaxCount int, insert bool) ([]SubUnit, error) {
  378. return GetVADInfoFeatureFromSubNeedOffsetTimeWillInsert(fileInfo, frontAndEndPer, subUnitMaxCount, 0, insert)
  379. }
  380. /*
  381. GetVADInfoFeatureFromSubNeedOffsetTimeWillInsert 只不过这里可以加一个每一句话固定的偏移时间
  382. 这里的字幕要求是完整的一个字幕
  383. 1. 抽取字幕的时间片段的时候,暂定,前 15% 和后 15% 要避开,前奏、主题曲、结尾曲
  384. 2. 将整个字幕,抽取连续 5 句对话为一个单元,提取时间片段信息
  385. 3. 这里抽取的是特征,也就有额外的逻辑去找这个特征(本程序内会描述为“钥匙”)
  386. */
  387. func GetVADInfoFeatureFromSubNeedOffsetTimeWillInsert(fileInfo *subparser.FileInfo, SkipFrontAndEndPer float64, subUnitMaxCount int, offsetTime float64, insert bool) ([]SubUnit, error) {
  388. if subUnitMaxCount < 0 {
  389. subUnitMaxCount = 0
  390. }
  391. nowDialogue := fileInfo.Dialogues
  392. srcSubUnitList := make([]SubUnit, 0)
  393. srcSubDialogueList := make([]subparser.OneDialogue, 0)
  394. srcOneSubUnit := NewSubUnit()
  395. // 最后一个对话的结束时间
  396. lastDialogueExTimeEnd, err := pkg.ParseTime(nowDialogue[len(nowDialogue)-1].EndTime)
  397. if err != nil {
  398. return nil, err
  399. }
  400. // 相当于总时长
  401. fullDuration := pkg.Time2SecondNumber(lastDialogueExTimeEnd)
  402. // 最低的起始时间,因为可能需要裁剪范围
  403. startRangeTimeMin := fullDuration * SkipFrontAndEndPer
  404. endRangeTimeMax := fullDuration * (1.0 - SkipFrontAndEndPer)
  405. println(startRangeTimeMin)
  406. println(endRangeTimeMax)
  407. for i := 0; i < len(nowDialogue); i++ {
  408. oneDialogueExTimeStart, err := pkg.ParseTime(nowDialogue[i].StartTime)
  409. if err != nil {
  410. return nil, err
  411. }
  412. oneDialogueExTimeEnd, err := pkg.ParseTime(nowDialogue[i].EndTime)
  413. if err != nil {
  414. return nil, err
  415. }
  416. oneStart := pkg.Time2SecondNumber(oneDialogueExTimeStart)
  417. if SkipFrontAndEndPer > 0 {
  418. if fullDuration*SkipFrontAndEndPer > oneStart || fullDuration*(1.0-SkipFrontAndEndPer) < oneStart {
  419. continue
  420. }
  421. }
  422. if nowDialogue[i].Lines == nil || len(nowDialogue[i].Lines) == 0 {
  423. continue
  424. }
  425. // 如果当前的这一句话,为空,或者进过正则表达式剔除特殊字符后为空,则跳过
  426. if pkg.ReplaceSpecString(nowDialogue[i].Lines[0], "") == "" {
  427. continue
  428. }
  429. // 如果当前的这一句话,为空,或者进过正则表达式剔除特殊字符后为空,则跳过
  430. if pkg.ReplaceSpecString(fileInfo.GetDialogueExContent(i), "") == "" {
  431. continue
  432. }
  433. // 低于 5句对白,则添加
  434. if srcOneSubUnit.GetDialogueCount() < subUnitMaxCount {
  435. // 算上偏移
  436. offsetTimeDuration := time.Duration(offsetTime * math.Pow10(9))
  437. oneDialogueExTimeStart = oneDialogueExTimeStart.Add(offsetTimeDuration)
  438. oneDialogueExTimeEnd = oneDialogueExTimeEnd.Add(offsetTimeDuration)
  439. // 如果没有偏移就是 0
  440. if insert == true {
  441. srcOneSubUnit.AddAndInsert(oneDialogueExTimeStart, oneDialogueExTimeEnd)
  442. } else {
  443. srcOneSubUnit.Add(oneDialogueExTimeStart, oneDialogueExTimeEnd)
  444. }
  445. // 这一个单元的 Dialogue 需要合并起来,才能判断是否符合“钥匙”的要求
  446. srcSubDialogueList = append(srcSubDialogueList, nowDialogue[i])
  447. } else {
  448. // 用完清空
  449. srcSubDialogueList = make([]subparser.OneDialogue, 0)
  450. // 将拼凑起来的对话组成一个单元进行存储起来
  451. srcSubUnitList = append(srcSubUnitList, *srcOneSubUnit)
  452. // 然后重置
  453. srcOneSubUnit = NewSubUnit()
  454. }
  455. }
  456. if srcOneSubUnit.GetDialogueCount() > 0 {
  457. srcSubUnitList = append(srcSubUnitList, *srcOneSubUnit)
  458. }
  459. return srcSubUnitList, nil
  460. }
  461. /*
  462. GetVADInfoFeatureFromSubNew 将 Sub 文件转换为 VAD List 信息
  463. */
  464. func GetVADInfoFeatureFromSubNew(fileInfo *subparser.FileInfo, SkipFrontAndEndPer float64) (*SubUnit, error) {
  465. outSubUnits := NewSubUnit()
  466. if len(fileInfo.Dialogues) <= 0 {
  467. return nil, errors.New("GetVADInfoFeatureFromSubNew fileInfo Dialogue Length is 0")
  468. }
  469. /*
  470. 先拼凑出完整的一个 VAD List
  471. 因为 VAD 的窗口是 10ms,那么需要多每一句话按 10 ms 的单位进行取整
  472. 每一句话开始、结束的时间,需要向下取整
  473. */
  474. subStartTimeFloor := pkg.MakeFloor10msMultipleFromFloat(pkg.Time2SecondNumber(fileInfo.GetStartTime()))
  475. subEndTimeFloor := pkg.MakeFloor10msMultipleFromFloat(pkg.Time2SecondNumber(fileInfo.GetEndTime()))
  476. // 如果想要从 0 时间点开始算,那么 subStartTimeFloor 这个值就需要重置到0
  477. subStartTimeFloor = 0
  478. subFullSecondTimeFloor := subEndTimeFloor - subStartTimeFloor
  479. // 根据这个时长就能够得到一个完整的 VAD List,然后再通过每一句对白进行 VAD 值的调整即可,这样就能够保证
  480. // 相同的一个字幕因为使用 ffmpeg 导出 srt 和 ass 后的,可能存在总体时间轴不一致的问题
  481. // 123.450 - > 12345
  482. vadLen := int(subFullSecondTimeFloor*100) + 2
  483. subVADs := make([]vad.VADInfo, vadLen)
  484. subStartTimeFloor10ms := subStartTimeFloor * 100
  485. for i := 0; i < vadLen; i++ {
  486. subVADs[i] = *vad.NewVADInfoBase(false, time.Duration((subStartTimeFloor10ms+float64(i))*math.Pow10(7)))
  487. }
  488. // 计算出需要截取的片段,起始和结束
  489. skipLen := int(float64(vadLen) * SkipFrontAndEndPer)
  490. skipStartIndex := skipLen
  491. skipEndIndex := vadLen - skipLen
  492. // 现在需要从 fileInfo 的每一句对白也就对应一段连续的 VAD active = true 来进行改写,记得向下取整
  493. lastDialogueIndex := 0
  494. for _, dialogue := range fileInfo.Dialogues {
  495. if dialogue.Lines == nil || len(dialogue.Lines) == 0 {
  496. continue
  497. }
  498. // 如果当前的这一句话,为空,或者进过正则表达式剔除特殊字符后为空,则跳过
  499. if pkg.ReplaceSpecString(dialogue.Lines[0], "") == "" {
  500. continue
  501. }
  502. // 字幕的开始时间
  503. oneDialogueStartTime, err := pkg.ParseTime(dialogue.StartTime)
  504. if err != nil {
  505. return nil, err
  506. }
  507. // 字幕的结束时间
  508. oneDialogueEndTime, err := pkg.ParseTime(dialogue.EndTime)
  509. if err != nil {
  510. return nil, err
  511. }
  512. // 字幕的时长,对时间进行向下取整
  513. oneDialogueStartTimeFloor := pkg.MakeCeil10msMultipleFromFloat(pkg.Time2SecondNumber(oneDialogueStartTime))
  514. oneDialogueEndTimeFloor := pkg.MakeFloor10msMultipleFromFloat(pkg.Time2SecondNumber(oneDialogueEndTime))
  515. // 得到一句对白的时长
  516. changeVADStartIndex := int(oneDialogueStartTimeFloor * 100)
  517. changeVADEndIndex := int(oneDialogueEndTimeFloor * 100)
  518. // 不能超过 最后一句话的时常
  519. if changeVADStartIndex > int(subEndTimeFloor*100) {
  520. continue
  521. }
  522. // 也不能比起始的第一句话时间轴更低
  523. if changeVADStartIndex < int(subStartTimeFloor10ms) {
  524. continue
  525. }
  526. // 当前这句话的开始和结束信息
  527. changerStartIndex := changeVADStartIndex - int(subStartTimeFloor10ms)
  528. if changerStartIndex < 0 {
  529. continue
  530. }
  531. changerEndIndex := changeVADEndIndex - int(subStartTimeFloor10ms)
  532. if changerEndIndex < 0 {
  533. continue
  534. }
  535. // 如果上一个对白的最后一个 OffsetIndex 连接着当前这一句的索引的 VAD 信息 active 是 true 就设置为 false
  536. if lastDialogueIndex == changerStartIndex {
  537. for i := 1; i <= 2; i++ {
  538. if lastDialogueIndex-i >= 0 && subVADs[lastDialogueIndex-i].Active == true {
  539. subVADs[lastDialogueIndex-i].Active = false
  540. }
  541. }
  542. }
  543. // 开始根据当前这句话进行 VAD 信息的设置
  544. // 调整之前做好的整体 VAD 的信息,符合 VAD active = true
  545. if changerEndIndex >= vadLen {
  546. changerEndIndex = vadLen - 1
  547. }
  548. for i := changerStartIndex; i <= changerEndIndex; i++ {
  549. subVADs[i].Active = true
  550. }
  551. lastDialogueIndex = changerEndIndex
  552. }
  553. // 截取出来当前这一段
  554. tmpVADList := subVADs[skipStartIndex:skipEndIndex]
  555. outSubUnits.VADList = tmpVADList
  556. tmpStartTime := time.Time{}
  557. tmpStartTime = tmpStartTime.Add(tmpVADList[0].Time)
  558. tmpEndTime := time.Time{}
  559. tmpEndTime = tmpEndTime.Add(tmpVADList[len(tmpVADList)-1].Time)
  560. outSubUnits.SetBaseTime(tmpStartTime)
  561. outSubUnits.SetOffsetStartTime(tmpStartTime)
  562. outSubUnits.SetOffsetEndTime(tmpEndTime)
  563. return outSubUnits, nil
  564. }