hyperframes 0.8.144 → 0.8.146

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 (209) hide show
  1. package/README.md +3 -2
  2. package/dist/{add-CWRTVGDB.js → add-6J4TLUDW.js} +15 -15
  3. package/dist/{animatedGifPrep-JTOHIWI4.js → animatedGifPrep-TPYA45ZZ.js} +10 -10
  4. package/dist/{asset-KU2Z4UFM.js → asset-B4C6ZU4V.js} +2 -2
  5. package/dist/{assetCataloger-3CS2P7PA.js → assetCataloger-GX4B55VB.js} +4 -4
  6. package/dist/{auth-IDSOIQWG.js → auth-T2V3URGO.js} +2 -2
  7. package/dist/{autoUpdate-373B4BA2.js → autoUpdate-O4R7IOYP.js} +2 -2
  8. package/dist/{backgroundChecks-53Q3NAMG.js → backgroundChecks-SQHGZCSH.js} +3 -3
  9. package/dist/backgroundChecksWorker.js +3 -3
  10. package/dist/{batchRender-UKZA6BME.js → batchRender-T7S2CGJE.js} +5 -5
  11. package/dist/{beats-N5JGEOFR.js → beats-RNEXDM7U.js} +16 -16
  12. package/dist/{benchmark-CUN3LN4V.js → benchmark-IVK6FCEU.js} +12 -12
  13. package/dist/{browser-NCZT5AP6.js → browser-WUE6VK2N.js} +5 -5
  14. package/dist/{bundleWithLocalizedFonts-FWC7PE2D.js → bundleWithLocalizedFonts-EEIRL7FT.js} +2 -2
  15. package/dist/{capture-YLS32UHT.js → capture-K3EONGGU.js} +2 -2
  16. package/dist/{capture-QBLG3VJR.js → capture-LEKIGJ4G.js} +13 -13
  17. package/dist/{catalog-HIWNAFHW.js → catalog-IXYQ2KXW.js} +15 -15
  18. package/dist/{catch-up-HGS7KIAC.js → catch-up-IIF7HKR6.js} +6 -6
  19. package/dist/{check-FH6UYYLO.js → check-W7CDOXNP.js} +38 -38
  20. package/dist/{checkBrowser-RNWNOMCZ.js → checkBrowser-5VSXEHBZ.js} +118 -61
  21. package/dist/{chunk-XU5GGV6R.js → chunk-2NAUEDA6.js} +2 -2
  22. package/dist/{chunk-GZM2EFOE.js → chunk-3HSWLZ6S.js} +1 -1
  23. package/dist/{chunk-YUAXFWSQ.js → chunk-4RUILXS6.js} +5 -5
  24. package/dist/{chunk-MXNDKDFT.js → chunk-54UXJKFF.js} +1 -1
  25. package/dist/{chunk-DIRJ322I.js → chunk-66SAATRI.js} +3 -3
  26. package/dist/{chunk-RI53TS2Y.js → chunk-6TTAXMDG.js} +2 -2
  27. package/dist/{chunk-YJXHMIZA.js → chunk-A72OAGNB.js} +6 -6
  28. package/dist/{chunk-G75ENCEZ.js → chunk-AIJS74LG.js} +3 -3
  29. package/dist/{chunk-GEWKFRFI.js → chunk-AJ5R4GM6.js} +1 -1
  30. package/dist/{chunk-3VYNNWVA.js → chunk-BAKPN2D2.js} +101 -94
  31. package/dist/{chunk-3XTEXWNN.js → chunk-CVPWZ5LY.js} +3 -3
  32. package/dist/{chunk-AUBOF2PN.js → chunk-DDNS2674.js} +1 -1
  33. package/dist/{chunk-QFSCCQIR.js → chunk-DMKI64EZ.js} +1 -1
  34. package/dist/{chunk-NOPBBSLA.js → chunk-DUU77E2S.js} +69 -64
  35. package/dist/{chunk-RVFEDAUV.js → chunk-EGO6Z24R.js} +3 -3
  36. package/dist/{chunk-KC5XXX3X.js → chunk-EOOEC32M.js} +1 -1
  37. package/dist/{chunk-2DA2BBNY.js → chunk-ETNQF37F.js} +10 -10
  38. package/dist/{chunk-54QISXOC.js → chunk-FDL4HV44.js} +2 -2
  39. package/dist/{chunk-VYSUZEJG.js → chunk-FJLHVAPO.js} +6 -6
  40. package/dist/{chunk-QZ3AZTXI.js → chunk-FJWEYVEM.js} +42 -16
  41. package/dist/{chunk-EX6C672Y.js → chunk-FKLYMV3U.js} +1 -1
  42. package/dist/{chunk-PUGIRFPX.js → chunk-FMNVTKX2.js} +4 -4
  43. package/dist/{chunk-L5TUCXWR.js → chunk-HBGJYBGE.js} +2 -2
  44. package/dist/{chunk-SLCFKB3Y.js → chunk-HTM4CIFS.js} +11 -8
  45. package/dist/{chunk-T42U2W2R.js → chunk-IXLUTE6N.js} +1 -1
  46. package/dist/{chunk-OUSLVRO4.js → chunk-JBQJKC23.js} +76 -6
  47. package/dist/{chunk-IEIJRMLZ.js → chunk-JDB65PPT.js} +62 -63
  48. package/dist/{chunk-USDEMXXL.js → chunk-JYVGYKXG.js} +1 -1
  49. package/dist/{chunk-P6WH6QTV.js → chunk-K7ES5RSK.js} +1 -1
  50. package/dist/{chunk-LC3RQURJ.js → chunk-KFZUUXUN.js} +3 -3
  51. package/dist/{chunk-OIDN7G42.js → chunk-KG265HQ7.js} +2 -2
  52. package/dist/{chunk-7RAYSCYT.js → chunk-KPJMBC4K.js} +2 -2
  53. package/dist/{chunk-MAYCCGMQ.js → chunk-KVGDFUVO.js} +1 -1
  54. package/dist/{chunk-JEHFZQYT.js → chunk-LE7T4ML7.js} +1 -1
  55. package/dist/{chunk-MQZVBHEY.js → chunk-LFNQDTSH.js} +1 -1
  56. package/dist/{chunk-ZOA2SC4P.js → chunk-MOL32UKR.js} +45 -11
  57. package/dist/{chunk-RG3QWWIE.js → chunk-QSSPIATO.js} +2 -2
  58. package/dist/{chunk-F2SMS24Q.js → chunk-R6R74HNZ.js} +117 -139
  59. package/dist/{chunk-7SDCIWHQ.js → chunk-SYMSKGXI.js} +2 -2
  60. package/dist/{chunk-ZEUONDQG.js → chunk-SYUGVPYH.js} +1 -1
  61. package/dist/{chunk-4OC7MQNB.js → chunk-TBOMPYG6.js} +2 -2
  62. package/dist/{chunk-KFKZ3ISO.js → chunk-TH4OXAWY.js} +1 -1
  63. package/dist/{chunk-4LAR447G.js → chunk-UOBYRLDD.js} +1 -1
  64. package/dist/{chunk-NV4RXIQ7.js → chunk-UZUUVYAO.js} +3 -3
  65. package/dist/{chunk-PJ3HDUCY.js → chunk-VAS6HQX3.js} +39 -9
  66. package/dist/{chunk-LFX2HEYP.js → chunk-VT563F4O.js} +7 -7
  67. package/dist/{chunk-5VMR4WBS.js → chunk-WU2B74TL.js} +363 -103
  68. package/dist/{chunk-5HJ343GH.js → chunk-WUZ6TMAO.js} +1 -1
  69. package/dist/{chunk-GQJZORQP.js → chunk-XBSPQ52S.js} +1 -1
  70. package/dist/{chunk-5WADG7PR.js → chunk-XJUZEB44.js} +1 -1
  71. package/dist/{chunk-I5KY7EZP.js → chunk-XVIYZU3D.js} +1 -1
  72. package/dist/{chunk-XGFHYAYN.js → chunk-Z5C4LEAD.js} +33 -28
  73. package/dist/chunk-Z6FLHIDV.js +3795 -0
  74. package/dist/{chunk-DDIVWXN7.js → chunk-ZFN6GQU4.js} +4 -4
  75. package/dist/{chunk-Y6UPYWJS.js → chunk-ZVZSFDW3.js} +2 -2
  76. package/dist/{chunk-A53CBD24.js → chunk-ZZ7H4IJ6.js} +9 -9
  77. package/dist/{clean-KPX27IKJ.js → clean-2GDD2B4F.js} +23 -23
  78. package/dist/cli.js +52 -52
  79. package/dist/{cloud-P7DAOISN.js → cloud-LUKGDTTE.js} +4 -4
  80. package/dist/{cloudrun-XM56TBOH.js → cloudrun-NQF5KFQI.js} +6 -6
  81. package/dist/{compare-F6YAFZZM.js → compare-4P6O4ONG.js} +32 -32
  82. package/dist/{compiler-JZ4GNFMG.js → compiler-ACCPOZMQ.js} +10 -6
  83. package/dist/{component-3CFOGIQA.js → component-SDDOLNJD.js} +3 -3
  84. package/dist/{compositions-CPCMWF24.js → compositions-P2L6HUVQ.js} +15 -15
  85. package/dist/{delete-GYVKOKB6.js → delete-Q6S4BJ3F.js} +2 -2
  86. package/dist/{deterministicFonts-U2BIMGPG.js → deterministicFonts-C5JQLCTS.js} +2 -2
  87. package/dist/{dist-YPWQDWJ4.js → dist-GPDOV3IQ.js} +4 -4
  88. package/dist/{dist-KDJTFJHA.js → dist-HUMFCJ6T.js} +3 -3
  89. package/dist/{doctor-GWI3B6JT.js → doctor-NL73UKHV.js} +15 -15
  90. package/dist/{events-BRRUDPPT.js → events-AVOPOKO3.js} +5 -5
  91. package/dist/{events-V2QFN4YM.js → events-X7KPU7LG.js} +4 -4
  92. package/dist/{feedback-CXMD7FWL.js → feedback-GJZ5NXSH.js} +10 -10
  93. package/dist/{ffmpeg-T5SH2EFJ.js → ffmpeg-5V4FP3R6.js} +2 -2
  94. package/dist/{figma-YABDWD66.js → figma-V7SVQQI5.js} +3 -3
  95. package/dist/fontLocalizeCli.js +14 -14
  96. package/dist/{get-L2SOQKHX.js → get-FO3YZYEU.js} +2 -2
  97. package/dist/{gpuPolicy-NBJNWHYS.js → gpuPolicy-QGS4TFWP.js} +10 -10
  98. package/dist/{grade-compare-STXQPKYQ.js → grade-compare-7ICSN7KE.js} +33 -33
  99. package/dist/{help-4H6BSFLB.js → help-Y5LYZG7K.js} +2 -2
  100. package/dist/{history-4IQOLTHJ.js → history-RKGBRNLD.js} +49 -22
  101. package/dist/hyperframe-runtime.js +214 -68
  102. package/dist/hyperframe.manifest.json +1 -1
  103. package/dist/hyperframe.runtime.iife.js +214 -68
  104. package/dist/hyperframes-player.global.js +4 -5
  105. package/dist/hyperframes-slideshow.global.js +3 -4
  106. package/dist/{info-2CZOTNBC.js → info-GGUTL2NR.js} +11 -11
  107. package/dist/{init-6BXK42VS.js → init-PG7NA5SQ.js} +31 -31
  108. package/dist/{inspect-VKU47TBH.js → inspect-FCXUB6DE.js} +33 -33
  109. package/dist/{installer-T2BSSUDY.js → installer-LZ4X5TZZ.js} +6 -6
  110. package/dist/{keyframes-IEYVHRPI.js → keyframes-R3K6WJ3N.js} +8 -8
  111. package/dist/{lambda-TIKG25CK.js → lambda-3GAKHMC3.js} +7 -7
  112. package/dist/{layout-XOI4PT7A.js → layout-XNIZIGA2.js} +33 -33
  113. package/dist/{lint-DWCMVIX5.js → lint-4KDS2F4O.js} +11 -11
  114. package/dist/{lintProject-UBZ3TDHU.js → lintProject-SSEXWCH6.js} +3 -3
  115. package/dist/{list-7GXKG5QM.js → list-OVPURFPK.js} +2 -2
  116. package/dist/{login-BQ5L2UHX.js → login-EE2AX2PE.js} +4 -4
  117. package/dist/{manager-YKMHR6LN.js → manager-5ORDVWJB.js} +3 -3
  118. package/dist/{media-treatment-IFB7OSZ3.js → media-treatment-UDUEOP67.js} +86 -55
  119. package/dist/{models-TY4PJ545.js → models-LPK2O4B2.js} +14 -14
  120. package/dist/{motionShot-WXMFHICW.js → motionShot-G76XBOYI.js} +35 -29
  121. package/dist/{normalize-audio-RFD776HQ.js → normalize-audio-TUKWZ3OR.js} +10 -10
  122. package/dist/{open-7IXXGO2E.js → open-7XB2TGVB.js} +6 -6
  123. package/dist/{parakeetRunner-IHLYYMTZ.js → parakeetRunner-DL556HXL.js} +14 -14
  124. package/dist/{pipeline-TXKPLL2U.js → pipeline-34N5DLWU.js} +11 -11
  125. package/dist/{play-J2KP64UK.js → play-DDA2UUQF.js} +26 -26
  126. package/dist/{preflight-B4AFK3RG.js → preflight-ONF5AJNO.js} +5 -5
  127. package/dist/{present-MHHNRVRA.js → present-LLNRKXF5.js} +23 -23
  128. package/dist/{preview-CONUOFFQ.js → preview-H3ZZS46F.js} +57 -26
  129. package/dist/{producer-ZI46K7HH.js → producer-IFQOZK2G.js} +1 -1
  130. package/dist/{projectConfig-FGGVG5MV.js → projectConfig-SBRMQZGG.js} +8 -8
  131. package/dist/{publication-TXFBMW6B.js → publication-5BQICRZO.js} +4 -4
  132. package/dist/{publish-YZSPBFRD.js → publish-XDUZA3E3.js} +27 -26
  133. package/dist/{remove-background-4ATVLHLG.js → remove-background-GNKRMFQS.js} +12 -12
  134. package/dist/{render-MH4IUPWB.js → render-JM6KNRIA.js} +27 -27
  135. package/dist/{render-6FBYLYGJ.js → render-KTTC3EE5.js} +12 -12
  136. package/dist/{render-GFW5BPES.js → render-PCFO45RK.js} +5 -5
  137. package/dist/{render-batch-XUCZ2PUJ.js → render-batch-7R2NRNVJ.js} +5 -5
  138. package/dist/{renderCancellation-XF7M7IUP.js → renderCancellation-GP3XD2FS.js} +10 -10
  139. package/dist/renderSetupWorker.js +3 -3
  140. package/dist/{resolver-W7AYCQGS.js → resolver-C4FPX72I.js} +6 -6
  141. package/dist/{sherpa-LDUB5NLY.js → sherpa-MZP3MV2X.js} +14 -14
  142. package/dist/skills/hyperframes/scripts/lib/frame-packets-core.mjs +2 -20
  143. package/dist/skills/hyperframes/scripts/lib/main-module.mjs +13 -0
  144. package/dist/skills/hyperframes/scripts/lib/main-module.test.mjs +51 -0
  145. package/dist/skills/hyperframes/scripts/plugin-cli.mjs +4 -3
  146. package/dist/skills/hyperframes/scripts/plugin-cli.test.mjs +5 -1
  147. package/dist/skills/hyperframes-cli/SKILL.md +2 -0
  148. package/dist/skills/media-use/audio/scripts/heygen-voice.mjs +2 -2
  149. package/dist/skills/media-use/audio/scripts/lib/main-module.mjs +13 -0
  150. package/dist/skills/media-use/references/media-treatments.md +5 -1
  151. package/dist/skills/media-use/scripts/lib/cube-validate.mjs +2 -3
  152. package/dist/skills/media-use/scripts/lib/main-module.mjs +13 -0
  153. package/dist/{skills-VENERS5X.js → skills-OET4XSHR.js} +7 -7
  154. package/dist/{skillsUpdateCheck-UXFYTACG.js → skillsUpdateCheck-IPFSKKJD.js} +3 -3
  155. package/dist/{slideshow-4OJEPWNZ.js → slideshow-YTWATJRT.js} +1 -1
  156. package/dist/{snapshot-C6T74HTI.js → snapshot-EC4N75MS.js} +34 -34
  157. package/dist/{src-VUJ2FUFK.js → src-LIERUCFW.js} +13 -9
  158. package/dist/{src-FS5RSVEI.js → src-ZZQJOKO6.js} +13 -13
  159. package/dist/{staticProjectServer-V2RZ4DWD.js → staticProjectServer-YH2RHYKW.js} +20 -20
  160. package/dist/{status-LXPGSSKY.js → status-PTKNQD2E.js} +2 -2
  161. package/dist/studio/assets/{hyperframes-player-Bw_jIaSX.js → hyperframes-player-tkxyWf5H.js} +3 -3
  162. package/dist/studio/assets/{index-Cj7W_c7p.js → index-BrdqOkqP.js} +1 -1
  163. package/dist/studio/assets/{index-BVM9NuWl.js → index-Cg48BxAo.js} +145 -145
  164. package/dist/studio/assets/{index-DyH9dT7b.js → index-Dllxg4aU.js} +1 -1
  165. package/dist/studio/index.html +1 -1
  166. package/dist/{studio-api-TQE7KWKO.js → studio-api-T2TS2CHL.js} +16 -14
  167. package/dist/{studioServer-CYKSQK7L.js → studioServer-GJVVJUEL.js} +54 -52
  168. package/dist/{system-OQLOGGPJ.js → system-OXRG57F2.js} +2 -2
  169. package/dist/{telemetry-A23PPRD6.js → telemetry-ZQ5XVE6Z.js} +5 -5
  170. package/dist/{timeline-T3QSGT3A.js → timeline-I2T5WIPD.js} +34 -25
  171. package/dist/{tokens-RT5HWA6C.js → tokens-ONUWOXEX.js} +2 -2
  172. package/dist/{transcribe-FOLNOC5G.js → transcribe-BCQMA537.js} +12 -12
  173. package/dist/{transcribe-HCF64LQK.js → transcribe-ZLJF7V5Y.js} +13 -13
  174. package/dist/{updateCheck-27PIP3AA.js → updateCheck-ZWHDXF73.js} +2 -2
  175. package/dist/{upgrade-TVGXBUPJ.js → upgrade-32JGFZWS.js} +2 -2
  176. package/dist/{urlDownloader-KBLBJKRL.js → urlDownloader-EVDP43GM.js} +9 -9
  177. package/dist/{validate-ZMYGK4QI.js → validate-G4PUGQCI.js} +27 -27
  178. package/dist/{video-AVE5GIGM.js → video-TILEY3VJ.js} +10 -10
  179. package/package.json +3 -2
  180. package/dist/chunk-WJB7WI4J.js +0 -3753
  181. package/dist/studio/chunk-3GHTHPZD.js +0 -1105
  182. package/dist/studio/chunk-3GHTHPZD.js.map +0 -1
  183. package/dist/studio/chunk-EEXSYY5F.js +0 -190
  184. package/dist/studio/chunk-EEXSYY5F.js.map +0 -1
  185. package/dist/studio/chunk-G3EDKRZH.js +0 -1442
  186. package/dist/studio/chunk-G3EDKRZH.js.map +0 -1
  187. package/dist/studio/chunk-PEAKCOUW.js +0 -942
  188. package/dist/studio/chunk-PEAKCOUW.js.map +0 -1
  189. package/dist/studio/chunk-PZ5AY32C.js +0 -10
  190. package/dist/studio/chunk-PZ5AY32C.js.map +0 -1
  191. package/dist/studio/chunk-Y2OHHNM2.js +0 -970
  192. package/dist/studio/chunk-Y2OHHNM2.js.map +0 -1
  193. package/dist/studio/domEditingLayers-PYST4G45.js +0 -47
  194. package/dist/studio/domEditingLayers-PYST4G45.js.map +0 -1
  195. package/dist/studio/index.css +0 -165
  196. package/dist/studio/index.css.map +0 -1
  197. package/dist/studio/index.d.ts +0 -4315
  198. package/dist/studio/index.js +0 -100971
  199. package/dist/studio/index.js.map +0 -1
  200. package/dist/studio/player.d.ts +0 -30
  201. package/dist/studio/player.js +0 -10
  202. package/dist/studio/player.js.map +0 -1
  203. package/dist/studio/styles/tailwind-preset.d.ts +0 -39
  204. package/dist/studio/styles/tailwind-preset.js +0 -50
  205. package/dist/studio/styles/tailwind-preset.js.map +0 -1
  206. package/dist/studio/styles.css +0 -1
  207. package/dist/studio/ui.d.ts +0 -297
  208. package/dist/studio/ui.js +0 -66
  209. package/dist/studio/ui.js.map +0 -1
