waha-shared 1.0.343 → 1.0.345

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 (32) hide show
  1. package/dist/data/bibleAudios/bibleAudios.json +22 -0
  2. package/dist/data/bibleStatuses/bibleStatuses.json +594 -159
  3. package/dist/data/bibleTexts/bibleTexts.json +21 -0
  4. package/dist/data/languageAssets/index.d.ts +1 -0
  5. package/dist/data/languageAssets/index.js +7 -0
  6. package/dist/data/languageAssets/languageAssets.json +44404 -0
  7. package/dist/data/languageAssets/languageAssets.schema.json +19 -0
  8. package/dist/data/languageAssets/languageAssets.zod.d.ts +3 -0
  9. package/dist/data/languageAssets/languageAssets.zod.js +7 -0
  10. package/dist/data/languages/index.d.ts +5 -1
  11. package/dist/data/languages/languages.json +148 -54
  12. package/dist/data/languages/languages.schema.json +24 -3
  13. package/dist/data/languages/languages.zod.d.ts +10 -1
  14. package/dist/data/languages/languages.zod.js +18 -5
  15. package/dist/data/mediaDurations/mediaDurations.json +31030 -684
  16. package/dist/data/releaseNotes/releaseNotes.json +16 -2
  17. package/dist/data/translationsApp/index.d.ts +1 -0
  18. package/dist/data/translationsApp/translationsApp.json +41 -0
  19. package/dist/data/translationsApp/translationsApp.zod.d.ts +2 -0
  20. package/dist/data/translationsApp/translationsApp.zod.js +1 -0
  21. package/dist/data/youtubeVideos/youtubeVideos.json +55 -115
  22. package/dist/functions/ffmpeg.d.ts +104 -0
  23. package/dist/functions/ffmpeg.js +307 -0
  24. package/dist/functions/scripturePassages.js +9 -11
  25. package/dist/functions/sets.d.ts +15 -4
  26. package/dist/functions/sets.js +172 -228
  27. package/dist/functions/upload.d.ts +34 -0
  28. package/dist/functions/upload.js +49 -0
  29. package/dist/functions/utils.d.ts +6 -1
  30. package/dist/functions/utils.js +18 -4
  31. package/dist/types/sets.d.ts +22 -19
  32. package/package.json +3 -2
@@ -0,0 +1,307 @@
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
+ }
@@ -12,11 +12,11 @@ exports.enrichSections = enrichSections;
12
12
  exports.normalizeVerseTimings = normalizeVerseTimings;
13
13
  const bibleStatuses_1 = require("../data/bibleStatuses");
14
14
  const firebase_1 = require("../data/firebase");
15
- const mediaDurations_1 = require("../data/mediaDurations");
16
15
  const languages_1 = require("../functions/languages");
17
16
  const sets_1 = require("../types/sets");
18
17
  const bibleBooks_1 = require("./bibleBooks");
19
18
  const bibles_1 = require("./bibles");
19
+ const utils_1 = require("./utils");
20
20
  var bibleBooks_2 = require("./bibleBooks");
21
21
  Object.defineProperty(exports, "parseVerseRange", { enumerable: true, get: function () { return bibleBooks_2.parseVerseRange; } });
