waha-shared 1.0.281 → 1.0.283

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 (38) hide show
  1. package/dist/data/bibleAudios/bibleAudios.json +2 -10
  2. package/dist/data/bibleStatuses/bibleStatuses.json +116 -227
  3. package/dist/data/bibleTexts/bibleTexts.json +4 -15
  4. package/dist/data/dblAudioLicenses/dblAudioLicenses.json +450 -392
  5. package/dist/data/dblTextLicenses/dblTextLicenses.json +1368 -1160
  6. package/dist/data/languageAssets/languageAssets.json +5428 -4086
  7. package/dist/data/languages/index.d.ts +2 -0
  8. package/dist/data/languages/languages.json +28 -36
  9. package/dist/data/languages/languages.schema.json +8 -0
  10. package/dist/data/languages/languages.zod.d.ts +2 -0
  11. package/dist/data/languages/languages.zod.js +8 -0
  12. package/dist/data/lessonPauses/index.d.ts +7 -0
  13. package/dist/data/lessonPauses/index.js +7 -0
  14. package/dist/data/lessonPauses/lessonPauses.json +10 -0
  15. package/dist/data/lessonPauses/lessonPauses.schema.json +43 -0
  16. package/dist/data/lessonPauses/lessonPauses.zod.d.ts +9 -0
  17. package/dist/data/lessonPauses/lessonPauses.zod.js +23 -0
  18. package/dist/data/mediaDurations/mediaDurations.json +3985 -66
  19. package/dist/data/releaseNotes/releaseNotes.json +49 -2
  20. package/dist/data/screenshots/screenshots.json +1 -1
  21. package/dist/data/specialIds/specialIds.schema.json +11 -36
  22. package/dist/data/specialIds/specialIds.zod.js +11 -17
  23. package/dist/data/translationsApp/index.d.ts +1 -0
  24. package/dist/data/translationsApp/translationsApp.json +61 -20
  25. package/dist/data/translationsApp/translationsApp.schema.json +1 -0
  26. package/dist/data/translationsApp/translationsApp.zod.d.ts +2 -0
  27. package/dist/data/translationsApp/translationsApp.zod.js +1 -0
  28. package/dist/data/translationsIntroduction/translationsIntroduction.json +42 -0
  29. package/dist/data/translationsQuestion/translationsQuestion.json +1 -1
  30. package/dist/data/translationsSet/translationsSet.json +448 -0
  31. package/dist/data/translationsSpokenQuestion/translationsSpokenQuestion.json +1 -1
  32. package/dist/functions/scripturePassages.d.ts +17 -4
  33. package/dist/functions/scripturePassages.js +136 -5
  34. package/dist/functions/sets.d.ts +44 -6
  35. package/dist/functions/sets.js +100 -44
  36. package/dist/types/analytics.d.ts +2 -2
  37. package/dist/types/sets.d.ts +5 -2
  38. package/package.json +1 -1
@@ -7,7 +7,12 @@ exports.getScripturePassage = getScripturePassage;
7
7
  exports.getLessonScripture = getLessonScripture;
8
8
  exports.parseBibleSnapshot = parseBibleSnapshot;
9
9
  exports.verseToSuperscript = verseToSuperscript;
10
+ exports.getLessonPauses = getLessonPauses;
11
+ exports.normalizeVerseTimings = normalizeVerseTimings;
10
12
  const bibleStatuses_1 = require("../data/bibleStatuses");
13
+ const lessonPauses_1 = require("../data/lessonPauses");
14
+ const mediaDurations_1 = require("../data/mediaDurations");
15
+ const specialIds_1 = require("../data/specialIds");
11
16
  const languages_1 = require("../functions/languages");
12
17
  const bibleChapters_1 = require("../types/bibleChapters");
13
18
  const sets_1 = require("./sets");
