CognxSafeTrack commited on
Commit
181ff6e
·
1 Parent(s): ef0913c

chore: test API distribution

Browse files
apps/api/data/calibration_stats.json ADDED
@@ -0,0 +1,237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "totalProcessed": 24,
3
+ "averageConfidence": 33,
4
+ "distribution": {
5
+ "red": {
6
+ "count": 23,
7
+ "percentage": 96
8
+ },
9
+ "orange": {
10
+ "count": 1,
11
+ "percentage": 4
12
+ },
13
+ "green": {
14
+ "count": 0,
15
+ "percentage": 0
16
+ }
17
+ },
18
+ "samples": [
19
+ {
20
+ "source": "FLEURS",
21
+ "index": 0,
22
+ "hfOriginalText": "groenlaand bariwul won way-dëkk ca tariix scandinaves ñoo ngii waxni erik le rouge dañuko toxal ci islaand ngir faat bàkkan bimuy tukki ci sowwu ci la gis groenlaand dal diko tudde groenlaand",
23
+ "transcribedText": "Groyeslande, Bariwul, Woon, Wai, Dek, Sitarikh, Skandinav, Nyongi, Wahni, Erik, Lerouj, Danjukur, Tokholsi, Island, Ngir, Fat, Bakan, Bimuytuki, Ziswau, Silegis, Groyelande, Daldikotude, Groyelande.",
24
+ "normalizedText": "Groyeslande, Bariwul, Woon, Wai, Dek, Sitarikh, Skandinav, Nyongi, Wahni, Erik, Lerouj, Danjukur, Tokholsi, Island, Ngir, Fat, Bakan, Bimuytuki, Ziswau, Silegis, Groyelande, Daldikotude, Groyelande.",
25
+ "confidenceScore": 48,
26
+ "status": "RED"
27
+ },
28
+ {
29
+ "source": "FLEURS",
30
+ "index": 1,
31
+ "hfOriginalText": "yenn pàcc yu bari ci batimaa bii dañu leen tabaxaat ngir may ñi fiy ñëw di siyaare ñu gën a mën a xam liy seen cosaan",
32
+ "transcribedText": "Si vous avez des questions sur ce bâtiment, vous pouvez m'envoyer un message en disant que je suis là pour vous aider pour qu'on puisse en savoir plus sur ce bâtiment.",
33
+ "normalizedText": "Ci vous avez des questions sur ce bâtiment, vous pouvez m'envoyer un message en disant que je suis là pour vous aider pour qu'on puisse en savoir plus sur ce bâtiment.",
34
+ "confidenceScore": 42,
35
+ "status": "RED"
36
+ },
37
+ {
38
+ "source": "FLEURS",
39
+ "index": 2,
40
+ "hfOriginalText": "yeneen tomb yu nekk ci porogaraam bi ci bali bokk na ci àar àll yi des ci addina bi te séddale xarala yi ngir réew yu nekk ci yoonu yokkute magg ci anam bu bariwul",
41
+ "transcribedText": "Sous-titres réalisés para la communauté d'Amara.org",
42
+ "normalizedText": "Sous-titres réalisés para la communauté d'Amara.org",
43
+ "confidenceScore": 72,
44
+ "status": "ORANGE"
45
+ },
46
+ {
47
+ "source": "FLEURS",
48
+ "index": 3,
49
+ "hfOriginalText": "am na nit ñu yaakaar ne waxoon na dëgg wante nit ñu bare yaakaaru ñu loolu; muy jànt bi dafay wë suuf ci; boole ci jant bi ak yeneen bideew yi",
50
+ "transcribedText": "Il y a des gens qui pensent que c'est vrai, mais il y a beaucoup d'autres qui ne pensent pas comme ça. C'est ce que j'ai essayé d'expliquer dans cette vidéo.",
51
+ "normalizedText": "Il y a des gens qui pensent que c'est vrai, mais il y a beaucoup d'autres qui ne pensent pas comme ça. C'est ce que j'ai essayé d'expliquer dans cette vidéo.",
52
+ "confidenceScore": 43,
53
+ "status": "RED"
54
+ },
55
+ {
56
+ "source": "FLEURS",
57
+ "index": 4,
58
+ "hfOriginalText": "ci beneen xatu spectre bi dañuy soppeeku doon nit kuñu mënul xammee buy yëg ni moom dafa wara sopi lépp lu ékip bi def te def ko yëfu boppam",
59
+ "transcribedText": "Si bien qu'il n'y a pas de spectre, nous devons l'éliminer pour qu'une personne puisse nous dire qu'elle doit l'éliminer pour que l'équipe puisse s'en sortir.",
60
+ "normalizedText": "Ci bien qu'il n'y a pas de spectre, nous devons l'éliminer pour qu'une personne puisse nous dire qu'elle doit l'éliminer pour que l'équipe puisse s'en sortir.",
61
+ "confidenceScore": 43,
62
+ "status": "RED"
63
+ },
64
+ {
65
+ "source": "FLEURS",
66
+ "index": 5,
67
+ "hfOriginalText": "séni doxalin nirowul ak binék ci suuf biy yamalé tangay bi waye amna doolé ak kamb yu xoot yi ci suuf si glen cushing bu united states geological survey usgs kurelu astrogeology ak northern arizona university nékoon ci flagstaff arizona mooko wax",
68
+ "transcribedText": "D'autres proches, une notion au nom d'une partie de ces industries c'est, les Thangais, qui ont quand même créé ces ouves pratiques. Glenn), le Produit de la Prachinath anschiné, Un collage géologique USGS et l'Astrologie du Corail à l'Université d'Arizona, Flashtaff, Arizona",
69
+ "normalizedText": "D'autres proches, une notion au nom d'une partie de ces industries c'est, les Thangais, qui ont quand même créé ces ouves pratiques. Glenn), le Produit de la Prachinath anschiné, Un collage géologique USGS et l'Astrologie du Corail à l'Université d'Arizona, Flashtaff, Arizona",
70
+ "confidenceScore": 7,
71
+ "status": "RED"
72
+ },
73
+ {
74
+ "source": "FLEURS",
75
+ "index": 6,
76
+ "hfOriginalText": "polotiku fomb bi dafay wuute waaye bi weeru màrs di waaja jeex polotiku fomb ndax coronavirus weesuwul suwe 2020 biñu jappee beneen dat ngir olympik yi",
77
+ "transcribedText": "Pour le monde, la politique est majeure, mais l'accent sur son finans à la politique s'est fait avec le coronavirus et les grosses entreprises qui nous ont poussé au spectacle est difficile.",
78
+ "normalizedText": "Pour le monde, la politique est majeure, mais l'accent sur son finans à la politique s'est fait avec le coronavirus et les grosses entreprises qui nous ont poussé au spectacle est difficile.",
79
+ "confidenceScore": 6,
80
+ "status": "RED"
81
+ },
82
+ {
83
+ "source": "FLEURS",
84
+ "index": 7,
85
+ "hfOriginalText": "ci atum 1990 yokkoon nañu limu barab yu woorandi yi addina bi am ndax daŋkaafu suufu dezeer bi",
86
+ "transcribedText": "rencies milieux sont États-Unis, Mexico, Syrie, Hamoum, Burma, Algérie, Tata guide des batailles d'ondes. Qu'est-ce qu'ils ont fait ?",
87
+ "normalizedText": "Rencies milieux sont États-Unis, Mexico, Syrie, Hamoum, Burma, Algérie, Tata guide des batailles d'ondes. Qu'est-ce qu'ils ont fait ?",
88
+ "confidenceScore": 5,
89
+ "status": "RED"
90
+ },
91
+ {
92
+ "source": "FLEURS",
93
+ "index": 8,
94
+ "hfOriginalText": "ñim dem tukki ci dëkk yu bari payum taks mën nañu fa sakkanalee xaalis ci beneen anam ndax sàngara si ak sigaret si fala gëna yombee",
95
+ "transcribedText": "J'ai des images allant de l'espace tel qu'on en a temporairement sur les images là-bas.",
96
+ "normalizedText": "J'ai des images allant de l'espace tel qu'on en a temporairement sur les images là-bas.",
97
+ "confidenceScore": 5,
98
+ "status": "RED"
99
+ },
100
+ {
101
+ "source": "FLEURS",
102
+ "index": 9,
103
+ "hfOriginalText": "aapia mooy kapitalu samoa dëkk bi dafa nekk ci dun bu upolu te am ay nit yu neew 40.000",
104
+ "transcribedText": "A Pia, la capitale de Samoa, il y a des réunions à l'occasion de l'anniversaire de l'anniversaire de Paul.",
105
+ "normalizedText": "A Pia, la capitale de Samoa, il y a des réunions à l'occasion de l'anniversaire de l'anniversaire de Paul.",
106
+ "confidenceScore": 37,
107
+ "status": "RED"
108
+ },
109
+ {
110
+ "source": "FLEURS",
111
+ "index": 10,
112
+ "hfOriginalText": "ci mbenuum weer beneen fafalnaaw genn na yoonam ca mashhad te mëkk benn miir rey fukki nit ak juróom ñaar",
113
+ "transcribedText": "Si tu veux voir une autre vidéo, abonne-toi à ma chaîne et n'oublies pas de t'abonner à ma chaîne pour ne manquer aucune de mes nouvelles vidéos.",
114
+ "normalizedText": "Ci tu veux voir une autre vidéo, abonne-toi à ma chaîne et n'oublies pas de t'abonner à ma chaîne pour ne manquer aucune de mes nouvelles vidéos.",
115
+ "confidenceScore": 42,
116
+ "status": "RED"
117
+ },
118
+ {
119
+ "source": "FLEURS",
120
+ "index": 12,
121
+ "hfOriginalText": "sàmm leen barab bii bu fi kenn bind ay graffitis wala mu am lumu fiy xaatim",
122
+ "transcribedText": "S'il n'y a pas de graffitis, il n'a pas d'intérêt.",
123
+ "normalizedText": "S'il n'y a pas de graffitis, il n'a pas d'intérêt.",
124
+ "confidenceScore": 39,
125
+ "status": "RED"
126
+ },
127
+ {
128
+ "source": "FLEURS",
129
+ "index": 13,
130
+ "hfOriginalText": "gox bi moo nekkoon itam fiñu daan sukkandiku ngir yeek ci wolkan bu nyiragongo ak yenn ci montaañi gorilla yu gëna yomb yuñuy topp afrik",
131
+ "transcribedText": "maintenant nous allons voir la soleil c'est le deuxième jour où il y a plein de soleil dans cette montagne fette. De plus il ne GOIT pas",
132
+ "normalizedText": "Maintenant nous allons voir la soleil c'est le deuxième jour où il y a plein de soleil dans cette montagne fette. De plus il ne GOIT pas",
133
+ "confidenceScore": 6,
134
+ "status": "RED"
135
+ },
136
+ {
137
+ "source": "FLEURS",
138
+ "index": 14,
139
+ "hfOriginalText": "aristotle bénn xalaat kat nééna lép lo giss amna bénn wala lu epp gnenti yééf yugnuko défaré suuf l ndox ak safara",
140
+ "transcribedText": "Aristote Behn a calculé que tout ce qui n'existe pas dans le monde ou vivant d'hier n'est qu'une queue sur terre dans l'univers. Sauf le ciel et le chemin.",
141
+ "normalizedText": "Aristote Behn a calculé que tout ce qui n'existe pas dans le monde ou vivant d'hier n'est qu'une queue sur terre dans l'univers. Sauf le ciel et le chemin.",
142
+ "confidenceScore": 10,
143
+ "status": "RED"
144
+ },
145
+ {
146
+ "source": "FLEURS",
147
+ "index": 15,
148
+ "hfOriginalText": "gimnastik bu amerig ak usoc ñoo bokk jubluwaay fexe ba gimnastik ak yeneen xeeti tàggat yaram wóor ngir jongantekat yi topp seeni mbëbët ci nekkin bu baax te am doole",
149
+ "transcribedText": "Gymnastique de l'Amérique et de l'Ouest, c'est ce que nous faisons. Nous faisons de la gymnastique et d'autres activités pour que les jeunes et les adultes puissent s'entraîner bien et bien.",
150
+ "normalizedText": "Gymnastique de l'Amérique et de l'Ouest, c'est ce que nous faisons. Nous faisons de la gymnastique et d'autres activités pour que les jeunes et les adultes puissent s'entraîner bien et bien.",
151
+ "confidenceScore": 40,
152
+ "status": "RED"
153
+ },
154
+ {
155
+ "source": "FLEURS",
156
+ "index": 16,
157
+ "hfOriginalText": "ndéem ak parku nasiyonal yu afrig du sud yepp am na ay waxtaan bés bu né ak fay ngir dug ci bërëb bi",
158
+ "transcribedText": "N'est pas parcours national de l'Afrique du Sud, il y a des choses que je voudrais divulguer et dóler ces raisons-là.",
159
+ "normalizedText": "N'est pas parcours national de l'Afrique du Sud, il y a des choses que je voudrais divulguer et dóler ces raisons-là.",
160
+ "confidenceScore": 15,
161
+ "status": "RED"
162
+ },
163
+ {
164
+ "source": "FLEURS",
165
+ "index": 17,
166
+ "hfOriginalText": "njiitu poliis bi chandra shekhar solanki wax na ni kiñu tuumal dafa ñëw ci tirbinaal bi ak kanam guñu nëbb",
167
+ "transcribedText": "La police de Giotou, Chandra Shekta Solanky, a dit qu'elle n'est pas sûre de son arrivée au tribunal et qu'elle ne veut pas qu'on l'appelle.",
168
+ "normalizedText": "La police de Giotou, Chandra Shekta Solanky, a dit qu'elle n'est pas sûre de son arrivée au tribunal et qu'elle ne veut pas qu'on l'appelle.",
169
+ "confidenceScore": 43,
170
+ "status": "RED"
171
+ },
172
+ {
173
+ "source": "FLEURS",
174
+ "index": 18,
175
+ "hfOriginalText": "ba tey ci bëj-gannar dem leen xooli sanktiyeer bu mag bi ci notre-dame de fatima sanktiyeer barab bu bu keemane bu mariyaama feeñ te ñu xam ko ci addina bi yépp",
176
+ "transcribedText": "Bataille de Bach-Gadnard, allez voir Sanky-Thier-Bou-Mac dans Notre-Dame-de-Fatima. Sanky-Thier-Barabou-Kemane-Bou-Maria-Mafégne, nous le connaissons tous.",
177
+ "normalizedText": "Bataille de Bach-Gadnard, allez voir Sanky-Thier-Bou-Mac dans Notre-Dame-de-Fatima. Sanky-Thier-Barabou-Kemane-Bou-Maria-Mafégne, nous le connaissons tous.",
178
+ "confidenceScore": 47,
179
+ "status": "RED"
180
+ },
181
+ {
182
+ "source": "FLEURS",
183
+ "index": 19,
184
+ "hfOriginalText": "situ xibaar ak begal xol bu tmz bu waxna ni photographe bi moo taxawal otoom ci beneen wetu yoon wu mag wii di sepulveda ba noppi bëggoon koo foto ci barab bi polisié yi di taxaw laata muy jeggi tali bi ba noppi dem yoonam loolu moo waral polisié bu californie bi taxawal auto yi ñaari yoon ngir may ko mu jeggi dem dellusi",
185
+ "transcribedText": "C'est ce que j'ai appris à l'université de Témzé, c'est qu'il y avait une photo d'un homme en voiture, dans un autre endroit, à l'endroit où il se trouvait. Ils voulaient qu'il y ait une photo avec les policiers pour qu'ils puissent voir l'homme et qu'ils puissent y aller. C'est ce que j'ai appris. Les policiers de Californie ont donné 2 millions d'euros à Michael Mujégidem Delussi.",
186
+ "normalizedText": "C'est ce que j'ai appris à l'université de Témzé, c'est qu'il y avait une photo d'un homme en voiture, dans un autre endroit, à l'endroit où il se trouvait. Ils voulaient qu'il y ait une photo avec les policiers pour qu'ils puissent voir l'homme et qu'ils puissent y aller. C'est ce que j'ai appris. Les policiers de Californie ont donné 2 millions d'euros à Michael Mujégidem Delussi.",
187
+ "confidenceScore": 43,
188
+ "status": "RED"
189
+ },
190
+ {
191
+ "source": "FLEURS",
192
+ "index": 20,
193
+ "hfOriginalText": "naam suñu waxee ia xel dem ci vscience-fiction ia nekkna bànxaas bu am solo ci wàllu ci informatique ndax mooy jàngat ni machine bi di doxalee ni muy xalaate ak ni muy adapter ci mbir mu bees",
194
+ "transcribedText": "Comme nous l'avons dit il y a quelques années, dans le domaine de la science-fiction, il y a eu beaucoup d'événements informatiques qui ont fait que les gens ont commencé à s'intégrer et à s'adapter à la science-fiction.",
195
+ "normalizedText": "Comme nous l'avons dit il y a quelques années, dans le domaine de la science-fiction, il y a eu beaucoup d'événements informatiques qui ont fait que les gens ont commencé à s'intégrer et à s'adapter à la science-fiction.",
196
+ "confidenceScore": 41,
197
+ "status": "RED"
198
+ },
199
+ {
200
+ "source": "FLEURS",
201
+ "index": 21,
202
+ "hfOriginalText": "lu yeex ci gaaawu njiitu réewu amerig donald trump ci ab wax bu mu wax jaaree ko ci sekereteeru tasskatu xibaar bi yëgle na takk deru waa amerig yi dinanu genn syrie",
203
+ "transcribedText": "Ce qui s'est passé à l'intérieur de l'Amérique, c'est ce que Donald Trump a dit à la secrétaire de l'Assemblée des Nations Unies. Il a dit qu'il allait sortir de l'Amérique et qu'il allait sortir de Syrie.",
204
+ "normalizedText": "Ce qui s'est passé à l'intérieur de l'Amérique, c'est ce que Donald Trump a dit à la secrétaire de l'Assemblée des Nations Unies. Il a dit qu'il allait sortir de l'Amérique et qu'il allait sortir de Syrie.",
205
+ "confidenceScore": 48,
206
+ "status": "RED"
207
+ },
208
+ {
209
+ "source": "FLEURS",
210
+ "index": 22,
211
+ "hfOriginalText": "amna atom yoo xamni seeni noyóo duñu taxaw benn barab soo leen laalee tuuti wala nga bañ leen laal ñu tàqaloo",
212
+ "transcribedText": "Il y a des atomes qui savent qu'il n'y a pas d'échec dans l'atmosphère. S'il y a de l'eau dans l'atmosphère, ou si il n'y a pas d'eau dans l'atmosphère, il n'y a pas d'échec.",
213
+ "normalizedText": "Il y a des atomes qui savent qu'il n'y a pas d'échec dans l'atmosphère. S'il y a de l'eau dans l'atmosphère, ou ci il n'y a pas d'eau dans l'atmosphère, il n'y a pas d'échec.",
214
+ "confidenceScore": 47,
215
+ "status": "RED"
216
+ },
217
+ {
218
+ "source": "FLEURS",
219
+ "index": 23,
220
+ "hfOriginalText": "kaaraange bu andak yaar xarañ ci baal ak mboolo bu mën liggéey taxna ñu genn ak te leerna modoonon mboolo buñu wara daan",
221
+ "transcribedText": "Si on entre en objectif, il est impossible de ne pas s'occupier de l'argent et la métier de voyageur à la destination.",
222
+ "normalizedText": "Ci on entre en objectif, il est impossible de ne pas s'occupier de l'argent et la métier de voyageur à la destination.",
223
+ "confidenceScore": 5,
224
+ "status": "RED"
225
+ },
226
+ {
227
+ "source": "FLEURS",
228
+ "index": 24,
229
+ "hfOriginalText": "jëfandikoo ay gaal ngir yóbbu ay marsandiss mooy anam bi gëna baax ngir jàllale nit ñu bare ak marsandiis yu bare ci géej yi",
230
+ "transcribedText": "J'espère que vous avez apprécié cette vidéo, si c'est le cas n'hésitez pas à vous abonner à ma chaine pour d'autres vidéos.",
231
+ "normalizedText": "J'espère que vous avez apprécié cette vidéo, ci c'est le cas n'hésitez pas à vous abonner à ma chaine pour d'autres vidéos.",
232
+ "confidenceScore": 47,
233
+ "status": "RED"
234
+ }
235
+ ],
236
+ "updatedAt": "2026-03-07T01:05:05.141Z"
237
+ }
apps/api/src/scripts/calibrate-whisper.ts CHANGED
@@ -1,95 +1,82 @@
 