@@ -28,7 +28,7 @@ import {
28
28
  } from "./chunk-APDLWWFG.js";
29
29
  import {
30
30
  findFfBinary
31
- } from "./chunk-PJ3HDUCY.js";
31
+ } from "./chunk-VAS6HQX3.js";
32
32
  import {
33
33
  COMPOSITION_ATTRIBUTES,
34
34
  checkSubCompositionUsability,
@@ -54,7 +54,7 @@ import {
54
54
  isSceneLikeCompositionId,
55
55
  parseSlideshowManifest,
56
56
  resolveSlideshow
57
- } from "./chunk-T42U2W2R.js";
57
+ } from "./chunk-IXLUTE6N.js";
58
58
  import {
59
59
  __commonJS,
60
60
  __require,
@@ -4362,7 +4362,10 @@ function truncateSnippet(value, maxLength = 220) {
4362
4362
  return `${normalized2.slice(0, maxLength - 3)}...`;
4363
4363
  }
4364
4364
  function mediaSrcTagRe(tagAlternation) {
4365
- return new RegExp(`<(${tagAlternation})\\b[^>]*\\ssrc\\s*=\\s*["']([^"']+)["'][^>]*>`, "gi");
4365
+ return new RegExp(`<(${tagAlternation})\\b[^>]*\\ssrc\\s*=\\s*("[^"]*"|'[^']*')[^>]*>`, "gi");
4366
+ }
4367
+ function mediaSrcOf(match) {
4368
+ return readDecodedAttr(match[0], "src") ?? "";
4366
4369
  }
4367
4370
  function normalized(source) {
4368
4371
  const chars = [];
@@ -4828,7 +4831,7 @@ function describeRootDimensionsDrift(source, dataWidth, dataHeight) {
4828
4831
  const declared = `Root composition declares data-width="${dataWidth}" data-height="${dataHeight}"`;
4829
4832
  if (!bodyCssMismatch) {
4830
4833
  return {
4831
- message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture \u2014 the renderer sizes the viewport from the root's own data-width/data-height \u2014 so this is stale metadata, not a clipping risk.`,
4834
+ message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture (the renderer sizes the viewport from the root's own data-width/data-height), so this is stale metadata, not a clipping risk.`,
4832
4835
  fixHint: "update the meta viewport to match, or scaffold with `hyperframes init --resolution portrait`"
4833
4836
  };
4834
4837
  }
@@ -5100,7 +5103,7 @@ var coreRules = [
5100
5103
  findings.push({
5101
5104
  code: "unclosed_tag_swallowed_element",
5102
5105
  severity: "error",
5103
- message: `<${tag.name}> is missing its closing \`>\` before the next \`<\` \u2014 the following element is swallowed as bogus attribute text and never becomes a real node.`,
5106
+ message: `<${tag.name}> is missing its closing \`>\` before the next \`<\`, so the following element is swallowed as bogus attribute text and never becomes a real node.`,
5104
5107
  fixHint: "Close the previous tag's `>` before opening the next element.",
5105
5108
  snippet: truncateSnippet(tag.raw)
5106
5109
  });
@@ -5806,7 +5809,7 @@ var mediaRules = [
5806
5809
  severity: "error",
5807
5810
  message: `<video${elementId ? ` id="${elementId}"` : ""}> has data-start but declares neither muted nor data-has-audio. If the file has sound, add data-has-audio="true" (the sound stays on this clip). If it is silent, add muted.`,
5808
5811
  elementId,
5809
- fixHint: 'Add data-has-audio="true" when the file has sound (recommended \u2014 keeps picture and sound on one clip), or add `muted` for silent footage.',
5812
+ fixHint: 'Add data-has-audio="true" when the file has sound (recommended: it keeps picture and sound on one clip), or add `muted` for silent footage.',
5810
5813
  snippet: truncateSnippet(tag.raw)
5811
5814
  });
5812
5815
  }
@@ -5891,7 +5894,7 @@ var mediaRules = [
5891
5894
  severity: "error",
5892
5895
  message: `Self-closing <${tagName}/> is invalid HTML. The browser will leave the tag open, swallowing all subsequent elements as invisible fallback content. This makes compositions INVISIBLE.`,
5893
5896
  elementId,
5894
- fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}> \u2014 media elements MUST have explicit closing tags.`,
5897
+ fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}>. Media elements MUST have explicit closing tags.`,
5895
5898
  snippet: truncateSnippet(scMatch[0])
5896
5899
  });
5897
5900
  }
