waha-shared 1.0.308 → 1.0.310

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 (110) hide show
  1. package/dist/data/areas/areas.json +50 -48
  2. package/dist/data/areas/areas.schema.json +2 -2
  3. package/dist/data/areas/areas.zod.d.ts +1 -1
  4. package/dist/data/areas/areas.zod.js +1 -1
  5. package/dist/data/areas/index.d.ts +1 -1
  6. package/dist/data/bibleAudios/bibleAudios.json +597 -314
  7. package/dist/data/bibleAudios/bibleAudios.schema.json +17 -8
  8. package/dist/data/bibleAudios/bibleAudios.zod.d.ts +43 -2
  9. package/dist/data/bibleAudios/bibleAudios.zod.js +16 -7
  10. package/dist/data/bibleAudios/index.d.ts +2 -1
  11. package/dist/data/bibleBooks/bibleBooks.json +792 -790
  12. package/dist/data/bibleStatuses/bibleStatuses.json +8302 -8711
  13. package/dist/data/bibleStatuses/bibleStatuses.schema.json +6 -0
  14. package/dist/data/bibleStatuses/bibleStatuses.zod.d.ts +1 -0
  15. package/dist/data/bibleStatuses/bibleStatuses.zod.js +4 -0
  16. package/dist/data/bibleStatuses/index.d.ts +1 -0
  17. package/dist/data/bibleTexts/bibleTexts.json +285 -187
  18. package/dist/data/bibleTexts/bibleTexts.schema.json +7 -0
  19. package/dist/data/bibleTexts/bibleTexts.zod.d.ts +28 -0
  20. package/dist/data/bibleTexts/bibleTexts.zod.js +10 -3
  21. package/dist/data/bibleTexts/index.d.ts +2 -0
  22. package/dist/data/clones/clones.json +2 -0
  23. package/dist/data/countries/countries.json +480 -480
  24. package/dist/data/countriesAndLanguages/countriesAndLanguages.json +6812 -6810
  25. package/dist/data/crowdinLanguages/crowdinLanguages.json +1440 -1438
  26. package/dist/data/crowdinLanguages/crowdinLanguages.schema.json +3 -3
  27. package/dist/data/crowdinLanguages/crowdinLanguages.zod.d.ts +2 -2
  28. package/dist/data/crowdinLanguages/crowdinLanguages.zod.js +2 -2
  29. package/dist/data/crowdinLanguages/index.d.ts +2 -2
  30. package/dist/data/curriculumFoundations/curriculumFoundations.json +86 -69
  31. package/dist/data/curriculumOnboarding/curriculumOnboarding.json +34 -0
  32. package/dist/data/curriculumOnboarding/curriculumOnboarding.schema.json +38 -0
  33. package/dist/data/curriculumOnboarding/curriculumOnboarding.zod.d.ts +11 -0
  34. package/dist/data/curriculumOnboarding/curriculumOnboarding.zod.js +18 -0
  35. package/dist/data/curriculumOnboarding/index.d.ts +9 -0
  36. package/dist/data/curriculumOnboarding/index.js +7 -0
  37. package/dist/data/curriculumQuestions/curriculumQuestions.json +194 -192
  38. package/dist/data/curriculumTopics/curriculumTopics.json +51 -8
  39. package/dist/data/dblAudioLicenses/dblAudioLicenses.json +450 -421
  40. package/dist/data/dblTextLicenses/dblTextLicenses.json +1251 -1161
  41. package/dist/data/firebase.d.ts +1 -0
  42. package/dist/data/firebase.js +4 -0
  43. package/dist/data/iosVoiceOverLanguages/iosVoiceOverLanguages.json +1 -1
  44. package/dist/data/iso6933LanguageCodes/iso6933LanguageCodes.json +1 -1
  45. package/dist/data/languages/index.d.ts +22 -10
  46. package/dist/data/languages/languages.json +3299 -2012
  47. package/dist/data/languages/languages.schema.json +102 -48
  48. package/dist/data/languages/languages.zod.d.ts +96 -30
  49. package/dist/data/languages/languages.zod.js +66 -37
  50. package/dist/data/lessonPauses/index.d.ts +0 -1
  51. package/dist/data/lessonPauses/lessonPauses.json +2 -3
  52. package/dist/data/lessonPauses/lessonPauses.schema.json +1 -11
  53. package/dist/data/lessonPauses/lessonPauses.zod.d.ts +0 -1
  54. package/dist/data/lessonPauses/lessonPauses.zod.js +0 -3
  55. package/dist/data/mediaDurations/mediaDurations.json +35797 -6076
  56. package/dist/data/mediaDurations/mediaDurations.schema.json +1 -1
  57. package/dist/data/mediaDurations/mediaDurations.zod.js +1 -1
  58. package/dist/data/notification/index.d.ts +1 -1
  59. package/dist/data/notification/notification.json +93 -169
  60. package/dist/data/phoneLanguages/phoneLanguages.json +637 -635
  61. package/dist/data/questions/questions.json +149 -147
  62. package/dist/data/releaseNotes/releaseNotes.json +176 -123
  63. package/dist/data/screenshots/screenshots.json +1 -1
  64. package/dist/data/scripts/scripts.json +6 -1
  65. package/dist/data/sets/index.d.ts +3 -3
  66. package/dist/data/sets/sets.json +1770 -480
  67. package/dist/data/sets/sets.schema.json +1 -1
  68. package/dist/data/sets/sets.zod.d.ts +3 -3
  69. package/dist/data/sets/sets.zod.js +3 -6
  70. package/dist/data/translationsApp/index.d.ts +1 -0
  71. package/dist/data/translationsApp/translationsApp.json +1610 -1569
  72. package/dist/data/translationsApp/translationsApp.schema.json +3 -1
  73. package/dist/data/translationsApp/translationsApp.zod.d.ts +2 -0
  74. package/dist/data/translationsApp/translationsApp.zod.js +1 -0
  75. package/dist/data/translationsFtb/translationsFtb.json +120 -664
  76. package/dist/data/translationsIntroduction/translationsIntroduction.json +1 -1
  77. package/dist/data/translationsQuestion/translationsQuestion.json +65 -33
  78. package/dist/data/translationsSet/translationsSet.json +30139 -19402
  79. package/dist/data/translationsSet/translationsSet.schema.json +3 -4
  80. package/dist/data/translationsSet/translationsSet.zod.js +4 -4
  81. package/dist/data/translationsSolarSpeaker/translationsSolarSpeaker.json +1 -1
  82. package/dist/data/translationsSpokenQuestion/translationsSpokenQuestion.json +65 -33
  83. package/dist/data/youtubePlaylists/youtubePlaylists.json +10 -10
  84. package/dist/data/youtubeVideos/youtubeVideos.json +82 -82
  85. package/dist/functions/bibleBooks.d.ts +16 -0
  86. package/dist/functions/bibleBooks.js +101 -0
  87. package/dist/functions/bibles.d.ts +47 -0
  88. package/dist/functions/bibles.js +315 -0
  89. package/dist/functions/languages.d.ts +6 -7
  90. package/dist/functions/languages.js +58 -53
  91. package/dist/functions/scripturePassages.d.ts +53 -47
  92. package/dist/functions/scripturePassages.js +217 -303
  93. package/dist/functions/sets.d.ts +13 -52
  94. package/dist/functions/sets.js +195 -205
  95. package/dist/functions/utils.d.ts +2 -0
  96. package/dist/functions/utils.js +8 -0
  97. package/dist/types/bibleChapters.d.ts +2 -2
  98. package/dist/types/bibleChapters.js +1 -1
  99. package/dist/types/languages.d.ts +48 -9
  100. package/dist/types/languages.js +2 -0
  101. package/dist/types/scripturePassages.d.ts +0 -1
  102. package/dist/types/scripturePassages.js +0 -1
  103. package/dist/types/sets.d.ts +28 -11
  104. package/package.json +1 -1
  105. package/dist/data/languageAssets/index.d.ts +0 -1
  106. package/dist/data/languageAssets/index.js +0 -7
  107. package/dist/data/languageAssets/languageAssets.json +0 -45406
  108. package/dist/data/languageAssets/languageAssets.schema.json +0 -19
  109. package/dist/data/languageAssets/languageAssets.zod.d.ts +0 -3
  110. package/dist/data/languageAssets/languageAssets.zod.js +0 -7
