waha-shared 1.0.307 → 1.0.308

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 (102) hide show
  1. package/dist/data/areas/areas.json +48 -50
  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 +314 -581
  7. package/dist/data/bibleAudios/bibleAudios.schema.json +8 -17
  8. package/dist/data/bibleAudios/bibleAudios.zod.d.ts +2 -43
  9. package/dist/data/bibleAudios/bibleAudios.zod.js +7 -16
  10. package/dist/data/bibleAudios/index.d.ts +1 -2
  11. package/dist/data/bibleBooks/bibleBooks.json +782 -784
  12. package/dist/data/bibleStatuses/bibleStatuses.json +8284 -7785
  13. package/dist/data/bibleStatuses/bibleStatuses.schema.json +0 -6
  14. package/dist/data/bibleStatuses/bibleStatuses.zod.d.ts +0 -1
  15. package/dist/data/bibleStatuses/bibleStatuses.zod.js +0 -4
  16. package/dist/data/bibleStatuses/index.d.ts +0 -1
  17. package/dist/data/bibleTexts/bibleTexts.json +184 -269
  18. package/dist/data/bibleTexts/bibleTexts.schema.json +0 -7
  19. package/dist/data/bibleTexts/bibleTexts.zod.d.ts +0 -28
  20. package/dist/data/bibleTexts/bibleTexts.zod.js +3 -10
  21. package/dist/data/bibleTexts/index.d.ts +0 -2
  22. package/dist/data/clones/clones.json +0 -2
  23. package/dist/data/countries/countries.json +480 -480
  24. package/dist/data/countriesAndLanguages/countriesAndLanguages.json +6798 -6800
  25. package/dist/data/crowdinLanguages/crowdinLanguages.json +1438 -1440
  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 +69 -86
  31. package/dist/data/curriculumQuestions/curriculumQuestions.json +192 -194
  32. package/dist/data/curriculumTopics/curriculumTopics.json +8 -51
  33. package/dist/data/dblAudioLicenses/dblAudioLicenses.json +7 -36
  34. package/dist/data/dblTextLicenses/dblTextLicenses.json +4 -4
  35. package/dist/data/iosVoiceOverLanguages/iosVoiceOverLanguages.json +1 -1
  36. package/dist/data/iso6933LanguageCodes/iso6933LanguageCodes.json +1 -1
  37. package/dist/data/languageAssets/index.d.ts +1 -0
  38. package/dist/data/languageAssets/index.js +7 -0
  39. package/dist/data/languageAssets/languageAssets.json +45406 -0
  40. package/dist/data/languageAssets/languageAssets.schema.json +19 -0
  41. package/dist/data/languageAssets/languageAssets.zod.d.ts +3 -0
  42. package/dist/data/languageAssets/languageAssets.zod.js +7 -0
  43. package/dist/data/languages/index.d.ts +10 -20
  44. package/dist/data/languages/languages.json +2008 -2983
  45. package/dist/data/languages/languages.schema.json +47 -93
  46. package/dist/data/languages/languages.zod.d.ts +30 -93
  47. package/dist/data/languages/languages.zod.js +37 -59
  48. package/dist/data/lessonPauses/index.d.ts +1 -0
  49. package/dist/data/lessonPauses/lessonPauses.json +3 -2
  50. package/dist/data/lessonPauses/lessonPauses.schema.json +11 -1
  51. package/dist/data/lessonPauses/lessonPauses.zod.d.ts +1 -0
  52. package/dist/data/lessonPauses/lessonPauses.zod.js +3 -0
  53. package/dist/data/mediaDurations/mediaDurations.json +4231 -33446
  54. package/dist/data/mediaDurations/mediaDurations.schema.json +1 -1
  55. package/dist/data/mediaDurations/mediaDurations.zod.js +1 -1
  56. package/dist/data/notification/index.d.ts +1 -1
  57. package/dist/data/notification/notification.json +169 -93
  58. package/dist/data/phoneLanguages/phoneLanguages.json +635 -637
  59. package/dist/data/questions/questions.json +147 -149
  60. package/dist/data/releaseNotes/releaseNotes.json +124 -126
  61. package/dist/data/screenshots/screenshots.json +1 -1
  62. package/dist/data/scripts/scripts.json +1 -6
  63. package/dist/data/sets/index.d.ts +3 -3
  64. package/dist/data/sets/sets.json +476 -1766
  65. package/dist/data/sets/sets.schema.json +1 -1
  66. package/dist/data/sets/sets.zod.d.ts +3 -3
  67. package/dist/data/sets/sets.zod.js +6 -3
  68. package/dist/data/translationsApp/index.d.ts +0 -1
  69. package/dist/data/translationsApp/translationsApp.json +1582 -1623
  70. package/dist/data/translationsApp/translationsApp.schema.json +0 -1
  71. package/dist/data/translationsApp/translationsApp.zod.d.ts +0 -2
  72. package/dist/data/translationsApp/translationsApp.zod.js +0 -1
  73. package/dist/data/translationsFtb/translationsFtb.json +613 -69
  74. package/dist/data/translationsIntroduction/translationsIntroduction.json +1 -1
  75. package/dist/data/translationsQuestion/translationsQuestion.json +33 -65
  76. package/dist/data/translationsSet/translationsSet.json +19565 -30302
  77. package/dist/data/translationsSet/translationsSet.schema.json +4 -3
  78. package/dist/data/translationsSet/translationsSet.zod.js +4 -4
  79. package/dist/data/translationsSolarSpeaker/translationsSolarSpeaker.json +1 -1
  80. package/dist/data/translationsSpokenQuestion/translationsSpokenQuestion.json +33 -65
  81. package/dist/data/youtubePlaylists/youtubePlaylists.json +10 -10
  82. package/dist/data/youtubeVideos/youtubeVideos.json +82 -82
  83. package/dist/functions/languages.d.ts +7 -6
  84. package/dist/functions/languages.js +53 -51
  85. package/dist/functions/scripturePassages.d.ts +49 -34
  86. package/dist/functions/scripturePassages.js +304 -167
  87. package/dist/functions/sets.d.ts +52 -13
  88. package/dist/functions/sets.js +205 -193
  89. package/dist/types/bibleChapters.d.ts +2 -2
  90. package/dist/types/bibleChapters.js +1 -1
  91. package/dist/types/languages.d.ts +9 -39
  92. package/dist/types/languages.js +0 -2
  93. package/dist/types/sets.d.ts +11 -21
  94. package/package.json +1 -1
  95. package/dist/data/firebase.d.ts +0 -1
  96. package/dist/data/firebase.js +0 -4
  97. package/dist/functions/bibleBooks.d.ts +0 -16
  98. package/dist/functions/bibleBooks.js +0 -101
  99. package/dist/functions/bibles.d.ts +0 -44
  100. package/dist/functions/bibles.js +0 -291
  101. package/dist/functions/utils.d.ts +0 -2
  102. package/dist/functions/utils.js +0 -8
