@storyteller-platform/align 0.1.48 → 0.1.49

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.
@@ -689,17 +689,22 @@ class Aligner {
689
689
  });
690
690
  const sentenceRanges = [];
691
691
  const chapterSentenceCounts = {};
692
+ const sentenceLengths = {};
692
693
  for (const alignedChapter of audioOrderedChapters) {
693
694
  sentenceRanges.push(...alignedChapter.sentenceRanges);
694
695
  const sentences = await this.getChapterSentences(
695
696
  alignedChapter.chapter.id
696
697
  );
697
698
  chapterSentenceCounts[alignedChapter.chapter.id] = sentences.length;
699
+ for (const [id, sentence] of (0, import_itertools.enumerate)(sentences)) {
700
+ sentenceLengths[(0, import_interpolateSentenceRanges.slotKey)({ chapterId: alignedChapter.chapter.id, id })] = sentence.text.length;
701
+ }
698
702
  }
699
703
  const interpolated = (0, import_interpolateSentenceRanges.interpolateSentenceRanges)(
700
704
  sentenceRanges,
701
705
  chapterSentenceCounts,
702
- this.audioFileDurations
706
+ this.audioFileDurations,
707
+ sentenceLengths
703
708
  );
704
709
  const expanded = (0, import_getSentenceRanges.expandEmptySentenceRanges)(interpolated);
705
710
  const collapsed = await (0, import_getSentenceRanges.collapseSentenceRangeGaps)(expanded);
@@ -43,7 +43,10 @@ import {
43
43
  getSentenceRanges,
44
44
  mapTranscriptionTimeline
45
45
  } from "./getSentenceRanges.js";
46
- import { interpolateSentenceRanges } from "./interpolateSentenceRanges.js";
46
+ import {
47
+ interpolateSentenceRanges,
48
+ slotKey
49
+ } from "./interpolateSentenceRanges.js";
47
50
  import { findBoundaries } from "./search.js";
48
51
  import { slugify } from "./slugify.js";
49
52
  import { TextFragmentFactory } from "./textFragments.js";
@@ -635,17 +638,22 @@ class Aligner {
635
638
  });
636
639
  const sentenceRanges = [];
637
640
  const chapterSentenceCounts = {};
641
+ const sentenceLengths = {};
638
642
  for (const alignedChapter of audioOrderedChapters) {
639
643
  sentenceRanges.push(...alignedChapter.sentenceRanges);
640
644
  const sentences = await this.getChapterSentences(
641
645
  alignedChapter.chapter.id
642
646
  );
643
647
  chapterSentenceCounts[alignedChapter.chapter.id] = sentences.length;
648
+ for (const [id, sentence] of enumerate(sentences)) {
649
+ sentenceLengths[slotKey({ chapterId: alignedChapter.chapter.id, id })] = sentence.text.length;
650
+ }
644
651
  }
645
652
  const interpolated = interpolateSentenceRanges(
646
653
  sentenceRanges,
647
654
  chapterSentenceCounts,
648
- this.audioFileDurations
655
+ this.audioFileDurations,
656
+ sentenceLengths
649
657
  );
650
658
  const expanded = expandEmptySentenceRanges(interpolated);
651
659
  const collapsed = await collapseSentenceRangeGaps(expanded);
@@ -18,55 +18,76 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var interpolateSentenceRanges_exports = {};
20
20
  __export(interpolateSentenceRanges_exports, {
21
- interpolateSentenceRanges: () => interpolateSentenceRanges
21
+ interpolateSentenceRanges: () => interpolateSentenceRanges,
22
+ slotKey: () => slotKey
22
23
  });
23
24
  module.exports = __toCommonJS(interpolateSentenceRanges_exports);
