fixer_test.go 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806
  1. package sub_timeline_fixer
  2. import (
  3. "fmt"
  4. "github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/ass"
  5. "github.com/allanpk716/ChineseSubFinder/internal/logic/sub_parser/srt"
  6. "github.com/allanpk716/ChineseSubFinder/internal/pkg/debug_view"
  7. "github.com/allanpk716/ChineseSubFinder/internal/pkg/my_util"
  8. "github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_helper"
  9. "github.com/allanpk716/ChineseSubFinder/internal/pkg/sub_parser_hub"
  10. "github.com/allanpk716/ChineseSubFinder/internal/pkg/vad"
  11. "github.com/allanpk716/ChineseSubFinder/internal/types/sub_timeline_fiexer"
  12. "github.com/james-bowman/nlp"
  13. "github.com/james-bowman/nlp/measures/pairwise"
  14. "gonum.org/v1/gonum/mat"
  15. "path/filepath"
  16. "strings"
  17. "testing"
  18. )
  19. func TestStopWordCounter(t *testing.T) {
  20. testDataPath := "../../../TestData/FixTimeline"
  21. testRootDir, err := my_util.CopyTestData(testDataPath)
  22. if err != nil {
  23. t.Fatal(err)
  24. }
  25. subParserHub := sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
  26. bFind, info, err := subParserHub.DetermineFileTypeFromFile(filepath.Join(testRootDir, "R&M S05E10 - English.srt"))
  27. if err != nil {
  28. t.Fatal(err)
  29. }
  30. if bFind == false {
  31. t.Fatal("not match sub types")
  32. }
  33. allString := strings.Join(info.OtherLines, " ")
  34. s := SubTimelineFixer{}
  35. stopWords := s.StopWordCounter(strings.ToLower(allString), 5)
  36. print(len(stopWords))
  37. println(info.Name)
  38. }
  39. func TestTFIDF(t *testing.T) {
  40. testCorpus := []string{
  41. "The quick brown fox jumped over the lazy dog",
  42. "hey diddle diddle, the cat and the fiddle",
  43. "the cow jumped over the moon",
  44. "the little dog laughed to see such fun",
  45. "and the dish ran away with the spoon",
  46. }
  47. query := "the brown fox ran around the dog"
  48. vectoriser := nlp.NewCountVectoriser(StopWords...)
  49. transformer := nlp.NewTfidfTransformer()
  50. // set k (the number of dimensions following truncation) to 4
  51. reducer := nlp.NewTruncatedSVD(4)
  52. lsiPipeline := nlp.NewPipeline(vectoriser, transformer, reducer)
  53. // Transform the corpus into an LSI fitting the model to the documents in the process
  54. lsi, err := lsiPipeline.FitTransform(testCorpus...)
  55. if err != nil {
  56. fmt.Printf("Failed to process documents because %v", err)
  57. return
  58. }
  59. // run the query through the same pipeline that was fitted to the corpus and
  60. // to project it into the same dimensional space
  61. queryVector, err := lsiPipeline.Transform(query)
  62. if err != nil {
  63. fmt.Printf("Failed to process documents because %v", err)
  64. return
  65. }
  66. // iterate over document feature vectors (columns) in the LSI matrix and compare
  67. // with the query vector for similarity. Similarity is determined by the difference
  68. // between the angles of the vectors known as the cosine similarity
  69. highestSimilarity := -1.0
  70. var matched int
  71. _, docs := lsi.Dims()
  72. for i := 0; i < docs; i++ {
  73. similarity := pairwise.CosineSimilarity(queryVector.(mat.ColViewer).ColView(0), lsi.(mat.ColViewer).ColView(i))
  74. if similarity > highestSimilarity {
  75. matched = i
  76. highestSimilarity = similarity
  77. }
  78. }
  79. fmt.Printf("Matched '%s'", testCorpus[matched])
  80. // Output: Matched 'The quick brown fox jumped over the lazy dog'
  81. }
  82. func TestGetOffsetTimeV1(t *testing.T) {
  83. testDataPath := "../../../TestData/FixTimeline"
  84. testRootDir, err := my_util.CopyTestData(testDataPath)
  85. if err != nil {
  86. t.Fatal(err)
  87. }
  88. testRootDirYes := filepath.Join(testRootDir, "yes")
  89. testRootDirNo := filepath.Join(testRootDir, "no")
  90. subParserHub := sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
  91. type args struct {
  92. enSubFile string
  93. ch_enSubFile string
  94. staticLineFileSavePath string
  95. }
  96. tests := []struct {
  97. name string
  98. args args
  99. want float64
  100. wantErr bool
  101. }{
  102. /*
  103. 这里有几个比较理想的字幕时间轴校正的示例
  104. */
  105. {name: "R&M S05E01", args: args{enSubFile: filepath.Join(testRootDirYes, "R&M S05E01 - English.srt"),
  106. ch_enSubFile: filepath.Join(testRootDirYes, "R&M S05E01 - 简英.srt"),
  107. staticLineFileSavePath: "bar.html"}, want: -6.42981818181818, wantErr: false},
  108. {name: "R&M S05E10", args: args{enSubFile: filepath.Join(testRootDirYes, "R&M S05E10 - English.ass"),
  109. ch_enSubFile: filepath.Join(testRootDirYes, "R&M S05E10 - 简英.ass"),
  110. staticLineFileSavePath: "bar.html"}, want: -6.335985401459854, wantErr: false},
  111. {name: "基地 S01E03", args: args{enSubFile: filepath.Join(testRootDirYes, "基地 S01E03 - English.ass"),
  112. ch_enSubFile: filepath.Join(testRootDirYes, "基地 S01E03 - 简英.ass"),
  113. staticLineFileSavePath: "bar.html"}, want: -32.09061538461539, wantErr: false},
  114. /*
  115. WTF,这部剧集
  116. Dan Brown'timelineFixer The Lost Symbol
  117. 内置的英文字幕时间轴是歪的,所以修正完了就错了
  118. */
  119. {name: "Dan Brown'timelineFixer The Lost Symbol - S01E01", args: args{
  120. enSubFile: filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E01.chinese(inside).ass"),
  121. ch_enSubFile: filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E01.chinese(简英,shooter).ass"),
  122. staticLineFileSavePath: "bar.html"},
  123. want: 1.3217821782178225, wantErr: false},
  124. {name: "Dan Brown'timelineFixer The Lost Symbol - S01E02", args: args{
  125. enSubFile: filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E02.chinese(inside).ass"),
  126. ch_enSubFile: filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E02.chinese(简英,subhd).ass"),
  127. staticLineFileSavePath: "bar.html"},
  128. want: -0.5253383458646617, wantErr: false},
  129. {name: "Dan Brown'timelineFixer The Lost Symbol - S01E03", args: args{
  130. enSubFile: filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E03.chinese(inside).ass"),
  131. ch_enSubFile: filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E03.chinese(繁英,xunlei).ass"),
  132. staticLineFileSavePath: "bar.html"},
  133. want: -0.505656, wantErr: false},
  134. {name: "Dan Brown'timelineFixer The Lost Symbol - S01E04", args: args{
  135. enSubFile: filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E04.chinese(inside).ass"),
  136. ch_enSubFile: filepath.Join(testRootDirNo, "Dan Brown's The Lost Symbol - S01E04.chinese(简英,zimuku).ass"),
  137. staticLineFileSavePath: "bar.html"},
  138. want: -0.633415, wantErr: false},
  139. /*
  140. 只有一个是字幕下载了一个错误的,其他的无需修正
  141. */
  142. {name: "Don't Breathe 2 (2021) - shooter-srt", args: args{
  143. enSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(inside).srt"),
  144. ch_enSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(简英,shooter).srt"),
  145. staticLineFileSavePath: "bar.html"},
  146. want: 0, wantErr: false},
  147. {name: "Don't Breathe 2 (2021) - subhd-srt error matched sub", args: args{
  148. enSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(inside).srt"),
  149. ch_enSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(简英,subhd).srt"),
  150. staticLineFileSavePath: "bar.html"},
  151. want: 0, wantErr: false},
  152. {name: "Don't Breathe 2 (2021) - xunlei-ass", args: args{
  153. enSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(inside).ass"),
  154. ch_enSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(简英,xunlei).ass"),
  155. staticLineFileSavePath: "bar.html"},
  156. want: 0, wantErr: false},
  157. {name: "Don't Breathe 2 (2021) - zimuku-ass", args: args{
  158. enSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(inside).ass"),
  159. ch_enSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(简英,zimuku).ass"),
  160. staticLineFileSavePath: "bar.html"},
  161. want: 0, wantErr: false},
  162. /*
  163. 基地
  164. */
  165. {name: "Foundation (2021) - S01E01", args: args{
  166. enSubFile: filepath.Join(testRootDirNo, "Foundation (2021) - S01E01.chinese(inside).ass"),
  167. ch_enSubFile: filepath.Join(testRootDirNo, "Foundation (2021) - S01E01.chinese(简英,zimuku).ass"),
  168. staticLineFileSavePath: "bar.html"},
  169. want: 0, wantErr: false},
  170. {name: "Foundation (2021) - S01E02", args: args{
  171. enSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E02.chinese(inside).ass"),
  172. ch_enSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E02.chinese(简英,subhd).ass"),
  173. staticLineFileSavePath: "bar.html"},
  174. want: -30.624840, wantErr: false},
  175. {name: "Foundation (2021) - S01E03", args: args{
  176. enSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E03.chinese(inside).ass"),
  177. ch_enSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E03.chinese(简英,subhd).ass"),
  178. staticLineFileSavePath: "bar.html"},
  179. want: -32.085037037037054, wantErr: false},
  180. {name: "Foundation (2021) - S01E04", args: args{
  181. enSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E04.chinese(inside).ass"),
  182. ch_enSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E04.chinese(简英,subhd).ass"),
  183. staticLineFileSavePath: "bar.html"},
  184. want: -36.885074, wantErr: false},
  185. {name: "Foundation (2021) - S01E04", args: args{
  186. enSubFile: filepath.Join(testRootDirNo, "Foundation (2021) - S01E04.chinese(inside).srt"),
  187. ch_enSubFile: filepath.Join(testRootDirNo, "Foundation (2021) - S01E04.chinese(繁英,shooter).srt"),
  188. staticLineFileSavePath: "bar.html"},
  189. want: 0, wantErr: false},
  190. /*
  191. The Card Counter
  192. */
  193. {name: "The Card Counter", args: args{
  194. enSubFile: filepath.Join(testRootDirNo, "The Card Counter (2021).chinese(inside).ass"),
  195. ch_enSubFile: filepath.Join(testRootDirNo, "The Card Counter (2021).chinese(简英,xunlei).ass"),
  196. staticLineFileSavePath: "bar.html"},
  197. want: 0, wantErr: false},
  198. {name: "The Card Counter", args: args{
  199. enSubFile: filepath.Join(testRootDirNo, "The Card Counter (2021).chinese(inside).ass"),
  200. ch_enSubFile: filepath.Join(testRootDirNo, "The Card Counter (2021).chinese(简英,shooter).ass"),
  201. staticLineFileSavePath: "bar.html"},
  202. want: 0.224844, wantErr: false},
  203. /*
  204. Kingdom Ashin of the North
  205. */
  206. {name: "Kingdom Ashin of the North - error matched sub", args: args{
  207. enSubFile: filepath.Join(testRootDirNo, "Kingdom Ashin of the North (2021).chinese(inside).ass"),
  208. ch_enSubFile: filepath.Join(testRootDirNo, "Kingdom Ashin of the North (2021).chinese(简英,subhd).ass"),
  209. staticLineFileSavePath: "bar.html"},
  210. want: 0, wantErr: false},
  211. /*
  212. Only Murders in the Building
  213. */
  214. {name: "Only Murders in the Building - S01E06", args: args{
  215. enSubFile: filepath.Join(testRootDirNo, "Only Murders in the Building - S01E06.chinese(inside).ass"),
  216. ch_enSubFile: filepath.Join(testRootDirNo, "Only Murders in the Building - S01E06.chinese(简英,subhd).ass"),
  217. staticLineFileSavePath: "bar.html"},
  218. want: 0, wantErr: false},
  219. {name: "Only Murders in the Building - S01E08", args: args{
  220. enSubFile: filepath.Join(testRootDirNo, "Only Murders in the Building - S01E08.chinese(inside).ass"),
  221. ch_enSubFile: filepath.Join(testRootDirNo, "Only Murders in the Building - S01E08.chinese(简英,subhd).ass"),
  222. staticLineFileSavePath: "bar.html"},
  223. want: 0, wantErr: false},
  224. /*
  225. Ted Lasso
  226. */
  227. {name: "Ted Lasso - S02E09", args: args{
  228. enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E09.chinese(inside).ass"),
  229. ch_enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E09.chinese(简英,subhd).ass"),
  230. staticLineFileSavePath: "bar.html"},
  231. want: 0, wantErr: false},
  232. {name: "Ted Lasso - S02E09", args: args{
  233. enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E09.chinese(inside).ass"),
  234. ch_enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E09.chinese(简英,zimuku).ass"),
  235. staticLineFileSavePath: "bar.html"},
  236. want: 0, wantErr: false},
  237. {name: "Ted Lasso - S02E10", args: args{
  238. enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(inside).ass"),
  239. ch_enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(简英,subhd).ass"),
  240. staticLineFileSavePath: "bar.html"},
  241. want: 0, wantErr: false},
  242. {name: "Ted Lasso - S02E10", args: args{
  243. enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(inside).ass"),
  244. ch_enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(简英,zimuku).ass"),
  245. staticLineFileSavePath: "bar.html"},
  246. want: 0, wantErr: false},
  247. {name: "Ted Lasso - S02E10", args: args{
  248. enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(inside).ass"),
  249. ch_enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(简英,shooter).ass"),
  250. staticLineFileSavePath: "bar.html"},
  251. want: 0, wantErr: false},
  252. {name: "Ted Lasso - S02E11", args: args{
  253. enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E11.chinese(inside).ass"),
  254. ch_enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E11.chinese(简英,subhd).ass"),
  255. staticLineFileSavePath: "bar.html"},
  256. want: 0, wantErr: false},
  257. {name: "Ted Lasso - S02E11", args: args{
  258. enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E11.chinese(inside).ass"),
  259. ch_enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E11.chinese(简英,zimuku).ass"),
  260. staticLineFileSavePath: "bar.html"},
  261. want: 0, wantErr: false},
  262. {name: "Ted Lasso - S02E12", args: args{
  263. enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E12.chinese(inside).ass"),
  264. ch_enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E12.chinese(简英,subhd).ass"),
  265. staticLineFileSavePath: "bar.html"},
  266. want: 0, wantErr: false},
  267. {name: "Ted Lasso - S02E12", args: args{
  268. enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E12.chinese(inside).ass"),
  269. ch_enSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E12.chinese(简英,shooter).ass"),
  270. staticLineFileSavePath: "bar.html"},
  271. want: 0, wantErr: false},
  272. /*
  273. The Protégé
  274. */
  275. {name: "The Protégé", args: args{
  276. enSubFile: filepath.Join(testRootDirNo, "The Protégé (2021).chinese(inside).ass"),
  277. ch_enSubFile: filepath.Join(testRootDirNo, "The Protégé (2021).chinese(简英,zimuku).ass"),
  278. staticLineFileSavePath: "bar.html"},
  279. want: 0, wantErr: false},
  280. {name: "The Protégé", args: args{
  281. enSubFile: filepath.Join(testRootDirNo, "The Protégé (2021).chinese(inside).srt"),
  282. ch_enSubFile: filepath.Join(testRootDirNo, "The Protégé (2021).chinese(简英,shooter).srt"),
  283. staticLineFileSavePath: "bar.html"},
  284. want: 0, wantErr: false},
  285. /*
  286. The Witcher Nightmare of the Wolf
  287. */
  288. {name: "The Witcher Nightmare of the Wolf", args: args{
  289. enSubFile: filepath.Join(testRootDirNo, "The Witcher Nightmare of the Wolf.chinese(inside).ass"),
  290. ch_enSubFile: filepath.Join(testRootDirNo, "The Witcher Nightmare of the Wolf.chinese(简英,zimuku).ass"),
  291. staticLineFileSavePath: "bar.html"},
  292. want: 0, wantErr: false},
  293. /*
  294. What If…!
  295. */
  296. {name: "What If…! - S01E07", args: args{
  297. enSubFile: filepath.Join(testRootDirNo, "What If…! - S01E07.chinese(inside).ass"),
  298. ch_enSubFile: filepath.Join(testRootDirNo, "What If…! - S01E07.chinese(简英,subhd).ass"),
  299. staticLineFileSavePath: "bar.html"},
  300. want: 0, wantErr: false},
  301. {name: "What If…! - S01E09", args: args{
  302. enSubFile: filepath.Join(testRootDirNo, "What If…! - S01E09.chinese(inside).srt"),
  303. ch_enSubFile: filepath.Join(testRootDirNo, "What If…! - S01E09.chinese(简英,shooter).srt"),
  304. staticLineFileSavePath: "bar.html"},
  305. want: 0, wantErr: false},
  306. }
  307. for _, tt := range tests {
  308. t.Run(tt.name, func(t *testing.T) {
  309. bFind, infoBase, err := subParserHub.DetermineFileTypeFromFile(tt.args.enSubFile)
  310. if err != nil {
  311. t.Fatal(err)
  312. }
  313. if bFind == false {
  314. t.Fatal("sub not match")
  315. }
  316. /*
  317. 这里发现一个梗,内置的英文字幕导出的时候,有可能需要合并多个 Dialogue,见
  318. internal/pkg/sub_helper/sub_helper.go 中 MergeMultiDialogue4EngSubtitle 的实现
  319. */
  320. sub_helper.MergeMultiDialogue4EngSubtitle(infoBase)
  321. bFind, infoSrc, err := subParserHub.DetermineFileTypeFromFile(tt.args.ch_enSubFile)
  322. if err != nil {
  323. t.Fatal(err)
  324. }
  325. if bFind == false {
  326. t.Fatal("sub not match")
  327. }
  328. /*
  329. 这里发现一个梗,内置的英文字幕导出的时候,有可能需要合并多个 Dialogue,见
  330. internal/pkg/sub_helper/sub_helper.go 中 MergeMultiDialogue4EngSubtitle 的实现
  331. */
  332. sub_helper.MergeMultiDialogue4EngSubtitle(infoSrc)
  333. bok, got, sd, err := timelineFixer.GetOffsetTimeV1(infoBase, infoSrc, tt.args.ch_enSubFile+"-bar.html", tt.args.ch_enSubFile+".log")
  334. if (err != nil) != tt.wantErr {
  335. t.Errorf("GetOffsetTimeV1() error = %v, wantErr %v", err, tt.wantErr)
  336. return
  337. }
  338. // 在一个正负范围内都可以接受
  339. if got > tt.want-0.1 && got < tt.want+0.1 {
  340. } else {
  341. t.Errorf("GetOffsetTimeV1() got = %v, want %v", got, tt.want)
  342. }
  343. //if got != tt.want {
  344. // t.Errorf("GetOffsetTimeV1() got = %v, want %v", got, tt.want)
  345. //}
  346. if bok == true && got != 0 {
  347. _, err = timelineFixer.FixSubTimeline(infoSrc, got, tt.args.ch_enSubFile+FixMask+infoBase.Ext)
  348. if err != nil {
  349. t.Fatal(err)
  350. }
  351. }
  352. println(fmt.Sprintf("GetOffsetTimeV1: %fs SD:%f", got, sd))
  353. })
  354. }
  355. }
  356. func TestGetOffsetTimeV2_BaseSub(t *testing.T) {
  357. testDataPath := "../../../TestData/FixTimeline"
  358. testRootDir, err := my_util.CopyTestData(testDataPath)
  359. if err != nil {
  360. t.Fatal(err)
  361. }
  362. testRootDirYes := filepath.Join(testRootDir, "yes")
  363. testRootDirNo := filepath.Join(testRootDir, "no")
  364. subParserHub := sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
  365. type args struct {
  366. baseSubFile string
  367. srcSubFile string
  368. staticLineFileSavePath string
  369. }
  370. tests := []struct {
  371. name string
  372. args args
  373. want float64
  374. wantErr bool
  375. }{
  376. /*
  377. 这里有几个比较理想的字幕时间轴校正的示例
  378. */
  379. {name: "R&M S05E01", args: args{baseSubFile: filepath.Join(testRootDirYes, "R&M S05E01 - English.srt"),
  380. srcSubFile: filepath.Join(testRootDirYes, "R&M S05E01 - 简英.srt"),
  381. staticLineFileSavePath: "bar.html"}, want: -6.4, wantErr: false},
  382. {name: "R&M S05E01-1", args: args{baseSubFile: filepath.Join(testRootDirYes, "R&M S05E01 - English.srt"),
  383. srcSubFile: filepath.Join(testRootDirYes, "R&M S05E01 - English.srt"),
  384. staticLineFileSavePath: "bar.html"}, want: 0, wantErr: false},
  385. {name: "R&M S05E10-0", args: args{baseSubFile: filepath.Join(testRootDirYes, "R&M S05E10 - English.ass"),
  386. srcSubFile: filepath.Join(testRootDirYes, "R&M S05E10 - 简英.ass"),
  387. staticLineFileSavePath: "bar.html"}, want: -6.405985401459854, wantErr: false},
  388. {name: "R&M S05E10-1", args: args{baseSubFile: filepath.Join(testRootDirYes, "R&M S05E10 - 简英.ass"),
  389. srcSubFile: filepath.Join(testRootDirYes, "R&M S05E10 - English.ass"),
  390. staticLineFileSavePath: "bar.html"}, want: 6.405985401459854, wantErr: false},
  391. {name: "R&M S05E10-2", args: args{baseSubFile: filepath.Join(testRootDirYes, "R&M S05E10 - 简英.ass"),
  392. srcSubFile: filepath.Join(testRootDirYes, "R&M S05E10 - 简英.ass"),
  393. staticLineFileSavePath: "bar.html"}, want: 0, wantErr: false},
  394. /*
  395. 基地
  396. */
  397. {name: "Foundation (2021) - S01E01", args: args{
  398. baseSubFile: filepath.Join(testRootDirNo, "Foundation (2021) - S01E01.chinese(inside).ass"),
  399. srcSubFile: filepath.Join(testRootDirNo, "Foundation (2021) - S01E01.chinese(简英,zimuku).ass"),
  400. staticLineFileSavePath: "bar.html"},
  401. want: 0, wantErr: false},
  402. {name: "Foundation (2021) - S01E02", args: args{
  403. baseSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E02.chinese(inside).ass"),
  404. srcSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E02.chinese(简英,subhd).ass"),
  405. staticLineFileSavePath: "bar.html"},
  406. want: -30.624840, wantErr: false},
  407. {name: "Foundation (2021) - S01E03", args: args{
  408. baseSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E03.chinese(inside).ass"),
  409. srcSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E03.chinese(简英,subhd).ass"),
  410. staticLineFileSavePath: "bar.html"},
  411. want: -32.085037037037054, wantErr: false},
  412. {name: "Foundation (2021) - S01E04", args: args{
  413. baseSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E04.chinese(inside).ass"),
  414. srcSubFile: filepath.Join(testRootDirYes, "Foundation (2021) - S01E04.chinese(简英,subhd).ass"),
  415. staticLineFileSavePath: "bar.html"},
  416. want: -36.885074, wantErr: false},
  417. {name: "Foundation (2021) - S01E04", args: args{
  418. baseSubFile: filepath.Join(testRootDirNo, "Foundation (2021) - S01E04.chinese(inside).srt"),
  419. srcSubFile: filepath.Join(testRootDirNo, "Foundation (2021) - S01E04.chinese(繁英,shooter).srt"),
  420. staticLineFileSavePath: "bar.html"},
  421. want: 0, wantErr: false},
  422. /*
  423. Don't Breathe 2 (2021)
  424. */
  425. {name: "Don't Breathe 2 (2021) - zimuku-ass", args: args{
  426. baseSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(inside).ass"),
  427. srcSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(简英,zimuku).ass"),
  428. staticLineFileSavePath: "bar.html"},
  429. want: 0, wantErr: false},
  430. {name: "Don't Breathe 2 (2021) - shooter-srt", args: args{
  431. baseSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(inside).srt"),
  432. srcSubFile: filepath.Join(testRootDirNo, "Don't Breathe 2 (2021).chinese(简英,shooter).srt"),
  433. staticLineFileSavePath: "bar.html"},
  434. want: 0, wantErr: false},
  435. /*
  436. Only Murders in the Building
  437. */
  438. {name: "Only Murders in the Building - S01E06", args: args{
  439. baseSubFile: filepath.Join(testRootDirNo, "Only Murders in the Building - S01E06.chinese(inside).ass"),
  440. srcSubFile: filepath.Join(testRootDirNo, "Only Murders in the Building - S01E06.chinese(简英,subhd).ass"),
  441. staticLineFileSavePath: "bar.html"},
  442. want: 0, wantErr: false},
  443. {name: "Only Murders in the Building - S01E08", args: args{
  444. baseSubFile: filepath.Join(testRootDirNo, "Only Murders in the Building - S01E08.chinese(inside).ass"),
  445. srcSubFile: filepath.Join(testRootDirNo, "Only Murders in the Building - S01E08.chinese(简英,subhd).ass"),
  446. staticLineFileSavePath: "bar.html"},
  447. want: 0, wantErr: false},
  448. /*
  449. Ted Lasso
  450. */
  451. {name: "Ted Lasso - S02E09", args: args{
  452. baseSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E09.chinese(inside).ass"),
  453. srcSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E09.chinese(简英,subhd).ass"),
  454. staticLineFileSavePath: "bar.html"},
  455. want: 0, wantErr: false},
  456. {name: "Ted Lasso - S02E09", args: args{
  457. baseSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E09.chinese(inside).ass"),
  458. srcSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E09.chinese(简英,zimuku).ass"),
  459. staticLineFileSavePath: "bar.html"},
  460. want: 0, wantErr: false},
  461. {name: "Ted Lasso - S02E10", args: args{
  462. baseSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(inside).ass"),
  463. srcSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(简英,subhd).ass"),
  464. staticLineFileSavePath: "bar.html"},
  465. want: 0, wantErr: false},
  466. {name: "Ted Lasso - S02E10", args: args{
  467. baseSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(inside).ass"),
  468. srcSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(简英,zimuku).ass"),
  469. staticLineFileSavePath: "bar.html"},
  470. want: 0, wantErr: false},
  471. {name: "Ted Lasso - S02E10", args: args{
  472. baseSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(inside).ass"),
  473. srcSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E10.chinese(简英,shooter).ass"),
  474. staticLineFileSavePath: "bar.html"},
  475. want: 0, wantErr: false},
  476. {name: "Ted Lasso - S02E11", args: args{
  477. baseSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E11.chinese(inside).ass"),
  478. srcSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E11.chinese(简英,subhd).ass"),
  479. staticLineFileSavePath: "bar.html"},
  480. want: 0, wantErr: false},
  481. {name: "Ted Lasso - S02E11", args: args{
  482. baseSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E11.chinese(inside).ass"),
  483. srcSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E11.chinese(简英,zimuku).ass"),
  484. staticLineFileSavePath: "bar.html"},
  485. want: 0, wantErr: false},
  486. {name: "Ted Lasso - S02E12", args: args{
  487. baseSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E12.chinese(inside).ass"),
  488. srcSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E12.chinese(简英,subhd).ass"),
  489. staticLineFileSavePath: "bar.html"},
  490. want: 0, wantErr: false},
  491. {name: "Ted Lasso - S02E12", args: args{
  492. baseSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E12.chinese(inside).ass"),
  493. srcSubFile: filepath.Join(testRootDirNo, "Ted Lasso - S02E12.chinese(简英,shooter).ass"),
  494. staticLineFileSavePath: "bar.html"},
  495. want: 0, wantErr: false},
  496. /*
  497. The Protégé
  498. */
  499. {name: "The Protégé", args: args{
  500. baseSubFile: filepath.Join(testRootDirNo, "The Protégé (2021).chinese(inside).ass"),
  501. srcSubFile: filepath.Join(testRootDirNo, "The Protégé (2021).chinese(简英,zimuku).ass"),
  502. staticLineFileSavePath: "bar.html"},
  503. want: 0, wantErr: false},
  504. {name: "The Protégé", args: args{
  505. baseSubFile: filepath.Join(testRootDirNo, "The Protégé (2021).chinese(inside).srt"),
  506. srcSubFile: filepath.Join(testRootDirNo, "The Protégé (2021).chinese(简英,shooter).srt"),
  507. staticLineFileSavePath: "bar.html"},
  508. want: 0, wantErr: false},
  509. /*
  510. The Witcher Nightmare of the Wolf
  511. */
  512. {name: "The Witcher Nightmare of the Wolf", args: args{
  513. baseSubFile: filepath.Join(testRootDirNo, "The Witcher Nightmare of the Wolf.chinese(inside).ass"),
  514. srcSubFile: filepath.Join(testRootDirNo, "The Witcher Nightmare of the Wolf.chinese(简英,zimuku).ass"),
  515. staticLineFileSavePath: "bar.html"},
  516. want: 0, wantErr: false},
  517. /*
  518. What If…!
  519. */
  520. {name: "What If…! - S01E07", args: args{
  521. baseSubFile: filepath.Join(testRootDirNo, "What If…! - S01E07.chinese(inside).ass"),
  522. srcSubFile: filepath.Join(testRootDirNo, "What If…! - S01E07.chinese(简英,subhd).ass"),
  523. staticLineFileSavePath: "bar.html"},
  524. want: 0, wantErr: false},
  525. {name: "What If…! - S01E09", args: args{
  526. baseSubFile: filepath.Join(testRootDirNo, "What If…! - S01E09.chinese(inside).srt"),
  527. srcSubFile: filepath.Join(testRootDirNo, "What If…! - S01E09.chinese(简英,shooter).srt"),
  528. staticLineFileSavePath: "bar.html"},
  529. want: 0, wantErr: false},
  530. }
  531. for _, tt := range tests {
  532. t.Run(tt.name, func(t *testing.T) {
  533. bFind, infoBase, err := subParserHub.DetermineFileTypeFromFile(tt.args.baseSubFile)
  534. if err != nil {
  535. t.Fatal(err)
  536. }
  537. if bFind == false {
  538. t.Fatal("sub not match")
  539. }
  540. /*
  541. 这里发现一个梗,内置的英文字幕导出的时候,有可能需要合并多个 Dialogue,见
  542. internal/pkg/sub_helper/sub_helper.go 中 MergeMultiDialogue4EngSubtitle 的实现
  543. */
  544. //sub_helper.MergeMultiDialogue4EngSubtitle(infoBase)
  545. bFind, infoSrc, err := subParserHub.DetermineFileTypeFromFile(tt.args.srcSubFile)
  546. if err != nil {
  547. t.Fatal(err)
  548. }
  549. if bFind == false {
  550. t.Fatal("sub not match")
  551. }
  552. /*
  553. 这里发现一个梗,内置的英文字幕导出的时候,有可能需要合并多个 Dialogue,见
  554. internal/pkg/sub_helper/sub_helper.go 中 MergeMultiDialogue4EngSubtitle 的实现
  555. */
  556. //sub_helper.MergeMultiDialogue4EngSubtitle(infoSrc)
  557. // ---------------------------------------------------------------------------------------
  558. // Base,截取的部分要大于 Src 的部分
  559. //baseUnitListOld, err := sub_helper.GetVADInfoFeatureFromSub(infoBase, V2_FrontAndEndPerBase, 100000, true)
  560. //if err != nil {
  561. // t.Fatal(err)
  562. //}
  563. //baseUnitOld := baseUnitListOld[0]
  564. baseUnitNew, err := sub_helper.GetVADInfoFeatureFromSubNew(infoBase, timelineFixer.FixerConfig.V2_FrontAndEndPerBase)
  565. if err != nil {
  566. t.Fatal(err)
  567. }
  568. // ---------------------------------------------------------------------------------------
  569. // Src,截取的部分要小于 Base 的部分
  570. //srcUnitListOld, err := sub_helper.GetVADInfoFeatureFromSub(infoSrc, V2_FrontAndEndPerSrc, 100000, true)
  571. //if err != nil {
  572. // t.Fatal(err)
  573. //}
  574. //srcUnitOld := srcUnitListOld[0]
  575. srcUnitNew, err := sub_helper.GetVADInfoFeatureFromSubNew(infoSrc, timelineFixer.FixerConfig.V2_FrontAndEndPerSrc)
  576. if err != nil {
  577. t.Fatal(err)
  578. }
  579. // ---------------------------------------------------------------------------------------
  580. //bok, got, sd, err := timelineFixer.GetOffsetTimeV2(&baseUnitOld, &srcUnitOld, nil, 0)
  581. bok, got, sd, err := timelineFixer.GetOffsetTimeV2(baseUnitNew, srcUnitNew, nil)
  582. if (err != nil) != tt.wantErr {
  583. t.Errorf("GetOffsetTimeV1() error = %v, wantErr %v", err, tt.wantErr)
  584. return
  585. }
  586. if bok == false {
  587. t.Fatal("GetOffsetTimeV2 return false")
  588. }
  589. if got > -0.2 && got < 0.2 && tt.want == 0 {
  590. // 如果 offset time > -0.2 且 < 0.2 则认为无需调整时间轴,为0
  591. } else if got > tt.want-0.1 && got < tt.want+0.1 {
  592. // 在一个正负范围内都可以接受
  593. } else {
  594. t.Errorf("GetOffsetTimeV1() got = %v, want %v", got, tt.want)
  595. }
  596. //if bok == true && got != 0 {
  597. // _, err = timelineFixer.FixSubTimeline(infoSrc, got, tt.args.srcSubFile+FixMask+infoBase.Ext)
  598. // if err != nil {
  599. // t.Fatal(err)
  600. // }
  601. //}
  602. println(fmt.Sprintf("GetOffsetTimeV2: %fs SD:%f", got, sd))
  603. })
  604. }
  605. }
  606. func TestGetOffsetTimeV2_BaseAudio(t *testing.T) {
  607. subParserHub := sub_parser_hub.NewSubParserHub(ass.NewParser(), srt.NewParser())
  608. type fields struct {
  609. fixerConfig sub_timeline_fiexer.SubTimelineFixerConfig
  610. }
  611. type args struct {
  612. audioInfo vad.AudioInfo
  613. subFilePath string
  614. }
  615. tests := []struct {
  616. name string
  617. fields fields
  618. args args
  619. want bool
  620. want1 float64
  621. want2 float64
  622. wantErr bool
  623. }{
  624. // Rick and Morty - S05E01
  625. {name: "Rick and Morty - S05E01 -- 0",
  626. args: args{audioInfo: vad.AudioInfo{
  627. FileFullPath: "C:\\Tmp\\Rick and Morty - S05E01\\未知语言_1.pcm"},
  628. subFilePath: "C:\\Tmp\\Rick and Morty - S05E01\\英_2.ass"},
  629. want: true, want1: 0,
  630. },
  631. {name: "Rick and Morty - S05E01 -- 1",
  632. args: args{audioInfo: vad.AudioInfo{
  633. FileFullPath: "C:\\Tmp\\Rick and Morty - S05E01\\未知语言_1.pcm"},
  634. subFilePath: "C:\\Tmp\\Rick and Morty - S05E01\\英_2.srt"},
  635. want: true, want1: 0,
  636. },
  637. {name: "Rick and Morty - S05E01 -- 2",
  638. args: args{audioInfo: vad.AudioInfo{
  639. FileFullPath: "C:\\Tmp\\Rick and Morty - S05E01\\未知语言_1.pcm"},
  640. subFilePath: "C:\\Tmp\\Rick and Morty - S05E01\\R&M S05E01 - 简英.srt"},
  641. want: true, want1: -6.1,
  642. },
  643. {name: "Rick and Morty - S05E01 -- 3",
  644. args: args{audioInfo: vad.AudioInfo{
  645. FileFullPath: "C:\\Tmp\\Rick and Morty - S05E01\\未知语言_1.pcm"},
  646. subFilePath: "C:\\Tmp\\Rick and Morty - S05E01\\R&M S05E01 - 简.ass"},
  647. want: true, want1: -6.1,
  648. },
  649. // Rick and Morty - S05E10
  650. {name: "Rick and Morty - S05E10 -- 0",
  651. args: args{audioInfo: vad.AudioInfo{
  652. FileFullPath: "C:\\Tmp\\Rick and Morty - S05E10\\英_1.pcm"},
  653. subFilePath: "C:\\Tmp\\Rick and Morty - S05E10\\英_2.ass"},
  654. want: true, want1: 0,
  655. },
  656. {name: "Rick and Morty - S05E10 -- 1",
  657. args: args{audioInfo: vad.AudioInfo{
  658. FileFullPath: "C:\\Tmp\\Rick and Morty - S05E10\\英_1.pcm"},
  659. subFilePath: "C:\\Tmp\\Rick and Morty - S05E10\\英_2.srt"},
  660. want: true, want1: 0,
  661. },
  662. {name: "Rick and Morty - S05E10 -- 2",
  663. args: args{audioInfo: vad.AudioInfo{
  664. FileFullPath: "C:\\Tmp\\Rick and Morty - S05E10\\英_1.pcm"},
  665. subFilePath: "C:\\Tmp\\Rick and Morty - S05E10\\R&M S05E10 - 简英.ass"},
  666. want: true, want1: -6.0,
  667. },
  668. // Foundation - S01E09
  669. {name: "Foundation - S01E09 -- 0",
  670. args: args{audioInfo: vad.AudioInfo{
  671. FileFullPath: "C:\\Tmp\\Foundation - S01E09\\英_1.pcm"},
  672. subFilePath: "C:\\Tmp\\Foundation - S01E09\\英_2.srt"},
  673. want: true, want1: 0,
  674. },
  675. {name: "Foundation - S01E09 -- 1",
  676. args: args{audioInfo: vad.AudioInfo{
  677. FileFullPath: "C:\\Tmp\\Foundation - S01E09\\英_1.pcm"},
  678. subFilePath: "C:\\Tmp\\Foundation - S01E09\\简_6.srt"},
  679. want: true, want1: 0,
  680. },
  681. {name: "Foundation - S01E09 -- 2",
  682. args: args{audioInfo: vad.AudioInfo{
  683. FileFullPath: "C:\\Tmp\\Foundation - S01E09\\英_1.pcm"},
  684. subFilePath: "C:\\Tmp\\Foundation - S01E09\\chinese(简英,zimuku).default.ass"},
  685. want: true, want1: -30,
  686. },
  687. {name: "Foundation - S01E09 -- 3",
  688. args: args{audioInfo: vad.AudioInfo{
  689. FileFullPath: "C:\\Tmp\\Foundation - S01E09\\英_1.pcm"},
  690. subFilePath: "C:\\Tmp\\Foundation - S01E09\\chinese(简英,zimuku-fix).ass"},
  691. want: true, want1: 0,
  692. },
  693. }
  694. for _, tt := range tests {
  695. t.Run(tt.name, func(t *testing.T) {
  696. bFind, infoSrc, err := subParserHub.DetermineFileTypeFromFile(tt.args.subFilePath)
  697. if err != nil {
  698. t.Fatal(err)
  699. }
  700. if bFind == false {
  701. t.Fatal("sub not match")
  702. }
  703. /*
  704. 这里发现一个梗,内置的英文字幕导出的时候,有可能需要合并多个 Dialogue,见
  705. internal/pkg/sub_helper/sub_helper.go 中 MergeMultiDialogue4EngSubtitle 的实现
  706. */
  707. //sub_helper.MergeMultiDialogue4EngSubtitle(infoSrc)
  708. // Src,截取的部分要小于 Base 的部分
  709. srcUnitNew, err := sub_helper.GetVADInfoFeatureFromSubNew(infoSrc, timelineFixer.FixerConfig.V2_FrontAndEndPerSrc)
  710. if err != nil {
  711. t.Fatal(err)
  712. }
  713. audioVADInfos, err := vad.GetVADInfoFromAudio(vad.AudioInfo{
  714. FileFullPath: tt.args.audioInfo.FileFullPath,
  715. SampleRate: 16000,
  716. BitDepth: 16,
  717. }, true)
  718. if err != nil {
  719. t.Fatal(err)
  720. }
  721. println("-------New--------")
  722. got, got1, sd, err := timelineFixer.GetOffsetTimeV2(nil, srcUnitNew, audioVADInfos)
  723. if (err != nil) != tt.wantErr {
  724. t.Errorf("GetOffsetTimeV3() error = %v, wantErr %v", err, tt.wantErr)
  725. return
  726. }
  727. debug_view.SaveDebugChartBase(audioVADInfos, "audioVADInfos", "audioVADInfos")
  728. debug_view.SaveDebugChart(*srcUnitNew, "srcUnitNew", "srcUnitNew")
  729. if got != tt.want {
  730. t.Errorf("GetOffsetTimeV3() got = %v, want %v", got, tt.want)
  731. }
  732. if got1 > -0.2 && got1 < 0.2 && tt.want1 == 0 {
  733. // 如果 offset time > -0.2 且 < 0.2 则认为无需调整时间轴,为0
  734. } else if got1 > tt.want1-0.1 && got1 < tt.want1+0.1 {
  735. // 在一个正负范围内都可以接受
  736. } else {
  737. t.Errorf("GetOffsetTimeV1() got = %v, want %v", got, tt.want)
  738. }
  739. _, err = timelineFixer.FixSubTimeline(infoSrc, got1, tt.args.subFilePath+FixMask+infoSrc.Ext)
  740. if err != nil {
  741. t.Fatal(err)
  742. }
  743. println(fmt.Sprintf("GetOffsetTimeV2: %vs SD:%v", got1, sd))
  744. })
  745. }
  746. }
  747. var timelineFixer = NewSubTimelineFixer(sub_timeline_fiexer.SubTimelineFixerConfig{
  748. // V1
  749. V1_MaxCompareDialogue: 3,
  750. V1_MaxStartTimeDiffSD: 0.1,
  751. V1_MinMatchedPercent: 0.1,
  752. V1_MinOffset: 0.1,
  753. // V2
  754. V2_SubOneUnitProcessTimeOut: 5 * 60,
  755. V2_FrontAndEndPerBase: 0.15,
  756. V2_FrontAndEndPerSrc: 0.0,
  757. V2_WindowMatchPer: 0.7,
  758. V2_CompareParts: 5,
  759. V2_FixThreads: 3,
  760. V2_MaxStartTimeDiffSD: 0.1,
  761. V2_MinOffset: 0.1,
  762. })