@@ -1,33 +1,61 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseVerseRange = void 0;
3
+ exports.parseVerseRange = parseVerseRange;
4
4
  exports.getChapterUrl = getChapterUrl;
5
5
  exports.getPassagesString = getPassagesString;
6
6
  exports.getScripturePassage = getScripturePassage;
7
7
  exports.getLessonScripture = getLessonScripture;
8
+ exports.parseBibleSnapshot = parseBibleSnapshot;
8
9
  exports.verseToSuperscript = verseToSuperscript;
9
- exports.getFtbDuration = getFtbDuration;
10
- exports.enrichSections = enrichSections;
10
+ exports.getLessonPauses = getLessonPauses;
11
11
  exports.normalizeVerseTimings = normalizeVerseTimings;
12
12
  const bibleStatuses_1 = require("../data/bibleStatuses");
13
- const firebase_1 = require("../data/firebase");
13
+ const lessonPauses_1 = require("../data/lessonPauses");
14
14
  const mediaDurations_1 = require("../data/mediaDurations");
15
+ const specialIds_1 = require("../data/specialIds");
15
16
  const languages_1 = require("../functions/languages");
16
- const sets_1 = require("../types/sets");
17
- const bibleBooks_1 = require("./bibleBooks");
18
- const bibles_1 = require("./bibles");
19
- var bibleBooks_2 = require("./bibleBooks");
20
- Object.defineProperty(exports, "parseVerseRange", { enumerable: true, get: function () { return bibleBooks_2.parseVerseRange; } });
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
+ }
21
47
  function getChapterUrl(params) {
22
- const book = 'book' in params ? params.book : (0, bibleBooks_1.parseVerseRange)(params.passageId).book;
23
- const chapter = 'chapter' in params
24
- ? params.chapter
25
- : (0, bibleBooks_1.parseVerseRange)(params.passageId).chapter;
26
- return (firebase_1.firebaseUrl +
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 +
27
55
  encodeURIComponent('audio_bibles' +
28
56
  `/${params.bibleAudioId}` +
29
- `/${book}` +
30
- `/${book}_${chapter.padStart(3, '0')}.mp3`) +
57
+ `/${startBook}` +
58
+ `/${startBook}_${startChapter.padStart(3, '0')}.mp3`) +
31
59
  `?alt=media`);