@@ -1,61 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseVerseRange = parseVerseRange;
3
+ exports.parseVerseRange = void 0;
4
4
  exports.getChapterUrl = getChapterUrl;
5
5
  exports.getPassagesString = getPassagesString;
6
+ exports.withChapterPlaceholders = withChapterPlaceholders;
6
7
  exports.getScripturePassage = getScripturePassage;
7
8
  exports.getLessonScripture = getLessonScripture;
8
- exports.parseBibleSnapshot = parseBibleSnapshot;
9
9
  exports.verseToSuperscript = verseToSuperscript;
10
- exports.getLessonPauses = getLessonPauses;
10
+ exports.getFtbDuration = getFtbDuration;
11
+ exports.enrichSections = enrichSections;
11
12
  exports.normalizeVerseTimings = normalizeVerseTimings;
12
13
  const bibleStatuses_1 = require("../data/bibleStatuses");
13
- const lessonPauses_1 = require("../data/lessonPauses");
14
+ const firebase_1 = require("../data/firebase");
14
15
  const mediaDurations_1 = require("../data/mediaDurations");
15
- const specialIds_1 = require("../data/specialIds");
16
16
  const languages_1 = require("../functions/languages");
17
- const bibleChapters_1 = require("../types/bibleChapters");
18
- const sets_1 = require("./sets");
19
- /**
20
- * Parses a verse range string like "GEN.1.1-GEN.1.25" or "JHN.3.16" or "REV.22"
21
- * Returns the start and end verse IDs
22
- */
23
- function parseVerseRange(passageId) {
24
- // Handle single verse: "GEN.1.1" -> "GEN.1.1-GEN.1.1"
25
- const parts = passageId.includes('-')
26
- ? passageId.split('-')
27
- : [passageId, passageId];
28
- const startParts = parts[0].split('.');
29
- const endParts = parts[1].split('.');
30
- const startBook = startParts[0];
31
- const startChapter = startParts[1];
32
- const startVerse = startParts.length > 2 ? startParts[2] : '1';
33
- const endBook = endParts[0];
34
- const endChapter = endParts[1];
35
- const endVerse = endParts.length > 2 ? endParts[2] : '1';
36
- return {
37
- startBook,
38
- startChapter,
39
- startVerse,
40
- endBook,
41
- endChapter,
42
- endVerse,
43
- startChapterId: `${startBook}.${startChapter}`,
44
- endChapterId: `${endBook}.${endChapter}`,
45
- };
46
- }
17
+ const sets_1 = require("../types/sets");
18
+ const bibleBooks_1 = require("./bibleBooks");
19
+ const bibles_1 = require("./bibles");
20
+ var bibleBooks_2 = require("./bibleBooks");
21
+ Object.defineProperty(exports, "parseVerseRange", { enumerable: true, get: function () { return bibleBooks_2.parseVerseRange; } });
47
22
  function getChapterUrl(params) {
48
- const startBook = 'startBook' in params
49
- ? params.startBook
50
- : parseVerseRange(params.passageId).startBook;
51
- const startChapter = 'startChapter' in params
52
- ? params.startChapter
53
- : parseVerseRange(params.passageId).startChapter;
54
- return (sets_1.firebaseUrl +
23
+ const book = 'book' in params ? params.book : (0, bibleBooks_1.parseVerseRange)(params.passageId).book;
24
+ const chapter = 'chapter' in params
25
+ ? params.chapter
26
+ : (0, bibleBooks_1.parseVerseRange)(params.passageId).chapter;
27
+ return (firebase_1.firebaseUrl +
55
28
  encodeURIComponent('audio_bibles' +
56
29
  `/${params.bibleAudioId}` +
57
- `/${startBook}` +
58
- `/${startBook}_${startChapter.padStart(3, '0')}.mp3`) +
30
+ `/${book}` +
31
+ `/${book}_${chapter.padStart(3, '0')}.mp3`) +
59
32
  `?alt=media`);
60
33
  }
