srt.go 8.7 KB

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