浏览代码

正在调试 GetOffsetTimeV2 的功能

Signed-off-by: allan716 <[email protected]>
allan716 4 年之前
父节点
当前提交
c5bb0def2f

+ 68 - 0
TestData/FixTimeline/org/2line-英_1_0-3-35#150_36#360000.srt

@@ -0,0 +1,68 @@
+1
+00:00:00,000 --> 00:00:01,530
+When are you gonna
+kill their big bad?
+
+2
+00:00:01,530 --> 00:00:03,260
+First off,
+the "will they/kill they"
+
+3
+00:00:03,270 --> 00:00:04,940
+is everything to my guys.
+
+4
+00:00:04,940 --> 00:00:06,670
+And second, [bleep] you
+for trying to undermine
+my happiness.
+
+5
+00:00:06,670 --> 00:00:08,670
+This may be the last time
+you ever lay eyes on me!
+
+6
+00:00:08,670 --> 00:00:10,400
+I hope that's not true,
+Morty,
+
+7
+00:00:10,410 --> 00:00:12,280
+because you look like
+s[burps]shit,
+
+8
+00:00:12,280 --> 00:00:14,650
+and this is not
+how I wanna remember you.
+
+9
+00:00:14,650 --> 00:00:21,320
+♪
+
+10
+00:00:21,320 --> 00:00:27,690
+♪
+
+11
+00:00:27,690 --> 00:00:29,690
+Crow Scare: Now that I have
+his two crows,
+
+12
+00:00:29,700 --> 00:00:32,770
+nothing will stop me
+from eliminating Crow Rick.
+
+13
+00:00:32,760 --> 00:00:34,430
+[ Crows cawing ]
+Don't defend him to me.
+
+14
+00:00:34,430 --> 00:00:36,360
+I don't even get
+what you see in him.
+

+ 1 - 0
go.mod

