@plasius/ai-speech 0.1.1 → 0.1.2
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.
- package/CHANGELOG.md +15 -0
- package/README.md +77 -3
- package/dist/index.cjs +374 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +140 -2
- package/dist/index.d.ts +140 -2
- package/dist/index.js +350 -2
- package/dist/index.js.map +1 -1
- package/docs/adrs/adr-0002-tts-cache-and-address-policy-contracts.md +34 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -18,9 +18,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
18
18
|
- **Security**
|
|
19
19
|
- (placeholder)
|
|
20
20
|
|
|
21
|
+
## [0.1.2] - 2026-06-01
|
|
22
|
+
|
|
23
|
+
- **Added**
|
|
24
|
+
- rollout-control, player-address, cache-planning, and voice-tier contracts for tracked TTS caching work
|
|
25
|
+
|
|
26
|
+
- **Changed**
|
|
27
|
+
- the package descriptor now points at the tracked TTS cache feature flag instead of the original scaffold placeholder
|
|
28
|
+
|
|
29
|
+
- **Fixed**
|
|
30
|
+
- default player-address rendering now redacts user/account identifiers before cache planning
|
|
31
|
+
|
|
32
|
+
- **Security**
|
|
33
|
+
- near-text reuse is now fail-closed for private, moderated, or identifier-bearing speech unless the caller narrows scope safely
|
|
34
|
+
|
|
21
35
|
## [0.1.1] - 2026-05-13
|
|
22
36
|
|
|
23
37
|
- Added initial public package scaffold with governance, legal, docs, build, test, and pack-check baselines.
|
|
24
38
|
|
|
25
39
|
|
|
26
40
|
[0.1.1]: https://github.com/Plasius-LTD/ai-speech/releases/tag/v0.1.1
|
|
41
|
+
[0.1.2]: https://github.com/Plasius-LTD/ai-speech/releases/tag/v0.1.2
|
package/README.md
CHANGED
|
@@ -4,7 +4,14 @@ Speech orchestration, TTS cache contracts, STT/TTS routing, and voice tier polic
|
|
|
4
4
|
|
|
5
5
|
## Scope
|
|
6
6
|
|
|
7
|
-
This package
|
|
7
|
+
This package defines public contracts for:
|
|
8
|
+
|
|
9
|
+
- rollout-controlled TTS cache planning
|
|
10
|
+
- cache-safe player address rendering
|
|
11
|
+
- development, standard, and premium-character voice tier routing
|
|
12
|
+
- cache telemetry payloads
|
|
13
|
+
|
|
14
|
+
It does not fetch remote feature flags, talk to providers, or store cache entries directly. Host applications and provider adapters remain responsible for runtime I/O.
|
|
8
15
|
|
|
9
16
|
## Install
|
|
10
17
|
|
|
@@ -15,11 +22,78 @@ npm install @plasius/ai-speech
|
|
|
15
22
|
## Usage
|
|
16
23
|
|
|
17
24
|
```ts
|
|
18
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
AI_SPEECH_FEATURE_FLAGS,
|
|
27
|
+
planAiSpeechCache,
|
|
28
|
+
renderAiSpeechText,
|
|
29
|
+
resolveAiSpeechVoiceTier,
|
|
30
|
+
} from "@plasius/ai-speech";
|
|
31
|
+
|
|
32
|
+
const rendered = renderAiSpeechText({
|
|
33
|
+
textTemplate: "Welcome back, {playerAddress}.",
|
|
34
|
+
playerAddress: {
|
|
35
|
+
source: "user-name",
|
|
36
|
+
rawValue: "Captain Zephod",
|
|
37
|
+
},
|
|
38
|
+
});
|
|
19
39
|
|
|
20
|
-
|
|
40
|
+
const cachePlan = planAiSpeechCache({
|
|
41
|
+
utteranceClass: "player-address",
|
|
42
|
+
textTemplate: "Welcome back, {playerAddress}.",
|
|
43
|
+
playerAddress: {
|
|
44
|
+
source: "user-name",
|
|
45
|
+
rawValue: "Captain Zephod",
|
|
46
|
+
},
|
|
47
|
+
voice: {
|
|
48
|
+
providerId: "openai",
|
|
49
|
+
modelId: "gpt-tts-1",
|
|
50
|
+
voiceId: "narrator",
|
|
51
|
+
locale: "en-GB",
|
|
52
|
+
format: "mp3",
|
|
53
|
+
pronunciationVersion: "2026-05",
|
|
54
|
+
cachePermission: "exact-and-near",
|
|
55
|
+
},
|
|
56
|
+
actorScopeKey: "user-42",
|
|
57
|
+
featureFlags: {
|
|
58
|
+
[AI_SPEECH_FEATURE_FLAGS.ttsCache]: true,
|
|
59
|
+
[AI_SPEECH_FEATURE_FLAGS.ttsNearReuse]: true,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const voiceTier = resolveAiSpeechVoiceTier({
|
|
64
|
+
environment: "production",
|
|
65
|
+
requestedTier: "premium-character",
|
|
66
|
+
featureFlags: {
|
|
67
|
+
[AI_SPEECH_FEATURE_FLAGS.premiumCharacters]: false,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
console.log(rendered.renderText);
|
|
72
|
+
console.log(cachePlan.mode);
|
|
73
|
+
console.log(voiceTier.resolvedTier);
|
|
21
74
|
```
|
|
22
75
|
|
|
76
|
+
## Rollout Controls
|
|
77
|
+
|
|
78
|
+
- `ai.tts.cache.enabled`: master switch for exact-cache planning
|
|
79
|
+
- `ai.tts.near-reuse.enabled`: enables near-text reuse only for approved low-risk utterance classes
|
|
80
|
+
- `ai.tts.premium-characters.enabled`: enables premium character voices in production routes
|
|
81
|
+
|
|
82
|
+
Default behavior is fail-closed when the caller does not provide a remote flag snapshot.
|
|
83
|
+
|
|
84
|
+
## Cache Safety Rules
|
|
85
|
+
|
|
86
|
+
- User names, account handles, and user-renamed character names are redacted to a generic label such as `Player`.
|
|
87
|
+
- Near-text reuse is blocked for redacted identifiers, personal data, moderation notices, and private player context.
|
|
88
|
+
- Sensitive speech can only produce an actor-scoped exact cache plan when the caller provides an explicit `actorScopeKey`.
|
|
89
|
+
- Provider adapters must declare whether they allow no cache, exact-only cache, or exact-plus-near cache decisions.
|
|
90
|
+
|
|
91
|
+
## Rollback
|
|
92
|
+
|
|
93
|
+
- Disable `ai.tts.near-reuse.enabled` to force safe callers back to exact-cache planning.
|
|
94
|
+
- Disable `ai.tts.cache.enabled` to bypass cache planning entirely.
|
|
95
|
+
- Disable `ai.tts.premium-characters.enabled` to downgrade premium character requests to standard voices.
|
|
96
|
+
|
|
23
97
|
## Development
|
|
24
98
|
|
|
25
99
|
```bash
|
package/dist/index.cjs
CHANGED
|
@@ -20,15 +20,363 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AI_SPEECH_CACHE_MODES: () => AI_SPEECH_CACHE_MODES,
|
|
24
|
+
AI_SPEECH_CACHE_SCOPES: () => AI_SPEECH_CACHE_SCOPES,
|
|
25
|
+
AI_SPEECH_DEFAULT_PLAYER_LABEL: () => AI_SPEECH_DEFAULT_PLAYER_LABEL,
|
|
26
|
+
AI_SPEECH_ENVIRONMENTS: () => AI_SPEECH_ENVIRONMENTS,
|
|
23
27
|
AI_SPEECH_ENV_PREFIX: () => AI_SPEECH_ENV_PREFIX,
|
|
28
|
+
AI_SPEECH_FEATURE_FLAGS: () => AI_SPEECH_FEATURE_FLAGS,
|
|
24
29
|
AI_SPEECH_FEATURE_FLAG_ID: () => AI_SPEECH_FEATURE_FLAG_ID,
|
|
30
|
+
AI_SPEECH_NEAR_REUSE_SAFE_CLASSES: () => AI_SPEECH_NEAR_REUSE_SAFE_CLASSES,
|
|
25
31
|
AI_SPEECH_PACKAGE: () => AI_SPEECH_PACKAGE,
|
|
26
|
-
|
|
32
|
+
AI_SPEECH_PLAYER_ADDRESS_SOURCES: () => AI_SPEECH_PLAYER_ADDRESS_SOURCES,
|
|
33
|
+
AI_SPEECH_PROVIDER_CACHE_PERMISSIONS: () => AI_SPEECH_PROVIDER_CACHE_PERMISSIONS,
|
|
34
|
+
AI_SPEECH_ROLLOUTS: () => AI_SPEECH_ROLLOUTS,
|
|
35
|
+
AI_SPEECH_ROLLOUT_EVALUATORS: () => AI_SPEECH_ROLLOUT_EVALUATORS,
|
|
36
|
+
AI_SPEECH_ROLLOUT_FALLBACK_MODES: () => AI_SPEECH_ROLLOUT_FALLBACK_MODES,
|
|
37
|
+
AI_SPEECH_UTTERANCE_CLASSES: () => AI_SPEECH_UTTERANCE_CLASSES,
|
|
38
|
+
AI_SPEECH_VOICE_TIERS: () => AI_SPEECH_VOICE_TIERS,
|
|
39
|
+
createAiSpeechCacheKey: () => createAiSpeechCacheKey,
|
|
40
|
+
createAiSpeechNearReuseFingerprint: () => createAiSpeechNearReuseFingerprint,
|
|
41
|
+
isAiSpeechFeatureEnabled: () => isAiSpeechFeatureEnabled,
|
|
42
|
+
isAiSpeechNearReuseSafeClass: () => isAiSpeechNearReuseSafeClass,
|
|
43
|
+
normalizeAiSpeechText: () => normalizeAiSpeechText,
|
|
44
|
+
packageDescriptor: () => packageDescriptor,
|
|
45
|
+
planAiSpeechCache: () => planAiSpeechCache,
|
|
46
|
+
renderAiSpeechText: () => renderAiSpeechText,
|
|
47
|
+
resolveAiSpeechPlayerAddress: () => resolveAiSpeechPlayerAddress,
|
|
48
|
+
resolveAiSpeechRolloutDecision: () => resolveAiSpeechRolloutDecision,
|
|
49
|
+
resolveAiSpeechVoiceTier: () => resolveAiSpeechVoiceTier
|
|
27
50
|
});
|
|
28
51
|
module.exports = __toCommonJS(index_exports);
|
|
52
|
+
function requireNonEmptyString(value, label) {
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
if (trimmed.length === 0) {
|
|
55
|
+
throw new Error(`${label} must be a non-empty string.`);
|
|
56
|
+
}
|
|
57
|
+
return trimmed;
|
|
58
|
+
}
|
|
59
|
+
function normalizeWhitespace(value) {
|
|
60
|
+
return value.replace(/\s+/gu, " ").trim();
|
|
61
|
+
}
|
|
29
62
|
var AI_SPEECH_PACKAGE = "@plasius/ai-speech";
|
|
30
|
-
var AI_SPEECH_FEATURE_FLAG_ID = "ai.speech.enabled";
|
|
31
63
|
var AI_SPEECH_ENV_PREFIX = "AI_SPEECH";
|
|
64
|
+
var AI_SPEECH_FEATURE_FLAGS = {
|
|
65
|
+
ttsCache: "ai.tts.cache.enabled",
|
|
66
|
+
ttsNearReuse: "ai.tts.near-reuse.enabled",
|
|
67
|
+
premiumCharacters: "ai.tts.premium-characters.enabled"
|
|
68
|
+
};
|
|
69
|
+
var AI_SPEECH_FEATURE_FLAG_ID = AI_SPEECH_FEATURE_FLAGS.ttsCache;
|
|
70
|
+
var AI_SPEECH_ROLLOUT_EVALUATORS = [
|
|
71
|
+
"remote-flag-service",
|
|
72
|
+
"host-application",
|
|
73
|
+
"break-glass-env"
|
|
74
|
+
];
|
|
75
|
+
var AI_SPEECH_ROLLOUT_FALLBACK_MODES = [
|
|
76
|
+
"fail-closed",
|
|
77
|
+
"fail-open"
|
|
78
|
+
];
|
|
79
|
+
var AI_SPEECH_ROLLOUTS = Object.freeze({
|
|
80
|
+
ttsCache: Object.freeze({
|
|
81
|
+
featureFlag: AI_SPEECH_FEATURE_FLAGS.ttsCache,
|
|
82
|
+
evaluator: "remote-flag-service",
|
|
83
|
+
defaultEnabled: false,
|
|
84
|
+
fallbackMode: "fail-closed"
|
|
85
|
+
}),
|
|
86
|
+
ttsNearReuse: Object.freeze({
|
|
87
|
+
featureFlag: AI_SPEECH_FEATURE_FLAGS.ttsNearReuse,
|
|
88
|
+
evaluator: "remote-flag-service",
|
|
89
|
+
defaultEnabled: false,
|
|
90
|
+
fallbackMode: "fail-closed"
|
|
91
|
+
}),
|
|
92
|
+
premiumCharacters: Object.freeze({
|
|
93
|
+
featureFlag: AI_SPEECH_FEATURE_FLAGS.premiumCharacters,
|
|
94
|
+
evaluator: "remote-flag-service",
|
|
95
|
+
defaultEnabled: false,
|
|
96
|
+
fallbackMode: "fail-closed"
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
function resolveAiSpeechRolloutDecision(control, snapshot = {}) {
|
|
100
|
+
const resolved = snapshot[control.featureFlag];
|
|
101
|
+
if (typeof resolved === "boolean") {
|
|
102
|
+
return {
|
|
103
|
+
...control,
|
|
104
|
+
enabled: resolved,
|
|
105
|
+
source: "snapshot"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
...control,
|
|
110
|
+
enabled: control.defaultEnabled,
|
|
111
|
+
source: "default"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function isAiSpeechFeatureEnabled(featureFlag, snapshot = {}) {
|
|
115
|
+
const control = Object.values(AI_SPEECH_ROLLOUTS).find(
|
|
116
|
+
(candidate) => candidate.featureFlag === featureFlag
|
|
117
|
+
);
|
|
118
|
+
if (!control) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return resolveAiSpeechRolloutDecision(control, snapshot).enabled;
|
|
122
|
+
}
|
|
123
|
+
var AI_SPEECH_VOICE_TIERS = [
|
|
124
|
+
"development",
|
|
125
|
+
"standard",
|
|
126
|
+
"premium-character"
|
|
127
|
+
];
|
|
128
|
+
var AI_SPEECH_ENVIRONMENTS = [
|
|
129
|
+
"development",
|
|
130
|
+
"preview",
|
|
131
|
+
"production"
|
|
132
|
+
];
|
|
133
|
+
function resolveAiSpeechVoiceTier(input) {
|
|
134
|
+
const requestedTier = input.requestedTier ?? (input.environment === "development" ? "development" : "standard");
|
|
135
|
+
if (requestedTier === "premium-character" && !isAiSpeechFeatureEnabled(
|
|
136
|
+
AI_SPEECH_FEATURE_FLAGS.premiumCharacters,
|
|
137
|
+
input.featureFlags
|
|
138
|
+
)) {
|
|
139
|
+
return {
|
|
140
|
+
requestedTier,
|
|
141
|
+
resolvedTier: "standard",
|
|
142
|
+
fallbackTier: "standard",
|
|
143
|
+
reasonCodes: ["premium-character-rollout-disabled"]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (requestedTier === "development" && input.environment !== "development") {
|
|
147
|
+
return {
|
|
148
|
+
requestedTier,
|
|
149
|
+
resolvedTier: "standard",
|
|
150
|
+
fallbackTier: "standard",
|
|
151
|
+
reasonCodes: ["development-voices-limited-to-development-environments"]
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
requestedTier,
|
|
156
|
+
resolvedTier: requestedTier,
|
|
157
|
+
reasonCodes: []
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
var AI_SPEECH_PLAYER_ADDRESS_SOURCES = [
|
|
161
|
+
"generic-player",
|
|
162
|
+
"class-title",
|
|
163
|
+
"faction-title",
|
|
164
|
+
"authored-character",
|
|
165
|
+
"user-name",
|
|
166
|
+
"account-handle",
|
|
167
|
+
"user-renamed-character"
|
|
168
|
+
];
|
|
169
|
+
var AI_SPEECH_DEFAULT_PLAYER_LABEL = "Player";
|
|
170
|
+
function resolveAiSpeechPlayerAddress(input) {
|
|
171
|
+
const fallbackLabel = normalizeWhitespace(input.fallbackLabel ?? "") || AI_SPEECH_DEFAULT_PLAYER_LABEL;
|
|
172
|
+
const rawValue = normalizeWhitespace(input.rawValue ?? "");
|
|
173
|
+
switch (input.source) {
|
|
174
|
+
case "generic-player":
|
|
175
|
+
return {
|
|
176
|
+
source: input.source,
|
|
177
|
+
renderText: fallbackLabel,
|
|
178
|
+
exactReuseAllowed: true,
|
|
179
|
+
nearReuseAllowed: true,
|
|
180
|
+
redacted: false,
|
|
181
|
+
reasonCodes: ["generic-player-address"]
|
|
182
|
+
};
|
|
183
|
+
case "class-title":
|
|
184
|
+
case "faction-title":
|
|
185
|
+
return {
|
|
186
|
+
source: input.source,
|
|
187
|
+
renderText: rawValue || fallbackLabel,
|
|
188
|
+
exactReuseAllowed: true,
|
|
189
|
+
nearReuseAllowed: true,
|
|
190
|
+
redacted: false,
|
|
191
|
+
reasonCodes: rawValue ? [] : ["player-address-fell-back-to-generic-label"]
|
|
192
|
+
};
|
|
193
|
+
case "authored-character":
|
|
194
|
+
return {
|
|
195
|
+
source: input.source,
|
|
196
|
+
renderText: rawValue || fallbackLabel,
|
|
197
|
+
exactReuseAllowed: true,
|
|
198
|
+
nearReuseAllowed: true,
|
|
199
|
+
redacted: false,
|
|
200
|
+
reasonCodes: rawValue ? [] : ["player-address-fell-back-to-generic-label"]
|
|
201
|
+
};
|
|
202
|
+
case "user-name":
|
|
203
|
+
case "account-handle":
|
|
204
|
+
case "user-renamed-character":
|
|
205
|
+
return {
|
|
206
|
+
source: input.source,
|
|
207
|
+
renderText: fallbackLabel,
|
|
208
|
+
exactReuseAllowed: true,
|
|
209
|
+
nearReuseAllowed: false,
|
|
210
|
+
redacted: true,
|
|
211
|
+
reasonCodes: ["player-identifier-redacted-from-render-text"]
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function renderAiSpeechText(input) {
|
|
216
|
+
const textTemplate = requireNonEmptyString(
|
|
217
|
+
input.textTemplate,
|
|
218
|
+
"Speech text template"
|
|
219
|
+
);
|
|
220
|
+
const hasPlayerPlaceholder = textTemplate.includes("{playerAddress}");
|
|
221
|
+
if (hasPlayerPlaceholder && !input.playerAddress) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
"Speech text template requires playerAddress when using the {playerAddress} placeholder."
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const playerAddress = input.playerAddress ? resolveAiSpeechPlayerAddress(input.playerAddress) : void 0;
|
|
227
|
+
const renderText = normalizeWhitespace(
|
|
228
|
+
hasPlayerPlaceholder && playerAddress ? textTemplate.replace(/\{playerAddress\}/gu, playerAddress.renderText) : textTemplate
|
|
229
|
+
);
|
|
230
|
+
return {
|
|
231
|
+
renderText,
|
|
232
|
+
normalizedRenderText: normalizeAiSpeechText(renderText),
|
|
233
|
+
playerAddress
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function normalizeAiSpeechText(value) {
|
|
237
|
+
return requireNonEmptyString(
|
|
238
|
+
normalizeWhitespace(value).toLocaleLowerCase("en-GB"),
|
|
239
|
+
"Speech text"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
var AI_SPEECH_PROVIDER_CACHE_PERMISSIONS = [
|
|
243
|
+
"forbidden",
|
|
244
|
+
"exact-only",
|
|
245
|
+
"exact-and-near"
|
|
246
|
+
];
|
|
247
|
+
var AI_SPEECH_CACHE_MODES = [
|
|
248
|
+
"disabled",
|
|
249
|
+
"no-cache",
|
|
250
|
+
"exact",
|
|
251
|
+
"near"
|
|
252
|
+
];
|
|
253
|
+
var AI_SPEECH_CACHE_SCOPES = ["none", "actor", "global"];
|
|
254
|
+
var AI_SPEECH_UTTERANCE_CLASSES = [
|
|
255
|
+
"system-generic",
|
|
256
|
+
"game-npc-dialogue",
|
|
257
|
+
"game-bark",
|
|
258
|
+
"player-address",
|
|
259
|
+
"moderation-notice",
|
|
260
|
+
"private-response"
|
|
261
|
+
];
|
|
262
|
+
var AI_SPEECH_NEAR_REUSE_SAFE_CLASSES = [
|
|
263
|
+
"system-generic",
|
|
264
|
+
"game-npc-dialogue",
|
|
265
|
+
"game-bark",
|
|
266
|
+
"player-address"
|
|
267
|
+
];
|
|
268
|
+
var AI_SPEECH_NEAR_REUSE_SAFE_CLASS_SET = new Set(AI_SPEECH_NEAR_REUSE_SAFE_CLASSES);
|
|
269
|
+
function createAiSpeechCacheKey(input) {
|
|
270
|
+
const scopeDiscriminator = normalizeWhitespace(input.scopeDiscriminator ?? "");
|
|
271
|
+
return [
|
|
272
|
+
requireNonEmptyString(input.providerId, "Provider id"),
|
|
273
|
+
requireNonEmptyString(input.modelId, "Model id"),
|
|
274
|
+
requireNonEmptyString(input.voiceId, "Voice id"),
|
|
275
|
+
requireNonEmptyString(input.locale, "Locale"),
|
|
276
|
+
normalizeWhitespace(input.styleId ?? "") || "_",
|
|
277
|
+
requireNonEmptyString(input.format, "Format"),
|
|
278
|
+
requireNonEmptyString(
|
|
279
|
+
input.pronunciationVersion,
|
|
280
|
+
"Pronunciation version"
|
|
281
|
+
),
|
|
282
|
+
scopeDiscriminator || "global",
|
|
283
|
+
normalizeAiSpeechText(input.normalizedRenderText)
|
|
284
|
+
].map((segment) => encodeURIComponent(segment)).join("::");
|
|
285
|
+
}
|
|
286
|
+
function createAiSpeechNearReuseFingerprint(value) {
|
|
287
|
+
const normalized = normalizeAiSpeechText(value).replace(/[^a-z0-9\s]/gu, " ").replace(/\s+/gu, " ").trim();
|
|
288
|
+
return requireNonEmptyString(normalized, "Near-reuse fingerprint");
|
|
289
|
+
}
|
|
290
|
+
function isAiSpeechNearReuseSafeClass(utteranceClass) {
|
|
291
|
+
return AI_SPEECH_NEAR_REUSE_SAFE_CLASS_SET.has(utteranceClass);
|
|
292
|
+
}
|
|
293
|
+
function planAiSpeechCache(input) {
|
|
294
|
+
const rendered = renderAiSpeechText({
|
|
295
|
+
textTemplate: input.textTemplate,
|
|
296
|
+
playerAddress: input.playerAddress
|
|
297
|
+
});
|
|
298
|
+
const enabledFeatureFlags = [];
|
|
299
|
+
const cacheEnabled = isAiSpeechFeatureEnabled(
|
|
300
|
+
AI_SPEECH_FEATURE_FLAGS.ttsCache,
|
|
301
|
+
input.featureFlags
|
|
302
|
+
);
|
|
303
|
+
if (!cacheEnabled) {
|
|
304
|
+
return {
|
|
305
|
+
mode: "disabled",
|
|
306
|
+
sharingScope: "none",
|
|
307
|
+
renderText: rendered.renderText,
|
|
308
|
+
normalizedRenderText: rendered.normalizedRenderText,
|
|
309
|
+
enabledFeatureFlags,
|
|
310
|
+
playerAddress: rendered.playerAddress,
|
|
311
|
+
reasonCodes: ["tts-cache-rollout-disabled"]
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
enabledFeatureFlags.push(AI_SPEECH_FEATURE_FLAGS.ttsCache);
|
|
315
|
+
if (input.voice.cachePermission === "forbidden") {
|
|
316
|
+
return {
|
|
317
|
+
mode: "no-cache",
|
|
318
|
+
sharingScope: "none",
|
|
319
|
+
renderText: rendered.renderText,
|
|
320
|
+
normalizedRenderText: rendered.normalizedRenderText,
|
|
321
|
+
enabledFeatureFlags,
|
|
322
|
+
playerAddress: rendered.playerAddress,
|
|
323
|
+
reasonCodes: ["provider-forbids-tts-caching"]
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const containsSensitiveContent = Boolean(input.containsPersonalData) || Boolean(input.containsPrivateContext) || Boolean(input.containsModerationNotice) || Boolean(rendered.playerAddress?.redacted);
|
|
327
|
+
const actorScopeKey = normalizeWhitespace(input.actorScopeKey ?? "");
|
|
328
|
+
const actorScopeRequired = containsSensitiveContent || input.utteranceClass === "private-response";
|
|
329
|
+
if (actorScopeRequired && !actorScopeKey) {
|
|
330
|
+
return {
|
|
331
|
+
mode: "no-cache",
|
|
332
|
+
sharingScope: "none",
|
|
333
|
+
renderText: rendered.renderText,
|
|
334
|
+
normalizedRenderText: rendered.normalizedRenderText,
|
|
335
|
+
enabledFeatureFlags,
|
|
336
|
+
playerAddress: rendered.playerAddress,
|
|
337
|
+
reasonCodes: ["actor-scope-key-required-for-sensitive-cache-entry"]
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
const exactKey = createAiSpeechCacheKey({
|
|
341
|
+
...input.voice,
|
|
342
|
+
normalizedRenderText: rendered.normalizedRenderText,
|
|
343
|
+
scopeDiscriminator: actorScopeRequired ? actorScopeKey : void 0
|
|
344
|
+
});
|
|
345
|
+
const nearReuseEnabled = input.voice.cachePermission === "exact-and-near" && isAiSpeechFeatureEnabled(
|
|
346
|
+
AI_SPEECH_FEATURE_FLAGS.ttsNearReuse,
|
|
347
|
+
input.featureFlags
|
|
348
|
+
) && isAiSpeechNearReuseSafeClass(input.utteranceClass) && !containsSensitiveContent && (rendered.playerAddress?.nearReuseAllowed ?? true);
|
|
349
|
+
if (!nearReuseEnabled) {
|
|
350
|
+
return {
|
|
351
|
+
mode: "exact",
|
|
352
|
+
sharingScope: actorScopeRequired ? "actor" : "global",
|
|
353
|
+
renderText: rendered.renderText,
|
|
354
|
+
normalizedRenderText: rendered.normalizedRenderText,
|
|
355
|
+
exactKey,
|
|
356
|
+
enabledFeatureFlags,
|
|
357
|
+
playerAddress: rendered.playerAddress,
|
|
358
|
+
reasonCodes: containsSensitiveContent ? ["exact-cache-only-for-sensitive-or-redacted-content"] : ["near-reuse-disabled-or-ineligible"]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
enabledFeatureFlags.push(AI_SPEECH_FEATURE_FLAGS.ttsNearReuse);
|
|
362
|
+
const nearKey = createAiSpeechCacheKey({
|
|
363
|
+
...input.voice,
|
|
364
|
+
normalizedRenderText: createAiSpeechNearReuseFingerprint(
|
|
365
|
+
rendered.normalizedRenderText
|
|
366
|
+
)
|
|
367
|
+
});
|
|
368
|
+
return {
|
|
369
|
+
mode: "near",
|
|
370
|
+
sharingScope: "global",
|
|
371
|
+
renderText: rendered.renderText,
|
|
372
|
+
normalizedRenderText: rendered.normalizedRenderText,
|
|
373
|
+
exactKey,
|
|
374
|
+
nearKey,
|
|
375
|
+
enabledFeatureFlags,
|
|
376
|
+
playerAddress: rendered.playerAddress,
|
|
377
|
+
reasonCodes: ["near-reuse-eligible"]
|
|
378
|
+
};
|
|
379
|
+
}
|
|
32
380
|
var packageDescriptor = Object.freeze({
|
|
33
381
|
packageName: AI_SPEECH_PACKAGE,
|
|
34
382
|
featureFlagId: AI_SPEECH_FEATURE_FLAG_ID,
|
|
@@ -37,9 +385,32 @@ var packageDescriptor = Object.freeze({
|
|
|
37
385
|
});
|
|
38
386
|
// Annotate the CommonJS export names for ESM import in node:
|
|
39
387
|
0 && (module.exports = {
|
|
388
|
+
AI_SPEECH_CACHE_MODES,
|
|
389
|
+
AI_SPEECH_CACHE_SCOPES,
|
|
390
|
+
AI_SPEECH_DEFAULT_PLAYER_LABEL,
|
|
391
|
+
AI_SPEECH_ENVIRONMENTS,
|
|
40
392
|
AI_SPEECH_ENV_PREFIX,
|
|
393
|
+
AI_SPEECH_FEATURE_FLAGS,
|
|
41
394
|
AI_SPEECH_FEATURE_FLAG_ID,
|
|
395
|
+
AI_SPEECH_NEAR_REUSE_SAFE_CLASSES,
|
|
42
396
|
AI_SPEECH_PACKAGE,
|
|
43
|
-
|
|
397
|
+
AI_SPEECH_PLAYER_ADDRESS_SOURCES,
|
|
398
|
+
AI_SPEECH_PROVIDER_CACHE_PERMISSIONS,
|
|
399
|
+
AI_SPEECH_ROLLOUTS,
|
|
400
|
+
AI_SPEECH_ROLLOUT_EVALUATORS,
|
|
401
|
+
AI_SPEECH_ROLLOUT_FALLBACK_MODES,
|
|
402
|
+
AI_SPEECH_UTTERANCE_CLASSES,
|
|
403
|
+
AI_SPEECH_VOICE_TIERS,
|
|
404
|
+
createAiSpeechCacheKey,
|
|
405
|
+
createAiSpeechNearReuseFingerprint,
|
|
406
|
+
isAiSpeechFeatureEnabled,
|
|
407
|
+
isAiSpeechNearReuseSafeClass,
|
|
408
|
+
normalizeAiSpeechText,
|
|
409
|
+
packageDescriptor,
|
|
410
|
+
planAiSpeechCache,
|
|
411
|
+
renderAiSpeechText,
|
|
412
|
+
resolveAiSpeechPlayerAddress,
|
|
413
|
+
resolveAiSpeechRolloutDecision,
|
|
414
|
+
resolveAiSpeechVoiceTier
|
|
44
415
|
});
|
|
45
416
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export interface AiPackageDescriptor {\n readonly packageName: string;\n readonly featureFlagId: string;\n readonly envPrefix: string;\n readonly summary: string;\n}\n\nexport const AI_SPEECH_PACKAGE = \"@plasius/ai-speech\";\nexport const AI_SPEECH_FEATURE_FLAG_ID = \"ai.speech.enabled\";\nexport const AI_SPEECH_ENV_PREFIX = \"AI_SPEECH\";\n\nexport const packageDescriptor: AiPackageDescriptor = Object.freeze({\n packageName: AI_SPEECH_PACKAGE,\n featureFlagId: AI_SPEECH_FEATURE_FLAG_ID,\n envPrefix: AI_SPEECH_ENV_PREFIX,\n summary: \"Speech orchestration, TTS cache contracts, STT/TTS routing, and voice tier policy for Plasius AI.\",\n});\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,IAAM,oBAAoB;AAC1B,IAAM,4BAA4B;AAClC,IAAM,uBAAuB;AAE7B,IAAM,oBAAyC,OAAO,OAAO;AAAA,EAClE,aAAa;AAAA,EACb,eAAe;AAAA,EACf,WAAW;AAAA,EACX,SAAS;AACX,CAAC;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export interface AiPackageDescriptor {\n readonly packageName: string;\n readonly featureFlagId: string;\n readonly envPrefix: string;\n readonly summary: string;\n}\n\nfunction requireNonEmptyString(value: string, label: string): string {\n const trimmed = value.trim();\n if (trimmed.length === 0) {\n throw new Error(`${label} must be a non-empty string.`);\n }\n\n return trimmed;\n}\n\nfunction normalizeWhitespace(value: string): string {\n return value.replace(/\\s+/gu, \" \").trim();\n}\n\nexport const AI_SPEECH_PACKAGE = \"@plasius/ai-speech\";\nexport const AI_SPEECH_ENV_PREFIX = \"AI_SPEECH\";\n\nexport const AI_SPEECH_FEATURE_FLAGS = {\n ttsCache: \"ai.tts.cache.enabled\",\n ttsNearReuse: \"ai.tts.near-reuse.enabled\",\n premiumCharacters: \"ai.tts.premium-characters.enabled\",\n} as const;\n\nexport type AiSpeechFeatureFlagKey =\n (typeof AI_SPEECH_FEATURE_FLAGS)[keyof typeof AI_SPEECH_FEATURE_FLAGS];\n\nexport type AiSpeechFeatureFlagSnapshot = Readonly<\n Record<string, boolean | undefined>\n>;\n\nexport const AI_SPEECH_FEATURE_FLAG_ID = AI_SPEECH_FEATURE_FLAGS.ttsCache;\n\nexport const AI_SPEECH_ROLLOUT_EVALUATORS = [\n \"remote-flag-service\",\n \"host-application\",\n \"break-glass-env\",\n] as const;\n\nexport type AiSpeechRolloutEvaluator =\n (typeof AI_SPEECH_ROLLOUT_EVALUATORS)[number];\n\nexport const AI_SPEECH_ROLLOUT_FALLBACK_MODES = [\n \"fail-closed\",\n \"fail-open\",\n] as const;\n\nexport type AiSpeechRolloutFallbackMode =\n (typeof AI_SPEECH_ROLLOUT_FALLBACK_MODES)[number];\n\nexport interface AiSpeechRolloutControl {\n readonly featureFlag: AiSpeechFeatureFlagKey;\n readonly evaluator: AiSpeechRolloutEvaluator;\n readonly defaultEnabled: boolean;\n readonly fallbackMode: AiSpeechRolloutFallbackMode;\n}\n\nexport interface AiSpeechRolloutDecision extends AiSpeechRolloutControl {\n readonly enabled: boolean;\n readonly source: \"snapshot\" | \"default\";\n}\n\nexport const AI_SPEECH_ROLLOUTS = Object.freeze({\n ttsCache: Object.freeze<AiSpeechRolloutControl>({\n featureFlag: AI_SPEECH_FEATURE_FLAGS.ttsCache,\n evaluator: \"remote-flag-service\",\n defaultEnabled: false,\n fallbackMode: \"fail-closed\",\n }),\n ttsNearReuse: Object.freeze<AiSpeechRolloutControl>({\n featureFlag: AI_SPEECH_FEATURE_FLAGS.ttsNearReuse,\n evaluator: \"remote-flag-service\",\n defaultEnabled: false,\n fallbackMode: \"fail-closed\",\n }),\n premiumCharacters: Object.freeze<AiSpeechRolloutControl>({\n featureFlag: AI_SPEECH_FEATURE_FLAGS.premiumCharacters,\n evaluator: \"remote-flag-service\",\n defaultEnabled: false,\n fallbackMode: \"fail-closed\",\n }),\n});\n\nexport function resolveAiSpeechRolloutDecision(\n control: AiSpeechRolloutControl,\n snapshot: AiSpeechFeatureFlagSnapshot = {}\n): AiSpeechRolloutDecision {\n const resolved = snapshot[control.featureFlag];\n if (typeof resolved === \"boolean\") {\n return {\n ...control,\n enabled: resolved,\n source: \"snapshot\",\n };\n }\n\n return {\n ...control,\n enabled: control.defaultEnabled,\n source: \"default\",\n };\n}\n\nexport function isAiSpeechFeatureEnabled(\n featureFlag: AiSpeechFeatureFlagKey,\n snapshot: AiSpeechFeatureFlagSnapshot = {}\n): boolean {\n const control = Object.values(AI_SPEECH_ROLLOUTS).find(\n (candidate) => candidate.featureFlag === featureFlag\n );\n\n if (!control) {\n return false;\n }\n\n return resolveAiSpeechRolloutDecision(control, snapshot).enabled;\n}\n\nexport const AI_SPEECH_VOICE_TIERS = [\n \"development\",\n \"standard\",\n \"premium-character\",\n] as const;\n\nexport type AiSpeechVoiceTier = (typeof AI_SPEECH_VOICE_TIERS)[number];\n\nexport const AI_SPEECH_ENVIRONMENTS = [\n \"development\",\n \"preview\",\n \"production\",\n] as const;\n\nexport type AiSpeechEnvironment = (typeof AI_SPEECH_ENVIRONMENTS)[number];\n\nexport interface AiSpeechVoiceTierDecision {\n readonly requestedTier: AiSpeechVoiceTier;\n readonly resolvedTier: AiSpeechVoiceTier;\n readonly fallbackTier?: AiSpeechVoiceTier;\n readonly reasonCodes: readonly string[];\n}\n\nexport interface ResolveAiSpeechVoiceTierInput {\n readonly environment: AiSpeechEnvironment;\n readonly requestedTier?: AiSpeechVoiceTier;\n readonly featureFlags?: AiSpeechFeatureFlagSnapshot;\n}\n\nexport function resolveAiSpeechVoiceTier(\n input: ResolveAiSpeechVoiceTierInput\n): AiSpeechVoiceTierDecision {\n const requestedTier =\n input.requestedTier ??\n (input.environment === \"development\" ? \"development\" : \"standard\");\n\n if (\n requestedTier === \"premium-character\" &&\n !isAiSpeechFeatureEnabled(\n AI_SPEECH_FEATURE_FLAGS.premiumCharacters,\n input.featureFlags\n )\n ) {\n return {\n requestedTier,\n resolvedTier: \"standard\",\n fallbackTier: \"standard\",\n reasonCodes: [\"premium-character-rollout-disabled\"],\n };\n }\n\n if (\n requestedTier === \"development\" &&\n input.environment !== \"development\"\n ) {\n return {\n requestedTier,\n resolvedTier: \"standard\",\n fallbackTier: \"standard\",\n reasonCodes: [\"development-voices-limited-to-development-environments\"],\n };\n }\n\n return {\n requestedTier,\n resolvedTier: requestedTier,\n reasonCodes: [],\n };\n}\n\nexport const AI_SPEECH_PLAYER_ADDRESS_SOURCES = [\n \"generic-player\",\n \"class-title\",\n \"faction-title\",\n \"authored-character\",\n \"user-name\",\n \"account-handle\",\n \"user-renamed-character\",\n] as const;\n\nexport type AiSpeechPlayerAddressSource =\n (typeof AI_SPEECH_PLAYER_ADDRESS_SOURCES)[number];\n\nexport const AI_SPEECH_DEFAULT_PLAYER_LABEL = \"Player\";\n\nexport interface AiSpeechPlayerAddressInput {\n readonly source: AiSpeechPlayerAddressSource;\n readonly rawValue?: string;\n readonly fallbackLabel?: string;\n}\n\nexport interface AiSpeechPlayerAddressDecision {\n readonly source: AiSpeechPlayerAddressSource;\n readonly renderText: string;\n readonly exactReuseAllowed: boolean;\n readonly nearReuseAllowed: boolean;\n readonly redacted: boolean;\n readonly reasonCodes: readonly string[];\n}\n\nexport function resolveAiSpeechPlayerAddress(\n input: AiSpeechPlayerAddressInput\n): AiSpeechPlayerAddressDecision {\n const fallbackLabel =\n normalizeWhitespace(input.fallbackLabel ?? \"\") ||\n AI_SPEECH_DEFAULT_PLAYER_LABEL;\n const rawValue = normalizeWhitespace(input.rawValue ?? \"\");\n\n switch (input.source) {\n case \"generic-player\":\n return {\n source: input.source,\n renderText: fallbackLabel,\n exactReuseAllowed: true,\n nearReuseAllowed: true,\n redacted: false,\n reasonCodes: [\"generic-player-address\"],\n };\n case \"class-title\":\n case \"faction-title\":\n return {\n source: input.source,\n renderText: rawValue || fallbackLabel,\n exactReuseAllowed: true,\n nearReuseAllowed: true,\n redacted: false,\n reasonCodes: rawValue ? [] : [\"player-address-fell-back-to-generic-label\"],\n };\n case \"authored-character\":\n return {\n source: input.source,\n renderText: rawValue || fallbackLabel,\n exactReuseAllowed: true,\n nearReuseAllowed: true,\n redacted: false,\n reasonCodes: rawValue ? [] : [\"player-address-fell-back-to-generic-label\"],\n };\n case \"user-name\":\n case \"account-handle\":\n case \"user-renamed-character\":\n return {\n source: input.source,\n renderText: fallbackLabel,\n exactReuseAllowed: true,\n nearReuseAllowed: false,\n redacted: true,\n reasonCodes: [\"player-identifier-redacted-from-render-text\"],\n };\n }\n}\n\nexport interface AiSpeechRenderTextInput {\n readonly textTemplate: string;\n readonly playerAddress?: AiSpeechPlayerAddressInput;\n}\n\nexport interface AiSpeechRenderTextResult {\n readonly renderText: string;\n readonly normalizedRenderText: string;\n readonly playerAddress?: AiSpeechPlayerAddressDecision;\n}\n\nexport function renderAiSpeechText(\n input: AiSpeechRenderTextInput\n): AiSpeechRenderTextResult {\n const textTemplate = requireNonEmptyString(\n input.textTemplate,\n \"Speech text template\"\n );\n const hasPlayerPlaceholder = textTemplate.includes(\"{playerAddress}\");\n\n if (hasPlayerPlaceholder && !input.playerAddress) {\n throw new Error(\n \"Speech text template requires playerAddress when using the {playerAddress} placeholder.\"\n );\n }\n\n const playerAddress = input.playerAddress\n ? resolveAiSpeechPlayerAddress(input.playerAddress)\n : undefined;\n const renderText = normalizeWhitespace(\n hasPlayerPlaceholder && playerAddress\n ? textTemplate.replace(/\\{playerAddress\\}/gu, playerAddress.renderText)\n : textTemplate\n );\n\n return {\n renderText,\n normalizedRenderText: normalizeAiSpeechText(renderText),\n playerAddress,\n };\n}\n\nexport function normalizeAiSpeechText(value: string): string {\n return requireNonEmptyString(\n normalizeWhitespace(value).toLocaleLowerCase(\"en-GB\"),\n \"Speech text\"\n );\n}\n\nexport const AI_SPEECH_PROVIDER_CACHE_PERMISSIONS = [\n \"forbidden\",\n \"exact-only\",\n \"exact-and-near\",\n] as const;\n\nexport type AiSpeechProviderCachePermission =\n (typeof AI_SPEECH_PROVIDER_CACHE_PERMISSIONS)[number];\n\nexport const AI_SPEECH_CACHE_MODES = [\n \"disabled\",\n \"no-cache\",\n \"exact\",\n \"near\",\n] as const;\n\nexport type AiSpeechCacheMode = (typeof AI_SPEECH_CACHE_MODES)[number];\n\nexport const AI_SPEECH_CACHE_SCOPES = [\"none\", \"actor\", \"global\"] as const;\n\nexport type AiSpeechCacheScope = (typeof AI_SPEECH_CACHE_SCOPES)[number];\n\nexport const AI_SPEECH_UTTERANCE_CLASSES = [\n \"system-generic\",\n \"game-npc-dialogue\",\n \"game-bark\",\n \"player-address\",\n \"moderation-notice\",\n \"private-response\",\n] as const;\n\nexport type AiSpeechUtteranceClass =\n (typeof AI_SPEECH_UTTERANCE_CLASSES)[number];\n\nexport const AI_SPEECH_NEAR_REUSE_SAFE_CLASSES = [\n \"system-generic\",\n \"game-npc-dialogue\",\n \"game-bark\",\n \"player-address\",\n] as const satisfies readonly AiSpeechUtteranceClass[];\n\nconst AI_SPEECH_NEAR_REUSE_SAFE_CLASS_SET: ReadonlySet<AiSpeechUtteranceClass> =\n new Set(AI_SPEECH_NEAR_REUSE_SAFE_CLASSES);\n\nexport interface AiSpeechCacheKeyInput {\n readonly providerId: string;\n readonly modelId: string;\n readonly voiceId: string;\n readonly locale: string;\n readonly format: string;\n readonly pronunciationVersion: string;\n readonly normalizedRenderText: string;\n readonly styleId?: string;\n readonly scopeDiscriminator?: string;\n}\n\nexport interface AiSpeechVoiceProfileRef {\n readonly providerId: string;\n readonly modelId: string;\n readonly voiceId: string;\n readonly locale: string;\n readonly format: string;\n readonly pronunciationVersion: string;\n readonly styleId?: string;\n readonly cachePermission: AiSpeechProviderCachePermission;\n readonly tier?: AiSpeechVoiceTier;\n readonly profileId?: string;\n}\n\nexport interface AiSpeechCacheTelemetry {\n readonly cacheHits: number;\n readonly nearCacheHits: number;\n readonly cacheMisses: number;\n readonly charactersSaved: number;\n readonly estimatedCostSavedUsd?: number;\n readonly voiceProfileIds?: readonly string[];\n}\n\nexport interface PlanAiSpeechCacheInput {\n readonly utteranceClass: AiSpeechUtteranceClass;\n readonly textTemplate: string;\n readonly playerAddress?: AiSpeechPlayerAddressInput;\n readonly voice: AiSpeechVoiceProfileRef;\n readonly featureFlags?: AiSpeechFeatureFlagSnapshot;\n readonly actorScopeKey?: string;\n readonly containsPersonalData?: boolean;\n readonly containsPrivateContext?: boolean;\n readonly containsModerationNotice?: boolean;\n}\n\nexport interface AiSpeechCachePlan {\n readonly mode: AiSpeechCacheMode;\n readonly sharingScope: AiSpeechCacheScope;\n readonly renderText: string;\n readonly normalizedRenderText: string;\n readonly exactKey?: string;\n readonly nearKey?: string;\n readonly enabledFeatureFlags: readonly AiSpeechFeatureFlagKey[];\n readonly playerAddress?: AiSpeechPlayerAddressDecision;\n readonly reasonCodes: readonly string[];\n}\n\nexport function createAiSpeechCacheKey(input: AiSpeechCacheKeyInput): string {\n const scopeDiscriminator = normalizeWhitespace(input.scopeDiscriminator ?? \"\");\n return [\n requireNonEmptyString(input.providerId, \"Provider id\"),\n requireNonEmptyString(input.modelId, \"Model id\"),\n requireNonEmptyString(input.voiceId, \"Voice id\"),\n requireNonEmptyString(input.locale, \"Locale\"),\n normalizeWhitespace(input.styleId ?? \"\") || \"_\",\n requireNonEmptyString(input.format, \"Format\"),\n requireNonEmptyString(\n input.pronunciationVersion,\n \"Pronunciation version\"\n ),\n scopeDiscriminator || \"global\",\n normalizeAiSpeechText(input.normalizedRenderText),\n ]\n .map((segment) => encodeURIComponent(segment))\n .join(\"::\");\n}\n\nexport function createAiSpeechNearReuseFingerprint(value: string): string {\n const normalized = normalizeAiSpeechText(value)\n .replace(/[^a-z0-9\\s]/gu, \" \")\n .replace(/\\s+/gu, \" \")\n .trim();\n\n return requireNonEmptyString(normalized, \"Near-reuse fingerprint\");\n}\n\nexport function isAiSpeechNearReuseSafeClass(\n utteranceClass: AiSpeechUtteranceClass\n): boolean {\n return AI_SPEECH_NEAR_REUSE_SAFE_CLASS_SET.has(utteranceClass);\n}\n\nexport function planAiSpeechCache(\n input: PlanAiSpeechCacheInput\n): AiSpeechCachePlan {\n const rendered = renderAiSpeechText({\n textTemplate: input.textTemplate,\n playerAddress: input.playerAddress,\n });\n const enabledFeatureFlags: AiSpeechFeatureFlagKey[] = [];\n const cacheEnabled = isAiSpeechFeatureEnabled(\n AI_SPEECH_FEATURE_FLAGS.ttsCache,\n input.featureFlags\n );\n\n if (!cacheEnabled) {\n return {\n mode: \"disabled\",\n sharingScope: \"none\",\n renderText: rendered.renderText,\n normalizedRenderText: rendered.normalizedRenderText,\n enabledFeatureFlags,\n playerAddress: rendered.playerAddress,\n reasonCodes: [\"tts-cache-rollout-disabled\"],\n };\n }\n\n enabledFeatureFlags.push(AI_SPEECH_FEATURE_FLAGS.ttsCache);\n\n if (input.voice.cachePermission === \"forbidden\") {\n return {\n mode: \"no-cache\",\n sharingScope: \"none\",\n renderText: rendered.renderText,\n normalizedRenderText: rendered.normalizedRenderText,\n enabledFeatureFlags,\n playerAddress: rendered.playerAddress,\n reasonCodes: [\"provider-forbids-tts-caching\"],\n };\n }\n\n const containsSensitiveContent =\n Boolean(input.containsPersonalData) ||\n Boolean(input.containsPrivateContext) ||\n Boolean(input.containsModerationNotice) ||\n Boolean(rendered.playerAddress?.redacted);\n const actorScopeKey = normalizeWhitespace(input.actorScopeKey ?? \"\");\n\n const actorScopeRequired =\n containsSensitiveContent || input.utteranceClass === \"private-response\";\n\n if (actorScopeRequired && !actorScopeKey) {\n return {\n mode: \"no-cache\",\n sharingScope: \"none\",\n renderText: rendered.renderText,\n normalizedRenderText: rendered.normalizedRenderText,\n enabledFeatureFlags,\n playerAddress: rendered.playerAddress,\n reasonCodes: [\"actor-scope-key-required-for-sensitive-cache-entry\"],\n };\n }\n\n const exactKey = createAiSpeechCacheKey({\n ...input.voice,\n normalizedRenderText: rendered.normalizedRenderText,\n scopeDiscriminator: actorScopeRequired ? actorScopeKey : undefined,\n });\n\n const nearReuseEnabled =\n input.voice.cachePermission === \"exact-and-near\" &&\n isAiSpeechFeatureEnabled(\n AI_SPEECH_FEATURE_FLAGS.ttsNearReuse,\n input.featureFlags\n ) &&\n isAiSpeechNearReuseSafeClass(input.utteranceClass) &&\n !containsSensitiveContent &&\n (rendered.playerAddress?.nearReuseAllowed ?? true);\n\n if (!nearReuseEnabled) {\n return {\n mode: \"exact\",\n sharingScope: actorScopeRequired ? \"actor\" : \"global\",\n renderText: rendered.renderText,\n normalizedRenderText: rendered.normalizedRenderText,\n exactKey,\n enabledFeatureFlags,\n playerAddress: rendered.playerAddress,\n reasonCodes: containsSensitiveContent\n ? [\"exact-cache-only-for-sensitive-or-redacted-content\"]\n : [\"near-reuse-disabled-or-ineligible\"],\n };\n }\n\n enabledFeatureFlags.push(AI_SPEECH_FEATURE_FLAGS.ttsNearReuse);\n\n const nearKey = createAiSpeechCacheKey({\n ...input.voice,\n normalizedRenderText: createAiSpeechNearReuseFingerprint(\n rendered.normalizedRenderText\n ),\n });\n\n return {\n mode: \"near\",\n sharingScope: \"global\",\n renderText: rendered.renderText,\n normalizedRenderText: rendered.normalizedRenderText,\n exactKey,\n nearKey,\n enabledFeatureFlags,\n playerAddress: rendered.playerAddress,\n reasonCodes: [\"near-reuse-eligible\"],\n };\n}\n\nexport const packageDescriptor: AiPackageDescriptor = Object.freeze({\n packageName: AI_SPEECH_PACKAGE,\n featureFlagId: AI_SPEECH_FEATURE_FLAG_ID,\n envPrefix: AI_SPEECH_ENV_PREFIX,\n summary:\n \"Speech orchestration, TTS cache contracts, STT/TTS routing, and voice tier policy for Plasius AI.\",\n});\n"],"mappings":"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","names":[]}
|