@omnimedia/omnitool 1.1.0-94 → 1.1.0-96

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 (91) hide show
  1. package/package.json +1 -1
  2. package/s/features/bg-remover/bg-remover.ts +26 -0
  3. package/s/features/bg-remover/default-spec.ts +11 -0
  4. package/s/features/bg-remover/types.ts +27 -0
  5. package/s/features/bg-remover/worker.bundle.ts +51 -0
  6. package/s/features/{speech/transcribe/parts → parts}/load-pipe.ts +3 -5
  7. package/s/features/parts/types.ts +19 -0
  8. package/s/features/speech/transcribe/default-spec.ts +2 -2
  9. package/s/features/speech/transcribe/types.ts +6 -16
  10. package/s/features/speech/transcribe/worker.bundle.ts +4 -3
  11. package/s/index.html.ts +2 -2
  12. package/s/timeline/parts/captions.ts +1 -0
  13. package/s/timeline/parts/item.ts +16 -5
  14. package/s/timeline/parts/media.ts +11 -0
  15. package/s/timeline/renderers/parts/handy.ts +16 -0
  16. package/s/timeline/renderers/parts/samplers/visual/parts/defaults.ts +7 -1
  17. package/s/timeline/renderers/parts/samplers/visual/parts/image-sink.ts +51 -0
  18. package/s/timeline/renderers/parts/samplers/visual/parts/sample.ts +7 -0
  19. package/s/timeline/renderers/parts/samplers/visual/parts/types.ts +2 -1
  20. package/s/timeline/renderers/parts/samplers/visual/sampler.ts +8 -5
  21. package/s/timeline/sugar/helpers.ts +9 -0
  22. package/s/timeline/sugar/o.ts +21 -1
  23. package/x/demo/demo.bundle.min.js +100 -100
  24. package/x/demo/demo.bundle.min.js.map +4 -4
  25. package/x/features/bg-remover/bg-remover.d.ts +5 -0
  26. package/x/features/bg-remover/bg-remover.js +18 -0
  27. package/x/features/bg-remover/bg-remover.js.map +1 -0
  28. package/x/features/bg-remover/default-spec.d.ts +2 -0
  29. package/x/features/bg-remover/default-spec.js +6 -0
  30. package/x/features/bg-remover/default-spec.js.map +1 -0
  31. package/x/features/bg-remover/types.d.ts +20 -0
  32. package/x/features/bg-remover/types.js +2 -0
  33. package/x/features/bg-remover/types.js.map +1 -0
  34. package/x/features/bg-remover/worker.bundle.d.ts +1 -0
  35. package/x/features/bg-remover/worker.bundle.js +38 -0
  36. package/x/features/bg-remover/worker.bundle.js.map +1 -0
  37. package/x/features/bg-remover/worker.bundle.min.js +2916 -0
  38. package/x/features/bg-remover/worker.bundle.min.js.map +7 -0
  39. package/x/features/parts/load-pipe.d.ts +2 -0
  40. package/x/features/{speech/transcribe/parts → parts}/load-pipe.js +1 -1
  41. package/x/features/parts/load-pipe.js.map +1 -0
  42. package/x/features/parts/types.d.ts +15 -0
  43. package/x/features/parts/types.js +2 -0
  44. package/x/features/parts/types.js.map +1 -0
  45. package/x/features/speech/transcribe/default-spec.js +2 -2
  46. package/x/features/speech/transcribe/default-spec.js.map +1 -1
  47. package/x/features/speech/transcribe/types.d.ts +5 -14
  48. package/x/features/speech/transcribe/worker.bundle.js +3 -2
  49. package/x/features/speech/transcribe/worker.bundle.js.map +1 -1
  50. package/x/features/speech/transcribe/worker.bundle.min.js +1 -1
  51. package/x/features/speech/transcribe/worker.bundle.min.js.map +3 -3
  52. package/x/index.html +4 -4
  53. package/x/index.html.js +2 -2
  54. package/x/tests.bundle.min.js +103 -103
  55. package/x/tests.bundle.min.js.map +4 -4
  56. package/x/tests.html +1 -1
  57. package/x/timeline/parts/captions.d.ts +1 -0
  58. package/x/timeline/parts/captions.js.map +1 -1
  59. package/x/timeline/parts/item.d.ts +15 -6
  60. package/x/timeline/parts/item.js +1 -0
  61. package/x/timeline/parts/item.js.map +1 -1
  62. package/x/timeline/parts/media.d.ts +1 -0
  63. package/x/timeline/parts/media.js +8 -0
  64. package/x/timeline/parts/media.js.map +1 -1
  65. package/x/timeline/renderers/parts/handy.d.ts +1 -0
  66. package/x/timeline/renderers/parts/handy.js +11 -0
  67. package/x/timeline/renderers/parts/handy.js.map +1 -1
  68. package/x/timeline/renderers/parts/samplers/visual/parts/defaults.d.ts +4 -1
  69. package/x/timeline/renderers/parts/samplers/visual/parts/defaults.js +3 -0
  70. package/x/timeline/renderers/parts/samplers/visual/parts/defaults.js.map +1 -1
  71. package/x/timeline/renderers/parts/samplers/visual/parts/image-sink.d.ts +11 -0
  72. package/x/timeline/renderers/parts/samplers/visual/parts/image-sink.js +36 -0
  73. package/x/timeline/renderers/parts/samplers/visual/parts/image-sink.js.map +1 -0
  74. package/x/timeline/renderers/parts/samplers/visual/parts/sample.js +6 -0
  75. package/x/timeline/renderers/parts/samplers/visual/parts/sample.js.map +1 -1
  76. package/x/timeline/renderers/parts/samplers/visual/parts/types.d.ts +2 -1
  77. package/x/timeline/renderers/parts/samplers/visual/parts/{sink.js → video-sink.js} +1 -1
  78. package/x/timeline/renderers/parts/samplers/visual/parts/video-sink.js.map +1 -0
  79. package/x/timeline/renderers/parts/samplers/visual/sampler.js +8 -5
  80. package/x/timeline/renderers/parts/samplers/visual/sampler.js.map +1 -1
  81. package/x/timeline/sugar/helpers.d.ts +3 -0
  82. package/x/timeline/sugar/helpers.js +3 -0
  83. package/x/timeline/sugar/helpers.js.map +1 -1
  84. package/x/timeline/sugar/o.d.ts +3 -0
  85. package/x/timeline/sugar/o.js +14 -1
  86. package/x/timeline/sugar/o.js.map +1 -1
  87. package/x/features/speech/transcribe/parts/load-pipe.d.ts +0 -2
  88. package/x/features/speech/transcribe/parts/load-pipe.js.map +0 -1
  89. package/x/timeline/renderers/parts/samplers/visual/parts/sink.js.map +0 -1
  90. /package/s/timeline/renderers/parts/samplers/visual/parts/{sink.ts → video-sink.ts} +0 -0
  91. /package/x/timeline/renderers/parts/samplers/visual/parts/{sink.d.ts → video-sink.d.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnimedia/omnitool",
3
- "version": "1.1.0-94",
3
+ "version": "1.1.0-96",
4
4
  "description": "open source video processing tools",
5
5
  "license": "MIT",
6
6
  "author": "Przemysław Gałęzki",
@@ -0,0 +1,26 @@
1
+ import {queue} from "@e280/stz"
2
+ import {Comrade, LoggerTap, tune} from "@e280/comrade"
3
+
4
+ import {BgRemoverOptions, BgRemoverSchematic, RemoverOptions} from "./types.js"
5
+
6
+
7
+ export async function makeBgRemover({spec, workerUrl, onLoading}: BgRemoverOptions) {
8
+ const thread = await Comrade.thread<BgRemoverSchematic>({
9
+ label: "OmnitoolBgRemover",
10
+ workerUrl,
11
+ tap: new LoggerTap(),
12
+ setupHost: () => ({
13
+ loading: async loading => onLoading(loading),
14
+ }),
15
+ })
16
+
17
+ await thread.work.prepare(spec)
18
+
19
+ return {
20
+ remove: queue(async(input: RemoverOptions) =>
21
+ await thread.work.remove[tune]({transfer: [input.frame]})(input.frame)
22
+ ),
23
+ dispose: () => thread.terminate()
24
+ }
25
+ }
26
+
@@ -0,0 +1,11 @@
1
+
2
+ import {PipelineSpec} from "../parts/types.js"
3
+
4
+
5
+ export const defaultBgRemoverSpec = (): PipelineSpec => ({
6
+ model: "Xenova/modnet",
7
+ dtype: "auto",
8
+ device: "webgpu"
9
+ })
10
+
11
+
@@ -0,0 +1,27 @@
1
+
2
+ import {AsSchematic} from "@e280/comrade"
3
+ import {Loading, PipelineSpec} from "../parts/types.js"
4
+
5
+ export type BgRemoverSchematic = AsSchematic<{
6
+ work: {
7
+ prepare(spec: PipelineSpec): Promise<void>
8
+ remove(request: VideoFrame): Promise<VideoFrame>
9
+ },
10
+
11
+ host: {
12
+ loading(load: Loading): Promise<void>
13
+ }
14
+ }>
15
+
16
+ export type RemoverOptions = {
17
+ frame: VideoFrame
18
+ }
19
+
20
+ export type BgRemoverModels = "onnx-community/ISNet-ONNX" | "Xenova/modnet" | "briaai/RMBG-1.4"
21
+
22
+ export type BgRemoverOptions = {
23
+ spec: PipelineSpec
24
+ workerUrl: URL | string
25
+ onLoading: (loading: Loading) => void
26
+ }
27
+
@@ -0,0 +1,51 @@
1
+
2
+ import {defer, once} from "@e280/stz"
3
+ import {Comrade, Host} from "@e280/comrade"
4
+ import {BackgroundRemovalPipeline} from "@huggingface/transformers"
5
+
6
+ import {PipelineSpec} from "../parts/types.js"
7
+ import {BgRemoverSchematic} from "./types.js"
8
+ import {loadPipe} from "../parts/load-pipe.js"
9
+
10
+ const deferred = defer<{spec: PipelineSpec, pipe: BackgroundRemovalPipeline}>()
11
+ const makePrepare = (host: Host<BgRemoverSchematic>) => once(async(spec: PipelineSpec) => {
12
+ deferred.resolve({
13
+ spec,
14
+ pipe: await loadPipe({
15
+ spec,
16
+ task: "background-removal",
17
+ onLoading: loading => host.loading(loading),
18
+ }) as BackgroundRemovalPipeline
19
+ })
20
+ })
21
+
22
+ const canvas = new OffscreenCanvas(1920, 1080)
23
+ const ctx = canvas.getContext("2d")
24
+
25
+ await Comrade.worker<BgRemoverSchematic>(shell => {
26
+ const prepare = makePrepare(shell.host)
27
+ return {
28
+ prepare,
29
+ async remove(request) {
30
+ const {pipe} = await deferred.promise
31
+
32
+ canvas.width = request.displayWidth
33
+ canvas.height = request.displayHeight
34
+ ctx?.drawImage(request, 0, 0)
35
+
36
+ const output = await pipe(canvas)
37
+ const mask = output[0]
38
+
39
+ const frame = new VideoFrame(mask.toCanvas(), {
40
+ timestamp: request.timestamp,
41
+ duration: request.duration ?? undefined,
42
+ })
43
+
44
+ request.close()
45
+ shell.transfer = [frame]
46
+ return frame
47
+ }
48
+ }
49
+ })
50
+
51
+
@@ -1,12 +1,11 @@
1
1
 
2
2
  import {pipeline} from "@huggingface/transformers"
3
+ import {PipeOptions} from "./types.js"
3
4
 
4
- import {TranscriberPipeOptions} from "../types.js"
5
-
6
- export async function loadPipe(options: TranscriberPipeOptions) {
5
+ export async function loadPipe(options: PipeOptions) {
7
6
  const {spec, onLoading} = options
8
7
 
9
- const pipe = await pipeline("automatic-speech-recognition", spec.model, {
8
+ const pipe = await pipeline(options.task, spec.model, {
10
9
  device: spec.device,
11
10
  dtype: spec.dtype,
12
11
  progress_callback: (data: any) => {
@@ -16,4 +15,3 @@ export async function loadPipe(options: TranscriberPipeOptions) {
16
15
 
17
16
  return pipe
18
17
  }
19
-
@@ -0,0 +1,19 @@
1
+
2
+ import {DataType, DeviceType, TaskType} from "@huggingface/transformers"
3
+
4
+ export type Loading = {
5
+ total: number
6
+ progress: number
7
+ }
8
+
9
+ export type PipelineSpec<Extras extends object = {}> = {
10
+ model: string
11
+ dtype: DataType
12
+ device: DeviceType
13
+ } & Extras
14
+
15
+ export type PipeOptions = {
16
+ spec: PipelineSpec
17
+ task: TaskType
18
+ onLoading: (loading: Loading) => void
19
+ }
@@ -3,8 +3,8 @@ import {TranscriberSpec} from "./types.js"
3
3
 
4
4
  export const defaultTranscriberSpec = (): TranscriberSpec => ({
5
5
  model: "onnx-community/whisper-tiny_timestamped",
6
- dtype: "q4",
7
- device: "wasm",
6
+ dtype: "auto",
7
+ device: "webgpu",
8
8
  chunkLength: 20,
9
9
  strideLength: 3,
10
10
  })
@@ -1,7 +1,8 @@
1
1
 
2
2
  import {AsSchematic} from "@e280/comrade"
3
- import {DataType, DeviceType, Pipeline} from "@huggingface/transformers"
3
+ import {Pipeline} from "@huggingface/transformers"
4
4
 
5
+ import {Loading, PipelineSpec} from "../../parts/types.js"
5
6
  import {Driver} from "../../../driver/driver.js"
6
7
 
7
8
  export type TranscriberSchematic = AsSchematic<{
@@ -17,11 +18,6 @@ export type TranscriberSchematic = AsSchematic<{
17
18
  }
18
19
  }>
19
20
 
20
- export type Loading = {
21
- total: number
22
- progress: number
23
- }
24
-
25
21
  export type TranscribeOptions = {
26
22
  pipe: Pipeline
27
23
  spec: TranscriberSpec
@@ -29,11 +25,6 @@ export type TranscribeOptions = {
29
25
  callbacks: TranscriptionCallbacks
30
26
  }
31
27
 
32
- export type TranscriberPipeOptions = {
33
- spec: TranscriberSpec
34
- onLoading: (loading: Loading) => void
35
- }
36
-
37
28
  export type SpeechTime = [start: number, end: number]
38
29
 
39
30
  export type TranscriptWord = {
@@ -48,13 +39,12 @@ export type Transcription = {
48
39
  chunks: TranscriptWord[]
49
40
  }
50
41
 
51
- export type TranscriberSpec = {
52
- model: string
53
- dtype: DataType
54
- device: DeviceType
42
+ export type TranscriberModels = "onnx-community/whisper-tiny_timestamped"
43
+
44
+ export type TranscriberSpec = PipelineSpec<{
55
45
  chunkLength: number
56
46
  strideLength: number
57
- }
47
+ }>
58
48
 
59
49
  export type TranscriptionOptions = {
60
50
  source: Blob
@@ -1,9 +1,9 @@
1
1
 
2
2
  import {defer, once} from "@e280/stz"
3
3
  import {Comrade, Host} from "@e280/comrade"
4
- import {Pipeline} from "@huggingface/transformers"
4
+ import {AutomaticSpeechRecognitionPipeline, Pipeline} from "@huggingface/transformers"
5
5
 
6
- import {loadPipe} from "./parts/load-pipe.js"
6
+ import {loadPipe} from "../../parts/load-pipe.js"
7
7
  import {transcribe} from "./parts/transcribe.js"
8
8
  import {TranscriberSchematic, TranscriberSpec} from "./types.js"
9
9
 
@@ -14,8 +14,9 @@ const makePrepare = (host: Host<TranscriberSchematic>) => once(async(spec: Trans
14
14
  spec,
15
15
  pipe: await loadPipe({
16
16
  spec,
17
+ task: "automatic-speech-recognition",
17
18
  onLoading: loading => host.loading(loading),
18
- }),
19
+ }) as AutomaticSpeechRecognitionPipeline
19
20
  })
20
21
  })
21
22
 
package/s/index.html.ts CHANGED
@@ -96,7 +96,7 @@ export default ssg.page(import.meta.url, async orb => ({
96
96
  <p>Build timeline and run the playback engine.</p>
97
97
  </header>
98
98
  <div class="demo-controls">
99
- <input type="file" accept="video/*,audio/*" />
99
+ <input type="file" accept="video/*,audio/*,image/*" />
100
100
  </div>
101
101
  <div class="player-canvas"></div>
102
102
  <div class="player">
@@ -120,7 +120,7 @@ export default ssg.page(import.meta.url, async orb => ({
120
120
  <p>Build timeline and export a render.</p>
121
121
  </header>
122
122
  <div class="demo-controls">
123
- <input type="file" accept="video/*,audio/*" />
123
+ <input type="file" accept="video/*,audio/*,image/*" />
124
124
  <button data-action="export" disabled>Export</button>
125
125
  </div>
126
126
  <div class="demo-progress">
@@ -5,6 +5,7 @@ import {TransformOptions, Vec2} from "../types.js"
5
5
  import {Transcription, TranscriptSegment} from "../../features/speech/transcribe/types.js"
6
6
 
7
7
  export type CaptionOptions = {
8
+ itemId?: Item.Caption["itemId"]
8
9
  start?: number
9
10
  duration?: number
10
11
  styles?: TextStyleOptions
@@ -21,7 +21,8 @@ export enum Kind {
21
21
  Transition,
22
22
  TextStyle,
23
23
  Filter,
24
- Caption
24
+ Caption,
25
+ Image
25
26
  }
26
27
 
27
28
  export enum Effect {
@@ -89,7 +90,16 @@ export namespace Item {
89
90
  spatialId?: Id
90
91
  animationIds?: Id[]
91
92
  filterIds?: Id[]
92
- captionId?: Id
93
+ }
94
+
95
+ export type Image = {
96
+ id: Id
97
+ kind: Kind.Image
98
+ mediaHash: Hash
99
+ duration: number
100
+ spatialId?: Id
101
+ animationIds?: Id[]
102
+ filterIds?: Id[]
93
103
  }
94
104
 
95
105
  export type Audio = {
@@ -99,7 +109,6 @@ export namespace Item {
99
109
  start: number
100
110
  duration: number
101
111
  gain?: number
102
- captionId?: Id
103
112
  }
104
113
 
105
114
  export type Text = {
@@ -118,6 +127,7 @@ export namespace Item {
118
127
  id: Id
119
128
  kind: Kind.Caption
120
129
  transcript: Transcription
130
+ itemId?: Id
121
131
  start: number
122
132
  duration: number
123
133
  maxChars?: number
@@ -140,6 +150,7 @@ export namespace Item {
140
150
  | Sequence
141
151
  | Stack
142
152
  | Video
153
+ | Image
143
154
  | Audio
144
155
  | Text
145
156
  | Caption
@@ -154,8 +165,8 @@ export namespace Item {
154
165
 
155
166
  export type ContainerItem = Item.Sequence | Item.Stack
156
167
  export type NonContainerItem = Exclude<Item.Any, ContainerItem>
157
- export type FilterableItem = Item.Sequence | Item.Stack | Item.Video | Item.Text | Item.Caption
158
- export type VisualAnimatableItem = Item.Video | Item.Text | Item.Caption
168
+ export type FilterableItem = Item.Sequence | Item.Stack | Item.Video | Item.Image | Item.Text | Item.Caption
169
+ export type VisualAnimatableItem = Item.Video | Item.Image | Item.Text | Item.Caption
159
170
 
160
171
  export type PlayableItem = Item.Any & {
161
172
  start: Ms
@@ -7,6 +7,7 @@ import {loadDecoderSource} from "../../driver/utils/load-decoder-source.js"
7
7
 
8
8
  export class Media {
9
9
  duration = 0
10
+ isImage = false
10
11
  hasVideo = false
11
12
  hasAudio = false
12
13
 
@@ -14,6 +15,12 @@ export class Media {
14
15
 
15
16
  static async analyze(datafile: Datafile) {
16
17
  const media = new this(datafile)
18
+
19
+ if (this.#isImage(datafile)) {
20
+ media.isImage = true
21
+ return media
22
+ }
23
+
17
24
  const duration = (await this.duration(datafile.url)) * 1000
18
25
  media.duration = duration
19
26
  const {video, audio} = await this.#has(datafile.url)
@@ -22,6 +29,10 @@ export class Media {
22
29
  return media
23
30
  }
24
31
 
32
+ static #isImage(datafile: Datafile) {
33
+ return datafile.blob.type.startsWith("image/")
34
+ }
35
+
25
36
  static async duration(source: DecoderSource) {
26
37
  const input = new Input({
27
38
  formats: ALL_FORMATS,
@@ -15,6 +15,7 @@ type WalkAtCallbacks = {
15
15
  sequence: (x: Item.Sequence, localTime: Ms, ancestors: AncestorAt[]) => void
16
16
  stack: (x: Item.Stack, localTime: Ms, ancestors: AncestorAt[]) => void
17
17
  video: (x: Item.Video, localTime: Ms, ancestors: AncestorAt[]) => void
18
+ image: (x: Item.Image, localTime: Ms, ancestors: AncestorAt[]) => void
18
19
  text: (x: Item.Text, localTime: Ms, ancestors: AncestorAt[]) => void
19
20
  caption: (x: Item.Caption, localTime: Ms, ancestors: AncestorAt[]) => void
20
21
  audio: (x: Item.Audio, localTime: Ms, ancestors: AncestorAt[]) => void
@@ -24,6 +25,7 @@ type WalkCallbacks = {
24
25
  sequence?: (x: Item.Sequence, matrix: Mat6, ancestors: AncestorAt[]) => void
25
26
  stack?: (x: Item.Stack, matrix: Mat6, ancestors: AncestorAt[]) => void
26
27
  video?: (x: Item.Video, matrix: Mat6, ancestors: AncestorAt[]) => void
28
+ image?: (x: Item.Image, matrix: Mat6, ancestors: AncestorAt[]) => void
27
29
  text?: (x: Item.Text, matrix: Mat6, ancestors: AncestorAt[]) => void
28
30
  caption?: (x: Item.Caption, matrix: Mat6, ancestors: AncestorAt[]) => void
29
31
  audio?: (x: Item.Audio) => void
@@ -53,6 +55,7 @@ export function itemsAt(p: Props): At[] {
53
55
  sequence: () => { },
54
56
  stack: () => { },
55
57
  video: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
58
+ image: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
56
59
  text: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
57
60
  caption: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
58
61
  audio: (item, localTime, ancestors) => results.push({ item, localTime, ancestors })
@@ -74,6 +77,7 @@ export function itemsFrom(p: FromProps): At[] {
74
77
  sequence: () => { },
75
78
  stack: () => { },
76
79
  video: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
80
+ image: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
77
81
  text: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
78
82
  caption: (item, localTime, ancestors) => results.push({ item, localTime, ancestors }),
79
83
  audio: (item, localTime, ancestors) => results.push({ item, localTime, ancestors })
@@ -176,6 +180,10 @@ export function walk(
176
180
  callbacks.video?.(item, currentMatrix, ancestors)
177
181
  break
178
182
 
183
+ case Kind.Image:
184
+ callbacks.image?.(item, currentMatrix, ancestors)
185
+ break
186
+
179
187
  case Kind.Text:
180
188
  callbacks.text?.(item, currentMatrix, ancestors)
181
189
  break
@@ -246,6 +254,10 @@ function walkAt(
246
254
  callbacks.video(item, time, ancestors)
247
255
  break
248
256
 
257
+ case Kind.Image:
258
+ callbacks.image(item, time, ancestors)
259
+ break
260
+
249
261
  case Kind.Text:
250
262
  callbacks.text(item, time, ancestors)
251
263
  break
@@ -318,6 +330,10 @@ function walkFrom(
318
330
  callbacks.video(item, from, ancestors)
319
331
  break
320
332
 
333
+ case Kind.Image:
334
+ callbacks.image(item, from, ancestors)
335
+ break
336
+
321
337
  case Kind.Text:
322
338
  callbacks.text(item, from, ancestors)
323
339
  break
@@ -1,9 +1,11 @@
1
1
 
2
- import {VideoSink} from "./sink.js"
2
+ import {VideoSink} from "./video-sink.js"
3
+ import {ImageSink} from "./image-sink.js"
3
4
  import {Ms} from "../../../../../../units/ms.js"
4
5
  import {Item} from "../../../../../parts/item.js"
5
6
 
6
7
  export type VideoSampler = (item: Item.Video, time: Ms) => Promise<VideoFrame | undefined>
8
+ export type ImageSampler = (item: Item.Image, time: Ms) => Promise<VideoFrame | undefined>
7
9
 
8
10
  export function createDefaultVideoSampler(sink: VideoSink): VideoSampler {
9
11
  return async (item, time) => {
@@ -15,3 +17,7 @@ export function createDefaultVideoSampler(sink: VideoSink): VideoSampler {
15
17
  return frame ?? undefined
16
18
  }
17
19
  }
20
+
21
+ export function createDefaultImageSampler(sink: ImageSink): ImageSampler {
22
+ return async (_item, time) => await sink.getFrame(_item.mediaHash, time)
23
+ }
@@ -0,0 +1,51 @@
1
+
2
+ import {Ms} from "../../../../../../units/ms.js"
3
+ import {Hash} from "../../../../../parts/basics.js"
4
+ import {DecoderSource} from "../../../../../../driver/fns/schematic.js"
5
+
6
+ type CachedImage = {
7
+ bitmap: ImageBitmap
8
+ }
9
+
10
+ export class ImageSink {
11
+ readonly #images = new Map<Hash, CachedImage>()
12
+
13
+ constructor(
14
+ private resolveMedia: (hash: string) => DecoderSource,
15
+ ) {}
16
+
17
+ async getFrame(hash: Hash, time: Ms) {
18
+ const image = await this.#getImage(hash)
19
+ return new VideoFrame(image.bitmap, {
20
+ timestamp: Math.round(time * 1000),
21
+ })
22
+ }
23
+
24
+ async #getImage(hash: Hash) {
25
+ const existing = this.#images.get(hash)
26
+ if (existing)
27
+ return existing
28
+
29
+ const source = this.resolveMedia(hash)
30
+ const blob = source instanceof Blob
31
+ ? source
32
+ : await fetch(source).then(response => response.blob())
33
+ const image = {bitmap: await createImageBitmap(blob)}
34
+
35
+ this.#images.set(hash, image)
36
+ return image
37
+ }
38
+
39
+ disposeAll() {
40
+ for (const image of this.#images.values())
41
+ image.bitmap.close()
42
+ this.#images.clear()
43
+ }
44
+
45
+ dispose(hash: Hash) {
46
+ const image = this.#images.get(hash)
47
+ image?.bitmap.close()
48
+ this.#images.delete(hash)
49
+ }
50
+ }
51
+
@@ -49,6 +49,13 @@ export async function sampleVisual(
49
49
  return frame ? [{kind: "image", frame, matrix, alpha, crop, filters, id: item.id}] : []
50
50
  }
51
51
 
52
+ case Kind.Image: {
53
+ if (time < 0 || time >= item.duration) return []
54
+
55
+ const frame = await ctx.imageSampler(item, time)
56
+ return frame ? [{kind: "image", frame, matrix, alpha, crop, filters, id: item.id}] : []
57
+ }
58
+
52
59
  case Kind.Text: {
53
60
  if (time < 0 || time >= item.duration) return []
54
61
 
@@ -1,9 +1,10 @@
1
1
 
2
- import {VideoSampler} from "./defaults.js"
2
+ import {ImageSampler, VideoSampler} from "./defaults.js"
3
3
  import {Item} from "../../../../../parts/item.js"
4
4
  import {TimelineFile} from "../../../../../parts/basics.js"
5
5
 
6
6
  export type SampleContext = {
7
+ imageSampler: ImageSampler
7
8
  videoSampler: VideoSampler
8
9
  timeline: TimelineFile
9
10
  items: Map<number, Item.Any>
@@ -1,17 +1,20 @@
1
1
 
2
- import {VideoSink} from "./parts/sink.js"
2
+ import {VideoSink} from "./parts/video-sink.js"
3
+ import {ImageSink} from "./parts/image-sink.js"
3
4
  import {sampleVisual} from "./parts/sample.js"
4
5
  import {Ms} from "../../../../../units/ms.js"
5
6
  import {TimelineFile} from "../../../../parts/basics.js"
6
7
  import {DecoderSource} from "../../../../../driver/fns/schematic.js"
7
- import {createDefaultVideoSampler, VideoSampler} from "./parts/defaults.js"
8
+ import {createDefaultImageSampler, createDefaultVideoSampler, VideoSampler} from "./parts/defaults.js"
8
9
 
9
10
  export function createVisualSampler(
10
11
  resolveMedia: (hash: string) => DecoderSource,
11
12
  sampleVideo?: VideoSampler
12
13
  ) {
13
- const sink = new VideoSink(resolveMedia)
14
- const videoSampler = sampleVideo ?? createDefaultVideoSampler(sink)
14
+ const imageSink = new ImageSink(resolveMedia)
15
+ const videoSink = new VideoSink(resolveMedia)
16
+ const imageSampler = createDefaultImageSampler(imageSink)
17
+ const videoSampler = sampleVideo ?? createDefaultVideoSampler(videoSink)
15
18
 
16
19
  return {
17
20
  async sample(timeline: TimelineFile, timecode: Ms) {
@@ -21,7 +24,7 @@ export function createVisualSampler(
21
24
  if (!root)
22
25
  return []
23
26
 
24
- return sampleVisual({videoSampler, timeline, items}, root, timecode, [])
27
+ return sampleVisual({imageSampler, videoSampler, timeline, items}, root, timecode, [])
25
28
  }
26
29
  }
27
30
  }
@@ -56,6 +56,15 @@ export function video(
56
56
  return o => o.video(media, options)
57
57
  }
58
58
 
59
+ export function image(
60
+ media: Media,
61
+ options?: {
62
+ duration?: number
63
+ }
64
+ ): Build<Item.Image> {
65
+ return o => o.image(media, options)
66
+ }
67
+
59
68
  export function audio(
60
69
  media: Media,
61
70
  options?: {
@@ -269,6 +269,25 @@ export class O {
269
269
  return item
270
270
  }
271
271
 
272
+ image = (
273
+ media: Media,
274
+ options?: {
275
+ duration?: number
276
+ }): Item.Image => {
277
+
278
+ if(!media.isImage)
279
+ throw new Error(`Image error: media "${media.datafile.filename}" is not an image.`)
280
+
281
+ const item: Item.Image = {
282
+ kind: Kind.Image,
283
+ id: this.getId(),
284
+ mediaHash: media.datafile.checksum.hash,
285
+ duration: options?.duration ?? 2000
286
+ }
287
+ this.register(item)
288
+ return item
289
+ }
290
+
272
291
  audio = (
273
292
  media: Media,
274
293
  options?: {
@@ -322,6 +341,7 @@ export class O {
322
341
  id: this.getId(),
323
342
  kind: Kind.Caption,
324
343
  transcript,
344
+ itemId: options?.itemId,
325
345
  start,
326
346
  duration,
327
347
  maxChars: options?.maxChars,
@@ -343,10 +363,10 @@ export class O {
343
363
  const action = ((item: CaptionSourceItem, transcript: Transcription, options?: CaptionOptions): Item.Stack => {
344
364
  const caption = make(transcript, {
345
365
  ...options,
366
+ itemId: item.id,
346
367
  start: options?.start ?? item.start,
347
368
  duration: options?.duration ?? item.duration,
348
369
  })
349
- this.set<CaptionSourceItem>(item.id, {captionId: caption.id})
350
370
  return this.stack(caption, item)
351
371
  }) as CaptionAction
352
372