@speakableio/core 1.0.25 → 1.0.27

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.
@@ -777,6 +777,7 @@ interface PageScore {
777
777
  fileName?: string | null;
778
778
  transcript?: string | null;
779
779
  } | null;
780
+ transcriptModel?: 'gemini' | 'assemblyai' | 'whisper';
780
781
  }
781
782
 
782
783
  declare enum AssignmentAnalyticsType {
@@ -1524,7 +1525,15 @@ declare function getTranscript(model: 'gemini' | 'assemblyai' | 'whisper', args:
1524
1525
  language: string;
1525
1526
  audioUrl: string;
1526
1527
  prompt?: string;
1527
- }): Promise<string | null>;
1528
+ }, cleanHallucinations?: boolean): Promise<string | null>;
1529
+ declare function getTranscriptCycle(args: {
1530
+ audioUrl: string;
1531
+ language: string;
1532
+ prompt: string;
1533
+ }): Promise<{
1534
+ transcript: string;
1535
+ success: boolean;
1536
+ }>;
1528
1537
 
1529
1538
  declare const getRespondCardTool: ({ language, standard, }: {
1530
1539
  language: string;
@@ -1899,6 +1908,7 @@ declare const useUserCredits: () => {
1899
1908
  isPaused: boolean;
1900
1909
  isRefetching: boolean;
1901
1910
  isStale: boolean;
1911
+ isEnabled: boolean;
1902
1912
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<{
1903
1913
  id: string;
1904
1914
  userId: string;
@@ -2042,6 +2052,7 @@ declare const useUserCredits: () => {
2042
2052
  isPaused: boolean;
2043
2053
  isRefetching: boolean;
2044
2054
  isStale: boolean;
2055
+ isEnabled: boolean;
2045
2056
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<{
2046
2057
  id: string;
2047
2058
  userId: string;
@@ -2146,6 +2157,7 @@ declare const useUserCredits: () => {
2146
2157
  isPaused: boolean;
2147
2158
  isRefetching: boolean;
2148
2159
  isStale: boolean;
2160
+ isEnabled: boolean;
2149
2161
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<{
2150
2162
  id: string;
2151
2163
  userId: string;
@@ -2250,6 +2262,7 @@ declare const useUserCredits: () => {
2250
2262
  isPaused: boolean;
2251
2263
  isRefetching: boolean;
2252
2264
  isStale: boolean;
2265
+ isEnabled: boolean;
2253
2266
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<{
2254
2267
  id: string;
2255
2268
  userId: string;
@@ -2354,6 +2367,7 @@ declare const useUserCredits: () => {
2354
2367
  isPaused: boolean;
2355
2368
  isRefetching: boolean;
2356
2369
  isStale: boolean;
2370
+ isEnabled: boolean;
2357
2371
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<{
2358
2372
  id: string;
2359
2373
  userId: string;
@@ -2497,6 +2511,7 @@ declare const useUserCredits: () => {
2497
2511
  isPaused: boolean;
2498
2512
  isRefetching: boolean;
2499
2513
  isStale: boolean;
2514
+ isEnabled: boolean;
2500
2515
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<{
2501
2516
  id: string;
2502
2517
  userId: string;
@@ -2626,6 +2641,7 @@ declare const useOrganizationAccess: () => {
2626
2641
  isPaused: boolean;
2627
2642
  isRefetching: boolean;
2628
2643
  isStale: boolean;
2644
+ isEnabled: boolean;
2629
2645
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<OrganizationAccess, Error>>;
2630
2646
  fetchStatus: _tanstack_react_query.FetchStatus;
2631
2647
  promise: Promise<OrganizationAccess>;
@@ -2652,6 +2668,7 @@ declare const useOrganizationAccess: () => {
2652
2668
  isPaused: boolean;
2653
2669
  isRefetching: boolean;
2654
2670
  isStale: boolean;
2671
+ isEnabled: boolean;
2655
2672
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<OrganizationAccess, Error>>;
2656
2673
  fetchStatus: _tanstack_react_query.FetchStatus;
2657
2674
  promise: Promise<OrganizationAccess>;
@@ -2678,6 +2695,7 @@ declare const useOrganizationAccess: () => {
2678
2695
  isPaused: boolean;
2679
2696
  isRefetching: boolean;
2680
2697
  isStale: boolean;
2698
+ isEnabled: boolean;
2681
2699
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<OrganizationAccess, Error>>;
2682
2700
  fetchStatus: _tanstack_react_query.FetchStatus;
2683
2701
  promise: Promise<OrganizationAccess>;
@@ -2704,6 +2722,7 @@ declare const useOrganizationAccess: () => {
2704
2722
  isPaused: boolean;
2705
2723
  isRefetching: boolean;
2706
2724
  isStale: boolean;
2725
+ isEnabled: boolean;
2707
2726
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<OrganizationAccess, Error>>;
2708
2727
  fetchStatus: _tanstack_react_query.FetchStatus;
2709
2728
  promise: Promise<OrganizationAccess>;
@@ -2730,6 +2749,7 @@ declare const useOrganizationAccess: () => {
2730
2749
  isPaused: boolean;
2731
2750
  isRefetching: boolean;
2732
2751
  isStale: boolean;
2752
+ isEnabled: boolean;
2733
2753
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<OrganizationAccess, Error>>;
2734
2754
  fetchStatus: _tanstack_react_query.FetchStatus;
2735
2755
  promise: Promise<OrganizationAccess>;
@@ -2756,6 +2776,7 @@ declare const useOrganizationAccess: () => {
2756
2776
  isPaused: boolean;
2757
2777
  isRefetching: boolean;
2758
2778
  isStale: boolean;
2779
+ isEnabled: boolean;
2759
2780
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<OrganizationAccess, Error>>;
2760
2781
  fetchStatus: _tanstack_react_query.FetchStatus;
2761
2782
  promise: Promise<OrganizationAccess>;
@@ -2769,6 +2790,16 @@ declare function useSpeakableTranscript(): {
2769
2790
  prompt?: string;
2770
2791
  }, unknown>;
2771
2792
  };
2793
+ declare function useSpeakableTranscriptCycle(): {
2794
+ mutationTranscriptCycle: _tanstack_react_query.UseMutationResult<{
2795
+ transcript: string;
2796
+ success: boolean;
2797
+ }, Error, {
2798
+ audioUrl: string;
2799
+ language: string;
2800
+ prompt: string;
2801
+ }, unknown>;
2802
+ };
2772
2803
 
2773
2804
  declare const useUpdateStudentVocab: (page: PageActivityWithId | null) => {
2774
2805
  studentVocabMarkVoiceSuccess: undefined;
@@ -2878,6 +2909,7 @@ declare const useActivityFeedbackAccess: ({ aiEnabled, isActivityRoute, }: {
2878
2909
  isPaused: boolean;
2879
2910
  isRefetching: boolean;
2880
2911
  isStale: boolean;
2912
+ isEnabled: boolean;
2881
2913
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<ActivityFeedbackAccess, Error>>;
2882
2914
  fetchStatus: _tanstack_react_query.FetchStatus;
2883
2915
  promise: Promise<ActivityFeedbackAccess>;
@@ -2904,6 +2936,7 @@ declare const useActivityFeedbackAccess: ({ aiEnabled, isActivityRoute, }: {
2904
2936
  isPaused: boolean;
2905
2937
  isRefetching: boolean;
2906
2938
  isStale: boolean;
2939
+ isEnabled: boolean;
2907
2940
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<ActivityFeedbackAccess, Error>>;
2908
2941
  fetchStatus: _tanstack_react_query.FetchStatus;
2909
2942
  promise: Promise<ActivityFeedbackAccess>;
@@ -2930,6 +2963,7 @@ declare const useActivityFeedbackAccess: ({ aiEnabled, isActivityRoute, }: {
2930
2963
  isPaused: boolean;
2931
2964
  isRefetching: boolean;
2932
2965
  isStale: boolean;
2966
+ isEnabled: boolean;
2933
2967
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<ActivityFeedbackAccess, Error>>;
2934
2968
  fetchStatus: _tanstack_react_query.FetchStatus;
2935
2969
  promise: Promise<ActivityFeedbackAccess>;
@@ -2956,6 +2990,7 @@ declare const useActivityFeedbackAccess: ({ aiEnabled, isActivityRoute, }: {
2956
2990
  isPaused: boolean;
2957
2991
  isRefetching: boolean;
2958
2992
  isStale: boolean;
2993
+ isEnabled: boolean;
2959
2994
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<ActivityFeedbackAccess, Error>>;
2960
2995
  fetchStatus: _tanstack_react_query.FetchStatus;
2961
2996
  promise: Promise<ActivityFeedbackAccess>;
@@ -2982,6 +3017,7 @@ declare const useActivityFeedbackAccess: ({ aiEnabled, isActivityRoute, }: {
2982
3017
  isPaused: boolean;
2983
3018
  isRefetching: boolean;
2984
3019
  isStale: boolean;
3020
+ isEnabled: boolean;
2985
3021
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<ActivityFeedbackAccess, Error>>;
2986
3022
  fetchStatus: _tanstack_react_query.FetchStatus;
2987
3023
  promise: Promise<ActivityFeedbackAccess>;
@@ -3008,6 +3044,7 @@ declare const useActivityFeedbackAccess: ({ aiEnabled, isActivityRoute, }: {
3008
3044
  isPaused: boolean;
3009
3045
  isRefetching: boolean;
3010
3046
  isStale: boolean;
3047
+ isEnabled: boolean;
3011
3048
  refetch: (options?: _tanstack_react_query.RefetchOptions) => Promise<_tanstack_react_query.QueryObserverResult<ActivityFeedbackAccess, Error>>;
3012
3049
  fetchStatus: _tanstack_react_query.FetchStatus;
3013
3050
  promise: Promise<ActivityFeedbackAccess>;
@@ -3277,4 +3314,4 @@ declare const createFsClientWeb: ({ db, httpsCallable, logEvent }: FsClientParam
3277
3314
  };
3278
3315
  };
3279
3316
 
3280
- export { ActivityPageType, type Assignment, type AssignmentAnalyticsType$1 as AssignmentAnalyticsType, type AssignmentWithId, BASE_MULTIPLE_CHOICE_FIELD_VALUES, BASE_REPEAT_FIELD_VALUES, BASE_RESPOND_FIELD_VALUES, type CreditContract, FeedbackTypesCard, FsCtx, type InstitutionSubscription, LENIENCY_OPTIONS, LeniencyCard, MULTIPLE_CHOICE_PAGE_ACTIVITY_TYPES, type Organization, type OrganizationAccess, type PageActivity, type PageActivityWithId, type PageScore, REPEAT_PAGE_ACTIVITY_TYPES, RESPOND_AUDIO_PAGE_ACTIVITY_TYPES, RESPOND_PAGE_ACTIVITY_TYPES, RESPOND_WRITE_PAGE_ACTIVITY_TYPES, type RefsCardsFiresotre, type RefsSetsFirestore, SPEAKABLE_NOTIFICATIONS, STUDENT_LEVELS_OPTIONS, type Score, type ScoreWithId, type Set, type SetWithId, type SpeakableNotificationType, SpeakableNotificationTypes, SpeakableProvider, VerificationCardStatus, assignmentQueryKeys, cardsQueryKeys, checkIsMCPage, checkIsMediaPage, checkIsRepeatPage, checkIsRespondAudioPage, checkIsRespondPage, checkIsRespondWrittenPage, checkIsShortAnswerPage, checkTypePageActivity, cleanString, createAssignmentRepo, createCardRepo, createFsClientWeb as createFsClient, createSetRepo, creditQueryKeys, debounce, getCardFromCache, getLabelPage, getPagePrompt, getPhraseLength, getRespondCardTool, getSetFromCache, getTotalCompletedCards, getTranscript, getWordHash, purify, refsCardsFiresotre, refsSetsFirestore, scoreQueryKeys, setsQueryKeys, updateCardInCache, updateSetInCache, useActivity, useActivityFeedbackAccess, useAssignment, useBaseOpenAI, useCards, useClearScore, useClearScoreV2, useCreateCard, useCreateCards, useCreateNotification, useGetCard, useOrganizationAccess, useScore, useSet, useSpeakableApi, useSpeakableTranscript, useSubmitAssignmentScore, useSubmitPracticeScore, useUpdateCardScore, useUpdateScore, useUpdateStudentVocab, useUserCredits };
3317
+ export { ActivityPageType, type Assignment, type AssignmentAnalyticsType$1 as AssignmentAnalyticsType, type AssignmentWithId, BASE_MULTIPLE_CHOICE_FIELD_VALUES, BASE_REPEAT_FIELD_VALUES, BASE_RESPOND_FIELD_VALUES, type CreditContract, FeedbackTypesCard, FsCtx, type InstitutionSubscription, LENIENCY_OPTIONS, LeniencyCard, MULTIPLE_CHOICE_PAGE_ACTIVITY_TYPES, type Organization, type OrganizationAccess, type PageActivity, type PageActivityWithId, type PageScore, REPEAT_PAGE_ACTIVITY_TYPES, RESPOND_AUDIO_PAGE_ACTIVITY_TYPES, RESPOND_PAGE_ACTIVITY_TYPES, RESPOND_WRITE_PAGE_ACTIVITY_TYPES, type RefsCardsFiresotre, type RefsSetsFirestore, SPEAKABLE_NOTIFICATIONS, STUDENT_LEVELS_OPTIONS, type Score, type ScoreWithId, type Set, type SetWithId, type SpeakableNotificationType, SpeakableNotificationTypes, SpeakableProvider, VerificationCardStatus, assignmentQueryKeys, cardsQueryKeys, checkIsMCPage, checkIsMediaPage, checkIsRepeatPage, checkIsRespondAudioPage, checkIsRespondPage, checkIsRespondWrittenPage, checkIsShortAnswerPage, checkTypePageActivity, cleanString, createAssignmentRepo, createCardRepo, createFsClientWeb as createFsClient, createSetRepo, creditQueryKeys, debounce, getCardFromCache, getLabelPage, getPagePrompt, getPhraseLength, getRespondCardTool, getSetFromCache, getTotalCompletedCards, getTranscript, getTranscriptCycle, getWordHash, purify, refsCardsFiresotre, refsSetsFirestore, scoreQueryKeys, setsQueryKeys, updateCardInCache, updateSetInCache, useActivity, useActivityFeedbackAccess, useAssignment, useBaseOpenAI, useCards, useClearScore, useClearScoreV2, useCreateCard, useCreateCards, useCreateNotification, useGetCard, useOrganizationAccess, useScore, useSet, useSpeakableApi, useSpeakableTranscript, useSpeakableTranscriptCycle, useSubmitAssignmentScore, useSubmitPracticeScore, useUpdateCardScore, useUpdateScore, useUpdateStudentVocab, useUserCredits };
package/dist/index.web.js CHANGED
@@ -1729,7 +1729,7 @@ var getCard = withErrorHandler(_getCard, "getCard");
1729
1729
  import { v4 } from "uuid";
1730
1730
 
1731
1731
  // src/utils/text-utils.ts
1732
- import sha1 from "js-sha1";
1732
+ import { sha1 } from "js-sha1";
1733
1733
  var purify = (word) => {
1734
1734
  return word.normalize("NFD").replace(/\/([^" "]*)/g, "").replace(/\([^()]*\)/g, "").replace(/([^()]*)/g, "").replace(/[\u0300-\u036f]/g, "").replace(/[-]/g, " ").replace(/[.,/#!¡¿?؟。,.?$%^&*;:{}=\-_`~()’'…\s]/g, "").replace(/\s\s+/g, " ").toLowerCase().trim();
1735
1735
  };
@@ -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,