@@ -5934,7 +5937,7 @@ var mediaRules = [
5934
5937
  findings.push({
5935
5938
  code: "base64_media_prohibited",
5936
5939
  severity: "error",
5937
- message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? " \u2014 likely fabricated data" : ""}. Base64 media is prohibited \u2014 it bloats file size and breaks rendering.`,
5940
+ message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? ", likely fabricated data" : ""}. Base64 media is prohibited: it bloats file size and breaks rendering.`,
5938
5941
  fixHint: "Use a relative path (assets/music.mp3) or HTTPS URL for the audio/video src. Never embed media as base64.",
5939
5942
  snippet: truncateSnippet((b64Match[1] ?? "").slice(0, 80) + "...")
5940
5943
  });
@@ -5963,7 +5966,7 @@ var mediaRules = [
5963
5966
  findings.push({
5964
5967
  code: "media_missing_id",
5965
5968
  severity: "error",
5966
- message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements \u2014 this ${tag.name === "audio" ? "audio will be SILENT" : "video will be FROZEN"} in renders.`,
5969
+ message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements, so this ${tag.name === "audio" ? "audio will be SILENT" : "video will be FROZEN"} in renders.`,
5967
5970
  fixHint: `Add a unique id attribute: <${tag.name} id="my-${tag.name}" ...>`,
5968
5971
  snippet: truncateSnippet(tag.raw)
5969
5972
  });
@@ -6020,7 +6023,7 @@ var mediaRules = [
6020
6023
  findings.push({
6021
6024
  code: "media_crossorigin_breaks_preview",
6022
6025
  severity: "error",
6023
- message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has crossorigin, which forces a CORS-checked fetch. If the media host omits Access-Control-Allow-Origin, the load silently fails in Studio preview (media shows BLANK/black) while server-side renders still work \u2014 hiding the bug.`,
6026
+ message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has crossorigin, which forces a CORS-checked fetch. If the media host omits Access-Control-Allow-Origin, the load silently fails in Studio preview (media shows BLANK/black) while server-side renders still work, which hides the bug.`,
6024
6027
  elementId,
6025
6028
  fixHint: "Remove the crossorigin attribute unless you read the media back via canvas/WebGL/WebAudio AND the host is known to send CORS headers. Plain displayed media never needs it.",
6026
6029
  snippet: truncateSnippet(tag.raw)
@@ -6054,7 +6057,7 @@ var mediaRules = [
6054
6057
  findings.push({
6055
6058
  code: "video_audio_double_source",
6056
6059
  severity: "error",
6057
- message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio \u2014 the duplicate <audio> will cause double playback and echo.`,
6060
+ message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio, so the duplicate <audio> will cause double playback and echo.`,
6058
6061
  elementId: audioInfo.id,
6059
6062
  fixHint: "Remove the <audio> element and let the video carry its own sound (recommended), or mute the video (add `muted`) and keep the separate <audio> when picture and sound must be cut independently.",
6060
6063
  snippet: truncateSnippet(audioInfo.raw)
@@ -6090,7 +6093,7 @@ function findVolumeTweenOverridesGainFindings(ctx) {
6090
6093
  findings.push({
6091
6094
  code: "audio_volume_tween_overrides_gain",
6092
6095
  severity: "warning",
6093
- message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute \u2014 they REPLACE this gain rather than scale it \u2014 so wherever the tween names a value the clip plays at that value, not at ${db}.`,
6096
+ message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute: they REPLACE this gain rather than scale it, so wherever the tween names a value the clip plays at that value, not at ${db}.`,
6094
6097
  elementId: id,
6095
6098
  fixHint: "Write the tween's targets in the same absolute gain (e.g. `volume: 1.95`, not `volume: 1`), or reset data-volume to 1 and let the tween carry the level on its own.",
6096
6099
  snippet: truncateSnippet(tag.raw)
@@ -6109,7 +6112,7 @@ function findVolumeDoubleAutomationFindings(ctx) {
6109
6112
  findings.push({
6110
6113
  code: "audio_volume_double_automation",
6111
6114
  severity: "warning",
6112
- message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins \u2014 the tween is ignored in preview and in the render.`,
6115
+ message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins, and the tween is ignored in preview and in the render.`,
6113
6116
  elementId: id,
6114
6117
  fixHint: "Keep one of them: delete the volume lane to go back to tweening, or drop the tween and shape the level in the automation lane.",
6115
6118
  snippet: truncateSnippet(tag.raw)
@@ -6157,7 +6160,7 @@ function findCarveUngroupedSourcesFindings(ctx) {
6157
6160
  severity: "warning",
6158
6161
  message: `${elementId ? `#${elementId}'s` : "This"} carve names ${clipIds.length} voice clips directly (${clipIds.join(", ")}) instead of a group.`,
6159
6162
  elementId,
6160
- fixHint: "Group the voice clips and carve against the group \u2014 a hand-rolled clip list silently rots when a clip is added.",
6163
+ fixHint: "Group the voice clips and carve against the group. A hand-rolled clip list silently rots when a clip is added.",
6161
6164
  snippet: truncateSnippet(tag.raw)
6162
6165
  });
6163
6166
  }
@@ -6206,7 +6209,7 @@ function findAudioGroupTimingAttrFindings(ctx) {
6206
6209
  findings.push({
6207
6210
  code: "audio_group_timing_attrs",
6208
6211
  severity: "warning",
6209
- message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for \u2014 its members carry the timing and its automation clock is composition time.`,
6212
+ message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for: its members carry the timing and its automation clock is composition time.`,
6210
6213
  elementId,
6211
6214
  fixHint: `Remove ${present.map((attr) => `\`${attr}\``).join(", ")} from the group element.`,
6212
6215
  snippet: truncateSnippet(tag.raw)
@@ -6223,7 +6226,7 @@ function findAudioGroupCarveAttrFindings(ctx) {
6223
6226
  findings.push({
6224
6227
  code: "audio_group_carve_attr",
6225
6228
  severity: "warning",
6226
- message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved \u2014 a bus has no audio of its own to level-match against, and a carve here stacks with any its members already have.`,
6229
+ message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved. A bus has no audio of its own to level-match against, and a carve here stacks with any its members already have.`,
6227
6230
  elementId,
6228
6231
  fixHint: "Remove `data-fx-carve` and the `fromCarve` nodes it wrote into this bus's `data-fx-chain`, and carve the bed clip instead.",
6229
6232
  snippet: truncateSnippet(tag.raw)
@@ -6888,7 +6891,7 @@ var compositionRules = [
6888
6891
  findings.push({
6889
6892
  code: "root_composition_missing_html_wrapper",
6890
6893
  severity: "error",
6891
- message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment \u2014 browsers quirks-mode it, the preview server cannot load it, and the bundler will fail to inject runtime scripts.",
6894
+ message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment: browsers quirks-mode it, the preview server cannot load it, and the bundler will fail to inject runtime scripts.",
6892
6895
  fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
6893
6896
  snippet: rootTag ? truncateSnippet(rootTag.raw) : void 0
6894
6897
  });
@@ -7043,7 +7046,7 @@ var compositionRules = [
7043
7046
  findings.push({
7044
7047
  code: "unknown_variable_binding",
7045
7048
  severity: "warning",
7046
- message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables \u2014 the binding will silently keep the authored fallback.`,
7049
+ message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables, so the binding will silently keep the authored fallback.`,
7047
7050
  fixHint: `Declare the variable on the composition root (<html>, or the [data-composition-id] root element for a template/fragment comp): data-composition-variables='[{"id":"${id}","type":"${attr === "data-var-src" ? "image" : "string"}","label":"${id}","default":"..."}]', or fix the binding id.`,
7048
7051
  elementId: readAttr(tag.raw, "id") || void 0,
7049
7052
  snippet: truncateSnippet(tag.raw)
@@ -7084,8 +7087,8 @@ var compositionRules = [
7084
7087
  {
7085
7088
  code: "html_dir_attribute_breaks_render",
7086
7089
  severity: "error",
7087
- message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render \u2014 a confirmed, silent failure.`,
7088
- fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead \u2014 text still shapes correctly via the browser's own bidi algorithm.`,
7090
+ message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render. This is a confirmed, silent failure.`,
7091
+ fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead. Text still shapes correctly via the browser's own bidi algorithm.`,
7089
7092
  snippet: truncateSnippet(htmlTag.raw)
7090
7093
  }
7091
7094
  ];
@@ -7123,7 +7126,7 @@ var compositionRules = [
7123
7126
  findings.push({
7124
7127
  code: "subcomposition_blanks_before_host",
7125
7128
  severity: "warning",
7126
- message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s \u2014 its slot will be blank for ~${gap}s.`,
7129
+ message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s, so its slot will be blank for ~${gap}s.`,
7127
7130
  elementId,
7128
7131
  fixHint: `data-duration is the slot's visible window. Set this sub-composition's data-duration to ${round3(rootDuration - t.start)} to fill the host window, or add another clip to cover the remaining ~${gap}s.`,
7129
7132
  snippet: truncateSnippet(t.tag.raw)
@@ -7162,7 +7165,7 @@ var compositionRules = [
7162
7165
  severity: "error",
7163
7166
  message: `Root element has class="${rootClasses.join(" ")}" and is styled by ${offenders.length} rule(s) keyed off that class (e.g. ${example}). At render, every sub-composition rule is scoped to [data-composition-id="${rootCompositionId}"] <selector>, so a selector whose leftmost part is the ROOT's own class becomes a descendant selector that cannot match the scoped element itself. Since #1886 the producer preserves the authored root as an inner wrapper, so this no longer renders the scene unstyled, but #root is the shape the scoper special-cases and the registry blocks model. Use it so preview, render, and Studio agree.`,
7164
7167
  selector: example,
7165
- fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`) \u2014 the runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant.`,
7168
+ fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`). The runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant.`,
7166
7169
  snippet: truncateSnippet(rootTag.raw)
7167
7170
  }
7168
7171
  ];
@@ -7245,7 +7248,7 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
7245
7248
  {
7246
7249
  code: "root_composition_missing_duration_source",
7247
7250
  severity: "error",
7248
- message: `Root composition uses Three.js with no data-duration. The runtime cannot discover a Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection) \u2014 render will fail with "Composition has zero duration".`,
7251
+ message: `Root composition uses Three.js with no data-duration. The runtime cannot discover a Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection), so render will fail with "Composition has zero duration".`,
7249
7252
  fixHint: 'Add data-duration="<seconds>" to the root element.',
7250
7253
  snippet: truncateSnippet(rootTag.raw)
7251
7254
  }
@@ -7308,8 +7311,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
7308
7311
  {
7309
7312
  code: "composition_heavy_overlay_count_high",
7310
7313
  severity: "warning",
7311
- message: `This composition has ${heavyCount} elements carrying "heavy overlay" CSS (filter:blur, radial-gradient, or clip-path). Field signal: a composition with ~40 such elements \u2014 including opacity:0 / visibility:hidden ones \u2014 captures solid-black for the first ~half of the render, recovering near the end. Reproduces identically via drawElement, forced screenshot capture, and snapshot, so the capture layer itself is the offender (not encoder/mux). Independent of duration. Presence alone matters; only display:none elements are excluded here.`,
7312
- fixHint: `${splitTarget} and concat the pieces (FFmpeg or the runtime's slideshow) so each capture only sees a small subset of heavy overlays at once. Even hidden overlays (opacity:0 / visibility:hidden) contribute \u2014 either remove truly unused ones from the source or scope them into their own per-transition sub-composition. If an overlay is genuinely inert for the whole clip, use display:none so it never enters the render tree. Field ref ts=1784040753 (#hyperframes-cli-feedback).`
7314
+ message: `This composition has ${heavyCount} elements carrying "heavy overlay" CSS (filter:blur, radial-gradient, or clip-path). Field signal: a composition with ~40 such elements (including opacity:0 / visibility:hidden ones) captures solid-black for the first ~half of the render, recovering near the end. Reproduces identically via drawElement, forced screenshot capture, and snapshot, so the capture layer itself is the offender (not encoder/mux). Independent of duration. Presence alone matters; only display:none elements are excluded here.`,
7315
+ fixHint: `${splitTarget} and concat the pieces (FFmpeg or the runtime's slideshow) so each capture only sees a small subset of heavy overlays at once. Even hidden overlays (opacity:0 / visibility:hidden) contribute: either remove truly unused ones from the source or scope them into their own per-transition sub-composition. If an overlay is genuinely inert for the whole clip, use display:none so it never enters the render tree. Field ref ts=1784040753 (#hyperframes-cli-feedback).`
7313
7316
  }
7314
7317
  ];
7315
7318
  },
@@ -7352,8 +7355,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
7352
7355
  {
7353
7356
  code: "root_zoom_rescales_a_fixed_canvas",
7354
7357
  severity: "error",
7355
- message: `\`zoom: ${hit.value}\` on ${hit.where} rescales the painted composition inside a frame that does not rescale with it. The capture frame is sized from the root's data-width/data-height in LAYOUT pixels, and \`zoom\` changes only what is painted inside it, so ` + (enlarging ? `content past the frame's edge renders zero pixels \u2014 it is inside the declared composition and absent from the output, with nothing else reporting it.` : `the composition paints into part of the frame and the remainder of every output frame is dead space.`),
7356
- fixHint: `Author the composition at its real size \u2014 set data-width/data-height (and the root's width/height) to the dimensions you want \u2014 and drop the canvas-level \`zoom\`. To scale the OUTPUT without changing layout, pass \`--output-resolution\` to render, which supersamples. \`zoom\` on descendants is fine and is not flagged: it is honoured exactly as specified.`,
7358
+ message: `\`zoom: ${hit.value}\` on ${hit.where} rescales the painted composition inside a frame that does not rescale with it. The capture frame is sized from the root's data-width/data-height in LAYOUT pixels, and \`zoom\` changes only what is painted inside it, so ` + (enlarging ? `content past the frame's edge renders zero pixels. It is inside the declared composition and absent from the output, with nothing else reporting it.` : `the composition paints into part of the frame and the remainder of every output frame is dead space.`),
7359
+ fixHint: `Author the composition at its real size: set data-width/data-height (and the root's width/height) to the dimensions you want, and drop the canvas-level \`zoom\`. To scale the OUTPUT without changing layout, pass \`--output-resolution\` to render, which supersamples. \`zoom\` on descendants is fine and is not flagged: it is honoured exactly as specified.`,
7357
7360
  snippet: hit.snippet
7358
7361
  }
7359
7362
  ];
@@ -7397,8 +7400,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
7397
7400
  {
7398
7401
  code: "composition_exceeds_inspection_viewport_cap",
7399
7402
  severity: "warning",
7400
- message: `${over.join(" and ")} exceeds the ${INSPECTION_VIEWPORT_CAP}px viewport cap that check, validate, snapshot, compare and layout clamp to. render is unaffected and produces the full size, so the video is correct \u2014 but every inspection command captures a ${INSPECTION_VIEWPORT_CAP}px-wide frame, and anything beyond that is missing from what they report. Measured at 0.8.71: a 5000x400 composition renders 5000x400 with all content, while snapshot returns 4096x400 with the element at left:4500 absent.`,
7401
- fixHint: `Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare \u2014 a clean inspection result does not cover it. If the large canvas is only there to gain resolution, author at the layout size and pass \`--output-resolution\` to render instead, which supersamples without changing layout.`,
7403
+ message: `${over.join(" and ")} exceeds the ${INSPECTION_VIEWPORT_CAP}px viewport cap that check, validate, snapshot, compare and layout clamp to. render is unaffected and produces the full size, so the video is correct, but every inspection command captures a ${INSPECTION_VIEWPORT_CAP}px-wide frame, and anything beyond that is missing from what they report. Measured at 0.8.71: a 5000x400 composition renders 5000x400 with all content, while snapshot returns 4096x400 with the element at left:4500 absent.`,
7404
+ fixHint: `Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare. A clean inspection result does not cover it. If the large canvas is only there to gain resolution, author at the layout size and pass \`--output-resolution\` to render instead, which supersamples without changing layout.`,
7402
7405
  snippet: truncateSnippet(rootTag.raw)
7403
7406
  }
7404
7407
  ];
@@ -8275,7 +8278,7 @@ ${right.raw}`)
8275
8278
  findings.push({
8276
8279
  code: "gsap_animates_clip_element",
8277
8280
  severity: "error",
8278
- message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity \u2014 do not animate these properties on clip elements.`,
8281
+ message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity. Do not animate these properties on clip elements.`,
8279
8282
  selector: sel,
8280
8283
  elementId: clipInfo.id || void 0,
8281
8284
  fixHint: "Remove the visibility/display/autoAlpha tween, or move the content into a child <div> and target that instead.",
@@ -8469,7 +8472,7 @@ ${right.raw}`)
8469
8472
  findings.push({
8470
8473
  code: "audio_reactive_single_tween_per_group",
8471
8474
  severity: "warning",
8472
- message: "Audio-reactive captions use a single tween per group based on peak values. This sets one static value at group.start \u2014 not perceptible as audio reactivity.",
8475
+ message: "Audio-reactive captions use a single tween per group based on peak values. This sets one static value at group.start, which is not perceptible as audio reactivity.",
8473
8476
  fixHint: "Sample audio data at 100-200ms intervals throughout each group's lifetime (for loop from group.start to group.end) and create a tween at each sample point for visible pulsing."
8474
8477
  });
8475
8478
  }
@@ -8554,7 +8557,7 @@ ${right.raw}`)
8554
8557
  ...locate(script, gsapCallOffset(script, "timeline")),
8555
8558
  code: "gsap_timeline_not_registered",
8556
8559
  severity: "error",
8557
- message: "GSAP timeline is created but never registered in window.__timelines. The runtime discovers timelines from this registry \u2014 without registration, animations will not play during preview or render.",
8560
+ message: "GSAP timeline is created but never registered in window.__timelines. The runtime discovers timelines from this registry. Without registration, animations will not play during preview or render.",
8558
8561
  fixHint: 'Add `window.__timelines = window.__timelines || {};` and `window.__timelines["root"] = tl;` after creating the timeline (use the composition\'s data-composition-id as the key).'
8559
8562
  });
8560
8563
  }
@@ -8622,9 +8625,9 @@ ${right.raw}`)
8622
8625
  findings.push({
8623
8626
  code: "gsap_from_opacity_noop",
8624
8627
  severity: "error",
8625
- message: `"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. gsap.from() animates FROM the specified value TO the current CSS value \u2014 since CSS is already 0, the element animates from 0\u21920 and never becomes visible.`,
8628
+ message: `"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. gsap.from() animates FROM the specified value TO the current CSS value. Since CSS is already 0, the element animates from 0\u21920 and never becomes visible.`,
8626
8629
  selector: sel,
8627
- fixHint: `Remove \`opacity: 0\` from the CSS/inline style on "${sel}". Let gsap.from({opacity: 0}) handle the initial hidden state \u2014 it will animate FROM 0 TO the CSS value (1 by default).`,
8630
+ fixHint: `Remove \`opacity: 0\` from the CSS/inline style on "${sel}". Let gsap.from({opacity: 0}) handle the initial hidden state. It will animate FROM 0 TO the CSS value (1 by default).`,
8628
8631
  snippet: truncateSnippet(win.raw)
8629
8632
  });
8630
8633
  }
@@ -8703,12 +8706,12 @@ ${right.raw}`)
8703
8706
  if (layoutProps.length > 0 && allTargetsHtmlInCanvas(call.selector)) layoutProps = [];
8704
8707
  if (layoutProps.length === 0 && reflowProps.length === 0 && !usesRoundProps) continue;
8705
8708
  const flagged = [...layoutProps, ...reflowProps, ...usesRoundProps ? ["roundProps"] : []];
8706
- const message = `GSAP tween on "${call.selector}" uses motion that snaps to integer device pixels: ${flagged.join(", ")}. Layout and text-reflow properties snap during browser layout; roundProps rounds the tween value. Slow motion or an ease-out tail then stutters under the seek-by-frame capture engine \u2014 animate transforms (x/y/scale/opacity) instead.`;
8709
+ const message = `GSAP tween on "${call.selector}" uses motion that snaps to integer device pixels: ${flagged.join(", ")}. Layout and text-reflow properties snap during browser layout; roundProps rounds the tween value. Slow motion or an ease-out tail then stutters under the seek-by-frame capture engine. Animate transforms (x/y/scale/opacity) instead.`;
8707
8710
  const fixes = [];
8708
8711
  if (layoutProps.length > 0) {
8709
8712
  const tokens = [...new Set(layoutProps.flatMap((p) => LAYOUT_FIX[p] ?? []))];
8710
8713
  fixes.push(
8711
- `replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")}) \u2014 e.g. tl.fromTo("${call.selector}", { x: -1300 }, { x: 0, ...yourAnimation })`
8714
+ `replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")}), e.g. tl.fromTo("${call.selector}", { x: -1300 }, { x: 0, ...yourAnimation })`
8712
8715
  );
8713
8716
  }
8714
8717
  if (reflowProps.length > 0) {
@@ -8720,7 +8723,7 @@ ${right.raw}`)
8720
8723
  }
8721
8724
  if (spacing.length > 0) {
8722
8725
  parts.push(
8723
- `for ${spacing.join("/")}, split the text into per-character elements and animate each glyph's x (the spread) \u2014 uniform scale is NOT equivalent \u2014 or hold the final value statically`
8726
+ `for ${spacing.join("/")}, split the text into per-character elements and animate each glyph's x (the spread), since uniform scale is NOT equivalent, or hold the final value statically`
8724
8727
  );
8725
8728
  }
8726
8729
  fixes.push(
@@ -8784,7 +8787,7 @@ ${right.raw}`)
8784
8787
  findings.push({
8785
8788
  code: "gsap_relative_value_second_writer",
8786
8789
  severity: "error",
8787
- message: `Relative value(s) ${values} on "${win.targetSelector}" start while another writer for the same propert${sharedProps.length > 1 ? "ies" : "y"} is active between ${formatTime(win.position)} and ${formatTime(overlapEnd)}. Relative tweens capture their base at tween init: the sequential path inits mid-flight of the other writer, a cold render worker landing later inits with its end state \u2014 the same frame renders at two different positions (snap at chunk boundaries).`,
8790
+ message: `Relative value(s) ${values} on "${win.targetSelector}" start while another writer for the same propert${sharedProps.length > 1 ? "ies" : "y"} is active between ${formatTime(win.position)} and ${formatTime(overlapEnd)}. Relative tweens capture their base at tween init: the sequential path inits mid-flight of the other writer, a cold render worker landing later inits with its end state, so the same frame renders at two different positions (snap at chunk boundaries).`,
8788
8791
  selector: win.targetSelector,
8789
8792
  fixHint: `Use absolute values for ${sharedProps.join(", ")}, or a fromTo() with explicit endpoints, so every seek path resolves the same state. Single-writer relative values are safe; the conflict is the second writer.`,
8790
8793
  snippet: truncateSnippet(`${win.raw}
@@ -8853,13 +8856,13 @@ ${other.raw}`)
8853
8856
  const readsInvariant = TRANSFORM_INVARIANT_READ.test(fn.body);
8854
8857
  const badMember = firstParamMemberAccessHazard(fn);
8855
8858
  if (!readsSensitive && !readsInvariant && !badMember) continue;
8856
- const reason = readsSensitive ? "reads transform-sensitive geometry, so its result depends on the worker's own seek order" : badMember ? `accesses .${badMember} on its first parameter \u2014 GSAP function values receive (index, target, targets), so the first parameter is a NUMBER and this throws at tween init` : "measures layout at tween init, which is deterministic across cold render workers only while the measured layout never animates";
8859
+ const reason = readsSensitive ? "reads transform-sensitive geometry, so its result depends on the worker's own seek order" : badMember ? `accesses .${badMember} on its first parameter. GSAP function values receive (index, target, targets), so the first parameter is a NUMBER and this throws at tween init` : "measures layout at tween init, which is deterministic across cold render workers only while the measured layout never animates";
8857
8860
  findings.push({
8858
8861
  code: "gsap_function_value_hazard",
8859
8862
  severity: readsSensitive || badMember ? "error" : "warning",
8860
8863
  message: `Function-valued tween var for ${prop} on "${anim.targetSelector}" ${reason}. Each render worker initializes tweens independently.`,
8861
8864
  selector: anim.targetSelector,
8862
- fixHint: badMember ? "Use the SECOND parameter for the element: (index, target) => ... \u2014 or index arithmetic like (i) => i * 20." : "Compute the value once at build time (before the timeline is registered) and pass a constant, or derive it from fixed composition coordinates.",
8865
+ fixHint: badMember ? "Use the SECOND parameter for the element: (index, target) => ..., or index arithmetic like (i) => i * 20." : "Compute the value once at build time (before the timeline is registered) and pass a constant, or derive it from fixed composition coordinates.",
8863
8866
  snippet: truncateSnippet(raw)
8864
8867
  });
8865
8868
  }
@@ -8895,9 +8898,9 @@ ${other.raw}`)
8895
8898
  findings.push({
8896
8899
  code: "gsap_callback_dom_measurement",
8897
8900
  severity: "warning",
8898
- message: "Timeline callback reaches DOM measurement (getBoundingClientRect/getTotalLength/getComputedStyle/...). The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek \u2014 and a cold render worker runs them against whatever DOM state its own non-linear seek order produced. Measured geometry is seek-order-dependent, and values measured at build time (before the callback ran) are stale or zero.",
8901
+ message: "Timeline callback reaches DOM measurement (getBoundingClientRect/getTotalLength/getComputedStyle/...). The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek, and a cold render worker runs them against whatever DOM state its own non-linear seek order produced. Measured geometry is seek-order-dependent, and values measured at build time (before the callback ran) are stale or zero.",
8899
8902
  selector: truncateSnippet(site, 120),
8900
- fixHint: "Do all measurement and DOM setup synchronously at build time, before registering the timeline \u2014 or derive geometry from fixed composition coordinates instead of measuring.",
8903
+ fixHint: "Do all measurement and DOM setup synchronously at build time, before registering the timeline, or derive geometry from fixed composition coordinates instead of measuring.",
8901
8904
  snippet: truncateSnippet(snippet)
8902
8905
  });
8903
8906
  };
