@editframe/assets 0.6.0-beta.9 → 0.7.0-beta.4

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 (39) hide show
  1. package/dist/lib/av/MP4File.cjs +187 -0
  2. package/dist/lib/av/MP4File.js +170 -0
  3. package/dist/lib/util/execPromise.cjs +6 -0
  4. package/dist/lib/util/execPromise.js +6 -0
  5. package/dist/packages/assets/src/Probe.cjs +224 -0
  6. package/dist/packages/assets/src/Probe.d.ts +646 -0
  7. package/dist/packages/assets/src/Probe.js +207 -0
  8. package/dist/packages/assets/src/VideoRenderOptions.cjs +36 -0
  9. package/dist/packages/assets/src/VideoRenderOptions.d.ts +169 -0
  10. package/dist/packages/assets/src/VideoRenderOptions.js +36 -0
  11. package/dist/packages/assets/src/idempotentTask.cjs +57 -0
  12. package/dist/packages/assets/src/idempotentTask.d.ts +13 -0
  13. package/dist/packages/assets/src/idempotentTask.js +57 -0
  14. package/dist/packages/assets/src/index.cjs +20 -0
  15. package/dist/packages/assets/src/index.d.ts +9 -0
  16. package/dist/packages/assets/src/index.js +20 -0
  17. package/dist/packages/assets/src/md5.cjs +60 -0
  18. package/dist/packages/assets/src/md5.d.ts +6 -0
  19. package/dist/packages/assets/src/md5.js +60 -0
  20. package/dist/packages/assets/src/mp4FileWritable.cjs +21 -0
  21. package/dist/packages/assets/src/mp4FileWritable.d.ts +4 -0
  22. package/dist/packages/assets/src/mp4FileWritable.js +21 -0
  23. package/dist/packages/assets/src/tasks/cacheImage.cjs +22 -0
  24. package/dist/packages/assets/src/tasks/cacheImage.d.ts +1 -0
  25. package/dist/packages/assets/src/tasks/cacheImage.js +22 -0
  26. package/dist/packages/assets/src/tasks/findOrCreateCaptions.cjs +26 -0
  27. package/dist/packages/assets/src/tasks/findOrCreateCaptions.d.ts +1 -0
  28. package/dist/packages/assets/src/tasks/findOrCreateCaptions.js +26 -0
  29. package/dist/packages/assets/src/tasks/generateTrack.cjs +52 -0
  30. package/dist/packages/assets/src/tasks/generateTrack.d.ts +1 -0
  31. package/dist/packages/assets/src/tasks/generateTrack.js +52 -0
  32. package/dist/packages/assets/src/tasks/generateTrackFragmentIndex.cjs +105 -0
  33. package/dist/packages/assets/src/tasks/generateTrackFragmentIndex.d.ts +1 -0
  34. package/dist/packages/assets/src/tasks/generateTrackFragmentIndex.js +105 -0
  35. package/package.json +10 -4
  36. package/src/tasks/cacheImage.ts +22 -0
  37. package/src/tasks/findOrCreateCaptions.ts +29 -0
  38. package/src/tasks/generateTrack.ts +61 -0
  39. package/src/tasks/generateTrackFragmentIndex.ts +120 -0
