hyperframes 0.8.150 → 0.8.152

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/dist/{add-3ZIFEN2U.js → add-SA33REHW.js} +14 -14
  2. package/dist/{animatedGifPrep-77LPFLKJ.js → animatedGifPrep-3YXJQZ7L.js} +9 -9
  3. package/dist/api.js +19 -19
  4. package/dist/{asset-EYSZEHM6.js → asset-PUVPTWPD.js} +2 -2
  5. package/dist/{assetCataloger-KX3HD7ZI.js → assetCataloger-VWE2PLBX.js} +4 -4
  6. package/dist/{auth-L6IWNRXY.js → auth-PS3M6AI7.js} +1 -1
  7. package/dist/{autoUpdate-2O3HZB65.js → autoUpdate-CMO4SHWU.js} +2 -2
  8. package/dist/{backgroundChecks-BH2LMJYE.js → backgroundChecks-EDZ2A55J.js} +3 -3
  9. package/dist/backgroundChecksWorker.js +3 -3
  10. package/dist/{batchRender-ZBKAYLWU.js → batchRender-7PLJISCN.js} +5 -5
  11. package/dist/{beats-IVLF2566.js → beats-2MDSIOE7.js} +15 -15
  12. package/dist/{benchmark-SYZFW2G7.js → benchmark-KBBXAPI4.js} +10 -10
  13. package/dist/{browser-OSW3L25S.js → browser-OMWKZRSW.js} +5 -5
  14. package/dist/{bundleWithLocalizedFonts-I4CWOXLK.js → bundleWithLocalizedFonts-76DMCRIW.js} +2 -2
  15. package/dist/{capture-AZOQ57TM.js → capture-LRUUXH5Q.js} +2 -2
  16. package/dist/{capture-V5ICGSVJ.js → capture-YGDC7GN3.js} +13 -13
  17. package/dist/{catalog-VBJOA2Y5.js → catalog-SMEBKEVO.js} +14 -14
  18. package/dist/{catch-up-TKSEDNJK.js → catch-up-OMILX3WE.js} +4 -4
  19. package/dist/{check-3HFISKDB.js → check-NUZ62LIW.js} +288 -64
  20. package/dist/{checkBrowser-MXBDNZJB.js → checkBrowser-MSTVEL4V.js} +181 -38
  21. package/dist/{chunk-73RBX2E7.js → chunk-3DZBQ7MH.js} +3 -3
  22. package/dist/{chunk-6QB7F2II.js → chunk-3E4OCOC5.js} +1 -1
  23. package/dist/{chunk-OQ7XMM3S.js → chunk-3HPJM5IQ.js} +1 -1
  24. package/dist/{chunk-UJB7356Y.js → chunk-3LO7BBJX.js} +3 -3
  25. package/dist/{chunk-APDLWWFG.js → chunk-3VGU2SH7.js} +1 -1
  26. package/dist/{chunk-DUUFDLC4.js → chunk-4MLYMDQ4.js} +1 -1
  27. package/dist/{chunk-AXPNVC3N.js → chunk-4UMKAUZI.js} +1 -1
  28. package/dist/{chunk-XPQSL5VS.js → chunk-54DMWFQZ.js} +8 -1
  29. package/dist/chunk-57GN6KWT.js +179 -0
  30. package/dist/{chunk-374QQGBQ.js → chunk-5CS4T6C6.js} +63 -12
  31. package/dist/{chunk-PCWOSS7G.js → chunk-5ERVRJMV.js} +3 -3
  32. package/dist/{chunk-TZPZE5PS.js → chunk-5VMSUW4Z.js} +6 -3
  33. package/dist/{chunk-M6UDJ2E7.js → chunk-6Z4YJ5Q3.js} +3 -3
  34. package/dist/{chunk-FJWEYVEM.js → chunk-7FYFEXD2.js} +658 -198
  35. package/dist/{chunk-UQYQ7TBA.js → chunk-7NPSXBIN.js} +2 -2
  36. package/dist/chunk-7TASSBN2.js +215 -0
  37. package/dist/{chunk-F2RLIE7P.js → chunk-ANS56TPS.js} +11 -11
  38. package/dist/{chunk-DOVJRUFM.js → chunk-BQK3J7XO.js} +1 -1
  39. package/dist/{chunk-FPAT5HTI.js → chunk-BYJYQWEX.js} +10 -10
  40. package/dist/{chunk-XXPYZBZ3.js → chunk-C6TPGUWD.js} +2 -2
  41. package/dist/{chunk-XQMEGSHG.js → chunk-D6RJXQLD.js} +1 -1
  42. package/dist/{chunk-RW7NLGPD.js → chunk-DFXK7KUO.js} +25 -17
  43. package/dist/{chunk-6SFPQ2RZ.js → chunk-E6EYZ33P.js} +2 -2
  44. package/dist/{chunk-AKYQGZLU.js → chunk-EVTHG34H.js} +2 -2
  45. package/dist/{chunk-5NAYNDBT.js → chunk-EXH6L2XZ.js} +6 -2
  46. package/dist/{chunk-AIJS74LG.js → chunk-FFDC5OWV.js} +8 -2
  47. package/dist/{chunk-4WFNTWKZ.js → chunk-FNN5QINS.js} +2 -2
  48. package/dist/{chunk-DMKI64EZ.js → chunk-FSZAFCU7.js} +1 -1
  49. package/dist/{chunk-S5F26DUL.js → chunk-GQETH4PE.js} +3 -3
  50. package/dist/{chunk-3UAIVF4L.js → chunk-H2SGM2V2.js} +1 -1
  51. package/dist/{chunk-O4DC5CHF.js → chunk-H7FXBK3Q.js} +1 -1
  52. package/dist/{chunk-UVVI2NJQ.js → chunk-HBCRRCTX.js} +109 -31
  53. package/dist/{chunk-NFQY7EIC.js → chunk-IBYC3VUA.js} +1 -1
  54. package/dist/{chunk-2KAL22B5.js → chunk-IEDT4EXE.js} +1 -1
  55. package/dist/{chunk-I6DATIHC.js → chunk-IZIRIDJH.js} +2 -2
  56. package/dist/{chunk-Z6WZDGCS.js → chunk-IZKXUYZE.js} +2 -2
  57. package/dist/chunk-KCFF4ENE.js +24 -0
  58. package/dist/{chunk-6KWA3XPS.js → chunk-KZ3L3W4Q.js} +1 -1
  59. package/dist/{chunk-7IEGPJ4P.js → chunk-LBT4AMSF.js} +1 -1
  60. package/dist/{chunk-7JTFPUMA.js → chunk-LECTCGGS.js} +1 -1
  61. package/dist/{chunk-7E4VHPKJ.js → chunk-LVPSWUS6.js} +5 -5
  62. package/dist/{chunk-6NPFJ255.js → chunk-MNHPG2UO.js} +1 -1
  63. package/dist/{chunk-FN6KAS7A.js → chunk-NHAG2KCE.js} +7 -7
  64. package/dist/{chunk-423DJVX2.js → chunk-PKO2EWO7.js} +1 -1
  65. package/dist/{chunk-ZNHAJCEA.js → chunk-PRXAOZWZ.js} +28 -13
  66. package/dist/{chunk-VQ7YIURZ.js → chunk-PTXZDSW5.js} +5 -5
  67. package/dist/{chunk-XANZ4R4P.js → chunk-QECEOV6W.js} +3 -3
  68. package/dist/{chunk-BLRJKBKP.js → chunk-QESSGUKX.js} +1 -1
  69. package/dist/{chunk-WLIUULZQ.js → chunk-RN5I2DP5.js} +29 -15
  70. package/dist/chunk-S3RCK56N.js +3795 -0
  71. package/dist/{chunk-UAIUVPZE.js → chunk-SWCSL6WS.js} +6 -6
  72. package/dist/{chunk-2PSPARWP.js → chunk-TYXH3EKP.js} +4 -0
  73. package/dist/{chunk-FMNVTKX2.js → chunk-U3BCDVZO.js} +1 -1
  74. package/dist/{chunk-LW5VFBSY.js → chunk-VS732BUG.js} +1 -1
  75. package/dist/{chunk-LFNQDTSH.js → chunk-VYKPBRCB.js} +63 -12
  76. package/dist/{chunk-M5B42RL5.js → chunk-WUUOFMHU.js} +3 -3
  77. package/dist/{chunk-A6ZO4BX7.js → chunk-XEFFJRYX.js} +2 -2
  78. package/dist/{chunk-EL5QVJEG.js → chunk-XINGD6MS.js} +4 -4
  79. package/dist/chunk-XSEGMQYA.js +30 -0
  80. package/dist/{chunk-CZQLCBWY.js → chunk-YG2Y55KF.js} +1 -1
  81. package/dist/{chunk-B3XLIBFL.js → chunk-YIJMVFEJ.js} +1 -1
  82. package/dist/{chunk-JDB65PPT.js → chunk-YOEGUBBY.js} +13 -12
  83. package/dist/{chunk-AFRXL75S.js → chunk-YYEYOFHW.js} +1 -1
  84. package/dist/{chunk-R5A7454P.js → chunk-ZTM3RMAD.js} +2 -2
  85. package/dist/{clean-7HU7S7DT.js → clean-F75G45BI.js} +56 -35
  86. package/dist/cli.js +53 -53
  87. package/dist/{cloud-HAXHBO25.js → cloud-KVJAOUWH.js} +4 -4
  88. package/dist/{cloudrun-A3PIJ6DX.js → cloudrun-Q7Q7O4GY.js} +6 -6
  89. package/dist/{compare-6IPSQMSM.js → compare-DGUJ4QWU.js} +30 -30
  90. package/dist/{compiler-UQP2VXN6.js → compiler-NCN6PTVH.js} +6 -6
  91. package/dist/{component-FF3AU24M.js → component-PSYGT3JV.js} +3 -3
  92. package/dist/{compositions-JSH4MZXB.js → compositions-J3GAETIC.js} +13 -13
  93. package/dist/{delete-G763SW64.js → delete-MR7LRGRP.js} +2 -2
  94. package/dist/{deterministicFonts-C5JQLCTS.js → deterministicFonts-EOMDR6ZF.js} +3 -3
  95. package/dist/{dist-HUMFCJ6T.js → dist-2YYJYYPY.js} +3 -3
  96. package/dist/{dist-Z7Q3JS7K.js → dist-3MHXG6EJ.js} +4 -4
  97. package/dist/{doctor-WZB6WMBY.js → doctor-JOUGR5UK.js} +15 -15
  98. package/dist/{events-T4YFQC5G.js → events-63BKDWVO.js} +3 -3
  99. package/dist/{events-MUWJJCC7.js → events-FZFBJCIH.js} +2 -2
  100. package/dist/{feedback-AUTHQNN2.js → feedback-QLKWUDXR.js} +8 -8
  101. package/dist/{ffmpeg-TZB5WAYE.js → ffmpeg-MJFMBBSF.js} +2 -2
  102. package/dist/{figma-NBG6WBI7.js → figma-YJ7HGVQC.js} +3 -3
  103. package/dist/fontLocalizeCli.js +12 -12
  104. package/dist/{get-OXBJLKLV.js → get-54MUUXMR.js} +2 -2
  105. package/dist/{gpuPolicy-HBCCEQUS.js → gpuPolicy-5OVNR75D.js} +9 -9
  106. package/dist/{grade-compare-2LX4FDH7.js → grade-compare-7IDUF673.js} +32 -32
  107. package/dist/{gsapParserAcorn-VXZ3ZIF2.js → gsapParserAcorn-7NCK2LVT.js} +2 -2
  108. package/dist/{help-LJI46EDO.js → help-GQFYO5LU.js} +1 -1
  109. package/dist/{history-MFKAQEHU.js → history-UI3O7CSO.js} +77 -205
  110. package/dist/hyperframe-runtime.js +54 -54
  111. package/dist/hyperframe.manifest.json +1 -1
  112. package/dist/hyperframe.runtime.iife.js +54 -54
  113. package/dist/hyperframes-player.global.js +3 -3
  114. package/dist/{info-D6Y5O2QE.js → info-VQV3APEX.js} +9 -9
  115. package/dist/{init-47NZPEBI.js → init-2UBFV5D7.js} +31 -31
  116. package/dist/{inspect-U5UXJK5D.js → inspect-36ACJ3CO.js} +31 -31
  117. package/dist/{installer-AW7VFVTQ.js → installer-GDUZDJG4.js} +7 -7
  118. package/dist/{keyframes-5G334NWB.js → keyframes-GPBU34BG.js} +227 -82
  119. package/dist/{lambda-GASGXN3A.js → lambda-CBJVLTHY.js} +7 -7
  120. package/dist/{layout-OXAQFLJS.js → layout-2IHFV6EI.js} +31 -31
  121. package/dist/{lint-F36QFSNF.js → lint-IR6IYCEO.js} +9 -9
  122. package/dist/{lintProject-SSEXWCH6.js → lintProject-XQWXJYSC.js} +3 -3
  123. package/dist/{list-TR6QCI53.js → list-UAGKWARH.js} +2 -2
  124. package/dist/{login-6LA2RWTC.js → login-2GYCNCZF.js} +4 -4
  125. package/dist/{manager-ES2UFT4X.js → manager-X44GKUEL.js} +3 -3
  126. package/dist/{manager-GWU5J6RJ.js → manager-ZLJD3EAY.js} +2 -2
  127. package/dist/{media-treatment-BOHYDWVY.js → media-treatment-Z553F7XI.js} +14 -14
  128. package/dist/{models-6GW5Q3IW.js → models-6AZTNNSO.js} +15 -15
  129. package/dist/{motionShot-O7AOMPUN.js → motionShot-ARRL7WXT.js} +29 -16
  130. package/dist/{normalize-audio-UO5B4O76.js → normalize-audio-CME4NUB6.js} +6 -6
  131. package/dist/{open-JV2EHVKM.js → open-HU2VXUFD.js} +4 -4
  132. package/dist/{parakeetRunner-23DZUF6G.js → parakeetRunner-X5MKFMCB.js} +16 -16
  133. package/dist/{pipeline-KTICSIK7.js → pipeline-MNFTEBJU.js} +11 -11
  134. package/dist/{play-CMUYPXBC.js → play-2BBJDDAZ.js} +24 -24
  135. package/dist/{preflight-JOD4OXGC.js → preflight-JYOX64YP.js} +4 -4
  136. package/dist/{present-ZDT5FM2H.js → present-OE2WL2NY.js} +20 -20
  137. package/dist/{preview-ASHLGKDD.js → preview-RYSCCAWJ.js} +21 -21
  138. package/dist/{producer-FFH2KUAZ.js → producer-SQWNRO66.js} +1 -1
  139. package/dist/{projectConfig-FHIAOPKW.js → projectConfig-B55Z7YKQ.js} +9 -9
  140. package/dist/{publication-WO5ZXURT.js → publication-MDZCOBJP.js} +4 -4
  141. package/dist/{publish-QWO2W7FB.js → publish-AILV36BO.js} +12 -12
  142. package/dist/{remove-background-BO5MQR25.js → remove-background-6F72LKHG.js} +12 -12
  143. package/dist/{render-XTJKGT3G.js → render-2MZKU3L5.js} +10 -10
  144. package/dist/{render-SDHFXL3Y.js → render-5EREH6YD.js} +95 -37
  145. package/dist/{render-HS3YOLSD.js → render-6GH5FSXU.js} +5 -5
  146. package/dist/{render-batch-J64AJHQY.js → render-batch-UMBUQWWS.js} +5 -5
  147. package/dist/renderSetupWorker.js +6 -6
  148. package/dist/{resolver-IHNWMDJA.js → resolver-KO6WWO5P.js} +6 -6
  149. package/dist/{sherpa-FXBBYMDR.js → sherpa-DADGYWNR.js} +15 -15
  150. package/dist/skills/hyperframes/references/frame-worker-core.md +11 -9
  151. package/dist/skills/hyperframes/references/subagent-dispatch.md +1 -1
  152. package/dist/skills/hyperframes-cli/SKILL.md +3 -0
  153. package/dist/skills/hyperframes-cli/references/lint-validate-inspect.md +5 -1
  154. package/dist/skills/media-use/audio/scripts/lib/tts.cleanup.test.mjs +175 -0
  155. package/dist/skills/media-use/audio/scripts/lib/tts.mjs +24 -19
  156. package/dist/skills/media-use/references/grading.md +1 -2
  157. package/dist/skills/media-use/references/meta.md +17 -17
  158. package/dist/skills/media-use/references/operations.md +6 -6
  159. package/dist/skills/media-use/references/setup-providers.md +3 -4
  160. package/dist/skills/media-use/scripts/lib/cutlist.mjs +9 -2
  161. package/dist/skills/media-use/scripts/lib/cutlist.test.mjs +12 -0
  162. package/dist/skills/media-use/scripts/lib/telemetry.mjs +4 -1
  163. package/dist/skills/media-use/scripts/lib/telemetry.test.mjs +20 -0
  164. package/dist/skills/media-use/scripts/transcript-cut.mjs +23 -4
  165. package/dist/skills/media-use/scripts/transcript-cut.test.mjs +224 -0
  166. package/dist/{skills-ALLSN7TB.js → skills-QLKSNU4Y.js} +5 -5
  167. package/dist/{skillsUpdateCheck-EIT5NWM6.js → skillsUpdateCheck-Z2QQGF2W.js} +3 -3
  168. package/dist/{snapshot-Y3I76OIZ.js → snapshot-D5BBFXOS.js} +342 -154
  169. package/dist/{src-7V6LBC56.js → src-AVQBAECC.js} +10 -8
  170. package/dist/{src-S5IZVJUX.js → src-V7263E47.js} +11 -11
  171. package/dist/{staticProjectServer-QLWWGHZE.js → staticProjectServer-HNWSVEWN.js} +20 -20
  172. package/dist/studio/assets/{hyperframes-player-kup62bDR.js → hyperframes-player-0BY7-PK7.js} +1 -1
  173. package/dist/studio/assets/{index-B8BBYY8L.js → index-COrWdzlj.js} +1 -1
  174. package/dist/studio/assets/{index-CS_m0TkX.js → index-p-VXwuJ2.js} +132 -132
  175. package/dist/studio/assets/{index-DQ5jGodF.js → index-rmD5k_ES.js} +1 -1
  176. package/dist/studio/index.html +1 -1
  177. package/dist/{studio-api-UBFLZ3DA.js → studio-api-UXKTDWLT.js} +30 -16
  178. package/dist/{studioServer-REOVCIFP.js → studioServer-JOVR24O3.js} +51 -51
  179. package/dist/{synthesize-22B7L5GX.js → synthesize-MBDVUSH3.js} +2 -1
  180. package/dist/{telemetry-C3GDIXHO.js → telemetry-UW7XAM5W.js} +3 -3
  181. package/dist/{timeline-H42EGLMT.js → timeline-JSGTZ3AN.js} +73 -20
  182. package/dist/{tokens-2K7ISTKV.js → tokens-ADDBGTQ7.js} +2 -2
  183. package/dist/{transcribe-NUPASAUP.js → transcribe-FEHOK626.js} +14 -14
  184. package/dist/{transcribe-DVM7UFZZ.js → transcribe-P6MJGSZJ.js} +20 -20
  185. package/dist/{tts-QSAQZVEZ.js → tts-POI553RG.js} +1 -1
  186. package/dist/{updateCheck-3RWFSEZD.js → updateCheck-IBWMLXQ4.js} +2 -2
  187. package/dist/{upgrade-YKKL36N7.js → upgrade-NHZ7AYPO.js} +2 -2
  188. package/dist/{urlDownloader-6JNJYLPN.js → urlDownloader-ULDRBWS5.js} +8 -8
  189. package/dist/{validate-Z7KQT7GO.js → validate-NVWNXNBV.js} +25 -25
  190. package/dist/{video-EXL2JDSL.js → video-74DRD2DN.js} +9 -9
  191. package/package.json +1 -1
  192. package/dist/chunk-CEND6BLF.js +0 -3795
  193. package/dist/chunk-WBLXWOYK.js +0 -15
