소스 검색

完成 ffsubsync fit和fit_gss 代码的复现,差异在制作 VAD 的细节

Signed-off-by: allan716 <[email protected]>
allan716 3 년 전
부모
커밋
42ffec1d52

+ 2 - 1
internal/logic/sub_parser/ass/ass.go

@@ -139,12 +139,13 @@ func (p Parser) oneLineSubDialogueParser0(matched [][]string, subFileInfo *subpa
 		startTime := oneLine[1]
 		endTime := oneLine[2]
 		nowStyleName := oneLine[3]
+		nowText := oneLine[4]
 		odl := subparser.OneDialogue{
 			StyleName: nowStyleName,
 			StartTime: startTime,
 			EndTime:   endTime,
+			Lines:     []string{nowText},
 		}
-		odl.Lines = make([]string, 0)
 		subFileInfo.Dialogues = append(subFileInfo.Dialogues, odl)
 	}
 }

+ 2 - 2
internal/pkg/language/whatlanggo.go

@@ -31,7 +31,7 @@ func IsWhiteListLang(lang whatlanggo.Lang) bool {
 
 // DetectSubLangAndStatistics 检测语言然后统计
 func DetectSubLangAndStatistics(oneDialogue subparser.OneDialogue, langDict map[int]int,
-	usefulDialoguseEx *[]subparser.OneDialogueEx, chLines *[]string, otherLines *[]string) int {
+	usefulDialogueEx *[]subparser.OneDialogueEx, chLines *[]string, otherLines *[]string) int {
 
 	var oneDialogueEx subparser.OneDialogueEx
 	oneDialogueEx.StartTime = oneDialogue.StartTime
@@ -79,7 +79,7 @@ func DetectSubLangAndStatistics(oneDialogue subparser.OneDialogue, langDict map[
 		}
 	}
 
-	*usefulDialoguseEx = append(*usefulDialoguseEx, oneDialogueEx)
+	*usefulDialogueEx = append(*usefulDialogueEx, oneDialogueEx)
 
 	return emptyLine
 }

+ 5 - 0
internal/pkg/my_util/util.go

@@ -337,6 +337,11 @@ func WriteStrings2File(desfilePath string, strings []string) error {
 	return nil
 }
 
+func TimeNumber2Time(inputTimeNumber float64) time.Time {
+	newTime := time.Time{}.Add(time.Duration(inputTimeNumber * math.Pow10(9)))
+	return newTime
+}
+
 func Time2SecondNumber(inTime time.Time) float64 {
 	outSecond := 0.0
 	outSecond += float64(inTime.Hour() * 60 * 60)

+ 28 - 67
internal/pkg/sub_helper/sub_helper.go

@@ -392,12 +392,15 @@ func GetVADInfoFeatureFromSubNeedOffsetTimeWillInsert(fileInfo *subparser.FileIn
 	if subUnitMaxCount < 0 {
 		subUnitMaxCount = 0
 	}
+
+	nowDialogue := fileInfo.Dialogues
+
 	srcSubUnitList := make([]SubUnit, 0)
-	srcSubDialogueList := make([]subparser.OneDialogueEx, 0)
+	srcSubDialogueList := make([]subparser.OneDialogue, 0)
 	srcOneSubUnit := NewSubUnit()
 
 	// 最后一个对话的结束时间
-	lastDialogueExTimeEnd, err := my_util.ParseTime(fileInfo.DialoguesFilterEx[len(fileInfo.DialoguesFilterEx)-1].EndTime)
+	lastDialogueExTimeEnd, err := my_util.ParseTime(nowDialogue[len(nowDialogue)-1].EndTime)
 	if err != nil {
 		return nil, err
 	}
@@ -410,13 +413,13 @@ func GetVADInfoFeatureFromSubNeedOffsetTimeWillInsert(fileInfo *subparser.FileIn
 	println(startRangeTimeMin)
 	println(endRangeTimeMax)
 
-	for i := 0; i < len(fileInfo.DialoguesFilterEx); i++ {
+	for i := 0; i < len(nowDialogue); i++ {
 
-		oneDialogueExTimeStart, err := my_util.ParseTime(fileInfo.DialoguesFilterEx[i].StartTime)
+		oneDialogueExTimeStart, err := my_util.ParseTime(nowDialogue[i].StartTime)
 		if err != nil {
 			return nil, err
 		}
-		oneDialogueExTimeEnd, err := my_util.ParseTime(fileInfo.DialoguesFilterEx[i].EndTime)
+		oneDialogueExTimeEnd, err := my_util.ParseTime(nowDialogue[i].EndTime)
 		if err != nil {
 			return nil, err
 		}
@@ -428,6 +431,13 @@ func GetVADInfoFeatureFromSubNeedOffsetTimeWillInsert(fileInfo *subparser.FileIn
 			}
 		}
 
+		if nowDialogue[i].Lines == nil || len(nowDialogue[i].Lines) == 0 {
+			continue
+		}
+		// 如果当前的这一句话,为空,或者进过正则表达式剔除特殊字符后为空,则跳过
+		if my_util.ReplaceSpecString(nowDialogue[i].Lines[0], "") == "" {
+			continue
+		}
 		// 如果当前的这一句话,为空,或者进过正则表达式剔除特殊字符后为空,则跳过
 		if my_util.ReplaceSpecString(fileInfo.GetDialogueExContent(i), "") == "" {
 			continue
@@ -445,11 +455,11 @@ func GetVADInfoFeatureFromSubNeedOffsetTimeWillInsert(fileInfo *subparser.FileIn
 				srcOneSubUnit.Add(oneDialogueExTimeStart, oneDialogueExTimeEnd)
 			}
 			// 这一个单元的 Dialogue 需要合并起来,才能判断是否符合“钥匙”的要求
-			srcSubDialogueList = append(srcSubDialogueList, fileInfo.DialoguesFilterEx[i])
+			srcSubDialogueList = append(srcSubDialogueList, nowDialogue[i])
 
 		} else {
 			// 用完清空
-			srcSubDialogueList = make([]subparser.OneDialogueEx, 0)
+			srcSubDialogueList = make([]subparser.OneDialogue, 0)
 			// 将拼凑起来的对话组成一个单元进行存储起来
 			srcSubUnitList = append(srcSubUnitList, *srcOneSubUnit)
 			// 然后重置
@@ -469,7 +479,7 @@ func GetVADInfoFeatureFromSubNeedOffsetTimeWillInsert(fileInfo *subparser.FileIn
 func GetVADInfoFeatureFromSubNew(fileInfo *subparser.FileInfo, SkipFrontAndEndPer float64) (*SubUnit, error) {
 
 	outSubUnits := NewSubUnit()
-	if len(fileInfo.DialoguesFilterEx) <= 0 {
+	if len(fileInfo.Dialogues) <= 0 {
 		return nil, errors.New("GetVADInfoFeatureFromSubNew fileInfo Dialogue Length is 0")
 	}
 	/*
@@ -477,17 +487,15 @@ func GetVADInfoFeatureFromSubNew(fileInfo *subparser.FileInfo, SkipFrontAndEndPe
 		因为 VAD 的窗口是 10ms,那么需要多每一句话按 10 ms 的单位进行取整
 		每一句话开始、结束的时间,需要向下取整
 	*/
-	subStartTimeFloor, subEndTimeFloor, err := ReadSubStartAndEndTime(fileInfo)
-	if err != nil {
-		return nil, err
-	}
+	subStartTimeFloor := my_util.MakeFloor10msMultipleFromFloat(my_util.Time2SecondNumber(fileInfo.GetStartTime()))
+	subEndTimeFloor := my_util.MakeFloor10msMultipleFromFloat(my_util.Time2SecondNumber(fileInfo.GetEndTime()))
 	// 如果想要从 0 时间点开始算,那么 subStartTimeFloor 这个值就需要重置到0
 	subStartTimeFloor = 0
 	subFullSecondTimeFloor := subEndTimeFloor - subStartTimeFloor
 	// 根据这个时长就能够得到一个完整的 VAD List,然后再通过每一句对白进行 VAD 值的调整即可,这样就能够保证
 	// 相同的一个字幕因为使用 ffmpeg 导出 srt 和 ass 后的,可能存在总体时间轴不一致的问题
 	// 123.450 - > 12345
-	vadLen := int(subFullSecondTimeFloor * 100)
+	vadLen := int(subFullSecondTimeFloor*100) + 2
 	subVADs := make([]vad.VADInfo, vadLen)
 	subStartTimeFloor10ms := subStartTimeFloor * 100
 	for i := 0; i < vadLen; i++ {
@@ -499,20 +507,22 @@ func GetVADInfoFeatureFromSubNew(fileInfo *subparser.FileInfo, SkipFrontAndEndPe
 	skipEndIndex := vadLen - skipLen
 	// 现在需要从 fileInfo 的每一句对白也就对应一段连续的 VAD active = true 来进行改写,记得向下取整
 	lastDialogueIndex := 0
-	for index, dialogueEx := range fileInfo.DialoguesFilterEx {
+	for _, dialogue := range fileInfo.Dialogues {
 
+		if dialogue.Lines == nil || len(dialogue.Lines) == 0 {
+			continue
+		}
 		// 如果当前的这一句话,为空,或者进过正则表达式剔除特殊字符后为空,则跳过
-		if my_util.ReplaceSpecString(fileInfo.GetDialogueExContent(index), "") == "" {
+		if my_util.ReplaceSpecString(dialogue.Lines[0], "") == "" {
 			continue
 		}
-
 		// 字幕的开始时间
-		oneDialogueStartTime, err := my_util.ParseTime(dialogueEx.StartTime)
+		oneDialogueStartTime, err := my_util.ParseTime(dialogue.StartTime)
 		if err != nil {
 			return nil, err
 		}
 		// 字幕的结束时间
-		oneDialogueEndTime, err := my_util.ParseTime(dialogueEx.EndTime)
+		oneDialogueEndTime, err := my_util.ParseTime(dialogue.EndTime)
 		if err != nil {
 			return nil, err
 		}
@@ -573,52 +583,3 @@ func GetVADInfoFeatureFromSubNew(fileInfo *subparser.FileInfo, SkipFrontAndEndPe
 
 	return outSubUnits, nil
 }
-
-func ReadSubStartAndEndTime(fileInfo *subparser.FileInfo) (float64, float64, error) {
-
-	/*
-		因为是先构建完整的时间轴 VAD ,然后再用每一句话去修改对应的 VAD 段
-		那么,如果字幕第一句话时间轴有问题,就会出问题,所以这里返回的时候需要判断是否 StartTime 正确
-		因为可能会有一种情况,读取到的字幕是经过 V1 校正时间的,那么第一句和前几句话,可能时间是 Dialogue: 0,23:59:31.32,23:59:33.23
-		明显时间过大,导致减出来的值是负值,会越界访问
-	*/
-
-	getTimeFunc := func(fileInfo *subparser.FileInfo, startIndex int) (bool, float64, float64, error) {
-		// 字幕的开始时间
-		subStartTime, err := my_util.ParseTime(fileInfo.DialoguesFilterEx[startIndex].StartTime)
-		if err != nil {
-			return false, 0, 0, err
-		}
-		// 字幕的结束时间
-		subEndTime, err := my_util.ParseTime(fileInfo.DialoguesFilterEx[len(fileInfo.DialoguesFilterEx)-1].EndTime)
-		if err != nil {
-			return false, 0, 0, err
-		}
-		// 字幕的时长,对时间进行向下取整
-		subStartTimeFloor := my_util.MakeFloor10msMultipleFromFloat(my_util.Time2SecondNumber(subStartTime))
-		subEndTimeFloor := my_util.MakeFloor10msMultipleFromFloat(my_util.Time2SecondNumber(subEndTime))
-
-		if subEndTimeFloor-subStartTimeFloor < 0 {
-			// 说明 StartTime 的数值太大,不正常,超过 EndTime 了,startIndex 需要累加
-			return false, 0, 0, nil
-		}
-
-		return true, subStartTimeFloor, subEndTimeFloor, nil
-	}
-	startIndex := 0
-	var err error
-	var subStartTimeFloor, subEndTimeFloor float64
-	bok := false
-	for bok == false {
-		bok, subStartTimeFloor, subEndTimeFloor, err = getTimeFunc(fileInfo, startIndex)
-		if err != nil {
-			return 0, 0, err
-		}
-		if bok == true {
-			break
-		}
-		startIndex++
-	}
-
-	return subStartTimeFloor, subEndTimeFloor, err
-}

+ 60 - 9
internal/pkg/sub_timeline_fixer/pipeline.go

@@ -3,10 +3,10 @@ package sub_timeline_fixer
 import (
 	"fmt"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/gss"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_helper"
 	"github.com/allanpk716/ChineseSubFinder/internal/types/subparser"
 	"github.com/huandu/go-clone"
-	"sort"
 )
 
 type Pipeline struct {
@@ -19,19 +19,45 @@ func NewPipeline() *Pipeline {
 	}
 }
 
-func (p Pipeline) FitGSS(infoBase, infoSrc *subparser.FileInfo) error {
+func (p Pipeline) Fit(infoBase, infoSrc *subparser.FileInfo, useGSS bool) error {
 
 	pipeResults := make([]PipeResult, 0)
 	// 排序
-	sort.Sort(subparser.OneDialogueByStartTime(infoBase.DialoguesFilter))
-	sort.Sort(subparser.OneDialogueByStartTime(infoSrc.DialoguesFilter))
+	infoBase.SortDialogues()
+	infoSrc.SortDialogues()
+	println(fmt.Sprintf("%f", my_util.Time2SecondNumber(infoBase.GetStartTime())))
+	println(fmt.Sprintf("%f", my_util.Time2SecondNumber(infoBase.GetEndTime())))
 	// 解析处 VAD 信息
 	baseUnitNew, err := sub_helper.GetVADInfoFeatureFromSubNew(infoBase, 0)
 	if err != nil {
 		return err
 	}
-
-	framerateRatios := p.getFramerateRatios2Try()
+	/*
+			这里复现 ffsubsync 的思路
+			1. 首先由 getFramerateRatios2Try 得到多个帧数比率的数值,理论上有以下 7 个值:
+		       将 frameRateRatio = 1.0 插入到 framerateRatios 这个队列的首位
+				[0] 1.0
+				[1] 1.001001001001001
+				[2] 1.0427093760427095
+				[3] 1.0416666666666667
+				[4] 0.9989999999999999
+				[5] 0.9590399999999999
+				[6] 0.96
+				得到一个 framerateRatios 列表
+			2. 计算 base 字幕的 num_frames,以及 frameRateRatio = 1.0 时 src 字幕的 num_frames
+				推断 frame ratio 比率是多少,得到一个,inferred_framerate_ratio_from_length = base / src
+				把这个值插入到 framerateRatios 的尾部也就是第八个元素
+			3. 使用上述的 framerateRatios 作为传入参数,开始 FFT 模块的 fit 计算,得到(分数、偏移)信息,选择分数最大的作为匹配的结论
+
+	*/
+	// 1.
+	framerateRatios := make([]float64, 0)
+	framerateRatios = p.getFramerateRatios2Try()
+	// 2.
+	inferredFramerateRatioFromLength := float64(infoBase.GetNumFrames()) / float64(infoSrc.GetNumFrames())
+	framerateRatios = append(framerateRatios, inferredFramerateRatioFromLength)
+	// 3.
+	fffAligner := NewFFTAligner(DefaultMaxOffsetSeconds, SampleRate)
 	for _, framerateRatio := range framerateRatios {
 
 		/*
@@ -44,21 +70,46 @@ func (p Pipeline) FitGSS(infoBase, infoSrc *subparser.FileInfo) error {
 		// 1. parse			解析字幕
 		tmpInfoSrc := clone.Clone(infoSrc).(*subparser.FileInfo)
 		// 2. scale			根据帧数比率调整时间轴
-		err := tmpInfoSrc.ChangeDialoguesFilterExTimeByFramerateRatio(framerateRatio)
+		err = tmpInfoSrc.ChangeDialoguesTimeByFramerateRatio(framerateRatio)
 		if err != nil {
 			// 还原
-			println("ChangeDialoguesFilterExTimeByFramerateRatio", err)
+			println("ChangeDialoguesTimeByFramerateRatio", err)
 			tmpInfoSrc = clone.Clone(infoSrc).(*subparser.FileInfo)
 		}
+		// 3. speech_extract	从字幕转换为 VAD 的语音检测信息
 		tmpSrcInfoUnit, err := sub_helper.GetVADInfoFeatureFromSubNew(tmpInfoSrc, 0)
 		if err != nil {
 			return err
 		}
+		// 不是用 GSS
+		bestOffset, score := fffAligner.Fit(baseUnitNew.GetVADFloatSlice(), tmpSrcInfoUnit.GetVADFloatSlice())
+		pipeResult := PipeResult{
+			Score:       score,
+			BestOffset:  bestOffset,
+			ScaleFactor: framerateRatio,
+		}
+		pipeResults = append(pipeResults, pipeResult)
+	}
 
+	if useGSS == true {
+		// 最后一个才需要额外使用 GSS
+		// 使用 GSS
 		optFunc := func(framerateRatio float64, isLastIter bool) float64 {
 
-			fffAligner := NewFFTAligner(DefaultMaxOffsetSeconds, SampleRate)
+			// 1. parse			解析字幕
+			tmpInfoSrc := clone.Clone(infoSrc).(*subparser.FileInfo)
+			// 2. scale			根据帧数比率调整时间轴
+			err = tmpInfoSrc.ChangeDialoguesTimeByFramerateRatio(framerateRatio)
+			if err != nil {
+				// 还原
+				println("ChangeDialoguesTimeByFramerateRatio", err)
+				tmpInfoSrc = clone.Clone(infoSrc).(*subparser.FileInfo)
+			}
 			// 3. speech_extract	从字幕转换为 VAD 的语音检测信息
+			tmpSrcInfoUnit, err := sub_helper.GetVADInfoFeatureFromSubNew(tmpInfoSrc, 0)
+			if err != nil {
+				return 0
+			}
 			// 然后进行 base 与 src 匹配计算,将每一次变动 framerateRatio 计算得到的 偏移值和分数进行记录
 			bestOffset, score := fffAligner.Fit(baseUnitNew.GetVADFloatSlice(), tmpSrcInfoUnit.GetVADFloatSlice())
 			println(fmt.Sprintf("got score %.0f (offset %d) for ratio %.3f", score, bestOffset, framerateRatio))

+ 1 - 9
internal/pkg/sub_timeline_fixer/pipeline_test.go

@@ -69,16 +69,8 @@ func TestPipeline_FitGSS(t *testing.T) {
 			if bFind == false {
 				t.Fatal("sub not match")
 			}
-
-			//bFind, orgFix, err := subParserHub.DetermineFileTypeFromFile(tt.args.orgFixSubFile)
-			//if err != nil {
-			//	t.Fatal(err)
-			//}
-			//if bFind == false {
-			//	t.Fatal("sub not match")
-			//}
 			// ---------------------------------------------------------------------------------------
-			err = NewPipeline().FitGSS(infoBase, infoSrc)
+			err = NewPipeline().Fit(infoBase, infoSrc, true)
 			if (err != nil) != tt.wantErr {
 				t.Errorf("GetOffsetTimeV3() error = %v, wantErr %v", err, tt.wantErr)
 				return

+ 125 - 19
internal/types/subparser/fileinfo.go

@@ -5,6 +5,7 @@ import (
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
 	"github.com/allanpk716/ChineseSubFinder/internal/types/language"
 	"math"
+	"sort"
 	"time"
 )
 
@@ -16,13 +17,20 @@ type FileInfo struct {
 	Lang              language.MyLanguage // 识别出来的语言
 	FileFullPath      string              // 字幕文件的全路径
 	Data              []byte              // 字幕的二进制文件内容
-	Dialogues         []OneDialogue       // 整个字幕文件的所有对话
+	Dialogues         []OneDialogue       // 整个字幕文件的所有对话,如果是做时间轴匹配,就使用原始的
 	DialoguesFilter   []OneDialogue       // 整个字幕文件的所有对话,过滤掉特殊字符的对白
 	DialoguesFilterEx []OneDialogueEx     // 整个字幕文件的所有对话,过滤掉特殊字符的对白,这里会把一句话中支持的 中、英、韩、日 四国语言给分离出来
 	CHLines           []string            // 抽取出所有的中文对话
 	OtherLines        []string            // 抽取出所有的第二语言对话,可能是英文、韩文、日文
 }
 
+// SortDialogues 排序对话,时间递减
+func (f *FileInfo) SortDialogues() {
+	sort.Sort(OneDialogueByStartTime(f.Dialogues))
+	sort.Sort(OneDialogueByStartTime(f.DialoguesFilter))
+	sort.Sort(OneDialogueByStartTimeEx(f.DialoguesFilterEx))
+}
+
 // GetTimeFormat 获取时间轴的格式化格式
 func (f FileInfo) GetTimeFormat() string {
 	if f.Ext == common.SubExtASS || f.Ext == common.SubExtSSA {
@@ -53,32 +61,75 @@ func (f FileInfo) GetDialogueExContent(index int) string {
 	}
 }
 
-// ChangeDialoguesFilterExTimeByFramerateRatio 根据帧数比率调整时间轴 对应 ffsubsync -- SubtitleScaler
-func (f *FileInfo) ChangeDialoguesFilterExTimeByFramerateRatio(framerateRatio float64) error {
+// ChangeDialoguesTimeByFramerateRatio 根据帧数比率调整时间轴 对应 ffsubsync -- SubtitleScaler
+func (f *FileInfo) ChangeDialoguesTimeByFramerateRatio(framerateRatio float64) error {
 
 	timeFormat := f.GetTimeFormat()
-	for i := 0; i < len(f.DialoguesFilter); i++ {
-
-		oneDialogue := f.DialoguesFilter[i]
-		timeStart, err := my_util.ParseTime(oneDialogue.StartTime)
-		if err != nil {
-			return err
-		}
-		timeEnd, err := my_util.ParseTime(oneDialogue.EndTime)
-		if err != nil {
-			return err
-		}
+	f.changeOneDialoguesFramerateRatio(f.Dialogues, framerateRatio, timeFormat)
+	f.changeOneDialoguesFramerateRatio(f.DialoguesFilter, framerateRatio, timeFormat)
+	f.changeOneDialogueExsFramerateRatio(f.DialoguesFilterEx, framerateRatio, timeFormat)
+
+	return nil
+}
+
+func (f *FileInfo) changeOneDialoguesFramerateRatio(oneDialogues []OneDialogue, framerateRatio float64, timeFormat string) {
+	for i := 0; i < len(oneDialogues); i++ {
+
+		timeStart := oneDialogues[i].GetStartTime()
+		timeEnd := oneDialogues[i].GetEndTime()
 		timeStartNumber := my_util.Time2SecondNumber(timeStart)
 		timeEndNumber := my_util.Time2SecondNumber(timeEnd)
 
-		scaleTimeStart := time.Time{}.Add(time.Duration(timeStartNumber * framerateRatio * math.Pow10(9)))
-		scaleTimeEnd := time.Time{}.Add(time.Duration(timeEndNumber * framerateRatio * math.Pow10(9)))
+		scaleTimeStart := my_util.TimeNumber2Time(timeStartNumber * framerateRatio)
+		scaleTimeEnd := my_util.TimeNumber2Time(timeEndNumber * framerateRatio)
 
-		f.DialoguesFilter[i].StartTime = my_util.Time2SubTimeString(scaleTimeStart, timeFormat)
-		f.DialoguesFilter[i].EndTime = my_util.Time2SubTimeString(scaleTimeEnd, timeFormat)
+		oneDialogues[i].StartTime = my_util.Time2SubTimeString(scaleTimeStart, timeFormat)
+		oneDialogues[i].EndTime = my_util.Time2SubTimeString(scaleTimeEnd, timeFormat)
 	}
+}
 
-	return nil
+func (f *FileInfo) changeOneDialogueExsFramerateRatio(oneDialogues []OneDialogueEx, framerateRatio float64, timeFormat string) {
+	for i := 0; i < len(oneDialogues); i++ {
+
+		timeStart := oneDialogues[i].GetStartTime()
+		timeEnd := oneDialogues[i].GetEndTime()
+		timeStartNumber := my_util.Time2SecondNumber(timeStart)
+		timeEndNumber := my_util.Time2SecondNumber(timeEnd)
+
+		scaleTimeStart := my_util.TimeNumber2Time(timeStartNumber * framerateRatio)
+		scaleTimeEnd := my_util.TimeNumber2Time(timeEndNumber * framerateRatio)
+
+		oneDialogues[i].StartTime = my_util.Time2SubTimeString(scaleTimeStart, timeFormat)
+		oneDialogues[i].EndTime = my_util.Time2SubTimeString(scaleTimeEnd, timeFormat)
+	}
+}
+
+// GetStartTime 获取的是从 Dialogues 得到的
+func (f FileInfo) GetStartTime() time.Time {
+	startTime := math.MaxFloat64
+	for i := 0; i < len(f.Dialogues); i++ {
+		// 找到最小的开始时间
+		tmpNowStartTimeNumber := my_util.Time2SecondNumber(f.Dialogues[i].GetStartTime())
+		startTime = math.Min(startTime, tmpNowStartTimeNumber)
+	}
+	return my_util.TimeNumber2Time(startTime)
+}
+
+// GetEndTime 获取的是从 Dialogues 得到的
+func (f FileInfo) GetEndTime() time.Time {
+	endTime := -math.MaxFloat64
+	for i := 0; i < len(f.Dialogues); i++ {
+		// 找到最大的结束时间
+		tmpNowEndTimeNumber := my_util.Time2SecondNumber(f.Dialogues[i].GetEndTime())
+		endTime = math.Max(endTime, tmpNowEndTimeNumber)
+	}
+	return my_util.TimeNumber2Time(endTime)
+}
+
+// GetNumFrames 获取这个字幕的时间 Frame 数量
+func (f FileInfo) GetNumFrames() int {
+
+	return int(math.Abs((my_util.Time2SecondNumber(f.GetEndTime()) - my_util.Time2SecondNumber(f.GetStartTime())) * 100))
 }
 
 // OneDialogue 一句对话
@@ -89,6 +140,22 @@ type OneDialogue struct {
 	Lines     []string // 台词
 }
 
+func (o OneDialogue) GetStartTime() time.Time {
+	srcTimeStartNow, err := my_util.ParseTime(o.StartTime)
+	if err != nil {
+		return time.Time{}
+	}
+	return srcTimeStartNow
+}
+
+func (o OneDialogue) GetEndTime() time.Time {
+	srcTimeEndNow, err := my_util.ParseTime(o.EndTime)
+	if err != nil {
+		return time.Time{}
+	}
+	return srcTimeEndNow
+}
+
 type OneDialogueByStartTime []OneDialogue
 
 func (d OneDialogueByStartTime) Len() int {
@@ -122,6 +189,45 @@ type OneDialogueEx struct {
 	JpLine    string
 }
 
+func (o OneDialogueEx) GetStartTime() time.Time {
+	srcTimeStartNow, err := my_util.ParseTime(o.StartTime)
+	if err != nil {
+		return time.Time{}
+	}
+	return srcTimeStartNow
+}
+
+func (o OneDialogueEx) GetEndTime() time.Time {
+	srcTimeEndNow, err := my_util.ParseTime(o.EndTime)
+	if err != nil {
+		return time.Time{}
+	}
+	return srcTimeEndNow
+}
+
+type OneDialogueByStartTimeEx []OneDialogueEx
+
+func (d OneDialogueByStartTimeEx) Len() int {
+	return len(d)
+}
+
+func (d OneDialogueByStartTimeEx) Swap(i, j int) {
+	d[i], d[j] = d[j], d[i]
+}
+
+func (d OneDialogueByStartTimeEx) Less(i, j int) bool {
+
+	subStartTimeI, err := my_util.ParseTime(d[i].StartTime)
+	if err != nil {
+		return false
+	}
+	subStartTimeJ, err := my_util.ParseTime(d[j].StartTime)
+	if err != nil {
+		return false
+	}
+	return my_util.Time2SecondNumber(subStartTimeI) < my_util.Time2SecondNumber(subStartTimeJ)
+}
+
 const (
 	Sub_Ext_Mark_Default = ".default" // 指定这个字幕是默认的
 	Sub_Ext_Mark_Forced  = ".forced"  // 指定这个字幕是强制的