ass.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. package ass
  2. import (
  3. "os"
  4. "path/filepath"
  5. "strings"
  6. "github.com/allanpk716/ChineseSubFinder/internal/pkg/language"
  7. "github.com/allanpk716/ChineseSubFinder/internal/pkg/regex_things"
  8. "github.com/allanpk716/ChineseSubFinder/internal/types/subparser"
  9. "github.com/emirpasic/gods/maps/treemap"
  10. "github.com/sirupsen/logrus"
  11. )
  12. type Parser struct {
  13. log *logrus.Logger
  14. }
  15. func NewParser(log *logrus.Logger) *Parser {
  16. return &Parser{log: log}
  17. }
  18. func (p Parser) GetParserName() string {
  19. return "ass"
  20. }
  21. /*
  22. DetermineFileTypeFromFile 确定字幕文件的类型,是双语字幕或者某一种语言等等信息
  23. 当 error 是 common.DetermineFileTypeFromFileExtNotFitASSorSSA
  24. 需要额外的处理逻辑,比如不用报错,而是跳过后续的逻辑
  25. */
  26. func (p Parser) DetermineFileTypeFromFile(filePath string) (bool, *subparser.FileInfo, error) {
  27. nowExt := filepath.Ext(filePath)
  28. p.log.Debugln("DetermineFileTypeFromFile", p.GetParserName(), filePath)
  29. fBytes, err := os.ReadFile(filePath)
  30. if err != nil {
  31. return false, nil, err
  32. }
  33. inBytes, err := language.ChangeFileCoding2UTF8(fBytes)
  34. if err != nil {
  35. return false, nil, err
  36. }
  37. return p.DetermineFileTypeFromBytes(inBytes, nowExt)
  38. }
  39. // DetermineFileTypeFromBytes 确定字幕文件的类型,是双语字幕或者某一种语言等等信息
  40. func (p Parser) DetermineFileTypeFromBytes(inBytes []byte, nowExt string) (bool, *subparser.FileInfo, error) {
  41. allString := string(inBytes)
  42. // 注意,需要替换掉 \r 不然正则表达式会有问题
  43. allString = strings.ReplaceAll(allString, "\r", "")
  44. // 找到 start end text
  45. matched := regex_things.ReMatchDialogueASS.FindAllStringSubmatch(allString, -1)
  46. if matched == nil || len(matched) < 1 {
  47. p.log.Debugln("DetermineFileTypeFromBytes can't found DialoguesFilter, Skip")
  48. return false, nil, nil
  49. }
  50. subFileInfo := subparser.FileInfo{}
  51. subFileInfo.Content = string(inBytes)
  52. subFileInfo.Ext = nowExt
  53. subFileInfo.Dialogues = make([]subparser.OneDialogue, 0)
  54. subFileInfo.DialoguesFilter = make([]subparser.OneDialogue, 0)
  55. // 这里需要统计一共有几个 \N,以及这个数量在整体行数中的比例,这样就知道是不是双语字幕了
  56. countLineFeed := 0
  57. // 有意义的对话统计数,排除 Style 类型
  58. usefullyDialogueCount := 0
  59. // 先进行字幕 StyleName 的出现次数排序,找到最多的,就是常规字幕的,不是特效的
  60. var nameMap = make(map[string]int)
  61. for _, oneLine := range matched {
  62. nowStyleName := oneLine[3]
  63. _, ok := nameMap[nowStyleName]
  64. if ok == false {
  65. nameMap[nowStyleName] = 1
  66. } else {
  67. nameMap[nowStyleName]++
  68. }
  69. }
  70. /*
  71. 现在可能会遇到两种可能出现的双语字幕:
  72. 1.
  73. 一个 Dialogue 中,直接描述两个语言
  74. 2.
  75. 排序的目标是找出 Name 有几种,一般来说都是 Default 一种
  76. 但是目前也会有用这个 Name 来做双语标记的
  77. 比如相同的时间点:一个 Name 是 Chs Subtitle
  78. 一个 Name 是 Eng Subtitle
  79. 那么排序来说,就应该是 Top1、2 两个
  80. 但是之前是为了剔除某一些特效动画,进行排序后只找 Top 1,但是遇到上面 2 的情况
  81. 解析就只读取到一个语言的字幕了
  82. 那么现在的解决方案就是,一开始先进行 Name 的统计。
  83. 然后统计是否有一个相同的时间段,出现了两个 Dialogue,比如:
  84. 0:01:01.00-0:01:11.00 这个时间段,一共有两个 Dialogue 使用了,然后需要统计这种情况占比所有的 Dialogue 的比例
  85. 如果比例很高,那么就认为是情况 2 的双语字幕
  86. 如果没有那么多,或者就没得。就任务是情况 1 的双语字幕,这个也不能说就是双语字幕,只不过走之前的逻辑就够了。
  87. */
  88. mapByValue := sortMapByValue(nameMap)
  89. // 把所有的对白缓存下来,其实优先是把时间信息缓存,其他信息无所谓
  90. p.oneLineSubDialogueParser0(matched, &subFileInfo)
  91. if p.detectOneOrTwoLineDialogue(matched) == true {
  92. // 情况1
  93. usefullyDialogueCount, countLineFeed = p.oneLineSubDialogueParser1(matched, mapByValue, &subFileInfo)
  94. } else {
  95. // 情况2
  96. usefullyDialogueCount, countLineFeed = p.oneLineSubDialogueParser2(matched, mapByValue, &subFileInfo)
  97. }
  98. // 再分析
  99. // 需要判断每一个 Line 是啥语言,[语言的code]次数
  100. var langDict map[int]int
  101. langDict = make(map[int]int)
  102. // 抽取出所有的中文对话
  103. var chLines = make([]string, 0)
  104. // 抽取出所有的第二语言对话
  105. var otherLines = make([]string, 0)
  106. // 抽取出来的对话数组,为了后续用来匹配和修改时间轴
  107. var usefulDialogueExs = make([]subparser.OneDialogueEx, 0)
  108. // 在这之前需要把 subFileInfo.DialoguesFilter 的内容填好,Lines 这里如果是单种语言应该就是一个元素,如果是双语就需要拆分成两个元素
  109. // 这样向后传递就简单了,也统一了
  110. emptyLines := 0
  111. for _, dialogue := range subFileInfo.DialoguesFilter {
  112. emptyLines += language.DetectSubLangAndStatistics(dialogue, langDict, &usefulDialogueExs, &chLines, &otherLines)
  113. }
  114. // 从统计出来的字典,找出 Top 1 或者 2 的出来,然后计算出是什么语言的字幕
  115. detectLang := language.SubLangStatistics2SubLangType(float32(countLineFeed), float32(usefullyDialogueCount-emptyLines), langDict, chLines)
  116. subFileInfo.Lang = detectLang
  117. subFileInfo.Data = inBytes
  118. subFileInfo.DialoguesFilterEx = usefulDialogueExs
  119. subFileInfo.CHLines = chLines
  120. subFileInfo.OtherLines = otherLines
  121. return true, &subFileInfo, nil
  122. }
  123. // oneLineSubDialogueParser0 情况 0 时候的解析器,不过滤,只要是对白都加进去
  124. func (p Parser) oneLineSubDialogueParser0(matched [][]string, subFileInfo *subparser.FileInfo) {
  125. for _, oneLine := range matched {
  126. startTime := oneLine[1]
  127. endTime := oneLine[2]
  128. nowStyleName := oneLine[3]
  129. nowText := oneLine[4]
  130. odl := subparser.OneDialogue{
  131. StyleName: nowStyleName,
  132. StartTime: startTime,
  133. EndTime: endTime,
  134. Lines: []string{nowText},
  135. }
  136. subFileInfo.Dialogues = append(subFileInfo.Dialogues, odl)
  137. }
  138. }
  139. // oneLineSubDialogueParser1 情况 1 时候的解析器
  140. func (p Parser) oneLineSubDialogueParser1(matched [][]string, mapByValue StyleNameInfos, subFileInfo *subparser.FileInfo) (int, int) {
  141. var countLineFeed = 0
  142. var usefullyDialogueCount = 0
  143. // 先读取一次字幕文件
  144. for _, oneLine := range matched {
  145. if len(oneLine) < 1 {
  146. continue
  147. }
  148. // 排除特效内容,只统计有意义的对话部分
  149. if strings.Contains(oneLine[0], mapByValue[0].Name) == false {
  150. continue
  151. }
  152. usefullyDialogueCount++
  153. startTime := oneLine[1]
  154. endTime := oneLine[2]
  155. nowStyleName := oneLine[3]
  156. nowText := oneLine[4]
  157. odl := subparser.OneDialogue{
  158. StyleName: nowStyleName,
  159. StartTime: startTime,
  160. EndTime: endTime,
  161. }
  162. odl.Lines = make([]string, 0)
  163. countLineFeed = p.parseOneDialogueText(nowText, &odl, countLineFeed)
  164. subFileInfo.DialoguesFilter = append(subFileInfo.DialoguesFilter, odl)
  165. }
  166. return usefullyDialogueCount, countLineFeed
  167. }
  168. // oneLineSubDialogueParser2 情况 2 时候的解析器
  169. func (p Parser) oneLineSubDialogueParser2(matched [][]string, mapByValue StyleNameInfos, subFileInfo *subparser.FileInfo) (int, int) {
  170. var countLineFeed = 0
  171. var usefullyDialogueCount = 0
  172. //var timeMap = make(map[string]subparser.OneDialogue, 0)
  173. // 更换数据结构的原因是为了能够使用顺序,go 内置的 map 不是顺序的,是随机的,会导致后续的逻辑出问题
  174. var timeMap = treemap.NewWithStringComparator()
  175. // 先读取一次字幕文件
  176. for _, oneLine := range matched {
  177. usefullyDialogueCount++
  178. // 这里可能会统计到特效的部分,但是这里忽略这个问题,因为目标不是这个
  179. // 统计 Dialogue 的开始和结束时间
  180. startTime := oneLine[1]
  181. endTime := oneLine[2]
  182. nowStyleName := oneLine[3]
  183. nowText := oneLine[4]
  184. mergeTime := startTime + "_" + endTime
  185. value, ok := timeMap.Get(mergeTime)
  186. if ok == false {
  187. // 首次新增
  188. odl := subparser.OneDialogue{
  189. StyleName: nowStyleName,
  190. StartTime: startTime,
  191. EndTime: endTime,
  192. }
  193. odl.Lines = make([]string, 0)
  194. countLineFeed = p.parseOneDialogueText(nowText, &odl, countLineFeed)
  195. timeMap.Put(mergeTime, odl)
  196. } else {
  197. // 双语
  198. odl := value.(subparser.OneDialogue)
  199. countLineFeed = p.parseOneDialogueText(nowText, &odl, countLineFeed)
  200. timeMap.Put(mergeTime, odl)
  201. }
  202. }
  203. for _, value := range timeMap.Values() {
  204. odl := value.(subparser.OneDialogue)
  205. subFileInfo.DialoguesFilter = append(subFileInfo.DialoguesFilter, odl)
  206. }
  207. return usefullyDialogueCount, countLineFeed
  208. }
  209. // parseOneDialogueText 对话的对白内容解析
  210. func (p Parser) parseOneDialogueText(nowText string, odl *subparser.OneDialogue, countLineFeed int) int {
  211. // nowText 优先移除 \h 这个是替换空格, \h 是让两个词在一行,不换行显示
  212. nowText = strings.ReplaceAll(nowText, `\h`, " ")
  213. // nowText 这个需要先把 {} 花括号内的内容给移除
  214. nowText1 := regex_things.ReMatchBrace.ReplaceAllString(nowText, "")
  215. nowText1 = regex_things.ReMatchBracket.ReplaceAllString(nowText1, "")
  216. nowText1 = strings.TrimRight(nowText1, "\r")
  217. // 然后判断是否有 \N 或者 \n
  218. // 直接把 \n 替换为 \N 来解析
  219. nowText1 = strings.ReplaceAll(nowText1, `\n`, `\N`)
  220. if strings.Contains(nowText1, `\N`) {
  221. // 有,那么就需要再次切割,一般是双语字幕
  222. for _, matched2 := range regex_things.ReCutDoubleLanguage.FindAllStringSubmatch(nowText1, -1) {
  223. if matched2 == nil {
  224. continue
  225. }
  226. for i, s := range matched2 {
  227. if i == 0 {
  228. continue
  229. }
  230. s = strings.ReplaceAll(s, `\N`, "")
  231. odl.Lines = append(odl.Lines, s)
  232. }
  233. }
  234. countLineFeed++
  235. } else {
  236. // 无,则可以直接添加
  237. nowText1 = strings.ReplaceAll(nowText1, `\N`, "")
  238. odl.Lines = append(odl.Lines, nowText1)
  239. }
  240. return countLineFeed
  241. }
  242. // detectOneOrTwoLineDialogue 优先检测一次字幕文件,可能存在的双语字幕的情况,是 1 还是 2 ,详细解释看调用此函数前的解释
  243. func (p Parser) detectOneOrTwoLineDialogue(matched [][]string) bool {
  244. /*
  245. 这里判断的方法粗暴一点,直接判断两个 Dialogue 都是一个时间段的比例是多少,达到了就是情况2,不是就是情况1
  246. */
  247. allDialogue := len(matched)
  248. twoLine := 0
  249. var timeMap = make(map[string]int, 0)
  250. // 先读取一次字幕文件
  251. for _, oneLine := range matched {
  252. // 这里可能会统计到特效的部分,但是这里忽略这个问题,因为目标不是这个
  253. // 统计 Dialogue 的开始和结束时间
  254. startTime := oneLine[1]
  255. endTime := oneLine[2]
  256. mergeTime := startTime + "_" + endTime
  257. _, ok := timeMap[mergeTime]
  258. if ok == false {
  259. timeMap[mergeTime] = 1
  260. } else {
  261. timeMap[mergeTime]++
  262. if timeMap[mergeTime] == 2 {
  263. twoLine++
  264. }
  265. }
  266. }
  267. // 目前看到的文件大概再 47% 以上,考虑到更多的“注释”、“特效”,至少有 38% 就够了
  268. per := float64(twoLine) / float64(allDialogue)
  269. if per > 0.38 {
  270. // 使用情况2的字幕分析方式
  271. return false
  272. }
  273. // 使用情况1的字幕分析方式
  274. return true
  275. }