@@ -8970,8 +8973,8 @@ ${other.raw}`)
8970
8973
  findings.push({
8971
8974
  code: "gsap_timeline_return_used_as_tween",
8972
8975
  severity: "error",
8973
- message: `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, but a timeline's \`.to()\`/\`.from()\`/\`.set()\` returns THE TIMELINE ITSELF, not the tween it created. Every captured value is the same master timeline, so a tween-scoped call on it \u2014 \`.kill()\` above all \u2014 hits the whole composition: it interrupts the timeline and detaches it from its parent, which freezes a parent-driven seek while an explicit \`tl.progress()\` keeps working. Only \`gsap.to()\` returns a tween.`,
8974
- fixHint: "To build a group of keyframes you can later discard, put them in a nested timeline: `const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);` \u2014 then `nested.kill()` replaces exactly those keyframes and cannot reach `tl`. Children keep their absolute times when the nest is added at 0. To hold a single real tween, create it with `gsap.to(...)` and place it with `tl.add(tween, at)`.",
8976
+ message: `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, but a timeline's \`.to()\`/\`.from()\`/\`.set()\` returns THE TIMELINE ITSELF, not the tween it created. Every captured value is the same master timeline, so a tween-scoped call on it (\`.kill()\` above all) hits the whole composition: it interrupts the timeline and detaches it from its parent, which freezes a parent-driven seek while an explicit \`tl.progress()\` keeps working. Only \`gsap.to()\` returns a tween.`,
8977
+ fixHint: "To build a group of keyframes you can later discard, put them in a nested timeline: `const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);`. Then `nested.kill()` replaces exactly those keyframes and cannot reach `tl`. Children keep their absolute times when the nest is added at 0. To hold a single real tween, create it with `gsap.to(...)` and place it with `tl.add(tween, at)`.",
8975
8978
  snippet: truncateSnippet(source.slice(contextStart, match.index + match[0].length + 60))
