framefields 2.12.0 → 2.13.1

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 (387) hide show
  1. package/README.md +9 -4
  2. package/dist/audio/index.d.mts +6 -5
  3. package/dist/audio/index.mjs +13 -13
  4. package/dist/{audio-afTBdB3U.d.mts → audio-C4B9p_yq.d.mts} +3 -4
  5. package/dist/audio-C4B9p_yq.d.mts.map +1 -0
  6. package/dist/{audio-DHLoNcxD.mjs → audio-DIvi6hTD.mjs} +4 -4
  7. package/dist/{audio-DHLoNcxD.mjs.map → audio-DIvi6hTD.mjs.map} +1 -1
  8. package/dist/{audio-KHBadECb.mjs → audio-DS9Jg2ls.mjs} +1 -1
  9. package/dist/{audio-KHBadECb.mjs.map → audio-DS9Jg2ls.mjs.map} +1 -1
  10. package/dist/{check-result-Culfu7Nx.mjs → check-result-DZWx73V8.mjs} +1 -1
  11. package/dist/{check-result-Culfu7Nx.mjs.map → check-result-DZWx73V8.mjs.map} +1 -1
  12. package/dist/cli/check-worker.mjs +51 -51
  13. package/dist/cli/check-worker.mjs.map +1 -1
  14. package/dist/cli/local-worker.d.mts +28 -0
  15. package/dist/cli/local-worker.d.mts.map +1 -0
  16. package/dist/cli/local-worker.mjs +83 -0
  17. package/dist/cli/local-worker.mjs.map +1 -0
  18. package/dist/cli.mjs +229 -54
  19. package/dist/cli.mjs.map +1 -1
  20. package/dist/cloud/index.d.mts +2 -0
  21. package/dist/cloud/index.d.mts.map +1 -1
  22. package/dist/cloud/index.mjs +1 -1
  23. package/dist/{cloud-COid1Ipa.mjs → cloud-Cj1TIme8.mjs} +306 -7
  24. package/dist/cloud-Cj1TIme8.mjs.map +1 -0
  25. package/dist/components/index.d.mts +15 -14
  26. package/dist/components/index.mjs +47 -47
  27. package/dist/{dist-CySGkG54.mjs → dist-BSKAbyea.mjs} +2 -2
  28. package/dist/{dist-CySGkG54.mjs.map → dist-BSKAbyea.mjs.map} +1 -1
  29. package/dist/{dist-BwtxciN4.mjs → dist-B_rSWeOz.mjs} +1 -1
  30. package/dist/{dist-Bf2_efYP.mjs → dist-C8XSqfTx.mjs} +1 -1
  31. package/dist/{dist-Bf2_efYP.mjs.map → dist-C8XSqfTx.mjs.map} +1 -1
  32. package/dist/{dist-BSqeChht.mjs → dist-CCrYOkcc.mjs} +145 -84
  33. package/dist/dist-CCrYOkcc.mjs.map +1 -0
  34. package/dist/dist-CiTgmfUw.mjs +502 -0
  35. package/dist/dist-CiTgmfUw.mjs.map +1 -0
  36. package/dist/{dist-BIE8fvlv.mjs → dist-DgZpN6IK.mjs} +1 -1
  37. package/dist/{dist-BIE8fvlv.mjs.map → dist-DgZpN6IK.mjs.map} +1 -1
  38. package/dist/{dist-B-OPY7Cs.mjs → dist-DtPPPsZV.mjs} +356 -145
  39. package/dist/dist-DtPPPsZV.mjs.map +1 -0
  40. package/dist/{dist-2_lT3Dwc.mjs → dist-Kbf-JTry.mjs} +2 -2
  41. package/dist/{dist-2_lT3Dwc.mjs.map → dist-Kbf-JTry.mjs.map} +1 -1
  42. package/dist/effects/index.d.mts +6 -6
  43. package/dist/effects/index.mjs +36 -36
  44. package/dist/{effects-C8CL9cOq.mjs → effects-B-L3rLs6.mjs} +3 -3
  45. package/dist/{effects-C8CL9cOq.mjs.map → effects-B-L3rLs6.mjs.map} +1 -1
  46. package/dist/fonts/index.d.mts +1 -1
  47. package/dist/fonts/index.mjs +3 -3
  48. package/dist/{fonts-BQrDjnpV.mjs → fonts-k5ll8NTE.mjs} +2 -2
  49. package/dist/{fonts-BQrDjnpV.mjs.map → fonts-k5ll8NTE.mjs.map} +1 -1
  50. package/dist/{gpu-DHa-3Dou.mjs → gpu-D2HT3KcE.mjs} +2 -2
  51. package/dist/{gpu-DHa-3Dou.mjs.map → gpu-D2HT3KcE.mjs.map} +1 -1
  52. package/dist/{index-CTv9ZieD.d.mts → index-B08eSpaI.d.mts} +3 -30
  53. package/dist/index-B08eSpaI.d.mts.map +1 -0
  54. package/dist/index-BFTtXosj.d.mts +63 -0
  55. package/dist/index-BFTtXosj.d.mts.map +1 -0
  56. package/dist/{index-Drk6HC2k.d.mts → index-BGlaws7-.d.mts} +3 -3
  57. package/dist/{index-Drk6HC2k.d.mts.map → index-BGlaws7-.d.mts.map} +1 -1
  58. package/dist/{index-CAuNuvIg.d.mts → index-ByhlTwxh.d.mts} +5 -5
  59. package/dist/{index-CAuNuvIg.d.mts.map → index-ByhlTwxh.d.mts.map} +1 -1
  60. package/dist/{index-Fliv2Z-w.d.mts → index-C2yaponf.d.mts} +132 -7
  61. package/dist/index-C2yaponf.d.mts.map +1 -0
  62. package/dist/{index-BW0Q1ygn.d.mts → index-C5sgRLlG.d.mts} +1 -1
  63. package/dist/{index-BW0Q1ygn.d.mts.map → index-C5sgRLlG.d.mts.map} +1 -1
  64. package/dist/{index-CdCrae-I.d.mts → index-CnZhBlob.d.mts} +2 -2
  65. package/dist/index-CnZhBlob.d.mts.map +1 -0
  66. package/dist/{index-BNClElng.d.mts → index-CtKUQ2VG.d.mts} +281 -283
  67. package/dist/index-CtKUQ2VG.d.mts.map +1 -0
  68. package/dist/{index-WwuuX8ZJ.d.mts → index-DBh3WphX.d.mts} +2 -2
  69. package/dist/{index-WwuuX8ZJ.d.mts.map → index-DBh3WphX.d.mts.map} +1 -1
  70. package/dist/{index-gOVtTm-P.d.mts → index-DQ1U3HC3.d.mts} +14 -6
  71. package/dist/index-DQ1U3HC3.d.mts.map +1 -0
  72. package/dist/{index-COYNQ_mf.d.mts → index-a6JrbAK0.d.mts} +1 -1
  73. package/dist/{index-COYNQ_mf.d.mts.map → index-a6JrbAK0.d.mts.map} +1 -1
  74. package/dist/{index-DQa7mhzq.d.mts → index-rtCXNxGO.d.mts} +2 -2
  75. package/dist/{index-DQa7mhzq.d.mts.map → index-rtCXNxGO.d.mts.map} +1 -1
  76. package/dist/index.d.mts +16 -15
  77. package/dist/index.mjs +58 -58
  78. package/dist/preview/index.d.mts +5 -4
  79. package/dist/preview/index.mjs +12 -12
  80. package/dist/preview-engine/audio.mjs +1 -1
  81. package/dist/preview-engine/{chunk-FJTURGJV.mjs → chunk-3C2XT64C.mjs} +1 -1
  82. package/dist/preview-engine/chunk-52X7SURQ.mjs +494 -0
  83. package/dist/preview-engine/{chunk-4JNIEQS2.mjs → chunk-5ZS46EFB.mjs} +1 -1
  84. package/dist/preview-engine/{chunk-KBCZKIIQ.mjs → chunk-CYM75INB.mjs} +1 -1
  85. package/dist/preview-engine/chunk-FZ4RD7HV.mjs +1 -0
  86. package/dist/preview-engine/chunk-LZNHGH56.mjs +5 -0
  87. package/dist/preview-engine/{chunk-RGKS6YIA.mjs → chunk-N2VISDTG.mjs} +1 -1
  88. package/dist/preview-engine/chunk-N3XTDAL6.mjs +2227 -0
  89. package/dist/preview-engine/{chunk-O75BFM4S.mjs → chunk-NVXFEDJS.mjs} +1 -1
  90. package/dist/preview-engine/chunk-OBIJIQCZ.mjs +1 -0
  91. package/dist/preview-engine/{chunk-VUO4K52B.mjs → chunk-PD2HKW2S.mjs} +1 -1
  92. package/dist/preview-engine/{chunk-HTAHYSCW.mjs → chunk-R4H52Q7S.mjs} +1 -1
  93. package/dist/preview-engine/{chunk-6O7UDVNL.mjs → chunk-RYSV44B4.mjs} +1 -1
  94. package/dist/preview-engine/{chunk-TKXOSEIJ.mjs → chunk-SV3ZNZQU.mjs} +1 -1
  95. package/dist/preview-engine/{chunk-JAUMCOOJ.mjs → chunk-UBY2UYEL.mjs} +1 -1
  96. package/dist/preview-engine/{chunk-32WC4FRR.mjs → chunk-UE7Y65C3.mjs} +3 -3
  97. package/dist/preview-engine/{chunk-M4QVCQ3O.mjs → chunk-VMXJZ4KQ.mjs} +1 -1
  98. package/dist/preview-engine/{chunk-EYKN2VET.mjs → chunk-XCPRRMMW.mjs} +31 -31
  99. package/dist/preview-engine/components.mjs +1 -1
  100. package/dist/preview-engine/effects.mjs +1 -1
  101. package/dist/preview-engine/framefields.mjs +1 -1
  102. package/dist/preview-engine/player.mjs +5 -5
  103. package/dist/preview-engine/{renderers-L6GFGHS6.mjs → renderers-AFTUFMQQ.mjs} +1 -1
  104. package/dist/preview-engine/renderers-DU7KPFVV.mjs +1 -1
  105. package/dist/preview-engine/{renderers-PREFONJG.mjs → renderers-EPSF5CSQ.mjs} +1 -1
  106. package/dist/preview-engine/{renderers-R22CDFO4.mjs → renderers-OJWWIVNW.mjs} +1 -1
  107. package/dist/preview-engine/{renderers-TS32FDV7.mjs → renderers-RHMRWX46.mjs} +1 -1
  108. package/dist/preview-engine/renderers-VEHYARKY.mjs +1 -0
  109. package/dist/preview-engine/renderers-VRXAG3FR.mjs +1 -0
  110. package/dist/preview-engine/renderers-ZHSOWEMQ.mjs +1 -0
  111. package/dist/preview-engine/renderers-ZMDRNYSS.mjs +1 -0
  112. package/dist/preview-engine/renderers-ZMMQ7S2A.mjs +228 -0
  113. package/dist/preview-engine/signals.mjs +1 -1
  114. package/dist/preview-engine/{src-GPF7RIBU.mjs → src-YXNX4MLK.mjs} +1 -1
  115. package/dist/{preview-BUtt481A.mjs → preview-lHH8Tv4j.mjs} +323 -35
  116. package/dist/preview-lHH8Tv4j.mjs.map +1 -0
  117. package/dist/{program-Cvv_nb9T.mjs → program-BfiB9ttP.mjs} +17 -4
  118. package/dist/program-BfiB9ttP.mjs.map +1 -0
  119. package/dist/renderer/index.d.mts +4 -3
  120. package/dist/renderer/index.mjs +3 -3
  121. package/dist/renderer-0rLmztAv.mjs +5 -0
  122. package/dist/{renderer-D3QPNBEj.mjs → renderer-4nsuReW2.mjs} +5 -5
  123. package/dist/{renderer-D3QPNBEj.mjs.map → renderer-4nsuReW2.mjs.map} +1 -1
  124. package/dist/{renderer-DO1SZ5xY.mjs → renderer-7xQmXXMH.mjs} +5 -5
  125. package/dist/{renderer-DO1SZ5xY.mjs.map → renderer-7xQmXXMH.mjs.map} +1 -1
  126. package/dist/renderer-B0pjOXFY.mjs +5 -0
  127. package/dist/{renderer-BSsKlcHk.mjs → renderer-B10HDC13.mjs} +5 -5
  128. package/dist/{renderer-BSsKlcHk.mjs.map → renderer-B10HDC13.mjs.map} +1 -1
  129. package/dist/{renderer-DaY04IyU.mjs → renderer-B2IUPjhw.mjs} +5 -5
  130. package/dist/{renderer-DaY04IyU.mjs.map → renderer-B2IUPjhw.mjs.map} +1 -1
  131. package/dist/{renderer-O4y02xkD.mjs → renderer-B3_GddgJ.mjs} +4 -4
  132. package/dist/{renderer-O4y02xkD.mjs.map → renderer-B3_GddgJ.mjs.map} +1 -1
  133. package/dist/{renderer-DQJHjU0L.mjs → renderer-B3un4Pdi.mjs} +5 -5
  134. package/dist/{renderer-DQJHjU0L.mjs.map → renderer-B3un4Pdi.mjs.map} +1 -1
  135. package/dist/{renderer-PnnkUWm-.mjs → renderer-B6bK9rXm.mjs} +4 -4
  136. package/dist/{renderer-PnnkUWm-.mjs.map → renderer-B6bK9rXm.mjs.map} +1 -1
  137. package/dist/renderer-B7J13reL.mjs +5 -0
  138. package/dist/{renderer-HXw4cJZx.mjs → renderer-BEhZiOT4.mjs} +3 -3
  139. package/dist/{renderer-HXw4cJZx.mjs.map → renderer-BEhZiOT4.mjs.map} +1 -1
  140. package/dist/{renderer-xNOKSN3V.mjs → renderer-BEuXJY-C.mjs} +5 -5
  141. package/dist/{renderer-xNOKSN3V.mjs.map → renderer-BEuXJY-C.mjs.map} +1 -1
  142. package/dist/{renderer-IAffbHXx.mjs → renderer-BGchUBW_.mjs} +4 -4
  143. package/dist/{renderer-IAffbHXx.mjs.map → renderer-BGchUBW_.mjs.map} +1 -1
  144. package/dist/{renderer-CZClDMgF.mjs → renderer-BIKkxw_Z.mjs} +5 -5
  145. package/dist/{renderer-CZClDMgF.mjs.map → renderer-BIKkxw_Z.mjs.map} +1 -1
  146. package/dist/{renderer-BvRM3OZ6.mjs → renderer-BM19iK8v.mjs} +3 -3
  147. package/dist/{renderer-BvRM3OZ6.mjs.map → renderer-BM19iK8v.mjs.map} +1 -1
  148. package/dist/{renderer-CtpEzVBn.mjs → renderer-BOlHW3Mi.mjs} +3 -3
  149. package/dist/{renderer-CtpEzVBn.mjs.map → renderer-BOlHW3Mi.mjs.map} +1 -1
  150. package/dist/renderer-BPGIKGgl.mjs +5 -0
  151. package/dist/{renderer-4uZGSmUD.mjs → renderer-BTwuM9u-.mjs} +5 -5
  152. package/dist/{renderer-4uZGSmUD.mjs.map → renderer-BTwuM9u-.mjs.map} +1 -1
  153. package/dist/{renderer-DO_zXwMJ.mjs → renderer-BZMh2io2.mjs} +5 -5
  154. package/dist/{renderer-DO_zXwMJ.mjs.map → renderer-BZMh2io2.mjs.map} +1 -1
  155. package/dist/{renderer-rKw5_EjG.mjs → renderer-BZX932Lj.mjs} +4 -4
  156. package/dist/{renderer-rKw5_EjG.mjs.map → renderer-BZX932Lj.mjs.map} +1 -1
  157. package/dist/{renderer-5fGj_tqN.mjs → renderer-BhcTYhaP.mjs} +6 -6
  158. package/dist/{renderer-5fGj_tqN.mjs.map → renderer-BhcTYhaP.mjs.map} +1 -1
  159. package/dist/{renderer-DL3o_lG1.mjs → renderer-BmyFfvVA.mjs} +5 -5
  160. package/dist/{renderer-DL3o_lG1.mjs.map → renderer-BmyFfvVA.mjs.map} +1 -1
  161. package/dist/{renderer-DX60Kr20.mjs → renderer-BrEeUw7T.mjs} +3 -3
  162. package/dist/{renderer-DX60Kr20.mjs.map → renderer-BrEeUw7T.mjs.map} +1 -1
  163. package/dist/{renderer-Bf7Z7WT5.mjs → renderer-BsrKj6Td.mjs} +4 -4
  164. package/dist/{renderer-Bf7Z7WT5.mjs.map → renderer-BsrKj6Td.mjs.map} +1 -1
  165. package/dist/{renderer-BOg8i8eF.mjs → renderer-BuvKGwk3.mjs} +1 -1
  166. package/dist/{renderer-BOg8i8eF.mjs.map → renderer-BuvKGwk3.mjs.map} +1 -1
  167. package/dist/renderer-Bv-Artj1.mjs +5 -0
  168. package/dist/{renderer-_KIBvES8.mjs → renderer-BvBqTAtk.mjs} +5 -5
  169. package/dist/{renderer-_KIBvES8.mjs.map → renderer-BvBqTAtk.mjs.map} +1 -1
  170. package/dist/renderer-BvLjbhcL.mjs +5 -0
  171. package/dist/{renderer-BC_Nse6h.mjs → renderer-Bw-_GZVw.mjs} +4 -4
  172. package/dist/{renderer-BC_Nse6h.mjs.map → renderer-Bw-_GZVw.mjs.map} +1 -1
  173. package/dist/{renderer-CLTRTtQ8.mjs → renderer-Bw0O--gW.mjs} +5 -5
  174. package/dist/{renderer-CLTRTtQ8.mjs.map → renderer-Bw0O--gW.mjs.map} +1 -1
  175. package/dist/renderer-By6awWaf.mjs +5 -0
  176. package/dist/{renderer-DsPLMzy1.mjs → renderer-C-3qhk5x.mjs} +2 -2
  177. package/dist/{renderer-DsPLMzy1.mjs.map → renderer-C-3qhk5x.mjs.map} +1 -1
  178. package/dist/renderer-C0rvsz7m.mjs +5 -0
  179. package/dist/{renderer-DP9ScNM6.mjs → renderer-C4hd6Cqi.mjs} +3 -3
  180. package/dist/{renderer-DP9ScNM6.mjs.map → renderer-C4hd6Cqi.mjs.map} +1 -1
  181. package/dist/{renderer-B09I95bo.mjs → renderer-C64_B8It.mjs} +3 -3
  182. package/dist/{renderer-DrL3J9ci.mjs → renderer-C98XwEIr.mjs} +4 -4
  183. package/dist/{renderer-DrL3J9ci.mjs.map → renderer-C98XwEIr.mjs.map} +1 -1
  184. package/dist/{renderer-6g1ri445.mjs → renderer-CC22Djgi.mjs} +5 -5
  185. package/dist/{renderer-6g1ri445.mjs.map → renderer-CC22Djgi.mjs.map} +1 -1
  186. package/dist/renderer-CDgud_HB.mjs +5 -0
  187. package/dist/renderer-CDiRF3bG.mjs +6 -0
  188. package/dist/{renderer-FvvywNvv.mjs → renderer-CHCRcCKw.mjs} +3 -3
  189. package/dist/{renderer-FvvywNvv.mjs.map → renderer-CHCRcCKw.mjs.map} +1 -1
  190. package/dist/renderer-CIIEQd4B.mjs +5 -0
  191. package/dist/renderer-CJblwOA9.mjs +5 -0
  192. package/dist/renderer-CNMOXtXE.mjs +5 -0
  193. package/dist/{renderer-D3YbHjRG.mjs → renderer-CO4NGvAh.mjs} +95 -80
  194. package/dist/renderer-CO4NGvAh.mjs.map +1 -0
  195. package/dist/{renderer-ByFPmHBC.mjs → renderer-CO9vE3sd.mjs} +4 -4
  196. package/dist/{renderer-ByFPmHBC.mjs.map → renderer-CO9vE3sd.mjs.map} +1 -1
  197. package/dist/renderer-COeSeNGg.mjs +5 -0
  198. package/dist/{renderer-CN-mCIoI.mjs → renderer-CQ4h8n7p.mjs} +5 -5
  199. package/dist/{renderer-CN-mCIoI.mjs.map → renderer-CQ4h8n7p.mjs.map} +1 -1
  200. package/dist/{renderer-DEoerd-j.mjs → renderer-CRhEGqW4.mjs} +4 -4
  201. package/dist/{renderer-DEoerd-j.mjs.map → renderer-CRhEGqW4.mjs.map} +1 -1
  202. package/dist/{renderer-CiHVnhUk.mjs → renderer-CSu_vYPn.mjs} +5 -5
  203. package/dist/{renderer-CiHVnhUk.mjs.map → renderer-CSu_vYPn.mjs.map} +1 -1
  204. package/dist/{renderer-BJEexai1.mjs → renderer-CU6WT5jW.mjs} +5 -5
  205. package/dist/{renderer-BJEexai1.mjs.map → renderer-CU6WT5jW.mjs.map} +1 -1
  206. package/dist/renderer-CUfElmtZ.mjs +5 -0
  207. package/dist/{renderer-BwJ0nXXu.mjs → renderer-CXrnzihz.mjs} +5 -5
  208. package/dist/{renderer-BwJ0nXXu.mjs.map → renderer-CXrnzihz.mjs.map} +1 -1
  209. package/dist/renderer-CYh2lFoJ.mjs +5 -0
  210. package/dist/renderer-CYtVZYMH.mjs +5 -0
  211. package/dist/{renderer-BwV-o_1b.mjs → renderer-CdXBRdFf.mjs} +4 -4
  212. package/dist/{renderer-BwV-o_1b.mjs.map → renderer-CdXBRdFf.mjs.map} +1 -1
  213. package/dist/{renderer-DrK6pyfz.mjs → renderer-Cfx_FCSf.mjs} +5 -5
  214. package/dist/{renderer-DrK6pyfz.mjs.map → renderer-Cfx_FCSf.mjs.map} +1 -1
  215. package/dist/renderer-CjxZlkjb.mjs +5 -0
  216. package/dist/{renderer-CInMMs5x.mjs → renderer-CkTehyRX.mjs} +6 -6
  217. package/dist/{renderer-CInMMs5x.mjs.map → renderer-CkTehyRX.mjs.map} +1 -1
  218. package/dist/{renderer-DGXqspB9.mjs → renderer-CldjJmRR.mjs} +3 -3
  219. package/dist/{renderer-HhJPFidU.mjs → renderer-CoC64eam.mjs} +5 -5
  220. package/dist/{renderer-HhJPFidU.mjs.map → renderer-CoC64eam.mjs.map} +1 -1
  221. package/dist/renderer-Cpd6TvTC.mjs +5 -0
  222. package/dist/{renderer-DLaUT6JZ.mjs → renderer-Cv9CKKWG.mjs} +4 -4
  223. package/dist/{renderer-DLaUT6JZ.mjs.map → renderer-Cv9CKKWG.mjs.map} +1 -1
  224. package/dist/renderer-CzgcZXKH.mjs +3 -0
  225. package/dist/{renderer-DCMoBppL.mjs → renderer-D-GWBoVT.mjs} +3 -3
  226. package/dist/{renderer-DCMoBppL.mjs.map → renderer-D-GWBoVT.mjs.map} +1 -1
  227. package/dist/{renderer-C3CGYjrC.mjs → renderer-D-mX1EVA.mjs} +5 -5
  228. package/dist/{renderer-C3CGYjrC.mjs.map → renderer-D-mX1EVA.mjs.map} +1 -1
  229. package/dist/{renderer-C2kFIZ4r.mjs → renderer-D0h1hTOo.mjs} +4 -4
  230. package/dist/{renderer-C2kFIZ4r.mjs.map → renderer-D0h1hTOo.mjs.map} +1 -1
  231. package/dist/{renderer-C_gR2H3R.mjs → renderer-D0xM67cK.mjs} +3 -3
  232. package/dist/{renderer-C_gR2H3R.mjs.map → renderer-D0xM67cK.mjs.map} +1 -1
  233. package/dist/renderer-D3ziOPhX.mjs +5 -0
  234. package/dist/renderer-D5yp74NL.mjs +3 -0
  235. package/dist/{renderer-BTd6tFcS.mjs → renderer-DCrKEVjb.mjs} +5 -5
  236. package/dist/{renderer-BTd6tFcS.mjs.map → renderer-DCrKEVjb.mjs.map} +1 -1
  237. package/dist/{renderer-Dak_wVh9.mjs → renderer-DGOMfyh8.mjs} +2 -2
  238. package/dist/{renderer-Dak_wVh9.mjs.map → renderer-DGOMfyh8.mjs.map} +1 -1
  239. package/dist/renderer-DKJOon6y.d.mts +1 -0
  240. package/dist/renderer-DL-phtL5.mjs +5 -0
  241. package/dist/{renderer-uA0UCxjA.mjs → renderer-DNynPM6X.mjs} +4 -4
  242. package/dist/{renderer-uA0UCxjA.mjs.map → renderer-DNynPM6X.mjs.map} +1 -1
  243. package/dist/renderer-DSnmYnHg.mjs +5 -0
  244. package/dist/renderer-DUGfsT-E.mjs +5 -0
  245. package/dist/renderer-DW5af9x1.mjs +5 -0
  246. package/dist/{renderer-CMCmljb0.mjs → renderer-DWYmJSrl.mjs} +3 -3
  247. package/dist/{renderer-CMCmljb0.mjs.map → renderer-DWYmJSrl.mjs.map} +1 -1
  248. package/dist/renderer-DYMrol1t.mjs +5 -0
  249. package/dist/{renderer-CGpi8CDT.mjs → renderer-DYeNNc_G.mjs} +4 -4
  250. package/dist/{renderer-CGpi8CDT.mjs.map → renderer-DYeNNc_G.mjs.map} +1 -1
  251. package/dist/{renderer-DKFaVaRJ.mjs → renderer-DYlx7VHo.mjs} +4 -4
  252. package/dist/{renderer-DKFaVaRJ.mjs.map → renderer-DYlx7VHo.mjs.map} +1 -1
  253. package/dist/{renderer-DZIxduEt.mjs → renderer-D_m6Qbsi.mjs} +3 -3
  254. package/dist/{renderer-DZIxduEt.mjs.map → renderer-D_m6Qbsi.mjs.map} +1 -1
  255. package/dist/renderer-DekeUHaz.mjs +5 -0
  256. package/dist/{renderer-snjhPaP-.mjs → renderer-DhGZ41hQ.mjs} +4 -4
  257. package/dist/{renderer-snjhPaP-.mjs.map → renderer-DhGZ41hQ.mjs.map} +1 -1
  258. package/dist/{renderer-BXGuCnCJ.mjs → renderer-DiZkZ0Pr.mjs} +4 -4
  259. package/dist/{renderer-BXGuCnCJ.mjs.map → renderer-DiZkZ0Pr.mjs.map} +1 -1
  260. package/dist/{renderer-BBjkX0Vt.mjs → renderer-Dm57W73p.mjs} +3 -3
  261. package/dist/renderer-Dt5uyJdZ.mjs +5 -0
  262. package/dist/{renderer-DwH5e3hx.mjs → renderer-DtFZbE1j.mjs} +3 -3
  263. package/dist/{renderer-Bxiiar50.mjs → renderer-DvHi_wXE.mjs} +4 -4
  264. package/dist/{renderer-Bxiiar50.mjs.map → renderer-DvHi_wXE.mjs.map} +1 -1
  265. package/dist/{renderer-DaLy1dFA.mjs → renderer-Dv_iRIyx.mjs} +21 -21
  266. package/dist/renderer-Dv_iRIyx.mjs.map +1 -0
  267. package/dist/renderer-DwLQAnaK.mjs +5 -0
  268. package/dist/{renderer-qVJdxbXB.mjs → renderer-DzON3wDZ.mjs} +4 -4
  269. package/dist/{renderer-qVJdxbXB.mjs.map → renderer-DzON3wDZ.mjs.map} +1 -1
  270. package/dist/{renderer-Dj54y3h3.mjs → renderer-GIlQ9lZq.mjs} +3 -3
  271. package/dist/{renderer-Dj54y3h3.mjs.map → renderer-GIlQ9lZq.mjs.map} +1 -1
  272. package/dist/{renderer-b0H3oDae.mjs → renderer-HKv5cBdp.mjs} +3 -3
  273. package/dist/{renderer-b0H3oDae.mjs.map → renderer-HKv5cBdp.mjs.map} +1 -1
  274. package/dist/renderer-IXQP2CWM.mjs +5 -0
  275. package/dist/renderer-K-Pkw63O.mjs +5 -0
  276. package/dist/renderer-MPw2KH9P.mjs +5 -0
  277. package/dist/{renderer-t1sXDiqm.mjs → renderer-QbSrnPZU.mjs} +3 -3
  278. package/dist/{renderer-t1sXDiqm.mjs.map → renderer-QbSrnPZU.mjs.map} +1 -1
  279. package/dist/renderer-RPu8RMav.mjs +5 -0
  280. package/dist/{renderer-Dp9FQKVP.mjs → renderer-TfeohPxt.mjs} +5 -5
  281. package/dist/{renderer-Dp9FQKVP.mjs.map → renderer-TfeohPxt.mjs.map} +1 -1
  282. package/dist/{renderer-BzTj_CeK.mjs → renderer-Y1kjtOto.mjs} +4 -4
  283. package/dist/{renderer-BzTj_CeK.mjs.map → renderer-Y1kjtOto.mjs.map} +1 -1
  284. package/dist/{renderer-LrAldCWN.mjs → renderer-YONlI6W-.mjs} +5 -5
  285. package/dist/{renderer-LrAldCWN.mjs.map → renderer-YONlI6W-.mjs.map} +1 -1
  286. package/dist/{renderer-tRi3KNoG.mjs → renderer-_J9JcCMR.mjs} +5 -5
  287. package/dist/{renderer-tRi3KNoG.mjs.map → renderer-_J9JcCMR.mjs.map} +1 -1
  288. package/dist/renderer-dCqxd5N2.mjs +5 -0
  289. package/dist/{renderer-CcmxR6Fy.d.mts → renderer-fG8Jy8Rx.d.mts} +2 -2
  290. package/dist/{renderer-CcmxR6Fy.d.mts.map → renderer-fG8Jy8Rx.d.mts.map} +1 -1
  291. package/dist/renderer-lhjmE-Fp.mjs +5 -0
  292. package/dist/{renderer-TiaHrU5z.mjs → renderer-mDBoz0zT.mjs} +5 -5
  293. package/dist/{renderer-TiaHrU5z.mjs.map → renderer-mDBoz0zT.mjs.map} +1 -1
  294. package/dist/{renderer-smP1F57W.mjs → renderer-pn52dzZz.mjs} +4 -4
  295. package/dist/{renderer-smP1F57W.mjs.map → renderer-pn52dzZz.mjs.map} +1 -1
  296. package/dist/renderer-u9cmBuvM.mjs +5 -0
  297. package/dist/{renderer-CNpq7rL-.mjs → renderer-uER8kn7f.mjs} +3 -3
  298. package/dist/{renderer-CNpq7rL-.mjs.map → renderer-uER8kn7f.mjs.map} +1 -1
  299. package/dist/{renderer-sMFJtt_H.mjs → renderer-uzU6dsdR.mjs} +3 -3
  300. package/dist/{renderer-sMFJtt_H.mjs.map → renderer-uzU6dsdR.mjs.map} +1 -1
  301. package/dist/renderer-vH12jcpz.mjs +5 -0
  302. package/dist/{renderer-oBgCEqaM.mjs → renderer-yNWYqvvq.mjs} +4 -4
  303. package/dist/{renderer-oBgCEqaM.mjs.map → renderer-yNWYqvvq.mjs.map} +1 -1
  304. package/dist/{renderer-Dy1xQOTu.mjs → renderer-zLi2qFGT.mjs} +4 -4
  305. package/dist/{renderer-Dy1xQOTu.mjs.map → renderer-zLi2qFGT.mjs.map} +1 -1
  306. package/dist/{renderers-CP8CFWDs-DWRjjeUg.mjs → renderers-CP8CFWDs-BufiCyn7.mjs} +4 -4
  307. package/dist/{renderers-CP8CFWDs-DWRjjeUg.mjs.map → renderers-CP8CFWDs-BufiCyn7.mjs.map} +1 -1
  308. package/dist/sam/index.d.mts +3 -3
  309. package/dist/sam/index.mjs +1 -1
  310. package/dist/signals/index.d.mts +7 -7
  311. package/dist/signals/index.mjs +8 -8
  312. package/dist/{signals-BfJ1aqpT.mjs → signals-tiho_9fp.mjs} +4 -4
  313. package/dist/{signals-BfJ1aqpT.mjs.map → signals-tiho_9fp.mjs.map} +1 -1
  314. package/dist/{src-BU6vm9sP-DFJ3phW0.mjs → src-BU6vm9sP-DdTHmCr4.mjs} +3 -3
  315. package/dist/{src-BU6vm9sP-DFJ3phW0.mjs.map → src-BU6vm9sP-DdTHmCr4.mjs.map} +1 -1
  316. package/dist/{src-D7M8VTzv.mjs → src-DrP7fGJX.mjs} +41 -16
  317. package/dist/{src-D7M8VTzv.mjs.map → src-DrP7fGJX.mjs.map} +1 -1
  318. package/dist/storage/index.d.mts +206 -0
  319. package/dist/storage/index.d.mts.map +1 -0
  320. package/dist/storage/index.mjs +3 -0
  321. package/package.json +67 -59
  322. package/dist/audio-afTBdB3U.d.mts.map +0 -1
  323. package/dist/cloud-COid1Ipa.mjs.map +0 -1
  324. package/dist/dist-B-OPY7Cs.mjs.map +0 -1
  325. package/dist/dist-BSqeChht.mjs.map +0 -1
  326. package/dist/index-BNClElng.d.mts.map +0 -1
  327. package/dist/index-CTv9ZieD.d.mts.map +0 -1
  328. package/dist/index-CdCrae-I.d.mts.map +0 -1
  329. package/dist/index-Fliv2Z-w.d.mts.map +0 -1
  330. package/dist/index-gOVtTm-P.d.mts.map +0 -1
  331. package/dist/preview-BUtt481A.mjs.map +0 -1
  332. package/dist/preview-engine/chunk-5SLBE2FX.mjs +0 -1
  333. package/dist/preview-engine/chunk-74J4DJQA.mjs +0 -1
  334. package/dist/preview-engine/chunk-KIPXBOYG.mjs +0 -1974
  335. package/dist/preview-engine/chunk-P4Y3PMJF.mjs +0 -1
  336. package/dist/preview-engine/chunk-SE2C623Z.mjs +0 -5
  337. package/dist/preview-engine/chunk-UKZVUYB6.mjs +0 -483
  338. package/dist/preview-engine/renderers-PSBTV2DO.mjs +0 -228
  339. package/dist/preview-engine/renderers-TEKHQHCL.mjs +0 -1
  340. package/dist/preview-engine/renderers-U3RLYFCN.mjs +0 -1
  341. package/dist/preview-engine/renderers-V5HM6PZ2.mjs +0 -1
  342. package/dist/preview-engine/renderers-ZGX3A3QZ.mjs +0 -1
  343. package/dist/program-Cvv_nb9T.mjs.map +0 -1
  344. package/dist/renderer--RJepuOy.mjs +0 -5
  345. package/dist/renderer-B1U6YRRV.d.mts +0 -117
  346. package/dist/renderer-B1U6YRRV.d.mts.map +0 -1
  347. package/dist/renderer-B4NFnaty.mjs +0 -5
  348. package/dist/renderer-B6g4ts0e.mjs +0 -5
  349. package/dist/renderer-BGoAr-tl.mjs +0 -5
  350. package/dist/renderer-BSOdBgdE.mjs +0 -5
  351. package/dist/renderer-BU6O0fpM.mjs +0 -5
  352. package/dist/renderer-BZ97iidG.mjs +0 -5
  353. package/dist/renderer-B_Klh1VT.mjs +0 -5
  354. package/dist/renderer-BaWQM1y-.mjs +0 -5
  355. package/dist/renderer-Bc0jvAj0.mjs +0 -5
  356. package/dist/renderer-Bd-NuU3M.mjs +0 -3
  357. package/dist/renderer-Bm077kZ6.mjs +0 -5
  358. package/dist/renderer-BtzE_e_Y.mjs +0 -5
  359. package/dist/renderer-C225eCAo.mjs +0 -5
  360. package/dist/renderer-CC4fhNup.mjs +0 -3
  361. package/dist/renderer-CLXdR4vz.mjs +0 -5
  362. package/dist/renderer-CszM8B3C.mjs +0 -5
  363. package/dist/renderer-Ctp8ZYfk.mjs +0 -5
  364. package/dist/renderer-CxQdSp8I.mjs +0 -5
  365. package/dist/renderer-D3YbHjRG.mjs.map +0 -1
  366. package/dist/renderer-D4GMRtDd.mjs +0 -6
  367. package/dist/renderer-DEsGIvTZ.mjs +0 -5
  368. package/dist/renderer-DIaUQ0EP.mjs +0 -5
  369. package/dist/renderer-DLjqcNzQ.mjs +0 -5
  370. package/dist/renderer-DMuQZa51.mjs +0 -5
  371. package/dist/renderer-DOgi2aF2.mjs +0 -5
  372. package/dist/renderer-DSh7cvi4.mjs +0 -5
  373. package/dist/renderer-DV1LVBBP.mjs +0 -5
  374. package/dist/renderer-DaLy1dFA.mjs.map +0 -1
  375. package/dist/renderer-Dhrj2bJz.mjs +0 -5
  376. package/dist/renderer-DoFUu1R4.mjs +0 -5
  377. package/dist/renderer-DsydX3cx.mjs +0 -5
  378. package/dist/renderer-DxVSoeYi.mjs +0 -5
  379. package/dist/renderer-Nc9bHb0n.mjs +0 -5
  380. package/dist/renderer-TFaSD2Db.mjs +0 -5
  381. package/dist/renderer-Z61ctppe.mjs +0 -5
  382. package/dist/renderer-awXf3sNS.mjs +0 -5
  383. package/dist/renderer-cHT28oYI.mjs +0 -5
  384. package/dist/renderer-g7Y3bFot.mjs +0 -5
  385. package/dist/renderer-qVv0OzVc.mjs +0 -5
  386. /package/dist/{chunk-W71TSjRM.mjs → chunk-DOL2i2sR.mjs} +0 -0
  387. /package/dist/{renderer-B6rZUBwE.mjs → renderer-Co2YqWX0.mjs} +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"file":"index-gOVtTm-P.d.mts","names":["pino","DestinationStream","VideoColorPrimaries","VideoFrame","VideoMatrixCoefficients","VideoTransferCharacteristics","RenderDiagnostic","VirtualMediaData","VideoSampleSource","preloadFonts","GPUDevice","Promise","FrameCostSample","ChunkPlanOptions","PlannedChunk","ChunkPlan","planChunks","StagingSlotStatus","VideoColorSpaceConfig","DmaStagingRingOptions","GPUTextureFormat","StagingSlotInfo","DmaStagingRing","GPUCommandEncoder","GPUTexture","Uint8Array","findNodesDir","discoverAndRegisterNodeRenderers","RenderPhase","RenderProgress","VideoQaOptions","QaSegment","QaIssue","VideoQaStats","AudioQaStats","VideoQaReport","FrameInspector","analyzeAudio","Float32Array","integratedLoudness","buildQaReport","formatQaReport","RenderSemaphore","renderSemaphore","FrameRange","HeadlessWebGPURenderer","Buffer","AbortSignal","HeadlessMediaRenderer","resetRenderCaches","resolveLogFormat","Record","textLogStream","rendererLogger","Logger","NODE_KINDS","NodeKind","NodeRendererExports","NodeManifest","NodeManifestError","Error","parseNodeManifest","loadNodeManifests","StitchOptions","StitchResult","stitchChunks","ZeroCopyWebCodecsPipelineOptions","WebCodecsPipelineStats","ZeroCopyWebCodecsPipeline"],"sources":["../../renderer/dist/index.d.mts","../src/renderer/framegrid.ts"],"sourcesContent":["import pino, { DestinationStream } from \"pino\";\nimport { VideoColorPrimaries, VideoFrame, VideoMatrixCoefficients, VideoTransferCharacteristics } from \"@napi-rs/webcodecs\";\nimport { RenderDiagnostic, VirtualMediaData } from \"@framefields/core\";\nimport { VideoSampleSource } from \"mediabunny\";\n\n//#region src/asset-preloader.d.ts\n\n/**\n * Preloads only the fonts (and emoji) a VirtualMediaData tree's text uses, so\n * typography renders correctly on Node.js without loading its media. Shared\n * with the browser preview; emoji need a font set with\n * `FontManager.registerEmojiFont()`.\n */\ndeclare function preloadFonts(media: VirtualMediaData, device?: GPUDevice | null): Promise<void>;\n//#endregion\n//#region src/chunk-plan.d.ts\n/**\n * Plans a render split across workers: how many chunks, and where each one\n * starts and ends. Pure arithmetic over measured frame costs, so it runs\n * anywhere (a worker's probe job, a coordinator, a local script).\n *\n * Every chunk pays a fixed cost before its first frame (worker start, fetch,\n * install, composition build). `workers: \"auto\"` adds workers only while each\n * chunk still spends at least `efficiency` of its time rendering, so a short\n * film stays on one worker and a long one spreads out until `maxWorkers`.\n * Chunks are cut at equal render cost, not equal frame counts, so a heavy\n * stretch (3D, vision) gets shorter chunks.\n */\n/** A measured render time for one frame. */\ninterface FrameCostSample {\n frame: number;\n ms: number;\n}\ninterface ChunkPlanOptions {\n /** Frames in the composition. */\n totalFrames: number;\n /** Measured frame times; between samples the cost is interpolated linearly. */\n samples: readonly FrameCostSample[];\n /** A worker count, or `\"auto\"` to pick one from the cost. Default 1. */\n workers?: number | \"auto\";\n /** Upper bound for `\"auto\"` (and for a number). Default 10. */\n maxWorkers?: number;\n /** Fixed cost each chunk pays before rendering, in seconds. Default 90 (a cold start). */\n overheadSec?: number;\n /**\n * `\"auto\"` only: the smallest share of a chunk's time spent rendering,\n * between 0 and 1. Default 0.85: each chunk renders at least\n * `overheadSec × 0.85 / 0.15` (about 5.7× the overhead).\n */\n efficiency?: number;\n /** No chunk is shorter than this many frames. Default 60. */\n minChunkFrames?: number;\n}\ninterface PlannedChunk {\n /** First frame (inclusive). */\n start: number;\n /** Last frame (exclusive). */\n end: number;\n /** Estimated render time of its frames, in seconds. */\n workSec: number;\n}\ninterface ChunkPlan {\n workers: number;\n /** Estimated render time of the whole composition on one worker, without overhead. */\n workSec: number;\n /** Estimated wall time with this plan: overhead plus the longest chunk. */\n etaSec: number;\n chunks: PlannedChunk[];\n}\ndeclare function planChunks(options: ChunkPlanOptions): ChunkPlan;\n//#endregion\n//#region src/dma-staging-ring.d.ts\ntype StagingSlotStatus = \"idle\" | \"rendering\" | \"mapping\" | \"mapped\";\ninterface VideoColorSpaceConfig {\n readonly primaries?: VideoColorPrimaries;\n readonly transfer?: VideoTransferCharacteristics;\n readonly matrix?: VideoMatrixCoefficients;\n readonly fullRange?: boolean;\n}\ninterface DmaStagingRingOptions {\n readonly width: number;\n readonly height: number;\n readonly capacity?: number;\n readonly format?: GPUTextureFormat;\n}\ninterface StagingSlotInfo {\n readonly index: number;\n readonly status: StagingSlotStatus;\n readonly inFlightFrame: number | null;\n}\n/**\n * Pipelined Asynchronous DMA Staging Ring for WebGPU.\n *\n * Implements double-buffered (or N-buffered) GPU staging buffers to overlap\n * GPU render passes with asynchronous DMA host transfers and WebCodecs hardware encoding.\n * Eliminates GPU stalls by allowing Frame i+1 to execute on the GPU while Frame i\n * is asynchronously mapped and consumed by the hardware video encoder.\n */\ndeclare class DmaStagingRing {\n readonly width: number;\n readonly height: number;\n readonly capacity: number;\n readonly bytesPerRow: number;\n readonly unpaddedBytesPerRow: number;\n readonly bufferSize: number;\n readonly format: GPUTextureFormat;\n readonly device: GPUDevice;\n private readonly slots;\n private isDestroyed;\n constructor(device: GPUDevice, options: DmaStagingRingOptions);\n /**\n * Prepares the staging buffer slot for writing by ensuring any prior mapping\n * from a previous cycle has been completed and unmapped.\n */\n prepareSlot(frameIndex: number): Promise<void>;\n /**\n * Records a GPU-to-buffer DMA copy command into the provided command encoder.\n */\n stageTexture(encoder: GPUCommandEncoder, texture: GPUTexture, frameIndex: number): void;\n /**\n * Initiates asynchronous DMA mapping on the slot for the specified frame.\n * Must be called immediately after submitting the command encoder to the device queue.\n */\n scheduleMap(frameIndex: number): Promise<void>;\n /**\n * Awaits asynchronous mapping and extracts pixels into a tightly packed Uint8Array.\n * The slot remains in the \"mapped\" state until releaseFrame() is called.\n */\n readFramePixels(frameIndex: number): Promise<Uint8Array>;\n /**\n * Awaits asynchronous mapping, extracts pixels, and immediately unmaps the staging buffer,\n * returning the slot to \"idle\" state so it can be reused immediately.\n */\n readAndReleaseFrame(frameIndex: number): Promise<Uint8Array>;\n /**\n * Directly reads the mapped staging buffer into a WebCodecs VideoFrame,\n * bypassing redundant intermediate JS allocations when row pitches align,\n * and immediately unmaps the buffer.\n */\n readFrameToVideoFrame(frameIndex: number, timestampUs: number, colorSpace?: VideoColorSpaceConfig, /** Sees the tightly packed RGBA pixels while they are mapped; must not keep them. */\n inspect?: (rgba: Uint8Array) => void): Promise<VideoFrame>;\n /**\n * Unmaps the staging buffer for the specified frame and marks the slot idle.\n */\n releaseFrame(frameIndex: number): void;\n /**\n * Returns current status info for each slot in the ring.\n */\n getSlotInfos(): readonly StagingSlotInfo[];\n /**\n * Destroys all staging buffers and releases resources.\n */\n destroy(): void;\n private checkDestroyed;\n}\n//#endregion\n//#region src/dynamic-node-discovery.d.ts\n/** Locates the workspace `nodes/` directory by climbing up from this file. */\ndeclare function findNodesDir(startDir?: string): string | null;\n/**\n * Registers the built-in node renderers, then any extra nodes found in the\n * directory named by `FRAMEFIELDS_NODES_DIR` (local node development). A node in\n * that directory overrides the built-in renderer for the same op.\n */\ndeclare function discoverAndRegisterNodeRenderers(): Promise<void>;\n//#endregion\n//#region src/render-reporter.d.ts\n/**\n * Stages of `renderVideo`, in order. Audio-only renders skip \"render\";\n * \"convert\" runs only for GIF output.\n */\ntype RenderPhase = \"prepare\" | \"render\" | \"audio\" | \"audio-encode\" | \"mux\" | \"convert\" | \"done\";\n/** Progress reported by `renderVideo({ onProgress })`. */\ninterface RenderProgress {\n /**\n * \"prepare\" loads fonts and warms the pipeline, \"render\" draws and encodes\n * frames, \"audio\" mixes the soundtrack, \"audio-encode\" encodes it, \"mux\"\n * writes the container, \"convert\" makes the GIF, \"done\" is the last event.\n */\n phase: RenderPhase;\n /** Frames finished so far (1-based); `totalFrames` once frames are done. */\n frame: number;\n totalFrames: number;\n /** Time since the render started. */\n elapsedMs: number;\n /** Share of the current phase done (0–1), when it can be measured. */\n phaseProgress?: number;\n /** Frames per second so far (\"render\" only). */\n fps?: number;\n /** Estimated seconds until the frames are done (\"render\" only). */\n etaSec?: number;\n /** What the phase is working on, e.g. \"3/7 sources: music.mp3\". */\n detail?: string;\n}\n//#endregion\n//#region src/video-qa.d.ts\n/**\n * Quality checks run on a video while it renders: frames are measured as\n * they are read back for the encoder and audio as it is mixed, so the report\n * describes exactly what was encoded without decoding the file again.\n */\ninterface VideoQaOptions {\n /** Luma (0-1) at or below which a pixel counts as black. Default 0.1. */\n readonly blackPixelThreshold?: number;\n /** Share of black pixels that makes a frame black. Default 0.98. */\n readonly blackFrameRatio?: number;\n /** Shortest run of black frames reported. Default 0.5 s. */\n readonly minBlackSec?: number;\n /** Shortest run of unchanging frames reported. Default 2 s. */\n readonly minFrozenSec?: number;\n /** Level (dBFS, 100 ms windows) below which audio counts as silent. Default -60. */\n readonly silenceDbfs?: number;\n /** Shortest silent stretch reported. Default 2 s. */\n readonly minSilenceSec?: number;\n /** Integrated loudness to hit, e.g. -14 for most streaming platforms. Unchecked when unset. */\n readonly targetLufs?: number;\n /** Allowed distance from `targetLufs`, in LU. Default 1. */\n readonly lufsTolerance?: number;\n}\ninterface QaSegment {\n readonly startSec: number;\n readonly endSec: number;\n}\ninterface QaIssue {\n readonly severity: \"error\" | \"warning\" | \"info\";\n /** Stable machine-readable kind, e.g. \"black_frames\", \"audio_clipping\" */\n readonly code: string;\n readonly message: string;\n readonly startSec?: number;\n readonly endSec?: number;\n readonly layerId?: string;\n}\ninterface VideoQaStats {\n readonly expectedFrames: number;\n readonly encodedFrames: number;\n readonly fps: number;\n readonly width: number;\n readonly height: number;\n /** Average luma over the whole video, 0-1 */\n readonly meanLuma: number;\n readonly blackSegments: readonly QaSegment[];\n readonly frozenSegments: readonly QaSegment[];\n}\ninterface AudioQaStats {\n readonly sampleRate: number;\n readonly channels: number;\n readonly durationSec: number;\n /** Highest sample, dBFS (sample peak, not true peak) */\n readonly peakDbfs: number;\n /** Mean level over the whole track, dBFS */\n readonly rmsDbfs: number;\n /** ITU-R BS.1770 integrated loudness; null when the track is silent */\n readonly integratedLufs: number | null;\n /** Samples at or beyond full scale */\n readonly clippedSamples: number;\n readonly silentSegments: readonly QaSegment[];\n}\ninterface VideoQaReport {\n /** No errors or warnings (info issues do not fail a render) */\n readonly passed: boolean;\n readonly video?: VideoQaStats;\n readonly audio?: AudioQaStats;\n readonly issues: readonly QaIssue[];\n}\n/** Measures frames handed to it in order, as RGBA bytes. */\ndeclare class FrameInspector {\n private readonly blackLuma;\n private readonly blackRatio;\n private readonly black;\n private readonly sameAsPrevious;\n private lumaSum;\n private previous;\n constructor(options?: VideoQaOptions);\n get frameCount(): number;\n inspect(rgba: Uint8Array, width: number, height: number): void;\n stats(expectedFrames: number, encodedFrames: number, fps: number, width: number, height: number, options?: VideoQaOptions): VideoQaStats;\n}\n/** Measures a mixed audio track (one Float32Array per channel). */\ndeclare function analyzeAudio(channels: readonly Float32Array[], sampleRate: number, options?: VideoQaOptions): AudioQaStats;\n/**\n * ITU-R BS.1770-4 integrated loudness (LUFS): K-weighted mean square in\n * 400 ms blocks with 75% overlap, gated at -70 LUFS and then at 10 LU below\n * the mean of what passed. Channels weigh 1 (stereo and mono layouts).\n */\ndeclare function integratedLoudness(channels: readonly Float32Array[], sampleRate: number): number | null;\n/** Turn measurements and document diagnostics into a report with issues. */\ndeclare function buildQaReport(parts: {\n video?: VideoQaStats;\n audio?: AudioQaStats;\n diagnostics?: readonly RenderDiagnostic[];\n options?: VideoQaOptions;\n}): VideoQaReport;\n/** A short human- and agent-readable summary of a report. */\ndeclare function formatQaReport(report: VideoQaReport): string;\n//#endregion\n//#region src/headless-webgpu-renderer.d.ts\ndeclare class RenderSemaphore {\n private _running;\n private readonly _max;\n private readonly _queue;\n constructor(max: number);\n acquire(timeoutMs?: number): Promise<void>;\n release(): void;\n get stats(): {\n active: number;\n queued: number;\n max: number;\n };\n}\ndeclare const renderSemaphore: RenderSemaphore;\n/** Composition frames `start` (inclusive) to `end` (exclusive). */\ninterface FrameRange {\n start: number;\n end: number;\n}\ndeclare class HeadlessWebGPURenderer {\n private static isInitialized;\n private static initPromise;\n private static initFailureCount;\n private static readonly MAX_INIT_FAILURES;\n private static nextRetryAt;\n static initialize(): Promise<void>;\n renderImage(virtualMedia: VirtualMediaData, frame?: number, fps?: number, options?: {\n renderId?: string;\n /** \"rgba\" skips PNG encoding and returns width*height*4 raw pixels. */\n format?: \"png\" | \"rgba\";\n }): Promise<Buffer>;\n /**\n * Renders a single frame to a PNG buffer at a specified timestamp in milliseconds or frame index.\n */\n renderFrame(source: VirtualMediaData | {\n toVirtualMedia(): VirtualMediaData;\n }, options?: {\n atMs?: number;\n frame?: number;\n fps?: number;\n /** Reuse compiled state across calls; see `renderImage`. */\n renderId?: string;\n /** \"rgba\" returns raw width*height*4 pixels instead of a PNG. */\n format?: \"png\" | \"rgba\";\n }): Promise<Buffer>;\n /**\n * Measures what frames cost to render across the timeline, for planning a\n * split render (`planChunks`). At `samples` evenly spaced points (default\n * 12) it renders three consecutive frames and times the last two, so\n * effects that carry state (trails, particles) are caught up and their\n * replay is not counted. Encoding is not included.\n */\n probeRender(source: VirtualMediaData | {\n toVirtualMedia(): VirtualMediaData;\n }, options?: {\n samples?: number;\n signal?: AbortSignal;\n /** Called after each sample point with how many are done. */\n onSample?: (done: number, total: number) => void;\n }): Promise<{\n totalFrames: number;\n fps: number;\n durationMs: number;\n samples: FrameCostSample[];\n }>;\n /**\n * Mixes the composition's full audio timeline (clips, effects, synthesized\n * tracks) to stereo PCM, exactly as `renderVideo` muxes it.\n */\n renderAudio(virtualMedia: VirtualMediaData, options?: {\n fps?: number;\n sampleRate?: number;\n }): Promise<{\n channels: Float32Array[];\n sampleRate: number;\n }>;\n renderLut(virtualMedia: VirtualMediaData, frame?: number, fps?: number): Promise<Buffer>;\n private resolveMediaDuration;\n renderVideo(virtualMedia: VirtualMediaData, options?: {\n codec?: string;\n audioCodec?: string;\n quality?: string;\n concurrency?: number;\n /**\n * Depth of the DMA staging ring (default 2, clamped 2–8). A deeper\n * ring overlaps more encoder latency at the cost of VRAM\n * (~8.3MB/slot at 1080p RGBA). Also settable via\n * `FRAMEFIELDS_RING_CAPACITY`.\n */\n ringCapacity?: number;\n /**\n * Measure the output while it renders (black and frozen frames,\n * dropped frames, loudness, clipping, silence, document warnings)\n * and return the findings as `qa`. `true` uses default thresholds.\n */\n qa?: boolean | VideoQaOptions;\n /**\n * Aborts the render between frames; the promise rejects with\n * `signal.reason` and the partial output is deleted.\n */\n signal?: AbortSignal;\n /**\n * Called as the render moves through its phases: \"prepare\", each\n * frame (\"render\"), \"audio\" (mixing, per clip), \"audio-encode\",\n * \"mux\", \"convert\" (GIF only) and \"done\".\n */\n onProgress?: (progress: RenderProgress) => void;\n /**\n * Renders only composition frames `start` (inclusive) to `end`\n * (exclusive), as one chunk of a render split across workers. The\n * chunk's timestamps start at 0 and its first frame is a keyframe,\n * so `stitchChunks` joins chunks without re-encoding. Effects that\n * carry state between frames rebuild it before `start`.\n */\n frames?: FrameRange;\n /**\n * `false` writes no audio track, as for chunks: the timeline's audio\n * is exported once with `codec: \"aac\"` and added by `stitchChunks`.\n */\n audio?: boolean;\n }): Promise<{\n filePath: string;\n cleanup: () => Promise<void>;\n qa?: VideoQaReport;\n }>;\n}\ndeclare const HeadlessMediaRenderer: typeof HeadlessWebGPURenderer;\ntype HeadlessMediaRenderer = HeadlessWebGPURenderer;\n//#endregion\n//#region src/job-reset.d.ts\n/**\n * Drops every cache keyed by a file path or URL: media inputs and decoders,\n * textures, LUTs, fonts and downloaded font files. A long-lived worker calls this between jobs,\n * because the next checkout may hold different bytes at the same path. The\n * GPU device and compiled pipelines stay warm.\n */\ndeclare function resetRenderCaches(): void;\n//#endregion\n//#region src/logger.d.ts\n/**\n * \"json\" (pino lines, for log collectors) or \"text\" (one short line each, for\n * people and agents reading a terminal). `FRAMEFIELDS_LOG_FORMAT` picks one;\n * otherwise production is JSON and everything else text.\n */\ndeclare function resolveLogFormat(vars?: Record<string, string | undefined> | undefined): \"json\" | \"text\";\n/**\n * Turns pino's JSON lines into `[framefields] Rendered 40% (…)`: the message\n * without its `[Component]` prefix, the level only for warnings and errors,\n * and an error's message (its stack reaches the caller with the throw).\n */\ndeclare function textLogStream(write?: (line: string) => void): DestinationStream;\ndeclare const rendererLogger: pino.Logger<never, boolean>;\n//#endregion\n//#region src/node-manifest.d.ts\n/**\n * The `framefields` block of a node package's `package.json`.\n *\n * This is the single declaration of a node's op: renderer discovery, the\n * effects generator (`scripts/generate-effects.ts`) and the skill catalog all\n * read it from here. There is deliberately no fallback that derives an op from\n * the package name. See `spec/sync-node.md` §5.1.\n */\ndeclare const NODE_KINDS: readonly [\"effect\", \"audio\", \"source\", \"signal\", \"utility\"];\ntype NodeKind = (typeof NODE_KINDS)[number];\ninterface NodeRendererExports {\n development?: string;\n import?: string;\n default?: string;\n}\ninterface NodeManifest {\n /** Directory name under `nodes/`, e.g. `node-apply-lut`. */\n dir: string;\n /** Absolute path of the node package. */\n path: string;\n /** npm package name, e.g. `@framefields/node-lut`. */\n packageName: string;\n /** Canonical op literal used in the operation AST and the renderer registry. */\n type: string;\n kind: NodeKind;\n /** Legacy ops that resolve to the same renderer. */\n aliases: readonly string[];\n /** Named zod schema export of `src/shared/config.ts`. */\n schema?: string;\n /** Authoring class name. Defaults to `type`. */\n className?: string;\n /** Key on `Effect.*`. Defaults to `className` with a lower-cased first letter. */\n factory?: string;\n /** A hand-written subclass exists under `effects/custom/`. */\n custom: boolean;\n /** `false` when the package opts out of discovery. */\n enabled: boolean;\n rendererExports?: NodeRendererExports;\n}\ndeclare class NodeManifestError extends Error {\n readonly problems: readonly string[];\n constructor(problems: readonly string[]);\n}\n/**\n * Validates one parsed `package.json`. Returns the manifest, or the list of\n * problems that make it unusable. Never throws.\n */\ndeclare function parseNodeManifest(pkg: unknown, dir: string, nodePath?: string): {\n manifest?: NodeManifest;\n problems: string[];\n};\n/**\n * Reads every `nodes/node-*` package and returns its manifest, sorted by\n * directory name. Throws a single {@link NodeManifestError} listing every\n * problem: a missing or malformed block, or an op/alias claimed twice.\n */\ndeclare function loadNodeManifests(nodesDir: string): NodeManifest[];\n//#endregion\n//#region src/stitch.d.ts\ninterface StitchOptions {\n /** Chunk files in timeline order, each a `renderVideo({ frames, audio: false })` output. */\n chunks: readonly string[];\n /**\n * An audio-only render of the whole timeline (`renderVideo({ codec: \"aac\" })`,\n * or `\"opus\"` for WebM chunks), copied in as the audio track.\n */\n audio?: string;\n outputPath: string;\n /** Stops between packets; the promise rejects and no file is left behind. */\n signal?: AbortSignal;\n}\ninterface StitchResult {\n filePath: string;\n /** Video frames written, across all chunks. */\n frames: number;\n /** End of the video track, in seconds. */\n durationSec: number;\n}\ndeclare function stitchChunks(options: StitchOptions): Promise<StitchResult>;\n//#endregion\n//#region src/zero-copy-webcodecs-pipeline.d.ts\ninterface ZeroCopyWebCodecsPipelineOptions {\n readonly width: number;\n readonly height: number;\n readonly fps: number;\n readonly videoSource: VideoSampleSource;\n readonly ringCapacity?: number;\n readonly colorSpace?: VideoColorSpaceConfig;\n readonly directCanvasSource?: unknown;\n /**\n * Called with each frame's RGBA pixels, in frame order, just before it is\n * encoded (staging-ring path only). The buffer is reused: read, don't keep.\n */\n readonly onFrame?: (rgba: Uint8Array, frameIndex: number) => void;\n}\ninterface WebCodecsPipelineStats {\n readonly totalFramesEnqueued: number;\n readonly totalFramesEncoded: number;\n readonly averageGpuSubmitTimeMs: number;\n readonly averageEncodeTimeMs: number;\n readonly averageCycleTimeMs: number;\n readonly maxCycleTimeMs: number;\n readonly minCycleTimeMs: number;\n readonly throughputFps: number;\n readonly ringCapacity: number;\n readonly isDirectZeroCopy: boolean;\n}\n/**\n * High-performance hardware video encoding pipeline integrating WebGPU textures with WebCodecs.\n *\n * Provides:\n * 1. Direct hardware acceleration when running with WebCodecs / OffscreenCanvas.\n * 2. Pipelined asynchronous double-buffered DMA staging ring for headless Node.js (Dawn native),\n * allowing GPU Frame i+1 rendering to overlap concurrently with Frame i hardware encoding.\n */\ndeclare class ZeroCopyWebCodecsPipeline {\n readonly width: number;\n readonly height: number;\n readonly fps: number;\n readonly ringCapacity: number;\n private readonly device;\n private readonly videoSource;\n private readonly stagingRing;\n private readonly colorSpace?;\n private readonly directCanvasSource?;\n private readonly onFrame?;\n private readonly inFlightFrames;\n private readonly cycleStartTimes;\n private totalFramesEnqueued;\n private totalFramesEncoded;\n private totalGpuSubmitTimeMs;\n private totalEncodeTimeMs;\n private totalCycleTimeMs;\n private maxCycleTimeMs;\n private minCycleTimeMs;\n private pipelineStartTime;\n constructor(device: GPUDevice, options: ZeroCopyWebCodecsPipelineOptions);\n /**\n * Returns true if direct zero-copy from an OffscreenCanvas or texture is active.\n */\n canDirectZeroCopy(): boolean;\n /**\n * Enqueues a rendered GPU texture frame into the pipeline.\n *\n * Uses double-buffered DMA staging: if the staging ring has reached capacity,\n * the oldest in-flight frame is asynchronously consumed and added to the video encoder\n * before staging the new frame, guaranteeing constant bounded VRAM usage and zero GPU stalls.\n */\n enqueueFrame(frameIndex: number, targetTexture: GPUTexture, encoder: GPUCommandEncoder): Promise<void>;\n /**\n * Drains all remaining in-flight frames in the pipeline, ensuring every enqueued\n * frame has been encoded and added to the video sample source.\n */\n drain(): Promise<void>;\n /**\n * Returns runtime throughput, latency, and pipeline statistics.\n */\n getStats(): WebCodecsPipelineStats;\n /**\n * Destroys the pipeline and releases all staging buffers.\n */\n destroy(): void;\n private consumeAndEncode;\n}\n//#endregion\nexport { AudioQaStats, ChunkPlan, ChunkPlanOptions, DmaStagingRing, DmaStagingRingOptions, FrameCostSample, FrameInspector, FrameRange, HeadlessMediaRenderer, HeadlessWebGPURenderer, NODE_KINDS, NodeKind, NodeManifest, NodeManifestError, NodeRendererExports, PlannedChunk, QaIssue, QaSegment, type RenderPhase, type RenderProgress, StagingSlotInfo, StagingSlotStatus, StitchOptions, StitchResult, VideoColorSpaceConfig, VideoQaOptions, VideoQaReport, VideoQaStats, WebCodecsPipelineStats, ZeroCopyWebCodecsPipeline, ZeroCopyWebCodecsPipelineOptions, analyzeAudio, buildQaReport, discoverAndRegisterNodeRenderers, findNodesDir, formatQaReport, integratedLoudness, loadNodeManifests, parseNodeManifest, planChunks, preloadFonts, renderSemaphore, rendererLogger, resetRenderCaches, resolveLogFormat, stitchChunks, textLogStream };\n//# sourceMappingURL=index.d.mts.map"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"preview-BUtt481A.mjs","names":["url","resolve"],"sources":["../src/preview/bundle.ts","../src/preview/notes.ts","../src/preview/page.ts","../src/preview/index.ts"],"sourcesContent":["/**\n * The preview page runs the project's composition code and renders it with\n * WebGPU. Two bundles make that work:\n *\n * - The engine (`buildEngine`): framefields and its browser player, built once\n * for the browser. Published packages ship it in `dist/preview-engine`;\n * running from source, it is built on first use.\n * - The project (`bundleProject`): only the user's own files, built on each\n * preview. Its `framefields` imports point at the engine's files, so the\n * composition and the player share one engine (signals, clock, caches).\n *\n * Node APIs that compositions use at build time are shimmed: `path`, `url`,\n * `os`, `import.meta.url` (each file keeps its own `file://` URL), and `fs`\n * reads, which fetch the file from the preview server. Absolute file paths\n * double as URLs there, so assets referenced by path load unchanged.\n * Node-only packages become stubs that throw when used.\n */\nimport fs from \"node:fs\";\nimport { createRequire, isBuiltin } from \"node:module\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport type { BuildOptions, PluginBuild } from \"esbuild\";\n\nconst HERE = path.dirname(fileURLToPath(import.meta.url));\n// Spelled in two parts so this file survives its own rewrite.\nconst META_URL = [\"import\", \"meta\", \"url\"].join(\".\");\nconst require = createRequire(import.meta.url);\n\n/** URL prefix the server serves the engine's files under. */\nexport const ENGINE_URL = \"/@framefields/engine\";\n\n/** Public entry points a composition may import, and their sources. */\nconst ENTRIES: Record<string, string> = {\n\tframefields: \"index.ts\",\n\t\"framefields/effects\": \"effects/index.ts\",\n\t\"framefields/audio\": \"audio/index.ts\",\n\t\"framefields/signals\": \"signals/index.ts\",\n\t\"framefields/fonts\": \"fonts/index.ts\",\n\t// Installed components import their toolkit from here (`framefields add`).\n\t\"framefields/components\": \"components/index.ts\",\n};\n\n/** Node built-ins with a browser version (shim file names); others are stubbed. */\nconst SHIMS: Record<string, string> = {\n\tpath: \"path\",\n\t\"path/posix\": \"path\",\n\turl: \"url\",\n\tos: \"os\",\n\tfs: \"fs\",\n\t\"fs/promises\": \"fs-promises\",\n\tmodule: \"module\",\n};\n\n/** Packages that only work in Node; the browser never needs them to draw. */\nconst NODE_ONLY = new Set([\n\t\"sharp\",\n\t\"skia-canvas\",\n\t\"node-av\",\n\t\"onnxruntime-node\",\n\t\"@resvg/resvg-js\",\n\t\"@napi-rs/webcodecs\",\n\t\"@mediabunny/server\",\n\t\"@mediabunny/mp3-encoder\",\n\t\"webgpu\",\n\t\"pino\",\n\t\"pino-pretty\",\n\t\"dotenv\",\n\t\"esbuild\",\n\t\"@framefields/renderer\",\n\t\"framefields/renderer\",\n\t\"framefields/preview\",\n]);\n\n// Reported as an ES module so named imports (`import { homedir } from \"os\"`)\n// read through the proxy instead of being copied off it.\nconst STUB = `\nconst stub = () => new Proxy(function () {}, {\n\tget: (_t, k) => (k === \"__esModule\" ? true : k === Symbol.toPrimitive ? () => \"\" : stub()),\n\tapply: () => stub(),\n\tconstruct: () => stub(),\n});\nmodule.exports = stub();\n`;\n\n/**\n * Builds framefields for the browser into `outDir`: one module per public entry,\n * the player and the shims, with shared code split into chunks (node renderers\n * load only when a composition uses them).\n */\nexport async function buildEngine(outDir: string): Promise<void> {\n\tconst { build } = await import(\"esbuild\");\n\tconst src = path.resolve(HERE, \"..\");\n\tconst shims = path.join(HERE, \"shims\");\n\tconst entryPoints: Record<string, string> = {\n\t\tplayer: path.join(HERE, \"player.ts\"),\n\t};\n\tfor (const [spec, file] of Object.entries(ENTRIES)) {\n\t\tentryPoints[engineName(spec)] = path.join(src, file);\n\t}\n\tfor (const name of new Set(Object.values(SHIMS))) {\n\t\tentryPoints[`shims/${name}`] = path.join(shims, `${name}.ts`);\n\t}\n\n\tawait fs.promises.rm(outDir, { recursive: true, force: true });\n\tawait build({\n\t\t...BROWSER,\n\t\tentryPoints,\n\t\toutdir: outDir,\n\t\toutExtension: { \".js\": \".mjs\" },\n\t\tsplitting: true,\n\t\tminify: true,\n\t\tconditions: [\"development\", \"browser\", \"import\"],\n\t\tdefine: { \"process.env.NODE_ENV\": '\"production\"', global: \"globalThis\" },\n\t\tinject: [path.join(shims, \"process.ts\")],\n\t\tplugins: [\n\t\t\t{\n\t\t\t\tname: \"framefields-engine\",\n\t\t\t\tsetup(b) {\n\t\t\t\t\tb.onResolve({ filter: /^framefields-preview:renderers$/ }, () => ({\n\t\t\t\t\t\tpath: builtinRenderersModule(),\n\t\t\t\t\t}));\n\t\t\t\t\tresolveNode(b, (name) => ({ path: path.join(shims, `${name}.ts`) }));\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t});\n}\n\nexport interface ProjectBundle {\n\tcode: string;\n\t/** The project's source files, to watch for changes. */\n\tinputs: string[];\n}\n\n/** Bundles the module that builds the composition, and what it imports. */\nexport async function bundleProject(options: {\n\t/** Module that exports the composition factory. */\n\tentry: string;\n\t/** Name of the export; a function returning a Composition (or one itself). */\n\texportName: string;\n\t/** Where cached vision models are, and where onnxruntime-web loads from. */\n\tmodelsDir: string;\n\tortBase: string;\n}): Promise<ProjectBundle> {\n\tconst { build } = await import(\"esbuild\");\n\tconst entry = path.resolve(options.entry);\n\tconst fromEngine = (file: string) => ({\n\t\tpath: `${ENGINE_URL}/${file}.mjs`,\n\t\texternal: true,\n\t});\n\tconst settings = { modelsDir: options.modelsDir, ortBase: options.ortBase };\n\n\tconst result = await build({\n\t\t...BROWSER,\n\t\tentryPoints: [\"framefields-preview:entry\"],\n\t\twrite: false,\n\t\tmetafile: true,\n\t\tsourcemap: \"inline\",\n\t\tplugins: [\n\t\t\t{\n\t\t\t\tname: \"framefields-project\",\n\t\t\t\tsetup(b) {\n\t\t\t\t\tb.onResolve({ filter: /^framefields-preview:entry$/ }, () => ({\n\t\t\t\t\t\tpath: \"entry\",\n\t\t\t\t\t\tnamespace: \"framefields-preview\",\n\t\t\t\t\t}));\n\t\t\t\t\tb.onLoad({ filter: /.*/, namespace: \"framefields-preview\" }, () => ({\n\t\t\t\t\t\tcontents: [\n\t\t\t\t\t\t\t`import { startPlayer } from ${JSON.stringify(`${ENGINE_URL}/player.mjs`)};`,\n\t\t\t\t\t\t\t`import * as mod from ${JSON.stringify(entry)};`,\n\t\t\t\t\t\t\t`startPlayer(mod, ${JSON.stringify(options.exportName)}, ${JSON.stringify(settings)});`,\n\t\t\t\t\t\t].join(\"\\n\"),\n\t\t\t\t\t\tresolveDir: path.dirname(entry),\n\t\t\t\t\t\tloader: \"js\",\n\t\t\t\t\t}));\n\t\t\t\t\tb.onResolve({ filter: /^\\/@framefields\\/engine\\// }, (args) => ({\n\t\t\t\t\t\tpath: args.path,\n\t\t\t\t\t\texternal: true,\n\t\t\t\t\t}));\n\t\t\t\t\tb.onResolve({ filter: /^framefields(\\/|$)/ }, (args) =>\n\t\t\t\t\t\tENTRIES[args.path] ? fromEngine(engineName(args.path)) : undefined,\n\t\t\t\t\t);\n\t\t\t\t\tresolveNode(b, (name) => fromEngine(`shims/${name}`));\n\t\t\t\t\trewriteMetaUrl(b);\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t});\n\n\tconst inputs = Object.keys(result.metafile?.inputs ?? {})\n\t\t.filter((p) => !p.includes(\":\") && !p.includes(\"node_modules\"))\n\t\t.map((p) => path.resolve(p));\n\treturn { code: result.outputFiles[0].text, inputs };\n}\n\nlet engineBuild: Promise<string> | undefined;\n\n/**\n * The prebuilt engine shipped beside a compiled bundle, if there is one. The\n * bundle lands either at the package's dist root or beside dist/preview,\n * depending on how the bundler splits it, so both are checked.\n */\nexport function prebuiltEngineDir(here: string): string | undefined {\n\treturn [here, path.join(here, \"..\")]\n\t\t.map((base) => path.join(base, \"preview-engine\"))\n\t\t.find((dir) => fs.existsSync(path.join(dir, \"player.mjs\")));\n}\n\n/**\n * The engine to serve: prebuilt in the package's dist, or, running from\n * source, built once per process (so engine edits show without a rebuild).\n */\nexport function engineDir(): Promise<string> {\n\t// From source HERE is src/preview, so this misses and the engine is built.\n\tconst prebuilt = prebuiltEngineDir(HERE);\n\tif (prebuilt) {\n\t\treturn Promise.resolve(prebuilt);\n\t}\n\tengineBuild ??= (async () => {\n\t\tconst dir = path.join(\n\t\t\tos.tmpdir(),\n\t\t\t`framefields-preview-engine-${process.pid}`,\n\t\t);\n\t\tawait buildEngine(dir);\n\t\treturn dir;\n\t})();\n\tengineBuild.catch(() => {\n\t\tengineBuild = undefined;\n\t});\n\treturn engineBuild;\n}\n\n/** Where vision models are cached, as `@framefields/vision` resolves it. */\nexport function visionModelsDir(): string {\n\treturn process.env.FRAMEFIELDS_MODELS_DIR\n\t\t? path.resolve(process.env.FRAMEFIELDS_MODELS_DIR)\n\t\t: path.join(os.homedir(), \".cache\", \"framefields\", \"models\");\n}\n\nconst ORT_WEB_VERSION = \"1.30.0\";\n\n/**\n * onnxruntime-web, for vision effects: the project's own install when it has\n * one (served from disk), otherwise a pinned CDN build. Loaded only when a\n * vision effect runs, so framefields doesn't ship its WebAssembly.\n */\nexport function onnxRuntimeWeb(fromDir: string): {\n\tbase: string;\n\tdir?: string;\n} {\n\tfor (const from of [fromDir, HERE]) {\n\t\ttry {\n\t\t\tconst main = createRequire(path.join(from, \"noop.js\")).resolve(\n\t\t\t\t\"onnxruntime-web\",\n\t\t\t);\n\t\t\tconst dir = path.dirname(main);\n\t\t\treturn { base: `/@fs${dir}/`, dir };\n\t\t} catch {}\n\t}\n\treturn {\n\t\tbase: `https://cdn.jsdelivr.net/npm/onnxruntime-web@${ORT_WEB_VERSION}/dist/`,\n\t};\n}\n\n// ── shared ──\n\nconst BROWSER: BuildOptions = {\n\tbundle: true,\n\tformat: \"esm\",\n\tplatform: \"browser\",\n\ttarget: \"es2022\",\n\tmainFields: [\"browser\", \"module\", \"main\"],\n\tlogLevel: \"silent\",\n};\n\nfunction engineName(spec: string): string {\n\treturn spec === \"framefields\"\n\t\t? \"framefields\"\n\t\t: spec.slice(\"framefields/\".length);\n}\n\n/** Node built-ins to shims, the rest of Node to stubs. */\nfunction resolveNode(\n\tb: PluginBuild,\n\tshim: (name: string) => { path: string; external?: boolean },\n): void {\n\tb.onResolve({ filter: /^(node:)?[a-z_/]+$/ }, (args) => {\n\t\tconst name = args.path.replace(/^node:/, \"\");\n\t\tif (SHIMS[name]) return shim(SHIMS[name]);\n\t\tif (args.path.startsWith(\"node:\") || isBuiltin(name))\n\t\t\treturn { path: name, namespace: \"framefields-stub\" };\n\t\treturn undefined;\n\t});\n\tb.onResolve({ filter: /.*/ }, (args) => {\n\t\tconst pkg = packageName(args.path);\n\t\tif (pkg && (NODE_ONLY.has(pkg) || NODE_ONLY.has(args.path)))\n\t\t\treturn { path: args.path, namespace: \"framefields-stub\" };\n\t\treturn undefined;\n\t});\n\tb.onLoad({ filter: /.*/, namespace: \"framefields-stub\" }, () => ({\n\t\tcontents: STUB,\n\t\tloader: \"js\",\n\t}));\n}\n\n/** Each project file keeps its own URL, as `fileURLToPath(import.meta.url)` expects. */\nfunction rewriteMetaUrl(b: PluginBuild): void {\n\tb.onLoad({ filter: /\\.[cm]?[jt]sx?$/ }, async (args) => {\n\t\tif (args.path.includes(`${path.sep}node_modules${path.sep}`))\n\t\t\treturn undefined;\n\t\tconst source = await fs.promises.readFile(args.path, \"utf8\");\n\t\tif (!source.includes(META_URL)) return undefined;\n\t\treturn {\n\t\t\tcontents: `const __framefieldsFileUrl = ${JSON.stringify(pathToFileURL(args.path).href)};\\n${source.replaceAll(META_URL, \"__framefieldsFileUrl\")}`,\n\t\t\tloader: loaderFor(args.path),\n\t\t};\n\t});\n}\n\n/** The generated list of node renderers (needed only to build the engine). */\nfunction builtinRenderersModule(): string {\n\tconst dist = require.resolve(\"@framefields/renderer\");\n\tconst file = path.join(\n\t\tpath.dirname(dist),\n\t\t\"..\",\n\t\t\"src\",\n\t\t\"generated\",\n\t\t\"node-renderers.ts\",\n\t);\n\tif (!fs.existsSync(file))\n\t\tthrow new Error(`[framefields] node renderer list not found at ${file}`);\n\treturn file;\n}\n\nfunction packageName(spec: string): string | undefined {\n\tif (spec.startsWith(\".\") || spec.startsWith(\"/\")) return undefined;\n\tconst parts = spec.split(\"/\");\n\treturn spec.startsWith(\"@\") ? parts.slice(0, 2).join(\"/\") : parts[0];\n}\n\nfunction loaderFor(file: string): \"ts\" | \"tsx\" | \"js\" | \"jsx\" {\n\tif (file.endsWith(\".tsx\")) return \"tsx\";\n\tif (file.endsWith(\".jsx\")) return \"jsx\";\n\treturn /\\.[cm]?ts$/.test(file) ? \"ts\" : \"js\";\n}\n","/**\n * Notes the user pins on the preview: a moment, a spot (or an area) on the\n * frame, and what should change there. They are kept as files so whoever\n * edits the film (a person, or an agent) can read them: `notes.json`, plus the\n * frame with the spot marked as `note-<id>.jpg`. The folder ignores itself in\n * git. `notes.json` is read on every change, so it can be edited by hand\n * (marking a note done, say) while the preview runs.\n */\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nexport interface PreviewNote {\n\tid: number;\n\tframe: number;\n\t/** Seconds into the film. */\n\ttime: number;\n\t/** The spot, as fractions of the frame's width and height (0..1). */\n\tx: number;\n\ty: number;\n\t/** Set when an area was marked: its size, from the spot at its top left. */\n\tw?: number;\n\th?: number;\n\ttext: string;\n\t/** The frame with the spot marked (absolute path), when it was captured. */\n\timage?: string;\n\tdone: boolean;\n\tcreated: string;\n}\n\nexport interface NewNote {\n\tframe: number;\n\ttime: number;\n\tx: number;\n\ty: number;\n\tw?: number;\n\th?: number;\n\ttext: string;\n\t/** JPEG as a base64 data URL. */\n\timage?: string;\n}\n\nexport class NoteStore {\n\treadonly file: string;\n\n\tconstructor(readonly dir: string) {\n\t\tthis.file = path.join(dir, \"notes.json\");\n\t}\n\n\tasync list(): Promise<PreviewNote[]> {\n\t\ttry {\n\t\t\tconst notes = JSON.parse(await fs.promises.readFile(this.file, \"utf8\"));\n\t\t\treturn Array.isArray(notes) ? notes : [];\n\t\t} catch {\n\t\t\treturn [];\n\t\t}\n\t}\n\n\tasync add(input: NewNote): Promise<PreviewNote> {\n\t\tconst notes = await this.list();\n\t\tconst id = notes.reduce((max, n) => Math.max(max, n.id), 0) + 1;\n\t\tconst note: PreviewNote = {\n\t\t\tid,\n\t\t\tframe: Math.max(0, Math.round(Number(input.frame) || 0)),\n\t\t\ttime: round(Number(input.time) || 0, 3),\n\t\t\tx: round(clamp01(input.x), 4),\n\t\t\ty: round(clamp01(input.y), 4),\n\t\t\t...(input.w !== undefined &&\n\t\t\t\tinput.h !== undefined && {\n\t\t\t\t\tw: round(clamp01(input.w), 4),\n\t\t\t\t\th: round(clamp01(input.h), 4),\n\t\t\t\t}),\n\t\t\ttext: String(input.text ?? \"\").trim(),\n\t\t\tdone: false,\n\t\t\tcreated: new Date().toISOString(),\n\t\t};\n\t\tawait this.ensureDir();\n\t\tconst jpeg = /^data:image\\/jpeg;base64,(.+)$/.exec(input.image ?? \"\");\n\t\tif (jpeg) {\n\t\t\tnote.image = path.join(this.dir, `note-${id}.jpg`);\n\t\t\tawait fs.promises.writeFile(note.image, Buffer.from(jpeg[1], \"base64\"));\n\t\t}\n\t\tawait this.save([...notes, note]);\n\t\treturn note;\n\t}\n\n\tasync update(\n\t\tid: number,\n\t\tpatch: { done?: boolean; text?: string },\n\t): Promise<PreviewNote | undefined> {\n\t\tconst notes = await this.list();\n\t\tconst note = notes.find((n) => n.id === id);\n\t\tif (!note) return undefined;\n\t\tif (typeof patch.done === \"boolean\") note.done = patch.done;\n\t\tif (typeof patch.text === \"string\" && patch.text.trim())\n\t\t\tnote.text = patch.text.trim();\n\t\tawait this.save(notes);\n\t\treturn note;\n\t}\n\n\tasync remove(id: number): Promise<PreviewNote | undefined> {\n\t\tconst notes = await this.list();\n\t\tconst note = notes.find((n) => n.id === id);\n\t\tif (!note) return undefined;\n\t\tif (note.image) await fs.promises.rm(note.image, { force: true });\n\t\tawait this.save(notes.filter((n) => n !== note));\n\t\treturn note;\n\t}\n\n\tprivate async save(notes: PreviewNote[]): Promise<void> {\n\t\tawait this.ensureDir();\n\t\tconst tmp = `${this.file}.${process.pid}.tmp`;\n\t\tawait fs.promises.writeFile(tmp, `${JSON.stringify(notes, null, \"\\t\")}\\n`);\n\t\tawait fs.promises.rename(tmp, this.file);\n\t}\n\n\tprivate async ensureDir(): Promise<void> {\n\t\tawait fs.promises.mkdir(this.dir, { recursive: true });\n\t\t// Notes are working material, not part of the project.\n\t\tconst ignore = path.join(path.dirname(this.dir), \".gitignore\");\n\t\tif (!fs.existsSync(ignore)) await fs.promises.writeFile(ignore, \"*\\n\");\n\t}\n}\n\n/** One line for a terminal (or an agent reading one). */\nexport function describeNote(note: PreviewNote, fps: number): string {\n\tconst pct = (v: number) => `${Math.round(v * 100)}%`;\n\tconst where =\n\t\tnote.w !== undefined && note.h !== undefined\n\t\t\t? `area ${pct(note.x)},${pct(note.y)} to ${pct(note.x + note.w)},${pct(note.y + note.h)}`\n\t\t\t: `spot ${pct(note.x)},${pct(note.y)}`;\n\treturn `note ${note.id} at ${timecode(note.frame, fps)} (frame ${note.frame}), ${where}: ${note.text}`;\n}\n\nfunction timecode(frame: number, fps: number): string {\n\tconst s = frame / fps;\n\tconst m = Math.floor(s / 60);\n\treturn `${m}:${(s - m * 60).toFixed(2).padStart(5, \"0\")}`;\n}\n\nfunction clamp01(v: unknown): number {\n\tconst n = Number(v);\n\treturn Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0;\n}\n\nfunction round(v: number, digits: number): number {\n\treturn Number(v.toFixed(digits));\n}\n","/**\n * Single-file preview UI served by `startPreview`. No framework, no build step.\n * The page script avoids template literals so it can live inside this one.\n */\nexport const previewPage = /* html */ `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>framefields preview</title>\n<link rel=\"icon\" href=\"data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 512 512%22%3E%3Cg fill=%22none%22 stroke=%22%233B82F6%22 stroke-linecap=%22round%22%3E%3Cpath d=%22M184 56H86q-30 0-30 30v98M328 56h98q30 0 30 30v98M184 456H86q-30 0-30-30v-98M328 456h98q30 0 30-30v-98%22 stroke-width=%2234%22/%3E%3Cpath d=%22M176 182v148m0-148c0 74 60 74 100 74%22 stroke-width=%2228%22/%3E%3C/g%3E%3Cg fill=%22%233B82F6%22%3E%3Ccircle cx=%22176%22 cy=%22172%22 r=%2232%22/%3E%3Ccircle cx=%22176%22 cy=%22340%22 r=%2232%22/%3E%3Cpath d=%22M284 200l78 56-78 56z%22 stroke=%22%233B82F6%22 stroke-width=%2234%22 stroke-linejoin=%22round%22/%3E%3C/g%3E%3C/svg%3E\">\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link href=\"https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap\" rel=\"stylesheet\">\n<style>\n :root {\n --bg: #0e0f11; --surface: #16171a; --surface-2: #1d1e22; --surface-3: #26282d;\n --line: #2a2c31; --line-2: #3a3c43;\n --ink: #ededf0; --ink-2: #a6a8b0; --ink-3: #74777f;\n --stage: #060607;\n --primary-bg: #ededf0; --primary-ink: #0e0f11;\n --brand: #3b82f6; --focus: #5eaefc; --ok: #4fbf7a; --bad: #f27d8d;\n --wave: #4b4e57;\n --radius: 10px;\n --font: \"Inter\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", sans-serif;\n color-scheme: dark;\n }\n @media (prefers-color-scheme: light) {\n :root {\n --bg: #f5f5f6; --surface: #ffffff; --surface-2: #f0f0f2; --surface-3: #e6e6e9;\n --line: #e2e2e6; --line-2: #cfcfd5;\n --ink: #15161a; --ink-2: #53555d; --ink-3: #85878f;\n --stage: #0b0b0c;\n --primary-bg: #15161a; --primary-ink: #ffffff;\n --brand: #2563eb; --focus: #1c74cc; --ok: #23874a; --bad: #cf4057;\n --wave: #c2c3ca;\n color-scheme: light;\n }\n }\n * { box-sizing: border-box; }\n html, body { height: 100%; margin: 0; overflow: hidden; background: var(--bg); color: var(--ink); }\n body {\n display: flex; flex-direction: column; height: 100dvh;\n font: 13.5px/1.45 var(--font); -webkit-font-smoothing: antialiased;\n font-feature-settings: \"cv11\", \"ss03\";\n }\n button, input { font: inherit; color: inherit; }\n button { cursor: pointer; }\n button:focus-visible, input:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }\n .tnum { font-variant-numeric: tabular-nums; }\n .grow { flex: 1; }\n .spinner {\n width: 12px; height: 12px; border-radius: 50%; flex: none;\n border: 1.5px solid var(--line-2); border-top-color: var(--ink); animation: spin .8s linear infinite;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }\n\n /* ── Top bar ── */\n .top { flex: none; display: flex; align-items: center; gap: 10px; padding: 0 20px; height: 50px; border-bottom: 1px solid var(--line); min-width: 0; }\n .logo { width: 22px; height: 22px; flex: none; color: var(--brand); }\n .sep { width: 1px; height: 18px; background: var(--line-2); flex: none; margin: 0 2px; }\n .top h1 { font-size: 14px; font-weight: 600; margin: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\n .spec { color: var(--ink-3); font-size: 12px; white-space: nowrap; }\n @media (max-width: 640px) { .spec { display: none; } }\n .chip {\n display: inline-flex; align-items: center; gap: 8px; height: 26px; padding: 0 10px; white-space: nowrap;\n border: 1px solid var(--line); border-radius: 999px; color: var(--ink-2); font-size: 12px; background: var(--surface);\n }\n .chip[data-state=\"ready\"] .dot { color: var(--ok); }\n .chip[data-state=\"none\"] .dot, .chip[data-state=\"offline\"] .dot { color: var(--ink-3); }\n .chip[data-state=\"error\"] { color: var(--bad); }\n\n /* ── Player: one screen, no page scroll; the picture takes the space left ── */\n .player {\n flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 10px;\n width: 100%; max-width: 1600px; margin: 0 auto; padding: 16px 20px 20px;\n }\n @media (max-width: 640px) { .player { padding: 12px; } }\n .stage-wrap { flex: 1 1 0; min-height: 96px; container-type: size; display: grid; place-items: center; }\n .stage {\n position: relative; background: var(--stage); border-radius: var(--radius); overflow: hidden;\n box-shadow: 0 0 0 1px var(--line); user-select: none; -webkit-user-select: none;\n aspect-ratio: var(--ar, 16 / 9); width: min(100cqw, calc(100cqh * var(--ar-n, 1.7778)));\n }\n .stage canvas { display: block; width: 100%; height: 100%; }\n .stage-msg {\n position: absolute; inset: 0; display: none; place-items: center; padding: 24px; text-align: center;\n color: #fff; background: rgb(0 0 0 / .72); font-size: 13px; line-height: 1.5; white-space: pre-wrap; overflow: auto;\n }\n .stage-msg.on { display: grid; }\n /* Buffering: a centred spinning arc over the picture, like YouTube's. It\n fades in after a beat so brief waits don't flash it. */\n .buffering {\n position: absolute; left: 50%; top: 50%; width: clamp(36px, 9cqmin, 64px); aspect-ratio: 1;\n transform: translate(-50%, -50%); pointer-events: none; opacity: 0; visibility: hidden;\n transition: opacity .15s, visibility 0s .15s; filter: drop-shadow(0 0 3px rgb(0 0 0 / .45));\n }\n .buffering.on { opacity: 1; visibility: visible; transition: opacity .2s .25s, visibility 0s .25s; }\n .buffering svg { width: 100%; height: 100%; animation: yt-rotate 1.4s linear infinite; }\n .buffering circle {\n fill: none; stroke: #fff; stroke-width: 3.6; stroke-linecap: round;\n stroke-dasharray: 1 150; stroke-dashoffset: 0; animation: yt-dash 1.4s ease-in-out infinite;\n }\n @keyframes yt-rotate { to { transform: rotate(360deg); } }\n @keyframes yt-dash {\n 0% { stroke-dasharray: 1 150; stroke-dashoffset: 0; }\n 50% { stroke-dasharray: 90 150; stroke-dashoffset: -35; }\n 100% { stroke-dasharray: 90 150; stroke-dashoffset: -124; }\n }\n @media (prefers-reduced-motion: reduce) { .buffering circle, .play-btn .spin circle { animation: none; stroke-dasharray: 70 150; } }\n\n /* ── Transport ── */\n .transport { flex: none; display: flex; align-items: center; gap: 4px; flex-wrap: wrap; row-gap: 6px; }\n .icon-btn {\n width: 32px; height: 32px; display: inline-grid; place-items: center; border-radius: 8px;\n border: 0; background: transparent; color: var(--ink-2); flex: none;\n }\n .icon-btn:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }\n .icon-btn:disabled { opacity: .35; cursor: default; }\n .icon-btn svg { width: 16px; height: 16px; }\n .play-btn {\n width: 36px; height: 36px; border-radius: 50%; border: 0; display: inline-grid; place-items: center; flex: none;\n background: var(--primary-bg); color: var(--primary-ink); margin: 0 2px;\n }\n .play-btn:hover:not(:disabled) { filter: brightness(.9); }\n .play-btn:disabled { opacity: .4; cursor: default; }\n .play-btn svg { width: 14px; height: 14px; }\n .play-btn .spin { display: none; width: 20px; height: 20px; animation: yt-rotate 1.4s linear infinite; }\n .play-btn .spin circle {\n fill: none; stroke: currentColor; stroke-width: 5; stroke-linecap: round;\n stroke-dasharray: 1 150; animation: yt-dash 1.4s ease-in-out infinite;\n }\n .play-btn.loading .spin { display: block; }\n .play-btn.loading svg:not(.spin) { display: none !important; }\n .timecode { display: flex; align-items: baseline; gap: 5px; margin-left: 8px; font-size: 13px; white-space: nowrap; }\n .timecode .dim { color: var(--ink-3); }\n .timecode .frame { color: var(--ink-3); font-size: 11.5px; margin-left: 6px; }\n .fps { margin-left: 10px; color: var(--ink-3); font-size: 11.5px; white-space: nowrap; }\n input[type=\"range\"].slider { -webkit-appearance: none; appearance: none; width: 72px; height: 4px; border-radius: 2px; background: var(--line-2); margin: 0 8px 0 0; }\n input[type=\"range\"].slider::-webkit-slider-thumb { -webkit-appearance: none; width: 12px; height: 12px; border-radius: 50%; background: var(--ink); border: 0; }\n input[type=\"range\"].slider::-moz-range-thumb { width: 12px; height: 12px; border-radius: 50%; background: var(--ink); border: 0; }\n input[type=\"range\"].slider:disabled { opacity: .35; }\n\n /* ── Timeline ── */\n .timeline {\n flex: none; border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);\n padding: 0 14px; user-select: none; -webkit-user-select: none; touch-action: none; cursor: pointer;\n }\n .tl-inner { position: relative; }\n .ruler { position: relative; height: 22px; border-bottom: 1px solid var(--line); }\n .tick { position: absolute; bottom: 0; width: 1px; height: 4px; background: var(--line-2); }\n .tick.major { height: 8px; background: var(--ink-3); }\n .tick span { position: absolute; bottom: 9px; left: 4px; font-size: 10.5px; line-height: 1; color: var(--ink-3); white-space: nowrap; font-variant-numeric: tabular-nums; }\n .wave-lane { position: relative; height: 48px; }\n @media (max-height: 600px) { .wave-lane { height: 30px; } }\n .wave-lane canvas { position: absolute; inset: 0; width: 100%; height: 100%; }\n .wave-state { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; color: var(--ink-3); font-size: 12px; }\n .wave-state.shimmer {\n background: linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--ink) 6%, transparent) 50%, transparent 100%);\n background-size: 200% 100%; animation: shimmer 1.4s linear infinite;\n }\n @keyframes shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }\n .playhead { position: absolute; top: 0; bottom: 0; width: 0; pointer-events: none; z-index: 3; }\n .playhead::before { content: \"\"; position: absolute; top: 0; bottom: 0; left: -0.5px; width: 1px; background: var(--ink); }\n .playhead::after { content: \"\"; position: absolute; top: 0; left: -5px; width: 10px; height: 11px; background: var(--ink); clip-path: polygon(0 0, 100% 0, 100% 55%, 50% 100%, 0 55%); }\n .hover-line { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--line-2); display: none; pointer-events: none; }\n .hover-time {\n position: absolute; top: -28px; transform: translateX(-50%); display: none; pointer-events: none;\n font-size: 11px; line-height: 1; background: var(--surface-3); color: var(--ink); padding: 5px 7px; border-radius: 5px;\n white-space: nowrap; z-index: 4; font-variant-numeric: tabular-nums;\n }\n /* ── Notes: pinned to a spot (or an area) and a moment ── */\n .stage.can-note { cursor: crosshair; }\n .overlay { position: absolute; inset: 0; pointer-events: none; }\n .pin {\n position: absolute; width: 22px; height: 22px; margin: -11px 0 0 -11px; padding: 0; border-radius: 50%;\n display: grid; place-items: center; pointer-events: auto; cursor: pointer;\n background: var(--brand); color: #fff; font-size: 11px; font-weight: 600; line-height: 1;\n border: 2px solid #fff; box-shadow: 0 1px 5px rgb(0 0 0 / .55); transition: transform .12s;\n }\n .pin:hover, .pin.active { transform: scale(1.18); }\n .pin.done { background: #6b6e76; }\n .pin.draft { animation: pin-pulse 1.5s ease-out infinite; }\n .pin svg { width: 11px; height: 11px; }\n @keyframes pin-pulse {\n from { box-shadow: 0 0 0 0 rgb(59 130 246 / .6), 0 1px 5px rgb(0 0 0 / .55); }\n to { box-shadow: 0 0 0 14px rgb(59 130 246 / 0), 0 1px 5px rgb(0 0 0 / .55); }\n }\n .area {\n position: absolute; border: 1.5px solid var(--brand); border-radius: 3px;\n background: rgb(59 130 246 / .12); box-shadow: 0 0 0 1px rgb(0 0 0 / .35);\n }\n .area.done { border-color: #9a9da5; background: rgb(255 255 255 / .05); }\n .marquee { position: absolute; display: none; border: 1.5px dashed #fff; background: rgb(59 130 246 / .16); border-radius: 3px; pointer-events: none; }\n .stage-hint, .toast {\n position: absolute; left: 50%; transform: translateX(-50%); pointer-events: none; white-space: nowrap;\n padding: 6px 11px; border-radius: 999px; font-size: 12px; color: #fff; background: rgb(0 0 0 / .62);\n backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); opacity: 0; transition: opacity .15s;\n }\n .stage-hint { top: 12px; }\n .stage.can-note.hint:hover .stage-hint { opacity: 1; }\n .toast { bottom: 14px; }\n .toast.on { opacity: 1; }\n\n .pop {\n position: fixed; z-index: 20; width: 280px; display: none; padding: 10px 10px 8px;\n background: var(--surface); border: 1px solid var(--line-2); border-radius: 12px;\n box-shadow: 0 12px 32px rgb(0 0 0 / .35);\n }\n .pop.on { display: block; }\n .pop-head { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--ink-3); margin-bottom: 6px; }\n .badge {\n min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; display: inline-grid; place-items: center; flex: none;\n background: var(--brand); color: #fff; font-size: 10.5px; font-weight: 600;\n }\n .badge.done { background: var(--ink-3); }\n .pop textarea {\n display: block; width: 100%; min-height: 54px; max-height: 180px; resize: none; padding: 2px 2px 0;\n border: 0; outline: none; background: transparent; color: var(--ink); font: inherit; line-height: 1.45;\n }\n .pop textarea::placeholder { color: var(--ink-3); }\n .pop-text { white-space: pre-wrap; overflow-wrap: anywhere; padding: 0 2px; max-height: 200px; overflow: auto; }\n .pop-foot { display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 11.5px; color: var(--ink-3); }\n .pop-foot .err { color: var(--bad); }\n .btn {\n height: 26px; padding: 0 10px; border-radius: 7px; border: 0; font-size: 12px; font-weight: 500;\n display: inline-flex; align-items: center; gap: 6px; background: transparent; color: var(--ink-2); white-space: nowrap;\n }\n .btn:hover:not(:disabled) { background: var(--surface-2); color: var(--ink); }\n .btn.primary { background: var(--primary-bg); color: var(--primary-ink); }\n .btn.primary:hover:not(:disabled) { background: var(--primary-bg); filter: brightness(.9); }\n .btn.danger:hover:not(:disabled) { color: var(--bad); }\n .btn:disabled { opacity: .4; cursor: default; }\n .btn svg { width: 13px; height: 13px; }\n .x-btn { width: 22px; height: 22px; padding: 0; justify-content: center; margin-right: -4px; }\n\n .transport { position: relative; }\n .notes-btn { height: 32px; padding: 0 9px 0 8px; margin-right: 6px; font-size: 12.5px; }\n .notes-btn svg { width: 15px; height: 15px; }\n .notes-btn .badge { background: var(--surface-3); color: var(--ink-2); }\n .notes-btn .badge.open { background: var(--brand); color: #fff; }\n .notes-panel {\n position: absolute; right: 0; bottom: calc(100% + 8px); z-index: 15; width: min(360px, calc(100vw - 24px));\n max-height: min(440px, 62vh); display: none; flex-direction: column; overflow: hidden;\n background: var(--surface); border: 1px solid var(--line-2); border-radius: 12px; box-shadow: 0 12px 32px rgb(0 0 0 / .35);\n }\n .notes-panel.on { display: flex; }\n .np-head { flex: none; display: flex; align-items: center; gap: 8px; padding: 8px 8px 8px 14px; border-bottom: 1px solid var(--line); }\n .np-head h2 { margin: 0; font-size: 13px; font-weight: 600; }\n .np-list { overflow: auto; padding: 4px; }\n .np-empty { padding: 22px 18px; text-align: center; color: var(--ink-3); font-size: 12.5px; }\n .np-foot { flex: none; padding: 8px 14px; border-top: 1px solid var(--line); color: var(--ink-3); font-size: 11.5px; overflow-wrap: anywhere; }\n .note-row {\n display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: start; padding: 8px 8px 8px 10px;\n border-radius: 8px; cursor: pointer;\n }\n .note-row:hover { background: var(--surface-2); }\n .note-row .badge { margin-top: 1px; }\n .note-row .when { font-size: 11.5px; color: var(--ink-3); }\n .note-row .text { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }\n .note-row.done .text { color: var(--ink-3); text-decoration: line-through; }\n .check {\n width: 20px; height: 20px; border-radius: 50%; padding: 0; display: grid; place-items: center; margin-top: 1px;\n border: 1.5px solid var(--line-2); background: transparent; color: transparent;\n }\n .check:hover { border-color: var(--ok); color: var(--ok); }\n .check.on { background: var(--ok); border-color: var(--ok); color: #fff; }\n .check svg { width: 11px; height: 11px; }\n\n .note-marks { position: absolute; left: 0; right: 0; top: 22px; bottom: 0; pointer-events: none; z-index: 2; }\n .note-mark {\n position: absolute; top: 5px; width: 14px; height: 14px; margin-left: -7px; padding: 0; border: 0; border-radius: 50%;\n display: grid; place-items: center; pointer-events: auto; cursor: pointer;\n background: var(--brand); color: #fff; font-size: 8.5px; font-weight: 700; box-shadow: 0 0 0 2px var(--surface);\n }\n .note-mark::after { content: \"\"; position: absolute; top: 14px; left: 6.5px; width: 1px; height: 30px; background: var(--brand); opacity: .45; }\n .note-mark.done { background: var(--ink-3); }\n .note-mark.stacked::after { display: none; }\n .note-mark.done::after { background: var(--ink-3); }\n .note-mark:hover { transform: scale(1.2); }\n</style>\n</head>\n<body>\n<header class=\"top\">\n <svg class=\"logo\" viewBox=\"0 0 512 512\" aria-label=\"framefields\" role=\"img\"><g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\"><path d=\"M184 56H86q-30 0-30 30v98M328 56h98q30 0 30 30v98M184 456H86q-30 0-30-30v-98M328 456h98q30 0 30-30v-98\" stroke-width=\"34\" stroke-linejoin=\"round\"/><path d=\"M176 182v148m0-148c0 74 60 74 100 74\" stroke-width=\"28\"/></g><g fill=\"currentColor\"><circle cx=\"176\" cy=\"172\" r=\"32\"/><circle cx=\"176\" cy=\"340\" r=\"32\"/><path d=\"M284 200l78 56-78 56z\" stroke=\"currentColor\" stroke-width=\"34\" stroke-linejoin=\"round\"/></g></svg>\n <span class=\"sep\" aria-hidden=\"true\"></span>\n <h1 id=\"title\">framefields preview</h1>\n <span class=\"grow\"></span>\n <span class=\"spec tnum\" id=\"spec\"></span>\n <span class=\"chip\" id=\"chip\" data-state=\"mixing\" role=\"status\" aria-live=\"polite\"><span class=\"spinner\"></span><span>Mixing audio</span></span>\n</header>\n\n<main class=\"player\" aria-label=\"Player\">\n <div class=\"stage-wrap\">\n <div class=\"stage\" id=\"stage\">\n <canvas id=\"canvas\" aria-label=\"Rendered frame\"></canvas>\n <div class=\"stage-msg\" id=\"stageMsg\" role=\"alert\"></div>\n <div class=\"overlay\" id=\"pins\"></div>\n <div class=\"marquee\" id=\"marquee\"></div>\n <div class=\"stage-hint\" id=\"stageHint\">Click to pin a note · drag to mark an area</div>\n <div class=\"toast\" id=\"toast\" role=\"status\" aria-live=\"polite\"></div>\n <div class=\"buffering\" id=\"buffering\" role=\"status\" aria-label=\"Loading\"><svg viewBox=\"0 0 50 50\"><circle cx=\"25\" cy=\"25\" r=\"20\"/></svg></div>\n </div>\n </div>\n\n <div class=\"transport\">\n <button class=\"icon-btn\" id=\"prev\" title=\"Previous frame\" aria-label=\"Previous frame\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M3 3h1.6v10H3zM13 3.6v8.8a.6.6 0 0 1-.92.5L5.8 8.5a.6.6 0 0 1 0-1l6.28-4.4a.6.6 0 0 1 .92.5z\"/></svg>\n </button>\n <button class=\"play-btn\" id=\"play\" title=\"Play\" aria-label=\"Play\" disabled>\n <svg id=\"playIcon\" viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M4.5 2.9v10.2a.7.7 0 0 0 1.06.6l8.1-5.1a.7.7 0 0 0 0-1.2l-8.1-5.1a.7.7 0 0 0-1.06.6z\"/></svg>\n <svg class=\"spin\" viewBox=\"0 0 50 50\" aria-hidden=\"true\"><circle cx=\"25\" cy=\"25\" r=\"18\"/></svg>\n <svg id=\"pauseIcon\" viewBox=\"0 0 16 16\" fill=\"currentColor\" style=\"display:none\"><rect x=\"3.5\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\".8\"/><rect x=\"9.3\" y=\"2.5\" width=\"3.2\" height=\"11\" rx=\".8\"/></svg>\n </button>\n <button class=\"icon-btn\" id=\"next\" title=\"Next frame\" aria-label=\"Next frame\">\n <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M11.4 3H13v10h-1.6zM3 3.6v8.8a.6.6 0 0 0 .92.5l6.28-4.4a.6.6 0 0 0 0-1L3.92 3.1A.6.6 0 0 0 3 3.6z\"/></svg>\n </button>\n <div class=\"timecode tnum\">\n <span id=\"tcNow\">0:00.00</span><span class=\"dim\">/</span><span class=\"dim\" id=\"tcDur\">0:00.00</span>\n <span class=\"frame\" id=\"tcFrame\">frame 0</span>\n </div>\n <span class=\"fps tnum\" id=\"fps\" role=\"status\" hidden></span>\n <span class=\"grow\"></span>\n <button class=\"btn notes-btn\" id=\"notesBtn\" title=\"Notes\" aria-haspopup=\"dialog\" aria-expanded=\"false\" hidden>\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linejoin=\"round\"><path d=\"M3 2.75h10a1.25 1.25 0 0 1 1.25 1.25v6.5A1.25 1.25 0 0 1 13 11.75H7.5L4.5 14v-2.25H3A1.25 1.25 0 0 1 1.75 10.5V4A1.25 1.25 0 0 1 3 2.75z\"/></svg>\n Notes <span class=\"badge\" id=\"notesCount\">0</span>\n </button>\n <div class=\"notes-panel\" id=\"notesPanel\" role=\"dialog\" aria-label=\"Notes\">\n <div class=\"np-head\">\n <h2>Notes</h2><span class=\"grow\"></span>\n <button class=\"btn\" id=\"notesCopy\" title=\"Copy the open notes, to paste into a chat\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"5.25\" y=\"5.25\" width=\"8\" height=\"8\" rx=\"1.5\"/><path d=\"M10.75 3.5V3.25a1.5 1.5 0 0 0-1.5-1.5h-5.5a1.5 1.5 0 0 0-1.5 1.5v5.5a1.5 1.5 0 0 0 1.5 1.5H4\"/></svg>\n <span id=\"notesCopyLabel\">Copy</span>\n </button>\n <button class=\"btn x-btn\" id=\"notesClose\" title=\"Close\" aria-label=\"Close\"><svg viewBox=\"0 0 16 16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg></button>\n </div>\n <div class=\"np-list\" id=\"notesList\"></div>\n <div class=\"np-foot\" id=\"notesFoot\"></div>\n </div>\n <button class=\"icon-btn\" id=\"mute\" title=\"Mute\" aria-label=\"Mute\" disabled>\n <svg id=\"volIcon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2.5 6h2.2L8 3.2v9.6L4.7 10H2.5z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M10.6 5.6a3.4 3.4 0 0 1 0 4.8M12.4 3.9a5.8 5.8 0 0 1 0 8.2\"/></svg>\n <svg id=\"muteIcon\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" style=\"display:none\"><path d=\"M2.5 6h2.2L8 3.2v9.6L4.7 10H2.5z\" fill=\"currentColor\" stroke=\"none\"/><path d=\"M10.5 6l3.5 4M14 6l-3.5 4\"/></svg>\n </button>\n <input class=\"slider\" type=\"range\" id=\"volume\" min=\"0\" max=\"1\" step=\"0.01\" value=\"0.9\" aria-label=\"Volume\" disabled>\n </div>\n\n <div class=\"timeline\" id=\"timeline\" aria-label=\"Timeline\">\n <div class=\"tl-inner\" id=\"tlInner\">\n <div class=\"ruler\" id=\"ruler\"></div>\n <div class=\"note-marks\" id=\"noteMarks\"></div>\n <div class=\"wave-lane\">\n <canvas id=\"wave\"></canvas>\n <div class=\"wave-state shimmer\" id=\"waveState\"><span class=\"spinner\"></span>Mixing audio</div>\n </div>\n <div class=\"hover-line\" id=\"hoverLine\"></div>\n <div class=\"hover-time\" id=\"hoverTime\"></div>\n <div class=\"playhead\" id=\"playhead\"></div>\n </div>\n </div>\n</main>\n\n<div class=\"pop\" id=\"pop\" role=\"dialog\" aria-label=\"Note\"></div>\n\n<script>\n(function () {\n var $ = function (id) { return document.getElementById(id); };\n var stage = $(\"stage\"), timeline = $(\"timeline\"), tlInner = $(\"tlInner\");\n\n var meta = null, session = null, frame = 0, shown = -1, playing = false, playToken = 0;\n var player = null;\n\n // ── Helpers ──\n function fmt(f) {\n var s = f / meta.fps, m = Math.floor(s / 60);\n return m + \":\" + (s - m * 60).toFixed(2).padStart(5, \"0\");\n }\n function last() { return meta.frameCount - 1; }\n function clampFrame(f) { return Math.max(0, Math.min(last(), Math.round(f))); }\n function pct(f) { return meta.frameCount > 1 ? (f / last()) * 100 : 0; }\n function el(tag, cls, text) {\n var e = document.createElement(tag);\n if (cls) e.className = cls;\n if (text != null) e.textContent = text;\n return e;\n }\n function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); }\n\n // ── Reporting: errors and the engine's warnings go to the preview's\n // terminal too, so whoever runs it sees them without opening this tab.\n // Once each; frame numbers aside, playback repeats the same one per frame ──\n var reportedKeys = new Set();\n function errText(x) {\n if (x && x.message) return x.stack && x.stack.indexOf(x.message) >= 0 ? x.stack.split(\"\\\\n\").slice(0, 4).join(\"\\\\n\") : x.message;\n return typeof x === \"string\" ? x : String(x);\n }\n function report(level, parts) {\n var text = parts.map(errText).join(\" \").trim(), key = level + text.replace(/\\\\d+/g, \"#\");\n if (!text || reportedKeys.has(key) || reportedKeys.size > 100) return;\n reportedKeys.add(key);\n fetch(\"/@framefields/log\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ level: level, message: text.slice(0, 4000) }),\n keepalive: true\n }).catch(function () {});\n }\n [\"error\", \"warn\"].forEach(function (name) {\n var original = console[name].bind(console);\n console[name] = function () {\n original.apply(null, arguments);\n var args = Array.prototype.slice.call(arguments);\n // Errors from anywhere; warnings only from the engine (fonts, renderers).\n if (name === \"error\" || (typeof args[0] === \"string\" && args[0].indexOf(\"[framefields]\") === 0)) {\n report(name === \"error\" ? \"error\" : \"warning\", args);\n }\n };\n });\n\n // ── Frames: drawn in this tab by the composition's own code ──\n function setTime(f) {\n frame = f;\n $(\"tcNow\").textContent = fmt(f);\n $(\"tcFrame\").textContent = \"frame \" + f;\n $(\"playhead\").style.left = pct(f) + \"%\";\n if (!playing && notesOn) renderPins();\n }\n function show(f) {\n setTime(f);\n if (!player) return Promise.resolve();\n $(\"buffering\").classList.add(\"on\");\n // Calls made while a frame draws collapse to the latest.\n return player.render(f).then(function (drawn) {\n shown = drawn;\n if (!stalled) $(\"buffering\").classList.remove(\"on\");\n });\n }\n function seek(f) {\n stop();\n var done = show(clampFrame(f));\n keepPosition();\n scheduleWarm();\n return done;\n }\n\n // ── Position in the address: #t=12.5 opens there, and a reload (the next\n // preview taking over) comes back to the same moment ──\n function startFrame() {\n var m = /[#&]t=([\\\\d.]+)/.exec(location.hash), n = /[#&]frame=(\\\\d+)/.exec(location.hash);\n if (m) return clampFrame(Number(m[1]) * meta.fps);\n if (n) return clampFrame(Number(n[1]));\n return 0;\n }\n function keepPosition() {\n var at = frame > 0 ? \"#t=\" + Number((frame / meta.fps).toFixed(3)) : location.pathname;\n try { history.replaceState(null, \"\", at); } catch (e) {}\n }\n\n // ── Analysing ahead: while paused, frames after the playhead are drawn\n // off-screen so vision results are cached before they play ──\n var warmTimer = null, warmToken = 0, warmed = new Set();\n function scheduleWarm() {\n clearTimeout(warmTimer);\n warmToken++;\n if (!player || !player.hasVision || playing) return;\n warmTimer = setTimeout(warmAhead, 1000);\n }\n async function warmAhead() {\n var token = ++warmToken;\n var end = Math.min(last(), frame + Math.round(meta.fps * 20));\n for (var f = frame + 1; f <= end && token === warmToken && !playing; f++) {\n if (warmed.has(f)) continue;\n if (!(await player.warm(f))) { scheduleWarm(); return; }\n warmed.add(f);\n await sleep(0);\n }\n }\n function showError(err) {\n report(\"error\", [err]);\n pageState = \"error\";\n $(\"buffering\").classList.remove(\"on\");\n $(\"stageMsg\").textContent = String((err && err.message) || err);\n $(\"stageMsg\").classList.add(\"on\");\n }\n\n // ── Audio ──\n var audio = { state: \"mixing\", error: \"\", buffer: null, peaks: null, ctx: null, gain: null, src: null, muted: false, volume: 0.9 };\n var AUDIO_LABEL = { mixing: \"Mixing audio\", loading: \"Loading audio\", ready: \"Audio ready\", none: \"No audio\", error: \"Audio failed\" };\n var offline = false;\n\n function renderChip() {\n var chip = $(\"chip\"), state, label, busy;\n if (offline) { state = \"offline\"; label = \"Preview stopped\"; busy = false; }\n else { state = audio.state; label = AUDIO_LABEL[audio.state]; busy = state === \"mixing\" || state === \"loading\"; }\n chip.dataset.state = state;\n chip.title = offline ? \"The preview command isn't running. This tab reconnects when it starts again.\" : audio.error || \"\";\n chip.replaceChildren(el(\"span\", busy ? \"spinner\" : \"dot\"), el(\"span\", null, label));\n }\n function setAudioStatus(state, error) {\n audio.state = state;\n audio.error = error || \"\";\n var busy = state === \"mixing\" || state === \"loading\", ws = $(\"waveState\");\n ws.classList.toggle(\"shimmer\", busy);\n ws.style.display = state === \"ready\" ? \"none\" : \"flex\";\n ws.replaceChildren();\n if (busy) ws.append(el(\"span\", \"spinner\"));\n ws.append(document.createTextNode(state === \"none\" ? \"No audio in this composition\" : state === \"error\" ? \"Audio could not be mixed\" : AUDIO_LABEL[state]));\n var usable = state === \"ready\" && !!audio.buffer;\n $(\"mute\").disabled = !usable;\n $(\"volume\").disabled = !usable;\n renderChip();\n drawWave();\n }\n function loadAudio(status) {\n stopClock();\n audio.buffer = null;\n audio.peaks = null;\n if (!status || status.state !== \"ready\") { setAudioStatus(status ? status.state : \"none\", status && status.error); return; }\n audio.peaks = status.peaks;\n setAudioStatus(\"loading\");\n if (!audio.ctx) {\n var Ctx = window.AudioContext || window.webkitAudioContext;\n audio.ctx = new Ctx();\n audio.gain = audio.ctx.createGain();\n audio.gain.connect(audio.ctx.destination);\n applyVolume();\n }\n fetch(\"/audio\")\n .then(function (r) { if (!r.ok) throw new Error(\"HTTP \" + r.status); return r.arrayBuffer(); })\n .then(function (data) { return audio.ctx.decodeAudioData(data); })\n .then(function (buf) { audio.buffer = buf; setAudioStatus(\"ready\"); })\n .catch(function (err) { setAudioStatus(\"error\", String((err && err.message) || err)); });\n }\n function applyVolume() {\n if (audio.gain) audio.gain.gain.value = audio.muted ? 0 : audio.volume;\n $(\"volIcon\").style.display = audio.muted ? \"none\" : \"\";\n $(\"muteIcon\").style.display = audio.muted ? \"\" : \"none\";\n $(\"mute\").title = audio.muted ? \"Unmute\" : \"Mute\";\n $(\"mute\").setAttribute(\"aria-label\", audio.muted ? \"Unmute\" : \"Mute\");\n }\n // The clock for real-time playback: the soundtrack when there is one,\n // otherwise the wall clock. Audio as master keeps picture and sound in sync.\n function startClock(f0) {\n if (audio.buffer && audio.ctx) {\n audio.ctx.resume();\n var src = audio.ctx.createBufferSource();\n src.buffer = audio.buffer;\n src.connect(audio.gain);\n var startAt = audio.ctx.currentTime + 0.06, offset = f0 / meta.fps;\n if (offset < audio.buffer.duration) src.start(startAt, offset);\n audio.src = src;\n return function () { return Math.max(0, audio.ctx.currentTime - startAt); };\n }\n var t0 = performance.now();\n return function () { return (performance.now() - t0) / 1000; };\n }\n function stopClock() {\n if (!audio.src) return;\n try { audio.src.stop(); } catch (e) {}\n audio.src.disconnect();\n audio.src = null;\n }\n\n // ── Playback ──\n var stalled = false;\n // Waiting on frames mid-playback: the play button and the picture both spin.\n function setStalled(on) {\n stalled = on;\n $(\"play\").classList.toggle(\"loading\", on);\n $(\"buffering\").classList.toggle(\"on\", on);\n }\n function setPlayingUi(on) {\n $(\"playIcon\").style.display = on ? \"none\" : \"\";\n $(\"pauseIcon\").style.display = on ? \"\" : \"none\";\n $(\"play\").title = on ? \"Pause\" : \"Play\";\n $(\"play\").setAttribute(\"aria-label\", on ? \"Pause\" : \"Play\");\n }\n function play() {\n if (playing || !player) return;\n playing = true;\n clearTimeout(warmTimer);\n warmToken++;\n var token = ++playToken;\n var alive = function () { return playing && token === playToken; };\n setPlayingUi(true);\n if (openId != null) closePop();\n if (draft && !draft.text.trim()) cancelDraft();\n if (notesOn) renderPins();\n if (frame >= last()) frame = 0;\n playFrames(alive).finally(function () { if (token === playToken && playing) pause(); });\n }\n function stop() {\n playing = false;\n stopClock();\n setStalled(false);\n setPlayingUi(false);\n meter.stop();\n renderFps();\n }\n\n // ── Frame rate reached: distinct frames shown per second while playing,\n // over the last 2 s, stalls included. The film's own rate is in meta ──\n var meter = (function () {\n var started = null, lastFrame = null, samples = [], lastFps = null;\n function read() {\n if (started === null) return { state: \"paused\", fps: lastFps };\n var now = performance.now(), elapsed = Math.min(2000, now - started);\n samples = samples.filter(function (t) { return t > now - 2000; });\n if (elapsed < 1000) return { state: \"measuring\", fps: null };\n // Counting whole frames in the window can overshoot the film's rate.\n return { state: \"playing\", fps: Math.min(meta ? meta.fps : Infinity, (samples.length * 1000) / elapsed) };\n }\n return {\n read: read,\n start: function () { started = performance.now(); lastFrame = null; samples = []; },\n stop: function () {\n if (started === null) return;\n // A playback too short to measure leaves no rate, not an older one.\n var r = read();\n lastFps = r.state === \"playing\" ? r.fps : null;\n started = null;\n samples = [];\n },\n record: function (f) {\n if (started === null || f === lastFrame) return;\n lastFrame = f;\n samples.push(performance.now());\n }\n };\n })();\n function renderFps() {\n var r = meter.read(), out = $(\"fps\");\n out.hidden = r.state === \"paused\" && r.fps === null;\n if (out.hidden) return;\n out.textContent = r.state === \"measuring\" ? \"… fps\" : r.fps.toFixed(1) + \" fps\";\n out.title = (r.state === \"paused\" ? \"Frame rate the last playback reached\" : \"Frame rate playback reaches\") + (meta ? \"; the film runs at \" + meta.fps + \" fps\" : \"\");\n }\n setInterval(function () { if (playing) renderFps(); }, 500);\n // Pausing settles on exactly the frame the timecode shows.\n function pause() { stop(); keepPosition(); show(frame).then(scheduleWarm); }\n function togglePlay() { playing ? pause() : play(); }\n\n // Real time: nothing is rendered ahead. Each turn draws the frame due on\n // the clock straight to the screen, and frames that drawing missed are\n // skipped. Playback starts at once and the sound keeps its speed; a device\n // that can't keep up shows fewer frames, and the frame rate says how many.\n async function playFrames(alive) {\n meter.start();\n renderFps();\n // A soundtrack still downloading would start late: wait for it.\n if (audio.state === \"loading\") {\n setStalled(true);\n while (alive() && audio.state === \"loading\") await sleep(50);\n // A newer playback may be waiting too: only this one's own stall ends here.\n if (!alive()) return;\n setStalled(false);\n }\n var f0 = frame, clock = startClock(f0), presented = -1;\n while (alive()) {\n var due = Math.min(last(), f0 + Math.floor(clock() * meta.fps));\n if (due === presented) { await sleep(4); continue; }\n setTime(due);\n // Each frame is drawn once; caching it would cost a texture and a copy per frame.\n var drawn = await player.render(due, { cache: false });\n if (!alive()) return;\n presented = shown = drawn;\n meter.record(drawn);\n if (drawn >= last()) return;\n }\n }\n\n // ── Timeline ──\n function frameAtX(clientX) {\n var r = tlInner.getBoundingClientRect();\n return clampFrame(Math.min(1, Math.max(0, (clientX - r.left) / r.width)) * last());\n }\n function renderRuler() {\n var ruler = $(\"ruler\"), width = tlInner.clientWidth || 800, dur = last() / meta.fps;\n ruler.replaceChildren();\n if (dur <= 0) return;\n var steps = [0.5, 1, 2, 5, 10, 15, 30, 60, 120, 300, 600];\n var major = steps.find(function (s) { return (s / dur) * width >= 80; }) || 1200, minor = major / 5;\n for (var i = 0; i * minor <= dur + 1e-6; i++) {\n var t = i * minor, isMajor = i % 5 === 0, x = (t / dur) * width;\n var tick = el(\"div\", \"tick\" + (isMajor ? \" major\" : \"\"));\n tick.style.left = (t / dur) * 100 + \"%\";\n if (isMajor && x < width - 34) {\n var m = Math.floor(t / 60), s = Math.round((t - m * 60) * 10) / 10;\n tick.append(el(\"span\", null, m + \":\" + (s < 10 ? \"0\" : \"\") + s));\n }\n ruler.append(tick);\n }\n }\n function drawWave() {\n var canvas = $(\"wave\"), dpr = window.devicePixelRatio || 1;\n var w = canvas.clientWidth, h = canvas.clientHeight;\n canvas.width = Math.round(w * dpr);\n canvas.height = Math.round(h * dpr);\n var ctx = canvas.getContext(\"2d\");\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, w, h);\n var peaks = audio.peaks;\n if (!peaks || !peaks.length || !meta) return;\n var videoSec = Math.max(1e-6, last() / meta.fps);\n var audioSec = audio.buffer ? audio.buffer.duration : videoSec;\n var spanPx = Math.min(1, audioSec / videoSec) * w, mid = h / 2;\n ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue(\"--wave\").trim();\n for (var x = 0; x < spanPx; x += 3) {\n var v = Math.max(peaks[Math.floor((x / spanPx) * peaks.length)] || 0, 0.015);\n var bh = Math.max(1, Math.sqrt(v) * (h - 8));\n ctx.fillRect(x, mid - bh / 2, 2, bh);\n }\n }\n\n var dragging = false;\n timeline.addEventListener(\"pointerdown\", function (e) {\n if (!meta || e.button !== 0) return;\n timeline.setPointerCapture(e.pointerId);\n dragging = true;\n seek(frameAtX(e.clientX));\n });\n timeline.addEventListener(\"pointermove\", function (e) {\n if (!meta) return;\n var f = frameAtX(e.clientX), r = tlInner.getBoundingClientRect();\n var x = Math.min(r.width, Math.max(0, e.clientX - r.left));\n $(\"hoverLine\").style.display = \"block\";\n $(\"hoverLine\").style.left = x + \"px\";\n $(\"hoverTime\").style.display = \"block\";\n $(\"hoverTime\").style.left = x + \"px\";\n $(\"hoverTime\").textContent = fmt(f);\n if (dragging && f !== frame) seek(f);\n });\n timeline.addEventListener(\"pointerleave\", function () {\n $(\"hoverLine\").style.display = \"none\";\n $(\"hoverTime\").style.display = \"none\";\n });\n timeline.addEventListener(\"pointerup\", function () { dragging = false; });\n timeline.addEventListener(\"pointercancel\", function () { dragging = false; });\n\n // ── Notes: pause, click the picture (or drag an area) and say what should\n // change. Each is saved with its moment, its spot and a picture of the\n // frame with the spot marked, for whoever edits the film ──\n var notes = [], notesOn = false, draft = null, openId = null, saving = false, press = null;\n var pop = $(\"pop\"), pins = $(\"pins\"), NL = String.fromCharCode(10);\n var ICON_CHECK = '<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3.5 8.5l3 3 6-7\"/></svg>';\n var ICON_X = '<svg viewBox=\"0 0 16 16\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"/></svg>';\n\n function pct1(v) { return Math.round(v * 100) + \"%\"; }\n function whereText(n) {\n return n.w != null ? \"area \" + pct1(n.x) + \",\" + pct1(n.y) + \" to \" + pct1(n.x + n.w) + \",\" + pct1(n.y + n.h) : \"spot \" + pct1(n.x) + \",\" + pct1(n.y);\n }\n function noteById(id) { return notes.find(function (n) { return n.id === id; }); }\n function openCount() { return notes.filter(function (n) { return !n.done; }).length; }\n\n function renderNotes() {\n renderPins();\n renderMarks();\n renderList();\n var open = openCount(), count = $(\"notesCount\");\n count.textContent = String(open || notes.length);\n count.classList.toggle(\"open\", open > 0);\n $(\"notesBtn\").title = open + \" open of \" + notes.length;\n }\n function renderPins() {\n if (!meta) return;\n stage.classList.toggle(\"can-note\", notesOn && !!player && !playing);\n stage.classList.toggle(\"hint\", notes.length === 0 && !draft);\n pins.replaceChildren();\n if (playing) return;\n var here = notes.filter(function (n) { return n.frame === frame; });\n if (draft && draft.frame === frame) here.push(draft);\n here.forEach(function (n) {\n if (n.w != null) {\n var a = el(\"div\", \"area\" + (n.done ? \" done\" : \"\"));\n a.style.left = n.x * 100 + \"%\"; a.style.top = n.y * 100 + \"%\";\n a.style.width = n.w * 100 + \"%\"; a.style.height = n.h * 100 + \"%\";\n pins.append(a);\n }\n var pin = el(\"button\", \"pin\" + (n.done ? \" done\" : \"\") + (n === draft ? \" draft\" : \"\") + (n.id === openId ? \" active\" : \"\"));\n pin.style.left = n.x * 100 + \"%\"; pin.style.top = n.y * 100 + \"%\";\n if (n === draft) { pin.setAttribute(\"aria-label\", \"New note\"); }\n else {\n if (n.done) pin.innerHTML = ICON_CHECK; else pin.textContent = String(n.id);\n pin.title = n.text;\n pin.setAttribute(\"aria-label\", \"Note \" + n.id);\n pin.addEventListener(\"click\", function (e) { e.stopPropagation(); openCard(n.id); });\n }\n pins.append(pin);\n });\n }\n function renderMarks() {\n var marks = $(\"noteMarks\");\n marks.replaceChildren();\n if (!meta) return;\n // Notes close together stack, up to three high.\n var width = tlInner.clientWidth || 800, lanes = [];\n notes.slice().sort(function (a, b) { return a.frame - b.frame || a.id - b.id; }).forEach(function (n) {\n var x = (pct(n.frame) / 100) * width, lane = lanes.findIndex(function (end) { return x - end >= 16; });\n if (lane < 0) lane = lanes.length < 3 ? lanes.length : 2;\n lanes[lane] = x;\n var m = el(\"button\", \"note-mark\" + (n.done ? \" done\" : \"\") + (lane ? \" stacked\" : \"\"), String(n.id));\n m.style.left = pct(n.frame) + \"%\";\n m.style.top = 4 + lane * 15 + \"px\";\n m.title = n.id + \" · \" + fmt(n.frame) + \" · \" + n.text;\n m.addEventListener(\"pointerdown\", function (e) { e.stopPropagation(); });\n m.addEventListener(\"click\", function (e) { e.stopPropagation(); goToNote(n.id); });\n marks.append(m);\n });\n }\n function renderList() {\n var list = $(\"notesList\");\n list.replaceChildren();\n $(\"notesCopy\").disabled = openCount() === 0;\n if (!notes.length) {\n list.append(el(\"div\", \"np-empty\", \"Pause and click the picture to pin a note to that spot and moment. Drag to mark an area.\"));\n }\n notes.slice().sort(function (a, b) { return a.frame - b.frame || a.id - b.id; }).forEach(function (n) {\n var row = el(\"div\", \"note-row\" + (n.done ? \" done\" : \"\"));\n var body = el(\"div\");\n body.append(el(\"div\", \"when tnum\", fmt(n.frame) + \" · frame \" + n.frame), el(\"div\", \"text\", n.text));\n var check = el(\"button\", \"check\" + (n.done ? \" on\" : \"\"));\n check.innerHTML = ICON_CHECK;\n check.title = n.done ? \"Reopen\" : \"Mark done\";\n check.setAttribute(\"aria-label\", check.title);\n check.addEventListener(\"click\", function (e) { e.stopPropagation(); setDone(n.id, !n.done); });\n row.append(el(\"span\", \"badge\" + (n.done ? \" done\" : \"\"), String(n.id)), body, check);\n row.addEventListener(\"click\", function () { goToNote(n.id); });\n list.append(row);\n });\n var dir = meta && meta.notesDir ? meta.notesDir.split(\"/\").slice(-2).join(\"/\") : \"\";\n $(\"notesFoot\").textContent = \"Saved in \" + dir + \" with a picture of each spot. Ask your agent to go through them.\";\n }\n\n function notesRequest(body) {\n return fetch(\"/@framefields/notes\", { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: JSON.stringify(body) })\n .then(function (r) { if (!r.ok) throw new Error(\"HTTP \" + r.status); return r.json(); });\n }\n function upsert(note) {\n var i = notes.findIndex(function (n) { return n.id === note.id; });\n if (i >= 0) notes[i] = note; else notes.push(note);\n renderNotes();\n }\n function setDone(id, done) {\n notesRequest({ op: \"update\", id: id, done: done }).then(function (n) {\n upsert(n);\n if (openId === id) openCard(id);\n }, function () { toast(\"Couldn't save. Is the preview running?\"); });\n }\n function removeNote(id) {\n notesRequest({ op: \"remove\", id: id }).then(function () {\n notes = notes.filter(function (n) { return n.id !== id; });\n closePop();\n renderNotes();\n }, function () { toast(\"Couldn't delete. Is the preview running?\"); });\n }\n function goToNote(id) {\n var n = noteById(id);\n if (!n) return;\n $(\"notesPanel\").classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n seek(n.frame).then(function () { openCard(id); });\n }\n\n var toastTimer = null;\n function toast(text) {\n var t = $(\"toast\");\n t.textContent = text;\n t.classList.add(\"on\");\n clearTimeout(toastTimer);\n toastTimer = setTimeout(function () { t.classList.remove(\"on\"); }, 2200);\n }\n\n // ── Placing a note on the picture ──\n function fracAt(e) {\n var r = stage.getBoundingClientRect();\n return { x: Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)), y: Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)), r: r };\n }\n stage.addEventListener(\"pointerdown\", function (e) {\n if (e.button !== 0 || !player || e.target.closest(\".pin\")) return;\n if (playing) { pause(); return; }\n if (!notesOn) return;\n if (draft && pop.querySelector(\"textarea\") && pop.querySelector(\"textarea\").value.trim()) {\n pop.querySelector(\"textarea\").focus();\n return;\n }\n cancelDraft();\n var p = fracAt(e);\n press = { x: p.x, y: p.y, cx: e.clientX, cy: e.clientY, area: null };\n stage.setPointerCapture(e.pointerId);\n e.preventDefault();\n });\n stage.addEventListener(\"pointermove\", function (e) {\n if (!press) return;\n var p = fracAt(e);\n if (!press.area && Math.hypot(e.clientX - press.cx, e.clientY - press.cy) < 6) return;\n press.area = { x: Math.min(press.x, p.x), y: Math.min(press.y, p.y), w: Math.abs(p.x - press.x), h: Math.abs(p.y - press.y) };\n var m = $(\"marquee\");\n m.style.display = \"block\";\n m.style.left = press.area.x * 100 + \"%\"; m.style.top = press.area.y * 100 + \"%\";\n m.style.width = press.area.w * 100 + \"%\"; m.style.height = press.area.h * 100 + \"%\";\n });\n function endPress() {\n if (!press) return;\n var a = press.area;\n $(\"marquee\").style.display = \"none\";\n draft = a && a.w > 0.01 && a.h > 0.01\n ? { frame: frame, x: a.x, y: a.y, w: a.w, h: a.h, text: \"\" }\n : { frame: frame, x: press.x, y: press.y, text: \"\" };\n press = null;\n openComposer();\n }\n stage.addEventListener(\"pointerup\", endPress);\n stage.addEventListener(\"pointercancel\", function () { press = null; $(\"marquee\").style.display = \"none\"; });\n\n // ── The popover: writing a note, or reading one ──\n var popAnchor = null;\n function placePop() {\n if (!popAnchor || !pop.classList.contains(\"on\")) return;\n var r = stage.getBoundingClientRect(), n = popAnchor;\n var ax = r.left + (n.x + (n.w || 0)) * r.width, ay = r.top + (n.y + (n.h || 0)) * r.height;\n var w = pop.offsetWidth, h = pop.offsetHeight, gap = 16;\n var left = ax + gap + w <= innerWidth - 8 ? ax + gap : ax - gap - w - (n.w || 0) * r.width;\n var top = ay + gap + h <= innerHeight - 8 ? ay + gap / 2 : ay - h - gap / 2 - (n.h || 0) * r.height;\n pop.style.left = Math.max(8, Math.min(innerWidth - w - 8, left)) + \"px\";\n pop.style.top = Math.max(8, Math.min(innerHeight - h - 8, top)) + \"px\";\n }\n function showPop(anchor) {\n popAnchor = anchor;\n pop.classList.add(\"on\");\n placePop();\n }\n function closePop() {\n pop.classList.remove(\"on\");\n pop.replaceChildren();\n popAnchor = null;\n openId = null;\n renderPins();\n }\n function cancelDraft() {\n if (!draft) return;\n draft = null;\n closePop();\n }\n function openComposer() {\n openId = null;\n var head = el(\"div\", \"pop-head\");\n head.append(el(\"span\", \"badge\", \"+\"), el(\"span\", \"tnum\", fmt(draft.frame) + \" · \" + (draft.w != null ? \"area\" : \"spot\")));\n var ta = document.createElement(\"textarea\");\n ta.placeholder = \"What should change here?\";\n ta.rows = 2;\n ta.setAttribute(\"aria-label\", \"Note\");\n var foot = el(\"div\", \"pop-foot\"), status = el(\"span\", \"grow\", \"Enter to add\");\n var cancel = el(\"button\", \"btn\", \"Cancel\"), add = el(\"button\", \"btn primary\", \"Add note\");\n add.disabled = true;\n foot.append(status, cancel, add);\n pop.replaceChildren(head, ta, foot);\n ta.addEventListener(\"input\", function () {\n draft.text = ta.value;\n add.disabled = !ta.value.trim() || saving;\n ta.style.height = \"auto\";\n ta.style.height = Math.min(180, ta.scrollHeight) + \"px\";\n placePop();\n });\n ta.addEventListener(\"keydown\", function (e) {\n if (e.key === \"Enter\" && !e.shiftKey && !e.isComposing) { e.preventDefault(); saveDraft(ta, add, status); }\n if (e.key === \"Escape\") { e.preventDefault(); cancelDraft(); }\n });\n cancel.addEventListener(\"click\", cancelDraft);\n add.addEventListener(\"click\", function () { saveDraft(ta, add, status); });\n renderPins();\n showPop(draft);\n ta.focus();\n }\n async function saveDraft(ta, add, status) {\n var d = draft, text = ta.value.trim();\n if (!d || !text || saving) return;\n saving = true;\n ta.readOnly = true;\n add.disabled = true;\n status.className = \"grow\";\n status.textContent = \"Saving…\";\n try {\n var image = null;\n try { image = await capture(d); } catch (err) { report(\"warning\", [\"[framefields] no picture for the note:\", err]); }\n var note = await notesRequest({ op: \"add\", note: { frame: d.frame, time: d.frame / meta.fps, x: d.x, y: d.y, w: d.w, h: d.h, text: text, image: image } });\n if (draft === d) draft = null;\n closePop();\n upsert(note);\n toast(\"Note \" + note.id + \" saved\");\n } catch (err) {\n status.className = \"grow err\";\n status.textContent = \"Couldn't save. Is the preview running?\";\n ta.readOnly = false;\n add.disabled = false;\n } finally {\n saving = false;\n }\n }\n // The frame with the spot marked, small enough to read at a glance.\n async function capture(d) {\n var bmp = await player.snapshot(d.frame);\n var scale = Math.min(1, 1600 / bmp.width), w = Math.round(bmp.width * scale), h = Math.round(bmp.height * scale);\n var c = document.createElement(\"canvas\");\n c.width = w; c.height = h;\n var g = c.getContext(\"2d\");\n g.drawImage(bmp, 0, 0, w, h);\n bmp.close();\n var lw = Math.max(3, w / 400);\n function mark(draw) {\n // Dark, white, then blue: it reads on any picture.\n g.lineWidth = lw + 5; g.strokeStyle = \"rgba(0,0,0,0.5)\"; draw();\n g.lineWidth = lw + 2.5; g.strokeStyle = \"#fff\"; draw();\n g.lineWidth = lw; g.strokeStyle = \"#3b82f6\"; draw();\n }\n if (d.w != null) {\n mark(function () { g.strokeRect(d.x * w, d.y * h, d.w * w, d.h * h); });\n } else {\n var rad = Math.max(16, w / 40);\n mark(function () { g.beginPath(); g.arc(d.x * w, d.y * h, rad, 0, Math.PI * 2); g.stroke(); });\n g.fillStyle = \"#3b82f6\";\n g.beginPath(); g.arc(d.x * w, d.y * h, lw, 0, Math.PI * 2); g.fill();\n }\n return c.toDataURL(\"image/jpeg\", 0.88);\n }\n function openCard(id) {\n var n = noteById(id);\n if (!n) return;\n if (draft) cancelDraft();\n openId = id;\n var head = el(\"div\", \"pop-head\");\n var close = el(\"button\", \"btn x-btn\");\n close.innerHTML = ICON_X;\n close.title = \"Close\";\n close.setAttribute(\"aria-label\", \"Close\");\n close.addEventListener(\"click\", closePop);\n head.append(el(\"span\", \"badge\" + (n.done ? \" done\" : \"\"), String(n.id)), el(\"span\", \"tnum grow\", fmt(n.frame) + \" · frame \" + n.frame), close);\n var foot = el(\"div\", \"pop-foot\");\n var done = el(\"button\", \"btn\");\n done.innerHTML = ICON_CHECK;\n done.append(document.createTextNode(n.done ? \"Reopen\" : \"Mark done\"));\n done.addEventListener(\"click\", function () { setDone(n.id, !n.done); });\n var del = el(\"button\", \"btn danger\", \"Delete\");\n del.addEventListener(\"click\", function () { removeNote(n.id); });\n foot.append(done, el(\"span\", \"grow\"), del);\n pop.replaceChildren(head, el(\"div\", \"pop-text\", n.text), foot);\n renderPins();\n showPop(n);\n }\n\n document.addEventListener(\"pointerdown\", function (e) {\n if (pop.classList.contains(\"on\") && !pop.contains(e.target) && !stage.contains(e.target) && !e.target.closest(\".note-mark, .note-row\")) {\n if (openId != null) closePop();\n else if (draft && !draft.text.trim()) cancelDraft();\n }\n var panel = $(\"notesPanel\");\n if (panel.classList.contains(\"on\") && !panel.contains(e.target) && !$(\"notesBtn\").contains(e.target)) {\n panel.classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n }\n });\n document.addEventListener(\"keydown\", function (e) {\n if (e.key !== \"Escape\") return;\n if (openId != null) closePop();\n $(\"notesPanel\").classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n });\n window.addEventListener(\"resize\", placePop);\n new ResizeObserver(placePop).observe(stage);\n\n $(\"notesBtn\").addEventListener(\"click\", function () {\n var on = $(\"notesPanel\").classList.toggle(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", String(on));\n });\n $(\"notesClose\").addEventListener(\"click\", function () {\n $(\"notesPanel\").classList.remove(\"on\");\n $(\"notesBtn\").setAttribute(\"aria-expanded\", \"false\");\n });\n // The open notes as text, for a chat that can't read the notes folder.\n $(\"notesCopy\").addEventListener(\"click\", function () {\n var lines = [\"Notes on the preview of \" + JSON.stringify(meta.title) + \":\"];\n notes.filter(function (n) { return !n.done; }).sort(function (a, b) { return a.frame - b.frame; }).forEach(function (n) {\n lines.push(\"- Note \" + n.id + \" at \" + fmt(n.frame) + \" (frame \" + n.frame + \"), \" + whereText(n) + \": \" + n.text + (n.image ? \" [picture: \" + n.image + \"]\" : \"\"));\n });\n var text = lines.join(NL), label = $(\"notesCopyLabel\");\n var done = function () { label.textContent = \"Copied\"; setTimeout(function () { label.textContent = \"Copy\"; }, 1600); };\n (navigator.clipboard ? navigator.clipboard.writeText(text) : Promise.reject()).then(done, function () {\n var t = document.createElement(\"textarea\");\n t.value = text;\n document.body.append(t);\n t.select();\n try { document.execCommand(\"copy\"); done(); } catch (e) {}\n t.remove();\n });\n });\n\n function loadNotes() {\n if (!notesOn) return;\n fetch(\"/@framefields/notes\").then(function (r) { return r.json(); }).then(function (list) {\n notes = Array.isArray(list) ? list : [];\n renderNotes();\n }, function () {});\n }\n\n // ── Wiring ──\n $(\"play\").addEventListener(\"click\", togglePlay);\n $(\"prev\").addEventListener(\"click\", function () { seek(frame - 1); });\n $(\"next\").addEventListener(\"click\", function () { seek(frame + 1); });\n $(\"mute\").addEventListener(\"click\", function () { audio.muted = !audio.muted; applyVolume(); });\n $(\"volume\").addEventListener(\"input\", function (e) { audio.volume = Number(e.target.value); audio.muted = audio.volume === 0; applyVolume(); });\n\n // A hidden tab's timers are throttled, so playback would stutter: pause.\n document.addEventListener(\"visibilitychange\", function () {\n if (document.hidden && playing) pause();\n });\n\n // ── For scripts and agents driving the page: seek, play, and read the\n // state instead of clicking at coordinates ──\n var pageState = \"loading\";\n window.framefieldsPreview = {\n /** Shows the frame at a time in seconds; resolves once it is drawn. */\n seek: function (sec) { return ready().then(function () { return seek(Number(sec) * meta.fps); }); },\n /** Shows a frame by number; resolves once it is drawn. */\n seekFrame: function (f) { return ready().then(function () { return seek(f); }); },\n play: function () { return ready().then(play); },\n /** Notes pinned on the page, as saved in notes.json. */\n notes: function () { return notes.slice(); },\n pause: function () { if (playing) pause(); },\n /** Where the preview is: loading, ready or error, and what it shows. */\n state: function () {\n var rate = meter.read().fps;\n return {\n state: pageState,\n error: pageState === \"error\" ? $(\"stageMsg\").textContent : null,\n frame: frame,\n time: meta ? Number((frame / meta.fps).toFixed(3)) : 0,\n playing: playing,\n buffering: stalled,\n audio: audio.state,\n connected: !offline,\n // Frames shown per second over the last 2 s of playback (the last\n // playback's while paused), and how long the last frame took to draw.\n playbackFps: rate == null ? null : Number(rate.toFixed(1)),\n renderMs: player ? Math.round(player.lastRenderMs) : null,\n meta: meta && { title: meta.title, width: meta.width, height: meta.height, fps: meta.fps, frameCount: meta.frameCount, duration: Number((last() / meta.fps).toFixed(3)) }\n };\n }\n };\n var readyWaiters = [];\n function ready() {\n if (player) return Promise.resolve();\n if (pageState === \"error\") return Promise.reject(new Error($(\"stageMsg\").textContent));\n return new Promise(function (resolve, reject) { readyWaiters.push([resolve, reject]); });\n }\n function settleReady(err) {\n readyWaiters.splice(0).forEach(function (w) { err ? w[1](err) : w[0](); });\n }\n\n function applyMeta(m) {\n meta = m;\n document.title = m.title + \" · preview\";\n $(\"title\").textContent = m.title;\n $(\"spec\").textContent = m.width + \"×\" + m.height + \" · \" + m.fps + \" fps · \" + fmt(last());\n $(\"tcDur\").textContent = fmt(last());\n stage.style.setProperty(\"--ar\", m.width + \" / \" + m.height);\n stage.style.setProperty(\"--ar-n\", String(m.width / m.height));\n renderRuler();\n }\n\n new ResizeObserver(function () {\n if (!meta) return;\n renderRuler();\n drawWave();\n renderMarks();\n }).observe(tlInner);\n\n // When the preview command ends the event stream drops and the browser keeps\n // retrying. The next preview of this project (same port) says hello with a\n // new session, and this tab reloads into it.\n var events = new EventSource(\"/events\");\n events.addEventListener(\"hello\", function (e) {\n var s = JSON.parse(e.data).session;\n if (session && s !== session) { location.reload(); return; }\n offline = false;\n renderChip();\n });\n events.addEventListener(\"error\", function () {\n if (!meta) return;\n if (playing) { stop(); keepPosition(); }\n offline = true;\n renderChip();\n });\n events.addEventListener(\"notes\", function (e) {\n notes = JSON.parse(e.data);\n if (openId != null && !noteById(openId)) closePop();\n renderNotes();\n });\n events.addEventListener(\"audio\", function (e) {\n if (meta) loadAudio(JSON.parse(e.data));\n });\n\n fetch(\"/meta\").then(function (r) { return r.json(); }).then(function (m) {\n session = m.session;\n applyMeta(m);\n notesOn = !!m.notesDir;\n $(\"notesBtn\").hidden = !notesOn;\n loadNotes();\n loadAudio(m.audio);\n setTime(startFrame());\n $(\"buffering\").classList.add(\"on\");\n whenPlayer();\n });\n\n // The composition's bundle (loaded below) announces its player.\n function whenPlayer() {\n if (!window.framefieldsPlayer) return;\n window.framefieldsPlayer.then(function (p) {\n player = p;\n $(\"play\").disabled = false;\n show(frame).then(function () {\n pageState = \"ready\";\n renderPins();\n document.documentElement.dataset.preview = \"ready\";\n settleReady();\n scheduleWarm();\n });\n }, function (err) {\n showError(err);\n document.documentElement.dataset.preview = \"error\";\n settleReady(err instanceof Error ? err : new Error(String(err)));\n });\n }\n window.addEventListener(\"framefields:player\", function () { if (meta) whenPlayer(); });\n window.addEventListener(\"error\", function (e) {\n if (!player) showError(e.error || e.message);\n else report(\"error\", [e.error || e.message]);\n });\n window.addEventListener(\"unhandledrejection\", function (e) { report(\"error\", [e.reason]); });\n})();\n</script>\n<script type=\"module\" src=\"/@framefields/player.js\"></script>\n</body>\n</html>\n`;\n","import { spawn } from \"node:child_process\";\nimport { createHash, randomUUID } from \"node:crypto\";\nimport fs from \"node:fs\";\nimport http from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport path from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport type { VirtualMediaData } from \"@framefields/core\";\nimport { encodeStereoWav } from \"../audio/index.js\";\nimport { HeadlessMediaRenderer } from \"../renderer/index.js\";\nimport {\n\tbundleProject,\n\tENGINE_URL,\n\tengineDir,\n\tonnxRuntimeWeb,\n\tvisionModelsDir,\n} from \"./bundle.js\";\nimport { describeNote, type NewNote, NoteStore } from \"./notes.js\";\nimport { previewPage } from \"./page.js\";\n\n/** Where the composition comes from: the module that builds it. */\nexport interface PreviewSource {\n\t/** Path or file URL of the module, e.g. `new URL(\"./film.ts\", import.meta.url)`. */\n\tentry: string | URL;\n\t/**\n\t * The export to preview: a function returning a `Composition` (it may be\n\t * async), or a `Composition`. Defaults to `\"default\"`.\n\t */\n\texport?: string;\n}\n\n/** What the server needs from the composition: its size, timing and soundtrack. */\ninterface PreviewTarget {\n\ttoVirtualMedia?(): VirtualMediaData;\n\ttoVirtualMediaAsync?(): Promise<VirtualMediaData>;\n\tcomputeDuration?(): Promise<number>;\n\tprepareMediaTime?(): Promise<void>;\n\tfps?: number;\n\tdurationMs?: number;\n\twidth?: number;\n\theight?: number;\n}\n\nexport interface PreviewOptions {\n\t/**\n\t * Port to listen on. Defaults to a port derived from the working directory,\n\t * so every preview of a project lands on the same URL and an open tab picks\n\t * up the next one by itself. A preview already running there is replaced.\n\t */\n\tport?: number;\n\t/** Interface to bind. Defaults to \"127.0.0.1\" so the preview never leaves the machine. */\n\thost?: string;\n\t/**\n\t * Also open the page in the system's default browser. Off by default: the\n\t * caller shows the URL where it suits, e.g. an agent app's own browser\n\t * pane. Skipped when a tab from an earlier preview reconnects on its own.\n\t */\n\topen?: boolean;\n\t/** Title shown in the page header. */\n\ttitle?: string;\n\t/** Mix and play the composition's audio. Defaults to true. */\n\taudio?: boolean;\n\t/** Optional shared renderer instance, used to mix the soundtrack. */\n\trenderer?: HeadlessMediaRenderer;\n\t/**\n\t * Close the server this many milliseconds after the last open tab goes\n\t * away (closed, not reloaded). Defaults to 5000. `false` keeps it serving\n\t * until `close()`. Nothing closes before a tab has connected.\n\t */\n\tidleCloseMs?: number | false;\n\t/**\n\t * Files the page may load by absolute path (the composition's assets).\n\t * Defaults to the project that holds the entry: the nearest `.git`, else\n\t * the nearest `package.json`, else the entry's directory. Pass one path or\n\t * several to allow assets kept outside it.\n\t */\n\troot?: string | string[];\n\t/**\n\t * Where the preview reports what happens out of sight: the page opening,\n\t * errors in the page (a failed build, a frame that threw, no WebGPU), and\n\t * why the server stopped. Defaults to stderr, so whoever runs the preview\n\t * (an agent's background command, say) reads them in its output. `false`\n\t * keeps it quiet.\n\t */\n\tlog?: ((line: string) => void) | false;\n\t/**\n\t * Where notes pinned on the page are kept: `notes.json` and, per note, the\n\t * frame with its spot marked. Defaults to `.framefields/preview-notes` in the\n\t * working directory (`.framefields` ignores itself in git). `false` turns\n\t * notes off.\n\t */\n\tnotesDir?: string | false;\n}\n\n/** Why a preview stopped. */\nexport type PreviewCloseReason =\n\t/** `close()` was called. */\n\t| \"closed\"\n\t/** Its last tab closed (see `idleCloseMs`). */\n\t| \"idle\"\n\t/** A newer preview of the project took over its port. */\n\t| \"replaced\";\n\nexport interface PreviewSession {\n\turl: string;\n\tclose(): Promise<void>;\n\t/** Resolves with the reason when the server stops. */\n\tclosed: Promise<PreviewCloseReason>;\n}\n\ninterface PreviewMeta {\n\tsession: string;\n\t/** Where notes are kept; null when they are off. */\n\tnotesDir: string | null;\n\ttitle: string;\n\tfps: number;\n\tframeCount: number;\n\twidth: number;\n\theight: number;\n}\n\ntype AudioStatus =\n\t| { state: \"mixing\" }\n\t| { state: \"ready\"; durationSec: number; peaks: number[] }\n\t| { state: \"none\" }\n\t| { state: \"error\"; error: string };\n\nconst PEAK_BINS = 1200;\n/** Requests carrying this header come from another preview process, not a page. */\nconst TAKEOVER_HEADER = \"x-framefields-preview\";\nconst PLAYER_PATH = \"/@framefields/player.js\";\n\n/**\n * Serves a localhost player for a composition. The page loads the\n * composition's own code, bundled with the engine, and renders its frames\n * with WebGPU in the browser, like the editor's player; nothing is streamed.\n * The server provides the bundle, the project's files, and the soundtrack,\n * which it mixes with the export engine.\n *\n * The server runs until its tab closes or `close()`. Starting another preview\n * on the same port (the default for a project) stops this one, and open tabs\n * reload into it.\n */\nexport async function startPreview(\n\tsource: PreviewSource,\n\toptions: PreviewOptions = {},\n): Promise<PreviewSession> {\n\tconst entry =\n\t\tsource.entry instanceof URL || String(source.entry).startsWith(\"file:\")\n\t\t\t? fileURLToPath(source.entry)\n\t\t\t: path.resolve(String(source.entry));\n\tconst exportName = source.export ?? \"default\";\n\t// Vision nodes load their cached models, and onnxruntime-web's files when\n\t// the project has it installed.\n\tconst modelsDir = visionModelsDir();\n\tconst ort = onnxRuntimeWeb(path.dirname(entry));\n\tconst roots = [\n\t\t...(options.root === undefined\n\t\t\t? [repoRoot(path.dirname(entry))]\n\t\t\t: Array.isArray(options.root)\n\t\t\t\t? options.root\n\t\t\t\t: [options.root]),\n\t\tmodelsDir,\n\t\tort.dir,\n\t]\n\t\t.filter((r): r is string => !!r)\n\t\t.map((r) => path.resolve(r));\n\tconst sseClients = new Set<http.ServerResponse>();\n\tconst idleCloseMs = options.idleCloseMs ?? 5000;\n\tlet idleTimer: NodeJS.Timeout | undefined;\n\tconst title = options.title ?? \"framefields preview\";\n\tconst session = randomUUID();\n\tconst log =\n\t\toptions.log === false\n\t\t\t? () => {}\n\t\t\t: (options.log ?? ((line: string) => console.error(line)));\n\tconst say = (line: string) => log(`[framefields preview] ${line}`);\n\t// Each distinct page error is reported once; playback repeats them per frame.\n\tconst reported = new Set<string>();\n\tlet pageOpened = false;\n\n\tconst notes =\n\t\toptions.notesDir === false\n\t\t\t? undefined\n\t\t\t: new NoteStore(\n\t\t\t\t\tpath.resolve(\n\t\t\t\t\t\toptions.notesDir ??\n\t\t\t\t\t\t\tpath.join(process.cwd(), \".framefields\", \"preview-notes\"),\n\t\t\t\t\t),\n\t\t\t\t);\n\t// Changes run one at a time: each reads and rewrites notes.json.\n\tlet noteQueue: Promise<unknown> = Promise.resolve();\n\n\tconst target = await loadTarget(entry, exportName);\n\tconst meta = await resolveMeta(target, title, session, notes?.dir ?? null);\n\t// The project's own code, bundled against the browser engine; the engine\n\t// itself is prebuilt (or built once, running from source).\n\tlet bundle: Promise<string> | undefined;\n\tconst playerBundle = () => {\n\t\tif (!bundle) {\n\t\t\tconst built = bundleProject({\n\t\t\t\tentry,\n\t\t\t\texportName,\n\t\t\t\tmodelsDir,\n\t\t\t\tortBase: ort.base,\n\t\t\t}).then((b) => b.code);\n\t\t\tbundle = built;\n\t\t\tbuilt.catch((err: unknown) => {\n\t\t\t\tsay(\n\t\t\t\t\t`could not build ${path.relative(process.cwd(), entry)}:\\n${errorText(err)}`,\n\t\t\t\t);\n\t\t\t\t// Let a later request try again, unless it started its own build.\n\t\t\t\tif (bundle === built) bundle = undefined;\n\t\t\t});\n\t\t}\n\t\treturn bundle;\n\t};\n\tconst engine = engineDir();\n\tengine.catch(() => {});\n\n\t// ── Audio: mixed once, by the export engine ──\n\tlet audio: AudioStatus = {\n\t\tstate: options.audio === false ? \"none\" : \"mixing\",\n\t};\n\tlet audioWav: Buffer | undefined;\n\n\tfunction broadcast(event: string, data: unknown) {\n\t\tfor (const client of sseClients) {\n\t\t\tclient.write(`event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`);\n\t\t}\n\t}\n\n\tfunction setAudio(next: AudioStatus) {\n\t\taudio = next;\n\t\tbroadcast(\"audio\", audio);\n\t}\n\n\tfunction startAudio() {\n\t\tif (audio.state !== \"mixing\") return;\n\t\tif (!target.toVirtualMediaAsync && !target.toVirtualMedia) {\n\t\t\tsetAudio({ state: \"none\" });\n\t\t\treturn;\n\t\t}\n\t\tconst renderer = options.renderer ?? new HeadlessMediaRenderer();\n\t\t(async () => {\n\t\t\tconst vm = target.toVirtualMediaAsync\n\t\t\t\t? await target.toVirtualMediaAsync()\n\t\t\t\t: (target.toVirtualMedia as () => VirtualMediaData)();\n\t\t\t// Live signal time is recorded frame by frame; finish it before mixing.\n\t\t\tawait target.prepareMediaTime?.();\n\t\t\treturn renderer.renderAudio(vm, { fps: meta.fps });\n\t\t})().then(\n\t\t\t({ channels, sampleRate }) => {\n\t\t\t\tconst left = channels[0] ?? new Float32Array(0);\n\t\t\t\tconst right = channels[1] ?? left;\n\t\t\t\tconst peaks = computePeaks(left, right, PEAK_BINS);\n\t\t\t\tif (left.length === 0 || Math.max(...peaks) < 0.001) {\n\t\t\t\t\tsetAudio({ state: \"none\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\taudioWav = encodeStereoWav([left, right], { sampleRate });\n\t\t\t\tsetAudio({\n\t\t\t\t\tstate: \"ready\",\n\t\t\t\t\tdurationSec: left.length / sampleRate,\n\t\t\t\t\tpeaks: peaks.map((p) => Math.round(p * 1000) / 1000),\n\t\t\t\t});\n\t\t\t},\n\t\t\t(err: unknown) => {\n\t\t\t\tsetAudio({\n\t\t\t\t\tstate: \"error\",\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t});\n\t\t\t},\n\t\t);\n\t}\n\n\tlet resolveClosed!: (reason: PreviewCloseReason) => void;\n\tconst closed = new Promise<PreviewCloseReason>((r) => {\n\t\tresolveClosed = r;\n\t});\n\tlet closing: Promise<void> | undefined;\n\tfunction close(reason: PreviewCloseReason = \"closed\"): Promise<void> {\n\t\tclosing ??= new Promise<void>((resolve) => {\n\t\t\tclearTimeout(idleTimer);\n\t\t\tif (reason === \"idle\") say(\"stopped: its tab was closed\");\n\t\t\tif (reason === \"replaced\") say(\"stopped: a newer preview took over\");\n\t\t\tfor (const client of sseClients) client.end();\n\t\t\tsseClients.clear();\n\t\t\tserver.close(() => {\n\t\t\t\tresolve();\n\t\t\t\tresolveClosed(reason);\n\t\t\t});\n\t\t\tserver.closeAllConnections();\n\t\t});\n\t\treturn closing;\n\t}\n\n\tconst server = http.createServer(async (req, res) => {\n\t\tconst url = new URL(req.url ?? \"/\", \"http://localhost\");\n\t\t// Cross-origin isolation: lets onnxruntime-web use SharedArrayBuffer and\n\t\t// threads for vision models (single-threaded otherwise). \"credentialless\"\n\t\t// keeps cross-origin assets such as the font stylesheet loading.\n\t\tres.setHeader(\"cross-origin-opener-policy\", \"same-origin\");\n\t\tres.setHeader(\"cross-origin-embedder-policy\", \"credentialless\");\n\t\ttry {\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/\") {\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"text/html; charset=utf-8\",\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(previewPage);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/meta\") {\n\t\t\t\tsendJson(res, 200, { ...meta, audio });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname.startsWith(`${ENGINE_URL}/`)) {\n\t\t\t\tconst dir = await engine;\n\t\t\t\tconst file = path.resolve(\n\t\t\t\t\tdir,\n\t\t\t\t\t`.${url.pathname.slice(ENGINE_URL.length)}`,\n\t\t\t\t);\n\t\t\t\tif (isInside(dir, file) && (await isFile(file))) {\n\t\t\t\t\tawait sendFile(req, res, file);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tsendJson(res, 404, { error: \"not found\" });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === PLAYER_PATH) {\n\t\t\t\tlet code: string;\n\t\t\t\ttry {\n\t\t\t\t\tawait engine;\n\t\t\t\t\tcode = await playerBundle();\n\t\t\t\t} catch (err) {\n\t\t\t\t\t// Shown on the page: usually an error in the composition's code.\n\t\t\t\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\t\t\t\tcode = `throw new Error(${JSON.stringify(`Could not build the preview:\\n${message}`)});`;\n\t\t\t\t}\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"text/javascript; charset=utf-8\",\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(code);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// `fs.existsSync` / `statSync` in the page: a missing file is an\n\t\t\t// answer here, not a 404 the browser logs as an error.\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/@framefields/stat\") {\n\t\t\t\tconst file = path.resolve(url.searchParams.get(\"path\") ?? \"\");\n\t\t\t\tlet size = -1;\n\t\t\t\tif (roots.some((r) => isInside(r, file))) {\n\t\t\t\t\tsize = await fs.promises.stat(file).then(\n\t\t\t\t\t\t(st) => (st.isFile() ? st.size : -1),\n\t\t\t\t\t\t() => -1,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tsendJson(res, 200, { exists: size >= 0, size: Math.max(0, size) });\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/events\") {\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"text/event-stream\",\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t\tconnection: \"keep-alive\",\n\t\t\t\t});\n\t\t\t\t// Reconnect fast: a tab left open finds the next preview's\n\t\t\t\t// server within a second of it starting.\n\t\t\t\tres.write(\"retry: 1000\\n\");\n\t\t\t\tres.write(\n\t\t\t\t\t`event: hello\\ndata: ${JSON.stringify({ session: meta.session })}\\n\\n`,\n\t\t\t\t);\n\t\t\t\tsseClients.add(res);\n\t\t\t\tclearTimeout(idleTimer);\n\t\t\t\tif (!pageOpened) {\n\t\t\t\t\tpageOpened = true;\n\t\t\t\t\tsay(\n\t\t\t\t\t\tidleCloseMs === false\n\t\t\t\t\t\t\t? \"page opened\"\n\t\t\t\t\t\t\t: `page opened; the server stops ${Number((idleCloseMs / 1000).toFixed(1))} s after its last tab closes`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tonClient?.();\n\t\t\t\treq.on(\"close\", () => {\n\t\t\t\t\tsseClients.delete(res);\n\t\t\t\t\t// A reload reconnects within a second; a closed tab doesn't.\n\t\t\t\t\tif (sseClients.size === 0 && idleCloseMs !== false && !closing) {\n\t\t\t\t\t\tidleTimer = setTimeout(() => {\n\t\t\t\t\t\t\tif (sseClients.size === 0) void close(\"idle\");\n\t\t\t\t\t\t}, idleCloseMs);\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// A newer preview of the same project wants this port. The custom\n\t\t\t// header keeps web pages from triggering it (it forces a CORS preflight).\n\t\t\tif (\n\t\t\t\treq.method === \"POST\" &&\n\t\t\t\turl.pathname === \"/takeover\" &&\n\t\t\t\treq.headers[TAKEOVER_HEADER] === \"1\"\n\t\t\t) {\n\t\t\t\tsendJson(res, 200, { ok: true });\n\t\t\t\tsetImmediate(() => void close(\"replaced\"));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Errors in the page, so they reach the terminal (and an agent)\n\t\t\t// without anyone looking at the browser. JSON only: other sites\n\t\t\t// can't send that without a preflight this server doesn't answer.\n\t\t\tif (\n\t\t\t\treq.method === \"POST\" &&\n\t\t\t\turl.pathname === \"/@framefields/log\" &&\n\t\t\t\treq.headers[\"content-type\"] === \"application/json\"\n\t\t\t) {\n\t\t\t\tconst body = await readBody(req, 16 * 1024);\n\t\t\t\tconst { message, level } = JSON.parse(body) as {\n\t\t\t\t\tmessage?: unknown;\n\t\t\t\t\tlevel?: unknown;\n\t\t\t\t};\n\t\t\t\tconst text = String(message ?? \"\").trim();\n\t\t\t\tif (text && !reported.has(text) && reported.size < 200) {\n\t\t\t\t\treported.add(text);\n\t\t\t\t\tsay(\n\t\t\t\t\t\t`${level === \"warning\" ? \"warning\" : \"error\"} in the page: ${text}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tres.writeHead(204).end();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (notes && url.pathname === \"/@framefields/notes\") {\n\t\t\t\tif (req.method === \"GET\") {\n\t\t\t\t\tsendJson(res, 200, await notes.list());\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tif (\n\t\t\t\t\treq.method === \"POST\" &&\n\t\t\t\t\treq.headers[\"content-type\"] === \"application/json\"\n\t\t\t\t) {\n\t\t\t\t\tconst body = JSON.parse(await readBody(req, 16 * 1024 * 1024)) as {\n\t\t\t\t\t\top?: string;\n\t\t\t\t\t\tid?: number;\n\t\t\t\t\t\tdone?: boolean;\n\t\t\t\t\t\ttext?: string;\n\t\t\t\t\t\tnote?: NewNote;\n\t\t\t\t\t};\n\t\t\t\t\tconst change = noteQueue.then(() =>\n\t\t\t\t\t\tchangeNote(notes, body, meta.fps, say),\n\t\t\t\t\t);\n\t\t\t\t\tnoteQueue = change.catch(() => {});\n\t\t\t\t\tconst note = await change;\n\t\t\t\t\tbroadcast(\"notes\", await notes.list());\n\t\t\t\t\tsendJson(res, note ? 200 : 404, note ?? { error: \"no such note\" });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (req.method === \"GET\" && url.pathname === \"/audio\") {\n\t\t\t\tif (audio.state !== \"ready\" || !audioWav) {\n\t\t\t\t\tsendJson(res, 404, { error: `audio is ${audio.state}` });\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tres.writeHead(200, {\n\t\t\t\t\t\"content-type\": \"audio/wav\",\n\t\t\t\t\t\"content-length\": audioWav.length,\n\t\t\t\t\t\"cache-control\": \"no-store\",\n\t\t\t\t});\n\t\t\t\tres.end(audioWav);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// Fonts referenced by family only resolve to assets/fonts/<Family>.ttf,\n\t\t\t// read from the project's folder, as the export engine does.\n\t\t\tconst fontMatch = /^\\/assets\\/fonts\\/([^/]+)$/.exec(url.pathname);\n\t\t\tif (fontMatch && (req.method === \"GET\" || req.method === \"HEAD\")) {\n\t\t\t\tconst file = path.join(\n\t\t\t\t\tprocess.cwd(),\n\t\t\t\t\t\"assets\",\n\t\t\t\t\t\"fonts\",\n\t\t\t\t\tdecodeURIComponent(fontMatch[1]),\n\t\t\t\t);\n\t\t\t\tif (await isFile(file)) {\n\t\t\t\t\tawait sendFile(req, res, file);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// The composition's files, by absolute path: what the program\n\t\t\t// references loads as-is.\n\t\t\tif (req.method === \"GET\" || req.method === \"HEAD\") {\n\t\t\t\t// The engine asks for local files under Vite's `/@fs/` prefix.\n\t\t\t\tconst file = path.resolve(\n\t\t\t\t\tdecodeURIComponent(url.pathname.replace(/^\\/@fs(?=\\/)/, \"\")),\n\t\t\t\t);\n\t\t\t\tif (roots.some((r) => isInside(r, file)) && (await isFile(file))) {\n\t\t\t\t\tawait sendFile(req, res, file);\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t\tsendJson(res, 404, { error: \"not found\" });\n\t\t} catch (err) {\n\t\t\tif (!res.headersSent) {\n\t\t\t\tsendJson(res, 500, {\n\t\t\t\t\terror: err instanceof Error ? err.message : String(err),\n\t\t\t\t});\n\t\t\t} else {\n\t\t\t\tres.end();\n\t\t\t}\n\t\t}\n\t});\n\n\tlet onClient: (() => void) | undefined;\n\tconst host = options.host ?? \"127.0.0.1\";\n\tconst port = options.port ?? stablePort();\n\tconst listen = (p: number) =>\n\t\tnew Promise<void>((resolve, reject) => {\n\t\t\tconst fail = (err: Error) => {\n\t\t\t\tserver.off(\"listening\", ok);\n\t\t\t\treject(err);\n\t\t\t};\n\t\t\tconst ok = () => {\n\t\t\t\tserver.off(\"error\", fail);\n\t\t\t\tresolve();\n\t\t\t};\n\t\t\tserver.once(\"error\", fail);\n\t\t\tserver.once(\"listening\", ok);\n\t\t\tserver.listen(p, host);\n\t\t});\n\ttry {\n\t\tawait listen(port);\n\t} catch (err) {\n\t\tif ((err as NodeJS.ErrnoException).code !== \"EADDRINUSE\") throw err;\n\t\t// Most likely the previous preview of this project: ask it to step aside.\n\t\tif (await requestTakeover(host, port)) {\n\t\t\tawait listen(port).catch(() => listen(0));\n\t\t} else if (options.port === undefined) {\n\t\t\tawait listen(0);\n\t\t} else {\n\t\t\tthrow err;\n\t\t}\n\t}\n\tconst { port: actualPort } = server.address() as AddressInfo;\n\tconst url = `http://${host.includes(\":\") ? `[${host}]` : host}:${actualPort}/`;\n\n\t// Build the page's bundle and mix the soundtrack while the browser opens.\n\tvoid playerBundle().catch(() => {});\n\tstartAudio();\n\tif (options.open === true) {\n\t\t// A tab from the last preview reconnects within ~1 s and reloads itself;\n\t\t// only open a new one when nobody comes back.\n\t\tconst reconnected = await new Promise<boolean>((resolve) => {\n\t\t\tconst timer = setTimeout(() => resolve(false), 2000);\n\t\t\tonClient = () => {\n\t\t\t\tclearTimeout(timer);\n\t\t\t\tresolve(true);\n\t\t\t};\n\t\t\tif (sseClients.size > 0) onClient();\n\t\t});\n\t\tonClient = undefined;\n\t\tif (!reconnected) openBrowser(url);\n\t}\n\n\treturn { url, closed, close: () => close() };\n}\n\nasync function loadTarget(\n\tentry: string,\n\texportName: string,\n): Promise<PreviewTarget> {\n\tconst mod = (await import(pathToFileURL(entry).href)) as Record<\n\t\tstring,\n\t\tunknown\n\t>;\n\tconst exported = mod[exportName];\n\tif (exported === undefined) {\n\t\tthrow new Error(\n\t\t\t`[framefields] ${entry} has no export named \"${exportName}\" to preview.`,\n\t\t);\n\t}\n\treturn (\n\t\ttypeof exported === \"function\"\n\t\t\t? await (exported as () => unknown)()\n\t\t\t: exported\n\t) as PreviewTarget;\n}\n\n/**\n * The project holding `dir`: the nearest `.git`, else the nearest\n * `package.json`, else `dir` itself. A repo wins over a nested package, so a\n * monorepo's shared assets stay reachable.\n */\nfunction repoRoot(dir: string): string {\n\tlet project: string | undefined;\n\tfor (let d = dir; ; d = path.dirname(d)) {\n\t\tif (fs.existsSync(path.join(d, \".git\"))) return d;\n\t\tif (!project && fs.existsSync(path.join(d, \"package.json\"))) project = d;\n\t\tif (path.dirname(d) === d) return project ?? dir;\n\t}\n}\n\n/** Applies one change from the page and says what it was. */\nasync function changeNote(\n\tnotes: NoteStore,\n\tbody: {\n\t\top?: string;\n\t\tid?: number;\n\t\tdone?: boolean;\n\t\ttext?: string;\n\t\tnote?: NewNote;\n\t},\n\tfps: number,\n\tsay: (line: string) => void,\n) {\n\tif (body.op === \"add\" && body.note && String(body.note.text).trim()) {\n\t\tconst note = await notes.add(body.note);\n\t\tconst files = [\n\t\t\tnote.image && `frame with the spot marked: ${note.image}`,\n\t\t\t`all notes: ${notes.file}`,\n\t\t];\n\t\tsay(`${describeNote(note, fps)}\\n ${files.filter(Boolean).join(\" · \")}`);\n\t\treturn note;\n\t}\n\tconst id = Number(body.id);\n\tif (body.op === \"update\") {\n\t\tconst note = await notes.update(id, body);\n\t\tif (note && typeof body.done === \"boolean\")\n\t\t\tsay(`note ${id} marked ${note.done ? \"done\" : \"open\"}`);\n\t\treturn note;\n\t}\n\tif (body.op === \"remove\") {\n\t\tconst note = await notes.remove(id);\n\t\tif (note) say(`note ${id} deleted`);\n\t\treturn note;\n\t}\n\treturn undefined;\n}\n\nfunction errorText(err: unknown): string {\n\treturn err instanceof Error ? err.message : String(err);\n}\n\nfunction readBody(req: http.IncomingMessage, limit: number): Promise<string> {\n\treturn new Promise((resolve, reject) => {\n\t\tlet body = \"\";\n\t\treq.setEncoding(\"utf8\");\n\t\treq.on(\"data\", (chunk: string) => {\n\t\t\tbody += chunk;\n\t\t\tif (body.length > limit) {\n\t\t\t\treject(new Error(\"request body too large\"));\n\t\t\t\treq.destroy();\n\t\t\t}\n\t\t});\n\t\treq.on(\"end\", () => resolve(body));\n\t\treq.on(\"error\", reject);\n\t});\n}\n\nfunction isInside(root: string, file: string): boolean {\n\tconst rel = path.relative(root, file);\n\treturn !!rel && !rel.startsWith(\"..\") && !path.isAbsolute(rel);\n}\n\nasync function isFile(file: string): Promise<boolean> {\n\ttry {\n\t\treturn (await fs.promises.stat(file)).isFile();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n\t\".png\": \"image/png\",\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".webp\": \"image/webp\",\n\t\".gif\": \"image/gif\",\n\t\".svg\": \"image/svg+xml\",\n\t\".mp4\": \"video/mp4\",\n\t\".mov\": \"video/quicktime\",\n\t\".webm\": \"video/webm\",\n\t\".mp3\": \"audio/mpeg\",\n\t\".wav\": \"audio/wav\",\n\t\".m4a\": \"audio/mp4\",\n\t\".ogg\": \"audio/ogg\",\n\t\".ttf\": \"font/ttf\",\n\t\".otf\": \"font/otf\",\n\t\".woff\": \"font/woff\",\n\t\".woff2\": \"font/woff2\",\n\t\".json\": \"application/json\",\n\t\".js\": \"text/javascript; charset=utf-8\",\n\t\".mjs\": \"text/javascript; charset=utf-8\",\n\t\".wasm\": \"application/wasm\",\n\t\".onnx\": \"application/octet-stream\",\n\t\".srt\": \"text/plain; charset=utf-8\",\n\t\".txt\": \"text/plain; charset=utf-8\",\n};\n\n/** Serves a file, with byte ranges so video can seek without loading it whole. */\nasync function sendFile(\n\treq: http.IncomingMessage,\n\tres: http.ServerResponse,\n\tfile: string,\n) {\n\tconst { size } = await fs.promises.stat(file);\n\tconst type =\n\t\tCONTENT_TYPES[path.extname(file).toLowerCase()] ??\n\t\t\"application/octet-stream\";\n\tconst range = /^bytes=(\\d*)-(\\d*)$/.exec(req.headers.range ?? \"\");\n\tlet start = 0;\n\tlet end = size - 1;\n\tif (range && (range[1] || range[2])) {\n\t\tif (range[1]) {\n\t\t\tstart = Number(range[1]);\n\t\t\tif (range[2]) end = Math.min(end, Number(range[2]));\n\t\t} else {\n\t\t\tstart = Math.max(0, size - Number(range[2]));\n\t\t}\n\t\tif (start > end || start >= size) {\n\t\t\tres.writeHead(416, { \"content-range\": `bytes */${size}` });\n\t\t\tres.end();\n\t\t\treturn;\n\t\t}\n\t}\n\tres.writeHead(range ? 206 : 200, {\n\t\t\"content-type\": type,\n\t\t\"content-length\": end - start + 1,\n\t\t\"accept-ranges\": \"bytes\",\n\t\t\"cache-control\": \"no-cache\",\n\t\t...(range && { \"content-range\": `bytes ${start}-${end}/${size}` }),\n\t});\n\tif (req.method === \"HEAD\") {\n\t\tres.end();\n\t\treturn;\n\t}\n\tfs.createReadStream(file, { start, end }).pipe(res);\n}\n\nasync function resolveMeta(\n\ttarget: PreviewTarget,\n\ttitle: string,\n\tsession: string,\n\tnotesDir: string | null,\n): Promise<PreviewMeta> {\n\tconst fps = target.fps ?? 24;\n\tconst durationMs =\n\t\ttarget.durationMs ?? (await target.computeDuration?.()) ?? 5000;\n\treturn {\n\t\tsession,\n\t\tnotesDir,\n\t\ttitle,\n\t\tfps,\n\t\tframeCount: Math.max(1, Math.round((durationMs / 1000) * fps)),\n\t\twidth: target.width ?? 1920,\n\t\theight: target.height ?? 1080,\n\t};\n}\n\n/** Same project, same port: 41000–41899, from the working directory. */\nfunction stablePort(): number {\n\tconst digest = createHash(\"sha1\").update(process.cwd()).digest();\n\treturn 41000 + (digest.readUInt16BE(0) % 900);\n}\n\n/** Asks a preview already on `port` to shut down; false if something else is there. */\nasync function requestTakeover(host: string, port: number): Promise<boolean> {\n\t// A fresh connection (no agent): a pooled keep-alive socket may point at a\n\t// server that is already gone.\n\tconst ok = await new Promise<boolean>((resolve) => {\n\t\tconst req = http.request(\n\t\t\t{\n\t\t\t\thost,\n\t\t\t\tport,\n\t\t\t\tmethod: \"POST\",\n\t\t\t\tpath: \"/takeover\",\n\t\t\t\tagent: false,\n\t\t\t\theaders: { [TAKEOVER_HEADER]: \"1\" },\n\t\t\t\ttimeout: 1500,\n\t\t\t},\n\t\t\t(res) => {\n\t\t\t\tres.resume();\n\t\t\t\tresolve(res.statusCode === 200);\n\t\t\t},\n\t\t);\n\t\treq.on(\"timeout\", () => req.destroy());\n\t\treq.on(\"error\", () => resolve(false));\n\t\treq.end();\n\t});\n\t// Give it a moment to release the socket.\n\tif (ok) await new Promise((r) => setTimeout(r, 150));\n\treturn ok;\n}\n\n/** Max absolute amplitude of both channels per bin, 0..1. */\nfunction computePeaks(\n\tleft: Float32Array,\n\tright: Float32Array,\n\tbins: number,\n): number[] {\n\tconst peaks = new Array<number>(bins).fill(0);\n\tif (left.length === 0) return peaks;\n\tconst per = left.length / bins;\n\tfor (let b = 0; b < bins; b++) {\n\t\tconst start = Math.floor(b * per);\n\t\tconst end = Math.min(left.length, Math.floor((b + 1) * per));\n\t\tlet max = 0;\n\t\tfor (let i = start; i < end; i++) {\n\t\t\tconst v = Math.max(Math.abs(left[i] ?? 0), Math.abs(right[i] ?? 0));\n\t\t\tif (v > max) max = v;\n\t\t}\n\t\tpeaks[b] = Math.min(1, max);\n\t}\n\treturn peaks;\n}\n\nfunction sendJson(res: http.ServerResponse, status: number, body: unknown) {\n\tres.writeHead(status, {\n\t\t\"content-type\": \"application/json\",\n\t\t\"cache-control\": \"no-store\",\n\t});\n\tres.end(JSON.stringify(body));\n}\n\nfunction openBrowser(url: string) {\n\tconst [cmd, args] =\n\t\tprocess.platform === \"darwin\"\n\t\t\t? [\"open\", [url]]\n\t\t\t: process.platform === \"win32\"\n\t\t\t\t? [\"cmd\", [\"/c\", \"start\", \"\", url]]\n\t\t\t\t: [\"xdg-open\", [url]];\n\ttry {\n\t\tspawn(cmd, args, { stdio: \"ignore\", detached: true })\n\t\t\t.on(\"error\", () => {})\n\t\t\t.unref();\n\t} catch {}\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- import{k as Te,l as Ce,n as he}from"./chunk-HOV6DDBZ.mjs";import{a as Ee}from"./chunk-UZTA2EUU.mjs";import{c as Pe}from"./chunk-P4Y3PMJF.mjs";import{i as me,l as Z}from"./chunk-RECQ74RP.mjs";import{c as Ie,e as be}from"./chunk-32WC4FRR.mjs";import{l as ye}from"./chunk-77MT33VR.mjs";import{f as Y,g as ie}from"./chunk-Y4NIR77Z.mjs";ie();ie();ie();function Re(t,e){return Math.round(t*e)}function fe(t,e,n={timeOffsetSec:0}){if(!t)return t;let{operation:b,metadata:i={},children:a=[]}=t,{timeOffsetSec:y,durationLimitMs:m}=n;if(!b)return{...t,children:(a||[]).map(r=>fe(r,e,n))};let f=b.timeline,c=f?.segments||[],h=Z(t)?.durationMs??0;if(c.length>0){let r=c.reduce((P,s)=>{let F=s.endSec!=null?s.endSec-s.startSec:0,L=Math.max(1,Math.round(F*e));return P+L},0);r>0&&(h=r/e*1e3)}let p={...b},w=f?.startFrame??p.startFrame;if(typeof w=="number"){let r=Re(y,e);p.startFrame=w-r}let S={...i,durationMs:h},C=typeof w=="number"||p.op==="compose"?{timeOffsetSec:0,durationLimitMs:void 0}:{timeOffsetSec:y,durationLimitMs:m},V=(a||[]).map(r=>fe(r,e,C));return{metadata:S,operation:p,children:V}}var Ve=typeof Y<"u"&&/^(debug|trace)$/i.test(Y.env?.LOG_LEVEL??""),I={debug:(t,...e)=>{Ve&&(typeof t=="object"&&t!==null?console.info("[AudioExtractor]",t,...e):console.info(`[AudioExtractor] ${t}`,...e))},warn:(t,...e)=>{typeof t=="object"&&t!==null?console.warn("[AudioExtractor]",t,...e):console.warn(`[AudioExtractor] ${t}`,...e)},error:(t,...e)=>{typeof t=="object"&&t!==null?console.error("[AudioExtractor]",t,...e):console.error(`[AudioExtractor] ${t}`,...e)}},we=()=>{if(typeof window>"u")return Promise.resolve();let t=globalThis.scheduler;return typeof t?.yield=="function"?t.yield():new Promise(e=>setTimeout(e,0))},Be=async(t=3e4)=>{await new Promise(n=>setTimeout(n,100));let e=Date.now();return new Promise(n=>{let b=()=>{let i=typeof window<"u"&&window.renderer_delayRenderHandles?window.renderer_delayRenderHandles:[],a=typeof globalThis<"u"?globalThis.__GATEWAI_DELAYS__:null,y=i.length>0,m=a instanceof Set&&a.size>0;if(!y&&!m){n();return}if(Date.now()-e>t){I.warn(`[AudioExtractor] waitForDelayRender timed out waiting for handles: ${JSON.stringify({browserHandles:i,globalDelays:a?Array.from(a):[]})}`),n();return}setTimeout(b,50)};b()})};async function rt(t){let e=await De(t);return e?{channels:e.channels,sampleRate:e.sampleRate}:null}var ze=.5;async function De(t,e){I.debug(`[AudioExtractor] decodeAudioSource started for url: ${t}`);try{if(typeof window<"u"&&typeof window.document<"u"&&!(typeof Y<"u"&&Y.versions&&Y.versions.node)){I.debug(`[AudioExtractor] Browser detected. Fetching arrayBuffer for ${t}...`);let i=window.AudioContext||window.webkitAudioContext;if(!i)throw new Error("AudioContext is not supported in this browser");let a=new i;try{let y=await fetch(t);if(I.debug(`[AudioExtractor] Fetch response status for ${t}: ${y.status} (${y.statusText})`),!y.ok)throw new Error(`Failed to fetch audio source: ${y.statusText}`);let m=await y.arrayBuffer();I.debug(`[AudioExtractor] Fetched ${m.byteLength} bytes. Decoding audio data...`);let f=await a.decodeAudioData(m);I.debug(`[AudioExtractor] Decoded successfully: ${f.numberOfChannels} channels, sampleRate: ${f.sampleRate}, duration: ${f.duration}s`);let c=[];for(let h=0;h<f.numberOfChannels;h++)c.push(new Float32Array(f.getChannelData(h)));return{channels:c,sampleRate:f.sampleRate,startIndex:0,validFromSec:0,validToSec:Number.POSITIVE_INFINITY}}finally{a.close(),I.debug(`[AudioExtractor] AudioContext closed for ${t}`)}}let b=!1;try{I.debug(`[AudioExtractor] Server / Node.js detected. Acquiring from inputStore for ${t}...`);let i=await be.acquire(t);b=!0,I.debug("[AudioExtractor] inputStore acquired. Retrieving primary audio track...");let a=await i.getPrimaryAudioTrack();if(!a)return I.warn(`[AudioExtractor] No primary audio track found in input for ${t}`),null;let y=new Ie(a),m=e?Math.max(0,e.startSec-ze):0,f=y.samples(m,e?.endSec),c=[],h=0,p=48e3,w=1,S=null;I.debug(`[AudioExtractor] Starting mediabunny generator loop for ${t}...`);for await(let r of f){S===null&&(S=r.timestamp),p=r.sampleRate,w=r.numberOfChannels;let P=r.numberOfFrames;if(c.length===0)for(let s=0;s<w;s++)c.push([]);for(let s=0;s<w;s++){let F;try{let L=r.allocationSize({planeIndex:s,format:"f32-planar"});F=new Float32Array(L/4),r.copyTo(F,{planeIndex:s,format:"f32-planar"})}catch{let L=r.allocationSize({planeIndex:s});if(r.format==="s16"||r.format==="s16-planar"){let E=new Int16Array(L/2);r.copyTo(E,{planeIndex:s}),F=new Float32Array(E.length);for(let M=0;M<E.length;M++)F[M]=(E[M]??0)/32768}else if(r.format==="s32"||r.format==="s32-planar"){let E=new Int32Array(L/4);r.copyTo(E,{planeIndex:s}),F=new Float32Array(E.length);for(let M=0;M<E.length;M++)F[M]=(E[M]??0)/2147483648}else if(r.format==="u8"||r.format==="u8-planar"){let E=new Uint8Array(L);r.copyTo(E,{planeIndex:s}),F=new Float32Array(E.length);for(let M=0;M<E.length;M++)F[M]=((E[M]??128)-128)/128}else F=new Float32Array(L/4),r.copyTo(F,{planeIndex:s})}c[s].push(F)}h+=P,r.close()}if(I.debug(`[AudioExtractor] Generator loop finished. Decoded total frames: ${h}`),h===0||c.length===0)return null;let _=c.map(r=>{let P=new Float32Array(h),s=0;for(let F of r)P.set(F,s),s+=F.length;return P}),C=0;if(m>0&&S!==null){let r=await y.getSample(0),P=r?.timestamp??await a.getFirstTimestamp();r?.close(),C=Math.round((S-P)*p)}let V=!e||(C+h)/p<e.endSec;return{channels:_,sampleRate:p,startIndex:C,validFromSec:m>0&&e?e.startSec:0,validToSec:V||!e?Number.POSITIVE_INFINITY:e.endSec}}catch(i){if(i instanceof Error&&i.message.includes("Cannot open local media file"))throw i;if(typeof Y<"u"&&Y.versions?.node)try{let{execFileSync:a}=await import("./child_process-B2TV2STW.mjs"),y=e?Math.round(Math.max(0,e.startSec)*48e3):0,m=y/48e3,f=a("ffmpeg",["-v","error",...m>0?["-ss",String(m)]:[],"-i",t,...e?["-t",String(e.endSec-m)]:[],"-f","f32le","-ar","48000","-ac","2","pipe:1"],{maxBuffer:Number.POSITIVE_INFINITY}),c=f.byteLength/4,h=c/2,p=new Float32Array(f.buffer,f.byteOffset,c),w=new Float32Array(h),S=new Float32Array(h);for(let C=0;C<h;C++)w[C]=p[C*2],S[C]=p[C*2+1];let _=!e||m+h/48e3<e.endSec;return{channels:[w,S],sampleRate:48e3,startIndex:y,validFromSec:m,validToSec:_||!e?Number.POSITIVE_INFINITY:e.endSec}}catch(a){I.warn({ffmpegErr:a},`[AudioExtractor] ffmpeg fallback decode failed for ${t}`)}return I.error({err:i},`[AudioExtractor] Failed to decode source ${t}`),null}finally{b&&be.release(t)}}catch(n){if(n instanceof Error&&n.message.includes("Cannot open local media file"))throw n;return I.error({err:n},`[AudioExtractor] Failed to decode source ${t}`),null}}async function ve(t,e,n,b,i,a,y,m){let f=new Float32Array(b);for(let c=0;c<b;c++){c>0&&c%1e5===0&&await we();let h=y+c/n*m;if(Ce(a,h))continue;let p=Te(a,h,m)*e-i,w=Math.floor(p);if(w<0||w>=t.length)continue;let S=t[w];f[c]=S+((t[w+1]??S)-S)*(p-w)}return f}async function $e(t,e,n,b,i,a,y=0){let m=new Float32Array(i),c=e/n*b,h=1e5;for(let p=0;p<i;p++){p>0&&p%h===0&&await we();let w=a+p*c-y,S=Math.floor(w),_=Math.ceil(w);if(S<0){m[p]=0;continue}if(S>=t.length)break;let C=w-S,V=t[S]??0,r=_>=0&&_<t.length?t[_]??V:V,P=V+C*(r-V);m[p]=Number.isFinite(P)?P:0}return m}var ke=.05,Ue=new Set(["Compositor","CompositorLayer"]);function Ge(t,e,n){let b=Math.max(0,-n),i=Math.min(e.length,t.length-n);for(let a=b;a<i;a++)t[n+a]+=e[a]}var pe=class{constructor(e){this.length=e}length;channels=null;get(){return this.channels??=[new Float32Array(this.length),new Float32Array(this.length)],this.channels}canAdopt(e,n){return this.channels===null&&n===0&&e.every(b=>b.length===this.length)}add(e,n){if(this.canAdopt(e,n)&&e[0]!==e[1]){this.channels=e;return}let b=this.get();for(let i=0;i<2;i++)Ge(b[i],e[i],n)}};function Ye(t){if(t.startsWith("data:"))return"inline audio";let e=t.split(/[?#]/)[0].split(/[\\/]/).pop();return e?He(e):"audio"}function He(t){try{return decodeURIComponent(t)}catch{return t}}async function at(t,e,n=48e3,b,i,a={}){I.debug(`[AudioExtractor] mixAudioTracks started. FPS: ${e}, targetSampleRate: ${n}`),await Be(),Ee.registerSignalsFromComposition(i,t);let y=Z(t)?.durationMs||1e3,m=y/1e3,f=Math.ceil(m*n);I.debug(`[AudioExtractor] Composition total duration: ${y}ms (${m}s). Total mixed samples to allocate: ${f}`);let c=new Map,h=new Set;async function p(o,l,u,g=!0){if(h.has(o))return null;let A=c.get(o),x=g?A?.find(d=>d.validFromSec<=l&&d.validToSec>=u):void 0;if(x)return x;let T=await De(o,{startSec:l,endSec:u});return T?(g&&(A||(A=[],c.set(o,A)),A.push(T)),T):(h.add(o),null)}async function w(o,l,u,g){let A=new Float32Array(l),x=new Float32Array(l);for(let T=0;T<l;){let d=Math.min(l-T,n*2),R=g+T/n*e,j=he(u,R,R+(d-1)/n*e,e);for(;j.max-j.min>3&&d>1;)d=Math.max(1,Math.floor(d/2)),j=he(u,R,R+(d-1)/n*e,e);let v=await p(o,j.min,j.max+ke,!1);if(!v)return null;let K=await ve(v.channels[0],v.sampleRate,n,d,v.startIndex,u,R,e),Q=v.channels.length>1?await ve(v.channels[1],v.sampleRate,n,d,v.startIndex,u,R,e):K;A.set(K,T),x.set(Q,T),T+=d}return[A,x]}let S=new WeakMap;function _(o){let l=S.get(o);if(l!==void 0)return l;let u=o.operation,g=!1;if(u?.op==="source"){let A=me(o);g=A==="Audio"||A==="Video"}else u?.op&&!Ue.has(u.op)&&ye.get(u.op)&&(g=!0);return!g&&o.children&&(g=o.children.some(A=>A&&_(A))),S.set(o,g),g}async function C(o,l,u,g,A,x,T){if(!o||!_(o))return null;let d=o.operation;if(!d){let B=Z(o)?.durationMs??0,z=Math.round(B/1e3*e),ne=T&&x!==null?x:x!==null?z>0?Math.min(x,z):x:z,q=Math.ceil(ne/e*n);if(q<=0)return null;let re=new pe(q);for(let X of o.children){let ae=await C(X,l,u,g,A,ne,T);if(ae){let $=X.operation,de=$?.startFrame??$?.timeline?.startFrame??0,k=Math.round(de/e*n);re.add(ae,k)}}return re.channels}let R=g*(d.volume??1)*(d.opacity===0?0:1)*(d.muted?0:1),j=A,v=d.startFrame??d.timeline?.startFrame??0,K=d.timeRemap,Q=K?{map:K,offsetFrames:0}:T?{map:T.map,offsetFrames:T.offsetFrames+v}:void 0,Se=d.op==="CompositorLayer"?v:u+v,Fe=d.timeline?.segments||[],ee=d,Me=ee.op==="CompositorLayer"?(ee.mediaStartFrame??ee.trimStartFrames??null)!==null?(ee.mediaStartFrame??ee.trimStartFrames??0)/e:Math.round((ee.trimStartSec??0)*e)/e:0,Ne=Z(o)?.durationMs??0,ue=Math.round(Ne/1e3*e),Le=T&&!K&&x!==null?x:x!==null?ue>0?Math.min(x,ue):x:ue,te=Math.ceil(Le/e*n);if(te<=0)return null;let oe=new pe(te),ge=async(B,z)=>{let ne=B?B.startSec:0,q=l+Me+ne,re=Se+z,X=0;B?X=Math.max(1,Math.round(((B.endSec??0)-B.startSec)*e)):X=ue;let ae=T&&!K&&!B&&x!==null?x:x!==null?Math.min(x,X):X,$=Math.round(z/e*n);if(d.op==="source"){let k=me(o),D=Pe(o);if(D.sourceUrl&&(k==="Audio"||k==="Video")&&R>0){let H=Math.max(.5,j*(Number(D.speed)||1)),W=Math.max(0,(Number(D.trimStartSec)||0)+q),se=ae/e,N=Math.min(te-$,Math.ceil(se*n)),ce=Q&&N>0?await w(D.sourceUrl,N,Q.map,Q.offsetFrames+z):null,U=!Q&&N>0&&$<te?await p(D.sourceUrl,W,W+N/n*H+ke):null;if(ce||U){let G,J;if(ce)[G,J]=ce;else if(U){let O=Math.round(W*U.sampleRate);G=await $e(U.channels[0],U.sampleRate,n,H,N,O,U.startIndex),J=U.channels.length>1?await $e(U.channels[1],U.sampleRate,n,H,N,O,U.startIndex):G}else throw new Error("No decoded audio");let Oe=[G,J];if(oe.canAdopt(Oe,$)){if(R!==1){for(let O=0;O<N;O++)G[O]*=R;if(J!==G)for(let O=0;O<N;O++)J[O]*=R}oe.channels=[G,J===G?G.slice():J]}else{let[O,_e]=oe.get();for(let le=0;le<N;le++){let xe=$+le;O[xe]+=G[le]*R,_e[xe]+=J[le]*R}}}await we()}(k==="Audio"||k==="Video")&&(P++,a.onProgress?.({done:P,total:r,...D.sourceUrl&&{source:Ye(D.sourceUrl)}}))}if(o.children)for(let k of o.children){let D=await C(k,q,re,R,j,ae,Q);if(D){let H=k.operation,W=H?.startFrame??H?.timeline?.startFrame??0,se=d.op==="CompositorLayer"&&H?.op==="CompositorLayer"?W-v:W,N=Math.round(se/e*n);oe.add(D,$+N)}}let de=ye.get(d.op);if(de)try{let k=ae/e,D=Math.min(te-$,Math.ceil(k*n));if(D>0&&$<te&&$>=0){let H=oe.get().map(ce=>ce.subarray($,$+D)),W=d.op==="CompositorLayer"?Se:0,se=((re-W)/e+q-Me)*1e3,N=Z(o)?.durationMs??void 0;await de(H,n,o,{device:b,frame:re,fps:e,renderId:i,elapsedMs:se,durationMs:N})}}catch(k){I.error({err:k},`[AudioExtractor] Custom plugin for operation '${d.op}' failed`)}};if(Fe.length>0){let B=0;for(let z of Fe){let ne=(z.endSec??0)-z.startSec,q=Math.max(1,Math.round(ne*e));await ge(z,B),B+=q}}else await ge(null,0);return oe.channels}let V=fe(t,e),r=0,P=0;if(a.onProgress){let o=l=>{if(!l)return;let u=l.operation;if(u?.op==="source"){let g=me(l);(g==="Audio"||g==="Video")&&(r+=Math.max(1,u.timeline?.segments?.length??0))}for(let g of l.children??[])o(g)};o(V),a.onProgress({done:0,total:r})}let s=await C(V,0,0,1,1,null),F=s?.every(o=>o.length===f)&&s[0]!==s[1]?s:[new Float32Array(f),new Float32Array(f)],[L,E]=F;if(s&&F!==s)for(let o=0;o<2;o++){let l=F[o],u=s[o],g=Math.min(u.length,l.length);for(let A=0;A<g;A++)l[A]=u[A]}c.clear();let M=0;for(let o=0;o<f;o++){let l=Math.abs(L[o]??0),u=Math.abs(E[o]??0);l>M&&(M=l),u>M&&(M=u)}let Ae=M>.95?.92/M:1;for(let o=0;o<f;o++){let l=(L[o]??0)*Ae,u=(E[o]??0)*Ae;L[o]=Number.isFinite(l)?Math.tanh(l):0,E[o]=Number.isFinite(u)?Math.tanh(u):0}return{channels:F,sampleRate:n}}export{rt as a,at as b};
@@ -1 +0,0 @@
1
- import{w as x,x as I}from"./chunk-UKZVUYB6.mjs";import{a as g,b as f,d as b,e as m}from"./chunk-32WC4FRR.mjs";import{b as S,j as u,v as D}from"./chunk-64L4GUOF.mjs";import{b as F}from"./chunk-354GX6IB.mjs";import{g as h}from"./chunk-Y4NIR77Z.mjs";h();var p=class{constructor(e,t){this.url=e;this.isHeadless=t}url;isHeadless;input=null;initPromise=null;iterator=null;iteratorStartTimestamp=-1;currentFetchId=0;fetchLock=Promise.resolve();destroyed=!1;async init(){if(!this.input)return this.initPromise?this.initPromise:(this.destroyed=!1,this.initPromise=(async()=>{try{let e=await m.acquire(this.url);this.destroyed?m.release(this.url):this.input=e}catch(e){throw this.initPromise=null,e}})(),this.initPromise)}destroy(){this.destroyed=!0,this.iterator?.return(),this.input&&(m.release(this.url),this.input=null),this.initPromise=null,this.onDestroy()}},y=class extends p{canvasSink=null;videoSampleSink=null;currentFrame=null;nextFrame=null;currentSample=null;nextSample=null;headlessQueue=[];backwardFrames=[];backwardBytes=0;backwardEnd;lastHeadlessTimestamp;isDecoding=!1;decodeBuffer=null;nextRequest=null;async durationSec(){await this.init();let e=await this.input?.getPrimaryVideoTrack();if(!e)throw new Error(`No video track in ${this.url}`);return e.computeDuration()}pushHeadlessQueue(e){let t=this.headlessQueue.findIndex(i=>i.timestamp>e.timestamp);t===-1?this.headlessQueue.push(e):this.headlessQueue.splice(t,0,e)}async fillHeadlessQueue(e){if(this.iterator)for(;this.headlessQueue.length<2;){let t=await this.iterator.next();if(this.currentFetchId!==e){t.value?.close?.();return}if(t.done||!t.value)break;this.pushHeadlessQueue(t.value)}}async closeIterator(e){if(e)try{await e.return()}catch{}}resetSinks(){let e=this.iterator;if(this.iterator=null,e)try{e.return()}catch{}this.canvasSink=null,this.videoSampleSink=null,this.currentFrame=null,this.nextFrame=null,this.currentSample?.close(),this.currentSample=null,this.nextSample?.close(),this.nextSample=null;for(let t of this.headlessQueue)try{t.close()}catch{}this.headlessQueue=[],this.clearBackwardFrames(),this.lastHeadlessTimestamp=void 0,this.decodeBuffer=null,this.iteratorStartTimestamp=-1}clearBackwardFrames(){for(let e of this.backwardFrames)e.close();this.backwardFrames=[],this.backwardBytes=0,this.backwardEnd=void 0}async copyHeadlessSample(e,t){let i=e.displayWidth,r=e.displayHeight,a=i*r*4,s=this.decodeBuffer;return(!s||s.length!==a)&&(s=new Uint8Array(a),this.decodeBuffer=s),await e.copyTo(s,{format:"RGBA"}),this.currentFetchId!==t||this.destroyed||!this.input?null:(F(s),{width:i,height:r,buffer:s})}cachedBackwardFrame(e){let t,i=-1;for(let[a,s]of this.backwardFrames.entries()){if(s.timestamp>e+1e-4)break;t=s,i=a}let r=this.backwardFrames[i+1]?.timestamp??this.backwardEnd;if(!(!t||!(t.duration>0)||e+1e-4>=Math.min(t.timestamp+t.duration,r??1/0)))return t}async fillBackwardFrames(e,t){if(!this.videoSampleSink)return;await this.closeIterator(this.iterator),this.iterator=null;for(let r of this.headlessQueue)r.close();this.headlessQueue=[],this.clearBackwardFrames();let i=this.videoSampleSink.samples(Math.max(0,e-.5));try{for await(let r of i){if(r.timestamp>e+1e-4&&(this.backwardEnd=r.timestamp),this.destroyed||this.currentFetchId!==t||r.timestamp>e+1e-4){r.close();break}let a=r.displayWidth*r.displayHeight*4;if(a>48*1024*1024){r.close();break}for(this.backwardFrames.push(r),this.backwardBytes+=a;this.backwardBytes>48*1024*1024&&this.backwardFrames.length>1;){let s=this.backwardFrames.shift();s&&(this.backwardBytes-=s.displayWidth*s.displayHeight*4,s.close())}}}finally{await this.closeIterator(i)}}onDestroy(){this.resetSinks()}async startIterator(e,t){if(!this.canvasSink)return;let i=Math.max(0,Number.isFinite(e)?e:0),r=this.iterator;if(this.iterator=null,await this.closeIterator(r),this.currentFetchId!==t||this.destroyed)return;this.iterator=this.canvasSink.canvases(i);let a=await this.iterator.next();if(this.currentFetchId!==t||this.destroyed)return;this.currentFrame=a.value??null;let s=await this.iterator.next();this.currentFetchId!==t||this.destroyed||(this.nextFrame=s.value??null,this.iteratorStartTimestamp=i)}async startIteratorHeadless(e,t){if(!this.videoSampleSink)return;let i=Math.max(0,Number.isFinite(e)?e:0),r=this.iterator;if(this.iterator=null,await this.closeIterator(r),this.currentFetchId!==t||this.destroyed)return;for(let s of this.headlessQueue)s.close();this.headlessQueue=[];let a=Math.max(0,i-.1);this.iterator=this.videoSampleSink.samples(a),await this.fillHeadlessQueue(t),!(this.currentFetchId!==t||this.destroyed)&&(this.iteratorStartTimestamp=i)}async getFrame(e){if(await this.init(),this.destroyed||!this.input)return null;let t=Math.max(0,Number.isFinite(e)?e:0);return this.isDecoding?(this.nextRequest&&this.nextRequest.resolve(null),new Promise((i,r)=>{this.nextRequest={timestampSec:t,resolve:i,reject:r}})):this.executeDecode(t)}async executeDecode(e){this.isDecoding=!0;try{return await this.performDecode(e)}finally{if(this.isDecoding=!1,this.nextRequest){let t=this.nextRequest;this.nextRequest=null,this.executeDecode(t.timestampSec).then(t.resolve).catch(t.reject)}}}async performDecodeHeadless(e,t){if(!this.videoSampleSink){if(this.destroyed||!this.input)return null;let d=await this.input.getPrimaryVideoTrack();if(!d)return null;this.videoSampleSink=new g(d)}let i=1e-4,r=1,a=.01,s=this.lastHeadlessTimestamp!==void 0&&e<this.lastHeadlessTimestamp-1e-4;this.lastHeadlessTimestamp=e;let o=this.cachedBackwardFrame(e);if(!o&&s&&(await this.fillBackwardFrames(e,t),o=this.cachedBackwardFrame(e)),o)return this.copyHeadlessSample(o,t);if((!this.iterator||e<this.iteratorStartTimestamp-i||this.headlessQueue.length>0&&e<this.headlessQueue[0].timestamp-a||this.headlessQueue.length>0&&e-this.headlessQueue[0].timestamp>r)&&(await this.startIteratorHeadless(e,t),this.currentFetchId!==t||this.destroyed||!this.input))return null;for(;;){if(await this.fillHeadlessQueue(t),this.currentFetchId!==t||this.destroyed||!this.input)return null;if(this.headlessQueue.length>=2&&this.headlessQueue[1].timestamp<=e+i){this.headlessQueue.shift()?.close();continue}break}let n=this.headlessQueue[0];return n?this.copyHeadlessSample(n,t):null}async performDecodeInternal(e){this.currentFetchId++;let t=this.currentFetchId;if(this.isHeadless)return this.performDecodeHeadless(e,t);if(!this.canvasSink){if(this.destroyed||!this.input)return null;let o=await this.input.getPrimaryVideoTrack();if(!o)return null;this.canvasSink=new f(o,{poolSize:3,alpha:!0})}let i=1e-4;if((!this.iterator||e<this.iteratorStartTimestamp-i||this.currentFrame&&e<this.currentFrame.timestamp-.01||this.currentFrame&&e-this.currentFrame.timestamp>1)&&(await this.startIterator(e,t),this.currentFetchId!==t||this.destroyed||!this.input))return null;for(;this.nextFrame&&this.nextFrame.timestamp<=e+i;)if(this.currentFrame=this.nextFrame,this.nextFrame=null,this.iterator){let o=await this.iterator.next();if(this.currentFetchId!==t||this.destroyed||!this.input)return null;this.nextFrame=o.value??null}if(!this.currentFrame)return null;let s=this.currentFrame.canvas;return{width:s.width,height:s.height,canvas:s}}async performDecode(e){let t=Math.max(0,Number.isFinite(e)?e:0);try{return await this.performDecodeInternal(t)}catch(i){let r=i;if(r?.name==="InputDisposedError"||r?.message?.includes("disposed")||this.destroyed||!this.input)return null;this.resetSinks();try{if(!this.isHeadless&&this.input&&!this.destroyed){let a=await this.input.getPrimaryVideoTrack();if(a&&!this.destroyed)return this.canvasSink=new f(a,{poolSize:3,alpha:!0,decoderOptions:{hardwareAcceleration:"prefer-software"}}),await this.performDecodeInternal(t)}}catch{}throw i}}},v=class extends p{audioSink=null;onDestroy(){this.audioSink=null}async startIterator(e,t){this.audioSink&&(this.iterator?.return(),this.iterator=this.audioSink.buffers(e),this.iteratorStartTimestamp=e)}async*getBuffers(e){try{if(await this.init(),this.destroyed||!this.input)return;if(!this.audioSink){let r=await this.input.getPrimaryAudioTrack();if(!r)return;this.audioSink=new b(r)}(!this.iterator||Math.abs(e-this.iteratorStartTimestamp)>.05)&&(this.currentFetchId++,await this.startIterator(e,this.currentFetchId));let i=this.currentFetchId;if(!this.iterator)return;for await(let r of this.iterator){if(this.currentFetchId!==i||this.destroyed||!this.input)break;this.iteratorStartTimestamp=r.timestamp+r.duration,yield r}}catch(t){let i=t;if(i?.name==="InputDisposedError"||i?.message?.includes("disposed")||this.destroyed||!this.input)return;throw t}finally{let t=this.currentFetchId;this.currentFetchId===t&&(this.iterator=null)}}},w=class{videoDecoders=new Map;audioDecoders=new Map;lastAccessed=new Map;ticks=0;lastTick=new Map;touchKey(e){this.lastAccessed.set(e,Date.now()),this.lastTick.set(e,this.ticks)}dropKey(e){let t=this.videoDecoders.get(e);if(t){try{t.destroy()}catch{}this.videoDecoders.delete(e)}let i=this.audioDecoders.get(e);if(i){try{i.destroy()}catch{}this.audioDecoders.delete(e)}this.lastAccessed.delete(e),this.lastTick.delete(e)}tick(e=120){this.ticks++;for(let[t,i]of this.lastTick)this.ticks-i>e&&this.dropKey(t)}destroy(){for(let e of this.videoDecoders.values())try{e.destroy()}catch{}this.videoDecoders.clear();for(let e of this.audioDecoders.values())try{e.destroy()}catch{}this.audioDecoders.clear(),this.lastAccessed.clear(),this.lastTick.clear()}get size(){return this.videoDecoders.size+this.audioDecoders.size}prune(e=3e5){let t=Date.now();for(let[i,r]of this.lastAccessed.entries())t-r>e&&this.dropKey(i)}getVideo(e,t,i){let r=`${e}-${t}-${i??"default"}`,a=this.videoDecoders.get(r);return a||(a=new y(e,t),this.videoDecoders.set(r,a)),this.touchKey(r),this.prune(),a}getAudio(e,t){let i=`${e}-${t??"default"}`,r=this.audioDecoders.get(i);return r||(r=new v(e,!1),this.audioDecoders.set(i,r)),this.touchKey(i),this.prune(),r}clearNode(e){let t=[...this.videoDecoders.keys(),...this.audioDecoders.keys()];for(let i of t)i.includes(e)&&this.dropKey(i)}},j=new w;h();async function V(l,e){if(typeof window<"u"&&typeof FontFace<"u"){let t=typeof e=="string"?e:"its registered bytes";try{let r=await new FontFace(l,typeof e=="string"?`url(${e})`:e).load();document.fonts.add(r),await document.fonts.ready}catch(i){console.warn(`[preloadFont] Failed to load font "${l}" from ${t}:`,i)}}else typeof e=="string"&&await S(l,e)}h();var T=!1;async function z(l,e,t=(i,r)=>console.warn(`[framefields] ${r}`,i)){let i=new Set,r=new Map;(function s(o){let n=o.operation;if(n){let d=n.op==="text"||n.kind==="text"||n.op==="caption"||n.kind==="caption"||typeof n.text=="string",c=n.fontFamily||(d?"Inter":"");if(c&&i.add(c),d&&typeof n.text=="string"){let k=n.fontSize??48;for(let B of D(n.text))r.set(`${B}-${k}`,k)}}for(let d of o.children??[])s(d)})(l);let a=[];for(let s of i){let o=x(s),n=I(s),d=typeof o=="string"?o:"its registered bytes";a.push(V(s,o).catch(c=>t(c,`font "${s}" from ${d}`))),e&&a.push(u.preloadSlugFont(e,s,n).catch(c=>t(c,`Slug font "${s}" from ${n}`)))}if(e&&r.size>0){!u.hasEmojiFont()&&!T&&(T=!0,console.warn("[framefields] Text uses emoji but no emoji font is registered, so exports draw nothing for them. Run `npx framefields fonts add emoji` and call the generated `registerFonts()`, or register a color emoji font (e.g. Noto Color Emoji) with FontManager.registerEmojiFont(path). `comp.lint()` names the layers (missing_emoji_font)."));for(let[s,o]of r){let n=s.slice(0,s.lastIndexOf("-"));a.push(u.preloadEmoji(e,n,o).catch(d=>{u.hasEmojiFont()&&t(d,`emoji "${n}"`)}))}}await Promise.all(a)}export{j as a,V as b,z as c};