| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572 |
- 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"
- "github.com/mndrix/tukey"
- "gonum.org/v1/gonum/mat"
- "os"
- "path/filepath"
- "strings"
- "time"
- )
- type SubTimelineFixer struct {
- fixerConfig sub_timeline_fiexer.SubTimelineFixerConfig
- ffmpegHelper *ffmpeg_helper.FFMPEGHelper
- }
- func NewSubTimelineFixer(fixerConfig sub_timeline_fiexer.SubTimelineFixerConfig) *SubTimelineFixer {
- return &SubTimelineFixer{
- fixerConfig: fixerConfig,
- ffmpegHelper: ffmpeg_helper.NewFFMPEGHelper(),
- }
- }
- // StopWordCounter 停止词统计
- func (s *SubTimelineFixer) StopWordCounter(inString string, per int) []string {
- statisticTimes := make(map[string]int)
- wordsLength := strings.Fields(inString)
- for counts, word := range wordsLength {
- // 判断key是否存在,这个word是字符串,这个counts是统计的word的次数。
- word, ok := statisticTimes[word]
- if ok {
- word = word
- statisticTimes[wordsLength[counts]] = statisticTimes[wordsLength[counts]] + 1
- } else {
- statisticTimes[wordsLength[counts]] = 1
- }
- }
- stopWords := make([]string, 0)
- mapByValue := sortMapByValue(statisticTimes)
- breakIndex := len(mapByValue) * per / 100
- for index, wordInfo := range mapByValue {
- if index > breakIndex {
- break
- }
- stopWords = append(stopWords, wordInfo.Name)
- }
- return stopWords
- }
- // FixSubTimeline 校正时间轴
- func (s *SubTimelineFixer) FixSubTimeline(infoSrc *subparser.FileInfo, inOffsetTime float64, desSaveSubFileFullPath string) (string, error) {
- /*
- 从解析的实例中,正常来说是可以匹配出所有的 Dialogue 对话的 Start 和 End time 的信息
- 然后找到对应的字幕的文件,进行文件内容的替换来做时间轴的校正
- */
- // 偏移时间
- offsetTime := time.Duration(inOffsetTime*1000) * time.Millisecond
- timeFormat := infoSrc.GetTimeFormat()
- fixContent := infoSrc.Content
- for _, srcOneDialogue := range infoSrc.Dialogues {
- timeStart, err := time.Parse(timeFormat, srcOneDialogue.StartTime)
- if err != nil {
- return "", err
- }
- timeEnd, err := time.Parse(timeFormat, srcOneDialogue.EndTime)
- if err != nil {
- return "", err
- }
- fixTimeStart := timeStart.Add(offsetTime)
- fixTimeEnd := timeEnd.Add(offsetTime)
- fixContent = strings.ReplaceAll(fixContent, srcOneDialogue.StartTime, fixTimeStart.Format(timeFormat))
- fixContent = strings.ReplaceAll(fixContent, srcOneDialogue.EndTime, fixTimeEnd.Format(timeFormat))
- }
- dstFile, err := os.Create(desSaveSubFileFullPath)
- if err != nil {
- return "", err
- }
- defer func() {
- _ = dstFile.Close()
- }()
- _, err = dstFile.WriteString(fixContent)
- if err != nil {
- return "", err
- }
- return fixContent, nil
- }
- /*
- 对于 V1 版本的字幕时间轴校正来说,是有特殊的前置要求的
- 1. 视频要有英文字幕
- 2. 外置的字幕必须是中文的双语字幕(简英、繁英)
- */
- // GetOffsetTimeV1 暂时只支持英文的基准字幕,源字幕必须是双语中英字幕
- func (s *SubTimelineFixer) GetOffsetTimeV1(infoBase, infoSrc *subparser.FileInfo, staticLineFileSavePath string, debugInfoFileSavePath string) (bool, float64, float64, error) {
- var debugInfos = make([]string, 0)
- // 构建基准语料库,目前阶段只需要考虑是 En 的就行了
- var baseCorpus = make([]string, 0)
- var baseDialogueFilterMap = make(map[int]int, 0)
- /*
- 这里原来的写法是所有的 base 的都放进去匹配,这样会带来一些不必要的对白
- 需要剔除空白。那么就需要建立一个转换的字典
- */
- for index, oneDialogueEx := range infoBase.DialoguesEx {
- if oneDialogueEx.EnLine == "" {
- continue
- }
- baseCorpus = append(baseCorpus, oneDialogueEx.EnLine)
- baseDialogueFilterMap[len(baseCorpus)-1] = index
- }
- // 初始化
- pipLine, tfidf, err := NewTFIDF(baseCorpus)
- if err != nil {
- return false, 0, 0, err
- }
- /*
- 确认两个字幕间的偏移,暂定的方案是两边都连续匹配上 5 个索引,再抽取一个对话的时间进行修正计算
- */
- maxCompareDialogue := s.fixerConfig.MaxCompareDialogue
- // 基线的长度
- _, docsLength := tfidf.Dims()
- var matchIndexList = make([]MatchIndex, 0)
- sc := NewSubCompare(maxCompareDialogue)
- // 开始比较相似度,默认认为是 Ch_en 就行了
- for srcIndex := 0; srcIndex < len(infoSrc.DialoguesEx); {
- srcOneDialogueEx := infoSrc.DialoguesEx[srcIndex]
- // 这里只考虑 英文 的语言
- if srcOneDialogueEx.EnLine == "" {
- srcIndex++
- continue
- }
- // run the query through the same pipeline that was fitted to the corpus and
- // to project it into the same dimensional space
- queryVector, err := pipLine.Transform(srcOneDialogueEx.EnLine)
- if err != nil {
- return false, 0, 0, err
- }
- // 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 baseIndex int
- // 这里理论上需要把所有的基线遍历一次,但是,一般来说,两个字幕不可能差距在 50 行
- // 这样的好处是有助于提高搜索的性能
- // 那么就以当前的 src 的位置,向前、向后各 50 来遍历
- nowMaxScanLength := srcIndex + 50
- nowMinScanLength := srcIndex - 50
- if nowMinScanLength < 0 {
- nowMinScanLength = 0
- }
- if nowMaxScanLength > docsLength {
- nowMaxScanLength = docsLength
- }
- for i := nowMinScanLength; i < nowMaxScanLength; i++ {
- similarity := pairwise.CosineSimilarity(queryVector.(mat.ColViewer).ColView(0), tfidf.(mat.ColViewer).ColView(i))
- if similarity > highestSimilarity {
- baseIndex = i
- highestSimilarity = similarity
- }
- }
- startBaseIndex, startSrcIndex := sc.GetStartIndex()
- if sc.Add(baseIndex, srcIndex) == false {
- sc.Clear()
- srcIndex = startSrcIndex + 1
- continue
- //sc.Add(baseIndex, srcIndex)
- }
- if sc.Check() == false {
- srcIndex++
- continue
- } else {
- sc.Clear()
- }
- matchIndexList = append(matchIndexList, MatchIndex{
- BaseNowIndex: startBaseIndex,
- //BaseNowIndex: baseDialogueFilterMap[startBaseIndex],
- SrcNowIndex: startSrcIndex,
- Similarity: highestSimilarity,
- })
- //println(fmt.Sprintf("Similarity: %f Base[%d] %s-%s '%s' <--> Src[%d] %s-%s '%s'",
- // highestSimilarity,
- // baseIndex, infoBase.DialoguesEx[baseIndex].relativelyStartTime, infoBase.DialoguesEx[baseIndex].relativelyEndTime, baseCorpus[baseIndex],
- // srcIndex, srcOneDialogueEx.relativelyStartTime, srcOneDialogueEx.relativelyEndTime, srcOneDialogueEx.EnLine))
- srcIndex++
- }
- timeFormat := infoBase.GetTimeFormat()
- var startDiffTimeLineData = make([]opts.LineData, 0)
- var endDiffTimeLineData = make([]opts.LineData, 0)
- var tmpStartDiffTime = make([]float64, 0)
- var tmpEndDiffTime = make([]float64, 0)
- var startDiffTimeList = make(stat.Float64Slice, 0)
- var endDiffTimeList = make(stat.Float64Slice, 0)
- var xAxis = make([]string, 0)
- // 上面找出了连续匹配 maxCompareDialogue:N 次的字幕语句块
- // 求出平均时间偏移
- for mIndex, matchIndexItem := range matchIndexList {
- for i := 0; i < maxCompareDialogue; i++ {
- // 这里会统计连续的这 5 句话的时间差
- //tmpBaseIndex := matchIndexItem.BaseNowIndex + i
- tmpBaseIndex := baseDialogueFilterMap[matchIndexItem.BaseNowIndex+i]
- tmpSrcIndex := matchIndexItem.SrcNowIndex + i
- baseTimeStart, err := time.Parse(timeFormat, infoBase.DialoguesEx[tmpBaseIndex].StartTime)
- if err != nil {
- return false, 0, 0, err
- }
- baseTimeEnd, err := time.Parse(timeFormat, infoBase.DialoguesEx[tmpBaseIndex].EndTime)
- if err != nil {
- return false, 0, 0, err
- }
- srtTimeStart, err := time.Parse(timeFormat, infoSrc.DialoguesEx[tmpSrcIndex].StartTime)
- if err != nil {
- return false, 0, 0, err
- }
- srtTimeEnd, err := time.Parse(timeFormat, infoSrc.DialoguesEx[tmpSrcIndex].EndTime)
- if err != nil {
- return false, 0, 0, err
- }
- TimeDiffStart := baseTimeStart.Sub(srtTimeStart)
- TimeDiffEnd := baseTimeEnd.Sub(srtTimeEnd)
- startDiffTimeLineData = append(startDiffTimeLineData, opts.LineData{Value: TimeDiffStart.Seconds()})
- endDiffTimeLineData = append(endDiffTimeLineData, opts.LineData{Value: TimeDiffEnd.Seconds()})
- tmpStartDiffTime = append(tmpStartDiffTime, TimeDiffStart.Seconds())
- tmpEndDiffTime = append(tmpEndDiffTime, TimeDiffEnd.Seconds())
- startDiffTimeList = append(startDiffTimeList, TimeDiffStart.Seconds())
- endDiffTimeList = append(endDiffTimeList, TimeDiffEnd.Seconds())
- xAxis = append(xAxis, fmt.Sprintf("%d_%d", mIndex, i))
- debugInfos = append(debugInfos, "bs "+infoBase.DialoguesEx[tmpBaseIndex].StartTime+" <-> "+infoBase.DialoguesEx[tmpBaseIndex].EndTime)
- debugInfos = append(debugInfos, "sc "+infoSrc.DialoguesEx[tmpSrcIndex].StartTime+" <-> "+infoSrc.DialoguesEx[tmpSrcIndex].EndTime)
- debugInfos = append(debugInfos, "StartDiffTime: "+fmt.Sprintf("%f", TimeDiffStart.Seconds()))
- //println(fmt.Sprintf("Diff Start-End: %s - %s Base[%d] %s-%s '%s' <--> Src[%d] %s-%s '%s'",
- // TimeDiffStart, TimeDiffEnd,
- // tmpBaseIndex, infoBase.DialoguesEx[tmpBaseIndex].relativelyStartTime, infoBase.DialoguesEx[tmpBaseIndex].relativelyEndTime, infoBase.DialoguesEx[tmpBaseIndex].EnLine,
- // tmpSrcIndex, infoSrc.DialoguesEx[tmpSrcIndex].relativelyStartTime, infoSrc.DialoguesEx[tmpSrcIndex].relativelyEndTime, infoSrc.DialoguesEx[tmpSrcIndex].EnLine))
- }
- debugInfos = append(debugInfos, "---------------------------------------------")
- //println("---------------------------------------------")
- }
- oldMean := stat.Mean(startDiffTimeList)
- oldSd := stat.Sd(startDiffTimeList)
- newMean := -1.0
- newSd := -1.0
- per := 1.0
- // 如果 SD 较大的时候才需要剔除
- if oldSd > 0.1 {
- var outliersMap = make(map[float64]int, 0)
- outliers, _, _ := tukey.Outliers(0.3, tmpStartDiffTime)
- for _, outlier := range outliers {
- outliersMap[outlier] = 0
- }
- var newStartDiffTimeList = make([]float64, 0)
- for _, f := range tmpStartDiffTime {
- _, ok := outliersMap[f]
- if ok == true {
- continue
- }
- newStartDiffTimeList = append(newStartDiffTimeList, f)
- }
- orgLen := startDiffTimeList.Len()
- startDiffTimeList = make(stat.Float64Slice, 0)
- for _, f := range newStartDiffTimeList {
- startDiffTimeList = append(startDiffTimeList, f)
- }
- newLen := startDiffTimeList.Len()
- per = float64(newLen) / float64(orgLen)
- newMean = stat.Mean(startDiffTimeList)
- newSd = stat.Sd(startDiffTimeList)
- }
- if newMean == -1.0 {
- newMean = oldMean
- }
- if newSd == -1.0 {
- newSd = oldSd
- }
- // 不为空的时候,生成调试文件
- if staticLineFileSavePath != "" {
- //staticLineFileSavePath = "bar.html"
- err = SaveStaticLineV1(staticLineFileSavePath, infoBase.Name, infoSrc.Name,
- per, oldMean, oldSd, newMean, newSd, xAxis,
- startDiffTimeLineData, endDiffTimeLineData)
- if err != nil {
- return false, 0, 0, err
- }
- }
- // 跳过的逻辑是 mean 是 0 ,那么现在如果判断有问题,缓存的调试文件继续生成,然后强制返回 0 来跳过后续的逻辑
- // 这里需要考虑,找到的连续 5 句话匹配的有多少句,占比整体所有的 Dialogue 是多少,太低也需要跳过
- matchIndexLineCount := len(matchIndexList) * maxCompareDialogue
- //perMatch := float64(matchIndexLineCount) / float64(len(infoSrc.DialoguesEx))
- perMatch := float64(matchIndexLineCount) / float64(len(baseCorpus))
- if perMatch < s.fixerConfig.MinMatchedPercent {
- tmpContent := infoSrc.Name + fmt.Sprintf(" Sequence match %d dialogues (< %f%%), Skip,", s.fixerConfig.MaxCompareDialogue, s.fixerConfig.MinMatchedPercent*100) + fmt.Sprintf(" %f%% ", perMatch*100)
- debugInfos = append(debugInfos, tmpContent)
- log_helper.GetLogger().Infoln(tmpContent)
- } else {
- tmpContent := infoSrc.Name + fmt.Sprintf(" Sequence match %d dialogues,", s.fixerConfig.MaxCompareDialogue) + fmt.Sprintf(" %f%% ", perMatch*100)
- debugInfos = append(debugInfos, tmpContent)
- log_helper.GetLogger().Infoln(tmpContent)
- }
- // 输出调试的匹配时间轴信息的列表
- if debugInfoFileSavePath != "" {
- err = my_util.WriteStrings2File(debugInfoFileSavePath, debugInfos)
- if err != nil {
- return false, 0, 0, err
- }
- }
- // 虽然有条件判断是认为有问题的,但是返回值还是要填写除去的
- if perMatch < s.fixerConfig.MinMatchedPercent {
- return false, newMean, newSd, nil
- }
- return true, newMean, newSd, nil
- }
- // GetOffsetTimeV2 使用内置的字幕校正外置的字幕时间轴
- func (s *SubTimelineFixer) GetOffsetTimeV2(infoBase, infoSrc *subparser.FileInfo, staticLineFileSavePath string, debugInfoFileSavePath string) (bool, float64, float64, error) {
- srcSubUnitList, err := sub_helper.GetVADINfoFromSub(infoSrc, FrontAndEndPer, SubUnitMaxCount)
- if err != nil {
- return false, 0, 0, err
- }
- // 开始针对对白单元进行匹配
- for _, srcSubUnit := range srcSubUnitList {
- startTimeString, subLength := srcSubUnit.GetFFMPEGCutRange(ExpandTimeRange)
- // 导出当前的字幕文件适合与匹配的范围的临时字幕文件
- nowTmpSubBaseFPath, errString, err := s.ffmpegHelper.ExportSubArgsByTimeRange(infoBase.FileFullPath, "base", startTimeString, subLength)
- if err != nil {
- log_helper.GetLogger().Errorln("ExportSubArgsByTimeRange base", errString, err)
- return false, 0, 0, err
- }
- // 导出当前的字幕文件适合与匹配的范围的临时字幕文件
- startTimeString, subLength = srcSubUnit.GetFFMPEGCutRange(0)
- nowTmpSubSrcFPath, errString, err := s.ffmpegHelper.ExportSubArgsByTimeRange(infoSrc.FileFullPath, "src", startTimeString, subLength)
- if err != nil {
- log_helper.GetLogger().Errorln("ExportSubArgsByTimeRange src", errString, err)
- return false, 0, 0, err
- }
- bok, nowTmpSubBaseFileInfo, err := s.ffmpegHelper.SubParserHub.DetermineFileTypeFromFile(nowTmpSubBaseFPath)
- if err != nil {
- return false, 0, 0, err
- }
- 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)
- outDir := filepath.Dir(nowTmpSubBaseFPath)
- outBaseName := filepath.Base(nowTmpSubBaseFPath)
- outSrcName := filepath.Base(nowTmpSubSrcFPath)
- outBaseNameWithOutExt := strings.ReplaceAll(outBaseName, filepath.Ext(outBaseName), "")
- outSrcNameWithOutExt := strings.ReplaceAll(outSrcName, filepath.Ext(outSrcName), "")
- srcSubVADStaticLineFullPath := filepath.Join(outDir, outSrcNameWithOutExt+"_sub_src.html")
- baseSubVADStaticLineFullPath := filepath.Join(outDir, outBaseNameWithOutExt+"_sub_base.html")
- // 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))
- }
- err = SaveStaticLineV2("Sub src", srcSubVADStaticLineFullPath, nowSrcSubXAxis, nowSrcSubTimeLineData)
- if err != nil {
- return false, 0, 0, err
- }
- // 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", nowVADInfoTimeNumber))
- }
- 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)
- return false, 0, 0, err
- }
- audioVADInfos, err := vad.GetVADInfoFromAudio(vad.AudioInfo{
- FileFullPath: outAudioFPath,
- SampleRate: 16000,
- BitDepth: 16,
- })
- if err != nil {
- return false, 0, 0, err
- }
- var subTimeLineData = make([]opts.LineData, 0)
- var subTimeLineFFTData = make([]opts.LineData, 0)
- var subXAxis = make([]string, 0)
- var audioTimeLineData = make([]opts.LineData, 0)
- var audioTimeLineFFTData = make([]opts.LineData, 0)
- var audioXAxis = make([]string, 0)
- 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.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)
- outBaseName := filepath.Base(outAudioFPath)
- outBaseNameWithOutExt := strings.ReplaceAll(outBaseName, filepath.Ext(outBaseName), "")
- subVADStaticLineFullPath := filepath.Join(outDir, outBaseNameWithOutExt+"_sub.html")
- err = SaveStaticLineV3("Sub", subVADStaticLineFullPath, subXAxis, subTimeLineData, subTimeLineFFTData)
- if err != nil {
- return false, 0, 0, err
- }
- for index, vadInfo := range audioVADInfos {
- audioTimeLineData = append(audioTimeLineData, opts.LineData{Value: vadInfo.Active})
- 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 = SaveStaticLineV3("Audio", audioVADStaticLineFullPath, audioXAxis, audioTimeLineData, audioTimeLineFFTData)
- if err != nil {
- return false, 0, 0, err
- }
- }
- return false, -1, -1, nil
- }
- const FixMask = "-fix"
- const FrontAndEndPer = 0.10 // 前百分之 15 和后百分之 15 都不进行识别
- const SubUnitMaxCount = 100 // 一个 Sub单元有五句对白
- const ExpandTimeRange = 50 // 从字幕的时间轴片段需要向前和向后多匹配一部分的音频,这里定义的就是这个 range 以分钟为单位, 正负 60 秒
|