Browse Source

保存进度

Signed-off-by: 716 <[email protected]>
716 3 years ago
parent
commit
d254981b80

+ 1 - 1
internal/pkg/emby_api/emby_api_test.go

@@ -59,7 +59,7 @@ func TestEmbyHelper_UpdateVideoSubList(t *testing.T) {
 	// 194046 -- 窃贼军团
 	// 178071 -- The Night House
 	// 215162 --  Black Lotus - S01E03
-	err := em.UpdateVideoSubList("215162")
+	err := em.UpdateVideoSubList("145499")
 	if err != nil {
 		t.Fatal(err)
 	}

+ 69 - 0
internal/pkg/gss/gss.go

@@ -0,0 +1,69 @@
+package gss
+
+import (
+	"log"
+	"math"
+)
+
+var (
+	sqrt5   = math.Sqrt(5)
+	invphi  = (sqrt5 - 1) / 2 //# 1/phi
+	invphi2 = (3 - sqrt5) / 2 //# 1/phi^2
+	nan     = math.NaN()
+)
+
+// Gss golden section search (recursive version)
+// https://en.wikipedia.org/wiki/Golden-section_search
+// https://github.com/pa-m/optimize/blob/master/gss.go
+// '''
+// Golden section search, recursive.
+// Given a function f with a single local minimum in
+// the interval [a,b], gss returns a subset interval
+// [c,d] that contains the minimum with d-c <= tol.
+//
+// logger may be nil
+//
+// example:
+// >>> f = lambda x: (x-2)**2
+// >>> a = 1
+// >>> b = 5
+// >>> tol = 1e-5
+// >>> (c,d) = gssrec(f, a, b, tol)
+// >>> print (c,d)
+// (1.9999959837979107, 2.0000050911830893)
+// '''
+func Gss(f func(float64) float64, a, b, tol float64, logger *log.Logger) (float64, float64) {
+	return gss(f, a, b, tol, nan, nan, nan, nan, nan, logger)
+}
+func gss(f func(float64) float64, a, b, tol, h, c, d, fc, fd float64, logger *log.Logger) (float64, float64) {
+	if a > b {
+		a, b = b, a
+	}
+	h = b - a
+	it := 0
+	for {
+		if logger != nil {
+			logger.Printf("%d\t%9.6g\t%9.6g\n", it, a, b)
+		}
+		it++
+		if h < tol {
+			return a, b
+		}
+		if a > b {
+			a, b = b, a
+		}
+		if math.IsNaN(c) {
+			c = a + invphi2*h
+			fc = f(c)
+		}
+		if math.IsNaN(d) {
+			d = a + invphi*h
+			fd = f(d)
+		}
+		if fc < fd {
+			b, h, c, fc, d, fd = d, h*invphi, nan, nan, c, fc
+		} else {
+			a, h, c, fc, d, fd = c, h*invphi, d, fd, nan, nan
+		}
+	}
+}

+ 55 - 0
internal/pkg/gss/gss_test.go

@@ -0,0 +1,55 @@
+package gss
+
+import (
+	"log"
+	"os"
+	"testing"
+)
+
+func TestGss(t *testing.T) {
+	f := func(x float64) float64 {
+		tmp := x - 2
+		return tmp * tmp
+	}
+	logger := log.New(os.Stdout, "", 0)
+	a, b := Gss(f, 1, 5, 1e-4, logger)
+	println(a, b)
+	// 1e-6
+	// Output:
+	// 0	        1	        5
+	// 1	        1	  3.47214
+	// 2	        1	  2.52786
+	// 3	  1.58359	  2.52786
+	// 4	  1.58359	  2.16718
+	// 5	   1.8065	  2.16718
+	// 6	  1.94427	  2.16718
+	// 7	  1.94427	  2.08204
+	// 8	  1.94427	  2.02942
+	// 9	  1.97679	  2.02942
+	// 10	  1.97679	  2.00932
+	// 11	  1.98922	  2.00932
+	// 12	  1.99689	  2.00932
+	// 13	  1.99689	  2.00457
+	// 14	  1.99689	  2.00164
+	// 15	  1.99871	  2.00164
+	// 16	  1.99871	  2.00052
+	// 17	   1.9994	  2.00052
+	// 18	  1.99983	  2.00052
+	// 19	  1.99983	  2.00025
+	// 20	  1.99983	  2.00009
+	// 21	  1.99993	  2.00009
+	// 22	  1.99993	  2.00003
+	// 23	  1.99997	  2.00003
+	// 24	  1.99999	  2.00003
+	// 25	  1.99999	  2.00001
+	// 26	  1.99999	  2.00001
+	// 27	        2	  2.00001
+	// 28	        2	        2
+	// 29	        2	        2
+	// 30	        2	        2
+	// 31	        2	        2
+	// 32	        2	        2
+
+	a, b = Gss(f, 0.9, 1.1, 1e-4, logger)
+	println(a, b)
+}

+ 0 - 0
internal/pkg/sub_timeline_fixer/fix_result.go → internal/pkg/sub_timeline_fixer/fix_result_v2.go


+ 186 - 8
internal/pkg/sub_timeline_fixer/fixer.go

@@ -507,7 +507,7 @@ func (s *SubTimelineFixer) GetOffsetTimeV2(baseUnit, srcUnit *sub_helper.SubUnit
 		})
 		// 实际 FFT 的匹配逻辑函数
 		// 时间轴差值数组