24
- function buildGapRanges(slots, left, right, audioFileDurations) {
25
+ function slotKey(slot) {
26
+ return `${slot.chapterId}\0${slot.id}`;
27
+ }
28
+ function slotWeight(slot, sentenceLengths) {
29
+ const length = sentenceLengths[slotKey(slot)];
30
+ return length && length > 0 ? length : 1;
31
+ }
32
+ function buildGapRanges(slots, left, right, audioFileDurations, sentenceLengths) {
25
33
  const n = slots.length;
26
34
  if (n === 0) return [];
35
+ const weights = slots.map((slot) => slotWeight(slot, sentenceLengths));
27
36
  if (left.audiofile === right.audiofile) {
28
37
  const span = right.time - left.time;
29
- return slots.map((slot, i) => ({
30
- ...slot,
31
- audiofile: left.audiofile,
32
- start: left.time + span * i / n,
33
- end: left.time + span * (i + 1) / n
34
- }));
38
+ const totalWeight2 = weights.reduce((a, b) => a + b, 0);
39
+ const result2 = [];
40
+ let cursor = left.time;
41
+ for (const [i, slot] of slots.entries()) {
42
+ const start = cursor;
43
+ const end = start + span * weights[i] / totalWeight2;
44
+ result2.push({ ...slot, audiofile: left.audiofile, start, end });
45
+ cursor = end;
46
+ }
47
+ return result2;
35
48
  }
36
49
  const leftDuration = audioFileDurations[left.audiofile] ?? left.time;
37
50
  const leftAvail = leftDuration - left.time;
38
51
  const rightAvail = right.time;
39
52
  const total = leftAvail + rightAvail;
40
- let n1 = total > 0 ? Math.round(n * (leftAvail / total)) : n;
41
- let n2 = n - n1;
42
- n1 = Math.max(0, n1);
43
- n2 = n - n1;
53
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
54
+ const leftWeightShare = total > 0 ? totalWeight * (leftAvail / total) : totalWeight;
55
+ let n1 = 0;
56
+ let accumulatedWeight = 0;
57
+ while (n1 < n && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
58
+ accumulatedWeight + weights[n1] / 2 <= leftWeightShare) {
59
+ accumulatedWeight += weights[n1];
60
+ n1++;
61
+ }
62
+ const n2 = n - n1;
44
63
  const result = [];
45
64
  if (n1 > 0) {
46
- for (let i = 0; i < n1; i++) {
47
- result.push({
48
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
49
- ...slots[i],
50
- audiofile: left.audiofile,
51
- start: left.time + leftAvail * i / n1,
52
- end: left.time + leftAvail * (i + 1) / n1
53
- });
65
+ const leftSlots = slots.slice(0, n1);
66
+ const leftWeights = weights.slice(0, n1);
67
+ const leftTotalWeight = leftWeights.reduce((a, b) => a + b, 0);
68
+ let cursor = left.time;
69
+ for (const [i, slot] of leftSlots.entries()) {
70
+ const start = cursor;
71
+ const end = start + leftAvail * leftWeights[i] / leftTotalWeight;
72
+ result.push({ ...slot, audiofile: left.audiofile, start, end });
73
+ cursor = end;
54
74
  }
55
75
  }
56
76
  if (n2 > 0) {
57
- for (let i = 0; i < n2; i++) {
58
- result.push({
59
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
60
- ...slots[n1 + i],
61
- audiofile: right.audiofile,
62
- start: rightAvail * i / n2,
63
- end: rightAvail * (i + 1) / n2
64
- });
77
+ const rightSlots = slots.slice(n1);
78
+ const rightWeights = weights.slice(n1);
79
+ const rightTotalWeight = rightWeights.reduce((a, b) => a + b, 0);
80
+ let cursor = 0;
81
+ for (const [i, slot] of rightSlots.entries()) {
82
+ const start = cursor;
83
+ const end = start + rightAvail * rightWeights[i] / rightTotalWeight;
84
+ result.push({ ...slot, audiofile: right.audiofile, start, end });
85
+ cursor = end;
65
86
  }
66
87
  }
67
88
  return result;
68
89
  }
69
- function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioFileDurations) {
90
+ function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioFileDurations, sentenceLengths = {}) {
70
91
  if (sentenceRanges.length === 0) return [];
71
92
  const result = [];
72
93
  const first = sentenceRanges[0];
@@ -77,7 +98,15 @@ function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioF
77
98
  }));
78
99
  const left = { time: 0, audiofile: first.audiofile };