8976
8979
  });
8977
8980
  }
@@ -9066,7 +9069,7 @@ ${other.raw}`)
9066
9069
  findings.push({
9067
9070
  code: "svg_drawon_css_dasharray_conflict",
9068
9071
  severity: "error",
9069
- message: `GSAP writes strokeDasharray on "${targetLabel}", but its CSS ("${conflictToken}") declares a multi-component stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px") \u2014 the draw-on hide only hides one gap's worth and the line stays visible the whole scene.`,
9072
+ message: `GSAP writes strokeDasharray on "${targetLabel}", but its CSS ("${conflictToken}") declares a multi-component stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px"). The draw-on hide only hides one gap's worth and the line stays visible the whole scene.`,
9070
9073
  selector: quotedSelector ?? void 0,
9071
9074
  fixHint: `Remove the CSS stroke-dasharray from "${conflictToken}" (decorative dashes belong on a separate element), or set the full two-component value in GSAP: strokeDasharray: "\${len} \${len}".`,
9072
9075
  snippet: truncateSnippet(match[0] + firstVars.slice(1))
@@ -9188,7 +9191,7 @@ ${other.raw}`)
9188
9191
  findings.push({
9189
9192
  code: "svg_measure_before_path_d",
9190
9193
  severity: sameVarAssignmentExists ? "warning" : "error",
9191
- message: sameVarAssignmentExists ? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body \u2014 if the measure runs before that function (e.g. the function is a timeline callback), the length is 0 and the dash animation is dead.` : `getTotalLength() is called on "${tokenLabel}", but the path has no static \`d\` attribute and no d assignment exists anywhere \u2014 getTotalLength() returns 0 in Chrome, silently killing dash animations.`,
9194
+ message: sameVarAssignmentExists ? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body. If the measure runs before that function (e.g. the function is a timeline callback), the length is 0 and the dash animation is dead.` : `getTotalLength() is called on "${tokenLabel}", but the path has no static \`d\` attribute and no d assignment exists anywhere, so getTotalLength() returns 0 in Chrome, silently killing dash animations.`,
9192
9195
  selector: tokenLabel,