@@ -47,6 +47,155 @@ function readPcm(filePath) {
47
47
  return samples;
48
48
  }
49
49
 
50
+ // Three kept ranges, none starting or ending on a frame boundary of any rate below.
51
+ const UNALIGNED_KEEP = "0.137-1.913,2.845-4.111,4.9-5.47";
52
+ const UNALIGNED_KEPT_SECONDS = 1.776 + 1.266 + 0.57;
53
+
54
+ function cutVideo(t, rate, keep = UNALIGNED_KEEP) {
55
+ const { dir, cleanup } = fixture();
56
+ t.after(cleanup);
57
+
58
+ const source = join(dir, "source.mp4");
59
+ execFileSync("ffmpeg", [
60
+ "-y",
61
+ "-hide_banner",
62
+ "-loglevel",
63
+ "error",
64
+ "-f",
65
+ "lavfi",
66
+ "-i",
67
+ `testsrc2=size=160x90:rate=${rate}:duration=6`,
68
+ "-f",
69
+ "lavfi",
70
+ "-i",
71
+ `sine=frequency=440:duration=6:sample_rate=${SAMPLE_RATE}`,
72
+ "-c:v",
73
+ "libx264",
74
+ "-preset",
75
+ "ultrafast",
76
+ "-pix_fmt",
77
+ "yuv420p",
78
+ "-c:a",
79
+ "aac",
80
+ source,
81
+ ]);
82
+
83
+ const transcriptPath = join(dir, "transcript.json");
84
+ writeFileSync(transcriptPath, JSON.stringify({ words: [{ text: "tone", start: 0, end: 6 }] }));
85
+
86
+ const output = join(dir, "out.mp4");
87
+ const result = run([
88
+ "--input",
89
+ source,
90
+ "--transcript",
91
+ transcriptPath,
92
+ "--keep",
93
+ keep,
94
+ "--out",
95
+ output,
96
+ "--json",
97
+ ]);
98
+ assert.equal(result.status, 0, result.stderr || result.stdout);
99
+ return output;
100
+ }
101
+
102
+ function readVideoTimestamps(filePath) {
103
+ const probe = JSON.parse(
104
+ execFileSync(
105
+ "ffprobe",
106
+ [
107
+ "-v",
108
+ "error",
109
+ "-select_streams",
110
+ "v:0",
111
+ "-show_entries",
112
+ "stream=time_base:packet=pts",
113
+ "-of",
114
+ "json",
115
+ "--",
116
+ filePath,
117
+ ],
118
+ { encoding: "utf8" },
119
+ ),
120
+ );
121
+ const [tickNum, tickDen] = probe.streams[0].time_base.split("/").map(Number);
122
+ // Packets come in decode order; B-frames put that out of presentation order.
123
+ const pts = probe.packets.map((packet) => Number(packet.pts)).sort((a, b) => a - b);
124
+ return { tickNum, tickDen, pts };
125
+ }
126
+
127
+ for (const rate of ["30", "30000/1001", "60"]) {
128
+ test(
129
+ `puts every frame of a ${rate} fps video cut on the frame grid, with no gaps`,
130
+ { skip: !HAS_FFMPEG },
131
+ (t) => {
132
+ const [rateNum, rateDen = 1] = rate.split("/").map(Number);
133
+ const { tickNum, tickDen, pts } = readVideoTimestamps(cutVideo(t, rate));
134
+ assert.ok(pts.length > rateNum / rateDen, "expected several seconds of video");
135
+
136
+ // Frame i belongs at exactly i / rate seconds. Compared as integers, so a
137
+ // timestamp rounded to the millisecond (0.067 for 2/30) cannot pass.
138
+ const offGrid = pts.filter(
139
+ (value, index) => value * tickNum * rateNum !== index * rateDen * tickDen,
140
+ );
141
+ assert.equal(
142
+ offGrid.length,
143
+ 0,
144
+ `${offGrid.length} of ${pts.length} frames are off the ${rate} fps grid (timestamps in units of ${tickNum}/${tickDen} s)`,
145
+ );
146
+ },
147
+ );
148
+ }
149
+
150
+ test(
151
+ "keeps the keyframes of a video cut no more than a second apart",
152
+ { skip: !HAS_FFMPEG },
153
+ (t) => {
154
+ // One kept range of almost four seconds, so only the encoder decides where keyframes go.
155
+ const output = cutVideo(t, "30", "0.137-4.111,4.9-5.47");
156
+ const probe = JSON.parse(
157
+ execFileSync(
158
+ "ffprobe",
159
+ [
160
+ "-v",
161
+ "error",
162
+ "-select_streams",
163
+ "v:0",
164
+ "-show_entries",
165
+ "packet=pts_time,flags",
166
+ "-of",
167
+ "json",
168
+ "--",
169
+ output,
170
+ ],
171
+ { encoding: "utf8" },
172
+ ),
173
+ );
174
+ const keyframes = probe.packets
175
+ .filter((packet) => packet.flags.startsWith("K"))
176
+ .map((packet) => Number(packet.pts_time))
177
+ .sort((a, b) => a - b);
178
+ const gaps = keyframes.slice(1).map((time, index) => time - keyframes[index]);
179
+ const widest = Math.max(...gaps);
180
+ // A frame of slack: the forced keyframe is the first frame at or after each second.
181
+ assert.ok(widest < 1.05, `keyframes are up to ${widest.toFixed(3)}s apart`);
182
+ },
183
+ );
184
+
185
+ test(
186
+ "does not pad the audio of a video cut with encoder priming at each splice",
187
+ { skip: !HAS_FFMPEG },
188
+ (t) => {
189
+ const seconds = readPcm(cutVideo(t, "30")).length / SAMPLE_RATE;
190
+ // The single AAC encode may round the end up by a frame or two (23 ms each).
191
+ // Encoding every segment separately instead adds about 25 ms per splice on top.
192
+ assert.ok(
193
+ Math.abs(seconds - UNALIGNED_KEPT_SECONDS) < 0.05,
194
+ `audio runs ${seconds.toFixed(3)}s for ${UNALIGNED_KEPT_SECONDS.toFixed(3)}s kept`,
195
+ );
196
+ },
197
+ );
198
+
50
199
  test(
51
200
  "keeps a spliced tone continuous at every cut, with no raw phase jump or silence gap",
52
201
  { skip: !HAS_FFMPEG },
@@ -115,3 +264,78 @@ test(
115
264
  );
116
265
  },