32
60
  }
33
61
  /** Extracts verses from a chapter based on verse numbers */
@@ -57,90 +85,170 @@ function extractVersesFromChapter(chapter, startVerse, endVerse) {
57
85
  * ["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"
58
86
  * ["JHN.1.1"] -> "John 1: 1"
59
87
  */
60
- function getPassagesString(passages) {
88
+ function getPassagesString(passageIds, language) {
89
+ if (!passageIds || passageIds.length === 0)
90
+ return '';
61
91
  const parts = [];
62
92
  let currentBook = null;
63
- for (const passage of passages) {
64
- const bibleInfo = (0, bibles_1.getBibleInfo)(passage.bibleId);
65
- if (!bibleInfo)
66
- continue;
67
- const { book, chapter, startVerse, endVerse } = (0, bibleBooks_1.parseVerseRange)(passage.passageId);
68
- const bookName = bibleInfo.bookNames[book];
69
- if (!bookName)
70
- continue;
71
- const verseRange = startVerse === endVerse ? startVerse : `${startVerse}-${endVerse}`;
72
- if (currentBook !== book) {
73
- currentBook = book;
74
- parts.push((0, languages_1.numerals)(`${bookName} ${chapter}: ${verseRange}`, bibleInfo.language.script));
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
+ }
75
121
  }
76
122
  else {
77
- parts.push((0, languages_1.numerals)(`${chapter}: ${verseRange}`, bibleInfo.language.script));
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
136
  }
79
137
  }
80
- return parts.join(', ');
138
+ return (0, languages_1.numerals)(parts.join(', '), language.script.name);
81
139
  }
82
140
  /** Gets a scripture passage by extracting verses from chapters documents. */
83
- async function getScripturePassage({ bibleId, passageId, getChapter, noTextAvailableString, }) {
84
- const bibleInfo = (0, bibles_1.getBibleInfo)(bibleId);
85
- const { startVerse, endVerse, chapterId } = (0, bibleBooks_1.parseVerseRange)(passageId);
86
- const noTextAvailableReturn = {
87
- bibleId,
88
- header: getPassagesString([{ passageId, bibleId }]),
89
- passageId,
90
- verses: [],
91
- };
92
- if (!bibleInfo)
93
- return noTextAvailableReturn;
94
- const returnExpected = bibleInfo.availableTextChapters.includes(chapterId) ||
95
- bibleInfo.availableTimingsChapters.includes(chapterId);
96
- /**
97
- * In this case, we know the bible does not have text for this chapter, so the
98
- * user will see a placeholder verse.
99
- */
100
- if (!returnExpected) {
101
- noTextAvailableReturn.verses = [
102
- { text: noTextAvailableString, verseId: '' },
103
- ];
104
- return noTextAvailableReturn;
105
- }
141
+ async function getScripturePassage({ bibleId, bibleFallbackId, passageId, getChapter, languageInfo, }) {
106
142
  try {
143
+ const { startBook, startChapter, startVerse, endBook, endChapter, endVerse, } = parseVerseRange(passageId);
107
144
  const startVerseNum = parseInt(startVerse);
108
145
  const endVerseNum = parseInt(endVerse);
109
- const chapter = await getChapter({ chapterId, bibleId });
110
- if (!chapter)
111
- return noTextAvailableReturn;
112
- const verses = extractVersesFromChapter(chapter, startVerseNum, endVerseNum);
113
- if (verses.length === 0) {
114
- console.warn(`No verses found for passage ${bibleId};${passageId}`);
115
- return noTextAvailableReturn;
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
+ }
161
+ return {
162
+ passageId,
163
+ bibleId,
164
+ header: getPassagesString([passageId], languageInfo),
165
+ verses,
166
+ };
116
167
  }
117
- if (!verses.some((v) => v.text))
118
- verses[0].text = noTextAvailableString;
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
+ });
119
206
  return {
120
207
  passageId,
121
- bibleId: chapter.translationId,
122
- header: getPassagesString([{ passageId, bibleId }]),
123
- verses,
208
+ bibleId,
209
+ header: getPassagesString([passageId], languageInfo),
210
+ verses: allVerses,
124
211
  };
125
212
  }
126
213
  catch (error) {
127
214
  console.error(`Error getting scripture passage ${passageId} in ${bibleId}`, error);
128
- throw error;
215
+ return;
129
216
  }
130
217
  }
