Skip to content

Commit 6d23846

Browse files
committed
feat(speech): add configurable TTFA initial segment prioritization to text partitioner
1 parent 08ea026 commit 6d23846

1 file changed

Lines changed: 81 additions & 8 deletions

File tree

packages/react-native-executorch/src/extensions/speech/utils/textPartitioner.ts

Lines changed: 81 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,54 @@
11
type Tag = 'eos' | 'pause' | 'whitespace';
22

3+
// Punctuation Regex Patterns
34
const EOS_PATTERN = /[.?!;|¿¡]/;
45
const PAUSE_PATTERN = /[,:\u2014\u00AB\u00BB]/;
56
const WHITESPACE_PATTERN = /\s/;
67

8+
// Standard Partitioning Constants
79
const MAX_TARGET_PHRASE_LENGTH = 120;
810
const MIN_PARTITION_LIMIT = 10;
911
const DEVIATION_SCALING = 0.01;
1012
const TARGET_LENGTH_RATIO = 0.5;
11-
const SEPARATOR_PENALTY: Record<Tag, number> = { eos: 5, pause: 18, whitespace: 1000 };
13+
14+
// TTFA Optimization Constants
15+
const INITIAL_TARGET_LENGTH_RATIO = 0.15;
16+
const RAMP_CHARACTER_COUNT = 150;
17+
const INITIAL_SHORT_DEVIATION_SCALE = 0.1;
18+
19+
// Separator Breakpoint Penalties (eos = sentence end, pause = comma/colon, whitespace = word space)
20+
const DEFAULT_SEPARATOR_PENALTY: Record<Tag, number> = { eos: 5, pause: 80, whitespace: 1000 };
21+
22+
/**
23+
* Configuration options for text partitioning.
24+
* @category Types
25+
*/
26+
export type PartitionOptions = {
27+
/**
28+
* Whether to prioritize shorter initial segment lengths and scale up
29+
* progressively to minimize Time To First Audio (TTFA).
30+
* @default false
31+
*/
32+
readonly prioritizeInitialTtfa?: boolean;
33+
34+
/**
35+
* Scaling multiplier for the length deviation penalty when the first segment
36+
* is shorter than target length. Lower values reduce the length penalty for
37+
* short initial chunks when prioritizeInitialTtfa is true.
38+
* @default 0.1
39+
*/
40+
readonly initialShortDeviationScale?: number;
41+
42+
/**
43+
* Custom separator penalties for breakpoint tag types ('eos', 'pause',
44+
* 'whitespace'). Default: { eos: 5, pause: 80, whitespace: 1000 }.
45+
*/
46+
readonly separatorPenalties?: {
47+
readonly eos?: number;
48+
readonly pause?: number;
49+
readonly whitespace?: number;
50+
};
51+
};
1252

1353
function tagFromChar(char: string): Tag | undefined {
1454
if (EOS_PATTERN.test(char)) return 'eos';
@@ -17,6 +57,23 @@ function tagFromChar(char: string): Tag | undefined {
1757
return;
1858
}
1959

60+
function getTargetLength(
61+
startCharIdx: number,
62+
limit: number,
63+
prioritizeInitialTtfa: boolean
64+
): number {
65+
const maxTarget = Math.min(MAX_TARGET_PHRASE_LENGTH, limit * TARGET_LENGTH_RATIO);
66+
if (!prioritizeInitialTtfa) {
67+
return maxTarget;
68+
}
69+
const minTarget = Math.min(
70+
maxTarget,
71+
Math.max(MIN_PARTITION_LIMIT * 2.5, limit * INITIAL_TARGET_LENGTH_RATIO)
72+
);
73+
const progress = Math.min(1.0, startCharIdx / RAMP_CHARACTER_COUNT);
74+
return minTarget + (maxTarget - minTarget) * progress;
75+
}
76+
2077
function sliceAtCuts(text: string, cutIndices: number[]): string[] {
2178
const chunks: string[] = [];
2279
let startIdx = 0;
@@ -34,9 +91,10 @@ function sliceAtCuts(text: string, cutIndices: number[]): string[] {
3491
* @category Utils
3592
* @param text The input text to partition.
3693
* @param limit The character limit per partition.
94+
* @param options Optional configuration for TTFA prioritization and custom penalties.
3795
* @returns An array of partitioned text segments.
3896
*/
39-
export function partition(text: string, limit: number): string[] {
97+
export function partition(text: string, limit: number, options?: PartitionOptions): string[] {
4098
if (!text) {
4199
return [];
42100
}
@@ -45,6 +103,15 @@ export function partition(text: string, limit: number): string[] {
45103
throw new Error(`partition: limit ${limit} is below minimum ${MIN_PARTITION_LIMIT}`);
46104
}
47105

106+
const prioritizeInitialTtfa = options?.prioritizeInitialTtfa ?? false;
107+
const initialShortDeviationScale =
108+
options?.initialShortDeviationScale ?? INITIAL_SHORT_DEVIATION_SCALE;
109+
const separatorPenalty: Record<Tag, number> = {
110+
eos: options?.separatorPenalties?.eos ?? DEFAULT_SEPARATOR_PENALTY.eos,
111+
pause: options?.separatorPenalties?.pause ?? DEFAULT_SEPARATOR_PENALTY.pause,
112+
whitespace: options?.separatorPenalties?.whitespace ?? DEFAULT_SEPARATOR_PENALTY.whitespace,
113+
};
114+
48115
const breakpoints: { idx: number; tag: Tag }[] = [];
49116
let charIdx = 0;
50117
for (const char of text) {
@@ -57,8 +124,6 @@ export function partition(text: string, limit: number): string[] {
57124
breakpoints.push({ idx: text.length - 1, tag: 'eos' });
58125
}
59126

60-
const targetLength = Math.min(MAX_TARGET_PHRASE_LENGTH, limit * TARGET_LENGTH_RATIO);
61-
62127
const length = (currBreakIdx: number, prevBreakIdx: number): number => {
63128
if (prevBreakIdx < 0) return breakpoints[currBreakIdx]!.idx + 1; // no previous cuts
64129
return breakpoints[currBreakIdx]!.idx - breakpoints[prevBreakIdx]!.idx;
@@ -67,10 +132,18 @@ export function partition(text: string, limit: number): string[] {
67132
const cost = (currBreakIdx: number, prevBreakIdx: number): number => {
68133
const len = length(currBreakIdx, prevBreakIdx);
69134
if (len > limit) return Infinity;
70-
return (
71-
SEPARATOR_PENALTY[breakpoints[currBreakIdx]!.tag] +
72-
DEVIATION_SCALING * (len - targetLength) ** 2
73-
);
135+
136+
const startCharIdx = prevBreakIdx < 0 ? 0 : breakpoints[prevBreakIdx]!.idx + 1;
137+
const targetLength = getTargetLength(startCharIdx, limit, prioritizeInitialTtfa);
138+
139+
const isFirstChunk = prevBreakIdx < 0;
140+
const diff = len - targetLength;
141+
const devScale =
142+
prioritizeInitialTtfa && isFirstChunk && diff < 0
143+
? DEVIATION_SCALING * initialShortDeviationScale
144+
: DEVIATION_SCALING;
145+
146+
return separatorPenalty[breakpoints[currBreakIdx]!.tag] + devScale * diff ** 2;
74147
};
75148

76149
// Forward DP Recurrence Relation

0 commit comments

Comments
 (0)