@@ -72,6 +72,7 @@ require (
 	github.com/baabaaox/go-webrtcvad v1.0.1 // indirect
 	github.com/bodgit/plumbing v1.1.0 // indirect
 	github.com/bodgit/windows v1.0.0 // indirect
+	github.com/brettbuddin/fourier v0.1.1 // indirect
 	github.com/connesc/cipherio v0.2.1 // indirect
 	github.com/dsnet/compress v0.0.1 // indirect
 	github.com/golang/snappy v0.0.3 // indirect

+ 2 - 0
go.sum

@@ -65,6 +65,8 @@ github.com/bodgit/sevenzip v1.1.0/go.mod h1:vRCJlX/FVjbcwUG9lyX1YQPbQA4Xxw/7puzc
 github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA=
 github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
 github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
+github.com/brettbuddin/fourier v0.1.1 h1:jjlYBAV018g3B2VH5wGI/XgZE9veEHXS7MmPa4BJgmo=
+github.com/brettbuddin/fourier v0.1.1/go.mod h1:o4lEcyAaNBt8evqo2VXTSpRnZTts9lyzt73uRFtmD70=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=

+ 5 - 5
internal/logic/sub_parser/ass/ass.go

@@ -2,9 +2,9 @@ package ass
 
 import (
 	"github.com/allanpk716/ChineseSubFinder/internal/common"
-	"github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/language"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/regex_things"
 	"github.com/allanpk716/ChineseSubFinder/internal/types/subparser"
 	"github.com/emirpasic/gods/maps/treemap"
 	"io/ioutil"
@@ -50,7 +50,7 @@ func (p Parser) DetermineFileTypeFromBytes(inBytes []byte, nowExt string) (bool,
 	// 注意,需要替换掉 \r 不然正则表达式会有问题
 	allString = strings.ReplaceAll(allString, "\r", "")
 	// 找到 start end text
-	matched := sub_parser.ReMatchDialogueASS.FindAllStringSubmatch(allString, -1)
+	matched := regex_things.ReMatchDialogueASS.FindAllStringSubmatch(allString, -1)
 	if len(matched) < 1 {
 		log_helper.GetLogger().Debugln("DetermineFileTypeFromBytes can't found Dialogues, Skip")
 		return false, nil, nil
@@ -208,15 +208,15 @@ func (p Parser) parseOneDialogueText(nowText string, odl *subparser.OneDialogue,
 	// nowText 优先移除 \h 这个是替换空格, \h 是让两个词在一行,不换行显示
 	nowText = strings.ReplaceAll(nowText, `\h`, " ")
 	// nowText 这个需要先把 {} 花括号内的内容给移除
-	nowText1 := sub_parser.ReMatchBrace.ReplaceAllString(nowText, "")
-	nowText1 = sub_parser.ReMatchBracket.ReplaceAllString(nowText1, "")
+	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 sub_parser.ReCutDoubleLanguage.FindAllStringSubmatch(nowText1, -1) {
+		for _, matched2 := range regex_things.ReCutDoubleLanguage.FindAllStringSubmatch(nowText1, -1) {
 			for i, s := range matched2 {
 				if i == 0 {
 					continue

+ 4 - 4
internal/logic/sub_parser/srt/srt.go

@@ -2,9 +2,9 @@ package srt
 
 import (
 	"github.com/allanpk716/ChineseSubFinder/internal/common"
-	"github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/language"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/regex_things"
 	"github.com/allanpk716/ChineseSubFinder/internal/types/subparser"
 	"io/ioutil"
 	"path/filepath"
@@ -51,7 +51,7 @@ func (p Parser) DetermineFileTypeFromBytes(inBytes []byte, nowExt string) (bool,
 	allString = strings.ReplaceAll(allString, "\r", "")
 
 	// 找到 start end text
-	matched := sub_parser.ReMatchDialogueSRT.FindAllStringSubmatch(allString, -1)
+	matched := regex_things.ReMatchDialogueSRT.FindAllStringSubmatch(allString, -1)
 	if len(matched) < 1 {
 		log_helper.GetLogger().Debugln("DetermineFileTypeFromBytes can't found Dialogues, Skip")
 		return false, nil, nil
@@ -79,8 +79,8 @@ func (p Parser) DetermineFileTypeFromBytes(inBytes []byte, nowExt string) (bool,
 				countLineFeed++
 			}
 			// 剔除 {\fn微软雅黑\fs14}C'mon, Rick. We're -- We're almost there. {} 这一段
-			text = sub_parser.ReMatchBrace.ReplaceAllString(text, "")
-			text = sub_parser.ReMatchBracket.ReplaceAllString(text, "")
+			text = regex_things.ReMatchBrace.ReplaceAllString(text, "")
+			text = regex_things.ReMatchBracket.ReplaceAllString(text, "")
 			text = strings.ReplaceAll(text, `\N`, "")
 			odl.Lines = append(odl.Lines, text)
 		}

+ 33 - 3
internal/pkg/ffmpeg_helper/ffmpeg_helper.go

@@ -20,12 +20,12 @@ import (
 )
 
 type FFMPEGHelper struct {
-	subParserHub *sub_parser_hub.SubParserHub // 字幕内容的解析器
+	SubParserHub *sub_parser_hub.SubParserHub // 字幕内容的解析器
 }
 
 func NewFFMPEGHelper() *FFMPEGHelper {
 	return &FFMPEGHelper{
-		subParserHub: sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser()),
+		SubParserHub: sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser()),
 	}
 }
 
@@ -95,7 +95,7 @@ func (f *FFMPEGHelper) GetFFMPEGInfo(videoFileFullPath string) (bool, *FFMPEGInf
 		}
 	}
 	// 查找当前这个视频外置字幕列表
-	err = ffMPEGInfo.GetExternalSubInfos(f.subParserHub)
+	err = ffMPEGInfo.GetExternalSubInfos(f.SubParserHub)
 	if err != nil {
 		return false, nil, err
 	}
@@ -174,6 +174,36 @@ func (f *FFMPEGHelper) ExportAudioAndSubArgsByTimeRange(audioFullPath, subFullPa
 	return outAudioFullPath, outSubFullPath, "", nil
 }
 
+// ExportSubArgsByTimeRange 根据输入的时间轴导出字幕分段信息 "0:1:27" "28.2"
+func (f *FFMPEGHelper) ExportSubArgsByTimeRange(subFullPath string, startTimeString, timeLength string) (string, string, error) {
+
+	outStartTimeString := strings.ReplaceAll(startTimeString, ":", "-")
+	outStartTimeString = strings.ReplaceAll(outStartTimeString, ".", "#")
+
+	outTimeLength := strings.ReplaceAll(timeLength, ".", "#")
+
+	frontName := strings.ReplaceAll(filepath.Base(subFullPath), filepath.Ext(subFullPath), "")
+
+	outSubName := frontName + "_" + outStartTimeString + "_" + outTimeLength + common.SubExtSRT
+
+	var outSubFullPath = filepath.Join(filepath.Dir(subFullPath), outSubName)
+
+	// 导出字幕
+	if my_util.IsFile(outSubFullPath) == true {
+		err := os.Remove(outSubFullPath)
+		if err != nil {
+			return "", "", err
+		}
+	}
+	args := f.getSubExportArgsByTimeRange(subFullPath, startTimeString, timeLength, outSubFullPath)
+	execFFMPEG, err := f.execFFMPEG(args)
+	if err != nil {
+		return "", execFFMPEG, err
+	}
+
+	return outSubFullPath, "", nil
+}
+
 // parseJsonString2GetFFProbeInfo 使用 ffprobe 获取视频的 stream 信息,从中解析出字幕和音频的索引
 func (f *FFMPEGHelper) parseJsonString2GetFFProbeInfo(videoFileFullPath, inputFFProbeString string) (bool, *FFMPEGInfo) {
 

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

@@ -5,6 +5,7 @@ import (
 	"github.com/allanpk716/ChineseSubFinder/internal/common"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/global_value"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/regex_things"
 	"github.com/allanpk716/ChineseSubFinder/internal/types"
 	browser "github.com/allanpk716/fake-useragent"
 	"github.com/go-resty/resty/v2"
@@ -460,3 +461,37 @@ func Time2SecendNumber(inTime time.Time) float64 {
 func Time2Duration(inTime time.Time) time.Duration {
 	return time.Duration(Time2SecendNumber(inTime) * math.Pow10(9))
 }
+
+// ReplaceSpecString 替换特殊的字符
+func ReplaceSpecString(instring string, rep string) string {
+	return regex_things.RegMatchSpString.ReplaceAllString(instring, rep)
+}
+
+func Bool2Int(inBool bool) int {
+	if inBool == true {
+		return 1
+	} else {
+		return 0
+	}
+}
+
+// Round 取整
+func Round(x float64) int64 {
+
+	if x-float64(int64(x)) > 0 {
+		return int64(x) + 1
+	} else {
+		return int64(x)
+	}
+
+	//return int64(math.Floor(x + 0.5))
+}
+
+// MakePowerOfTwo 2的整次幂数 buffer length is not a power of two
+func MakePowerOfTwo(x int64) int64 {
+
+	power := math.Log2(float64(x))
+	tmpRound := Round(power)
+
+	return int64(math.Pow(2, float64(tmpRound)))
+}

+ 4 - 1
internal/logic/sub_parser/sub_parser_regex.go → internal/pkg/regex_things/regex_things.go

@@ -1,4 +1,4 @@
-package sub_parser
+package regex_things
 
 import "regexp"
 
@@ -14,6 +14,9 @@ var ReMatchBracket = regexp.MustCompile(`(?m)((?i)\[[^]]*\])`)
 
 var ReCutDoubleLanguage = regexp.MustCompile(`(?i)(.*)\\N(.*)`)
 
+// RegMatchSpString 替换特殊字符
+var RegMatchSpString = regexp.MustCompile(`(?i)[^\w\s]`)
+
 // 字幕文件对话的每一行
 // regStringASS = `Dialogue: [^,.]*[0-9]*,([1-9]?[0-9]*:[0-9]*:[0-9]*.[0-9]*),([1-9]?[0-9]*:[0-9]*:[0-9]*.[0-9]*),[^,.]*,[^,.]*,[0-9]*,[0-9]*,[0-9]*,[^,.]*,(.*)`
 const regStringASS = `Dialogue: [^,.]*[0-9]*,([1-9]?[0-9]*:[0-9]*:[0-9]*.[0-9]*),([1-9]?[0-9]*:[0-9]*:[0-9]*.[0-9]*),([^,.]*),[^,.]*,[0-9]*,[0-9]*,[0-9]*,[^,.]*,(.*)`

+ 3 - 1
internal/pkg/sub_helper/dialogue_merger.go

@@ -33,8 +33,10 @@ func (d *DialogueMerger) Add(inDialogueEx subparser.OneDialogueEx) bool {
 		if d.lastStartTime == "" {
 			return false
 		}
+		// 这里除了拼接 EnLine,还需要把 EndTime 更新
 		d.dialogueMap[d.lastStartTime].EnLine += " " + inDialogueEx.EnLine
-		d.lastStartTime = ""
+		d.dialogueMap[d.lastStartTime].EndTime = inDialogueEx.EndTime
+		//d.lastStartTime = ""
 		return true
 	} else {
 		// 其他情况也新增

+ 2 - 1
internal/pkg/sub_helper/dialogue_merger_test.go

@@ -64,7 +64,8 @@ func TestNewDialogueMerger(t *testing.T) {
 	}
 
 	subParserHub := sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
-	bFind, infoBase, err := subParserHub.DetermineFileTypeFromFile(filepath.Join(testRootDir, "2line-The Card Counter (2021) WEBDL-1080p.chinese(inside).ass"))
+	//bFind, infoBase, err := subParserHub.DetermineFileTypeFromFile(filepath.Join(testRootDir, "2line-The Card Counter (2021) WEBDL-1080p.chinese(inside).ass"))
+	bFind, infoBase, err := subParserHub.DetermineFileTypeFromFile(filepath.Join(testRootDir, "2line-英_1_0-3-35#150_36#360000.srt"))
 	if err != nil {
 		t.Fatal(err)
 	}

+ 59 - 0
internal/pkg/sub_helper/sub_helper.go

@@ -17,6 +17,7 @@ import (
 	"regexp"
 	"strconv"
 	"strings"
+	"time"
 )
 
 // OrganizeDlSubFiles 需要从汇总来是网站字幕中,解压对应的压缩包中的字幕出来
@@ -367,6 +368,64 @@ func MergeMultiDialogue4EngSubtitle(inSubParser *subparser.FileInfo) {
 	inSubParser.DialoguesEx = merger.Get()
 }
 
+/*
+	GetVADINfoFromSub
+	这里的字幕要求是完整的一个字幕
+	1. 抽取字幕的时间片段的时候,暂定,前 15% 和后 15% 要避开,前奏、主题曲、结尾曲
+	2. 将整个字幕,抽取连续 5 句对话为一个单元,提取时间片段信息
+*/
+func GetVADINfoFromSub(infoSrc *subparser.FileInfo, FrontAndEndPer float64, SubUnitMaxCount int) ([]SubUnit, error) {
+
+	srcSubUnitList := make([]SubUnit, 0)
+	srcOneSubUnit := NewSubUnit()
+	srcTimeFormat := infoSrc.GetTimeFormat()
+
+	// srcDuration
+	lastDialogueExTimeEnd, err := time.Parse(srcTimeFormat, infoSrc.DialoguesEx[len(infoSrc.DialoguesEx)-1].EndTime)
+	if err != nil {
+		return nil, err
+	}
+	srcDuration := my_util.Time2SecendNumber(lastDialogueExTimeEnd)
+
+	for index, oneDialogueEx := range infoSrc.DialoguesEx {
+
+		oneDialogueExTimeStart, err := time.Parse(srcTimeFormat, oneDialogueEx.StartTime)
+		if err != nil {
+			return nil, err
+		}
+		oneDialogueExTimeEnd, err := time.Parse(srcTimeFormat, oneDialogueEx.EndTime)
+		if err != nil {
+			return nil, err
+		}
+
+		oneStart := my_util.Time2SecendNumber(oneDialogueExTimeStart)
+		//oneEnd := pkg.Time2SecendNumber(oneDialogueExTimeEnd)
+
+		if FrontAndEndPer > 0 {
+			if srcDuration*FrontAndEndPer > oneStart || srcDuration*(1.0-FrontAndEndPer) < oneStart {
+				continue
+			}
+		}
+
+		// 如果当前的这一句话,为空,或者进过正则表达式剔除特殊字符后为空,则跳过
+		if my_util.ReplaceSpecString(infoSrc.GetDialogueExContent(index), "") == "" {
+			continue
+		}
+		// 低于 5句对白,则添加
+		if srcOneSubUnit.GetDialogueCount() < SubUnitMaxCount {
+			srcOneSubUnit.AddAndInsert(oneDialogueExTimeStart, oneDialogueExTimeEnd)
+		} else {
+			srcSubUnitList = append(srcSubUnitList, *srcOneSubUnit)
+			srcOneSubUnit = NewSubUnit()
+		}
+	}
+	if srcOneSubUnit.GetDialogueCount() > 0 {
+		srcSubUnitList = append(srcSubUnitList, *srcOneSubUnit)
+	}
+
+	return srcSubUnitList, nil
+}
+
 var (
 	regOneSeasonSubFolderNameMatch = regexp.MustCompile(`(?m)^Sub_S\dE0`)
 )

+ 124 - 0
internal/pkg/sub_helper/sub_unit.go

@@ -0,0 +1,124 @@
+package sub_helper
+
+import (
+	"fmt"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/vad"
+	"math"
+	"time"
+)
+
+type SubUnit struct {
+	baseTime  time.Time // 这个是基础的时间,后续需要减去这个,不然与导出的片段字幕去对比会有一个起始时间的偏差
+	StartTime time.Time // 这个时间会减去 baseTime 再存储
+	EndTime   time.Time // 这个时间会减去 baseTime 再存储
+	VADList   []vad.VADInfo
+	subCount  int
+	firstAdd  bool
+}
+
+func NewSubUnit() *SubUnit {
+	return &SubUnit{
+		VADList:  make([]vad.VADInfo, 0),
+		subCount: 0,
+		firstAdd: false,
+	}
+}
+
+// AddAndInsert 添加一句对白进来,并且填充中间的空白,间隔 10ms
+func (s *SubUnit) AddAndInsert(oneSubStartTime, oneSubEndTime time.Time) {
+
+	const perWindows = float64(vad.FrameDuration) / 1000
+
+	// 不是第一次添加,那么就需要把两句对白中间间隔的 active == false 的插入,插入间隙
+	if len(s.VADList) > 0 {
+		nowStartTime := s.RealTimeToOffsetTime(oneSubStartTime)
+		nowStartOffsetTime := my_util.Time2SecendNumber(nowStartTime)
+		nowEndOffsetTime := s.GetEndTimeNumber(false)
+
+		needAddRange := nowStartOffsetTime - nowEndOffsetTime
+		if needAddRange > perWindows {
+			for i := 0.0; i < needAddRange; {
+
+				s.VADList = append(s.VADList, *vad.NewVADInfoBase(false, time.Duration((s.GetEndTimeNumber(true)+i)*math.Pow10(9))))
+				i += perWindows
+			}
+		}
+	}
+
+	if s.firstAdd == false {
+		// 第一次 Add 需要给 baseTime 赋值
+		s.baseTime = oneSubStartTime
+		s.StartTime = s.RealTimeToOffsetTime(oneSubStartTime)
+		s.firstAdd = true
+	}
+
+	s.EndTime = oneSubEndTime.Add(-my_util.Time2Duration(s.baseTime))
+
+	nowStartTime := s.RealTimeToOffsetTime(oneSubStartTime)
+	nowEndTime := s.RealTimeToOffsetTime(oneSubEndTime)
+
+	nowStartOffsetTime := my_util.Time2SecendNumber(nowStartTime)
+	nowEndOffsetTime := my_util.Time2SecendNumber(nowEndTime)
+
+	needAddRange := nowEndOffsetTime - nowStartOffsetTime
+	if needAddRange > perWindows {
+		for i := 0.0; i < needAddRange; {
+			s.VADList = append(s.VADList, *vad.NewVADInfoBase(true, time.Duration((my_util.Time2SecendNumber(oneSubStartTime)+i)*math.Pow10(9))))
+			i += perWindows
+		}
+	}
+	s.subCount++
+}
+
+// GetDialogueCount 获取这个对白单元由几个对话
+func (s SubUnit) GetDialogueCount() int {
+	return s.subCount
+}
+
+// GetStartTimeNumber 获取这个单元的起始时间,单位是秒
+func (s SubUnit) GetStartTimeNumber(realOrOffsetTime bool) float64 {
+	if realOrOffsetTime == true {
+		return my_util.Time2SecendNumber(s.StartTime.Add(my_util.Time2Duration(s.baseTime)))
+	} else {
+		return my_util.Time2SecendNumber(s.StartTime)
+	}
+}
+
+// GetEndTimeNumber 获取这个单元的结束时间,单位是秒
+func (s SubUnit) GetEndTimeNumber(realOrOffsetTime bool) float64 {
+	if realOrOffsetTime == true {
+		return my_util.Time2SecendNumber(s.EndTime.Add(my_util.Time2Duration(s.baseTime)))
+	} else {
+		return my_util.Time2SecendNumber(s.EndTime)
+	}
+}
+
+// GetTimelineRange 开始到结束的时间长度,单位是秒
+func (s SubUnit) GetTimelineRange() float64 {
+	return s.GetEndTimeNumber(false) - s.GetStartTimeNumber(false)
+}
+
+func (s SubUnit) GetOffsetTimeNumber() float64 {
+	return my_util.Time2SecendNumber(s.baseTime)
+}
+
+// GetFFMPEGCutRange 这里会生成导出 FFMPEG 的参数字段,起始时间和结束的时间长度
+func (s SubUnit) GetFFMPEGCutRange(expandTimeRange float64) (string, string) {
+
+	var tmpStartTime time.Time
+	if s.GetStartTimeNumber(true)-expandTimeRange*60 < 0 {
+		tmpStartTime = time.Time{}
+	} else {
+		tmpStartTime = s.StartTime.Add(time.Duration(expandTimeRange) * time.Minute).Add(my_util.Time2Duration(s.baseTime))
+	}
+
+	return fmt.Sprintf("%d:%d:%d.%d", tmpStartTime.Hour(), tmpStartTime.Minute(), tmpStartTime.Second(), tmpStartTime.Nanosecond()/1000/1000),
+		fmt.Sprintf("%f", s.GetTimelineRange()+expandTimeRange*60.0)
+}
+
+// RealTimeToOffsetTime 真实时间转偏移时间
+func (s SubUnit) RealTimeToOffsetTime(realTime time.Time) time.Time {
+	dd := my_util.Time2Duration(s.baseTime)
+	return realTime.Add(-dd)
+}

+ 128 - 47
internal/pkg/sub_timeline_fixer/fixer.go

@@ -1,13 +1,16 @@
 package sub_timeline_fixer
 
 import (
+	"errors"
 	"fmt"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/ffmpeg_helper"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/log_helper"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
+	"github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_helper"
 	"github.com/allanpk716/ChineseSubFinder/internal/pkg/vad"
 	"github.com/allanpk716/ChineseSubFinder/internal/types/sub_timeline_fiexer"
 	"github.com/allanpk716/ChineseSubFinder/internal/types/subparser"
+	"github.com/brettbuddin/fourier"
 	"github.com/go-echarts/go-echarts/v2/opts"
 	"github.com/grd/stat"
 	"github.com/james-bowman/nlp/measures/pairwise"
@@ -362,63 +365,112 @@ func (s *SubTimelineFixer) GetOffsetTimeV1(infoBase, infoSrc *subparser.FileInfo
 	return true, newMean, newSd, nil
 }
 
-// GetOffsetTimeV2 使用 VAD 检测语音是否有人声,输出连续的点标记,再通过 SimHash 进行匹配,找到最佳的偏移时间
-func (s *SubTimelineFixer) GetOffsetTimeV2(audioInfo vad.AudioInfo, infoSrc *subparser.FileInfo, staticLineFileSavePath string, debugInfoFileSavePath string) (bool, float64, float64, error) {
+// GetOffsetTimeV2 使用内置的字幕校正外置的字幕时间轴
+func (s *SubTimelineFixer) GetOffsetTimeV2(infoBase, infoSrc *subparser.FileInfo, staticLineFileSavePath string, debugInfoFileSavePath string) (bool, float64, float64, error) {
 
-	/*
-		分割字幕成若干段,然后得到若干段的时间轴,将这些段从字幕文字转换成 VADInfo
-		从上面若干段时间轴,把音频给分割成多段
-		然后使用 simhash 的进行比较,输出分析的曲线图等信息
-	*/
-
-	bok, duration, err := s.ffmpegHelper.GetAudioInfo(audioInfo.FileFullPath)
-	if err != nil || bok == false {
+	srcSubUnitList, err := sub_helper.GetVADINfoFromSub(infoSrc, FrontAndEndPer, SubUnitMaxCount)
+	if err != nil {
 		return false, 0, 0, err
 	}
+	// 开始针对对白单元进行匹配
+	for _, srcSubUnit := range srcSubUnitList {
 
-	/*
-		这里的字幕要求是完整的一个字幕
-		1. 抽取字幕的时间片段的时候,暂定,前 15% 和后 15% 要避开,前奏、主题曲、结尾曲
-		2. 将整个字幕,抽取连续 5 句对话为一个单元,提取时间片段信息
-	*/
-	subUnitList := make([]SubUnit, 0)
-	oneSubUnit := NewSubUnit()
-	timeFormat := infoSrc.GetTimeFormat()
-	for _, oneDialogueEx := range infoSrc.DialoguesEx {
+		startTimeString, subLength := srcSubUnit.GetFFMPEGCutRange(ExpandTimeRange)
+		// 导出当前的字幕文件适合与匹配的范围的临时字幕文件
+		nowTmpSubBaseFPath, errString, err := s.ffmpegHelper.ExportSubArgsByTimeRange(infoBase.FileFullPath, startTimeString, subLength)
+		if err != nil {
+			log_helper.GetLogger().Errorln("ExportSubArgsByTimeRange", errString, err)
+			return false, 0, 0, err
+		}
 
-		oneDialogueExTimeStart, err := time.Parse(timeFormat, oneDialogueEx.StartTime)
+		bok, nowTmpSubBaseFileInfo, err := s.ffmpegHelper.SubParserHub.DetermineFileTypeFromFile(nowTmpSubBaseFPath)
 		if err != nil {
 			return false, 0, 0, err
 		}
-		oneDialogueExTimeEnd, err := time.Parse(timeFormat, oneDialogueEx.EndTime)
+		if bok == false {
+			return false, 0, 0, errors.New("DetermineFileTypeFromFile == false")
+		}
+
+		nowTmpBaseSubUnitList, err := sub_helper.GetVADINfoFromSub(nowTmpSubBaseFileInfo, 0, 10000)
 		if err != nil {
 			return false, 0, 0, err
 		}
+		nowTmpBaseSubVADList := nowTmpBaseSubUnitList[0]
+
+		var nowBaseSubTimeLineData = make([]opts.LineData, 0)
+		var nowBaseSubXAxis = make([]string, 0)
+
+		var nowSrcSubTimeLineData = make([]opts.LineData, 0)
+		var nowSrcSubXAxis = make([]string, 0)
+		// src
+		for _, vadInfo := range srcSubUnit.VADList {
+			nowSrcSubTimeLineData = append(nowSrcSubTimeLineData, opts.LineData{Value: vadInfo.Active})
+			baseTime := srcSubUnit.GetOffsetTimeNumber()
+			nowVADInfoTimeNumber := vadInfo.Time.Seconds()
+			//println(fmt.Sprintf("%d - %f", index, nowVADInfoTimeNumber-baseTime))
+			nowOffsetTime := nowVADInfoTimeNumber - baseTime
+			nowSrcSubXAxis = append(nowSrcSubXAxis, fmt.Sprintf("%f", nowOffsetTime))
+		}
 
-		oneStart := my_util.Time2SecendNumber(oneDialogueExTimeStart)
-		//oneEnd := pkg.Time2SecendNumber(oneDialogueExTimeEnd)
+		outDir := filepath.Dir(nowTmpSubBaseFPath)
+		outBaseName := filepath.Base(nowTmpSubBaseFPath)
+		outBaseNameWithOutExt := strings.ReplaceAll(outBaseName, filepath.Ext(outBaseName), "")
 
-		if duration*FrontAndEndPer > oneStart || duration*(1.0-FrontAndEndPer) < oneStart {
-			continue
+		srcSubVADStaticLineFullPath := filepath.Join(outDir, outBaseNameWithOutExt+"_sub_src.html")
+
+		err = SaveStaticLineV2("Sub src", srcSubVADStaticLineFullPath, nowSrcSubXAxis, nowSrcSubTimeLineData)
+		if err != nil {
+			return false, 0, 0, err
 		}
-		// TODO V2 版本是时间轴校正,必须带有中文
-		//if oneDialogueEx.ChLine == "" {
-		//	continue
-		//}
-		// 低于 5句对白,则添加
-		if oneSubUnit.GetDialogueCount() < SubUnitMaxCount {
-			oneSubUnit.AddAndInsert(oneDialogueExTimeStart, oneDialogueExTimeEnd)
-		} else {
-			subUnitList = append(subUnitList, *oneSubUnit)
-			oneSubUnit = NewSubUnit()
+		// base
+		for _, vadInfo := range nowTmpBaseSubVADList.VADList {
+			nowBaseSubTimeLineData = append(nowBaseSubTimeLineData, opts.LineData{Value: vadInfo.Active})
+			baseTime := srcSubUnit.GetOffsetTimeNumber()
+			nowVADInfoTimeNumber := vadInfo.Time.Seconds()
+			//println(fmt.Sprintf("%d - %f", index, nowVADInfoTimeNumber-baseTime))
+			nowOffsetTime := nowVADInfoTimeNumber - baseTime
+			nowBaseSubXAxis = append(nowBaseSubXAxis, fmt.Sprintf("%f", nowOffsetTime))
+		}
+
+		baseSubVADStaticLineFullPath := filepath.Join(outDir, outBaseNameWithOutExt+"_sub_base.html")
+
+		err = SaveStaticLineV2("Sub base", baseSubVADStaticLineFullPath, nowBaseSubXAxis, nowBaseSubTimeLineData)
+		if err != nil {
+			return false, 0, 0, err
 		}
 	}
 
+	return false, -1, -1, nil
+}
+
+// GetOffsetTimeV3 使用 VAD 检测语音是否有人声,输出连续的点标记,再通过 SimHash 进行匹配,找到最佳的偏移时间
+func (s *SubTimelineFixer) GetOffsetTimeV3(audioInfo vad.AudioInfo, infoSrc *subparser.FileInfo, staticLineFileSavePath string, debugInfoFileSavePath string) (bool, float64, float64, error) {
+
+	/*
+		分割字幕成若干段,然后得到若干段的时间轴,将这些段从字幕文字转换成 VADInfo
+		从上面若干段时间轴,把音频给分割成多段
+		然后使用 simhash 的进行比较,输出分析的曲线图等信息
+	*/
+
+	//bok, duration, err := s.ffmpegHelper.GetAudioInfo(audioInfo.FileFullPath)
+	//if err != nil || bok == false {
+	//	return false, 0, 0, err
+	//}
+
+	/*
+		这里的字幕要求是完整的一个字幕
+		1. 抽取字幕的时间片段的时候,暂定,前 15% 和后 15% 要避开,前奏、主题曲、结尾曲
+		2. 将整个字幕,抽取连续 5 句对话为一个单元,提取时间片段信息
+	*/
+	subUnitList, err := sub_helper.GetVADINfoFromSub(infoSrc, FrontAndEndPer, SubUnitMaxCount)
+	if err != nil {
+		return false, 0, 0, err
+	}
 	// 开始针对对白单元进行匹配
 	for _, subUnit := range subUnitList {
 
 		startTimeString, subLength := subUnit.GetFFMPEGCutRange(ExpandTimeRange)
-
+		// 导出当前的音频文件适合与匹配的范围的临时音频文件
 		outAudioFPath, _, errString, err := s.ffmpegHelper.ExportAudioAndSubArgsByTimeRange(audioInfo.FileFullPath, infoSrc.FileFullPath, startTimeString, subLength)
 		if err != nil {
 			log_helper.GetLogger().Errorln("ExportAudioAndSubArgsByTimeRange", errString, err)
@@ -435,15 +487,33 @@ func (s *SubTimelineFixer) GetOffsetTimeV2(audioInfo vad.AudioInfo, infoSrc *sub
 		}
 
 		var subTimeLineData = make([]opts.LineData, 0)
-		var subxAxis = make([]string, 0)
+		var subTimeLineFFTData = make([]opts.LineData, 0)
+		var subXAxis = make([]string, 0)
+
 		var audioTimeLineData = make([]opts.LineData, 0)
-		var audioxAxis = make([]string, 0)
+		var audioTimeLineFFTData = make([]opts.LineData, 0)
+		var audioXAxis = make([]string, 0)
 
-		for _, vadInfo := range subUnit.VADList {
+		subBuf := make([]complex128, my_util.MakePowerOfTwo(int64(len(subUnit.VADList))))
+		audioBuf := make([]complex128, my_util.MakePowerOfTwo(int64(len(audioVADInfos))))
+		for index, vadInfo := range subUnit.VADList {
 
 			subTimeLineData = append(subTimeLineData, opts.LineData{Value: vadInfo.Active})
-			baseTime := subUnit.GetBaseTimeNumber()
-			subxAxis = append(subxAxis, fmt.Sprintf("%f", vadInfo.Time.Seconds()-baseTime))
+			baseTime := subUnit.GetOffsetTimeNumber()
+			nowVADInfoTimeNumber := vadInfo.Time.Seconds()
+			//println(fmt.Sprintf("%d - %f", index, nowVADInfoTimeNumber-baseTime))
+			nowOffsetTime := nowVADInfoTimeNumber - baseTime
+			subXAxis = append(subXAxis, fmt.Sprintf("%f", nowOffsetTime))
+
+			subBuf[index] = complex(float64(my_util.Bool2Int(vadInfo.Active)), nowOffsetTime)
+		}
+		// FFT 转换
+		err = fourier.Forward(subBuf)
+		if err != nil {
+			return false, 0, 0, err
+		}
+		for i := 0; i < len(subUnit.VADList); i++ {
+			subTimeLineFFTData = append(subTimeLineFFTData, opts.LineData{Value: real(subBuf[i])})
 		}
 
 		outDir := filepath.Dir(outAudioFPath)
@@ -452,20 +522,31 @@ func (s *SubTimelineFixer) GetOffsetTimeV2(audioInfo vad.AudioInfo, infoSrc *sub
 
 		subVADStaticLineFullPath := filepath.Join(outDir, outBaseNameWithOutExt+"_sub.html")
 
-		err = SaveStaticLineV2("Sub", subVADStaticLineFullPath, subxAxis, subTimeLineData)
+		err = SaveStaticLineV3("Sub", subVADStaticLineFullPath, subXAxis, subTimeLineData, subTimeLineFFTData)
 		if err != nil {
 			return false, 0, 0, err
 		}
 
-		for _, vadInfo := range audioVADInfos {
+		for index, vadInfo := range audioVADInfos {
 
 			audioTimeLineData = append(audioTimeLineData, opts.LineData{Value: vadInfo.Active})
-			audioxAxis = append(audioxAxis, fmt.Sprintf("%f", vadInfo.Time.Seconds()))
+			audioXAxis = append(audioXAxis, fmt.Sprintf("%f", vadInfo.Time.Seconds()))
+
+			audioBuf[index] = complex(float64(my_util.Bool2Int(vadInfo.Active)), vadInfo.Time.Seconds())
+		}
+
+		// FFT 转换
+		err = fourier.Forward(audioBuf)
+		if err != nil {
+			return false, 0, 0, err
+		}
+		for i := 0; i < len(audioBuf); i++ {
+			audioTimeLineFFTData = append(audioTimeLineFFTData, opts.LineData{Value: real(audioBuf[i])})
 		}
 
 		audioVADStaticLineFullPath := filepath.Join(outDir, outBaseNameWithOutExt+"_audio.html")
 
-		err = SaveStaticLineV2("Audio", audioVADStaticLineFullPath, audioxAxis, audioTimeLineData)
+		err = SaveStaticLineV3("Audio", audioVADStaticLineFullPath, audioXAxis, audioTimeLineData, audioTimeLineFFTData)
 		if err != nil {
 			return false, 0, 0, err
 		}
@@ -475,6 +556,6 @@ func (s *SubTimelineFixer) GetOffsetTimeV2(audioInfo vad.AudioInfo, infoSrc *sub
 }
 
 const FixMask = "-fix"
-const FrontAndEndPer = 0.15 // 前百分之 15 和后百分之 15 都不进行识别
+const FrontAndEndPer = 0.10 // 前百分之 15 和后百分之 15 都不进行识别
 const SubUnitMaxCount = 10  // 一个 Sub单元有五句对白
-const ExpandTimeRange = 0   // 从字幕的时间轴片段需要向前和向后多匹配一部分的音频,这里定义的就是这个 range 以分钟为单位, 正负 1 分钟
+const ExpandTimeRange = 0.7 // 从字幕的时间轴片段需要向前和向后多匹配一部分的音频,这里定义的就是这个 range 以分钟为单位, 正负 1 分钟

+ 163 - 64
internal/pkg/sub_timeline_fixer/fixer_test.go

@@ -44,6 +44,58 @@ func TestStopWordCounter(t *testing.T) {
 	println(info.Name)
 }
 
+func TestTFIDF(t *testing.T) {
+	testCorpus := []string{
+		"The quick brown fox jumped over the lazy dog",
+		"hey diddle diddle, the cat and the fiddle",
+		"the cow jumped over the moon",
+		"the little dog laughed to see such fun",
+		"and the dish ran away with the spoon",
+	}
+
+	query := "the brown fox ran around the dog"
+
+	vectoriser := nlp.NewCountVectoriser(StopWords...)
+	transformer := nlp.NewTfidfTransformer()
+
+	// set k (the number of dimensions following truncation) to 4
+	reducer := nlp.NewTruncatedSVD(4)
+
+	lsiPipeline := nlp.NewPipeline(vectoriser, transformer, reducer)
+
+	// Transform the corpus into an LSI fitting the model to the documents in the process
+	lsi, err := lsiPipeline.FitTransform(testCorpus...)
+	if err != nil {
+		fmt.Printf("Failed to process documents because %v", err)
+		return
+	}
+
+	// run the query through the same pipeline that was fitted to the corpus and
+	// to project it into the same dimensional space
+	queryVector, err := lsiPipeline.Transform(query)
+	if err != nil {
+		fmt.Printf("Failed to process documents because %v", err)
+		return
+	}
+
+	// iterate over document feature vectors (columns) in the LSI matrix and compare
+	// with the query vector for similarity.  Similarity is determined by the difference
+	// between the angles of the vectors known as the cosine similarity
+	highestSimilarity := -1.0
+	var matched int
+	_, docs := lsi.Dims()
+	for i := 0; i < docs; i++ {
+		similarity := pairwise.CosineSimilarity(queryVector.(mat.ColViewer).ColView(0), lsi.(mat.ColViewer).ColView(i))
+		if similarity > highestSimilarity {
+			matched = i
+			highestSimilarity = similarity
+		}
+	}
+
+	fmt.Printf("Matched '%s'", testCorpus[matched])
+	// Output: Matched 'The quick brown fox jumped over the lazy dog'
+}
+
 func TestGetOffsetTimeV1(t *testing.T) {
 	testDataPath := "../../../TestData/FixTimeline"
 	testRootDir, err := my_util.CopyTestData(testDataPath)
@@ -79,27 +131,27 @@ func TestGetOffsetTimeV1(t *testing.T) {
 			staticLineFileSavePath: "bar.html"}, want: -32.09061538461539, wantErr: false},
 		/*
 			WTF,这部剧集
-			Dan Brown's The Lost Symbol
+			Dan Brown'timelineFixer The Lost Symbol
 			内置的英文字幕时间轴是歪的,所以修正完了就错了
 		*/
-		{name: "Dan Brown's The Lost Symbol - S01E01", args: args{
-			enSubFile:              filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E01.chinese(inside).ass"),
-			ch_enSubFile:           filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E01.chinese(简英,shooter).ass"),
+		{name: "Dan Brown'timelineFixer The Lost Symbol - S01E01", args: args{
+			enSubFile:              filepath.Join(testRootDirNo, "Dan Brown'timelineFixer The Lost Symbol - S01E01.chinese(inside).ass"),
+			ch_enSubFile:           filepath.Join(testRootDirNo, "Dan Brown'timelineFixer The Lost Symbol - S01E01.chinese(简英,shooter).ass"),
 			staticLineFileSavePath: "bar.html"},
 			want: 1.3217821782178225, wantErr: false},
-		{name: "Dan Brown's The Lost Symbol - S01E02", args: args{
-			enSubFile:              filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E02.chinese(inside).ass"),
-			ch_enSubFile:           filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E02.chinese(简英,subhd).ass"),
+		{name: "Dan Brown'timelineFixer The Lost Symbol - S01E02", args: args{
+			enSubFile:              filepath.Join(testRootDirNo, "Dan Brown'timelineFixer The Lost Symbol - S01E02.chinese(inside).ass"),
+			ch_enSubFile:           filepath.Join(testRootDirNo, "Dan Brown'timelineFixer The Lost Symbol - S01E02.chinese(简英,subhd).ass"),
 			staticLineFileSavePath: "bar.html"},
 			want: -0.5253383458646617, wantErr: false},
-		{name: "Dan Brown's The Lost Symbol - S01E03", args: args{
-			enSubFile:              filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E03.chinese(inside).ass"),
-			ch_enSubFile:           filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E03.chinese(繁英,xunlei).ass"),
+		{name: "Dan Brown'timelineFixer The Lost Symbol - S01E03", args: args{
+			enSubFile:              filepath.Join(testRootDirNo, "Dan Brown'timelineFixer The Lost Symbol - S01E03.chinese(inside).ass"),
+			ch_enSubFile:           filepath.Join(testRootDirNo, "Dan Brown'timelineFixer The Lost Symbol - S01E03.chinese(繁英,xunlei).ass"),
 			staticLineFileSavePath: "bar.html"},
 			want: -0.505656, wantErr: false},
-		{name: "Dan Brown's The Lost Symbol - S01E04", args: args{
-			enSubFile:              filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E04.chinese(inside).ass"),
-			ch_enSubFile:           filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E04.chinese(简英,zimuku).ass"),
+		{name: "Dan Brown'timelineFixer The Lost Symbol - S01E04", args: args{
+			enSubFile:              filepath.Join(testRootDirNo, "Dan Brown'timelineFixer The Lost Symbol - S01E04.chinese(inside).ass"),
+			ch_enSubFile:           filepath.Join(testRootDirNo, "Dan Brown'timelineFixer The Lost Symbol - S01E04.chinese(简英,zimuku).ass"),
 			staticLineFileSavePath: "bar.html"},
 			want: -0.633415, wantErr: false},
 		/*
@@ -271,7 +323,7 @@ func TestGetOffsetTimeV1(t *testing.T) {
 			want: 0, wantErr: false},
 	}
 
-	s := NewSubTimelineFixer(sub_timeline_fiexer.SubTimelineFixerConfig{
+	timelineFixer := NewSubTimelineFixer(sub_timeline_fiexer.SubTimelineFixerConfig{
 		MaxCompareDialogue: 3,
 		MaxStartTimeDiffSD: 0.1,
 		MinMatchedPercent:  0.1,
@@ -307,7 +359,7 @@ func TestGetOffsetTimeV1(t *testing.T) {
 			*/
 			sub_helper.MergeMultiDialogue4EngSubtitle(infoSrc)
 
-			bok, got, sd, err := s.GetOffsetTimeV1(infoBase, infoSrc, tt.args.ch_enSubFile+"-bar.html", tt.args.ch_enSubFile+".log")
+			bok, got, sd, err := timelineFixer.GetOffsetTimeV1(infoBase, infoSrc, tt.args.ch_enSubFile+"-bar.html", tt.args.ch_enSubFile+".log")
 			if (err != nil) != tt.wantErr {
 				t.Errorf("GetOffsetTimeV1() error = %v, wantErr %v", err, tt.wantErr)
 				return
@@ -324,7 +376,7 @@ func TestGetOffsetTimeV1(t *testing.T) {
 			//}
 
 			if bok == true && got != 0 {
-				_, err = s.FixSubTimeline(infoSrc, got, tt.args.ch_enSubFile+FixMask+infoBase.Ext)
+				_, err = timelineFixer.FixSubTimeline(infoSrc, got, tt.args.ch_enSubFile+FixMask+infoBase.Ext)
 				if err != nil {
 					t.Fatal(err)
 				}
@@ -335,59 +387,106 @@ func TestGetOffsetTimeV1(t *testing.T) {
 	}
 }
 
-func TestTFIDF(t *testing.T) {
-	testCorpus := []string{
-		"The quick brown fox jumped over the lazy dog",
-		"hey diddle diddle, the cat and the fiddle",
-		"the cow jumped over the moon",
-		"the little dog laughed to see such fun",
-		"and the dish ran away with the spoon",
+func TestGetOffsetTimeV2(t *testing.T) {
+	testDataPath := "../../../TestData/FixTimeline"
+	testRootDir, err := my_util.CopyTestData(testDataPath)
+	if err != nil {
+		t.Fatal(err)
 	}
+	testRootDirYes := filepath.Join(testRootDir, "yes")
+	//testRootDirNo := filepath.Join(testRootDir, "no")
+	subParserHub := sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
 
-	query := "the brown fox ran around the dog"
+	type args struct {
+		enSubFile              string
+		ch_enSubFile           string
+		staticLineFileSavePath string
+	}
+	tests := []struct {
+		name    string
+		args    args
+		want    float64
+		wantErr bool
+	}{
+		/*
+			这里有几个比较理想的字幕时间轴校正的示例
+		*/
+		{name: "R&M S05E01", args: args{enSubFile: filepath.Join(testRootDirYes, "R&M S05E01 - English.srt"),
+			ch_enSubFile:           filepath.Join(testRootDirYes, "R&M S05E01 - 简英.srt"),
+			staticLineFileSavePath: "bar.html"}, want: -6.42981818181818, wantErr: false},
+		{name: "R&M S05E10", args: args{enSubFile: filepath.Join(testRootDirYes, "R&M S05E10 - English.ass"),
+			ch_enSubFile:           filepath.Join(testRootDirYes, "R&M S05E10 - 简英.ass"),
+			staticLineFileSavePath: "bar.html"}, want: -6.335985401459854, wantErr: false},
+		{name: "基地 S01E03", args: args{enSubFile: filepath.Join(testRootDirYes, "基地 S01E03 - English.ass"),
+			ch_enSubFile:           filepath.Join(testRootDirYes, "基地 S01E03 - 简英.ass"),
+			staticLineFileSavePath: "bar.html"}, want: -32.09061538461539, wantErr: false},
+	}
 
-	vectoriser := nlp.NewCountVectoriser(StopWords...)
-	transformer := nlp.NewTfidfTransformer()
+	timelineFixer := NewSubTimelineFixer(sub_timeline_fiexer.SubTimelineFixerConfig{
+		MaxCompareDialogue: 3,
+		MaxStartTimeDiffSD: 0.1,
+		MinMatchedPercent:  0.1,
+		MinOffset:          0.1,
+	})
 
-	// set k (the number of dimensions following truncation) to 4
-	reducer := nlp.NewTruncatedSVD(4)
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
 
-	lsiPipeline := nlp.NewPipeline(vectoriser, transformer, reducer)
+			bFind, infoBase, err := subParserHub.DetermineFileTypeFromFile(tt.args.enSubFile)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if bFind == false {
+				t.Fatal("sub not match")
+			}
+			/*
+				这里发现一个梗,内置的英文字幕导出的时候,有可能需要合并多个 Dialogue,见
+				internal/pkg/sub_helper/sub_helper.go 中 MergeMultiDialogue4EngSubtitle 的实现
+			*/
+			//sub_helper.MergeMultiDialogue4EngSubtitle(infoBase)
 
-	// Transform the corpus into an LSI fitting the model to the documents in the process
-	lsi, err := lsiPipeline.FitTransform(testCorpus...)
-	if err != nil {
-		fmt.Printf("Failed to process documents because %v", err)
-		return
-	}
+			bFind, infoSrc, err := subParserHub.DetermineFileTypeFromFile(tt.args.ch_enSubFile)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if bFind == false {
+				t.Fatal("sub not match")
+			}
+			/*
+				这里发现一个梗,内置的英文字幕导出的时候,有可能需要合并多个 Dialogue,见
+				internal/pkg/sub_helper/sub_helper.go 中 MergeMultiDialogue4EngSubtitle 的实现
+			*/
+			//sub_helper.MergeMultiDialogue4EngSubtitle(infoSrc)
 
-	// run the query through the same pipeline that was fitted to the corpus and
-	// to project it into the same dimensional space
-	queryVector, err := lsiPipeline.Transform(query)
-	if err != nil {
-		fmt.Printf("Failed to process documents because %v", err)
-		return
-	}
+			bok, got, sd, err := timelineFixer.GetOffsetTimeV2(infoBase, infoSrc, tt.args.ch_enSubFile+"-bar.html", tt.args.ch_enSubFile+".log")
+			if (err != nil) != tt.wantErr {
+				t.Errorf("GetOffsetTimeV1() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
 
-	// iterate over document feature vectors (columns) in the LSI matrix and compare
-	// with the query vector for similarity.  Similarity is determined by the difference
-	// between the angles of the vectors known as the cosine similarity
-	highestSimilarity := -1.0
-	var matched int
-	_, docs := lsi.Dims()
-	for i := 0; i < docs; i++ {
-		similarity := pairwise.CosineSimilarity(queryVector.(mat.ColViewer).ColView(0), lsi.(mat.ColViewer).ColView(i))
-		if similarity > highestSimilarity {
-			matched = i
-			highestSimilarity = similarity
-		}
-	}
+			// 在一个正负范围内都可以接受
+			if got > tt.want-0.1 && got < tt.want+0.1 {
 
-	fmt.Printf("Matched '%s'", testCorpus[matched])
-	// Output: Matched 'The quick brown fox jumped over the lazy dog'
+			} else {
+				t.Errorf("GetOffsetTimeV1() got = %v, want %v", got, tt.want)
+			}
+			//if got != tt.want {
+			//	t.Errorf("GetOffsetTimeV1() got = %v, want %v", got, tt.want)
+			//}
+
+			if bok == true && got != 0 {
+				_, err = timelineFixer.FixSubTimeline(infoSrc, got, tt.args.ch_enSubFile+FixMask+infoBase.Ext)
+				if err != nil {
+					t.Fatal(err)
+				}
+			}
+
+			println(fmt.Sprintf("GetOffsetTimeV1: %fs SD:%f", got, sd))
+		})
+	}
 }
 
-func TestSubTimelineFixer_GetOffsetTimeV2(t *testing.T) {
+func TestSubTimelineFixer_GetOffsetTimeV3(t *testing.T) {
 
 	subParserHub := sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
 
@@ -428,21 +527,21 @@ func TestSubTimelineFixer_GetOffsetTimeV2(t *testing.T) {
 				这里发现一个梗,内置的英文字幕导出的时候,有可能需要合并多个 Dialogue,见
 				internal/pkg/sub_helper/sub_helper.go 中 MergeMultiDialogue4EngSubtitle 的实现
 			*/
-			sub_helper.MergeMultiDialogue4EngSubtitle(fileInfo)
+			//sub_helper.MergeMultiDialogue4EngSubtitle(fileInfo)
 
-			got, got1, got2, err := s.GetOffsetTimeV2(tt.args.audioInfo, fileInfo, tt.args.staticLineFileSavePath, tt.args.debugInfoFileSavePath)
+			got, got1, got2, err := s.GetOffsetTimeV3(tt.args.audioInfo, fileInfo, tt.args.staticLineFileSavePath, tt.args.debugInfoFileSavePath)
 			if (err != nil) != tt.wantErr {
-				t.Errorf("GetOffsetTimeV2() error = %v, wantErr %v", err, tt.wantErr)
+				t.Errorf("GetOffsetTimeV3() error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
 			if got != tt.want {
-				t.Errorf("GetOffsetTimeV2() got = %v, want %v", got, tt.want)
+				t.Errorf("GetOffsetTimeV3() got = %v, want %v", got, tt.want)
 			}
 			if got1 != tt.want1 {
-				t.Errorf("GetOffsetTimeV2() got1 = %v, want %v", got1, tt.want1)
+				t.Errorf("GetOffsetTimeV3() got1 = %v, want %v", got1, tt.want1)
 			}
 			if got2 != tt.want2 {
-				t.Errorf("GetOffsetTimeV2() got2 = %v, want %v", got2, tt.want2)
+				t.Errorf("GetOffsetTimeV3() got2 = %v, want %v", got2, tt.want2)
 			}
 		})
 	}

+ 29 - 2
internal/pkg/sub_timeline_fixer/static_line.go

@@ -38,7 +38,7 @@ func SaveStaticLineV1(saveFPath string, infoBaseName, infoSrcName string,
 	return nil
 }
 
-func SaveStaticLineV2(name, saveFPath string, xAxis []string, timeLineData []opts.LineData) error {
+func SaveStaticLineV2(name, saveFPath string, xAxis []string, timeLineOrgData []opts.LineData) error {
 
 	// 1.New 一个条形图对象
 	bar := charts.NewLine()
@@ -48,7 +48,34 @@ func SaveStaticLineV2(name, saveFPath string, xAxis []string, timeLineData []opt
 	}))
 	// 3.设置 数据组
 	bar.SetXAxis(xAxis).
-		AddSeries(name+" VAD", timeLineData)
+		AddSeries(name+" VAD", timeLineOrgData)
+	// 4.绘图 生成html
+	outfile, err := os.Create(saveFPath)
+	defer func() {
+		_ = outfile.Close()
+	}()
+	if err != nil {
+		return err
+	}
+	err = bar.Render(outfile)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func SaveStaticLineV3(name, saveFPath string, xAxis []string, timeLineOrgData, fftData []opts.LineData) error {
+
+	// 1.New 一个条形图对象
+	bar := charts.NewLine()
+	// 2.设置 标题 和 子标题
+	bar.SetGlobalOptions(charts.WithTitleOpts(opts.Title{
+		Title: name + " VAD",
+	}))
+	// 3.设置 数据组
+	bar.SetXAxis(xAxis).
+		AddSeries(name+" VAD", timeLineOrgData) //.
+		//AddSeries(name+" FFT", fftData)
 	// 4.绘图 生成html
 	outfile, err := os.Create(saveFPath)
 	defer func() {

+ 0 - 116
internal/pkg/sub_timeline_fixer/sub_unit.go

@@ -1,116 +0,0 @@
-package sub_timeline_fixer
-
-import (
-	"fmt"
-	"github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
-	"github.com/allanpk716/ChineseSubFinder/internal/pkg/vad"
-	"math"
-	"time"
-)
-
-type SubUnit struct {
-	baseTime  time.Time // 这个是基础的时间,后续需要减去这个,不然与导出的片段字幕去对比会有一个起始时间的偏差
-	StartTime time.Time // 这个时间会减去 baseTime 再存储
-	EndTime   time.Time // 这个时间会减去 baseTime 再存储
-	VADList   []vad.VADInfo
-	subCount  int
-	firstAdd  bool
-}
-
-func NewSubUnit() *SubUnit {
-	return &SubUnit{
-		VADList:  make([]vad.VADInfo, 0),
-		subCount: 0,
-		firstAdd: false,
-	}
-}
-
-// Add 添加一句对白进来
-func (s *SubUnit) Add(oneSubStartTime, oneSubEndTime time.Time) {
-
-	if s.firstAdd == false {
-		s.baseTime = oneSubStartTime
-		s.StartTime = oneSubStartTime.Add(-my_util.Time2Duration(s.baseTime))
-		s.firstAdd = true
-	}
-	s.EndTime = oneSubEndTime.Add(-my_util.Time2Duration(s.baseTime))
-	// 每一句对白的开始就人为 VAD active 是 1,直到结束,才是 0
-	s.VADList = append(s.VADList, *vad.NewVADInfoBase(true, time.Duration(s.GetStartTimeNumber()*math.Pow10(9))))
-
-	s.VADList = append(s.VADList, *vad.NewVADInfoBase(false, time.Duration(s.GetEndTimeNumber()*math.Pow10(9))))
-
-	s.subCount++
-}
-
-// AddAndInsert 添加一句对白进来,并且填充中间的空白,间隔 10ms
-func (s *SubUnit) AddAndInsert(oneSubStartTime, oneSubEndTime time.Time) {
-
-	perWindows := float64(vad.FrameDuration) / 1000
-	// 不是第一次添加,那么就需要把两句对白中间间隔的 active == false 的插入,插入间隙
-	if len(s.VADList) > 0 {
-		dd := my_util.Time2Duration(s.baseTime)
-		tmpSubStartTime := oneSubStartTime.Add(-dd)
-		needAddRange := my_util.Time2SecendNumber(tmpSubStartTime) - s.GetEndTimeNumber()
-		for i := 0.0; i < needAddRange; {
-
-			s.VADList = append(s.VADList, *vad.NewVADInfoBase(false, time.Duration((s.GetEndTimeNumber()+i)*math.Pow10(9))))
-			i += perWindows
-		}
-	}
-
-	if s.firstAdd == false {
-		s.baseTime = oneSubStartTime
-		dd := my_util.Time2Duration(s.baseTime)
-		s.StartTime = oneSubStartTime.Add(-dd)
-		s.firstAdd = true
-	}
-
-	s.EndTime = oneSubEndTime.Add(-my_util.Time2Duration(s.baseTime))
-
-	needAddRange := my_util.Time2SecendNumber(oneSubEndTime) - my_util.Time2SecendNumber(oneSubStartTime)
-	for i := 0.0; i < needAddRange; {
-
-		s.VADList = append(s.VADList, *vad.NewVADInfoBase(true, time.Duration((s.GetStartTimeNumber()+i)*math.Pow10(9))))
-		i += perWindows
-	}
-
-	s.subCount++
-}
-
-// GetDialogueCount 获取这个对白单元由几个对话
-func (s SubUnit) GetDialogueCount() int {
-	return s.subCount
-}
-
-// GetStartTimeNumber 获取这个单元的起始时间,单位是秒
-func (s SubUnit) GetStartTimeNumber() float64 {
-	return my_util.Time2SecendNumber(s.StartTime.Add(my_util.Time2Duration(s.baseTime)))
-}
-
-// GetEndTimeNumber 获取这个单元的结束时间,单位是秒
-func (s SubUnit) GetEndTimeNumber() float64 {
-	return my_util.Time2SecendNumber(s.EndTime.Add(my_util.Time2Duration(s.baseTime)))
-}
-
-// GetTimelineRange 开始到结束的时间长度,单位是秒
-func (s SubUnit) GetTimelineRange() float64 {
-	return s.GetEndTimeNumber() - s.GetStartTimeNumber()
-}
-
-func (s SubUnit) GetBaseTimeNumber() float64 {
-	return my_util.Time2SecendNumber(s.baseTime)
-}
-
-// GetFFMPEGCutRange 这里会生成导出 FFMPEG 的参数字段,起始时间和结束的时间长度
-func (s SubUnit) GetFFMPEGCutRange(expandTimeRange int) (string, string) {
-
-	var tmpStartTime time.Time
-	if s.GetStartTimeNumber()-float64(expandTimeRange)*60 < 0 {
-		tmpStartTime = time.Time{}
-	} else {
-		tmpStartTime = s.StartTime.Add(time.Duration(expandTimeRange) * time.Minute).Add(my_util.Time2Duration(s.baseTime))
-	}
-
-	return fmt.Sprintf("%d:%d:%d.%d", tmpStartTime.Hour(), tmpStartTime.Minute(), tmpStartTime.Second(), tmpStartTime.Nanosecond()/1000/1000),
-		fmt.Sprintf("%f", s.GetTimelineRange()+float64(expandTimeRange)*60.0)
-}

+ 2 - 2
internal/pkg/vad/vad_info.go

@@ -28,8 +28,8 @@ func NewVADInfoBase(active bool, nowTime time.Duration) *VADInfo {
 }
 
 const (
-	// Mode vad mode,VAD 的模式
-	Mode = 1
+	// Mode vad mode,VAD 的模式 0-3
+	Mode = 3
 	// FrameDuration frame duration,分析的时间窗口
 	FrameDuration = 10
 )

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

@@ -27,6 +27,27 @@ func (f FileInfo) GetTimeFormat() string {
 	}
 }
 
+// GetDialogueExContent 获取当前字幕文件语言对应索引的对白内容
+// 凡是带有 Eng 的返回 Eng,其他的就与对应语言相关
+func (f FileInfo) GetDialogueExContent(index int) string {
+
+	switch f.Lang {
+	case language.ChineseSimple, language.ChineseTraditional,
+		language.ChineseSimpleJapanese, language.ChineseSimpleKorean,
+		language.ChineseTraditionalJapanese, language.ChineseTraditionalKorean:
+		// 带有中文的,但是又不是中英的
+		return f.DialoguesEx[index].ChLine
+	case language.English, language.ChineseSimpleEnglish, language.ChineseTraditionalEnglish:
+		return f.DialoguesEx[index].EnLine
+	case language.Japanese:
+		return f.DialoguesEx[index].JpLine
+	case language.Korean:
+		return f.DialoguesEx[index].KrLine
+	default:
+		return f.DialoguesEx[index].EnLine
+	}
+}
+
 // OneDialogue 一句对话
 type OneDialogue struct {
 	StartTime string   // 开始时间