117
266
  );
267
+
268
+ // Frame 32 at 30 fps sits at 1.0666666 s. A -ss rounded to 1.067 lands just past
269
+ // it, so ffmpeg's accurate seek starts that segment on frame 33 and the cut comes
270
+ // out a frame short from the join on.
271
+ const THIRTY_FPS_FRAME_32 = 32 / 30;
272
+
273
+ test("a kept range starting on a 30 fps frame keeps that frame", { skip: !HAS_FFMPEG }, (t) => {
274
+ const { dir, cleanup } = fixture();
275
+ t.after(cleanup);
276
+
277
+ const source = join(dir, "src.mp4");
278
+ execFileSync("ffmpeg", [
279
+ "-y",
280
+ "-hide_banner",
281
+ "-loglevel",
282
+ "error",
283
+ "-f",
284
+ "lavfi",
285
+ "-i",
286
+ "testsrc2=size=320x180:rate=30",
287
+ "-f",
288
+ "lavfi",
289
+ "-i",
290
+ `sine=frequency=440:sample_rate=${SAMPLE_RATE}`,
291
+ "-t",
292
+ "4",
293
+ "-c:v",
294
+ "libx264",
295
+ "-pix_fmt",
296
+ "yuv420p",
297
+ "-c:a",
298
+ "aac",
299
+ source,
300
+ ]);
301
+
302
+ const transcriptPath = join(dir, "transcript.json");
303
+ writeFileSync(transcriptPath, JSON.stringify([{ text: "a", start: 0.1, end: 3.9 }]));
304
+
305
+ const output = join(dir, "out.mp4");
306
+ const result = run([
307
+ "--input",
308
+ source,
309
+ "--transcript",
310
+ transcriptPath,
311
+ "--keep",
312
+ `0-1,${THIRTY_FPS_FRAME_32}-3`,
313
+ "--out",
314
+ output,
315
+ "--json",
316
+ ]);
317
+ assert.equal(result.status, 0, result.stderr || result.stdout);
318
+
319
+ // Frames 0-29 for 0-1s, then frames 32-89 for the second segment. Rounding
320
+ // -ss to the millisecond drops frame 32 and yields 87.
321
+ const frames = Number(
322
+ execFileSync(
323
+ "ffprobe",
324
+ [
325
+ "-v",
326
+ "error",
327
+ "-select_streams",
328
+ "v",
329
+ "-count_packets",
330
+ "-show_entries",
331
+ "stream=nb_read_packets",
332
+ "-of",
333
+ "csv=p=0",
334
+ "--",
335
+ output,
336
+ ],
337
+ { encoding: "utf8" },
338
+ ).trim(),
339
+ );
340
+ assert.equal(frames, 88, `expected 88 frames, got ${frames}`);
341
+ });
@@ -6,8 +6,8 @@ var __filename = __hf_fileURLToPath(import.meta.url);
6
6
  var __dirname = __hf_dirname(__filename);