1
  import fs from 'fs';
2
  import path from 'path';
 
3
  import { aiService } from '../services/ai';
4
  import { normalizeWolof } from './normalizeWolof';
5
 
6
- const STATS_PATH = path.join(__dirname, '../../data/calibration_stats.json');
7
-
8
- async function downloadAudioBuffer(url: string): Promise<Buffer> {
9
- const res = await fetch(url);
10
- if (!res.ok) throw new Error(`Failed to fetch audio from ${url}`);
11
- const arrayBuffer = await res.arrayBuffer();
12
- return Buffer.from(arrayBuffer);
13
- }
14
-
15
- async function fetchHuggingFaceRows(dataset: string, config: string, split: string, limit: number) {
16
- const url = `https://datasets-server.huggingface.co/rows?dataset=${encodeURIComponent(dataset)}&config=${encodeURIComponent(config)}&split=${encodeURIComponent(split)}&offset=0&length=${limit}`;
17
- const res = await fetch(url);
18
- if (!res.ok) throw new Error(`Failed to fetch dataset rows from ${url}`);
19
- const data = await res.json() as any;
20
- return data.rows;
21
- }
22
 
23
  export async function runCalibration() {
24
  console.log("🚀 Starting Whisper Confidence Calibration Stress-Test...");
25
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  const results = [];
27
  let redCount = 0;
28
  let orangeCount = 0;
29
  let greenCount = 0;
30
  let totalConfidence = 0;
31
 
32
- const sources = [
33
- { dataset: 'mozilla-foundation/common_voice_17_0', config: 'wo', name: 'CommonVoice' },
34
- { dataset: 'google/fleurs', config: 'wo_sn', name: 'FLEURS' }
35
- ];
36
-
37
  let totalProcessed = 0;
38
 
39
- for (const source of sources) {
40
- console.log(`\nFetching 25 samples from ${source.name}...`);
 
 
41
  try {
42
- const rows = await fetchHuggingFaceRows(source.dataset, source.config, 'test', 25);
43
-
44
- for (let i = 0; i < rows.length; i++) {
45
- const row = rows[i].row;
46
- // Field names might vary, usually it's 'audio' which is an object containing 'src' (array) or just an object
47
- const audioData = row.audio || row.audio_data || Object.values(row).find((v: any) => v && v[0]?.src);
48
-
49
- let audioUrl = '';
50
- if (Array.isArray(audioData) && audioData[0]?.src) {
51
- audioUrl = audioData[0].src;
52
- } else if (audioData?.src) {
53
- audioUrl = audioData.src; // sometimes a direct object
54
- }
55
-
56
- if (!audioUrl) {
57
- console.warn(`[SKIP] No audio URL found for row ${i} in ${source.name}`);
58
- continue;
59
- }
60
-
61
- console.log(`[${source.name} ${i + 1}/${rows.length}] Transcribing...`);
62
-
63
- try {
64
- const audioBuffer = await downloadAudioBuffer(audioUrl);
65
- // Pass to Whisper
66
- const { text, confidence } = await aiService.transcribeAudio(audioBuffer, `sample_${i}.mp3`, 'WOLOF');
67
-
68
- // Normalize
69
- const normResult = normalizeWolof(text);
70
-
71
- totalConfidence += confidence;
72
- totalProcessed++;
73
-
74
- if (confidence <= 50) redCount++;
75
- else if (confidence <= 80) orangeCount++;
76
- else greenCount++;
77
-
78
- results.push({
79
- source: source.name,
80
- index: i,
81
- originalText: text,
82
- normalizedText: normResult.normalizedText,
83
- confidenceScore: confidence,
84
- status: confidence <= 50 ? 'RED' : confidence <= 80 ? 'ORANGE' : 'GREEN'
85
- });
86
-
87
- } catch (err: any) {
88
- console.error(`Error processing sample ${i} from ${source.name}: ${err.message}`);
89
- }
90
- }
91
  } catch (err: any) {
92
- console.error(`Failed to fetch or process ${source.name}: ${err.message}`);
93
  }
94
  }
95
 
@@ -107,12 +94,6 @@ export async function runCalibration() {
107
  updatedAt: new Date().toISOString()
108
  };
109
 
110
- // Ensure data dir exists
111
- const dataDir = path.dirname(STATS_PATH);
112
- if (!fs.existsSync(dataDir)) {
113
- fs.mkdirSync(dataDir, { recursive: true });
114
- }
115
-
116
  fs.writeFileSync(STATS_PATH, JSON.stringify(stats, null, 2));
117
  console.log(`\n✅ Calibration finished! Stats saved to ${STATS_PATH}`);
118
  console.log(`Average Confidence: ${averageConfidence}%`);
@@ -120,9 +101,7 @@ export async function runCalibration() {
120
  }
121
 
122
  // Allow running directly from command line
123
- if (require.main === module) {
124
- runCalibration().then(() => process.exit(0)).catch(err => {
125
- console.error(err);
126
- process.exit(1);
127
- });
128
- }
 
1
+ import 'dotenv/config';
2
  import fs from 'fs';
3
  import path from 'path';
4
+ import { execSync } from 'child_process';
5
  import { aiService } from '../services/ai';
6
  import { normalizeWolof } from './normalizeWolof';
7
 
8
+ const DATA_DIR = path.join(__dirname, '../../data');
9
+ const STATS_PATH = path.join(DATA_DIR, 'calibration_stats.json');
10
+ const HF_SAMPLES_PATH = path.join(DATA_DIR, 'hf_samples.json');
11
+ const PY_SCRIPT = path.join(__dirname, 'fetch_hf_audio.py');
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
  export async function runCalibration() {
14
  console.log("🚀 Starting Whisper Confidence Calibration Stress-Test...");
15
 
16
+ // Ensure data dir exists
17
+ if (!fs.existsSync(DATA_DIR)) {
18
+ fs.mkdirSync(DATA_DIR, { recursive: true });
19
+ }
20
+
21
+ // Run Python fetching script if samples not already present
22
+ if (!fs.existsSync(HF_SAMPLES_PATH)) {
23
+ console.log("📥 Calling Python datasets library to download Hugging Face audio...");
24
+ try {
25
+ execSync(`python3 ${PY_SCRIPT} --output ${DATA_DIR}`, { stdio: 'inherit' });
26
+ } catch (e) {
27
+ console.error("❌ Python script failed to fetch samples. Please check HF_TOKEN or network.");
28
+ }
29
+ } else {
30
+ console.log("♻️ Using cached Hugging Face samples...");
31
+ }
32
+
33
+ if (!fs.existsSync(HF_SAMPLES_PATH)) {
34
+ console.error("❌ No samples mapped. Exiting calibration.");
35
+ return;
36
+ }
37
+
38
+ const samples = JSON.parse(fs.readFileSync(HF_SAMPLES_PATH, 'utf-8'));
39
+ console.log(`\n🎧 Processing ${samples.length} samples through Whisper STT...`);
40
+
41
  const results = [];
42
  let redCount = 0;
43
  let orangeCount = 0;
44
  let greenCount = 0;
45
  let totalConfidence = 0;
46
 
 
 
 
 
 
47
  let totalProcessed = 0;
48
 
49
+ for (let i = 0; i < samples.length; i++) {
50
+ const sample = samples[i];
51
+ console.log(`[${sample.source} ${i + 1}/${samples.length}] Transcribing...`);
52
+
53
  try {
54
+ const audioBuffer = Buffer.from(sample.audio_base64, 'base64');
55
+ // Pass to Whisper
56
+ const { text, confidence } = await aiService.transcribeAudio(audioBuffer, `sample_${i}.wav`, 'WOLOF');
57
+
58
+ // Normalize
59
+ const normResult = normalizeWolof(text);
60
+
61
+ totalConfidence += confidence;
62
+ totalProcessed++;
63
+
64
+ if (confidence <= 50) redCount++;
65
+ else if (confidence <= 80) orangeCount++;
66
+ else greenCount++;
67
+
68
+ results.push({
69
+ source: sample.source,
70
+ index: i,
71
+ hfOriginalText: sample.original_text,
72
+ transcribedText: text,
73
+ normalizedText: normResult.normalizedText,
74
+ confidenceScore: confidence,
75
+ status: confidence <= 50 ? 'RED' : confidence <= 80 ? 'ORANGE' : 'GREEN'
76
+ });
77
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  } catch (err: any) {
79
+ console.error(`Error processing sample ${i} from ${sample.source}: ${err.message}`);
80
  }
81
  }
82
 
 
94
  updatedAt: new Date().toISOString()
95
  };
96
 
 
 
 
 
 
 
97
  fs.writeFileSync(STATS_PATH, JSON.stringify(stats, null, 2));
98
  console.log(`\n✅ Calibration finished! Stats saved to ${STATS_PATH}`);
99
  console.log(`Average Confidence: ${averageConfidence}%`);
 
101
  }
