waha-shared 1.0.335 → 1.0.336
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.
- package/dist/data/bibleAudios/bibleAudios.json +0 -10
- package/dist/data/bibleStatuses/bibleStatuses.json +2013 -32
- package/dist/data/bibleTexts/bibleTexts.json +0 -10
- package/dist/data/languages/index.d.ts +1 -5
- package/dist/data/languages/languages.json +48 -139
- package/dist/data/languages/languages.schema.json +3 -24
- package/dist/data/languages/languages.zod.d.ts +1 -10
- package/dist/data/languages/languages.zod.js +5 -18
- package/dist/data/mediaDurations/mediaDurations.json +675 -24823
- package/dist/data/releaseNotes/releaseNotes.json +9 -2
- package/dist/data/translationsApp/index.d.ts +0 -1
- package/dist/data/translationsApp/translationsApp.json +0 -41
- package/dist/data/translationsApp/translationsApp.zod.d.ts +0 -2
- package/dist/data/translationsApp/translationsApp.zod.js +0 -1
- package/dist/data/youtubeVideos/youtubeVideos.json +115 -55
- package/dist/functions/scripturePassages.js +11 -9
- package/dist/functions/sets.d.ts +4 -15
- package/dist/functions/sets.js +228 -168
- package/dist/functions/utils.d.ts +1 -6
- package/dist/functions/utils.js +4 -18
- package/dist/types/sets.d.ts +19 -22
- package/package.json +2 -3
- package/dist/data/languageAssets/index.d.ts +0 -1
- package/dist/data/languageAssets/index.js +0 -7
- package/dist/data/languageAssets/languageAssets.json +0 -44404
- package/dist/data/languageAssets/languageAssets.schema.json +0 -19
- package/dist/data/languageAssets/languageAssets.zod.d.ts +0 -3
- package/dist/data/languageAssets/languageAssets.zod.js +0 -7
- package/dist/functions/ffmpeg.d.ts +0 -104
- package/dist/functions/ffmpeg.js +0 -307
- package/dist/functions/upload.d.ts +0 -34
- package/dist/functions/upload.js +0 -49
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"description": "This file is auto-generated by scripts/prep. Do not edit manually.",
|
|
3
|
-
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
4
|
-
"type": "object",
|
|
5
|
-
"properties": {
|
|
6
|
-
"$schema": { "type": "string" },
|
|
7
|
-
"data": {
|
|
8
|
-
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
9
|
-
"type": "object",
|
|
10
|
-
"propertyNames": { "type": "string", "pattern": "^[a-z]{3}$" },
|
|
11
|
-
"additionalProperties": {
|
|
12
|
-
"type": "array",
|
|
13
|
-
"items": { "type": "string" },
|
|
14
|
-
"description": "Language code (3-letter ISO code) containing assets"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
},
|
|
18
|
-
"required": ["$schema", "data"]
|
|
19
|
-
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LanguageAssets = void 0;
|
|
4
|
-
const zod_1 = require("zod");
|
|
5
|
-
exports.LanguageAssets = zod_1.z.record(zod_1.z.string().regex(new RegExp('^[a-z]{3}$')), zod_1.z
|
|
6
|
-
.array(zod_1.z.string())
|
|
7
|
-
.describe('Language code (3-letter ISO code) containing assets'));
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
/** Concatenate audio files using ffmpeg's concat demuxer. */
|
|
2
|
-
export declare function concatAudio(files: string[], output: string): Promise<void>;
|
|
3
|
-
/** Generate a silence MP3 of the given duration in seconds. */
|
|
4
|
-
export declare function generateSilence(duration: number, output: string): Promise<void>;
|
|
5
|
-
/** Extract an audio segment from a file by start/end times (seconds). */
|
|
6
|
-
export declare function extractAudioSegment({ endTime, input, output, startTime, }: {
|
|
7
|
-
input: string;
|
|
8
|
-
startTime: number;
|
|
9
|
-
endTime: number;
|
|
10
|
-
output: string;
|
|
11
|
-
}): Promise<void>;
|
|
12
|
-
/** Extract the audio track from a video file and re-encode to MP3. */
|
|
13
|
-
export declare function extractVideoAudio(input: string, output: string): Promise<void>;
|
|
14
|
-
/**
|
|
15
|
-
* Normalize audio to EBU R128 standard using ffmpeg-normalize. Returns the path
|
|
16
|
-
* to the normalized file. Skips if already exists.
|
|
17
|
-
*/
|
|
18
|
-
export declare function normalizeAudio(input: string): Promise<string>;
|
|
19
|
-
/**
|
|
20
|
-
* Source-of-truth for our standard H.264 / AAC video encode profile. Use these
|
|
21
|
-
* named values when calling APIs that take typed options (e.g. Remotion's
|
|
22
|
-
* `renderMedia`, which surfaces some flags as `codec` / `x264Preset` / `crf` /
|
|
23
|
-
* `pixelFormat` / `audioCodec` / `audioBitrate`). For direct ffmpeg shell-out,
|
|
24
|
-
* use the pre-flattened arg arrays below.
|
|
25
|
-
*
|
|
26
|
-
* The settings are tuned for concat-safe output:
|
|
27
|
-
*
|
|
28
|
-
* - `gop` of 30 puts a keyframe every 1s at 30fps, required for seekability and
|
|
29
|
-
* `-c:v copy` concat
|
|
30
|
-
* - `bFrames: 0` prevents non-monotonic DTS at concat seams
|
|
31
|
-
* - `videoTrackTimescale` of 90000 matches Remotion's default timebase
|
|
32
|
-
*/
|
|
33
|
-
export declare const VIDEO_ENCODE_SETTINGS: {
|
|
34
|
-
readonly videoCodec: "libx264";
|
|
35
|
-
/**
|
|
36
|
-
* Value for Remotion's `codec` option (which uses short names, not ffmpeg
|
|
37
|
-
* codec ids).
|
|
38
|
-
*/
|
|
39
|
-
readonly remotionCodec: "h264";
|
|
40
|
-
readonly crf: 23;
|
|
41
|
-
readonly x264Preset: "medium";
|
|
42
|
-
readonly gop: 30;
|
|
43
|
-
readonly bFrames: 0;
|
|
44
|
-
readonly pixelFormat: "yuv420p";
|
|
45
|
-
readonly videoTrackTimescale: 90000;
|
|
46
|
-
readonly audioCodec: "aac";
|
|
47
|
-
readonly audioSampleRate: 48000;
|
|
48
|
-
readonly audioBitrate: "128k";
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Ffmpeg args for H.264 video encoding (concat-safe). Includes `-movflags
|
|
52
|
-
* +faststart` so the muxed MP4 supports progressive HTTP playback.
|
|
53
|
-
*/
|
|
54
|
-
export declare const VIDEO_ENCODE_ARGS: string[];
|
|
55
|
-
/** Standard AAC audio encode args to pair with {@link VIDEO_ENCODE_ARGS}. */
|
|
56
|
-
export declare const VIDEO_AUDIO_ENCODE_ARGS: string[];
|
|
57
|
-
/**
|
|
58
|
-
* Args for use inside Remotion's `ffmpegOverride` — just the flags that
|
|
59
|
-
* `renderMedia`'s typed options don't surface natively. Currently only `-bf 0`
|
|
60
|
-
* (no B-frames), which prevents non-monotonic DTS at concat seams between
|
|
61
|
-
* Remotion-rendered clips and the B-frame-free training videos compressed by
|
|
62
|
-
* {@link compressVideo}. Pair with the typed options fed from
|
|
63
|
-
* {@link VIDEO_ENCODE_SETTINGS} for a render that matches our standard profile.
|
|
64
|
-
*
|
|
65
|
-
* Example:
|
|
66
|
-
*
|
|
67
|
-
* renderMedia({
|
|
68
|
-
* codec: VIDEO_ENCODE_SETTINGS.remotionCodec,
|
|
69
|
-
* x264Preset: VIDEO_ENCODE_SETTINGS.x264Preset,
|
|
70
|
-
* crf: VIDEO_ENCODE_SETTINGS.crf,
|
|
71
|
-
* pixelFormat: VIDEO_ENCODE_SETTINGS.pixelFormat,
|
|
72
|
-
* audioCodec: VIDEO_ENCODE_SETTINGS.audioCodec,
|
|
73
|
-
* audioBitrate: VIDEO_ENCODE_SETTINGS.audioBitrate,
|
|
74
|
-
* ffmpegOverride: ({ args }) => {
|
|
75
|
-
* const out = args[args.length - 1]
|
|
76
|
-
* const yflag = args[args.length - 2]
|
|
77
|
-
* return [...args.slice(0, -2), ...VIDEO_REMOTION_OVERRIDE_ARGS, yflag, out]
|
|
78
|
-
* },
|
|
79
|
-
* ...
|
|
80
|
-
* })
|
|
81
|
-
*/
|
|
82
|
-
export declare const VIDEO_REMOTION_OVERRIDE_ARGS: string[];
|
|
83
|
-
/**
|
|
84
|
-
* Compress a video using the standard H.264 settings. If `audio` is provided,
|
|
85
|
-
* its track replaces the source video's audio.
|
|
86
|
-
*/
|
|
87
|
-
export declare function compressVideo({ input, output, audio, }: {
|
|
88
|
-
input: string;
|
|
89
|
-
output: string;
|
|
90
|
-
audio?: string;
|
|
91
|
-
}): Promise<void>;
|
|
92
|
-
/**
|
|
93
|
-
* Mix narration audio with background music at a fixed volume ratio. Music is
|
|
94
|
-
* trimmed to the shorter of the two inputs; if `targetDuration` is provided,
|
|
95
|
-
* the output is padded with silence to match exactly.
|
|
96
|
-
*/
|
|
97
|
-
export declare function mixAudioWithMusic({ music, narration, output, targetDuration, }: {
|
|
98
|
-
narration: string;
|
|
99
|
-
music: string;
|
|
100
|
-
output: string;
|
|
101
|
-
targetDuration: number;
|
|
102
|
-
}): Promise<void>;
|
|
103
|
-
/** Get the duration of an audio file in seconds using ffprobe. */
|
|
104
|
-
export declare function getDurationFromFile(file: string): Promise<number>;
|
package/dist/functions/ffmpeg.js
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.VIDEO_REMOTION_OVERRIDE_ARGS = exports.VIDEO_AUDIO_ENCODE_ARGS = exports.VIDEO_ENCODE_ARGS = exports.VIDEO_ENCODE_SETTINGS = void 0;
|
|
4
|
-
exports.concatAudio = concatAudio;
|
|
5
|
-
exports.generateSilence = generateSilence;
|
|
6
|
-
exports.extractAudioSegment = extractAudioSegment;
|
|
7
|
-
exports.extractVideoAudio = extractVideoAudio;
|
|
8
|
-
exports.normalizeAudio = normalizeAudio;
|
|
9
|
-
exports.compressVideo = compressVideo;
|
|
10
|
-
exports.mixAudioWithMusic = mixAudioWithMusic;
|
|
11
|
-
exports.getDurationFromFile = getDurationFromFile;
|
|
12
|
-
const child_process_1 = require("child_process");
|
|
13
|
-
const fs_1 = require("fs");
|
|
14
|
-
const promises_1 = require("fs/promises");
|
|
15
|
-
const os_1 = require("os");
|
|
16
|
-
const path_1 = require("path");
|
|
17
|
-
function exec(cmd, args) {
|
|
18
|
-
return new Promise((resolve, reject) => {
|
|
19
|
-
(0, child_process_1.execFile)(cmd, args, { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
20
|
-
if (err)
|
|
21
|
-
reject(new Error(`${cmd} failed: ${stderr}`));
|
|
22
|
-
else
|
|
23
|
-
resolve({ stdout, stderr });
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
/** MP3 encoder used across all ffmpeg commands. */
|
|
28
|
-
const CODEC = ['-c:a', 'libmp3lame'];
|
|
29
|
-
/**
|
|
30
|
-
* Target audio bitrate — 64 kbps is a good balance of quality and file size for
|
|
31
|
-
* speech.
|
|
32
|
-
*/
|
|
33
|
-
const BITRATE = ['-b:a', '64k'];
|
|
34
|
-
/** Sample rate — 22050 Hz is sufficient for speech content. */
|
|
35
|
-
const SAMPLE_RATE = ['-ar', '22050'];
|
|
36
|
-
/** Audio channels: stereo (2 channels). */
|
|
37
|
-
const CHANNELS = ['-ac', '2'];
|
|
38
|
-
/** Concatenate audio files using ffmpeg's concat demuxer. */
|
|
39
|
-
async function concatAudio(files, output) {
|
|
40
|
-
const dir = await (0, promises_1.mkdtemp)((0, path_1.join)((0, os_1.tmpdir)(), 'ffconcat-'));
|
|
41
|
-
const listFile = (0, path_1.join)(dir, 'list.txt');
|
|
42
|
-
await (0, promises_1.writeFile)(listFile, files.map((f) => `file '${f}'`).join('\n'));
|
|
43
|
-
await exec('ffmpeg', [
|
|
44
|
-
// Use the concat demuxer to join multiple input files
|
|
45
|
-
'-f',
|
|
46
|
-
'concat',
|
|
47
|
-
// Allow absolute paths and paths outside the working directory in the list file
|
|
48
|
-
'-safe',
|
|
49
|
-
'0',
|
|
50
|
-
// Input: the generated list file containing paths to all audio segments
|
|
51
|
-
'-i',
|
|
52
|
-
listFile,
|
|
53
|
-
// FFmpeg 8.0 introduced stricter AVFrame buffer alignment requirements for
|
|
54
|
-
// libmp3lame. The concat demuxer produces frames that don't meet this new
|
|
55
|
-
// padding requirement, causing "inadequate AVFrame plane padding" errors.
|
|
56
|
-
// aresample forces frames to be reallocated with proper alignment as a workaround.
|
|
57
|
-
'-af',
|
|
58
|
-
'aresample=resampler=swr',
|
|
59
|
-
...CODEC,
|
|
60
|
-
...BITRATE,
|
|
61
|
-
...SAMPLE_RATE,
|
|
62
|
-
...CHANNELS,
|
|
63
|
-
'-y',
|
|
64
|
-
output,
|
|
65
|
-
]);
|
|
66
|
-
}
|
|
67
|
-
/** Generate a silence MP3 of the given duration in seconds. */
|
|
68
|
-
async function generateSilence(duration, output) {
|
|
69
|
-
await exec('ffmpeg', [
|
|
70
|
-
// Use the Libavfilter virtual device as the input format
|
|
71
|
-
'-f',
|
|
72
|
-
'lavfi',
|
|
73
|
-
// Input: null audio source generating silence at 22050 Hz in stereo
|
|
74
|
-
'-i',
|
|
75
|
-
'anullsrc=r=22050:cl=stereo',
|
|
76
|
-
// Duration: stop after this many seconds
|
|
77
|
-
'-t',
|
|
78
|
-
String(duration),
|
|
79
|
-
...CODEC,
|
|
80
|
-
...BITRATE,
|
|
81
|
-
...CHANNELS,
|
|
82
|
-
'-y',
|
|
83
|
-
output,
|
|
84
|
-
]);
|
|
85
|
-
}
|
|
86
|
-
/** Extract an audio segment from a file by start/end times (seconds). */
|
|
87
|
-
async function extractAudioSegment({ endTime, input, output, startTime, }) {
|
|
88
|
-
await exec('ffmpeg', [
|
|
89
|
-
// Seek to this start time (seconds) before reading input — placed before -i for fast seeking
|
|
90
|
-
'-ss',
|
|
91
|
-
String(startTime),
|
|
92
|
-
// Duration: read this many seconds of audio (endTime - startTime)
|
|
93
|
-
'-t',
|
|
94
|
-
String(endTime - startTime),
|
|
95
|
-
// Input audio file to extract from
|
|
96
|
-
'-i',
|
|
97
|
-
input,
|
|
98
|
-
// FFmpeg 8.0 workaround: force frame reallocation to satisfy libmp3lame's
|
|
99
|
-
// stricter AVFrame buffer alignment requirements (see concatAudio).
|
|
100
|
-
'-af',
|
|
101
|
-
'aresample=resampler=swr',
|
|
102
|
-
...CODEC,
|
|
103
|
-
...BITRATE,
|
|
104
|
-
...SAMPLE_RATE,
|
|
105
|
-
...CHANNELS,
|
|
106
|
-
'-y',
|
|
107
|
-
output,
|
|
108
|
-
]);
|
|
109
|
-
}
|
|
110
|
-
/** Extract the audio track from a video file and re-encode to MP3. */
|
|
111
|
-
async function extractVideoAudio(input, output) {
|
|
112
|
-
await exec('ffmpeg', [
|
|
113
|
-
'-i',
|
|
114
|
-
input,
|
|
115
|
-
// Drop the video stream — we only want the audio track.
|
|
116
|
-
'-vn',
|
|
117
|
-
'-af',
|
|
118
|
-
'aresample=resampler=swr',
|
|
119
|
-
...CODEC,
|
|
120
|
-
...BITRATE,
|
|
121
|
-
...SAMPLE_RATE,
|
|
122
|
-
...CHANNELS,
|
|
123
|
-
'-y',
|
|
124
|
-
output,
|
|
125
|
-
]);
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Normalize audio to EBU R128 standard using ffmpeg-normalize. Returns the path
|
|
129
|
-
* to the normalized file. Skips if already exists.
|
|
130
|
-
*/
|
|
131
|
-
async function normalizeAudio(input) {
|
|
132
|
-
const output = input.replace(/\.mp3$/, '.normalized.mp3');
|
|
133
|
-
if ((0, fs_1.existsSync)(output))
|
|
134
|
-
return output;
|
|
135
|
-
await exec('ffmpeg-normalize', [
|
|
136
|
-
// Input file to normalize
|
|
137
|
-
input,
|
|
138
|
-
// Output file path
|
|
139
|
-
'-o',
|
|
140
|
-
output,
|
|
141
|
-
// Normalization type: EBU R128 loudness standard
|
|
142
|
-
'-nt',
|
|
143
|
-
'ebu',
|
|
144
|
-
// Target integrated loudness level: -15 LUFS
|
|
145
|
-
'-t',
|
|
146
|
-
'-15',
|
|
147
|
-
// True peak ceiling: -1.0 dBTP (prevents clipping after encoding)
|
|
148
|
-
'-tp',
|
|
149
|
-
'-1.0',
|
|
150
|
-
// Loudness range target: 7.0 LU (controls dynamic range)
|
|
151
|
-
'-lrt',
|
|
152
|
-
'7.0',
|
|
153
|
-
...CODEC,
|
|
154
|
-
...SAMPLE_RATE,
|
|
155
|
-
]);
|
|
156
|
-
return output;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Source-of-truth for our standard H.264 / AAC video encode profile. Use these
|
|
160
|
-
* named values when calling APIs that take typed options (e.g. Remotion's
|
|
161
|
-
* `renderMedia`, which surfaces some flags as `codec` / `x264Preset` / `crf` /
|
|
162
|
-
* `pixelFormat` / `audioCodec` / `audioBitrate`). For direct ffmpeg shell-out,
|
|
163
|
-
* use the pre-flattened arg arrays below.
|
|
164
|
-
*
|
|
165
|
-
* The settings are tuned for concat-safe output:
|
|
166
|
-
*
|
|
167
|
-
* - `gop` of 30 puts a keyframe every 1s at 30fps, required for seekability and
|
|
168
|
-
* `-c:v copy` concat
|
|
169
|
-
* - `bFrames: 0` prevents non-monotonic DTS at concat seams
|
|
170
|
-
* - `videoTrackTimescale` of 90000 matches Remotion's default timebase
|
|
171
|
-
*/
|
|
172
|
-
exports.VIDEO_ENCODE_SETTINGS = {
|
|
173
|
-
videoCodec: 'libx264',
|
|
174
|
-
/**
|
|
175
|
-
* Value for Remotion's `codec` option (which uses short names, not ffmpeg
|
|
176
|
-
* codec ids).
|
|
177
|
-
*/
|
|
178
|
-
remotionCodec: 'h264',
|
|
179
|
-
crf: 23,
|
|
180
|
-
x264Preset: 'medium',
|
|
181
|
-
gop: 30,
|
|
182
|
-
bFrames: 0,
|
|
183
|
-
pixelFormat: 'yuv420p',
|
|
184
|
-
videoTrackTimescale: 90000,
|
|
185
|
-
audioCodec: 'aac',
|
|
186
|
-
audioSampleRate: 48000,
|
|
187
|
-
audioBitrate: '128k',
|
|
188
|
-
};
|
|
189
|
-
/**
|
|
190
|
-
* Ffmpeg args for H.264 video encoding (concat-safe). Includes `-movflags
|
|
191
|
-
* +faststart` so the muxed MP4 supports progressive HTTP playback.
|
|
192
|
-
*/
|
|
193
|
-
exports.VIDEO_ENCODE_ARGS = [
|
|
194
|
-
'-c:v',
|
|
195
|
-
exports.VIDEO_ENCODE_SETTINGS.videoCodec,
|
|
196
|
-
'-crf',
|
|
197
|
-
String(exports.VIDEO_ENCODE_SETTINGS.crf),
|
|
198
|
-
'-preset',
|
|
199
|
-
exports.VIDEO_ENCODE_SETTINGS.x264Preset,
|
|
200
|
-
'-g',
|
|
201
|
-
String(exports.VIDEO_ENCODE_SETTINGS.gop),
|
|
202
|
-
'-bf',
|
|
203
|
-
String(exports.VIDEO_ENCODE_SETTINGS.bFrames),
|
|
204
|
-
'-pix_fmt',
|
|
205
|
-
exports.VIDEO_ENCODE_SETTINGS.pixelFormat,
|
|
206
|
-
'-video_track_timescale',
|
|
207
|
-
String(exports.VIDEO_ENCODE_SETTINGS.videoTrackTimescale),
|
|
208
|
-
'-movflags',
|
|
209
|
-
'+faststart',
|
|
210
|
-
];
|
|
211
|
-
/** Standard AAC audio encode args to pair with {@link VIDEO_ENCODE_ARGS}. */
|
|
212
|
-
exports.VIDEO_AUDIO_ENCODE_ARGS = [
|
|
213
|
-
'-c:a',
|
|
214
|
-
exports.VIDEO_ENCODE_SETTINGS.audioCodec,
|
|
215
|
-
'-ar',
|
|
216
|
-
String(exports.VIDEO_ENCODE_SETTINGS.audioSampleRate),
|
|
217
|
-
'-b:a',
|
|
218
|
-
exports.VIDEO_ENCODE_SETTINGS.audioBitrate,
|
|
219
|
-
];
|
|
220
|
-
/**
|
|
221
|
-
* Args for use inside Remotion's `ffmpegOverride` — just the flags that
|
|
222
|
-
* `renderMedia`'s typed options don't surface natively. Currently only `-bf 0`
|
|
223
|
-
* (no B-frames), which prevents non-monotonic DTS at concat seams between
|
|
224
|
-
* Remotion-rendered clips and the B-frame-free training videos compressed by
|
|
225
|
-
* {@link compressVideo}. Pair with the typed options fed from
|
|
226
|
-
* {@link VIDEO_ENCODE_SETTINGS} for a render that matches our standard profile.
|
|
227
|
-
*
|
|
228
|
-
* Example:
|
|
229
|
-
*
|
|
230
|
-
* renderMedia({
|
|
231
|
-
* codec: VIDEO_ENCODE_SETTINGS.remotionCodec,
|
|
232
|
-
* x264Preset: VIDEO_ENCODE_SETTINGS.x264Preset,
|
|
233
|
-
* crf: VIDEO_ENCODE_SETTINGS.crf,
|
|
234
|
-
* pixelFormat: VIDEO_ENCODE_SETTINGS.pixelFormat,
|
|
235
|
-
* audioCodec: VIDEO_ENCODE_SETTINGS.audioCodec,
|
|
236
|
-
* audioBitrate: VIDEO_ENCODE_SETTINGS.audioBitrate,
|
|
237
|
-
* ffmpegOverride: ({ args }) => {
|
|
238
|
-
* const out = args[args.length - 1]
|
|
239
|
-
* const yflag = args[args.length - 2]
|
|
240
|
-
* return [...args.slice(0, -2), ...VIDEO_REMOTION_OVERRIDE_ARGS, yflag, out]
|
|
241
|
-
* },
|
|
242
|
-
* ...
|
|
243
|
-
* })
|
|
244
|
-
*/
|
|
245
|
-
exports.VIDEO_REMOTION_OVERRIDE_ARGS = [
|
|
246
|
-
'-bf',
|
|
247
|
-
String(exports.VIDEO_ENCODE_SETTINGS.bFrames),
|
|
248
|
-
];
|
|
249
|
-
/**
|
|
250
|
-
* Compress a video using the standard H.264 settings. If `audio` is provided,
|
|
251
|
-
* its track replaces the source video's audio.
|
|
252
|
-
*/
|
|
253
|
-
async function compressVideo({ input, output, audio, }) {
|
|
254
|
-
const args = ['-i', input];
|
|
255
|
-
if (audio)
|
|
256
|
-
args.push('-i', audio, '-map', '0:v:0', '-map', '1:a:0');
|
|
257
|
-
args.push(...exports.VIDEO_ENCODE_ARGS, ...exports.VIDEO_AUDIO_ENCODE_ARGS, '-y', output);
|
|
258
|
-
await exec('ffmpeg', args);
|
|
259
|
-
}
|
|
260
|
-
/**
|
|
261
|
-
* Mix narration audio with background music at a fixed volume ratio. Music is
|
|
262
|
-
* trimmed to the shorter of the two inputs; if `targetDuration` is provided,
|
|
263
|
-
* the output is padded with silence to match exactly.
|
|
264
|
-
*/
|
|
265
|
-
async function mixAudioWithMusic({ music, narration, output, targetDuration, }) {
|
|
266
|
-
const narrationDuration = await getDurationFromFile(narration);
|
|
267
|
-
const musicDuration = await getDurationFromFile(music);
|
|
268
|
-
const minDuration = Math.min(narrationDuration, musicDuration);
|
|
269
|
-
const filterComplex = [
|
|
270
|
-
`[0:a]volume=1.0[a1]`,
|
|
271
|
-
`[1:a]atrim=0:${minDuration},volume=0.1[a2]`,
|
|
272
|
-
// duration=first keeps narration length; normalize=0 preserves its volume.
|
|
273
|
-
// apad pads the mix with silence to the exact target duration.
|
|
274
|
-
`[a1][a2]amix=inputs=2:duration=first:dropout_transition=0:normalize=0,apad=whole_dur=${targetDuration}`,
|
|
275
|
-
].join(';');
|
|
276
|
-
await exec('ffmpeg', [
|
|
277
|
-
'-i',
|
|
278
|
-
narration,
|
|
279
|
-
'-i',
|
|
280
|
-
music,
|
|
281
|
-
'-filter_complex',
|
|
282
|
-
filterComplex,
|
|
283
|
-
...CODEC,
|
|
284
|
-
...BITRATE,
|
|
285
|
-
...SAMPLE_RATE,
|
|
286
|
-
...CHANNELS,
|
|
287
|
-
'-y',
|
|
288
|
-
output,
|
|
289
|
-
]);
|
|
290
|
-
}
|
|
291
|
-
/** Get the duration of an audio file in seconds using ffprobe. */
|
|
292
|
-
async function getDurationFromFile(file) {
|
|
293
|
-
const { stdout } = await exec('ffprobe', [
|
|
294
|
-
// Verbosity: only show errors (suppress informational output)
|
|
295
|
-
'-v',
|
|
296
|
-
'error',
|
|
297
|
-
// Only extract the duration field from the format section
|
|
298
|
-
'-show_entries',
|
|
299
|
-
'format=duration',
|
|
300
|
-
// Output format: plain value with no section wrappers or key name
|
|
301
|
-
'-of',
|
|
302
|
-
'default=noprint_wrappers=1:nokey=1',
|
|
303
|
-
// Input file to probe
|
|
304
|
-
file,
|
|
305
|
-
]);
|
|
306
|
-
return parseFloat(stdout.trim());
|
|
307
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal structural shape of `@google-cloud/storage`'s `Bucket`. Defined
|
|
3
|
-
* structurally so this module doesn't pull `@google-cloud/storage` into the
|
|
4
|
-
* shared package's dependency graph — callers pass the real bucket.
|
|
5
|
-
*/
|
|
6
|
-
export interface UploadBucket {
|
|
7
|
-
file(path: string): {
|
|
8
|
-
exists(): Promise<[boolean]>;
|
|
9
|
-
setMetadata(meta: {
|
|
10
|
-
metadata: Record<string, string>;
|
|
11
|
-
}): Promise<unknown>;
|
|
12
|
-
};
|
|
13
|
-
upload(localPath: string, options: {
|
|
14
|
-
destination: string;
|
|
15
|
-
contentType: string;
|
|
16
|
-
public?: boolean;
|
|
17
|
-
}): Promise<unknown>;
|
|
18
|
-
}
|
|
19
|
-
export type UploadResult = 'uploaded' | 'skipped';
|
|
20
|
-
/**
|
|
21
|
-
* Upload a local file to a Cloud Storage bucket. The content type is inferred
|
|
22
|
-
* from the file extension via {@link CONTENT_TYPE_BY_EXTENSION}. Returns
|
|
23
|
-
* `'skipped'` if the remote file already exists and `overwrite` is falsy,
|
|
24
|
-
* `'uploaded'` otherwise. Throws if the underlying bucket operation fails.
|
|
25
|
-
*
|
|
26
|
-
* Sets a `duration` metadata field (in seconds) from ffprobe when probing
|
|
27
|
-
* succeeds; metadata failures are swallowed since they're non-fatal.
|
|
28
|
-
*/
|
|
29
|
-
export declare function uploadFile({ bucket, localPath, overwrite, remotePath, }: {
|
|
30
|
-
bucket: UploadBucket;
|
|
31
|
-
localPath: string;
|
|
32
|
-
remotePath: string;
|
|
33
|
-
overwrite?: boolean;
|
|
34
|
-
}): Promise<UploadResult>;
|
package/dist/functions/upload.js
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.uploadFile = uploadFile;
|
|
4
|
-
const path_1 = require("path");
|
|
5
|
-
const ffmpeg_1 = require("./ffmpeg");
|
|
6
|
-
const CONTENT_TYPE_BY_EXTENSION = {
|
|
7
|
-
'.mp3': 'audio/mpeg',
|
|
8
|
-
'.mp4': 'video/mp4',
|
|
9
|
-
};
|
|
10
|
-
function contentTypeFor(path) {
|
|
11
|
-
const ext = (0, path_1.extname)(path).toLowerCase();
|
|
12
|
-
const contentType = CONTENT_TYPE_BY_EXTENSION[ext];
|
|
13
|
-
if (!contentType)
|
|
14
|
-
throw new Error(`uploadFile: no content type mapped for extension '${ext}' (path: ${path}). ` +
|
|
15
|
-
`Add it to CONTENT_TYPE_BY_EXTENSION in shared/functions/upload.ts.`);
|
|
16
|
-
return contentType;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Upload a local file to a Cloud Storage bucket. The content type is inferred
|
|
20
|
-
* from the file extension via {@link CONTENT_TYPE_BY_EXTENSION}. Returns
|
|
21
|
-
* `'skipped'` if the remote file already exists and `overwrite` is falsy,
|
|
22
|
-
* `'uploaded'` otherwise. Throws if the underlying bucket operation fails.
|
|
23
|
-
*
|
|
24
|
-
* Sets a `duration` metadata field (in seconds) from ffprobe when probing
|
|
25
|
-
* succeeds; metadata failures are swallowed since they're non-fatal.
|
|
26
|
-
*/
|
|
27
|
-
async function uploadFile({ bucket, localPath, overwrite, remotePath, }) {
|
|
28
|
-
const contentType = contentTypeFor(localPath);
|
|
29
|
-
const [exists] = await bucket.file(remotePath).exists();
|
|
30
|
-
if (exists && !overwrite)
|
|
31
|
-
return 'skipped';
|
|
32
|
-
await bucket.upload(localPath, {
|
|
33
|
-
destination: remotePath,
|
|
34
|
-
contentType,
|
|
35
|
-
public: true,
|
|
36
|
-
});
|
|
37
|
-
try {
|
|
38
|
-
const duration = await (0, ffmpeg_1.getDurationFromFile)(localPath);
|
|
39
|
-
if (!isNaN(duration) && duration > 0) {
|
|
40
|
-
await bucket.file(remotePath).setMetadata({
|
|
41
|
-
metadata: { duration: duration.toString() },
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// Non-fatal — proceed even if setting duration metadata fails.
|
|
47
|
-
}
|
|
48
|
-
return 'uploaded';
|
|
49
|
-
}
|