#!/usr/bin/env node /** * check-audio.mjs — état des prononciations kréyòl. * * 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 ». * * 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. * * 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 */ 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'); 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)); async function existe(p) { try { await access(p); return true; } catch { return false; } } 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; } async function main() { const horsLigne = process.argv.includes('--hors-ligne'); const fichiers = (await readdir(DATA)).filter((f) => f.endsWith('.json')).sort(); const manquants = []; const absents = []; const presents = []; const dejaEnregistres = []; let totalLL = 0; if (!horsLigne) { try { totalLL = await tailleCategorie(); } catch { console.warn('⚠ Lingua Libre injoignable — rapport local uniquement.'); } } for (const f of fichiers) { const e = JSON.parse(await readFile(join(DATA, f), 'utf8')); if (e.audio.gcf) { if (await existe(join(STATIC, e.audio.gcf))) presents.push(e); else absents.push(e); } else { manquants.push(e); } 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)'); } } } const l = [ '# Prononciations kréyòl — état et travail à faire', '', '> 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.', '', 'Format embarqué : `.opus`, un fichier par entrée dans `static/audio/.opus`.', '' ]; 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}`); } l.push(''); if (absents.length) { 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/\``); l.push(''); } if (presents.length) { l.push(`## En place (${presents.length})`, ''); for (const e of presents) l.push(`- \`${e.id}\` — ${e.audio.locuteur ?? 'locuteur non renseigné'} (${e.audio.licence ?? 'licence ?'})`); l.push(''); } 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.', '' ); await writeFile(join(ROOT, 'docs', 'audio-a-enregistrer.md'), l.join('\n')); 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}`); console.log('\n docs/audio-a-enregistrer.md'); } main().catch((e) => { console.error('✘', e.stack ?? e.message); process.exit(1); });