79
100
  const right = { time: first.start, audiofile: first.audiofile };
80
- result.push(...buildGapRanges(slots, left, right, audioFileDurations));
101
+ result.push(
102
+ ...buildGapRanges(
103
+ slots,
104
+ left,
105
+ right,
106
+ audioFileDurations,
107
+ sentenceLengths
108
+ )
109
+ );
81
110
  }
82
111
  result.push(first);
83
112
  for (let idx = 1; idx < sentenceRanges.length; idx++) {
@@ -100,7 +129,15 @@ function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioF
100
129
  }
101
130
  }
102
131
  if (gapSlots.length > 0) {
103
- result.push(...buildGapRanges(gapSlots, left, right, audioFileDurations));
132
+ result.push(
133
+ ...buildGapRanges(
134
+ gapSlots,
135
+ left,
136
+ right,
137
+ audioFileDurations,
138
+ sentenceLengths
139
+ )
140
+ );
104
141
  }
105
142
  result.push(curr);
106
143
  }
@@ -114,11 +151,20 @@ function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioF
114
151
  const fileEnd = audioFileDurations[last.audiofile] ?? last.end;
115
152
  const left = { time: last.end, audiofile: last.audiofile };
116
153
  const right = { time: fileEnd, audiofile: last.audiofile };
117
- result.push(...buildGapRanges(slots, left, right, audioFileDurations));
154
+ result.push(
155
+ ...buildGapRanges(
156
+ slots,
157
+ left,
158
+ right,
159
+ audioFileDurations,
160
+ sentenceLengths
161
+ )
162
+ );
118
163
  }
119
164
  return result;
120
165
  }
121
166
  // Annotate the CommonJS export names for ESM import in node:
122
167
  0 && (module.exports = {
123
- interpolateSentenceRanges
168
+ interpolateSentenceRanges,
169
+ slotKey
124
170
  });
@@ -3,6 +3,19 @@ import '@storyteller-platform/ghost-story';
3
3
  import '@echogarden/text-segmentation';
4
4
  import '@storyteller-platform/transliteration';
5
5
 
6
+ /** A "slot" is a sentence that needs to be synthesised. */
7
+ type Slot = {
8
+ chapterId: string;
9
+ id: number;
10
+ };
11
+ /**
12
+ * Maps a slot to the character length of its sentence text, used to weight
13
+ * how much of a gap's time it should occupy. Keyed by `${chapterId}\0${id}`.
14
+ * Slots missing from this map (or with a non-positive length) fall back to
15
+ * a weight of 1, i.e. even division, same as before this map existed.
16
+ */
17
+ type SentenceLengths = Record<string, number>;
18
+ declare function slotKey(slot: Slot): string;
6
19
  /**
7
20
  * Given a sequence of sentence ranges from an entire book,
8
21
  * ordered by occurrence in audio, interpolates sentence ranges
@@ -18,6 +31,6 @@ import '@storyteller-platform/transliteration';
18
31
  * e.g. chapter001#325 -> chapter002#0, where
19
32
  * chapterSentenceCounts["chapter001"] === 330
20
33
  */
21
- declare function interpolateSentenceRanges(sentenceRanges: SentenceRange[], chapterSentenceCounts: Record<string, number>, audioFileDurations: Record<string, number>): SentenceRange[];
34
+ declare function interpolateSentenceRanges(sentenceRanges: SentenceRange[], chapterSentenceCounts: Record<string, number>, audioFileDurations: Record<string, number>, sentenceLengths?: SentenceLengths): SentenceRange[];
22
35
 
23
- export { interpolateSentenceRanges };
36
+ export { type SentenceLengths, interpolateSentenceRanges, slotKey };
@@ -3,6 +3,19 @@ import '@storyteller-platform/ghost-story';
3
3
  import '@echogarden/text-segmentation';
4
4
  import '@storyteller-platform/transliteration';
5
5
 