131
- /** Gets all scripture passages for a lesson. */
132
- async function getLessonScripture({ getChapter, lessonInfo, noTextAvailableString, }) {
133
- return Promise.all(lessonInfo.sections
134
- .filter((s) => s.chapter === sets_1.Chapter.STORY)
135
- .map((s) => {
136
- const [bibleId, passageId] = s.id.split(';');
137
- return getScripturePassage({
138
- bibleId,
139
- passageId,
140
- getChapter,
141
- noTextAvailableString,
142
- });
143
- }));
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
+ }
144
252
  }
145
253
  /**
146
254
  * Converts a verse number to a superscript string. This is useful for
@@ -177,102 +285,131 @@ function verseToSuperscript(num) {
177
285
  .join('');
178
286
  }
179
287
  /**
180
- * Returns the FTB audio duration + afterFtb pause for a section, or 0 if the
181
- * section has no FTB.
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.
182
292
  */
183
- function getFtbDuration(section, lessonInfo) {
184
- if (!section.ftbFileName)
185
- return 0;
186
- const ftbDuration = mediaDurations_1.mediaDurations[lessonInfo.contentLanguages.ftbs]?.[section.ftbFileName];
187
- if (ftbDuration == null) {
188
- console.warn(`FTB duration not found for ${section.ftbFileName}`);
189
- }
190
- return (ftbDuration ?? 0) + lessonInfo.lessonPauses.afterFtb;
191
- }
192
- /**
193
- * Fills in `length` (for verse-timed story sections) and `startTime` (for all
194
- * sections) by accumulating forward through the lesson. After this, every
195
- * section has both fields set.
196
- */
197
- function enrichSections(lessonInfo, scripture) {
198
- const storySections = lessonInfo.sections.filter((s) => s.chapter === sets_1.Chapter.STORY);
199
- // Forward pass: compute lengths and start times for all non-application
200
- // sections.
201
- let currentTime = 0;
202
- const enriched = lessonInfo.sections.map((section) => {
203
- if (!section.chapter)
204
- return { ...section, startTime: 0, length: 0 };
205
- currentTime += section.pauseBefore ?? 0;
206
- let length;
207
- const startTime = currentTime;
208
- if (section.chapter !== sets_1.Chapter.STORY) {
209
- length = mediaDurations_1.mediaDurations[section.languageId]?.[section.fileName];
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;
210
308
  }
211
309
  else {
212
- currentTime += getFtbDuration(section, lessonInfo);
213
- const [bibleId, passageId] = section.id.split(';');
214
- length = bibleStatuses_1.bibleStatuses[bibleId]?.customPassages?.[passageId];
215
- if (!length) {
216
- const passage = scripture[storySections.indexOf(section)];
217
- if (passage) {
218
- const firstTimed = passage.verses.find((v) => v.timings);
219
- const lastTimed = [...passage.verses].reverse().find((v) => v.timings);
220
- if (firstTimed?.timings && lastTimed?.timings) {
221
- length = lastTimed.timings[1] - firstTimed.timings[0];
222
- }
223
- }
224
- }
225
- }
226
- if (length == null) {
227
- console.warn(`Missing duration for section ${section.id} (${section.chapter}, ${section.fileName})`);
228
- }
229
- currentTime += length ?? 0;
230
- return { ...section, length: length ?? 0, startTime };
231
- });
232
- // Backward pass: compute application section start times from the end of the
233
- // lesson file, so they are always correct even if story durations are
234
- // slightly off.
235
- const fullFileName = lessonInfo.type === 'dbs'
236
- ? lessonInfo.full.localFileName
237
- : lessonInfo.video.localFileName;
238
- const totalDuration = mediaDurations_1.mediaDurations[lessonInfo.languageId]?.[fullFileName] ?? 0;
239
- if (totalDuration > 0) {
240
- let timeFromEnd = totalDuration;
241
- const applicationSections = enriched.filter((s) => s.chapter === sets_1.Chapter.APPLICATION);
242
- for (const section of [...applicationSections].reverse()) {
243
- timeFromEnd -= section.length;
244
- section.startTime = timeFromEnd;
245
- timeFromEnd -= section.pauseBefore ?? 0;
310
+ chunks[chunks.length - 1].verses.push(verse);
246
311
  }
247
312
  }
248
- return enriched;
313
+ return chunks;
249
314
  }