102
 
103
  // Allow running directly from command line
104
+ runCalibration().then(() => process.exit(0)).catch(err => {
105
+ console.error(err);
106
+ process.exit(1);
107
+ });
 
 
apps/api/src/scripts/fetch_hf_audio.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import base64
4
+ import argparse
5
+ from pathlib import Path
6
+ try:
7
+ from datasets import load_dataset
8
+ except ImportError:
9
+ import subprocess
10
+ import sys
11
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "datasets==2.19.1", "soundfile", "librosa", "huggingface_hub"])
12
+ from datasets import load_dataset
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser()
16
+ parser.add_argument("--output", required=True)
17
+ args = parser.parse_args()
18
+
19
+ # Create output directory
20
+ out_dir = Path(args.output)
21
+ out_dir.mkdir(parents=True, exist_ok=True)
22
+
23
+ results = []
24
+
25
+ # Check for token
26
+ token = os.environ.get("HF_TOKEN")
27
+ if not token:
28
+ print("⚠️ Warning: No HF_TOKEN found in environment. Common Voice 17.0 might fail because it is gated.", flush=True)
29
+
30
+ sources = [
31
+ {"name": "CommonVoice", "path": "mozilla-foundation/common_voice_17_0", "config": "wo", "split": "test", "limit": 25},
32
+ {"name": "FLEURS", "path": "google/fleurs", "config": "wo_sn", "split": "test", "limit": 25}
33
+ ]
34
+
35
+ for source in sources:
36
+ print(f"\n=> Loading {source['name']} ({source['path']} - {source['config']}) limit {source['limit']}...", flush=True)
37
+ try:
38
+ # We use streaming to avoid downloading the entire massive dataset
39
+ ds = load_dataset(source["path"], source["config"], split=source["split"], streaming=True, token=token, trust_remote_code=True)
40
+
41
+ count = 0
42
+ for row in ds:
43
+ if count >= source["limit"]:
44
+ break
45
+
46
+ # Different dataset structures
47
+ audio_array = None
48
+ sampling_rate = None
49
+ original_text = ""
50
+
51
+ if "audio" in row and row["audio"] is not None:
52
+ audio_dict = row["audio"]
53
+ if "array" in audio_dict:
54
+ audio_array = audio_dict["array"]
55
+ sampling_rate = audio_dict.get("sampling_rate", 16000)
56
+
57
+ if "sentence" in row:
58
+ original_text = row["sentence"]
59
+ elif "transcription" in row:
60
+ original_text = row["transcription"]
61
+ elif "text" in row:
62
+ original_text = row["text"]
63
+ elif "raw_transcription" in row:
64
+ original_text = row["raw_transcription"]
65
+
66
+ if audio_array is not None:
67
+ import soundfile as sf
68
+ from io import BytesIO
69
+
70
+ buf = BytesIO()
71
+ sf.write(buf, audio_array, sampling_rate, format='WAV')
72
+ wav_data = buf.getvalue()
73
+ b64_audio = base64.b64encode(wav_data).decode('utf-8')
74
+
75
+ results.append({
76
+ "source": source["name"],
77
+ "original_text": original_text,
78
+ "audio_base64": b64_audio
79
+ })
80
+ count += 1
81
+ if count % 5 == 0:
82
+ print(f"Downloaded {count}/{source['limit']} from {source['name']}", flush=True)
83
+
84
+ print(f"✅ Success for {source['name']}: {count} samples.", flush=True)
85
+
86
+ except Exception as e:
87
+ print(f"❌ Failed to load {source['name']}: {str(e)}", flush=True)
88
+
89
+ # Save to JSON
90
+ out_file = out_dir / "hf_samples.json"
91
+ with open(out_file, "w") as f:
92
+ json.dump(results, f)
93
+
94
+ print(f"\n🎉 Finished fetching. Saved {len(results)} total samples to {out_file}", flush=True)
95
+
96
+ if __name__ == "__main__":
97
+ main()