@@ -90,7 +95,7 @@ function getPassagesString(passageIds, language) {
90
95
  let bookName =
91
96
  // American sign language should use English book names.
92
97
  language.languageId === 'ase'
93
- ? bibleStatuses_1.bibleStatuses.BSB.bookNames[startBook]
98
+ ? bibleStatuses_1.bibleStatuses.NLT.bookNames[startBook]
94
99
  : language.bible.bookNames[startBook];
95
100
  if (!bookName) {
96
101
  bookName = language.bibleFallback?.bookNames[startBook];
@@ -132,10 +137,7 @@ function getPassagesString(passageIds, language) {
132
137
  }
133
138
  return (0, languages_1.numerals)(parts.join(', '), language.script.name);
134
139
  }
135
- /**
136
- * Gets a scripture passage by extracting verses from chapters This is the core
137
- * parsing logic, independent of the data source
138
- */
140
+ /** Gets a scripture passage by extracting verses from chapters documents. */
139
141
  async function getScripturePassage({ bibleId, bibleFallbackId, passageId, getChapter, languageInfo, }) {
140
142
  try {
141
143
  const { startBook, startChapter, startVerse, endBook, endChapter, endVerse, } = parseVerseRange(passageId);
@@ -282,3 +284,132 @@ function verseToSuperscript(num) {
282
284
  })
283
285
  .join('');
284
286
  }
287
+ /**
288
+ * Splits a passage's verses into per-chapter chunks. Single-chapter passages
289
+ * produce one chunk. Multi-chapter passages (rare, e.g. GEN.1.26-GEN.2.3)
290
+ * produce one chunk per chapter, with isPartOfPrevious=true on the 2nd+ chunks.
291
+ * This mirrors leviathan's passage_ids_by_chapter splitting logic.
292
+ */
293
+ function splitPassageIntoChunks(verses) {
294
+ if (verses.length === 0)
295
+ return [];
296
+ const chunks = [];
297
+ let currentChapterId = null;
298
+ for (const verse of verses) {
299
+ const parts = verse.verseId.split('.');
300
+ const chapterId = `${parts[0]}.${parts[1]}`;
301
+ if (chapterId !== currentChapterId) {
302
+ chunks.push({
303
+ verses: [verse],
304
+ bookId: parts[0],
305
+ isPartOfPrevious: currentChapterId !== null,
306
+ });
307
+ currentChapterId = chapterId;
308
+ }
309
+ else {
310
+ chunks[chunks.length - 1].verses.push(verse);
311
+ }
312
+ }
313
+ return chunks;
314
+ }
315
+ /**
316
+ * Some lessons were built using a system that used slightly different pause
317
+ * values.
318
+ */
319
+ const alternativeLessonPauses = {
320
+ beforeStory: 1,
321
+ afterStory: 1,
322
+ betweenPassages: 0.5,
323
+ beforeFtb: 1,
324
+ afterFtb: 0.5,
325
+ };
326
+ /**
327
+ * List of languages whose lessons were built using slightly different pause
328
+ * values.
329
+ */
330
+ const usesAlternativeLessonPauses = ['tel', 'som', 'kmr', 'swz', 'vie'];
331
+ /** Returns the correct lesson pauses for a given language. */
332
+ function getLessonPauses(languageId) {
333
+ return usesAlternativeLessonPauses.includes(languageId)
334
+ ? alternativeLessonPauses
335
+ : lessonPauses_1.lessonPauses;
336
+ }
337
+ /**
338
+ * Converts verse timings within each scripture passage from chapter-relative to
339
+ * full-lesson relative. This includes accounting for question durations,
340
+ * pauses, and from-the-book durations.
341
+ *
342
+ * Multi-chapter passages are split into per-chapter chunks internally, since
343
+ * the audio is clipped from individual chapter files.
344
+ */
345
+ function normalizeVerseTimings({ lessonInfo, scripture, }) {
346
+ const languageInfo = (0, languages_1.getLanguageInfo)(lessonInfo.meetLanguageId);
347
+ const pauses = getLessonPauses(lessonInfo.meetLanguageId);
348
+ let cumulativeTime = lessonInfo.fellowshipDuration + pauses.beforeStory;
349
+ if (specialIds_1.specialIds.introductionLessonIds.includes(lessonInfo.lessonId) &&
350
+ lessonInfo.type === 'dbs' &&
351
+ lessonInfo.introLength)
352
+ cumulativeTime += lessonInfo.introLength;
353
+ let lastBook = null;
354
+ /** Tracks the overall passage index (not chunk index) for FTB/pause logic. */
355
+ let globalChunkIndex = 0;
356
+ return scripture?.map((passage) => {
357
+ if (!passage)
358
+ return undefined;
359
+ const chunks = splitPassageIntoChunks(passage.verses);
360
+ if (chunks.length === 0)
361
+ return passage;
362
+ const adjustedVerses = [];
363
+ for (const chunk of chunks) {
364
+ const firstTimedVerse = chunk.verses.find((v) => v.timings);
365
+ if (!firstTimedVerse?.timings) {
366
+ // No timing data — pass through unadjusted
367
+ adjustedVerses.push(...chunk.verses);
368
+ if (!chunk.isPartOfPrevious)
369
+ globalChunkIndex++;
370
+ continue;
371
+ }
372
+ const chunkAudioStart = firstTimedVerse.timings[0];
373
+ // Add FTB + pauses. Chunks that are continuations of a multi-chapter
374
+ // passage (isPartOfPrevious) get no pause — they are concatenated directly.
375
+ if (!chunk.isPartOfPrevious) {
376
+ if (lastBook !== chunk.bookId) {
377
+ if (globalChunkIndex !== 0) {
378
+ cumulativeTime += pauses.beforeFtb;
379
+ }
380
+ const ftbLanguage = languageInfo.titles ?? languageInfo.languageId;
381
+ const ftbKey = `${ftbLanguage}.${chunk.bookId}.mp3`;
382
+ const ftbDuration = mediaDurations_1.mediaDurations[ftbLanguage]?.[ftbKey];
383
+ if (ftbDuration != null)
384
+ cumulativeTime += ftbDuration;
385
+ else {
386
+ console.error(`FTB duration not found for ${ftbKey}`);
387
+ return passage;
388
+ }
389
+ cumulativeTime += pauses.afterFtb;
390
+ lastBook = chunk.bookId;
391
+ }
392
+ else if (globalChunkIndex !== 0) {
393
+ cumulativeTime += pauses.betweenPassages;
394
+ }
395
+ globalChunkIndex++;
396
+ }
397
+ const offset = cumulativeTime - chunkAudioStart;
398
+ for (const verse of chunk.verses) {
399
+ if (!verse.timings)
400
+ adjustedVerses.push(verse);
401
+ else
402
+ adjustedVerses.push({
403
+ ...verse,
404
+ timings: [verse.timings[0] + offset, verse.timings[1] + offset],
405
+ });
406
+ }
407
+ // Advance cumulative time by this chunk's audio duration
408
+ const lastTimedVerse = [...chunk.verses].reverse().find((v) => v.timings);
409
+ if (lastTimedVerse?.timings) {
410
+ cumulativeTime += lastTimedVerse.timings[1] - chunkAudioStart;
411
+ }
412
+ }
413
+ return { ...passage, verses: adjustedVerses };
414
+ });
415
+ }
@@ -1,5 +1,6 @@
1
1
  import { TranslationsApp } from '../data/translationsApp/translationsApp.zod';
2
2
  import type { LanguageInfo, MeetTranslations } from '../types/languages';
3
+ import { ScripturePassage } from '../types/scripturePassages';
3
4
  import { DbsInfo, Lesson, LessonInfo, SetInfo, VideoInfo } from '../types/sets';
4
5
  export declare const firebaseUrl = "https://firebasestorage.googleapis.com/v0/b/waha-app-db.appspot.com/o/";
5
6
  export declare function getSetInfo({ setId, meetLanguageId, setIds, t, }: {
@@ -22,10 +23,47 @@ export declare function shouldShowLesson(lessonInfo: LessonInfo | undefined, lan
22
23
  * Calculate start times for each section based on question durations and total
23
24
  * audio duration. Must be called after audio is loaded to get accurate total
24
25
  * duration.
25
- *
26
- * @param thisLesson - The lesson to calculate timings for
27
- * @param totalDuration - Total duration of the lesson audio in seconds
28
- * @param languageInfo - The language information to look up question durations
29
- * @returns Map of section ID to start time in seconds
30
26
  */
31
- export declare function calculateSectionTimings(thisLesson: DbsInfo | VideoInfo, totalDuration: number, languageInfo: LanguageInfo): Map<string, number>;
27
+ export declare function calculateSectionTimings({ languageInfo, thisLesson, totalDuration, normalizedScripture, supportsVerseHighlight, }: {
28
+ thisLesson: DbsInfo | VideoInfo;
29
+ totalDuration: number;
30
+ languageInfo: LanguageInfo;
31
+ normalizedScripture: Array<ScripturePassage | undefined>;
32
+ /**
33
+ * Determine if the timings are valid to be used for section timings.
34
+ *
35
+ * Supports verse highlight example:
36
+ *
37
+ * - GEN.1.1-GEN.1.10 -> 83
38
+ * - GEN.2.1-GEN.2.4 -> 100
39
+ * - GEN.2.7-GEN.2.10 -> 120
40
+ *
41
+ * Doesn't support verse highlight example:
42
+ *
43
+ * - GEN.1.1-GEN.1.10 -> 83
44
+ * - GEN.2.1-GEN.2.4 -> undefined
45
+ * - GEN.2.7-GEN.2.10 -> undefined
46
+ *
47
+ * First passage is always valid because its start time is simply the length
48
+ * of the previous audio. Subsequent passages require good verse timing data.
49
+ */
50
+ supportsVerseHighlight: boolean;
51
+ }): Map<string, number>;
52
+ /**
53
+ * Checks if pre-normalized verse timings for given scripture are valid. Does
54
+ * this by comparing the real duration of the full lesson mp3 file with the
55
+ * calculated duration based on individual question durations and verse timing
56
+ * data from bible chapter documents. For lessons generated programmatically
57
+ * with recent systems, they should be nearly identical. For older lessons that
58
+ * were made by hand, they may be off. If they're too off, they will be flagged
59
+ * as invalid and the lesson will default to not using verse timings.
60
+ */
61
+ export declare function getSupportsVerseHighlight({ lessonInfo, scripture, totalDuration, }: {
62
+ lessonInfo: DbsInfo | VideoInfo;
63
+ scripture: Array<ScripturePassage | undefined>;
64
+ totalDuration: number | undefined;
65
+ }): {
66
+ computedLength: number;
67
+ diff: number;
68
+ passed: boolean;
69
+ };
@@ -6,6 +6,7 @@ exports.getLessonInfo = getLessonInfo;
6
6
  exports.convertSToString = convertSToString;
7
7
  exports.shouldShowLesson = shouldShowLesson;
8
8
  exports.calculateSectionTimings = calculateSectionTimings;
9
+ exports.getSupportsVerseHighlight = getSupportsVerseHighlight;
9
10
  const languageAssets_1 = require("../data/languageAssets");
10
11
  const mediaDurations_1 = require("../data/mediaDurations");
11
12
  const questions_1 = require("../data/questions");
@@ -118,6 +119,7 @@ function getLessonInfo({ lessonId, meetLanguageInfo, setInfo, t, useSpokenQuesti
118
119
  hasBeep,
119
120
  });
120
121
  });
122
+ let introLength = undefined;
121
123
  if (specialIds_1.specialIds.evaluationQuestionLessonIds.includes(lesson.lessonId)) {
122
124
  sections.push({
123
125
  id: 'eq',
@@ -141,6 +143,9 @@ function getLessonInfo({ lessonId, meetLanguageInfo, setInfo, t, useSpokenQuesti
141
143
  text: t.introductions.introductions[lessonId],
142
144
  hasBeep: false,
143
145
  });
146
+ const introLanguageId = meetLanguageInfo.introBridge ?? meetLanguageId;
147
+ introLength =
148
+ mediaDurations_1.mediaDurations[introLanguageId]?.[`${introLanguageId}.${lessonId}.mp3`];
144
149
  }
145
150
  lesson.s?.forEach((passageId, index) => {
146
151
  sections.push({
@@ -171,7 +176,6 @@ function getLessonInfo({ lessonId, meetLanguageInfo, setInfo, t, useSpokenQuesti
171
176
  });
172
177
  });
173
178
  const baseInfo = {
174
- lessonIntroduction: t.introductions.introduction,
175
179
  lessonLink: `https://web.waha.app/lesson?lesson-id=${lessonId}&meet-language=${meetLanguageId}`,
176
180
  lessonNumber,
177
181
  lessonTitle,
@@ -179,6 +183,15 @@ function getLessonInfo({ lessonId, meetLanguageInfo, setInfo, t, useSpokenQuesti
179
183
  ...setInfo,
180
184
  ...lesson,
181
185
  };
186
+ const fellowshipDuration = fellowshipQuestions.reduce((sum, questionId) => {
187
+ const key = `${meetLanguageId}.${questionId}.mp3`;
188
+ return sum + (mediaDurations_1.mediaDurations[meetLanguageId]?.[key] ?? 0);
189
+ }, 0);
190
+ let applicationDuration = applicationQuestions.reduce((sum, questionId) => {
191
+ return (sum +
192
+ (mediaDurations_1.mediaDurations[meetLanguageId]?.[`${meetLanguageId}.${questionId}.mp3`] ??
193
+ 0));
194
+ }, 0);
182
195
  if (specialIds_1.specialIds.evaluationQuestionLessonIds.includes(lesson.lessonId)) {
183
196
  const languageId = meetLanguageInfo.introBridge ?? meetLanguageId;
184
197
  const id = `${languageId}.eq`;
@@ -187,6 +200,8 @@ function getLessonInfo({ lessonId, meetLanguageInfo, setInfo, t, useSpokenQuesti
187
200
  return {
188
201
  type: 'eq',
189
202
  ...baseInfo,
203
+ fellowshipDuration,
204
+ applicationDuration,
190
205
  eq: {
191
206
  id,
192
207
  languageId,
@@ -228,6 +243,9 @@ function getLessonInfo({ lessonId, meetLanguageInfo, setInfo, t, useSpokenQuesti
228
243
  };
229
244
  const trainingVideoId = `${meetLanguageInfo.trainingVideoLanguage}.${lessonId}`;
230
245
  const trainingVideoFileName = `${trainingVideoId}.mp4`;
246
+ // Add the length of the training video to the application duration.
247
+ applicationDuration +=
248
+ mediaDurations_1.mediaDurations[meetLanguageInfo.trainingVideoLanguage]?.[trainingVideoFileName] ?? 0;
231
249
  const trainingVideoPath = `${meetLanguageInfo.trainingVideoLanguage}/course/videos_compressed/${trainingVideoFileName}`;
232
250
  trainingVideo = {
233
251
  id: trainingVideoId,
@@ -246,6 +264,8 @@ function getLessonInfo({ lessonId, meetLanguageInfo, setInfo, t, useSpokenQuesti
246
264
  return {
247
265
  type: 'video',
248
266
  ...baseInfo,
267
+ fellowshipDuration,
268
+ applicationDuration,
249
269
  lessonSectionHeader: thisSetTranslations?.sectionHeaders?.[lessonId],
250
270
  lessonSectionBody: thisSetTranslations?.sectionBodies?.[lessonId],
251
271
  video,
@@ -258,14 +278,17 @@ function getLessonInfo({ lessonId, meetLanguageInfo, setInfo, t, useSpokenQuesti
258
278
  return {
259
279
  type: 'dbs',
260
280
  ...baseInfo,
281
+ fellowshipDuration,
282
+ applicationDuration,
283
+ introLength,
261
284
  full: {
262
285
  id: `${meetLanguageId}.${lessonId}`,
263
286
  localFileName: `${meetLanguageId}.${lessonId}.mp3`,
264
287
  remoteFileName: `${meetLanguageId}.${lessonId}.mp3`,
265
288
  languageId: meetLanguageId,
266
- path: `${meetLanguageId}/full_lessons/${setInfo.setId}/${meetLanguageId}.${lessonId}.mp3`,
289
+ path: `${meetLanguageId}/full_lessons${meetLanguageInfo.audioAssetVersion ?? ''}/${setInfo.setId}/${meetLanguageId}.${lessonId}.mp3`,
267
290
  url: exports.firebaseUrl +
268
- encodeURIComponent(`${meetLanguageId}/full_lessons/${setInfo.setId}/${meetLanguageId}.${lessonId}.mp3`) +
291
+ encodeURIComponent(`${meetLanguageId}/full_lessons${meetLanguageInfo.audioAssetVersion ?? ''}/${setInfo.setId}/${meetLanguageId}.${lessonId}.mp3`) +
269
292
  `?alt=media`,
270
293
  },
271
294
  dbsCast: {
@@ -324,12 +347,6 @@ function shouldShowLesson(lessonInfo, languageInfo) {
324
347
  console.log('Missing lesson info or lessonTitle lesson:', lessonInfo?.lessonId);
325
348
  return false;
326
349
  }
327
- else if (specialIds_1.specialIds.growingAsDmcSetIds.includes(lessonInfo.setId) &&
328
- (lessonInfo.lessonIntroduction === undefined ||
329
- lessonInfo.lessonIntroduction === '')) {
330
- console.log('Missing intro for lesson:', lessonInfo.lessonId);
331
- return false;
332
- }
333
350
  else if (lessonInfo.type === 'eq' &&
334
351
  !languageAssets_1.languageAssets[languageInfo.introBridge ?? languageInfo.languageId].find((f) => f === lessonInfo.eq.remoteFileName)) {
335
352
  console.log('Missing remote eq file for lesson:', lessonInfo.lessonId);
@@ -353,14 +370,10 @@ function shouldShowLesson(lessonInfo, languageInfo) {
353
370
  * Calculate start times for each section based on question durations and total
354
371
  * audio duration. Must be called after audio is loaded to get accurate total
355
372
  * duration.
356
- *
357
- * @param thisLesson - The lesson to calculate timings for
358
- * @param totalDuration - Total duration of the lesson audio in seconds
359
- * @param languageInfo - The language information to look up question durations
360
- * @returns Map of section ID to start time in seconds
361
373
  */
362
- function calculateSectionTimings(thisLesson, totalDuration, languageInfo) {
374
+ function calculateSectionTimings({ languageInfo, thisLesson, totalDuration, normalizedScripture, supportsVerseHighlight, }) {
363
375
  const languageDurations = mediaDurations_1.mediaDurations[languageInfo.languageId];
376
+ const pauses = (0, scripturePassages_1.getLessonPauses)(thisLesson.meetLanguageId);
364
377
  const getQuestionDuration = (questionId) => {
365
378
  const fileName = `${languageInfo.languageId}.${questionId}.mp3`;
366
379
  return languageDurations?.[fileName] ?? 0;
@@ -370,42 +383,85 @@ function calculateSectionTimings(thisLesson, totalDuration, languageInfo) {
370
383
  timings.set('title', 0);
371
384
  // Calculate fellowship question timings from the start
372
385
  let currentTime = 0;
373
- const fellowshipSections = thisLesson.sections.filter((s) => s.chapter === sets_2.Chapter.FELLOWSHIP);
374
- fellowshipSections.forEach((section) => {
386
+ /** Add timings for each fellowship question. */
387
+ thisLesson.sections
388
+ .filter((s) => s.chapter === sets_2.Chapter.FELLOWSHIP)
389
+ .forEach((section) => {
375
390
  timings.set(section.id, currentTime);
376
391
  currentTime += getQuestionDuration(section.id);
377
392
  });
378
- // Story/Scripture section starts after fellowship
379
- const storySections = thisLesson.sections.filter((s) => s.chapter === sets_2.Chapter.STORY || s.chapter === sets_2.Chapter.INTRODUCTION);
380
- const storyStartTime = currentTime;
381
- storySections.forEach((section) => {
382
- timings.set(section.id, storyStartTime);
383
- });
384
- // Calculate application question timings by working backwards from total duration
385
- const applicationSections = thisLesson.sections.filter((s) => s.chapter === sets_2.Chapter.APPLICATION);
386
- // Calculate total duration of all application questions
387
- const totalApplicationDuration = applicationSections.reduce((sum, section) => {
388
- if (section.id.includes('video') &&
389
- thisLesson.type === 'video' &&
390
- thisLesson.trainingVideo)
391
- return (sum +
392
- mediaDurations_1.mediaDurations[thisLesson.trainingVideo.languageId][thisLesson.trainingVideo.localFileName]);
393
- else
394
- return sum + getQuestionDuration(section.id);
395
- }, 0);
396
- // Application section starts after story ends
397
- // Story duration = totalDuration - fellowship duration - application duration
398
- const applicationStartTime = totalDuration - totalApplicationDuration;
399
- let applicationTime = applicationStartTime;
400
- applicationSections.forEach((section) => {
401
- timings.set(section.id, applicationTime);
393
+ /** Add the pause before the story chapter starts. */
394
+ currentTime += pauses.beforeStory;
395
+ /** Add the intro timing and pause if we have one. */
396
+ const introSection = thisLesson.sections.find((s) => s.chapter === sets_2.Chapter.INTRODUCTION);
397
+ if (introSection && thisLesson.type === 'dbs' && thisLesson.introLength) {
398
+ timings.set(introSection.id, currentTime);
399
+ currentTime += thisLesson.introLength;
400
+ }
401
+ const storySections = thisLesson.sections.filter((s) => s.chapter === sets_2.Chapter.STORY);
402
+ if (storySections.length > 0) {
403
+ /** First passage will always start right after all previous sections. */
404
+ timings.set(storySections[0].id, currentTime);
405
+ /** Subsequent passages require normalized scripture data to get timings for. */
406
+ if (storySections.length > 1 && supportsVerseHighlight)
407
+ storySections.slice(1).forEach((section) => {
408
+ const passageStartTime = normalizedScripture[section.indexWithinChapter]?.verses?.[0]
409
+ ?.timings?.[0];
410
+ if (passageStartTime != null)
411
+ timings.set(section.id, passageStartTime);
412
+ });
413
+ }
414
+ /**
415
+ * Application section starts after story ends, but we can work backwards and
416
+ * calculate its start time by subtracting its duration from the total
417
+ * duration. This way, if we don't have verse timings data, we can still have
418
+ * timings for the application section.
419
+ */
420
+ const applicationStartTime = totalDuration - thisLesson.applicationDuration;
421
+ currentTime = applicationStartTime;
422
+ thisLesson.sections
423
+ .filter((s) => s.chapter === sets_2.Chapter.APPLICATION)
424
+ .forEach((section) => {
425
+ timings.set(section.id, currentTime);
402
426
  if (section.id.includes('video') &&
403
427
  thisLesson.type === 'video' &&
404
428
  thisLesson.trainingVideo)
405
- applicationTime +=
406
- mediaDurations_1.mediaDurations[thisLesson.trainingVideo.languageId][thisLesson.trainingVideo.localFileName];
429
+ currentTime +=
430
+ mediaDurations_1.mediaDurations[thisLesson.trainingVideo.languageId]?.[thisLesson.trainingVideo.localFileName] ?? 0;
407
431
  else
408
- applicationTime += getQuestionDuration(section.id);
432
+ currentTime += getQuestionDuration(section.id);
409
433
  });
410
434
  return timings;
411
435
  }
436
+ /**
437
+ * Checks if pre-normalized verse timings for given scripture are valid. Does
438
+ * this by comparing the real duration of the full lesson mp3 file with the
439
+ * calculated duration based on individual question durations and verse timing
440
+ * data from bible chapter documents. For lessons generated programmatically
441
+ * with recent systems, they should be nearly identical. For older lessons that
442
+ * were made by hand, they may be off. If they're too off, they will be flagged
443
+ * as invalid and the lesson will default to not using verse timings.
444
+ */
445
+ function getSupportsVerseHighlight({ lessonInfo, scripture, totalDuration, }) {
446
+ if (!totalDuration)
447
+ return { computedLength: 0, diff: 0, passed: false };
448
+ let lastEnd;
449
+ for (const passage of scripture) {
450
+ if (!passage)
451
+ continue;
452
+ for (const verse of passage.verses) {
453
+ if (verse.timings) {
454
+ if (lastEnd === undefined || verse.timings[1] > lastEnd) {
455
+ lastEnd = verse.timings[1];
456
+ }
457
+ }
458
+ }
459
+ }
460
+ if (lastEnd === undefined)
461
+ return { computedLength: 0, diff: 0, passed: false };
462
+ const computedLength = lastEnd +
463
+ (0, scripturePassages_1.getLessonPauses)(lessonInfo.meetLanguageId).afterStory +
464
+ lessonInfo.applicationDuration;
465
+ const diff = Math.abs(computedLength - totalDuration);
466
+ return { computedLength, diff, passed: diff < 3 };
467
+ }
@@ -30,8 +30,8 @@ type Meeting = {
30
30
  payload: {
31
31
  timeToCompletion: number;
32
32
  isAudioFacilitator: boolean;
33
- howManyShared?: StoplightOption;
34
- howManyObeyed?: StoplightOption;
33
+ howManyShared: StoplightOption | undefined;
34
+ howManyObeyed: StoplightOption | undefined;
35
35
  };
36
36
  } | {
37
37
  name: 'ScheduleNextMeeting';
@@ -24,9 +24,12 @@ export interface Content {
24
24
  export interface BaseInfo {
25
25
  lessonNumber: string;
26
26
  lessonTitle: string | undefined;
27
- lessonIntroduction: string | undefined;
28
27
  lessonLink: string;
29
28
  sections: Section[];
29
+ /** Full duration in seconds of all of the lesson's fellowship questions. */
30
+ fellowshipDuration: number;
31
+ /** Full duration in seconds of all of the lesson's application questions. */
32
+ applicationDuration: number;
30
33
  }
31
34
  export interface Section {
32
35
  id: string;
@@ -45,7 +48,7 @@ export interface DbsInfo extends BaseInfo, SetInfo, Lesson {
45
48
  remoteFileName: string;
46
49
  url: string;
47
50
  };
48
- lessonIntroduction: string | undefined;
51
+ introLength: number | undefined;
49
52
  passagesString: string;
50
53
  }
51
54
  export interface VideoInfo extends BaseInfo, SetInfo, Lesson {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waha-shared",
3
- "version": "1.0.281",
3
+ "version": "1.0.283",
4
4
  "author": "Waha",
5
5
  "dependencies": {
6
6
  "@types/signale": "^1.4.7",