250
315
  /**
251
- * Converts verse timings from chapter-relative to lesson-relative using each
252
- * section's `startTime` (set by {@link enrichSections}).
316
+ * Some lessons were built using a system that used slightly different pause
317
+ * values.
253
318
  */
254
- function normalizeVerseTimings({ enrichedSections, scripture, lessonInfo, }) {
255
- const storySections = enrichedSections.filter((s) => s.chapter === sets_1.Chapter.STORY);
256
- return scripture.map((passage, passageIndex) => {
257
- const section = storySections[passageIndex];
258
- if (!section)
259
- return passage;
260
- const firstTimedVerse = passage.verses.find((v) => v.timings);
261
- if (!firstTimedVerse?.timings)
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)
262
361
  return passage;
263
- const offset = section.startTime +
264
- getFtbDuration(section, lessonInfo) -
265
- firstTimedVerse.timings[0];
266
- return {
267
- ...passage,
268
- verses: passage.verses.map((verse) => {
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) {
269
399
  if (!verse.timings)
270
- return verse;
271
- return {
272
- ...verse,
273
- timings: [verse.timings[0] + offset, verse.timings[1] + offset],
274
- };
275
- }),
276
- };
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 };
277
414
  });
278
415
  }
@@ -1,15 +1,17 @@
1
1
  import { TranslationsApp } from '../data/translationsApp/translationsApp.zod';
2
2
  import type { LanguageInfo, MeetTranslations } from '../types/languages';
3
- import { DbsInfo, EnrichedSection, Lesson, LessonInfo, SetInfo, VideoInfo } from '../types/sets';
4
- export declare function getSetInfo({ setId, languageId, setIds, t, }: {
3
+ import { ScripturePassage } from '../types/scripturePassages';
4
+ import { DbsInfo, Lesson, LessonInfo, SetInfo, VideoInfo } from '../types/sets';
5
+ export declare const firebaseUrl = "https://firebasestorage.googleapis.com/v0/b/waha-app-db.appspot.com/o/";
6
+ export declare function getSetInfo({ setId, meetLanguageId, setIds, t, }: {
5
7
  setId: string;
6
- languageId: string;
8
+ meetLanguageId: string;
7
9
  t: MeetTranslations & TranslationsApp[string];
8
10
  setIds: string[];
9
11
  }): SetInfo | undefined;
10
- export declare function getLessonInfo({ lessonId, languageInfo, setInfo, t, useSpokenQuestions, }: {
12
+ export declare function getLessonInfo({ lessonId, meetLanguageInfo, setInfo, t, useSpokenQuestions, }: {
11
13
  lessonId: Lesson['lessonId'] | undefined;
12
- languageInfo: LanguageInfo;
14
+ meetLanguageInfo: LanguageInfo;
13
15
  setInfo?: SetInfo;
14
16
  t: MeetTranslations & TranslationsApp[string];
15
17
  useSpokenQuestions?: boolean;
@@ -18,13 +20,50 @@ export declare function convertSToString(time: number): string;
18
20
  /** Determines whether a lesson should be visible in Waha. */
19
21
  export declare function shouldShowLesson(lessonInfo: LessonInfo | undefined, languageInfo: LanguageInfo): boolean;
20
22
  /**
21
- * Validates that the computed lesson duration (from section lengths) matches
22
- * the actual audio file duration. Sections must be enriched (all have `length`)
23
- * via {@link enrichSections} before calling this.
24
- *
25
- * @returns The difference between the computed length and the total duration.
23
+ * Calculate start times for each section based on question durations and total
24
+ * audio duration. Must be called after audio is loaded to get accurate total
25
+ * duration.
26
+ */
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.
26
60
  */
27
- export declare function getExpectedLessonDuration({ enrichedSections, lessonInfo, }: {
28
- enrichedSections: EnrichedSection[];
61
+ export declare function getSupportsVerseHighlight({ lessonInfo, scripture, totalDuration, }: {
29
62
  lessonInfo: DbsInfo | VideoInfo;
30
- }): number;
63
+ scripture: Array<ScripturePassage | undefined>;
64
+ totalDuration: number | undefined;
65
+ }): {
66
+ computedLength: number;
67
+ diff: number;
68
+ passed: boolean;
69
+ };