6
+ /** A "slot" is a sentence that needs to be synthesised. */
7
+ type Slot = {
8
+ chapterId: string;
9
+ id: number;
10
+ };
11
+ /**
12
+ * Maps a slot to the character length of its sentence text, used to weight
13
+ * how much of a gap's time it should occupy. Keyed by `${chapterId}\0${id}`.
14
+ * Slots missing from this map (or with a non-positive length) fall back to
15
+ * a weight of 1, i.e. even division, same as before this map existed.
16
+ */
17
+ type SentenceLengths = Record<string, number>;
18
+ declare function slotKey(slot: Slot): string;
6
19
  /**
7
20
  * Given a sequence of sentence ranges from an entire book,
8
21
  * ordered by occurrence in audio, interpolates sentence ranges
@@ -18,6 +31,6 @@ import '@storyteller-platform/transliteration';
18
31
  * e.g. chapter001#325 -> chapter002#0, where
19
32
  * chapterSentenceCounts["chapter001"] === 330
20
33
  */
21
- declare function interpolateSentenceRanges(sentenceRanges: SentenceRange[], chapterSentenceCounts: Record<string, number>, audioFileDurations: Record<string, number>): SentenceRange[];
34
+ declare function interpolateSentenceRanges(sentenceRanges: SentenceRange[], chapterSentenceCounts: Record<string, number>, audioFileDurations: Record<string, number>, sentenceLengths?: SentenceLengths): SentenceRange[];
22
35
 
23
- export { interpolateSentenceRanges };
36
+ export { type SentenceLengths, interpolateSentenceRanges, slotKey };
@@ -1,50 +1,70 @@
1
1
  import "../chunk-BIEQXUOY.js";
2
- function buildGapRanges(slots, left, right, audioFileDurations) {
2
+ function slotKey(slot) {
3
+ return `${slot.chapterId}\0${slot.id}`;
4
+ }
5
+ function slotWeight(slot, sentenceLengths) {
6
+ const length = sentenceLengths[slotKey(slot)];
7
+ return length && length > 0 ? length : 1;
8
+ }
9
+ function buildGapRanges(slots, left, right, audioFileDurations, sentenceLengths) {
3
10
  const n = slots.length;
4
11
  if (n === 0) return [];
12
+ const weights = slots.map((slot) => slotWeight(slot, sentenceLengths));
5
13
  if (left.audiofile === right.audiofile) {
6
14
  const span = right.time - left.time;
7
- return slots.map((slot, i) => ({
8
- ...slot,
9
- audiofile: left.audiofile,
10
- start: left.time + span * i / n,
11
- end: left.time + span * (i + 1) / n
12
- }));
15
+ const totalWeight2 = weights.reduce((a, b) => a + b, 0);
16
+ const result2 = [];
17
+ let cursor = left.time;
18
+ for (const [i, slot] of slots.entries()) {
19
+ const start = cursor;
20
+ const end = start + span * weights[i] / totalWeight2;
21
+ result2.push({ ...slot, audiofile: left.audiofile, start, end });
22
+ cursor = end;
23
+ }
24
+ return result2;
13
25
  }
14
26
  const leftDuration = audioFileDurations[left.audiofile] ?? left.time;
15
27
  const leftAvail = leftDuration - left.time;
16
28
  const rightAvail = right.time;
17
29
  const total = leftAvail + rightAvail;
18
- let n1 = total > 0 ? Math.round(n * (leftAvail / total)) : n;
19
- let n2 = n - n1;
20
- n1 = Math.max(0, n1);
21
- n2 = n - n1;
30
+ const totalWeight = weights.reduce((a, b) => a + b, 0);
31
+ const leftWeightShare = total > 0 ? totalWeight * (leftAvail / total) : totalWeight;
32
+ let n1 = 0;
33
+ let accumulatedWeight = 0;
34
+ while (n1 < n && // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
35
+ accumulatedWeight + weights[n1] / 2 <= leftWeightShare) {
36
+ accumulatedWeight += weights[n1];
37
+ n1++;
38
+ }
39
+ const n2 = n - n1;
22
40
  const result = [];
23
41
  if (n1 > 0) {
24
- for (let i = 0; i < n1; i++) {
25
- result.push({
26
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
27
- ...slots[i],
28
- audiofile: left.audiofile,
29
- start: left.time + leftAvail * i / n1,
30
- end: left.time + leftAvail * (i + 1) / n1
31
- });
42
+ const leftSlots = slots.slice(0, n1);
43
+ const leftWeights = weights.slice(0, n1);
44
+ const leftTotalWeight = leftWeights.reduce((a, b) => a + b, 0);
45
+ let cursor = left.time;
46
+ for (const [i, slot] of leftSlots.entries()) {
47
+ const start = cursor;
48
+ const end = start + leftAvail * leftWeights[i] / leftTotalWeight;
49
+ result.push({ ...slot, audiofile: left.audiofile, start, end });
50
+ cursor = end;
32
51
  }
33
52
  }
34
53
  if (n2 > 0) {
35
- for (let i = 0; i < n2; i++) {
36
- result.push({
37
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
38
- ...slots[n1 + i],
39
- audiofile: right.audiofile,
40
- start: rightAvail * i / n2,
41
- end: rightAvail * (i + 1) / n2
42
- });
54
+ const rightSlots = slots.slice(n1);
55
+ const rightWeights = weights.slice(n1);
56
+ const rightTotalWeight = rightWeights.reduce((a, b) => a + b, 0);
57
+ let cursor = 0;
58
+ for (const [i, slot] of rightSlots.entries()) {
59
+ const start = cursor;
60
+ const end = start + rightAvail * rightWeights[i] / rightTotalWeight;
61
+ result.push({ ...slot, audiofile: right.audiofile, start, end });
62
+ cursor = end;
43
63
  }
44
64
  }
45
65
  return result;
46
66
  }
47
- function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioFileDurations) {
67
+ function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioFileDurations, sentenceLengths = {}) {
48
68
  if (sentenceRanges.length === 0) return [];
49
69
  const result = [];
50
70
  const first = sentenceRanges[0];
@@ -55,7 +75,15 @@ function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioF
55
75
  }));
