@speakableio/core 1.0.26 → 1.0.28
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/dist/index.native.d.mts +76 -3
- package/dist/index.native.d.ts +76 -3
- package/dist/index.native.js +249 -4
- package/dist/index.native.js.map +1 -1
- package/dist/index.native.mjs +249 -4
- package/dist/index.native.mjs.map +1 -1
- package/dist/index.web.d.mts +76 -3
- package/dist/index.web.js +249 -4
- package/dist/index.web.js.map +1 -1
- package/package.json +1 -1
package/dist/index.native.mjs
CHANGED
|
@@ -2134,8 +2134,209 @@ var createSetRepo = () => {
|
|
|
2134
2134
|
};
|
|
2135
2135
|
};
|
|
2136
2136
|
|
|
2137
|
+
// src/utils/ai/detect-transcript-hallucionation.ts
|
|
2138
|
+
var HALLUCINATION_THRESHOLDS = {
|
|
2139
|
+
// Short repeats
|
|
2140
|
+
MIN_CONSECUTIVE_REPEATS: 3,
|
|
2141
|
+
MIN_WORDS_FOR_RATIO_CHECK: 10,
|
|
2142
|
+
MAX_UNIQUE_WORDS_FOR_RATIO: 3,
|
|
2143
|
+
MIN_REPETITION_RATIO: 3,
|
|
2144
|
+
// Phrase repeats
|
|
2145
|
+
MIN_SENTENCE_LENGTH: 10,
|
|
2146
|
+
MIN_CONSECUTIVE_SIMILAR_SENTENCES: 2,
|
|
2147
|
+
MIN_SENTENCES_FOR_DUPLICATE_CHECK: 3,
|
|
2148
|
+
// Cyclic patterns
|
|
2149
|
+
MIN_CYCLE_LENGTH: 20,
|
|
2150
|
+
MIN_CYCLE_REPEATS: 3,
|
|
2151
|
+
// Entropy detection
|
|
2152
|
+
MIN_LENGTH_FOR_ENTROPY_CHECK: 50,
|
|
2153
|
+
MAX_ENTROPY_THRESHOLD: 2.5,
|
|
2154
|
+
// bits per character
|
|
2155
|
+
// Similarity
|
|
2156
|
+
SENTENCE_SIMILARITY_THRESHOLD: 0.8,
|
|
2157
|
+
SEGMENT_SIMILARITY_THRESHOLD: 0.85
|
|
2158
|
+
};
|
|
2159
|
+
function detectTranscriptHallucinationWithDetails(transcript) {
|
|
2160
|
+
if (!transcript || transcript.trim().length === 0) {
|
|
2161
|
+
return { isHallucination: false };
|
|
2162
|
+
}
|
|
2163
|
+
const text = transcript.trim();
|
|
2164
|
+
if (text.length < 10) {
|
|
2165
|
+
return { isHallucination: false };
|
|
2166
|
+
}
|
|
2167
|
+
const shortRepeats = detectShortRepeats(text);
|
|
2168
|
+
if (shortRepeats) {
|
|
2169
|
+
return {
|
|
2170
|
+
isHallucination: true,
|
|
2171
|
+
reason: "Detected repeated short words or phrases",
|
|
2172
|
+
confidence: 0.9
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
const phraseRepeats = detectPhraseRepeats(text);
|
|
2176
|
+
if (phraseRepeats) {
|
|
2177
|
+
return {
|
|
2178
|
+
isHallucination: true,
|
|
2179
|
+
reason: "Detected repeated sentences or phrases",
|
|
2180
|
+
confidence: 0.85
|
|
2181
|
+
};
|
|
2182
|
+
}
|
|
2183
|
+
const cyclicRepeats = detectCyclicPattern(text);
|
|
2184
|
+
if (cyclicRepeats) {
|
|
2185
|
+
return {
|
|
2186
|
+
isHallucination: true,
|
|
2187
|
+
reason: "Detected cyclic repetition pattern",
|
|
2188
|
+
confidence: 0.8
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
if (text.length >= HALLUCINATION_THRESHOLDS.MIN_LENGTH_FOR_ENTROPY_CHECK) {
|
|
2192
|
+
const entropy = calculateEntropy(text);
|
|
2193
|
+
if (entropy < HALLUCINATION_THRESHOLDS.MAX_ENTROPY_THRESHOLD) {
|
|
2194
|
+
return {
|
|
2195
|
+
isHallucination: true,
|
|
2196
|
+
reason: "Detected low entropy (likely gibberish or excessive repetition)",
|
|
2197
|
+
confidence: 0.75
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
return { isHallucination: false };
|
|
2202
|
+
}
|
|
2203
|
+
function detectShortRepeats(text) {
|
|
2204
|
+
const words = text.toLowerCase().split(/[\s,;.!?]+/).filter((w) => w.length > 0);
|
|
2205
|
+
if (words.length < 4) return false;
|
|
2206
|
+
let repeatCount = 1;
|
|
2207
|
+
for (let i = 1; i < words.length; i++) {
|
|
2208
|
+
if (words[i] === words[i - 1]) {
|
|
2209
|
+
repeatCount++;
|
|
2210
|
+
if (repeatCount >= HALLUCINATION_THRESHOLDS.MIN_CONSECUTIVE_REPEATS) {
|
|
2211
|
+
return true;
|
|
2212
|
+
}
|
|
2213
|
+
} else {
|
|
2214
|
+
repeatCount = 1;
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
const uniqueWords = new Set(words);
|
|
2218
|
+
const repetitionRatio = words.length / uniqueWords.size;
|
|
2219
|
+
if (words.length >= HALLUCINATION_THRESHOLDS.MIN_WORDS_FOR_RATIO_CHECK && uniqueWords.size <= HALLUCINATION_THRESHOLDS.MAX_UNIQUE_WORDS_FOR_RATIO && repetitionRatio >= HALLUCINATION_THRESHOLDS.MIN_REPETITION_RATIO) {
|
|
2220
|
+
return true;
|
|
2221
|
+
}
|
|
2222
|
+
return false;
|
|
2223
|
+
}
|
|
2224
|
+
function detectPhraseRepeats(text) {
|
|
2225
|
+
const sentences = text.split(/[.!?]+/).map((s) => s.trim().toLowerCase()).filter((s) => s.length > HALLUCINATION_THRESHOLDS.MIN_SENTENCE_LENGTH);
|
|
2226
|
+
if (sentences.length < 2) return false;
|
|
2227
|
+
for (let i = 0; i < sentences.length - 1; i++) {
|
|
2228
|
+
let consecutiveRepeats = 1;
|
|
2229
|
+
for (let j = i + 1; j < sentences.length; j++) {
|
|
2230
|
+
if (isSimilarSentence(sentences[i], sentences[j])) {
|
|
2231
|
+
consecutiveRepeats++;
|
|
2232
|
+
} else {
|
|
2233
|
+
break;
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
if (consecutiveRepeats >= HALLUCINATION_THRESHOLDS.MIN_CONSECUTIVE_SIMILAR_SENTENCES) {
|
|
2237
|
+
return true;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const uniqueSentences = new Set(sentences);
|
|
2241
|
+
if (sentences.length >= HALLUCINATION_THRESHOLDS.MIN_SENTENCES_FOR_DUPLICATE_CHECK && uniqueSentences.size === 1) {
|
|
2242
|
+
return true;
|
|
2243
|
+
}
|
|
2244
|
+
return false;
|
|
2245
|
+
}
|
|
2246
|
+
function isSimilarSentence(s1, s2, threshold = HALLUCINATION_THRESHOLDS.SENTENCE_SIMILARITY_THRESHOLD) {
|
|
2247
|
+
if (s1 === s2) return true;
|
|
2248
|
+
const normalized1 = s1.replace(/\s+/g, " ").trim();
|
|
2249
|
+
const normalized2 = s2.replace(/\s+/g, " ").trim();
|
|
2250
|
+
if (normalized1 === normalized2) return true;
|
|
2251
|
+
const words1 = normalized1.split(/\s+/);
|
|
2252
|
+
const words2 = normalized2.split(/\s+/);
|
|
2253
|
+
if (Math.abs(words1.length - words2.length) > 2) return false;
|
|
2254
|
+
const set1 = new Set(words1);
|
|
2255
|
+
const set2 = new Set(words2);
|
|
2256
|
+
const intersection = new Set([...set1].filter((w) => set2.has(w)));
|
|
2257
|
+
const similarity = intersection.size * 2 / (set1.size + set2.size);
|
|
2258
|
+
return similarity >= threshold;
|
|
2259
|
+
}
|
|
2260
|
+
function detectCyclicPattern(text) {
|
|
2261
|
+
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
2262
|
+
const length = normalized.length;
|
|
2263
|
+
const minCycleLength = HALLUCINATION_THRESHOLDS.MIN_CYCLE_LENGTH;
|
|
2264
|
+
const maxCycleLength = Math.floor(length / 2);
|
|
2265
|
+
if (maxCycleLength < minCycleLength) return false;
|
|
2266
|
+
const step = 5;
|
|
2267
|
+
for (let cycleLen = minCycleLength; cycleLen <= maxCycleLength; cycleLen += step) {
|
|
2268
|
+
const pattern = normalized.substring(0, cycleLen);
|
|
2269
|
+
let matchCount = 0;
|
|
2270
|
+
let pos = 0;
|
|
2271
|
+
while (pos < length) {
|
|
2272
|
+
const segment = normalized.substring(pos, pos + cycleLen);
|
|
2273
|
+
if (segment.length < cycleLen) {
|
|
2274
|
+
const partialMatch = pattern.startsWith(segment);
|
|
2275
|
+
if (partialMatch && matchCount > 0) {
|
|
2276
|
+
matchCount++;
|
|
2277
|
+
}
|
|
2278
|
+
break;
|
|
2279
|
+
}
|
|
2280
|
+
if (segment === pattern || isSegmentSimilar(segment, pattern)) {
|
|
2281
|
+
matchCount++;
|
|
2282
|
+
pos += cycleLen;
|
|
2283
|
+
} else {
|
|
2284
|
+
break;
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
if (matchCount >= HALLUCINATION_THRESHOLDS.MIN_CYCLE_REPEATS) {
|
|
2288
|
+
return true;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
return false;
|
|
2292
|
+
}
|
|
2293
|
+
function isSegmentSimilar(s1, s2) {
|
|
2294
|
+
if (s1 === s2) return true;
|
|
2295
|
+
if (s1.length !== s2.length) return false;
|
|
2296
|
+
let matches = 0;
|
|
2297
|
+
const minLength = Math.min(s1.length, s2.length);
|
|
2298
|
+
for (let i = 0; i < minLength; i++) {
|
|
2299
|
+
if (s1[i] === s2[i]) {
|
|
2300
|
+
matches++;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
const similarity = matches / minLength;
|
|
2304
|
+
return similarity >= HALLUCINATION_THRESHOLDS.SEGMENT_SIMILARITY_THRESHOLD;
|
|
2305
|
+
}
|
|
2306
|
+
function calculateEntropy(text) {
|
|
2307
|
+
if (!text || text.length === 0) {
|
|
2308
|
+
return 0;
|
|
2309
|
+
}
|
|
2310
|
+
const frequencies = /* @__PURE__ */ new Map();
|
|
2311
|
+
for (const char of text.toLowerCase()) {
|
|
2312
|
+
frequencies.set(char, (frequencies.get(char) || 0) + 1);
|
|
2313
|
+
}
|
|
2314
|
+
let entropy = 0;
|
|
2315
|
+
const length = text.length;
|
|
2316
|
+
for (const count of frequencies.values()) {
|
|
2317
|
+
const probability = count / length;
|
|
2318
|
+
entropy -= probability * Math.log2(probability);
|
|
2319
|
+
}
|
|
2320
|
+
return entropy;
|
|
2321
|
+
}
|
|
2322
|
+
function cleanHallucinatedTranscript(transcript) {
|
|
2323
|
+
var _a, _b;
|
|
2324
|
+
const result = detectTranscriptHallucinationWithDetails(transcript);
|
|
2325
|
+
if (result.isHallucination) {
|
|
2326
|
+
console.warn(
|
|
2327
|
+
"Hallucinated transcript detected and removed:",
|
|
2328
|
+
transcript.substring(0, 100),
|
|
2329
|
+
`
|
|
2330
|
+
Reason: ${(_a = result.reason) != null ? _a : "Unknown"}`,
|
|
2331
|
+
`Confidence: ${String((_b = result.confidence) != null ? _b : "Unknown")}`
|
|
2332
|
+
);
|
|
2333
|
+
return "";
|
|
2334
|
+
}
|
|
2335
|
+
return transcript;
|
|
2336
|
+
}
|
|
2337
|
+
|
|
2137
2338
|
// src/utils/ai/get-transcript.ts
|
|
2138
|
-
async function getTranscript(model, args) {
|
|
2339
|
+
async function getTranscript(model, args, cleanHallucinations = true) {
|
|
2139
2340
|
var _a, _b, _c, _d, _e, _f;
|
|
2140
2341
|
const getGeminiTranscript = (_b = (_a = api).httpsCallable) == null ? void 0 : _b.call(_a, "getGeminiTranscript");
|
|
2141
2342
|
const getAssemblyAITranscript = (_d = (_c = api).httpsCallable) == null ? void 0 : _d.call(_c, "transcribeAssemblyAIAudio");
|
|
@@ -2146,7 +2347,7 @@ async function getTranscript(model, args) {
|
|
|
2146
2347
|
audioUrl: args.audioUrl,
|
|
2147
2348
|
language: args.language
|
|
2148
2349
|
}));
|
|
2149
|
-
return data;
|
|
2350
|
+
return cleanHallucinations ? cleanHallucinatedTranscript(data) : data;
|
|
2150
2351
|
} catch (error) {
|
|
2151
2352
|
console.error("Error getting transcript from Whisper:", error);
|
|
2152
2353
|
throw error;
|
|
@@ -2159,7 +2360,7 @@ async function getTranscript(model, args) {
|
|
|
2159
2360
|
targetLanguage: args.language,
|
|
2160
2361
|
prompt: args.prompt
|
|
2161
2362
|
}));
|
|
2162
|
-
return data.transcript;
|
|
2363
|
+
return cleanHallucinations ? cleanHallucinatedTranscript(data.transcript) : data.transcript;
|
|
2163
2364
|
} catch (error) {
|
|
2164
2365
|
console.error("Error getting transcript from Gemini:", error);
|
|
2165
2366
|
throw error;
|
|
@@ -2171,7 +2372,7 @@ async function getTranscript(model, args) {
|
|
|
2171
2372
|
audioUrl: args.audioUrl,
|
|
2172
2373
|
language: args.language
|
|
2173
2374
|
}));
|
|
2174
|
-
return response.data;
|
|
2375
|
+
return cleanHallucinations ? cleanHallucinatedTranscript(response.data) : response.data;
|
|
2175
2376
|
} catch (error) {
|
|
2176
2377
|
console.error("Error getting transcript from AssemblyAI:", error);
|
|
2177
2378
|
throw error;
|
|
@@ -2179,6 +2380,37 @@ async function getTranscript(model, args) {
|
|
|
2179
2380
|
}
|
|
2180
2381
|
return null;
|
|
2181
2382
|
}
|
|
2383
|
+
async function getTranscriptCycle(args) {
|
|
2384
|
+
const models = ["whisper", "gemini", "assemblyai"];
|
|
2385
|
+
let transcript = "";
|
|
2386
|
+
let lastError = null;
|
|
2387
|
+
for (const model of models) {
|
|
2388
|
+
try {
|
|
2389
|
+
const transcriptResult = await getTranscript(model, args, false);
|
|
2390
|
+
const rawTranscript = transcriptResult || "";
|
|
2391
|
+
transcript = cleanHallucinatedTranscript(rawTranscript);
|
|
2392
|
+
if (transcript !== "") {
|
|
2393
|
+
console.log(`Successfully got transcript from ${model}`);
|
|
2394
|
+
break;
|
|
2395
|
+
}
|
|
2396
|
+
console.warn(`${model} returned empty transcript, trying next model`);
|
|
2397
|
+
} catch (e) {
|
|
2398
|
+
console.error(`Error with ${model} transcript:`, e);
|
|
2399
|
+
lastError = e;
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
if (transcript === "") {
|
|
2403
|
+
console.error("All transcript models failed or returned empty", lastError);
|
|
2404
|
+
return {
|
|
2405
|
+
transcript: "",
|
|
2406
|
+
success: false
|
|
2407
|
+
};
|
|
2408
|
+
}
|
|
2409
|
+
return {
|
|
2410
|
+
transcript,
|
|
2411
|
+
success: true
|
|
2412
|
+
};
|
|
2413
|
+
}
|
|
2182
2414
|
|
|
2183
2415
|
// src/constants/all-langs.json
|
|
2184
2416
|
var all_langs_default = {
|
|
@@ -2925,6 +3157,17 @@ function useSpeakableTranscript() {
|
|
|
2925
3157
|
mutation
|
|
2926
3158
|
};
|
|
2927
3159
|
}
|
|
3160
|
+
function useSpeakableTranscriptCycle() {
|
|
3161
|
+
const mutation = useMutation3({
|
|
3162
|
+
mutationFn: async (args) => {
|
|
3163
|
+
return getTranscriptCycle(args);
|
|
3164
|
+
},
|
|
3165
|
+
retry: false
|
|
3166
|
+
});
|
|
3167
|
+
return {
|
|
3168
|
+
mutationTranscriptCycle: mutation
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
2928
3171
|
|
|
2929
3172
|
// src/hooks/useUpdateStudentVoc.ts
|
|
2930
3173
|
var useUpdateStudentVocab = (page) => {
|
|
@@ -3478,6 +3721,7 @@ export {
|
|
|
3478
3721
|
getSetFromCache,
|
|
3479
3722
|
getTotalCompletedCards,
|
|
3480
3723
|
getTranscript,
|
|
3724
|
+
getTranscriptCycle,
|
|
3481
3725
|
getWordHash,
|
|
3482
3726
|
purify,
|
|
3483
3727
|
refsCardsFiresotre,
|
|
@@ -3502,6 +3746,7 @@ export {
|
|
|
3502
3746
|
useSet,
|
|
3503
3747
|
useSpeakableApi,
|
|
3504
3748
|
useSpeakableTranscript,
|
|
3749
|
+
useSpeakableTranscriptCycle,
|
|
3505
3750
|
useSubmitAssignmentScore,
|
|
3506
3751
|
useSubmitPracticeScore,
|
|
3507
3752
|
useUpdateCardScore,
|