@kvman/kvvideo 0.0.0-stage → 0.1.0

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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -2
  3. package/dist/api.d.ts +79 -0
  4. package/dist/api.d.ts.map +1 -0
  5. package/dist/api.js +2 -0
  6. package/dist/api.js.map +1 -0
  7. package/dist/browser/channels.d.ts +6 -0
  8. package/dist/browser/channels.d.ts.map +1 -0
  9. package/dist/browser/channels.js +5 -0
  10. package/dist/browser/channels.js.map +1 -0
  11. package/dist/browser/launch.d.ts +4 -0
  12. package/dist/browser/launch.d.ts.map +1 -0
  13. package/dist/browser/launch.js +8 -0
  14. package/dist/browser/launch.js.map +1 -0
  15. package/dist/browser/start-browser.d.ts +5 -0
  16. package/dist/browser/start-browser.d.ts.map +1 -0
  17. package/dist/browser/start-browser.js +17 -0
  18. package/dist/browser/start-browser.js.map +1 -0
  19. package/dist/compose/ass-file.d.ts +12 -0
  20. package/dist/compose/ass-file.d.ts.map +1 -0
  21. package/dist/compose/ass-file.js +36 -0
  22. package/dist/compose/ass-file.js.map +1 -0
  23. package/dist/compose/caption-font.d.ts +6 -0
  24. package/dist/compose/caption-font.d.ts.map +1 -0
  25. package/dist/compose/caption-font.js +23 -0
  26. package/dist/compose/caption-font.js.map +1 -0
  27. package/dist/compose/compose-input.d.ts +48 -0
  28. package/dist/compose/compose-input.d.ts.map +1 -0
  29. package/dist/compose/compose-input.js +37 -0
  30. package/dist/compose/compose-input.js.map +1 -0
  31. package/dist/compose/player-copies.d.ts +6 -0
  32. package/dist/compose/player-copies.d.ts.map +1 -0
  33. package/dist/compose/player-copies.js +31 -0
  34. package/dist/compose/player-copies.js.map +1 -0
  35. package/dist/compose/register-compose.d.ts +3 -0
  36. package/dist/compose/register-compose.d.ts.map +1 -0
  37. package/dist/compose/register-compose.js +65 -0
  38. package/dist/compose/register-compose.js.map +1 -0
  39. package/dist/compose/render.d.ts +16 -0
  40. package/dist/compose/render.d.ts.map +1 -0
  41. package/dist/compose/render.js +35 -0
  42. package/dist/compose/render.js.map +1 -0
  43. package/dist/compose/scene-args.d.ts +12 -0
  44. package/dist/compose/scene-args.d.ts.map +1 -0
  45. package/dist/compose/scene-args.js +42 -0
  46. package/dist/compose/scene-args.js.map +1 -0
  47. package/dist/compose/scene-plan.d.ts +28 -0
  48. package/dist/compose/scene-plan.d.ts.map +1 -0
  49. package/dist/compose/scene-plan.js +32 -0
  50. package/dist/compose/scene-plan.js.map +1 -0
  51. package/dist/connector.d.ts +79 -0
  52. package/dist/connector.d.ts.map +1 -0
  53. package/dist/connector.js +44 -0
  54. package/dist/connector.js.map +1 -0
  55. package/dist/files/answer-path.d.ts +3 -0
  56. package/dist/files/answer-path.d.ts.map +1 -0
  57. package/dist/files/answer-path.js +6 -0
  58. package/dist/files/answer-path.js.map +1 -0
  59. package/dist/files/place-output.d.ts +8 -0
  60. package/dist/files/place-output.d.ts.map +1 -0
  61. package/dist/files/place-output.js +24 -0
  62. package/dist/files/place-output.js.map +1 -0
  63. package/dist/files/workspace-paths.d.ts +7 -0
  64. package/dist/files/workspace-paths.d.ts.map +1 -0
  65. package/dist/files/workspace-paths.js +48 -0
  66. package/dist/files/workspace-paths.js.map +1 -0
  67. package/dist/fonts/bundled-fonts.d.ts +10 -0
  68. package/dist/fonts/bundled-fonts.d.ts.map +1 -0
  69. package/dist/fonts/bundled-fonts.js +10 -0
  70. package/dist/fonts/bundled-fonts.js.map +1 -0
  71. package/dist/fonts/font-family.d.ts +2 -0
  72. package/dist/fonts/font-family.d.ts.map +1 -0
  73. package/dist/fonts/font-family.js +43 -0
  74. package/dist/fonts/font-family.js.map +1 -0
  75. package/dist/fonts/register-fonts.d.ts +3 -0
  76. package/dist/fonts/register-fonts.d.ts.map +1 -0
  77. package/dist/fonts/register-fonts.js +12 -0
  78. package/dist/fonts/register-fonts.js.map +1 -0
  79. package/dist/guide.d.ts +3 -0
  80. package/dist/guide.d.ts.map +1 -0
  81. package/dist/guide.js +6 -0
  82. package/dist/guide.js.map +1 -0
  83. package/dist/index.d.ts +5 -0
  84. package/dist/index.d.ts.map +1 -0
  85. package/dist/index.js +24 -0
  86. package/dist/index.js.map +1 -0
  87. package/dist/narration/captions.d.ts +10 -0
  88. package/dist/narration/captions.d.ts.map +1 -0
  89. package/dist/narration/captions.js +42 -0
  90. package/dist/narration/captions.js.map +1 -0
  91. package/dist/narration/piper.d.ts +8 -0
  92. package/dist/narration/piper.d.ts.map +1 -0
  93. package/dist/narration/piper.js +19 -0
  94. package/dist/narration/piper.js.map +1 -0
  95. package/dist/narration/register-narration.d.ts +17 -0
  96. package/dist/narration/register-narration.d.ts.map +1 -0
  97. package/dist/narration/register-narration.js +75 -0
  98. package/dist/narration/register-narration.js.map +1 -0
  99. package/dist/narration/speech-services.d.ts +19 -0
  100. package/dist/narration/speech-services.d.ts.map +1 -0
  101. package/dist/narration/speech-services.js +50 -0
  102. package/dist/narration/speech-services.js.map +1 -0
  103. package/dist/problems.d.ts +12 -0
  104. package/dist/problems.d.ts.map +1 -0
  105. package/dist/problems.js +19 -0
  106. package/dist/problems.js.map +1 -0
  107. package/dist/programs/kill-tree.d.ts +16 -0
  108. package/dist/programs/kill-tree.d.ts.map +1 -0
  109. package/dist/programs/kill-tree.js +28 -0
  110. package/dist/programs/kill-tree.js.map +1 -0
  111. package/dist/programs/media-duration.d.ts +10 -0
  112. package/dist/programs/media-duration.d.ts.map +1 -0
  113. package/dist/programs/media-duration.js +10 -0
  114. package/dist/programs/media-duration.js.map +1 -0
  115. package/dist/programs/piped-program.d.ts +7 -0
  116. package/dist/programs/piped-program.d.ts.map +1 -0
  117. package/dist/programs/piped-program.js +45 -0
  118. package/dist/programs/piped-program.js.map +1 -0
  119. package/dist/programs/programs.d.ts +6 -0
  120. package/dist/programs/programs.d.ts.map +1 -0
  121. package/dist/programs/programs.js +6 -0
  122. package/dist/programs/programs.js.map +1 -0
  123. package/dist/programs/run-program.d.ts +22 -0
  124. package/dist/programs/run-program.d.ts.map +1 -0
  125. package/dist/programs/run-program.js +46 -0
  126. package/dist/programs/run-program.js.map +1 -0
  127. package/dist/record/address.d.ts +5 -0
  128. package/dist/record/address.d.ts.map +1 -0
  129. package/dist/record/address.js +28 -0
  130. package/dist/record/address.js.map +1 -0
  131. package/dist/record/cursor-script.d.ts +2 -0
  132. package/dist/record/cursor-script.d.ts.map +1 -0
  133. package/dist/record/cursor-script.js +21 -0
  134. package/dist/record/cursor-script.js.map +1 -0
  135. package/dist/record/encoder-args.d.ts +3 -0
  136. package/dist/record/encoder-args.d.ts.map +1 -0
  137. package/dist/record/encoder-args.js +13 -0
  138. package/dist/record/encoder-args.js.map +1 -0
  139. package/dist/record/frame-writer.d.ts +8 -0
  140. package/dist/record/frame-writer.d.ts.map +1 -0
  141. package/dist/record/frame-writer.js +30 -0
  142. package/dist/record/frame-writer.js.map +1 -0
  143. package/dist/record/own-port.d.ts +2 -0
  144. package/dist/record/own-port.d.ts.map +1 -0
  145. package/dist/record/own-port.js +23 -0
  146. package/dist/record/own-port.js.map +1 -0
  147. package/dist/record/pointer.d.ts +9 -0
  148. package/dist/record/pointer.d.ts.map +1 -0
  149. package/dist/record/pointer.js +23 -0
  150. package/dist/record/pointer.js.map +1 -0
  151. package/dist/record/recorder.d.ts +24 -0
  152. package/dist/record/recorder.d.ts.map +1 -0
  153. package/dist/record/recorder.js +69 -0
  154. package/dist/record/recorder.js.map +1 -0
  155. package/dist/record/register-record.d.ts +3 -0
  156. package/dist/record/register-record.d.ts.map +1 -0
  157. package/dist/record/register-record.js +49 -0
  158. package/dist/record/register-record.js.map +1 -0
  159. package/dist/record/run-step.d.ts +7 -0
  160. package/dist/record/run-step.d.ts.map +1 -0
  161. package/dist/record/run-step.js +49 -0
  162. package/dist/record/run-step.js.map +1 -0
  163. package/dist/record/screencast.d.ts +9 -0
  164. package/dist/record/screencast.d.ts.map +1 -0
  165. package/dist/record/screencast.js +38 -0
  166. package/dist/record/screencast.js.map +1 -0
  167. package/dist/record/steps.d.ts +71 -0
  168. package/dist/record/steps.d.ts.map +1 -0
  169. package/dist/record/steps.js +29 -0
  170. package/dist/record/steps.js.map +1 -0
  171. package/dist/register-with-kvcoder.d.ts +3 -0
  172. package/dist/register-with-kvcoder.d.ts.map +1 -0
  173. package/dist/register-with-kvcoder.js +15 -0
  174. package/dist/register-with-kvcoder.js.map +1 -0
  175. package/dist/settings.d.ts +29 -0
  176. package/dist/settings.d.ts.map +1 -0
  177. package/dist/settings.js +27 -0
  178. package/dist/settings.js.map +1 -0
  179. package/dist/setup/browser-readiness.d.ts +14 -0
  180. package/dist/setup/browser-readiness.d.ts.map +1 -0
  181. package/dist/setup/browser-readiness.js +16 -0
  182. package/dist/setup/browser-readiness.js.map +1 -0
  183. package/dist/setup/ffmpeg-readiness.d.ts +19 -0
  184. package/dist/setup/ffmpeg-readiness.d.ts.map +1 -0
  185. package/dist/setup/ffmpeg-readiness.js +33 -0
  186. package/dist/setup/ffmpeg-readiness.js.map +1 -0
  187. package/dist/setup/narration-readiness.d.ts +21 -0
  188. package/dist/setup/narration-readiness.d.ts.map +1 -0
  189. package/dist/setup/narration-readiness.js +18 -0
  190. package/dist/setup/narration-readiness.js.map +1 -0
  191. package/dist/setup/register-setup.d.ts +3 -0
  192. package/dist/setup/register-setup.d.ts.map +1 -0
  193. package/dist/setup/register-setup.js +51 -0
  194. package/dist/setup/register-setup.js.map +1 -0
  195. package/dist/ui.d.ts +3 -0
  196. package/dist/ui.d.ts.map +1 -0
  197. package/dist/ui.js +25 -0
  198. package/dist/ui.js.map +1 -0
  199. package/dist/video-start.d.ts +3 -0
  200. package/dist/video-start.d.ts.map +1 -0
  201. package/dist/video-start.js +28 -0
  202. package/dist/video-start.js.map +1 -0
  203. package/dist/web/components/player.css +2 -0
  204. package/dist/web/components/player.js +43 -0
  205. package/docs/guide.md +16 -0
  206. package/fonts/Amiri-Bold.ttf +0 -0
  207. package/fonts/IBMPlexSansArabic-Bold.ttf +0 -0
  208. package/fonts/Lato-Bold.ttf +0 -0
  209. package/fonts/OFL-Amiri.txt +94 -0
  210. package/fonts/OFL-IBMPlexSansArabic.txt +93 -0
  211. package/fonts/OFL-Lato.txt +93 -0
  212. package/fonts/OFL-Poppins.txt +93 -0
  213. package/fonts/Poppins-Bold.ttf +0 -0
  214. package/locales/ar.json +37 -0
  215. package/locales/en.json +37 -0
  216. package/package.json +67 -3