61
34
  /** Extracts verses from a chapter based on verse numbers */
@@ -85,170 +58,119 @@ function extractVersesFromChapter(chapter, startVerse, endVerse) {
85
58
  * ["GEN.1.1-GEN.1.25", "GEN.2.4-GEN.2.7", "EXO.3.3-EXO.3.25"] -> "Genesis 1: 1-25, 2: 4-7, Exodus 3: 3-25"
86
59
  * ["JHN.1.1"] -> "John 1: 1"
87
60
  */
88
- function getPassagesString(passageIds, language) {
89
- if (!passageIds || passageIds.length === 0)
90
- return '';
61
+ function getPassagesString(passages) {
91
62
  const parts = [];
92
63
  let currentBook = null;
93
- for (const passageId of passageIds) {
94
- const { startBook, startChapter, startVerse, endChapter, endVerse } = parseVerseRange(passageId);
95
- let bookName =
96
- // American sign language should use English book names.
97
- language.languageId === 'ase'
98
- ? bibleStatuses_1.bibleStatuses.NLT.bookNames[startBook]
99
- : language.bible.bookNames[startBook];
100
- if (!bookName) {
101
- bookName = language.bibleFallback?.bookNames[startBook];
102
- if (!bookName)
103
- continue;
104
- }
105
- // New book - include book name
106
- if (currentBook !== startBook) {
107
- currentBook = startBook;
108
- // Same chapter
109
- if (startChapter === endChapter) {
110
- if (startVerse === endVerse) {
111
- parts.push(`${bookName} ${startChapter}: ${startVerse}`);
112
- }
113
- else {
114
- parts.push(`${bookName} ${startChapter}: ${startVerse}-${endVerse}`);
115
- }
116
- }
117
- else {
118
- // Different chapters
119
- parts.push(`${bookName} ${startChapter}: ${startVerse}-${endChapter}: ${endVerse}`);
120
- }
64
+ for (const passage of passages) {
65
+ const bibleInfo = (0, bibles_1.getBibleInfo)(passage.bibleId);
66
+ if (!bibleInfo)
67
+ continue;
68
+ const { book, chapter, startVerse, endVerse } = (0, bibleBooks_1.parseVerseRange)(passage.passageId);
69
+ const bookName = bibleInfo.bookNames[book];
70
+ if (!bookName)
71
+ continue;
72
+ const verseRange = startVerse === endVerse ? startVerse : `${startVerse}-${endVerse}`;
73
+ if (currentBook !== book) {
74
+ currentBook = book;
75
+ parts.push((0, languages_1.numerals)(`${bookName} ${chapter}: ${verseRange}`, bibleInfo.language.script));
121
76
  }
122
77
  else {
123
- // Same book - omit book name
124
- if (startChapter === endChapter) {
125
- if (startVerse === endVerse) {
126
- parts.push(`${startChapter}: ${startVerse}`);
127
- }
128
- else {
129
- parts.push(`${startChapter}: ${startVerse}-${endVerse}`);
130
- }
131
- }
132
- else {
133
- // Different chapters
134
- parts.push(`${startChapter}: ${startVerse}-${endChapter}: ${endVerse}`);
135
- }
78
+ parts.push((0, languages_1.numerals)(`${chapter}: ${verseRange}`, bibleInfo.language.script));
136
79
  }
137
80
  }
138
- return (0, languages_1.numerals)(parts.join(', '), language.script.name);
81
+ return parts.join(', ');
82
+ }
83
+ /**
84
+ * Wraps a raw chapter fetcher with placeholder logic shared across all
85
+ * backends:
86
+ *
87
+ * - If the chapter is not in `availableTextChapters` nor
88
+ * `availableTimingsChapters`, synthesize a one-verse placeholder without
89
+ * calling the underlying fetcher.
90
+ * - If a fetched chapter is timings-only (verses exist but none have text), patch
91
+ * verse 1's text with `noTextAvailableString` so downstream consumers can
92
+ * render something.
93
+ */
94
+ function withChapterPlaceholders(fetchRaw) {
95
+ return async ({ bibleId, chapterId, noTextAvailableString }) => {
96
+ const bibleInfo = (0, bibles_1.getBibleInfo)(bibleId);
97
+ if (!bibleInfo)
98
+ return undefined;
99
+ if (!bibleInfo.availableTextChapters.includes(chapterId) &&
100
+ !bibleInfo.availableTimingsChapters.includes(chapterId)) {
101
+ return {
102
+ bookName: bibleInfo.bookNames[chapterId.split('.')[0]],
103
+ chapterId,
104
+ reference: chapterId,
105
+ titles: null,
106
+ translationId: bibleId,
107
+ verses: [{ text: noTextAvailableString, verseId: `${chapterId}.1` }],
108
+ };
109
+ }
110
+ const chapter = await fetchRaw({ bibleId, chapterId });
111
+ if (!chapter)
112
+ return undefined;
113
+ if (chapter.verses.length > 0 && !chapter.verses.some((v) => v.text)) {
114
+ chapter.verses[0].text = noTextAvailableString;
115
+ }
116
+ return chapter;
117
+ };
139
118
  }
140
119
  /** Gets a scripture passage by extracting verses from chapters documents. */
141
- async function getScripturePassage({ bibleId, bibleFallbackId, passageId, getChapter, languageInfo, }) {
120
+ async function getScripturePassage({ bibleId, passageId, getChapter, noTextAvailableString, }) {
121
+ const bibleInfo = (0, bibles_1.getBibleInfo)(bibleId);
122
+ const { startVerse, endVerse, chapterId } = (0, bibleBooks_1.parseVerseRange)(passageId);
123
+ if (!bibleInfo)
124
+ return;
142
125
  try {
143
- const { startBook, startChapter, startVerse, endBook, endChapter, endVerse, } = parseVerseRange(passageId);
144
126
  const startVerseNum = parseInt(startVerse);
145
127
  const endVerseNum = parseInt(endVerse);
146
- // Same chapter - simple case
147
- if (startBook === endBook && startChapter === endChapter) {
148
- const chapterId = `${startBook}.${startChapter}`;
149
- let chapter = await getChapter({ chapterId, bibleId });
150
- if (!chapter && bibleFallbackId)
151
- chapter = await getChapter({ chapterId, bibleId: bibleFallbackId });
152
- if (!chapter) {
153
- console.warn(`Chapter not found: ${bibleId}.${chapterId} for passage ${passageId}`);
154
- return;
155
- }
156
- const verses = extractVersesFromChapter(chapter, startVerseNum, endVerseNum);
157
- if (verses.length === 0) {
158
- console.warn(`No verses found for passage ${passageId}`);
159
- return;
160
- }
128
+ const chapter = await getChapter({
129
+ chapterId,
130
+ bibleId,
131
+ noTextAvailableString,
132
+ });
133
+ if (!chapter || chapter.verses.length === 0)
134
+ return;
135
+ /**
136
+ * If we only have one verse for a chapter, it is the placeholder verse and
137
+ * we should return them as is instead of trying to extract a passage.
138
+ */
139
+ if (chapter.verses.length === 1) {
161
140
  return {
162
141
  passageId,
163
- bibleId,
164
- header: getPassagesString([passageId], languageInfo),
165
- verses,
142
+ bibleId: chapter.translationId,
143
+ verses: chapter.verses,
166
144
  };
167
145
  }
168
- // Multi-chapter passage (rare but possible)
169
- // Example: GEN.1.26-GEN.2.3
170
- const chapters = [];
171
- // Get all chapters in the range
172
- const startChapterNum = parseInt(startChapter);
173
- const endChapterNum = parseInt(endChapter);
174
- for (let i = startChapterNum; i <= endChapterNum; i++) {
175
- const chapterId = `${startBook}.${i}`;
176
- let chapter = await getChapter({ chapterId, bibleId });
177
- if (!chapter && bibleFallbackId)
178
- chapter = await getChapter({ chapterId, bibleId: bibleFallbackId });
179
- if (chapter) {
180
- chapters.push(chapter);
181
- }
182
- else {
183
- console.warn(`Missing chapter in range: ${bibleId}.${chapterId}`);
184
- }
185
- }
186
- if (chapters.length === 0) {
187
- return;
188
- }
189
- // Extract verses from each chapter
190
- const allVerses = [];
191
- chapters.forEach((chapter, index) => {
192
- if (index === 0) {
193
- // First chapter - from startVerse to end
194
- const lastVerse = Math.max(...chapter.verses.map((v) => parseInt(v.verseId.split('.')[2])));
195
- allVerses.push(...extractVersesFromChapter(chapter, startVerseNum, lastVerse));
196
- }
197
- else if (index === chapters.length - 1) {
198
- // Last chapter - from beginning to endVerse
199
- allVerses.push(...extractVersesFromChapter(chapter, 1, endVerseNum));
200
- }
201
- else {
202
- // Middle chapters - all verses
203
- allVerses.push(...chapter.verses);
204
- }
205
- });
206
- return {
207
- passageId,
208
- bibleId,
209
- header: getPassagesString([passageId], languageInfo),
210
- verses: allVerses,
211
- };
146
+ const verses = extractVersesFromChapter(chapter, startVerseNum, endVerseNum);
147
+ /**
148
+ * For textless chapters, verse 1 is always the placeholder verse. After
149
+ * extraction, we may have lost verse 1, so we patch back in the placeholder
150
+ * text.
151
+ */
152
+ if (!verses.some((v) => v.text) && verses.length > 0)
153
+ verses[0].text = noTextAvailableString;
154
+ return { passageId, bibleId: chapter.translationId, verses };
212
155
  }
213
156
  catch (error) {
214
157
  console.error(`Error getting scripture passage ${passageId} in ${bibleId}`, error);
215
- return;
158
+ throw error;
216
159
  }
217
160
  }
218
- /**
219
- * Gets multiple scripture passages efficiently (Future optimization: could
220
- * batch the chapter fetches)
221
- */
222
- async function getLessonScripture(params) {
223
- const passages = await Promise.all(params.passageIds.map((passageId) => getScripturePassage({ ...params, passageId })));
224
- return passages;
225
- }
226
- function parseBibleSnapshot(snapshot, bible) {
227
- const validChapters = [];
228
- if (!snapshot) {
229
- console.warn('No snapshot for', bible);
230
- return [];
231
- }
232
- else if (snapshot.empty) {
233
- console.warn('Empty snapshot for', bible);
234
- return [];
235
- }
236
- for (const doc of snapshot.docs) {
237
- const chapterParse = bibleChapters_1.BibleChapter.safeParse(doc.data());
238
- if (!chapterParse.success) {
239
- console.warn('Skipping invalid bible chapter document', doc.id, chapterParse.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`));
240
- }
241
- else {
242
- validChapters.push(chapterParse.data);
243
- }
244
- }
245
- if (validChapters.length === 0) {
246
- console.warn('No valid chapters for', bible);
247
- return [];
248
- }
249
- else {
250
- return validChapters;
251
- }
161
+ /** Gets all scripture passages for a lesson. */
162
+ async function getLessonScripture({ getChapter, lessonInfo, noTextAvailableString, }) {
163
+ return Promise.all(lessonInfo.sections
164
+ .filter((s) => s.chapter === sets_1.Chapter.STORY)
165
+ .map((s) => {
166
+ const [bibleId, passageId] = s.id.split(';');
167
+ return getScripturePassage({
168
+ bibleId,
169
+ passageId,
170
+ getChapter,
171
+ noTextAvailableString,
172
+ });
173
+ }));
252
174
  }
253
175
  /**
254
176
  * Converts a verse number to a superscript string. This is useful for
@@ -285,131 +207,123 @@ function verseToSuperscript(num) {
285
207
  .join('');
286
208
  }
287
209
  /**
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.
210
+ * Returns the FTB audio duration + afterFtb pause for a section, or 0 if the
211
+ * section has no FTB.
292
212
  */
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
- }
213
+ function getFtbDuration(section, lessonInfo) {
214
+ if (!section.ftbFileName)
215
+ return 0;
216
+ const ftbDuration = mediaDurations_1.mediaDurations[lessonInfo.contentLanguages.ftbs]?.[section.ftbFileName];
217
+ if (ftbDuration == null) {
218
+ console.warn(`FTB duration not found for ${section.ftbFileName}`);
312
219
  }
313
- return chunks;
220
+ return (ftbDuration ?? 0) + lessonInfo.lessonPauses.afterFtb;
314
221
  }
315
222
  /**
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.
223
+ * Fills in `length` (for verse-timed story sections) and `startTime` (for all
224
+ * sections) by accumulating forward through the lesson. After this, every
225
+ * section has both fields set.
329
226
  */
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;
227
+ function enrichSections(lessonInfo, scripture) {
228
+ const storySections = lessonInfo.sections.filter((s) => s.chapter === sets_1.Chapter.STORY);
229
+ /**
230
+ * Forward pass: compute lengths and start times for all non-application
231
+ * sections. We track whether any prior story had an unknown length — once
232
+ * that happens, every later story's startTime is unreliable (it accumulates
233
+ * from a bogus length), so we flag it with `ignoreTimings`.
234
+ */
235
+ let currentTime = 0;
236
+ let priorStoryLengthUnknown = false;
237
+ const enriched = lessonInfo.sections.map((section) => {
238
+ if (!section.chapter)
239
+ return { ...section, startTime: 0, length: 0 };
240
+ currentTime += section.pauseBefore ?? 0;
241
+ let length;
242
+ const isStory = section.chapter === sets_1.Chapter.STORY;
243
+ const ignoreTimings = isStory && priorStoryLengthUnknown ? true : undefined;
244
+ const startTime = currentTime;
245
+ if (!isStory) {
246
+ length = mediaDurations_1.mediaDurations[section.languageId]?.[section.fileName];
247
+ }
248
+ else {
249
+ currentTime += getFtbDuration(section, lessonInfo);
250
+ const [bibleId, passageId] = section.id.split(';');
251
+ length = bibleStatuses_1.bibleStatuses[bibleId]?.customPassages?.[passageId];
252
+ if (!length) {
253
+ const passage = scripture[storySections.indexOf(section)];
254
+ if (passage) {
255
+ const firstTimed = passage.verses.find((v) => v.timings);
256
+ const lastTimed = [...passage.verses].reverse().find((v) => v.timings);
257
+ if (firstTimed?.timings && lastTimed?.timings) {
258
+ length = lastTimed.timings[1] - firstTimed.timings[0];
259
+ }
260
+ }
261
+ }
262
+ if (length == null)
263
+ priorStoryLengthUnknown = true;
264
+ }
265
+ if (length == null) {
266
+ console.warn(`Missing duration for section ${section.id} (${section.chapter}, ${section.fileName})`);
267
+ }
268
+ currentTime += length ?? 0;
269
+ return { ...section, length: length ?? 0, startTime, ignoreTimings };
270
+ });
271
+ // Backward pass: compute application section start times from the end of the
272
+ // lesson file, so they are always correct even if story durations are
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) {
279
+ let timeFromEnd = totalDuration;
280
+ const applicationSections = enriched.filter((s) => s.chapter === sets_1.Chapter.APPLICATION);
281
+ for (const section of [...applicationSections].reverse()) {
282
+ timeFromEnd -= section.length;
283
+ section.startTime = timeFromEnd;
284
+ timeFromEnd -= section.pauseBefore ?? 0;
285
+ }
286
+ }
287
+ return enriched;
336
288
  }
337
289
  /**
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.
290
+ * Converts verse timings from chapter-relative to lesson-relative using each
291
+ * section's `startTime` (set by {@link enrichSections}).
344
292
  */
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) => {
293
+ function normalizeVerseTimings({ enrichedSections, scripture, lessonInfo, }) {
294
+ const storySections = enrichedSections.filter((s) => s.chapter === sets_1.Chapter.STORY);
295
+ return scripture.map((passage, passageIndex) => {
357
296
  if (!passage)
358
297
  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) {
298
+ const section = storySections[passageIndex];
299
+ const ignoreTimingsReturn = {
300
+ ...passage,
301
+ verses: passage.verses.map((verse) => ({ ...verse, timings: null })),
302
+ };
303
+ /**
304
+ * Section's startTime is wrong (e.g. an earlier story's scripture hasn't
305
+ * loaded), so applying it as an offset would produce bogus lesson-relative
306
+ * timings. Strip verse timings instead, which disables karaoke until the
307
+ * section becomes trusted.
308
+ */
309
+ if (section.ignoreTimings)
310
+ return ignoreTimingsReturn;
311
+ const firstTimedVerse = passage?.verses.find((v) => v.timings);
312
+ if (!firstTimedVerse?.timings)
313
+ return ignoreTimingsReturn;
314
+ const offset = section.startTime +
315
+ getFtbDuration(section, lessonInfo) -
316
+ firstTimedVerse.timings[0];
317
+ return {
318
+ ...passage,
319
+ verses: passage.verses.map((verse) => {
399
320
  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 };
321
+ return verse;
322
+ return {
323
+ ...verse,
324
+ timings: [verse.timings[0] + offset, verse.timings[1] + offset],
325
+ };
326
+ }),
327
+ };
414
328
  });
415
329
  }