9193
9196
  fixHint: "Assign the path's `d` synchronously at build time (top level, before measuring), or author a static d attribute in the HTML.",
9194
9197
  snippet: truncateSnippet(match[0] + ")")
@@ -9725,7 +9728,7 @@ var fontRules = [
9725
9728
  {
9726
9729
  code: "system_font_will_alias",
9727
9730
  severity: "warning",
9728
- message: `Font ${aliased.length === 1 ? "family" : "families"} will be substituted at render time: ${aliased.join(", ")}. In distributed/Lambda rendering system-font capture is disabled \u2014 these fonts will fall back to OS defaults. Embed explicit @font-face declarations instead.`
9731
+ message: `Font ${aliased.length === 1 ? "family" : "families"} will be substituted at render time: ${aliased.join(", ")}. In distributed/Lambda rendering system-font capture is disabled, so these fonts will fall back to OS defaults. Embed explicit @font-face declarations instead.`
9729
9732
  }
9730
9733
  ];
9731
9734
  },
@@ -9745,7 +9748,7 @@ var fontRules = [
9745
9748
  code: "font_family_without_font_face",
9746
9749
  severity: "error",
9747
9750
  message: `Font ${undeclared.length === 1 ? "family" : "families"} used without @font-face declaration: ${undeclared.join(", ")}. These are not in the auto-resolved font list, so the renderer cannot supply them automatically. Text will fall back to a generic font, producing incorrect typography in the video.`,
9748
- fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files. For an OS-bundled system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, use src: local('Exact Font Name') instead \u2014 the declaration alone satisfies this check without needing a font file."
9751
+ fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files. For an OS-bundled system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, use src: local('Exact Font Name') instead. The declaration alone satisfies this check without needing a font file."
9749
9752
  });