22
22
  function getChapterUrl(params) {
@@ -211,11 +211,11 @@ function verseToSuperscript(num) {
211
211
  * section has no FTB.
212
212
  */
213
213
  function getFtbDuration(section, lessonInfo) {
214
- if (!section.ftbFileName)
214
+ if (!section.ftbPath)
215
215
  return 0;
216
- const ftbDuration = mediaDurations_1.mediaDurations[lessonInfo.contentLanguages.ftbs]?.[section.ftbFileName];
216
+ const ftbDuration = (0, utils_1.getCachedDuration)(section.ftbPath);
217
217
  if (ftbDuration == null) {
218
- console.warn(`FTB duration not found for ${section.ftbFileName}`);
218
+ console.warn(`FTB duration not found for ${(0, utils_1.basename)(section.ftbPath)}`);
219
219
  }
220
220
  return (ftbDuration ?? 0) + lessonInfo.lessonPauses.afterFtb;
221
221
  }
@@ -243,7 +243,7 @@ function enrichSections(lessonInfo, scripture) {
243
243
  const ignoreTimings = isStory && priorStoryLengthUnknown ? true : undefined;
244
244
  const startTime = currentTime;
245
245
  if (!isStory) {
246
- length = mediaDurations_1.mediaDurations[section.languageId]?.[section.fileName];
246
+ length = (0, utils_1.getCachedDuration)(section.path);
247
247
  }
248
248
  else {
249
249
  currentTime += getFtbDuration(section, lessonInfo);
@@ -263,7 +263,7 @@ function enrichSections(lessonInfo, scripture) {
263
263
  priorStoryLengthUnknown = true;
264
264
  }
265
265
  if (length == null) {
266
- console.warn(`Missing duration for section ${section.id} (${section.chapter}, ${section.fileName})`);
266
+ console.warn(`Missing duration for section ${section.id} (${section.chapter}, ${(0, utils_1.basename)(section.path)})`);
267
267
  }
268
268
  currentTime += length ?? 0;
269
269
  return { ...section, length: length ?? 0, startTime, ignoreTimings };
@@ -271,11 +271,9 @@ function enrichSections(lessonInfo, scripture) {
271
271
  // Backward pass: compute application section start times from the end of the
272
272
  // lesson file, so they are always correct even if story durations are
273
273
  // slightly off.
274
- const fullFileName = lessonInfo.type === 'dbs'
275
- ? lessonInfo.full.localFileName
276
- : lessonInfo.video.localFileName;
277
- const totalDuration = mediaDurations_1.mediaDurations[lessonInfo.languageId]?.[fullFileName] ?? 0;
278
- if (totalDuration > 0) {
274
+ const path = lessonInfo.type === 'dbs' ? lessonInfo.fullPath : lessonInfo.videoPath;
275
+ const totalDuration = (0, utils_1.getCachedDuration)(path);
276
+ if (totalDuration) {
279
277
  let timeFromEnd = totalDuration;
280
278
  const applicationSections = enriched.filter((s) => s.chapter === sets_1.Chapter.APPLICATION);
281
279
  for (const section of [...applicationSections].reverse()) {
@@ -1,12 +1,24 @@
1
1
  import { TranslationsApp } from '../data/translationsApp/translationsApp.zod';
2
- import type { LanguageInfo, MeetTranslations } from '../types/languages';
3
- import { DbsInfo, EnrichedSection, Lesson, LessonInfo, SetInfo, VideoInfo } from '../types/sets';
2
+ import type { AppTranslations, LanguageInfo, MeetTranslations } from '../types/languages';
3
+ import { DbsInfo, EnrichedSection, Lesson, LessonInfo, SetInfo, VideoInfo, type Section } from '../types/sets';
4
4
  export declare function getSetInfo({ setId, languageId, setIds, t, }: {
5
5
  setId: string;
6
6
  languageId: string;
7
7
  t: MeetTranslations & TranslationsApp[string];
8
8
  setIds: string[];
9
9
  }): SetInfo | undefined;
10
+ /** Assembles all of the audio sections for a lesson. */
11
+ export declare function assembleLessonSections({ languageInfo, setInfo, lesson, t, useSpokenQuestions, }: {
12
+ languageInfo: LanguageInfo;
13
+ setInfo: SetInfo;
14
+ lesson: Lesson;
15
+ t: MeetTranslations & AppTranslations;
16
+ useSpokenQuestions: boolean | undefined;
17
+ }): {
18
+ sections: Section[];
19
+ fellowshipDuration: number;
20
+ applicationDuration: number;
21
+ };
10
22
  export declare function getLessonInfo({ lessonId, languageInfo, setInfo, t, useSpokenQuestions, }: {
11
23
  lessonId: Lesson['lessonId'] | undefined;
12
24
  languageInfo: LanguageInfo;
@@ -14,9 +26,8 @@ export declare function getLessonInfo({ lessonId, languageInfo, setInfo, t, useS
14
26
  t: MeetTranslations & TranslationsApp[string];
15
27
  useSpokenQuestions?: boolean;
16
28
  }): LessonInfo | undefined;
17
- export declare function convertSToString(time: number): string;
18
29
  /** Determines whether a lesson should be visible in Waha. */
19
- export declare function shouldShowLesson(lessonInfo: LessonInfo | undefined, languageInfo: LanguageInfo): boolean;
30
+ export declare function shouldShowLesson(lessonInfo: LessonInfo | undefined): boolean;
20
31
  /**
21
32
  * Validates that the computed lesson duration (from section lengths) matches
22
33
  * the actual audio file duration. Sections must be enriched (all have `length`)