@@ -0,0 +1,6 @@
1
+ import { ReadStream } from 'node:fs';
2
+ import { Ora } from 'ora';
3
+
4
+ export declare function md5Directory(directory: string, spinner?: Ora): Promise<string>;
5
+ export declare function md5FilePath(filePath: string): Promise<string>;
6
+ export declare function md5ReadStream(readStream: ReadStream): Promise<string>;
@@ -0,0 +1,60 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import crypto from "node:crypto";
5
+ import ora from "ora";
6
+ async function md5Directory(directory, spinner) {
7
+ const shouldEndSpinner = !spinner;
8
+ spinner ||= ora("⚡️ Calculating MD5").start();
9
+ spinner.suffixText = directory;
10
+ const files = await readdir(directory, { withFileTypes: true });
11
+ const hashes = await Promise.all(
12
+ files.map(async (file) => {
13
+ const filePath = join(directory, file.name);
14
+ if (file.isDirectory()) {
15
+ return md5Directory(filePath, spinner);
16
+ }
17
+ spinner.suffixText = filePath;
18
+ return md5FilePath(filePath);
19
+ })
20
+ );
21
+ const hash = crypto.createHash("md5");
22
+ for (const fileHash of hashes) {
23
+ hash.update(fileHash);
24
+ }
25
+ if (shouldEndSpinner) {
26
+ spinner.succeed("MD5 calculated");
27
+ spinner.suffixText = directory;
28
+ }
29
+ return addDashesToUUID(hash.digest("hex"));
30
+ }
31
+ async function md5FilePath(filePath) {
32
+ const readStream = createReadStream(filePath);
33
+ return md5ReadStream(readStream);
34
+ }
35
+ function md5ReadStream(readStream) {
36
+ return new Promise((resolve, reject) => {
37
+ const hash = crypto.createHash("md5");
38
+ readStream.on("data", (data) => {
39
+ hash.update(data);
40
+ });
41
+ readStream.on("error", reject);
42
+ readStream.on("end", () => {
43
+ resolve(addDashesToUUID(hash.digest("hex")));
44
+ });
45
+ });
46
+ }
47
+ function addDashesToUUID(uuidWithoutDashes) {
48
+ if (uuidWithoutDashes.length !== 32) {
49
+ throw new Error("Invalid UUID without dashes. Expected 32 characters.");
50
+ }
51
+ return (
52
+ // biome-ignore lint/style/useTemplate: using a template makes a long line
53
+ uuidWithoutDashes.slice(0, 8) + "-" + uuidWithoutDashes.slice(8, 12) + "-" + uuidWithoutDashes.slice(12, 16) + "-" + uuidWithoutDashes.slice(16, 20) + "-" + uuidWithoutDashes.slice(20, 32)
54
+ );
55
+ }
56
+ export {
57
+ md5Directory,
58
+ md5FilePath,
59
+ md5ReadStream
60
+ };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const node_stream = require("node:stream");
4
+ const mp4FileWritable = (mp4File) => {
5
+ let arrayBufferStart = 0;
6
+ return new node_stream.Writable({
7
+ write: (chunk, _encoding, callback) => {
8
+ const mp4BoxBuffer = chunk.buffer;
9
+ mp4BoxBuffer.fileStart = arrayBufferStart;
10
+ arrayBufferStart += chunk.length;
11
+ mp4File.appendBuffer(mp4BoxBuffer, false);
12
+ callback();
13
+ },
14
+ final: (callback) => {
15
+ mp4File.flush();
16
+ mp4File.processSamples(true);
17
+ callback();
18
+ }
19
+ });
20
+ };
21
+ exports.mp4FileWritable = mp4FileWritable;
@@ -0,0 +1,4 @@
1
+ import { Writable } from 'node:stream';
2
+ import { MP4File } from '../../../lib/av/MP4File';
3
+
4
+ export declare const mp4FileWritable: (mp4File: MP4File) => Writable;
@@ -0,0 +1,21 @@
1
+ import { Writable } from "node:stream";
2
+ const mp4FileWritable = (mp4File) => {
3
+ let arrayBufferStart = 0;
4
+ return new Writable({
5
+ write: (chunk, _encoding, callback) => {
6
+ const mp4BoxBuffer = chunk.buffer;
7
+ mp4BoxBuffer.fileStart = arrayBufferStart;
8
+ arrayBufferStart += chunk.length;
9
+ mp4File.appendBuffer(mp4BoxBuffer, false);
10
+ callback();
11
+ },
12
+ final: (callback) => {
13
+ mp4File.flush();
14
+ mp4File.processSamples(true);
15
+ callback();
16
+ }
17
+ });
18
+ };
19
+ export {
20
+ mp4FileWritable
21
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const idempotentTask = require("../idempotentTask.cjs");
4
+ const node_fs = require("node:fs");
5
+ const path = require("node:path");
6
+ const cacheImageTask = idempotentTask.idempotentTask({
7
+ label: "image",
8
+ filename: (absolutePath) => path.basename(absolutePath),
9
+ runner: async (absolutePath) => {
10
+ return node_fs.createReadStream(absolutePath);
11
+ }
12
+ });
13
+ const cacheImage = async (cacheRoot, absolutePath) => {
14
+ try {
15
+ return await cacheImageTask(cacheRoot, absolutePath);
16
+ } catch (error) {
17
+ console.error(error);
18
+ console.trace("Error generating track fragment index", error);
19
+ throw error;
20
+ }
21
+ };
22
+ exports.cacheImage = cacheImage;
@@ -0,0 +1 @@
1
+ export declare const cacheImage: (cacheRoot: string, absolutePath: string) => Promise<import('../idempotentTask').TaskResult>;
@@ -0,0 +1,22 @@
1
+ import { idempotentTask } from "../idempotentTask.js";
2
+ import { createReadStream } from "node:fs";
3
+ import path from "node:path";
4
+ const cacheImageTask = idempotentTask({
5
+ label: "image",
6
+ filename: (absolutePath) => path.basename(absolutePath),
7
+ runner: async (absolutePath) => {
8
+ return createReadStream(absolutePath);
9
+ }
10
+ });
11
+ const cacheImage = async (cacheRoot, absolutePath) => {
12
+ try {
13
+ return await cacheImageTask(cacheRoot, absolutePath);
14
+ } catch (error) {
15
+ console.error(error);
16
+ console.trace("Error generating track fragment index", error);
17
+ throw error;
18
+ }
19
+ };
20
+ export {
21
+ cacheImage
22
+ };
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const execPromise = require("../../../../lib/util/execPromise.cjs");
4
+ const idempotentTask = require("../idempotentTask.cjs");
5
+ const debug = require("debug");
6
+ const path = require("node:path");
7
+ const log = debug("ef:generateCaptions");
8
+ const generateCaptionData = idempotentTask.idempotentTask({
9
+ label: "captions",
10
+ filename: (absolutePath) => `${path.basename(absolutePath)}.captions.json`,
11
+ runner: async (absolutePath) => {
12
+ const command = `whisper_timestamped --language en --efficient --output_format vtt ${absolutePath}`;
13
+ log(`Running command: ${command}`);
14
+ const { stdout } = await execPromise.execPromise(command);
15
+ return stdout;
16
+ }
17
+ });
18
+ const findOrCreateCaptions = async (cacheRoot, absolutePath) => {
19
+ try {
20
+ return await generateCaptionData(cacheRoot, absolutePath);
21
+ } catch (error) {
22
+ console.trace("Error finding or creating captions", error);
23
+ throw error;
24
+ }
25
+ };
26
+ exports.findOrCreateCaptions = findOrCreateCaptions;
@@ -0,0 +1 @@
1
+ export declare const findOrCreateCaptions: (cacheRoot: string, absolutePath: string) => Promise<import('../idempotentTask').TaskResult>;
@@ -0,0 +1,26 @@
1
+ import { execPromise } from "../../../../lib/util/execPromise.js";
2
+ import { idempotentTask } from "../idempotentTask.js";
3
+ import debug from "debug";
4
+ import { basename } from "node:path";
5
+ const log = debug("ef:generateCaptions");
6
+ const generateCaptionData = idempotentTask({
7
+ label: "captions",
8
+ filename: (absolutePath) => `${basename(absolutePath)}.captions.json`,
9
+ runner: async (absolutePath) => {
10
+ const command = `whisper_timestamped --language en --efficient --output_format vtt ${absolutePath}`;
11
+ log(`Running command: ${command}`);
12
+ const { stdout } = await execPromise(command);
13
+ return stdout;
14
+ }
15
+ });
16
+ const findOrCreateCaptions = async (cacheRoot, absolutePath) => {
17
+ try {
18
+ return await generateCaptionData(cacheRoot, absolutePath);
19
+ } catch (error) {
20
+ console.trace("Error finding or creating captions", error);
21
+ throw error;
22
+ }
23
+ };
24
+ export {
25
+ findOrCreateCaptions
26
+ };
@@ -0,0 +1,52 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const idempotentTask = require("../idempotentTask.cjs");
4
+ const MP4File = require("../../../../lib/av/MP4File.cjs");
5
+ const debug = require("debug");
6
+ const mp4FileWritable = require("../mp4FileWritable.cjs");
7
+ const node_stream = require("node:stream");
8
+ const path = require("node:path");
9
+ const Probe = require("../Probe.cjs");
10
+ const generateTrackTask = idempotentTask.idempotentTask({
11
+ label: "track",
12
+ filename: (absolutePath, trackId) => `${path.basename(absolutePath)}.track-${trackId}.mp4`,
13
+ runner: async (absolutePath, trackId) => {
14
+ const log = debug("ef:generateTrackFragment");
15
+ const probe = await Probe.Probe.probePath(absolutePath);
16
+ const readStream = probe.createConformingReadstream();
17
+ const mp4File = new MP4File.MP4File();
18
+ log(`Generating track fragment index for ${absolutePath}`);
19
+ readStream.pipe(mp4FileWritable.mp4FileWritable(mp4File));
20
+ await new Promise((resolve, reject) => {
21
+ readStream.on("end", resolve);
22
+ readStream.on("error", reject);
23
+ });
24
+ const trackStream = new node_stream.PassThrough();
25
+ for await (const fragment of mp4File.fragmentIterator()) {
26
+ if (fragment.track !== trackId) {
27
+ continue;
28
+ }
29
+ trackStream.write(Buffer.from(fragment.data), "binary");
30
+ }
31
+ trackStream.end();
32
+ return trackStream;
33
+ }
34
+ });
35
+ const generateTrack = async (cacheRoot, absolutePath, url) => {
36
+ try {
37
+ const trackId = new URL(
38
+ `http://localhost${url}` ?? "bad-url"
39
+ ).searchParams.get("trackId");
40
+ if (trackId === null) {
41
+ throw new Error(
42
+ "No trackId provided. IT must be specified in the query string: ?trackId=0"
43
+ );
44
+ }
45
+ return await generateTrackTask(cacheRoot, absolutePath, Number(trackId));
46
+ } catch (error) {
47
+ console.error(error);
48
+ console.trace("Error generating track fragment index", error);
49
+ throw error;
50
+ }
51
+ };
52
+ exports.generateTrack = generateTrack;
@@ -0,0 +1 @@
1
+ export declare const generateTrack: (cacheRoot: string, absolutePath: string, url: string) => Promise<import('../idempotentTask').TaskResult>;
@@ -0,0 +1,52 @@
1
+ import { idempotentTask } from "../idempotentTask.js";
2
+ import { MP4File } from "../../../../lib/av/MP4File.js";
3
+ import debug from "debug";
4
+ import { mp4FileWritable } from "../mp4FileWritable.js";
5
+ import { PassThrough } from "node:stream";
6
+ import { basename } from "node:path";
7
+ import { Probe } from "../Probe.js";
8
+ const generateTrackTask = idempotentTask({
9
+ label: "track",
10
+ filename: (absolutePath, trackId) => `${basename(absolutePath)}.track-${trackId}.mp4`,
11
+ runner: async (absolutePath, trackId) => {
12
+ const log = debug("ef:generateTrackFragment");
13
+ const probe = await Probe.probePath(absolutePath);
14
+ const readStream = probe.createConformingReadstream();
15
+ const mp4File = new MP4File();
16
+ log(`Generating track fragment index for ${absolutePath}`);
17
+ readStream.pipe(mp4FileWritable(mp4File));
18
+ await new Promise((resolve, reject) => {
19
+ readStream.on("end", resolve);
20
+ readStream.on("error", reject);
21
+ });
22
+ const trackStream = new PassThrough();
23
+ for await (const fragment of mp4File.fragmentIterator()) {
24
+ if (fragment.track !== trackId) {
25
+ continue;
26
+ }
27
+ trackStream.write(Buffer.from(fragment.data), "binary");
28
+ }
29
+ trackStream.end();
30
+ return trackStream;
31
+ }
32
+ });
33
+ const generateTrack = async (cacheRoot, absolutePath, url) => {
34
+ try {
35
+ const trackId = new URL(
36
+ `http://localhost${url}` ?? "bad-url"
37
+ ).searchParams.get("trackId");
38
+ if (trackId === null) {
39
+ throw new Error(
40
+ "No trackId provided. IT must be specified in the query string: ?trackId=0"
41
+ );
42
+ }
43
+ return await generateTrackTask(cacheRoot, absolutePath, Number(trackId));
44
+ } catch (error) {
45
+ console.error(error);
46
+ console.trace("Error generating track fragment index", error);
47
+ throw error;
48
+ }
49
+ };
50
+ export {
51
+ generateTrack
52
+ };
@@ -0,0 +1,105 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const idempotentTask = require("../idempotentTask.cjs");
4
+ const MP4File = require("../../../../lib/av/MP4File.cjs");
5
+ const debug = require("debug");
6
+ const mp4FileWritable = require("../mp4FileWritable.cjs");
7
+ const path = require("node:path");
8
+ const Probe = require("../Probe.cjs");
9
+ const generateTrackFragmentIndexFromPath = async (absolutePath) => {
10
+ const log = debug("ef:generateTrackFragment");
11
+ const probe = await Probe.Probe.probePath(absolutePath);
12
+ const readStream = probe.createConformingReadstream();
13
+ const mp4File = new MP4File.MP4File();
14
+ log(`Generating track fragment index for ${absolutePath}`);
15
+ readStream.pipe(mp4FileWritable.mp4FileWritable(mp4File));
16
+ await new Promise((resolve, reject) => {
17
+ readStream.on("end", resolve);
18
+ readStream.on("error", reject);
19
+ });
20
+ const trackFragmentIndexes = {};
21
+ const trackByteOffsets = {};
22
+ for await (const fragment of mp4File.fragmentIterator()) {
23
+ const track = mp4File.getInfo().tracks.find((track2) => track2.id === fragment.track);
24
+ if (!track) {
25
+ throw new Error("Track not found");
26
+ }
27
+ if (fragment.segment === "init") {
28
+ trackByteOffsets[fragment.track] = fragment.data.byteLength;
29
+ if (track?.type === "video") {
30
+ const videoTrack = mp4File.getInfo().videoTracks.find((track2) => track2.id === fragment.track);
31
+ if (!videoTrack) {
32
+ throw new Error("Video track not found");
33
+ }
34
+ trackFragmentIndexes[fragment.track] = {
35
+ track: fragment.track,
36
+ type: "video",
37
+ width: videoTrack.video.width,
38
+ height: videoTrack.video.height,
39
+ timescale: track.timescale,
40
+ sample_count: videoTrack.nb_samples,
41
+ duration: 0,
42
+ initSegment: {
43
+ offset: 0,
44
+ size: fragment.data.byteLength
45
+ },
46
+ segments: []
47
+ };
48
+ }
49
+ if (track?.type === "audio") {
50
+ const audioTrack = mp4File.getInfo().audioTracks.find((track2) => track2.id === fragment.track);
51
+ if (!audioTrack) {
52
+ throw new Error("Audio track not found");
53
+ }
54
+ trackFragmentIndexes[fragment.track] = {
55
+ track: fragment.track,
56
+ type: "audio",
57
+ channel_count: audioTrack.audio.channel_count,
58
+ sample_rate: audioTrack.audio.sample_rate,
59
+ sample_size: audioTrack.audio.sample_size,
60
+ sample_count: audioTrack.nb_samples,
61
+ timescale: track.timescale,
62
+ duration: 0,
63
+ initSegment: {
64
+ offset: 0,
65
+ size: fragment.data.byteLength
66
+ },
67
+ segments: []
68
+ };
69
+ }
70
+ } else {
71
+ const fragmentIndex = trackFragmentIndexes[fragment.track];
72
+ if (trackByteOffsets[fragment.track] === void 0) {
73
+ throw new Error("Fragment index not found");
74
+ }
75
+ if (!fragmentIndex) {
76
+ throw new Error("Fragment index not found");
77
+ }
78
+ fragmentIndex.duration += fragment.duration;
79
+ fragmentIndex.segments.push({
80
+ cts: fragment.cts,
81
+ dts: fragment.dts,
82
+ duration: fragment.duration,
83
+ // biome-ignore lint/style/noNonNullAssertion: This was checked above
84
+ offset: trackByteOffsets[fragment.track],
85
+ size: fragment.data.byteLength
86
+ });
87
+ trackByteOffsets[fragment.track] += fragment.data.byteLength;
88
+ }
89
+ }
90
+ return JSON.stringify(trackFragmentIndexes, null, 2);
91
+ };
92
+ const generateTrackFragmentIndexTask = idempotentTask.idempotentTask({
93
+ label: "trackFragmentIndex",
94
+ filename: (absolutePath) => `${path.basename(absolutePath)}.tracks.json`,
95
+ runner: generateTrackFragmentIndexFromPath
96
+ });
97
+ const generateTrackFragmentIndex = async (cacheRoot, absolutePath) => {
98
+ try {
99
+ return await generateTrackFragmentIndexTask(cacheRoot, absolutePath);
100
+ } catch (error) {
101
+ console.trace("Error generating track fragment index", error);
102
+ throw error;
103
+ }
104
+ };
105
+ exports.generateTrackFragmentIndex = generateTrackFragmentIndex;
@@ -0,0 +1 @@
1
+ export declare const generateTrackFragmentIndex: (cacheRoot: string, absolutePath: string) => Promise<import('../idempotentTask').TaskResult>;
@@ -0,0 +1,105 @@
1
+ import { idempotentTask } from "../idempotentTask.js";
2
+ import { MP4File } from "../../../../lib/av/MP4File.js";
3
+ import debug from "debug";
4
+ import { mp4FileWritable } from "../mp4FileWritable.js";
5
+ import { basename } from "node:path";
6
+ import { Probe } from "../Probe.js";
7
+ const generateTrackFragmentIndexFromPath = async (absolutePath) => {
8
+ const log = debug("ef:generateTrackFragment");
9
+ const probe = await Probe.probePath(absolutePath);
10
+ const readStream = probe.createConformingReadstream();
11
+ const mp4File = new MP4File();
12
+ log(`Generating track fragment index for ${absolutePath}`);
13
+ readStream.pipe(mp4FileWritable(mp4File));
14
+ await new Promise((resolve, reject) => {
15
+ readStream.on("end", resolve);
16
+ readStream.on("error", reject);
17
+ });
18
+ const trackFragmentIndexes = {};
19
+ const trackByteOffsets = {};
20
+ for await (const fragment of mp4File.fragmentIterator()) {
21
+ const track = mp4File.getInfo().tracks.find((track2) => track2.id === fragment.track);
22
+ if (!track) {
23
+ throw new Error("Track not found");
24
+ }
25
+ if (fragment.segment === "init") {
26
+ trackByteOffsets[fragment.track] = fragment.data.byteLength;
27
+ if (track?.type === "video") {
28
+ const videoTrack = mp4File.getInfo().videoTracks.find((track2) => track2.id === fragment.track);
29
+ if (!videoTrack) {
30
+ throw new Error("Video track not found");
31
+ }
32
+ trackFragmentIndexes[fragment.track] = {
33
+ track: fragment.track,
34
+ type: "video",
35
+ width: videoTrack.video.width,
36
+ height: videoTrack.video.height,
37
+ timescale: track.timescale,
38
+ sample_count: videoTrack.nb_samples,
39
+ duration: 0,
40
+ initSegment: {
41
+ offset: 0,
42
+ size: fragment.data.byteLength
43
+ },
44
+ segments: []
45
+ };
46
+ }
47
+ if (track?.type === "audio") {
48
+ const audioTrack = mp4File.getInfo().audioTracks.find((track2) => track2.id === fragment.track);
49
+ if (!audioTrack) {
50
+ throw new Error("Audio track not found");
51
+ }
52
+ trackFragmentIndexes[fragment.track] = {
53
+ track: fragment.track,
54
+ type: "audio",
55
+ channel_count: audioTrack.audio.channel_count,
56
+ sample_rate: audioTrack.audio.sample_rate,
57
+ sample_size: audioTrack.audio.sample_size,
58
+ sample_count: audioTrack.nb_samples,
59
+ timescale: track.timescale,
60
+ duration: 0,
61
+ initSegment: {
62
+ offset: 0,
63
+ size: fragment.data.byteLength
64
+ },
65
+ segments: []
66
+ };
67
+ }
68
+ } else {
69
+ const fragmentIndex = trackFragmentIndexes[fragment.track];
70
+ if (trackByteOffsets[fragment.track] === void 0) {
71
+ throw new Error("Fragment index not found");
72
+ }
73
+ if (!fragmentIndex) {
74
+ throw new Error("Fragment index not found");
75
+ }
76
+ fragmentIndex.duration += fragment.duration;
77
+ fragmentIndex.segments.push({
78
+ cts: fragment.cts,
79
+ dts: fragment.dts,
80
+ duration: fragment.duration,
81
+ // biome-ignore lint/style/noNonNullAssertion: This was checked above
82
+ offset: trackByteOffsets[fragment.track],
83
+ size: fragment.data.byteLength
84
+ });
85
+ trackByteOffsets[fragment.track] += fragment.data.byteLength;
86
+ }
87
+ }
88
+ return JSON.stringify(trackFragmentIndexes, null, 2);
89
+ };
90
+ const generateTrackFragmentIndexTask = idempotentTask({
91
+ label: "trackFragmentIndex",
92
+ filename: (absolutePath) => `${basename(absolutePath)}.tracks.json`,
93
+ runner: generateTrackFragmentIndexFromPath
94
+ });
95
+ const generateTrackFragmentIndex = async (cacheRoot, absolutePath) => {
96
+ try {
97
+ return await generateTrackFragmentIndexTask(cacheRoot, absolutePath);
98
+ } catch (error) {
99
+ console.trace("Error generating track fragment index", error);
100
+ throw error;
101
+ }
102
+ };
103
+ export {
104
+ generateTrackFragmentIndex
105
+ };
package/package.json CHANGED
@@ -1,15 +1,20 @@
1
1
  {
2
2
  "name": "@editframe/assets",
3
- "version": "0.6.0-beta.9",
3
+ "version": "0.7.0-beta.4",
4
4
  "description": "",
5
5
  "exports": {
6
6
  ".": {
7
- "import": "./dist/packages/assets/src/index.js",
8
- "require": "./dist/packages/assets/src/index.cjs"
7
+ "import": {
8
+ "default": "./dist/packages/assets/src/index.js",
9
+ "types": "./dist/packages/assets/src/index.d.ts"
10
+ },
11
+ "require": {
12
+ "default": "./dist/packages/assets/src/index.cjs",
13
+ "types": "./dist/packages/assets/src/index.d.ts"
14
+ }
9
15
  }
10
16
  },
11
17
  "type": "module",
12
- "types": "./dist/packages/assets/src/index.d.ts",
13
18
  "scripts": {
14
19
  "typecheck": "tsc --noEmit --emitDeclarationOnly false",
15
20
  "build": "vite build",
@@ -18,6 +23,7 @@
18
23
  "author": "",
19
24
  "license": "UNLICENSED",
20
25
  "dependencies": {
26
+ "debug": "^4.3.5",
21
27
  "mp4box": "^0.5.2",
22
28
  "zod": "^3.23.8"
23
29
  },
@@ -0,0 +1,22 @@
1
+ import { idempotentTask } from "../idempotentTask";
2
+ import { createReadStream } from "node:fs";
3
+
4
+ import path from "node:path";
5
+
6
+ const cacheImageTask = idempotentTask({
7
+ label: "image",
8
+ filename: (absolutePath: string) => path.basename(absolutePath),
9
+ runner: async (absolutePath) => {
10
+ return createReadStream(absolutePath);
11
+ },
12
+ });
13
+
14
+ export const cacheImage = async (cacheRoot: string, absolutePath: string) => {
15
+ try {
16
+ return await cacheImageTask(cacheRoot, absolutePath);
17
+ } catch (error) {
18
+ console.error(error);
19
+ console.trace("Error generating track fragment index", error);
20
+ throw error;
21
+ }
22
+ };
@@ -0,0 +1,29 @@
1
+ import { execPromise } from "@/util/execPromise";
2
+ import { idempotentTask } from "../idempotentTask";
3
+ import debug from "debug";
4
+ import { basename } from "node:path";
5
+
6
+ const log = debug("ef:generateCaptions");
7
+
8
+ const generateCaptionData = idempotentTask({
9
+ label: "captions",
10
+ filename: (absolutePath) => `${basename(absolutePath)}.captions.json`,
11
+ runner: async (absolutePath) => {
12
+ const command = `whisper_timestamped --language en --efficient --output_format vtt ${absolutePath}`;
13
+ log(`Running command: ${command}`);
14
+ const { stdout } = await execPromise(command);
15
+ return stdout;
16
+ },
17
+ });
18
+
19
+ export const findOrCreateCaptions = async (
20
+ cacheRoot: string,
21
+ absolutePath: string,
22
+ ) => {
23
+ try {
24
+ return await generateCaptionData(cacheRoot, absolutePath);
25
+ } catch (error) {
26
+ console.trace("Error finding or creating captions", error);
27
+ throw error;
28
+ }
29
+ };