9750
9753
  return findings;
9751
9754
  }
@@ -10216,7 +10219,7 @@ function collectLocalVideoCandidates(projectDir, htmlSources) {
10216
10219
  const re = new RegExp(videoSrcRe.source, videoSrcRe.flags);
10217
10220
  let match;
10218
10221
  while ((match = re.exec(scannable)) !== null) {
10219
- const rawSrc = match[2] ?? "";
10222
+ const rawSrc = mediaSrcOf(match);
10220
10223
  const reference = resolveLocalVideoReference(projectDir, rawSrc, compSrcPath);
10221
10224
  if (!reference || candidates.has(reference.resolved)) continue;
10222
10225
  candidates.set(reference.resolved, reference.src);
@@ -10602,9 +10605,10 @@ function lintAudioSrcNotFound(projectDir, htmlSources) {
10602
10605
  const audioSrcRe = mediaSrcTagRe("audio");
10603
10606
  const missingByFile = /* @__PURE__ */ new Map();
10604
10607
  for (const { html, compSrcPath } of htmlSources) {
10608
+ const scannable = maskNonScannableRanges(html);
10605
10609
  let match;
10606
- while ((match = audioSrcRe.exec(html)) !== null) {
10607
- const src = match[2];
10610
+ while ((match = audioSrcRe.exec(scannable)) !== null) {
10611
+ const src = mediaSrcOf(match);
10608
10612
  if (/^(https?:|data:|blob:)/i.test(src)) continue;
10609
10613
  if (isUnresolvedAssetPlaceholder(src)) continue;
10610
10614
  const rootRelative = compSrcPath ? rewriteAssetPath(compSrcPath, src, (path) => existsSync2(join2(projectDir, path))) : src;
@@ -10636,7 +10640,7 @@ function lintMissingLocalAsset(projectDir, htmlSources) {
10636
10640
  let match;
10637
10641
  while ((match = re.exec(scannable)) !== null) {
10638
10642
  const tagName = (match[1] ?? "").toLowerCase();
10639
- const rawSrc = match[2] ?? "";
10643
+ const rawSrc = mediaSrcOf(match);
10640
10644
  if (isUnresolvedAssetPlaceholder(rawSrc)) continue;
10641
10645
  const src = cleanAssetUrl(rawSrc);
10642
10646
  if (!src) continue;
@@ -10662,7 +10666,7 @@ function lintMissingLocalAsset(projectDir, htmlSources) {
10662
10666
  severity: "error",
10663
10667
  file,
10664
10668
  message: `<${tagName}> element references local file(s) not found in the project: ${unique.join(", ")}. The renderer will silently skip these and produce a video with missing visuals.`,
10665
- fixHint: unique.length === 1 ? `Add "${unique[0]}" to the project directory, or update the src attribute to point to an existing file. Common cause: captured asset filenames are unreliable (heygen-logo.svg often contains Google, nvidia-logo.svg may contain Autodesk, etc.). Open the contact sheets and verify the file actually exists at this path before referencing it.` : "Add the missing files to the project directory, or update the src attributes to point to existing files. Captured asset filenames are unreliable \u2014 verify against capture/contact-sheets/ and capture/extracted/asset-descriptions.md."
10669
+ fixHint: unique.length === 1 ? `Add "${unique[0]}" to the project directory, or update the src attribute to point to an existing file. Common cause: captured asset filenames are unreliable (heygen-logo.svg often contains Google, nvidia-logo.svg may contain Autodesk, etc.). Open the contact sheets and verify the file actually exists at this path before referencing it.` : "Add the missing files to the project directory, or update the src attributes to point to existing files. Captured asset filenames are unreliable. Verify against capture/contact-sheets/ and capture/extracted/asset-descriptions.md."
10666
10670
  });
10667
10671
  }
10668
10672
  return findings;
@@ -10729,11 +10733,6 @@ function lintMultipleRootCompositions(projectDir) {
10729
10733
  }
10730
10734
  function lintDuplicateAudioTracks(htmlSources) {
10731
10735
  const findings = [];
10732
- function extractAttr(tag, name) {
10733
- const re = new RegExp(`\\b${name}\\s*=\\s*["']([^"']+)["']`, "i");
10734
- const m = tag.match(re);
10735
- return m?.[1] ?? null;
10736
- }
10737
10736
  const tracks = [];
10738
10737
  const seen = /* @__PURE__ */ new Set();
10739
10738
  for (const { html } of htmlSources) {
@@ -10742,10 +10741,10 @@ function lintDuplicateAudioTracks(htmlSources) {
10742
10741
  while ((match = soundTagRe.exec(html)) !== null) {
10743
10742
  const tag = match[0];
10744
10743
  if (/^<video/i.test(tag) && !isAudibleVideoTag(tag)) continue;
10745
- const trackStr = extractAttr(tag, "data-track-index");
10746
- const startStr = extractAttr(tag, "data-start");
10747
- const durStr = extractAttr(tag, "data-duration");
10748
- const src = extractAttr(tag, "src") ?? "unknown";
10744
+ const trackStr = readAttr(tag, "data-track-index");
10745
+ const startStr = readAttr(tag, "data-start");
10746
+ const durStr = readAttr(tag, "data-duration");
10747
+ const src = readDecodedAttr(tag, "src") || "unknown";
10749
10748
  if (!trackStr || !startStr) continue;
10750
10749
  const trackIndex = parseInt(trackStr, 10);
10751
10750
  const start = parseFloat(startStr);
@@ -10819,7 +10818,7 @@ function lintMissingOrEmptySubComposition(projectDir, rootHtml) {
10819
10818
  code: "missing_or_empty_sub_composition",
10820
10819
  severity: "error",
10821
10820
  message: `data-composition-src references "${srcPath}", but ${problem}.`,
10822
- fixHint: folder ? `Point data-composition-src at the HTML file inside the folder, such as "${srcPath}/index.html".` : `Fix this before rendering \u2014 the render pre-flight rejects unusable sub-compositions. Write valid HTML into "${srcPath}" \u2014 it needs a <template> or <body> containing an element with data-composition-id, data-width, and data-height. Preview/studio still tolerates and skips the scene while you author it. If a scene-authoring step is still running, wait for it to finish before referencing the file, or re-run the step that generates it.`
10821
+ fixHint: folder ? `Point data-composition-src at the HTML file inside the folder, such as "${srcPath}/index.html".` : `Fix this before rendering: the render pre-flight rejects unusable sub-compositions. Write valid HTML into "${srcPath}". It needs a <template> or <body> containing an element with data-composition-id, data-width, and data-height. Preview/studio still tolerates and skips the scene while you author it. If a scene-authoring step is still running, wait for it to finish before referencing the file, or re-run the step that generates it.`
10823
10822
  });
10824
10823
  }
10825
10824
  return findings;
@@ -58,7 +58,7 @@ function getCgroupMemoryLimitMb() {
58
58
  _cachedCgroupLimitMb = parseCgroupLimitMb(v2Content, v1Content);
59
59
  if (_cachedCgroupLimitMb !== null) {
60
60
  console.warn(
61
- `[SystemMemory] cgroup memory limit detected: ${_cachedCgroupLimitMb} MiB \u2014 it governs memory-adaptive render behaviour instead of host RAM.`
61
+ `[SystemMemory] cgroup memory limit detected: ${_cachedCgroupLimitMb} MiB. It governs memory-adaptive render behaviour instead of host RAM.`
62
62
  );
63
63
  }
64
64
  return _cachedCgroupLimitMb;
@@ -17,7 +17,7 @@ import {
17
17
  } from "./chunk-ISN5GRYI.js";
18
18
  import {
19
19
  VERSION
20
- } from "./chunk-4OC7MQNB.js";
20
+ } from "./chunk-TBOMPYG6.js";
21
21
 
22
22
  // src/utils/updateCheck.ts
23
23
  import { existsSync, readFileSync } from "fs";
@@ -15,12 +15,12 @@ import {
15
15
  findFFmpeg,
16
16
  findFFprobe,
17
17
  getFFmpegInstallHint
18
- } from "./chunk-ZEUONDQG.js";
18
+ } from "./chunk-SYUGVPYH.js";
19
19
  import {
20
20
  DEFAULT_VP9_CPU_USED,
21
21
  SDR_RGB_TO_BT709_FILTER,
22
22
  renderProvenanceArgs
23
- } from "./chunk-5VMR4WBS.js";
23
+ } from "./chunk-WU2B74TL.js";
24
24
 
25
25
  // src/background-removal/pipeline.ts
26
26
  import { spawn } from "child_process";
@@ -203,7 +203,7 @@ function inferInputKind(inputPath) {
203
203
  }
204
204
  async function probeMedia(inputPath) {
205
205
  const isImage = inferInputKind(inputPath) === "image";
206
- const engine = await import("./src-VUJ2FUFK.js");
206
+ const engine = await import("./src-LIERUCFW.js");
207
207
  const meta = await engine.extractMediaMetadata(inputPath);
208
208
  if (isImage) {
209
209
  return { width: meta.width, height: meta.height, fps: 0, frameCount: 1 };
@@ -8,10 +8,10 @@ import {
8
8
  DEFAULT_VP9_CPU_USED,
9
9
  isHttpUrl,
10
10
  runFfmpeg
11
- } from "./chunk-5VMR4WBS.js";
11
+ } from "./chunk-WU2B74TL.js";
12
12
  import {
13
13
  parseAnimatedGifMetadata
14
- } from "./chunk-3VYNNWVA.js";
14
+ } from "./chunk-BAKPN2D2.js";
15
15
  import {
16
16
  parseHTML
17
17
  } from "./chunk-SDYDNHEE.js";
@@ -16,10 +16,10 @@ import {
16
16
  import {
17
17
  formatVariableValidationIssue,
18
18
  validateVariables
19
- } from "./chunk-3VYNNWVA.js";
19
+ } from "./chunk-BAKPN2D2.js";
20
20
  import {
21
21
  extractCompositionMetadata
22
- } from "./chunk-MQZVBHEY.js";
22
+ } from "./chunk-LFNQDTSH.js";
23
23
  import {
24
24
  c
25
25
  } from "./chunk-ZTZA4UWZ.js";
@@ -9,7 +9,7 @@ import {
9
9
  } from "./chunk-BHVFTYSI.js";
10
10
  import {
11
11
  findFFmpeg
12
- } from "./chunk-ZEUONDQG.js";
12
+ } from "./chunk-SYUGVPYH.js";
13
13
 
14
14
  // src/whisper/manager.ts
15
15
  import { execFileSync } from "child_process";
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-HVW7ABXD.js";
13
13
  import {
14
14
  DEFAULT_REGISTRY_URL
15
- } from "./chunk-I5KY7EZP.js";
15
+ } from "./chunk-XVIYZU3D.js";
16
16
 
17
17
  // src/utils/projectConfig.ts
18
18
  import { readFileSync, writeFileSync } from "fs";
@@ -4357,7 +4357,7 @@ function buildAspectMismatch(compositionWidth, compositionHeight, outputResoluti
4357
4357
  compositionHeight,
4358
4358
  outputResolution
4359
4359
  );
4360
- const suggestion = suggestedResolution ? ` The composition is ${describeOrientation(compositionWidth, compositionHeight)} \u2014 use --resolution ${suggestedResolution} instead.` : ` Pick a preset whose orientation matches, or omit --resolution to render at the composition's native dimensions.`;
4360
+ const suggestion = suggestedResolution ? ` The composition is ${describeOrientation(compositionWidth, compositionHeight)}, so use --resolution ${suggestedResolution} instead.` : ` Pick a preset whose orientation matches, or omit --resolution to render at the composition's native dimensions.`;
4361
4361
  return {
4362
4362
  ok: false,
4363
4363
  kind: "aspect-mismatch",