7
7
  import {
8
8
  trackSkillsInstallSkipped
9
- } from "./chunk-3UAIVF4L.js";
10
- import "./chunk-6KWA3XPS.js";
9
+ } from "./chunk-H2SGM2V2.js";
10
+ import "./chunk-KZ3L3W4Q.js";
11
11
  import {
12
12
  diag
13
13
  } from "./chunk-VWG7OQKU.js";
@@ -35,15 +35,15 @@ import {
35
35
  isCoreSkill,
36
36
  presentSkills,
37
37
  pruneOrphanedLockEntries
38
- } from "./chunk-O4DC5CHF.js";
38
+ } from "./chunk-H7FXBK3Q.js";
39
39
  import {
40
40
  withMeta
41
- } from "./chunk-AFRXL75S.js";
41
+ } from "./chunk-YYEYOFHW.js";
42
42
  import "./chunk-K4WIOZFP.js";
43
43
  import "./chunk-XUXGIA7H.js";
44
44
  import "./chunk-WERLJGIC.js";
45
45
  import "./chunk-ISN5GRYI.js";
46
- import "./chunk-I6DATIHC.js";
46
+ import "./chunk-IZIRIDJH.js";
47
47
  import {
48
48
  buildNpxCommand
49
49
  } from "./chunk-6BEFEWFB.js";
@@ -9,13 +9,13 @@ import {
9
9
  printSkillsUpdateNotice,
10
10
  refreshSkillsCache,
11
11
  skillsCheckDue
12
- } from "./chunk-O4DC5CHF.js";
13
- import "./chunk-AFRXL75S.js";
12
+ } from "./chunk-H7FXBK3Q.js";
13
+ import "./chunk-YYEYOFHW.js";
14
14
  import "./chunk-K4WIOZFP.js";
15
15
  import "./chunk-XUXGIA7H.js";
16
16
  import "./chunk-WERLJGIC.js";
17
17
  import "./chunk-ISN5GRYI.js";
18
- import "./chunk-I6DATIHC.js";
18
+ import "./chunk-IZIRIDJH.js";
19
19
  import "./chunk-KNU5COPB.js";
20
20
  import "./chunk-M2WBOQ2Q.js";
21
21
  export {