Skip to content

Commit ea21ae3

Browse files
authored
Merge pull request #131 from ToolboxAid/team/BRAVO/messages
Use active TTS profiles in Messages
2 parents d9724b1 + c384b4f commit ea21ae3

72 files changed

Lines changed: 41763 additions & 1290 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
const TEXT_TO_SPEECH_PROFILE_STORAGE_KEY = "gamefoundry.textToSpeech.profiles.v1";
2+
const TEXT_TO_SPEECH_PROFILE_STORE_VERSION = "tts-profile-store-v1";
3+
4+
const DEFAULT_LANGUAGE = "en-US";
5+
const DEFAULT_PROVIDER_KEY = "browser-speech";
6+
const DEFAULT_VOICE_AGE = "adult";
7+
8+
function clampNumber(value, fallback, min, max) {
9+
const numberValue = Number(value);
10+
if (!Number.isFinite(numberValue)) {
11+
return fallback;
12+
}
13+
return Math.min(max, Math.max(min, numberValue));
14+
}
15+
16+
function normalizedText(value, fallback = "") {
17+
const text = String(value || "").trim();
18+
return text || fallback;
19+
}
20+
21+
function slugFromText(value, fallback = "item") {
22+
return normalizedText(value, fallback)
23+
.toLowerCase()
24+
.replace(/[^a-z0-9]+/g, "-")
25+
.replace(/^-+|-+$/g, "") || fallback;
26+
}
27+
28+
function labelFromSlug(value, fallback = "Neutral") {
29+
return normalizedText(value, fallback)
30+
.replace(/[-_]+/g, " ")
31+
.replace(/\b\w/g, (letter) => letter.toUpperCase());
32+
}
33+
34+
function defaultStorage() {
35+
try {
36+
return typeof window === "undefined" ? null : window.localStorage;
37+
} catch {
38+
return null;
39+
}
40+
}
41+
42+
function storagePayloadProfiles(payload) {
43+
if (Array.isArray(payload)) {
44+
return payload;
45+
}
46+
if (Array.isArray(payload?.profiles)) {
47+
return payload.profiles;
48+
}
49+
return [];
50+
}
51+
52+
function normalizeSavedEmotion(emotion = {}) {
53+
const emotionKey = slugFromText(emotion.emotion || emotion.id || emotion.emotionLabel, "neutral");
54+
const emotionLabel = normalizedText(emotion.emotionLabel || emotion.name, labelFromSlug(emotionKey));
55+
return {
56+
active: emotion.active !== false,
57+
emotion: emotionKey,
58+
emotionLabel,
59+
id: normalizedText(emotion.id, emotionKey),
60+
messagePartsUsageCount: Math.max(0, Number(emotion.messagePartsUsageCount) || 0),
61+
pitch: clampNumber(emotion.pitch, 1, 0.1, 2),
62+
rate: clampNumber(emotion.rate, 1, 0.1, 2),
63+
ssmlLikePreset: normalizedText(emotion.ssmlLikePreset, "normal"),
64+
volume: clampNumber(emotion.volume, 1, 0, 1),
65+
};
66+
}
67+
68+
function normalizeSavedProfile(profile = {}) {
69+
const name = normalizedText(profile.name, "Default Balanced Profile");
70+
const emotions = Array.isArray(profile.emotions) && profile.emotions.length
71+
? profile.emotions.map(normalizeSavedEmotion)
72+
: [normalizeSavedEmotion()];
73+
return {
74+
active: profile.active !== false,
75+
age: normalizedText(profile.age, DEFAULT_VOICE_AGE),
76+
emotions,
77+
gender: normalizedText(profile.gender, "neutral"),
78+
id: normalizedText(profile.id, slugFromText(name, "tts-profile")),
79+
language: normalizedText(profile.language, DEFAULT_LANGUAGE),
80+
messageStudioUsageCount: Math.max(0, Number(profile.messageStudioUsageCount) || 0),
81+
name,
82+
owner: "Audio",
83+
providerKey: normalizedText(profile.providerKey, DEFAULT_PROVIDER_KEY),
84+
voice: normalizedText(profile.voice),
85+
voiceName: normalizedText(profile.voiceName || profile.voice, "Default browser voice"),
86+
};
87+
}
88+
89+
function normalizeSavedTextToSpeechProfiles(profiles = []) {
90+
return Array.isArray(profiles) ? profiles.map(normalizeSavedProfile) : [];
91+
}
92+
93+
function readSavedTextToSpeechProfiles(storage = defaultStorage()) {
94+
if (!storage || typeof storage.getItem !== "function") {
95+
return [];
96+
}
97+
const raw = storage.getItem(TEXT_TO_SPEECH_PROFILE_STORAGE_KEY);
98+
if (!raw) {
99+
return [];
100+
}
101+
let payload;
102+
try {
103+
payload = JSON.parse(raw);
104+
} catch {
105+
throw new Error("Saved Text To Speech profiles could not be read.");
106+
}
107+
return normalizeSavedTextToSpeechProfiles(storagePayloadProfiles(payload));
108+
}
109+
110+
function writeSavedTextToSpeechProfiles(profiles = [], storage = defaultStorage()) {
111+
if (!storage || typeof storage.setItem !== "function") {
112+
return false;
113+
}
114+
const payload = {
115+
profiles: normalizeSavedTextToSpeechProfiles(profiles),
116+
updatedAt: new Date().toISOString(),
117+
version: TEXT_TO_SPEECH_PROFILE_STORE_VERSION,
118+
};
119+
storage.setItem(TEXT_TO_SPEECH_PROFILE_STORAGE_KEY, JSON.stringify(payload));
120+
return true;
121+
}
122+
123+
function textToSpeechProfilesToMessageOptions(profiles = []) {
124+
return normalizeSavedTextToSpeechProfiles(profiles)
125+
.filter((profile) => profile.active !== false)
126+
.map((profile) => ({
127+
active: true,
128+
age: profile.age,
129+
ageFilter: profile.age,
130+
emotionSettings: profile.emotions
131+
.filter((emotion) => emotion.active !== false)
132+
.map((emotion) => ({
133+
active: true,
134+
emotion: emotion.emotion,
135+
emotionLabel: emotion.emotionLabel,
136+
key: emotion.id,
137+
pitch: emotion.pitch,
138+
rate: emotion.rate,
139+
ssmlLikePreset: emotion.ssmlLikePreset,
140+
volume: emotion.volume,
141+
})),
142+
gender: profile.gender,
143+
key: profile.id,
144+
language: profile.language,
145+
name: profile.name,
146+
providerKey: profile.providerKey,
147+
sourceProfileId: profile.id,
148+
voice: profile.voice,
149+
voiceName: profile.voiceName || profile.voice || "Default browser voice",
150+
}));
151+
}
152+
153+
export {
154+
TEXT_TO_SPEECH_PROFILE_STORAGE_KEY,
155+
TEXT_TO_SPEECH_PROFILE_STORE_VERSION,
156+
normalizeSavedTextToSpeechProfiles,
157+
readSavedTextToSpeechProfiles,
158+
textToSpeechProfilesToMessageOptions,
159+
writeSavedTextToSpeechProfiles,
160+
};

0 commit comments

Comments
 (0)