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