56
76
  const left = { time: 0, audiofile: first.audiofile };
57
77
  const right = { time: first.start, audiofile: first.audiofile };
58
- result.push(...buildGapRanges(slots, left, right, audioFileDurations));
78
+ result.push(
79
+ ...buildGapRanges(
80
+ slots,
81
+ left,
82
+ right,
83
+ audioFileDurations,
84
+ sentenceLengths
85
+ )
86
+ );
59
87
  }
60
88
  result.push(first);
61
89
  for (let idx = 1; idx < sentenceRanges.length; idx++) {
@@ -78,7 +106,15 @@ function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioF
78
106
  }
79
107
  }
80
108
  if (gapSlots.length > 0) {
81
- result.push(...buildGapRanges(gapSlots, left, right, audioFileDurations));
109
+ result.push(
110
+ ...buildGapRanges(
111
+ gapSlots,
112
+ left,
113
+ right,
114
+ audioFileDurations,
115
+ sentenceLengths
116
+ )
117
+ );
82
118
  }
83
119
  result.push(curr);
84
120
  }
@@ -92,10 +128,19 @@ function interpolateSentenceRanges(sentenceRanges, chapterSentenceCounts, audioF
92
128
  const fileEnd = audioFileDurations[last.audiofile] ?? last.end;
93
129
  const left = { time: last.end, audiofile: last.audiofile };
94
130
  const right = { time: fileEnd, audiofile: last.audiofile };
95
- result.push(...buildGapRanges(slots, left, right, audioFileDurations));
131
+ result.push(
132
+ ...buildGapRanges(
133
+ slots,
134
+ left,
135
+ right,
136
+ audioFileDurations,
137
+ sentenceLengths
138
+ )
139
+ );
96
140
  }
97
141
  return result;
98
142
  }
99
143
  export {
100
- interpolateSentenceRanges
144
+ interpolateSentenceRanges,
145
+ slotKey
101
146
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyteller-platform/align",
3
- "version": "0.1.48",
3
+ "version": "0.1.49",
4
4
  "description": "A library and CLI for automatically aligning audiobooks and EPUBs to produce Media Overlays",
5
5
  "author": "Shane Friedman",
6
6
  "license": "MIT",