-		matchInfo, err := s.slidingWindowProcessor(&windowInfo)
+		matchInfo, err := s.slidingWindowProcessorV2(&windowInfo)
 		if err != nil {
 			return false, nil, err
 		}
@@ -521,7 +521,7 @@ func (s *SubTimelineFixer) GetOffsetTimeV2(baseUnit, srcUnit *sub_helper.SubUnit
 	for index, matchInfo := range matchedInfos {
 
 		log_helper.GetLogger().Infoln(index, "------------------------------------")
-		outCorrelationFixResult := s.calcMeanAndSD(matchInfo.StartDiffTimeListEx, matchInfo.StartDiffTimeList)
+		outCorrelationFixResult := s.calcMeanAndSDV2(matchInfo.StartDiffTimeListEx, matchInfo.StartDiffTimeList)
 		log_helper.GetLogger().Infoln(fmt.Sprintf("FFTAligner Old Mean: %v SD: %f Per: %v", outCorrelationFixResult.OldMean, outCorrelationFixResult.OldSD, outCorrelationFixResult.Per))
 		log_helper.GetLogger().Infoln(fmt.Sprintf("FFTAligner New Mean: %v SD: %f Per: %v", outCorrelationFixResult.NewMean, outCorrelationFixResult.NewSD, outCorrelationFixResult.Per))
 
@@ -574,7 +574,7 @@ func (s *SubTimelineFixer) GetOffsetTimeV2(baseUnit, srcUnit *sub_helper.SubUnit
 	for index, fixedResult := range fixedResults {
 		// SD 大于 0.1 或者是 当前的 NewMean 与上一个点的 NewMean 差值大于 0.3
 		if fixedResult.NewSD >= 0.1 || (index > 1 && math.Abs(fixedResult.NewMean-fixedResults[index-1].NewMean) > 0.3) {
-			bok, newMean, newSD := s.fixOnePart(index, fixedResults)
+			bok, newMean, newSD := s.fixOnePartV2(index, fixedResults)
 			if bok == true {
 				fixedResults[index].NewMean = newMean
 				fixedResults[index].NewSD = newSD
@@ -585,8 +585,8 @@ func (s *SubTimelineFixer) GetOffsetTimeV2(baseUnit, srcUnit *sub_helper.SubUnit
 	return true, fixedResults, nil
 }
 
-// fixOnePart 轻微地跳动可以根据左或者右去微调
-func (s SubTimelineFixer) fixOnePart(startIndex int, fixedResults []FixResult) (bool, float64, float64) {
+// fixOnePartV2 轻微地跳动可以根据左或者右去微调
+func (s SubTimelineFixer) fixOnePartV2(startIndex int, fixedResults []FixResult) (bool, float64, float64) {
 
 	/*
 		找到这样情况的进行修正
@@ -667,8 +667,8 @@ func (s SubTimelineFixer) fixOnePart(startIndex int, fixedResults []FixResult) (
 	return false, 0, 0
 }
 
-// slidingWindowProcessor 滑动窗口计算时间轴偏移
-func (s *SubTimelineFixer) slidingWindowProcessor(windowInfo *WindowInfo) (*MatchInfo, error) {
+// slidingWindowProcessorV2 滑动窗口计算时间轴偏移
+func (s *SubTimelineFixer) slidingWindowProcessorV2(windowInfo *WindowInfo) (*MatchInfo, error) {
 
 	// -------------------------------------------------
 	var bUseSubOrAudioAsBase = true
@@ -821,7 +821,7 @@ func (s *SubTimelineFixer) slidingWindowProcessor(windowInfo *WindowInfo) (*Matc
 	return &outMatchInfo, nil
 }
 
-func (s *SubTimelineFixer) calcMeanAndSD(startDiffTimeList stat.Float64Slice, tmpStartDiffTime []float64) FixResult {
+func (s *SubTimelineFixer) calcMeanAndSDV2(startDiffTimeList stat.Float64Slice, tmpStartDiffTime []float64) FixResult {
 
 	oldMean := stat.Mean(startDiffTimeList)
 	oldSd := stat.Sd(startDiffTimeList)
@@ -893,6 +893,184 @@ func (s *SubTimelineFixer) calcMeanAndSD(startDiffTimeList stat.Float64Slice, tm
 	}
 }
 
+// GetOffsetTimeV3 使用内置的字幕校正外置的字幕时间轴
+func (s *SubTimelineFixer) GetOffsetTimeV3(infoBase, infoSrc, orgFix *subparser.FileInfo, audioVadList []vad.VADInfo) error {
+
+	// -------------------------------------------------
+	var bUseSubOrAudioAsBase = true
+	if infoBase == nil && audioVadList != nil {
+		// 使用 音频 来进行匹配
+		bUseSubOrAudioAsBase = false
+	} else if infoBase != nil {
+		// 使用 字幕 来进行匹配
+		bUseSubOrAudioAsBase = true
+	} else {
+		return errors.New("GetOffsetTimeV2 input baseUnit or AudioVad is nil")
+	}
+	// -------------------------------------------------
+	audioFloatList := vad.GetFloatSlice(audioVadList)
+	baseUnitNew, err := sub_helper.GetVADInfoFeatureFromSubNew(infoBase, 0)
+	if err != nil {
+		return err
+	}
+	srcUnitNew, err := sub_helper.GetVADInfoFeatureFromSubNew(infoSrc, 0)
+	if err != nil {
+		return err
+	}
+	/*
+		上面直接得到所有的输入源,都是完整的一个文件,字幕 or 音频
+		然后根据字幕文件每一个对白进行匹配,这里就使用 V2_FrontAndEndPerSrc 进行字幕的选择,不打算从第一句话开始
+		那么假如 一共有 100 句话,V2_FrontAndEndPerSrc 是 0.2,那么就是从 20 - 80 句话进行匹配计算
+		然后 < 20 的就继承 20 的偏移,> 80 的就继承 80 的偏移即可
+		那么现在就需要从对白中开始遍历
+	*/
+	fffAligner := NewFFTAligner()
+	err2, done := s.caleOne(0.1, srcUnitNew, fffAligner, baseUnitNew)
+	if done {
+		return err2
+	}
+	err2, done = s.caleOne(0.2, srcUnitNew, fffAligner, baseUnitNew)
+	if done {
+		return err2
+	}
+
+	skipSubLen := int(float64(len(infoSrc.DialoguesFilter)) * s.FixerConfig.V2_FrontAndEndPerSrc)
+
+	sort.Sort(subparser.OneDialogueByStartTime(infoSrc.DialoguesFilter))
+	sort.Sort(subparser.OneDialogueByStartTime(orgFix.DialoguesFilter))
+
+	for i := 0; i < len(infoSrc.DialoguesFilter); i++ {
+
+		// 得到的是真实的时间
+		srcOneDialogueNow := infoSrc.DialoguesFilter[i]
+		srcTimeStartNow, err := my_util.ParseTime(srcOneDialogueNow.StartTime)
+		if err != nil {
+			return err
+		}
+		orgFixOneDialogueNow := orgFix.DialoguesFilter[i]
+		orgFixTimeStartNow, err := my_util.ParseTime(orgFixOneDialogueNow.StartTime)
+		if err != nil {
+			return err
+		}
+
+		println("Index:", i, "srcTimeStartOrg:", srcTimeStartNow.Format("15:04:05.000"),
+			"src-fix-offset:", my_util.Time2SecondNumber(orgFixTimeStartNow)-my_util.Time2SecondNumber(srcTimeStartNow))
+	}
+	println("------------------")
+	for i := skipSubLen; i < len(infoSrc.DialoguesFilter)-skipSubLen-1; i++ {
+
+		var bok = false
+		var nowBaseStartTime = 0.0
+		var offsetIndex = 0
+		var score = 0.0
+		const next = 20
+		const secondRange = 45
+		// -------------------------------------------------
+		srcOneDialogueNow := infoSrc.DialoguesFilter[i]
+		iNext := i + next
+		if iNext >= len(infoSrc.DialoguesFilter)-skipSubLen-1 {
+			iNext = len(infoSrc.DialoguesFilter) - skipSubLen - 1
+		}
+		srcOneDialogueNext := infoSrc.DialoguesFilter[iNext]
+		// 得到的是真实的时间
+		srcTimeStartNow, err := my_util.ParseTime(srcOneDialogueNow.StartTime)
+		if err != nil {
+			return err
+		}
+		srcTimeEndNext, err := my_util.ParseTime(srcOneDialogueNext.EndTime)
+		if err != nil {
+			return err
+		}
+		orgFixOneDialogueNow := orgFix.DialoguesFilter[i]
+		orgFixTimeStartNow, err := my_util.ParseTime(orgFixOneDialogueNow.StartTime)
+		if err != nil {
+			return err
+		}
+		// -------------------------------------------------
+		// 需要转换为 VAD 的对应 Index,需要减去 baseTime,然后根据 10ms 进行计算
+		// -------------------------------------------------
+		// Src
+		srcStartOffsetTimeNow := srcUnitNew.RealTimeToOffsetTime(srcTimeStartNow)
+		srcStartTimeVADIndexNow := int(my_util.Time2SecondNumber(srcStartOffsetTimeNow) * 100)
+
+		srcEndOffsetTimeNext := srcUnitNew.RealTimeToOffsetTime(srcTimeEndNext)
+		srcEndTimeVADIndexNext := int(my_util.Time2SecondNumber(srcEndOffsetTimeNext) * 100)
+		// -------------------------------------------------
+		if bUseSubOrAudioAsBase == false {
+			// 使用 音频 来进行匹配
+
+		} else {
+			// 使用 字幕 来进行匹配
+			// -------------------------------------------------
+			// Base
+			baseStartOffsetTimeNow := baseUnitNew.RealTimeToOffsetTime(srcTimeStartNow).Add(-secondRange * time.Second)
+			baseStartTimeVADIndexNow := int(my_util.Time2SecondNumber(baseStartOffsetTimeNow) * 100)
+
+			baseEndOffsetTimeNext := baseUnitNew.RealTimeToOffsetTime(srcTimeEndNext).Add(secondRange * time.Second)
+			baseEndTimeVADIndexNext := int(my_util.Time2SecondNumber(baseEndOffsetTimeNext) * 100)
+			if baseEndTimeVADIndexNext >= len(baseUnitNew.VADList)-1 {
+				baseEndTimeVADIndexNext = len(baseUnitNew.VADList) - 1
+			}
+			// -------------------------------------------------
+			offsetIndex, score = fffAligner.Fit(baseUnitNew.GetVADFloatSlice()[baseStartTimeVADIndexNow:baseEndTimeVADIndexNext], srcUnitNew.GetVADFloatSlice()[srcStartTimeVADIndexNow:srcEndTimeVADIndexNext])
+			if offsetIndex < 0 {
+				//return nil
+				continue
+			}
+			bok, nowBaseStartTime = baseUnitNew.GetIndexTimeNumber(baseStartTimeVADIndexNow+offsetIndex, true)
+			if bok == false {
+				return nil
+			}
+		}
+		// 需要校正的字幕
+		bok, nowSrcStartTime := srcUnitNew.GetIndexTimeNumber(srcStartTimeVADIndexNow, true)
+		if bok == false {
+			return nil
+		}
+		// 时间差值
+		TimeDiffStartCorrelation := nowBaseStartTime - nowSrcStartTime
+
+		println("Index:", i, "srcTimeStartOrg:", srcTimeStartNow.Format("15:04:05.000"),
+			"OffsetTime:", TimeDiffStartCorrelation, "Score:", score,
+			"ChangedTime:", srcTimeStartNow.Add(time.Duration(TimeDiffStartCorrelation*1000)*time.Millisecond).Format("15:04:05.000"),
+			"OrgFixTime:", orgFixTimeStartNow.Format("15:04:05.000"),
+			"src-fix-offset:", my_util.Time2SecondNumber(orgFixTimeStartNow)-my_util.Time2SecondNumber(srcTimeStartNow))
+	}
+
+	//if baseStartTimeVADIndexNow > 3600000 {
+	//	baseStartTimeVADIndexNow = 0
+	//}
+
+	println(len(audioFloatList))
+	println(len(baseUnitNew.VADList))
+	println(len(srcUnitNew.VADList))
+
+	return nil
+}
+
+func (s *SubTimelineFixer) caleOne(cutPer float64, srcUnitNew *sub_helper.SubUnit, fffAligner *FFTAligner, baseUnitNew *sub_helper.SubUnit) (error, bool) {
+	srcVADLen := len(srcUnitNew.VADList)
+	srcCutStartIndex := int(float64(srcVADLen) * cutPer)
+	offsetIndexAll, scoreAll := fffAligner.Fit(baseUnitNew.GetVADFloatSlice(), srcUnitNew.GetVADFloatSlice()[srcCutStartIndex:srcVADLen-srcCutStartIndex])
+	bok, nowBaseStartTime := baseUnitNew.GetIndexTimeNumber(0+offsetIndexAll, true)
+	if bok == false {
+		return nil, true
+	}
+	// 需要校正的字幕
+	bok, nowSrcStartTime := srcUnitNew.GetIndexTimeNumber(srcCutStartIndex, true)
+	if bok == false {
+		return nil, true
+	}
+	// 时间差值
+	TimeDiffStartCorrelation := nowBaseStartTime - nowSrcStartTime
+	bok, srcIndexCutTime := srcUnitNew.GetIndexTime(srcCutStartIndex, true)
+	if bok == false {
+		return nil, true
+	}
+	println(srcIndexCutTime.Format("15:04:05.000"), TimeDiffStartCorrelation, scoreAll)
+	return nil, false
+}
+
 const FixMask = "-fix"
 const MinValue = -9999.0
 

+ 71 - 0
internal/pkg/sub_timeline_fixer/fixer_test.go

@@ -830,6 +830,77 @@ func TestGetOffsetTimeV2_MoreTest(t *testing.T) {
 	}
 }
 
+func TestGetOffsetTimeV3_MoreTest(t *testing.T) {
+	subParserHub := sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
+
+	type args struct {
+		baseSubFile   string
+		orgFixSubFile string
+		srcSubFile    string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    float64
+		wantErr bool
+	}{
+		{name: "BL S01E03", args: args{
+			baseSubFile:   "C:\\Tmp\\BL - S01E03\\英_2.ass",
+			orgFixSubFile: "C:\\Tmp\\BL - S01E03\\org-fix.ass",
+			srcSubFile:    "C:\\Tmp\\BL - S01E03\\org.ass",
+		}, want: -4.1, wantErr: false},
+		{name: "Rick and Morty - S05E10", args: args{
+			baseSubFile:   "C:\\Tmp\\Rick and Morty - S05E10\\英_2.ass",
+			orgFixSubFile: "C:\\Tmp\\Rick and Morty - S05E10\\org-fix.ass",
+			srcSubFile:    "C:\\Tmp\\Rick and Morty - S05E10\\org.ass",
+		}, want: -4.1, wantErr: false},
+		{name: "Foundation - S01E09", args: args{
+			baseSubFile:   "C:\\Tmp\\Foundation - S01E09\\英_2.ass",
+			orgFixSubFile: "C:\\Tmp\\Foundation - S01E09\\org-fix.ass",
+			srcSubFile:    "C:\\Tmp\\Foundation - S01E09\\org.ass",
+		}, want: -4.1, wantErr: false},
+		{name: "mix", args: args{
+			baseSubFile: "C:\\Tmp\\Rick and Morty - S05E10\\英_2.ass",
+			srcSubFile:  "C:\\Tmp\\BL - S01E03\\org.ass",
+		}, want: -4.1, wantErr: false},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+
+			bFind, infoBase, err := subParserHub.DetermineFileTypeFromFile(tt.args.baseSubFile)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if bFind == false {
+				t.Fatal("sub not match")
+			}
+
+			bFind, infoSrc, err := subParserHub.DetermineFileTypeFromFile(tt.args.srcSubFile)
+			if err != nil {
+				t.Fatal(err)
+			}
+			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 = timelineFixer.GetOffsetTimeV3(infoBase, infoSrc, orgFix, nil)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GetOffsetTimeV3() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+		})
+	}
+}
+
 var timelineFixer = NewSubTimelineFixer(sub_timeline_fiexer.SubTimelineFixerConfig{
 	// V1
 	V1_MaxCompareDialogue: 3,

+ 55 - 0
internal/pkg/sub_timeline_fixer/pipeline.go

@@ -0,0 +1,55 @@
+package sub_timeline_fixer
+
+import (
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/gss"
+	"github.com/allanpk716/ChineseSubFinder/internal/types/subparser"
+)
+
+type Pipeline struct {
+	framerateRatios []float64
+}
+
+func NewPipeline() *Pipeline {
+	return &Pipeline{
+		framerateRatios: make([]float64, 0),
+	}
+}
+
+func (p Pipeline) Fit_gss(infoBase, infoSrc subparser.FileInfo) error {
+
+	/*
+		ffsubsync 的 pipeline 有这三个步骤
+		1. parse			解析字幕
+		2. scale			根据帧数比率调整时间轴
+		3. speech_extract	从字幕转换为 VAD 的语音检测信息
+	*/
+	opt_func := func(framerateRatio float64) float64 {
+		nowInfoSrc := infoSrc
+		err := nowInfoSrc.ChangeDialoguesFilterExTimeByFramerateRatio(framerateRatio)
+		if err != nil {
+			// 还原
+			println("ChangeDialoguesFilterExTimeByFramerateRatio", err)
+			nowInfoSrc = infoSrc
+		}
+		// 然后进行 base 与 src 匹配计算,将每一次变动 framerateRatio 计算得到的 偏移值和分数进行记录
+
+	}
+	gss.Gss(opt_func, MIN_FRAMERATE_RATIO, MAX_FRAMERATE_RATIO, 1e-4, nil)
+}
+
+func (p *Pipeline) getFramerateRatios2Try() []float64 {
+
+	if len(p.framerateRatios) > 0 {
+		return p.framerateRatios
+	}
+	p.framerateRatios = append(p.framerateRatios, FRAMERATE_RATIOS...)
+	for i := 0; i < len(FRAMERATE_RATIOS); i++ {
+		p.framerateRatios = append(p.framerateRatios, 1.0/FRAMERATE_RATIOS[i])
+	}
+	return p.framerateRatios
+}
+
+var FRAMERATE_RATIOS = []float64{24. / 23.976, 25. / 23.976, 25. / 24.}
+
+const MIN_FRAMERATE_RATIO = 0.9
+const MAX_FRAMERATE_RATIO = 1.1

+ 14 - 0
internal/pkg/sub_timeline_fixer/pipeline_test.go

@@ -0,0 +1,14 @@
+package sub_timeline_fixer
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestPipeline_getFramerateRatios2Try(t *testing.T) {
+
+	outList := NewPipeline().getFramerateRatios2Try()
+	for i, value := range outList {
+		println(i, fmt.Sprintf("%v", value))
+	}
+}

+ 27 - 0
internal/types/subparser/fileinfo.go

@@ -4,6 +4,8 @@ import (
 	"github.com/allanpk716/ChineseSubFinder/internal/common"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
 	"github.com/allanpk716/ChineseSubFinder/internal/types/language"
+	"math"
+	"time"
 )
 
 type FileInfo struct {
@@ -51,6 +53,31 @@ func (f FileInfo) GetDialogueExContent(index int) string {
 	}
 }
 
+// ChangeDialoguesFilterExTimeByFramerateRatio 根据帧数比率调整时间轴 对应 ffsubsync -- SubtitleScaler
+func (f *FileInfo) ChangeDialoguesFilterExTimeByFramerateRatio(framerateRatio float64) error {
+
+	timeFormat := f.GetTimeFormat()
+	for i := 0; i < len(f.DialoguesFilterEx); i++ {
+
+		oneDialogue := f.DialoguesFilterEx[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
+		}
+		scaleTimeStart := timeStart.Add(time.Duration(my_util.Time2SecondNumber(timeStart) * framerateRatio * math.Pow10(9)))
+		scaleTimeEnd := timeEnd.Add(time.Duration(my_util.Time2SecondNumber(timeEnd) * framerateRatio * math.Pow10(9)))
+
+		my_util.Time2SubTimeString(scaleTimeStart, timeFormat)
+		my_util.Time2SubTimeString(scaleTimeEnd, timeFormat)
+	}
+
+	return nil
+}
+
 // OneDialogue 一句对话
 type OneDialogue struct {
 	StartTime string   // 开始时间