@@ -0,0 +1,42 @@
1
+ // A narration's captions (plan 16 §16.6, ADR 0107, 7): lines of whole words, each shown for the share of the
2
+ // narration's length that its characters have, one after another from 0 with no gap.
3
+ const maximumLine = 42;
4
+ const length = (text) => [...text].length;
5
+ function sentencesOf(line) {
6
+ return line.split(/(?<=[.!?؟…])\s+/u).filter((sentence) => sentence !== '');
7
+ }
8
+ function linesOf(sentence) {
9
+ const lines = [];
10
+ let current = '';
11
+ for (const word of sentence.split(' ')) {
12
+ const joined = current === '' ? word : `${current} ${word}`;
13
+ if (current !== '' && length(joined) > maximumLine) {
14
+ lines.push(current);
15
+ current = word;
16
+ }
17
+ else {
18
+ current = joined;
19
+ }
20
+ }
21
+ if (current !== '')
22
+ lines.push(current);
23
+ return lines;
24
+ }
25
+ /** The caption lines of a text, in order. */
26
+ export function captionLines(text) {
27
+ const textLines = text.split(/\r?\n/u).map((line) => line.replace(/\s+/gu, ' ').trim());
28
+ return textLines.flatMap(sentencesOf).flatMap(linesOf);
29
+ }
30
+ /** The text's captions over a narration of `durationMs`. */
31
+ export function timedCaptions(text, durationMs) {
32
+ const lines = captionLines(text);
33
+ const total = lines.reduce((sum, line) => sum + length(line), 0);
34
+ let before = 0;
35
+ return lines.map((line, index) => {
36
+ const startMs = Math.floor((durationMs * before) / total);
37
+ before += length(line);
38
+ const endMs = index === lines.length - 1 ? durationMs : Math.floor((durationMs * before) / total);
39
+ return { text: line, startMs, endMs };
40
+ });
41
+ }
42
+ //# sourceMappingURL=captions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captions.js","sourceRoot":"","sources":["../../src/narration/captions.ts"],"names":[],"mappings":"AAAA,6GAA6G;AAC7G,qFAAqF;AAIrF,MAAM,WAAW,GAAG,EAAE,CAAC;AAEvB,MAAM,MAAM,GAAG,CAAC,IAAY,EAAU,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,MAAM,CAAC;AAE1D,SAAS,WAAW,CAAC,IAAY;IAC/B,OAAO,IAAI,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,KAAK,EAAE,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,OAAO,CAAC,QAAgB;IAC/B,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACvC,MAAM,MAAM,GAAG,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,IAAI,EAAE,CAAC;QAC5D,IAAI,OAAO,KAAK,EAAE,IAAI,MAAM,CAAC,MAAM,CAAC,GAAG,WAAW,EAAE,CAAC;YACnD,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACpB,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;aAAM,CAAC;YACN,OAAO,GAAG,MAAM,CAAC;QACnB,CAAC;IACH,CAAC;IACD,IAAI,OAAO,KAAK,EAAE;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACxC,OAAO,KAAK,CAAC;AACf,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACxF,OAAO,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;AACzD,CAAC;AAED,4DAA4D;AAC5D,MAAM,UAAU,aAAa,CAAC,IAAY,EAAE,UAAkB;IAC5D,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,KAAK,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACjE,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC/B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC;QAC1D,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC;QACvB,MAAM,KAAK,GAAG,KAAK,KAAK,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC;QAClG,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IACxC,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { type Program } from '../programs/run-program.ts';
2
+ export type PiperPrograms = {
3
+ piper: Program;
4
+ ffmpeg: Program;
5
+ };
6
+ /** Narrates `text` with the voice into `<folder>/narration.mp3`, and answers that file. */
7
+ export declare function piperNarration(programs: PiperPrograms, voice: string, text: string, folder: string, signal: AbortSignal): Promise<string>;
8
+ //# sourceMappingURL=piper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piper.d.ts","sourceRoot":"","sources":["../../src/narration/piper.ts"],"names":[],"mappings":"AACA,OAAO,EAAyB,KAAK,OAAO,EAAE,MAAM,4BAA4B,CAAC;AAKjF,MAAM,MAAM,aAAa,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAMhE,2FAA2F;AAC3F,wBAAsB,cAAc,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ/I"}
@@ -0,0 +1,19 @@
1
+ import path from 'node:path';
2
+ import { lastLines, runProgram } from "../programs/run-program.js";
3
+ import { kvvideoProblem } from "../problems.js";
4
+ function failed(reason) {
5
+ return kvvideoProblem('NARRATION_FAILED', `piper couldn't narrate: ${reason}`, { service: 'piper', reason });
6
+ }
7
+ /** Narrates `text` with the voice into `<folder>/narration.mp3`, and answers that file. */
8
+ export async function piperNarration(programs, voice, text, folder, signal) {
9
+ const wav = path.join(folder, 'narration.wav');
10
+ const mp3 = path.join(folder, 'narration.mp3');
11
+ const spoken = await runProgram(programs.piper, ['-m', voice, '-f', wav, '--', text], { signal });
12
+ if (spoken.exitCode !== 0)
13
+ throw failed(lastLines(spoken.output));
14
+ const encoded = await runProgram(programs.ffmpeg, ['-hide_banner', '-loglevel', 'error', '-i', wav, '-codec:a', 'libmp3lame', '-b:a', '128k', '-y', mp3], { signal });
15
+ if (encoded.exitCode !== 0)
16
+ throw failed(lastLines(encoded.output));
17
+ return mp3;
18
+ }
19
+ //# sourceMappingURL=piper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piper.js","sourceRoot":"","sources":["../../src/narration/piper.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,UAAU,EAAgB,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAMhD,SAAS,MAAM,CAAC,MAAc;IAC5B,OAAO,cAAc,CAAC,kBAAkB,EAAE,2BAA2B,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;AAC/G,CAAC;AAED,2FAA2F;AAC3F,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,QAAuB,EAAE,KAAa,EAAE,IAAY,EAAE,MAAc,EAAE,MAAmB;IAC5H,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAClG,IAAI,MAAM,CAAC,QAAQ,KAAK,CAAC;QAAE,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAClE,MAAM,OAAO,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,cAAc,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IACtK,IAAI,OAAO,CAAC,QAAQ,KAAK,CAAC;QAAE,MAAM,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IACpE,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { z, type Ctx } from '@kvman/sdk';
2
+ export declare const narrateInputSchema: z.ZodObject<{
3
+ text: z.ZodString;
4
+ file: z.ZodString;
5
+ replace: z.ZodExactOptional<z.ZodBoolean>;
6
+ }, z.core.$strict>;
7
+ export declare const narrateOutputSchema: z.ZodObject<{
8
+ file: z.ZodString;
9
+ durationMs: z.ZodNumber;
10
+ captions: z.ZodArray<z.ZodObject<{
11
+ text: z.ZodString;
12
+ startMs: z.ZodNumber;
13
+ endMs: z.ZodNumber;
14
+ }, z.core.$strip>>;
15
+ }, z.core.$strip>;
16
+ export declare function registerNarration(ctx: Ctx): void;
17
+ //# sourceMappingURL=register-narration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-narration.d.ts","sourceRoot":"","sources":["../../src/narration/register-narration.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,CAAC,EAAE,KAAK,GAAG,EAAE,MAAM,YAAY,CAAC;AAgBzC,eAAO,MAAM,kBAAkB;;;;kBAI7B,CAAC;AAEH,eAAO,MAAM,mBAAmB;;;;;;;;iBAI9B,CAAC;AAuBH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,GAAG,GAAG,IAAI,CAwBhD"}
@@ -0,0 +1,75 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { z } from '@kvman/sdk';
4
+ import { answerPath } from "../files/answer-path.js";
5
+ import { placeOutput, workFolder } from "../files/place-output.js";
6
+ import { outputPath } from "../files/workspace-paths.js";
7
+ import { mediaDuration } from "../programs/media-duration.js";
8
+ import { programOf } from "../programs/programs.js";
9
+ import { kvvideoProblem } from "../problems.js";
10
+ import { keySecrets } from "../setup/narration-readiness.js";
11
+ import { timedCaptions } from "./captions.js";
12
+ import { piperNarration } from "./piper.js";
13
+ import { speak, speechRequest } from "./speech-services.js";
14
+ // `video narrate` (plan 16 §16.6): the chosen service speaks the text into an MP3 in the workspace, and the answer has
15
+ // its length and its captions. The output is checked before anything runs or is paid for.
16
+ export const narrateInputSchema = z.strictObject({
17
+ text: z.string().min(1).max(4000).describe('What the narrator says: one scene, a few sentences.'),
18
+ file: z.string().min(1).max(1000).describe('The .mp3 file to write, in the workspace.'),
19
+ replace: z.boolean().describe('Write over an existing file.').exactOptional(),
20
+ });
21
+ export const narrateOutputSchema = z.object({
22
+ file: z.string(),
23
+ durationMs: z.number().int().nonnegative(),
24
+ captions: z.array(z.object({ text: z.string(), startMs: z.number().int(), endMs: z.number().int() })),
25
+ });
26
+ async function chosenService(ctx) {
27
+ const service = await ctx.settings.get('kvvideo.narration.service');
28
+ if (service === null)
29
+ throw kvvideoProblem('NARRATION_NOT_SET', 'No narration service is chosen; set kvvideo.narration.service.', { setting: 'kvvideo.narration.service' });
30
+ return service;
31
+ }
32
+ async function narrateInto(ctx, service, text, folder) {
33
+ const choice = { voice: await ctx.settings.get('kvvideo.narration.voice'), model: await ctx.settings.get('kvvideo.narration.model') };
34
+ if (service === 'piper') {
35
+ if (choice.voice === null)
36
+ throw kvvideoProblem('NARRATION_NOT_SET', "Piper has no voice; set kvvideo.narration.voice to a voice's .onnx file.", { setting: 'kvvideo.narration.voice' });
37
+ return piperNarration({ piper: await programOf(ctx, 'piper'), ffmpeg: await programOf(ctx, 'ffmpeg') }, choice.voice, text, folder, ctx.job.signal);
38
+ }
39
+ const secret = keySecrets[service];
40
+ const key = await ctx.secrets.get(secret);
41
+ if (key === undefined)
42
+ throw kvvideoProblem('NARRATION_KEY_MISSING', `The ${service} key is missing; add the secret ${secret} on kvvideo's extension page.`, { service, secret });
43
+ const audio = await speak(service, speechRequest(service, text, choice, key), key, fetch, ctx.job.signal);
44
+ const file = path.join(folder, 'narration.mp3');
45
+ writeFileSync(file, audio);
46
+ return file;
47
+ }
48
+ export function registerNarration(ctx) {
49
+ ctx.registerCommand('kvvideo.narration.make', {
50
+ description: 'Narrates text with the chosen text-to-speech service into an MP3 file in the workspace, with its length and captions.',
51
+ input: narrateInputSchema,
52
+ output: narrateOutputSchema,
53
+ public: true,
54
+ retries: 0,
55
+ timeoutMs: 120_000,
56
+ handle: async (input) => {
57
+ const root = ctx.job.workspace.path;
58
+ const output = outputPath(root, input.file, '.mp3', input.replace === true);
59
+ const service = await chosenService(ctx);
60
+ const work = workFolder();
61
+ try {
62
+ const made = await narrateInto(ctx, service, input.text, work.folder);
63
+ const duration = await mediaDuration(await programOf(ctx, 'ffprobe'), made, ctx.job.signal);
64
+ if (!duration.ok)
65
+ throw kvvideoProblem('NARRATION_FAILED', `ffprobe couldn't read the narration: ${duration.reason}`, { service, reason: duration.reason });
66
+ placeOutput(made, output);
67
+ return { file: answerPath(root, output), durationMs: duration.ms, captions: timedCaptions(input.text, duration.ms) };
68
+ }
69
+ finally {
70
+ work.remove();
71
+ }
72
+ },
73
+ });
74
+ }
75
+ //# sourceMappingURL=register-narration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-narration.js","sourceRoot":"","sources":["../../src/narration/register-narration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export type WebService = 'elevenlabs' | 'openai';
2
+ export type SpeechRequest = {
3
+ url: string;
4
+ headers: Record<string, string>;
5
+ body: string;
6
+ };
7
+ export type Fetch = (url: string, init: {
8
+ method: 'POST';
9
+ headers: Record<string, string>;
10
+ body: string;
11
+ signal: AbortSignal;
12
+ }) => Promise<Response>;
13
+ export declare function speechRequest(service: WebService, text: string, choice: {
14
+ voice: string | null;
15
+ model: string | null;
16
+ }, key: string): SpeechRequest;
17
+ /** The audio a service answers, or `kvvideo/NARRATION_FAILED`. */
18
+ export declare function speak(service: WebService, request: SpeechRequest, key: string, fetch: Fetch, signal: AbortSignal): Promise<Buffer>;
19
+ //# sourceMappingURL=speech-services.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech-services.d.ts","sourceRoot":"","sources":["../../src/narration/speech-services.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,QAAQ,CAAC;AAEjD,MAAM,MAAM,aAAa,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3F,MAAM,MAAM,KAAK,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,WAAW,CAAA;CAAE,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AASrJ,wBAAgB,aAAa,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE;IAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,GAAG,EAAE,MAAM,GAAG,aAAa,CAenJ;AAQD,kEAAkE;AAClE,wBAAsB,KAAK,CAAC,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBxI"}
@@ -0,0 +1,50 @@
1
+ import { ProblemError } from '@kvman/sdk';
2
+ import { kvvideoProblem } from "../problems.js";
3
+ const defaults = {
4
+ elevenlabs: { voice: 'JBFqnCBsd6RMkjVDRZzb', model: 'eleven_multilingual_v2' },
5
+ openai: { voice: 'alloy', model: 'gpt-4o-mini-tts' },
6
+ };
7
+ const requestTimeoutMs = 60_000;
8
+ export function speechRequest(service, text, choice, key) {
9
+ const voice = choice.voice ?? defaults[service].voice;
10
+ const model = choice.model ?? defaults[service].model;
11
+ if (service === 'elevenlabs') {
12
+ return {
13
+ url: `https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voice)}?output_format=mp3_44100_128`,
14
+ headers: { 'xi-api-key': key, 'Content-Type': 'application/json' },
15
+ body: JSON.stringify({ text, model_id: model }),
16
+ };
17
+ }
18
+ return {
19
+ url: 'https://api.openai.com/v1/audio/speech',
20
+ headers: { Authorization: `Bearer ${key}`, 'Content-Type': 'application/json' },
21
+ body: JSON.stringify({ model, voice, input: text, response_format: 'mp3' }),
22
+ };
23
+ }
24
+ function withoutKey(text, key) {
25
+ return key === '' ? text : text.split(key).join('…');
26
+ }
27
+ const reasonOf = (error) => (error instanceof Error ? error.message : String(error));
28
+ /** The audio a service answers, or `kvvideo/NARRATION_FAILED`. */
29
+ export async function speak(service, request, key, fetch, signal) {
30
+ const timeout = new AbortController();
31
+ const timer = setTimeout(() => timeout.abort(new Error(`no answer in ${String(requestTimeoutMs / 1000)} s`)), requestTimeoutMs);
32
+ try {
33
+ const response = await fetch(request.url, { method: 'POST', headers: request.headers, body: request.body, signal: AbortSignal.any([signal, timeout.signal]) });
34
+ if (!response.ok) {
35
+ const reason = withoutKey((await response.text()).slice(0, 300), key);
36
+ throw kvvideoProblem('NARRATION_FAILED', `${service} answered ${String(response.status)}: ${reason}`, { service, status: response.status, reason });
37
+ }
38
+ return Buffer.from(await response.arrayBuffer());
39
+ }
40
+ catch (error) {
41
+ if (signal.aborted || error instanceof ProblemError)
42
+ throw error;
43
+ const reason = withoutKey(timeout.signal.aborted ? reasonOf(timeout.signal.reason) : reasonOf(error), key);
44
+ throw kvvideoProblem('NARRATION_FAILED', `${service} couldn't be reached: ${reason}`, { service, reason });
45
+ }
46
+ finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+ //# sourceMappingURL=speech-services.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"speech-services.js","sourceRoot":"","sources":["../../src/narration/speech-services.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { ProblemError, type Json } from '@kvman/sdk';
2
+ export type KvvideoCode = 'PROGRAM_MISSING' | 'BROWSER_MISSING' | 'STEP_FAILED' | 'NARRATION_NOT_SET' | 'NARRATION_KEY_MISSING' | 'NARRATION_FAILED' | 'RENDER_FAILED';
3
+ export declare const kvvideoCodes: readonly KvvideoCode[];
4
+ /** A `kvvideo/<code>` Problem to throw. */
5
+ export declare function kvvideoProblem(code: KvvideoCode, message: string, params?: Record<string, Json>): ProblemError;
6
+ /** A `VALIDATION_FAILED` Problem to throw. */
7
+ export declare function invalid(message: string, params?: Record<string, Json>): ProblemError;
8
+ /** A `NOT_FOUND` Problem to throw. */
9
+ export declare function notFound(message: string, params?: Record<string, Json>): ProblemError;
10
+ /** Whether an error is the Problem of a kvvideo code. */
11
+ export declare function isKvvideoProblem(error: unknown, code: KvvideoCode): boolean;
12
+ //# sourceMappingURL=problems.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"problems.d.ts","sourceRoot":"","sources":["../src/problems.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,EAAE,MAAM,YAAY,CAAC;AAIrD,MAAM,MAAM,WAAW,GAAG,iBAAiB,GAAG,iBAAiB,GAAG,aAAa,GAAG,mBAAmB,GAAG,uBAAuB,GAAG,kBAAkB,GAAG,eAAe,CAAC;AAEvK,eAAO,MAAM,YAAY,EAAE,SAAS,WAAW,EAA6I,CAAC;AAE7L,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAM,GAAG,YAAY,CAElH;AAED,8CAA8C;AAC9C,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAM,GAAG,YAAY,CAExF;AAED,sCAAsC;AACtC,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,IAAI,CAAM,GAAG,YAAY,CAEzF;AAED,yDAAyD;AACzD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAE3E"}
@@ -0,0 +1,19 @@
1
+ import { ProblemError } from '@kvman/sdk';
2
+ export const kvvideoCodes = ['PROGRAM_MISSING', 'BROWSER_MISSING', 'STEP_FAILED', 'NARRATION_NOT_SET', 'NARRATION_KEY_MISSING', 'NARRATION_FAILED', 'RENDER_FAILED'];
3
+ /** A `kvvideo/<code>` Problem to throw. */
4
+ export function kvvideoProblem(code, message, params = {}) {
5
+ return new ProblemError({ code: `kvvideo/${code}`, message, params });
6
+ }
7
+ /** A `VALIDATION_FAILED` Problem to throw. */
8
+ export function invalid(message, params = {}) {
9
+ return new ProblemError({ code: 'VALIDATION_FAILED', message, params });
10
+ }
11
+ /** A `NOT_FOUND` Problem to throw. */
12
+ export function notFound(message, params = {}) {
13
+ return new ProblemError({ code: 'NOT_FOUND', message, params });
14
+ }
15
+ /** Whether an error is the Problem of a kvvideo code. */
16
+ export function isKvvideoProblem(error, code) {
17
+ return error instanceof ProblemError && error.problem.code === `kvvideo/${code}`;
18
+ }
19
+ //# sourceMappingURL=problems.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"problems.js","sourceRoot":"","sources":["../src/problems.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAa,MAAM,YAAY,CAAC;AAMrD,MAAM,CAAC,MAAM,YAAY,GAA2B,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,aAAa,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,eAAe,CAAC,CAAC;AAE7L,2CAA2C;AAC3C,MAAM,UAAU,cAAc,CAAC,IAAiB,EAAE,OAAe,EAAE,SAA+B,EAAE;IAClG,OAAO,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,WAAW,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;AACxE,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,OAAO,CAAC,OAAe,EAAE,SAA+B,EAAE;IACxE,OAAO,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;AAC1E,CAAC;AAED,sCAAsC;AACtC,MAAM,UAAU,QAAQ,CAAC,OAAe,EAAE,SAA+B,EAAE;IACzE,OAAO,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;AAClE,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,gBAAgB,CAAC,KAAc,EAAE,IAAiB;IAChE,OAAO,KAAK,YAAY,YAAY,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,WAAW,IAAI,EAAE,CAAC;AACnF,CAAC"}
@@ -0,0 +1,16 @@
1
+ /** How a tree is killed on one OS. */
2
+ export type TreeKill = {
3
+ kind: 'group';
4
+ pid: number;
5
+ signal: 'SIGKILL';
6
+ } | {
7
+ kind: 'taskkill';
8
+ command: 'taskkill';
9
+ args: string[];
10
+ };
11
+ export declare function treeKillOf(platform: NodeJS.Platform, pid: number): TreeKill;
12
+ /** Whether a program starts as its own group's leader, so its tree can be killed. */
13
+ export declare function startsOwnGroup(platform: NodeJS.Platform): boolean;
14
+ /** Kills a running program's tree; a tree that is already gone is left alone. */
15
+ export declare function killTree(pid: number | undefined): void;
16
+ //# sourceMappingURL=kill-tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kill-tree.d.ts","sourceRoot":"","sources":["../../src/programs/kill-tree.ts"],"names":[],"mappings":"AAKA,sCAAsC;AACtC,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAErI,wBAAgB,UAAU,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,QAAQ,CAG3E;AAED,qFAAqF;AACrF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,OAAO,CAEjE;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAYtD"}
@@ -0,0 +1,28 @@
1
+ import { spawn } from 'node:child_process';
2
+ export function treeKillOf(platform, pid) {
3
+ if (platform === 'win32')
4
+ return { kind: 'taskkill', command: 'taskkill', args: ['/PID', String(pid), '/T', '/F'] };
5
+ return { kind: 'group', pid: -pid, signal: 'SIGKILL' };
6
+ }
7
+ /** Whether a program starts as its own group's leader, so its tree can be killed. */
8
+ export function startsOwnGroup(platform) {
9
+ return platform !== 'win32';
10
+ }
11
+ /** Kills a running program's tree; a tree that is already gone is left alone. */
12
+ export function killTree(pid) {
13
+ if (pid === undefined)
14
+ return;
15
+ const kill = treeKillOf(process.platform, pid);
16
+ if (kill.kind === 'taskkill') {
17
+ spawn(kill.command, kill.args, { stdio: 'ignore', windowsHide: true });
18
+ return;
19
+ }
20
+ try {
21
+ process.kill(kill.pid, kill.signal);
22
+ }
23
+ catch (error) {
24
+ if (!(error instanceof Error && 'code' in error && error.code === 'ESRCH'))
25
+ throw error;
26
+ }
27
+ }
28
+ //# sourceMappingURL=kill-tree.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kill-tree.js","sourceRoot":"","sources":["../../src/programs/kill-tree.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAQ3C,MAAM,UAAU,UAAU,CAAC,QAAyB,EAAE,GAAW;IAC/D,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;IACpH,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AACzD,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,cAAc,CAAC,QAAyB;IACtD,OAAO,QAAQ,KAAK,OAAO,CAAC;AAC9B,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,QAAQ,CAAC,GAAuB;IAC9C,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO;IAC9B,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC/C,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QACvE,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACtC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,CAAC;YAAE,MAAM,KAAK,CAAC;IAC1F,CAAC;AACH,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type Program } from './run-program.ts';
2
+ export type Duration = {
3
+ ok: true;
4
+ ms: number;
5
+ } | {
6
+ ok: false;
7
+ reason: string;
8
+ };
9
+ export declare function mediaDuration(ffprobe: Program, file: string, signal: AbortSignal): Promise<Duration>;
10
+ //# sourceMappingURL=media-duration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-duration.d.ts","sourceRoot":"","sources":["../../src/programs/media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAIvE,MAAM,MAAM,QAAQ,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhF,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAM1G"}
@@ -0,0 +1,10 @@
1
+ import { lastLines, runProgram } from "./run-program.js";
2
+ export async function mediaDuration(ffprobe, file, signal) {
3
+ const run = await runProgram(ffprobe, ['-v', 'error', '-show_entries', 'format=duration', '-of', 'csv=p=0', file], { signal });
4
+ const answer = run.stdout.trim();
5
+ const seconds = Number(answer);
6
+ if (run.exitCode !== 0 || answer === '' || !Number.isFinite(seconds))
7
+ return { ok: false, reason: lastLines(run.output) || `ffprobe answered ${answer}` };
8
+ return { ok: true, ms: Math.round(seconds * 1000) };
9
+ }
10
+ //# sourceMappingURL=media-duration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-duration.js","sourceRoot":"","sources":["../../src/programs/media-duration.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAgB,MAAM,kBAAkB,CAAC;AAMvE,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAgB,EAAE,IAAY,EAAE,MAAmB;IACrF,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IAC/H,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC,IAAI,MAAM,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,oBAAoB,MAAM,EAAE,EAAE,CAAC;IAC1J,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,EAAE,CAAC;AACtD,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { Program, ProgramRun } from './run-program.ts';
2
+ export type PipedProgram = {
3
+ write(chunk: Buffer): Promise<void>;
4
+ end(): Promise<ProgramRun>;
5
+ };
6
+ export declare function startPiped(program: Program, args: readonly string[], signal: AbortSignal): Promise<PipedProgram>;
7
+ //# sourceMappingURL=piped-program.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piped-program.d.ts","sourceRoot":"","sources":["../../src/programs/piped-program.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAM5D,MAAM,MAAM,YAAY,GAAG;IAAE,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,GAAG,IAAI,OAAO,CAAC,UAAU,CAAC,CAAA;CAAE,CAAC;AAI/F,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAqChH"}
@@ -0,0 +1,45 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { kvvideoProblem } from "../problems.js";
3
+ import { programSetting } from "../settings.js";
4
+ import { killTree, startsOwnGroup } from "./kill-tree.js";
5
+ const notStarted = new Set(['ENOENT', 'EACCES', 'ENOTDIR']);
6
+ export function startPiped(program, args, signal) {
7
+ return new Promise((resolve, reject) => {
8
+ const child = spawn(program.command, args, { stdio: ['pipe', 'pipe', 'pipe'], detached: startsOwnGroup(process.platform), windowsHide: true });
9
+ let stdout = '';
10
+ let output = '';
11
+ let pipeError;
12
+ child.stdout.setEncoding('utf8').on('data', (text) => {
13
+ stdout += text;
14
+ output += text;
15
+ });
16
+ child.stderr.setEncoding('utf8').on('data', (text) => (output += text));
17
+ child.stdin.on('error', (error) => (pipeError = error));
18
+ const abort = () => killTree(child.pid);
19
+ signal.addEventListener('abort', abort, { once: true });
20
+ const closed = new Promise((settle) => child.once('close', (code) => settle(code ?? 1)));
21
+ child.once('error', (error) => {
22
+ signal.removeEventListener('abort', abort);
23
+ const missing = 'code' in error && typeof error.code === 'string' && notStarted.has(error.code);
24
+ reject(missing ? kvvideoProblem('PROGRAM_MISSING', `${program.name} can't be started (${program.command}); install it or set ${programSetting(program.name)}.`, { program: program.name, setting: programSetting(program.name) }) : error);
25
+ });
26
+ child.once('spawn', () => {
27
+ resolve({
28
+ write: (chunk) => new Promise((done) => {
29
+ if (pipeError !== undefined || child.stdin.destroyed)
30
+ return done();
31
+ child.stdin.write(chunk, () => done());
32
+ }),
33
+ end: async () => {
34
+ child.stdin.end();
35
+ const exitCode = await closed;
36
+ signal.removeEventListener('abort', abort);
37
+ if (exitCode === 0 && pipeError !== undefined)
38
+ throw pipeError;
39
+ return { exitCode, stdout, output, timedOut: false };
40
+ },
41
+ });
42
+ });
43
+ });
44
+ }
45
+ //# sourceMappingURL=piped-program.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piped-program.js","sourceRoot":"","sources":["../../src/programs/piped-program.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import type { Ctx } from '@kvman/sdk';
2
+ import { type ProgramName } from '../settings.ts';
3
+ import type { Program } from './run-program.ts';
4
+ /** A program as its setting names it (plan 16 §16.2, ADR 0107, 20). */
5
+ export declare function programOf(ctx: Ctx, name: ProgramName): Promise<Program>;
6
+ //# sourceMappingURL=programs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"programs.d.ts","sourceRoot":"","sources":["../../src/programs/programs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAEhD,uEAAuE;AACvE,wBAAsB,SAAS,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CAE7E"}
@@ -0,0 +1,6 @@
1
+ import { programSetting } from "../settings.js";
2
+ /** A program as its setting names it (plan 16 §16.2, ADR 0107, 20). */
3
+ export async function programOf(ctx, name) {
4
+ return { name, command: await ctx.settings.get(programSetting(name)) };
5
+ }
6
+ //# sourceMappingURL=programs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"programs.js","sourceRoot":"","sources":["../../src/programs/programs.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAoB,MAAM,gBAAgB,CAAC;AAGlE,uEAAuE;AACvE,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAQ,EAAE,IAAiB;IACzD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;AACzE,CAAC"}
@@ -0,0 +1,22 @@
1
+ import { type ProgramName } from '../settings.ts';
2
+ /** A finished run. */
3
+ export type ProgramRun = {
4
+ exitCode: number;
5
+ stdout: string;
6
+ output: string;
7
+ timedOut: boolean;
8
+ };
9
+ /** How one program runs: the program kvvideo calls it, and where its executable is. */
10
+ export type Program = {
11
+ name: ProgramName;
12
+ command: string;
13
+ };
14
+ export type RunOptions = {
15
+ signal: AbortSignal;
16
+ cwd?: string;
17
+ timeoutMs?: number;
18
+ };
19
+ export declare function runProgram(program: Program, args: readonly string[], options: RunOptions): Promise<ProgramRun>;
20
+ /** The last lines of a run's output, for a Problem's reason. */
21
+ export declare function lastLines(output: string, count?: number): string;
22
+ //# sourceMappingURL=run-program.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-program.d.ts","sourceRoot":"","sources":["../../src/programs/run-program.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAOlE,sBAAsB;AACtB,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAC;AAEjG,uFAAuF;AACvF,MAAM,MAAM,OAAO,GAAG;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7D,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAQnF,wBAAgB,UAAU,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CA+B9G;AAED,gEAAgE;AAChE,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,CAE5D"}
@@ -0,0 +1,46 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { kvvideoProblem } from "../problems.js";
3
+ import { programSetting } from "../settings.js";
4
+ import { killTree, startsOwnGroup } from "./kill-tree.js";
5
+ const notStarted = new Set(['ENOENT', 'EACCES', 'ENOTDIR']);
6
+ function missing(program) {
7
+ return kvvideoProblem('PROGRAM_MISSING', `${program.name} can't be started (${program.command}); install it or set ${programSetting(program.name)}.`, { program: program.name, setting: programSetting(program.name) });
8
+ }
9
+ export function runProgram(program, args, options) {
10
+ return new Promise((resolve, reject) => {
11
+ const child = spawn(program.command, args, { cwd: options.cwd, stdio: ['ignore', 'pipe', 'pipe'], detached: startsOwnGroup(process.platform), windowsHide: true });
12
+ let stdout = '';
13
+ let output = '';
14
+ let timedOut = false;
15
+ child.stdout.setEncoding('utf8').on('data', (text) => {
16
+ stdout += text;
17
+ output += text;
18
+ });
19
+ child.stderr.setEncoding('utf8').on('data', (text) => (output += text));
20
+ const abort = () => killTree(child.pid);
21
+ const timer = options.timeoutMs === undefined ? undefined : setTimeout(() => {
22
+ timedOut = true;
23
+ abort();
24
+ }, options.timeoutMs);
25
+ const settle = () => {
26
+ clearTimeout(timer);
27
+ options.signal.removeEventListener('abort', abort);
28
+ };
29
+ options.signal.addEventListener('abort', abort, { once: true });
30
+ if (options.signal.aborted)
31
+ abort();
32
+ child.once('error', (error) => {
33
+ settle();
34
+ reject('code' in error && typeof error.code === 'string' && notStarted.has(error.code) ? missing(program) : error);
35
+ });
36
+ child.once('close', (code) => {
37
+ settle();
38
+ resolve({ exitCode: code ?? 1, stdout, output, timedOut });
39
+ });
40
+ });
41
+ }
42
+ /** The last lines of a run's output, for a Problem's reason. */
43
+ export function lastLines(output, count = 20) {
44
+ return output.trimEnd().split('\n').slice(-count).join('\n');
45
+ }
46
+ //# sourceMappingURL=run-program.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-program.js","sourceRoot":"","sources":["../../src/programs/run-program.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /** Whether a request goes to kvman's port, on any host. */
2
+ export declare function isOwnAddress(url: URL, ownPort: number | null): boolean;
3
+ /** The address, when it is one a recording may open. */
4
+ export declare function localAddress(address: string, ownPort: number | null): URL;
5
+ //# sourceMappingURL=address.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"address.d.ts","sourceRoot":"","sources":["../../src/record/address.ts"],"names":[],"mappings":"AAaA,2DAA2D;AAC3D,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAEtE;AAED,wDAAwD;AACxD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,GAAG,GAAG,CAOzE"}