2026-07-26 23:47:27 -04:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
2026-07-27 10:40:23 -04:00
|
|
|
|
* check-audio.mjs — état des prononciations kréyòl.
|
2026-07-26 23:47:27 -04:00
|
|
|
|
*
|
2026-07-27 10:40:23 -04:00
|
|
|
|
* Le brief §6 le demande : « prévois le contrat (audio.gcf, format .opus, repli silencieux
|
|
|
|
|
|
* si le fichier manque) et un script de vérification listant les enregistrements manquants ».
|
2026-07-26 23:47:27 -04:00
|
|
|
|
*
|
2026-07-27 10:40:23 -04:00
|
|
|
|
* Ce script fait deux choses :
|
|
|
|
|
|
* 1. il liste ce qui manque localement ;
|
|
|
|
|
|
* 2. il cherche sur **Lingua Libre** si quelqu'un a déjà enregistré le mot.
|
2026-07-26 23:47:27 -04:00
|
|
|
|
*
|
2026-07-27 10:40:23 -04:00
|
|
|
|
* Le second point est le cœur de la démarche : Lingua Libre est l'atelier d'enregistrement
|
|
|
|
|
|
* de Wikimedia, et ses prises sont versées sur Commons sous licence libre. Un nom enregistré
|
|
|
|
|
|
* là-bas sert à ce jeu, mais aussi au Wiktionnaire, aux lexiques, aux synthèses vocales
|
|
|
|
|
|
* futures — bref, à la langue, pas seulement à nous.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Pas de synthèse vocale : ce script n'en fabrique aucune, et le schéma n'en accepte pas.
|
|
|
|
|
|
* Ce script n'échoue jamais : un enregistrement manquant est un travail à faire.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Sortie : docs/audio-a-enregistrer.md
|
2026-07-26 23:47:27 -04:00
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
import { readFile, readdir, writeFile, access } from 'node:fs/promises';
|
|
|
|
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
|
|
import { dirname, join } from 'node:path';
|
|
|
|
|
|
|
|
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
|
|
|
|
const DATA = join(ROOT, 'src', 'lib', 'data', 'fruits');
|
|
|
|
|
|
const STATIC = join(ROOT, 'static');
|
2026-07-27 10:40:23 -04:00
|
|
|
|
const UA = 'ki-fwi-dataset/0.1 (https://o-k-i.net ; projet educatif ORGANISATION KA INTERNATIONALE)';
|
|
|
|
|
|
|
|
|
|
|
|
/** Élément Wikidata du kréyòl gwadloupéyen — sert de code de langue à Lingua Libre. */
|
|
|
|
|
|
const Q_GWADLOUPEYEN = 'Q3006280';
|
|
|
|
|
|
const CATEGORIE_LL = 'Lingua Libre pronunciation-gcf';
|
|
|
|
|
|
|
|
|
|
|
|
const pause = (ms) => new Promise((r) => setTimeout(r, ms));
|
2026-07-26 23:47:27 -04:00
|
|
|
|
|
|
|
|
|
|
async function existe(p) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await access(p);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 10:40:23 -04:00
|
|
|
|
async function commons(params) {
|
|
|
|
|
|
const u = new URL('https://commons.wikimedia.org/w/api.php');
|
|
|
|
|
|
for (const [k, v] of Object.entries({ format: 'json', formatversion: '2', ...params })) u.searchParams.set(k, v);
|
|
|
|
|
|
const res = await fetch(u, { headers: { 'User-Agent': UA, Accept: 'application/json' } });
|
|
|
|
|
|
if (!res.ok) throw new Error(`Commons ${res.status}`);
|
|
|
|
|
|
return res.json();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const sansBalises = (s) =>
|
|
|
|
|
|
s
|
|
|
|
|
|
? String(s)
|
|
|
|
|
|
.replace(/<[^>]+>/g, ' ')
|
|
|
|
|
|
.replace(/\s+/g, ' ')
|
|
|
|
|
|
.trim()
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
|
|
/** Cherche un enregistrement du mot dans la catégorie Lingua Libre du kréyòl gwadloupéyen. */
|
|
|
|
|
|
async function chercherLinguaLibre(mot) {
|
|
|
|
|
|
const j = await commons({
|
|
|
|
|
|
action: 'query',
|
|
|
|
|
|
list: 'search',
|
|
|
|
|
|
srnamespace: '6',
|
|
|
|
|
|
srlimit: '3',
|
|
|
|
|
|
srsearch: `incategory:"${CATEGORIE_LL}" ${mot}`
|
|
|
|
|
|
});
|
|
|
|
|
|
const trouves = j.query?.search ?? [];
|
|
|
|
|
|
if (!trouves.length) return null;
|
|
|
|
|
|
|
|
|
|
|
|
const detail = await commons({
|
|
|
|
|
|
action: 'query',
|
|
|
|
|
|
prop: 'imageinfo',
|
|
|
|
|
|
iiprop: 'url|mime|extmetadata',
|
|
|
|
|
|
iiextmetadatafilter: 'LicenseShortName|Artist',
|
|
|
|
|
|
titles: trouves[0].title
|
|
|
|
|
|
});
|
|
|
|
|
|
const p = detail.query?.pages?.[0];
|
|
|
|
|
|
const i = p?.imageinfo?.[0];
|
|
|
|
|
|
return {
|
|
|
|
|
|
titre: p?.title ?? trouves[0].title,
|
|
|
|
|
|
licence: sansBalises(i?.extmetadata?.LicenseShortName?.value),
|
|
|
|
|
|
auteur: sansBalises(i?.extmetadata?.Artist?.value),
|
|
|
|
|
|
url_fichier: i?.url ?? null,
|
|
|
|
|
|
url_description: i?.descriptionurl ?? null,
|
|
|
|
|
|
autres: trouves.length - 1
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Combien d'enregistrements existent en tout dans cette langue ? Mesure l'état de la communauté. */
|
|
|
|
|
|
async function tailleCategorie() {
|
|
|
|
|
|
const j = await commons({ action: 'query', prop: 'categoryinfo', titles: `Category:${CATEGORIE_LL}` });
|
|
|
|
|
|
return j.query?.pages?.[0]?.categoryinfo?.files ?? 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-26 23:47:27 -04:00
|
|
|
|
async function main() {
|
2026-07-27 10:40:23 -04:00
|
|
|
|
const horsLigne = process.argv.includes('--hors-ligne');
|
2026-07-26 23:47:27 -04:00
|
|
|
|
const fichiers = (await readdir(DATA)).filter((f) => f.endsWith('.json')).sort();
|
2026-07-27 10:40:23 -04:00
|
|
|
|
|
2026-07-26 23:47:27 -04:00
|
|
|
|
const manquants = [];
|
|
|
|
|
|
const absents = [];
|
|
|
|
|
|
const presents = [];
|
2026-07-27 10:40:23 -04:00
|
|
|
|
const dejaEnregistres = [];
|
|
|
|
|
|
|
|
|
|
|
|
let totalLL = 0;
|
|
|
|
|
|
if (!horsLigne) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
totalLL = await tailleCategorie();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.warn('⚠ Lingua Libre injoignable — rapport local uniquement.');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-26 23:47:27 -04:00
|
|
|
|
|
|
|
|
|
|
for (const f of fichiers) {
|
|
|
|
|
|
const e = JSON.parse(await readFile(join(DATA, f), 'utf8'));
|
2026-07-27 10:40:23 -04:00
|
|
|
|
|
|
|
|
|
|
if (e.audio.gcf) {
|
|
|
|
|
|
if (await existe(join(STATIC, e.audio.gcf))) presents.push(e);
|
|
|
|
|
|
else absents.push(e);
|
|
|
|
|
|
} else {
|
2026-07-26 23:47:27 -04:00
|
|
|
|
manquants.push(e);
|
|
|
|
|
|
}
|
2026-07-27 10:40:23 -04:00
|
|
|
|
|
|
|
|
|
|
if (!horsLigne && !e.audio.gcf) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
process.stdout.write(`→ Lingua Libre : ${e.noms.gcf}… `);
|
|
|
|
|
|
const trouve = await chercherLinguaLibre(e.noms.gcf);
|
|
|
|
|
|
console.log(trouve ? '✔ déjà enregistré' : '—');
|
|
|
|
|
|
if (trouve) dejaEnregistres.push({ e, trouve });
|
|
|
|
|
|
await pause(400);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
console.log('(échec de la requête)');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-26 23:47:27 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const l = [
|
2026-07-27 10:40:23 -04:00
|
|
|
|
'# Prononciations kréyòl — état et travail à faire',
|
2026-07-26 23:47:27 -04:00
|
|
|
|
'',
|
2026-07-27 10:40:23 -04:00
|
|
|
|
'> Généré par `npm run check:audio`. **Aucune synthèse vocale** : ces noms doivent être',
|
|
|
|
|
|
'> prononcés par une voix humaine locale (brief §6). Le locuteur conserve ses droits',
|
|
|
|
|
|
'> voisins et est crédité nominativement.',
|
2026-07-26 23:47:27 -04:00
|
|
|
|
'',
|
2026-07-27 10:40:23 -04:00
|
|
|
|
'Format embarqué : `.opus`, un fichier par entrée dans `static/audio/<id>.opus`.',
|
2026-07-26 23:47:27 -04:00
|
|
|
|
''
|
|
|
|
|
|
];
|
2026-07-27 10:40:23 -04:00
|
|
|
|
|
|
|
|
|
|
if (totalLL) {
|
|
|
|
|
|
l.push(
|
|
|
|
|
|
'## Il existe déjà une communauté',
|
|
|
|
|
|
'',
|
|
|
|
|
|
`**${totalLL} enregistrements en kréyòl gwadloupéyen** sont déjà publiés sur`,
|
|
|
|
|
|
`[Lingua Libre](https://lingualibre.org), l'atelier d'enregistrement de Wikimedia, et versés`,
|
|
|
|
|
|
`sur Commons sous licence libre (langue \`gcf\`, [${Q_GWADLOUPEYEN}](https://www.wikidata.org/wiki/${Q_GWADLOUPEYEN})).`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'Enregistrer là-bas plutôt que dans notre coin, c’est faire servir sa voix au',
|
|
|
|
|
|
'Wiktionnaire, aux lexiques et à tous les projets à venir — pas seulement à ce jeu.',
|
|
|
|
|
|
''
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (dejaEnregistres.length) {
|
|
|
|
|
|
l.push(
|
|
|
|
|
|
`## ✔ Déjà enregistrés par la communauté (${dejaEnregistres.length})`,
|
|
|
|
|
|
'',
|
|
|
|
|
|
'Ces noms n’ont pas besoin d’être réenregistrés : la prise existe, sous licence libre.',
|
|
|
|
|
|
'Il reste à vérifier qu’il s’agit bien du fruit, puis à renseigner le bloc `audio`.',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'**C’est aussi une corroboration de notre graphie** : le nom enregistré porte',
|
|
|
|
|
|
'l’orthographe employée par le locuteur.',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'| entrée | notre graphie | enregistrement | licence | locuteur |',
|
|
|
|
|
|
'|---|---|---|---|---|'
|
|
|
|
|
|
);
|
|
|
|
|
|
for (const { e, trouve } of dejaEnregistres) {
|
|
|
|
|
|
const t = trouve.titre.replace('File:', '').replace(/\|/g, '\\|');
|
|
|
|
|
|
l.push(
|
|
|
|
|
|
`| \`${e.id}\` | ${e.noms.gcf} | [${t.slice(0, 52)}](${trouve.url_description}) | ${trouve.licence ?? '?'} | ${(trouve.auteur ?? '?').slice(0, 40)} |`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
l.push('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const restants = manquants.filter((e) => !dejaEnregistres.some((d) => d.e.id === e.id));
|
|
|
|
|
|
l.push(`## 🎙️ À enregistrer (${restants.length})`, '');
|
|
|
|
|
|
if (!restants.length) l.push('_Rien à enregistrer._', '');
|
|
|
|
|
|
for (const e of restants) {
|
|
|
|
|
|
const q = e.orthographe.question_ouverte;
|
|
|
|
|
|
l.push(`- [ ] **${e.noms.gcf}** — \`static/audio/${e.id}.opus\` _(${e.noms.fr ?? ''})_`);
|
|
|
|
|
|
if (q) l.push(` ⚠ Orthographe en discussion : ${q}`);
|
|
|
|
|
|
}
|
2026-07-26 23:47:27 -04:00
|
|
|
|
l.push('');
|
2026-07-27 10:40:23 -04:00
|
|
|
|
|
2026-07-26 23:47:27 -04:00
|
|
|
|
if (absents.length) {
|
2026-07-27 10:40:23 -04:00
|
|
|
|
l.push(`## ⚠ Déclarés mais fichier introuvable (${absents.length})`, '');
|
|
|
|
|
|
for (const e of absents) l.push(`- \`${e.id}\` → \`${e.audio.gcf}\` absent de \`static/\``);
|
2026-07-26 23:47:27 -04:00
|
|
|
|
l.push('');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (presents.length) {
|
|
|
|
|
|
l.push(`## En place (${presents.length})`, '');
|
2026-07-27 10:40:23 -04:00
|
|
|
|
for (const e of presents)
|
|
|
|
|
|
l.push(`- \`${e.id}\` — ${e.audio.locuteur ?? 'locuteur non renseigné'} (${e.audio.licence ?? 'licence ?'})`);
|
2026-07-26 23:47:27 -04:00
|
|
|
|
l.push('');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-27 10:40:23 -04:00
|
|
|
|
l.push(
|
|
|
|
|
|
'## Comment enregistrer',
|
|
|
|
|
|
'',
|
|
|
|
|
|
'Marche à suivre détaillée dans [`kreyol-participatif.md`](kreyol-participatif.md).',
|
|
|
|
|
|
'En deux lignes : une pièce calme, un téléphone, le mot dit deux fois, naturellement.',
|
|
|
|
|
|
'Une voix locale vaut mieux qu’un studio.',
|
|
|
|
|
|
''
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-07-26 23:47:27 -04:00
|
|
|
|
await writeFile(join(ROOT, 'docs', 'audio-a-enregistrer.md'), l.join('\n'));
|
|
|
|
|
|
|
2026-07-27 10:40:23 -04:00
|
|
|
|
console.log(`\nDéjà enregistrés par la communauté : ${dejaEnregistres.length}`);
|
|
|
|
|
|
console.log(`À enregistrer : ${restants.length}`);
|
|
|
|
|
|
console.log(`Introuvables : ${absents.length}`);
|
|
|
|
|
|
console.log(`En place : ${presents.length}`);
|
2026-07-26 23:47:27 -04:00
|
|
|
|
console.log('\n docs/audio-a-enregistrer.md');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main().catch((e) => {
|
|
|
|
|
|
console.error('✘', e.stack ?? e.message);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
});
|