srt.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. package srt
  2. import (
  3. "os"
  4. "path/filepath"
  5. "strconv"
  6. "strings"
  7. "github.com/ChineseSubFinder/ChineseSubFinder/pkg"
  8. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/types/subparser"
  9. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/language"
  10. "github.com/ChineseSubFinder/ChineseSubFinder/pkg/regex_things"
  11. "github.com/sirupsen/logrus"
  12. )
  13. type Parser struct {
  14. log *logrus.Logger
  15. }
  16. func NewParser(log *logrus.Logger) *Parser {
  17. return &Parser{log: log}
  18. }
  19. func (p Parser) GetParserName() string {
  20. return "srt"
  21. }
  22. /*
  23. DetermineFileTypeFromFile 确定字幕文件的类型,是双语字幕或者某一种语言等等信息
  24. 当 error 是 common.DetermineFileTypeFromFileExtNotFitSRT
  25. 需要额外的处理逻辑,比如不用报错,而是跳过后续的逻辑
  26. */
  27. func (p Parser) DetermineFileTypeFromFile(filePath string) (bool, *subparser.FileInfo, error) {
  28. nowExt := filepath.Ext(filePath)
  29. if p.log != nil {
  30. p.log.Debugln("DetermineFileTypeFromFile", p.GetParserName(), filePath)
  31. }
  32. fBytes, err := os.ReadFile(filePath)
  33. if err != nil {
  34. return false, nil, err
  35. }
  36. inBytes, err := language.ChangeFileCoding2UTF8(fBytes)
  37. if err != nil {
  38. return false, nil, err
  39. }
  40. return p.DetermineFileTypeFromBytes(inBytes, nowExt)
  41. }
  42. // DetermineFileTypeFromBytes 确定字幕文件的类型,是双语字幕或者某一种语言等等信息
  43. func (p Parser) DetermineFileTypeFromBytes(inBytes []byte, nowExt string) (bool, *subparser.FileInfo, error) {
  44. subFileInfo := subparser.FileInfo{}
  45. subFileInfo.Content = string(inBytes)
  46. subFileInfo.Ext = nowExt
  47. subFileInfo.Dialogues = make([]subparser.OneDialogue, 0)
  48. subFileInfo.DialoguesFilter = make([]subparser.OneDialogue, 0)
  49. orgDialogues := p.parseContent(inBytes)
  50. if len(orgDialogues) <= 0 {
  51. if p.log != nil {
  52. p.log.Debugln("DetermineFileTypeFromBytes can't found DialoguesFilter, Skip")
  53. }
  54. return false, nil, nil
  55. }
  56. subFileInfo.Dialogues = orgDialogues
  57. // 这里需要统计一共有几个 \N,以及这个数量在整体行数中的比例,这样就知道是不是双语字幕了
  58. countLineFeed := 0
  59. for _, oneDialogue := range orgDialogues {
  60. if len(oneDialogue.Lines) == 0 || pkg.ReplaceSpecString(oneDialogue.Lines[0], "") == "" {
  61. continue
  62. }
  63. ol := oneDialogue
  64. for i, line := range oneDialogue.Lines {
  65. fixedLine := line
  66. // 剔除 {\fn微软雅黑\fs14}C'mon, Rick. We're -- We're almost there. {} 这一段
  67. fixedLine = regex_things.ReMatchBrace.ReplaceAllString(line, "")
  68. fixedLine = regex_things.ReMatchBracket.ReplaceAllString(fixedLine, "")
  69. fixedLine = strings.ReplaceAll(fixedLine, `\N`, "")
  70. if pkg.ReplaceSpecString(fixedLine, "") == "" {
  71. ol.Lines[i] = ""
  72. break
  73. } else {
  74. if i == 1 {
  75. // 这样说明有两行字幕,也就是双语啦
  76. countLineFeed++
  77. }
  78. ol.Lines[i] = fixedLine
  79. }
  80. }
  81. if ol.Lines[0] == "" {
  82. continue
  83. }
  84. subFileInfo.DialoguesFilter = append(subFileInfo.DialoguesFilter, ol)
  85. }
  86. // 再分析
  87. // 需要判断每一个 Line 是啥语言,[语言的code]次数
  88. var langDict map[int]int
  89. langDict = make(map[int]int)
  90. // 抽取出所有的中文对话
  91. var chLines = make([]string, 0)
  92. // 抽取出所有的第二语言对话
  93. var otherLines = make([]string, 0)
  94. // 抽取出来的对话数组,为了后续用来匹配和修改时间轴
  95. var usefulDialogueExs = make([]subparser.OneDialogueEx, 0)
  96. emptyLines := 0
  97. for _, dialogue := range subFileInfo.DialoguesFilter {
  98. emptyLines += language.DetectSubLangAndStatistics(dialogue, langDict, &usefulDialogueExs, &chLines, &otherLines)
  99. }
  100. // 从统计出来的字典,找出 Top 1 或者 2 的出来,然后计算出是什么语言的字幕
  101. detectLang := language.SubLangStatistics2SubLangType(float32(countLineFeed), float32(len(subFileInfo.DialoguesFilter)-emptyLines), langDict, chLines)
  102. subFileInfo.Lang = detectLang
  103. subFileInfo.Data = inBytes
  104. subFileInfo.DialoguesFilterEx = usefulDialogueExs
  105. subFileInfo.CHLines = chLines
  106. subFileInfo.OtherLines = otherLines
  107. return true, &subFileInfo, nil
  108. }
  109. func (p Parser) parseContent(inBytes []byte) []subparser.OneDialogue {
  110. allString := string(inBytes)
  111. // 注意,需要替换掉 \r 不然正则表达式会有问题
  112. allString = strings.ReplaceAll(allString, "\r", "")
  113. lines := strings.Split(allString, "\n")
  114. // 需要把每一行如果是多余的特殊剔除掉
  115. // 这里的目标是后续的匹配更加容易,但是,后续也得注意
  116. // 因为这个样的操作,那么匹配对白内容的时候,可能是不存在的,只要是 index 和 时间匹配上了,就应该算一句话,只要在 dialogue 上是没得问题的
  117. // 而 dialogueFilter 中则可以把这样没有内容的排除,但是实际时间轴匹配的时候还是用 dialogue 而不是 dialogueFilter
  118. filterLines := make([]string, 0)
  119. for _, line := range lines {
  120. // 如果当前的这一句话,为空,或者进过正则表达式剔除特殊字符后为空,则跳过
  121. if pkg.ReplaceSpecString(line, "") == "" {
  122. continue
  123. }
  124. filterLines = append(filterLines, line)
  125. }
  126. dialogues := make([]subparser.OneDialogue, 0)
  127. /*
  128. 这里可以确定,srt 格式,开始一定是第一句话,那么首先就需要找到,第一行,一定是数字的,从这里开始算起
  129. 1. 先将 content 进行 \r 的替换为空
  130. 2. 将 content 进行 \n 来分割
  131. 3. 将分割的数组进行筛选,把空行剔除掉
  132. 4. 然后使用循环,用下面的 steps 进行解析一句对白
  133. steps:
  134. 0 找对白的 ID
  135. 1 找时间轴
  136. 2 找对白内容,可能有多行,停止的方式,一个是向后能找到 0以及2 或者 是最后一行
  137. */
  138. steps := 0
  139. nowDialogue := subparser.NewOneDialogue()
  140. newOneDialogueFun := func() {
  141. // 重新新建一个缓存对白,从新开始
  142. steps = 0
  143. nowDialogue = subparser.NewOneDialogue()
  144. }
  145. // 使用过滤后的列表
  146. for i, line := range filterLines {
  147. if steps == 0 {
  148. // 匹配对白的索引
  149. line = pkg.ReplaceSpecString(line, "")
  150. dialogueIndex, err := strconv.Atoi(line)
  151. if err != nil {
  152. newOneDialogueFun()
  153. continue
  154. }
  155. nowDialogue.Index = dialogueIndex
  156. // 继续
  157. steps = 1
  158. continue
  159. }
  160. if steps == 1 {
  161. // 匹配时间
  162. matched := regex_things.ReMatchDialogueTimeSRT.FindAllStringSubmatch(line, -1)
  163. if matched == nil || len(matched) < 1 || matched[0][0] != line {
  164. matched = regex_things.ReMatchDialogueTimeSRT2.FindAllStringSubmatch(line, -1)
  165. if matched == nil || len(matched) < 1 || matched[0][0] != line {
  166. newOneDialogueFun()
  167. continue
  168. }
  169. }
  170. nowDialogue.StartTime = matched[0][1]
  171. nowDialogue.EndTime = matched[0][2]
  172. // 是否到结尾
  173. if i+1 > len(filterLines)-1 {
  174. // 是尾部
  175. // 那么这一个对白就需要 add 到总列表中了
  176. dialogues = append(dialogues, nowDialogue)
  177. newOneDialogueFun()
  178. continue
  179. }
  180. // 如上面提到的,因为把特殊字符的行去除了,那么一个对话,如果只有 index 和 时间,也是需要添加进去的
  181. if p.needMatchNextContentLine(filterLines, i+1) == true {
  182. // 是,那么也认为当前这个对话完成了,需要 add 到总列表中了
  183. dialogues = append(dialogues, nowDialogue)
  184. newOneDialogueFun()
  185. continue
  186. }
  187. // 非上述特殊情况,继续
  188. steps = 2
  189. continue
  190. }
  191. if steps == 2 {
  192. // 在上述情况排除后,才继续
  193. // 匹配内容
  194. if len(regex_things.ReMatchSrtSubtitleEffects.FindAllString(line, -1)) > 5 {
  195. continue
  196. }
  197. nowDialogue.Lines = append(nowDialogue.Lines, line)
  198. // 是否到结尾
  199. if i+1 > len(filterLines)-1 {
  200. // 是尾部
  201. // 那么这一个对白就需要 add 到总列表中了
  202. dialogues = append(dialogues, nowDialogue)
  203. newOneDialogueFun()
  204. continue
  205. }
  206. // 不是尾部,那么就需要往后看两句话,是否是下一个对白的头部(index 和 时间)
  207. if p.needMatchNextContentLine(filterLines, i+1) == true {
  208. // 是,那么也认为当前这个对话完成了,需要 add 到总列表中了
  209. dialogues = append(dialogues, nowDialogue)
  210. newOneDialogueFun()
  211. continue
  212. } else {
  213. // 如果还不是,那么就可能是这个对白有多行,有可能是同一种语言的多行,也可能是多语言的多行
  214. // 那么 step 应该不变继续是 2
  215. continue
  216. }
  217. }
  218. }
  219. return dialogues
  220. }
  221. // needMatchNextContentLine 是否需要继续匹配下一句话作为一个对白的对话内容
  222. func (p Parser) needMatchNextContentLine(lines []string, index int) bool {
  223. if index+1 > len(lines)-1 {
  224. return false
  225. }
  226. // 匹配到对白的 Index
  227. _, err := strconv.Atoi(lines[index])
  228. if err != nil {
  229. return false
  230. }
  231. // 匹配到字幕的时间
  232. matched := regex_things.ReMatchDialogueTimeSRT.FindAllStringSubmatch(lines[index+1], -1)
  233. if matched == nil || len(matched) < 1 {
  234. matched = regex_things.ReMatchDialogueTimeSRT2.FindAllStringSubmatch(lines[index+1], -1)
  235. if matched == nil || len(matched) < 1 {
  236. return false
  237. }
  238. }
  239. return true
  240. }