Amendement de l'ADR 0005 : la relecture ne repose plus sur un relecteur unique. La porte ne s'abaisse pas — l'ouverture porte sur qui peut proposer, jamais sur qui peut valider. La communauté existait déjà - Lingua Libre compte 456 enregistrements en kréyòl gwadloupéyen, versés sur Commons sous licence libre. - Trois de nos treize noms y figuraient déjà — gouyav, kénèt, zikak — en CC BY-SA 4.0 et AVEC EXACTEMENT NOTRE GRAPHIE. Trois entrées sortent du doute par corroboration externe, sans que le projet décide quoi que ce soit. La question n'était pas qui trancherait, mais d'aller regarder. - En regard : 0 lexème Wikidata en gcf, 0 étiquette gcf sur les 13 items. Modèle de données - orthographe.statut : a_relire | en_discussion | relu. Un doute signalé sans question posée est refusé par le validateur — on ne décourage pas le lecteur, on l'appelle. 4 entrées en discussion : ake, medisinye, lorye-jonn, pom-dlo. - noms.variantes_gcf : les usages proposés sont conservés avec leur source et leur île, jamais écrasés. Une graphie identique au nom principal est refusée comme variante : c'est une corroboration, pas un désaccord. - audio : source, licence, url_description, locuteur crédité, valide. check:audio interroge Lingua Libre et trouve seul les prises existantes. Porte : 14/14 (5 cas ajoutés sur la langue et la prononciation). Pis-aller assumé : les 4 questions sont rédigées en français. Les écrire en kréyòl reviendrait à produire du kréyòl non relu au moment même où l'on reconnaît ne pas savoir le faire. Leur traduction est la première contribution demandée. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
242 lines
8.8 KiB
JavaScript
242 lines
8.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* build-dataset.mjs — compose le dataset à partir de la graine et des sorties vérifiées.
|
|
*
|
|
* fruits.seed.json (contenu éditorial d'amorçage)
|
|
* + tools/data/out/taxa-verified.json (Wikidata + GBIF, réponses d'API)
|
|
* + tools/data/out/commons-candidats.json (médias sous licence libre)
|
|
* → src/lib/data/fruits/<id>.json
|
|
*
|
|
* AMORÇAGE SEUL. Une fois les fichiers écrits, ils sont édités à la main par le porteur
|
|
* (choix des photos, assignation des vues, passage de `valide` à true, corrections kréyòl).
|
|
* Le script refuse donc d'écraser un fichier existant sans `--force`, pour ne jamais
|
|
* détruire une validation humaine.
|
|
*/
|
|
|
|
import { readFile, writeFile, mkdir, 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 DEST = join(ROOT, 'src', 'lib', 'data', 'fruits');
|
|
const FORCE = process.argv.includes('--force');
|
|
|
|
/** Nombre de médias candidats pré-inscrits par entrée. Le brief §8 en exige au moins deux. */
|
|
const MEDIAS_PAR_ENTREE = 3;
|
|
|
|
/** Licences à attribution — recopié de la classification d'ingest-commons.mjs. */
|
|
const SANS_ATTRIBUTION = new Set(['CC0-1.0', 'PD']);
|
|
const AVEC_SA = new Set(['CC-BY-SA-2.0', 'CC-BY-SA-2.5', 'CC-BY-SA-3.0', 'CC-BY-SA-4.0', 'CC-BY-NC-SA-4.0']);
|
|
const AVEC_NC = new Set(['CC-BY-NC-4.0', 'CC-BY-NC-SA-4.0']);
|
|
|
|
const slug = (s) =>
|
|
s
|
|
.replace(/^File:/, '')
|
|
.replace(/\.[a-z0-9]+$/i, '')
|
|
.normalize('NFD')
|
|
.replace(/[\u0300-\u036f]/g, '')
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-+|-+$/g, '')
|
|
.slice(0, 60)
|
|
.replace(/-+$/, '');
|
|
|
|
/**
|
|
* Une planche gravée du XVIIIe siècle est exacte mais stylisée : elle ne peut pas servir
|
|
* d'épreuve de reconnaissance du réel. On la détecte grossièrement — c'est un pré-tri,
|
|
* pas un verdict : le porteur corrige le champ à la relecture.
|
|
*/
|
|
const INDICES_PLANCHE = /blanco|merian|k[oö]hler|flora|sylva|botanical|plate|lithograph|engrav|descourtilz|1[6-9]\d\d/i;
|
|
|
|
function typeMedia(c) {
|
|
// La date de prise de vue fait partie du texte examiné : sans elle, la planche de
|
|
// Jacquin (1780) ressortait en « photo » — son titre et son auteur ne disent rien
|
|
// de son siècle. Constaté en session 0, corrigé ici.
|
|
const hay = `${c.titre} ${c.description ?? ''} ${c.auteur ?? ''} ${c.date_prise_de_vue ?? ''}`;
|
|
if (INDICES_PLANCHE.test(hay)) return 'planche_botanique';
|
|
|
|
// La photographie de terrain n'existe pas avant le XXe siècle dans ces fonds.
|
|
const annee = Number((c.date_prise_de_vue ?? '').match(/\b(1[5-9]\d\d)\b/)?.[1]);
|
|
if (annee && annee < 1900) return 'planche_botanique';
|
|
|
|
return 'photo';
|
|
}
|
|
|
|
/**
|
|
* Écart constaté dans la graine : les dangers de niveau « vigilance » (kénèt, pòm-kannèl)
|
|
* portent leur texte dans `note`, les niveaux « élevé » et « critique » dans `resume`.
|
|
* Le schéma impose un champ unique — on normalise vers `resume` sans rien réécrire.
|
|
* Écart signalé dans docs/sessions/00-cadre-et-donnees.md, conformément au brief §9.
|
|
*/
|
|
function normaliserDanger(d) {
|
|
if (!d) return null;
|
|
const { note, resume, ...reste } = d;
|
|
return { ...reste, resume: resume ?? note };
|
|
}
|
|
|
|
/**
|
|
* Les questions posées aux locuteurs, affichées telles quelles dans le jeu.
|
|
*
|
|
* Ce sont des QUESTIONS, pas des corrections : le projet n'a pas à trancher l'orthographe
|
|
* kréyòl (ADR 0005), mais il a le devoir de dire où il doute plutôt que d'afficher une
|
|
* graphie douteuse avec l'aplomb d'une certitude.
|
|
*
|
|
* Une entrée qui figure ici sort en `statut: "en_discussion"`, ce qui déclenche l'affichage
|
|
* de la question dans l'encyclopédie et dans le jeu.
|
|
*/
|
|
const QUESTIONS_KREYOL = {
|
|
ake: "Nous écrivons « aki », mais l'identifiant du fruit dit « ake ». Laquelle des deux graphies est la bonne en Guadeloupe ? Et comment le prononcez-vous chez vous ?",
|
|
medisinye: "« medsinyé » ou « medisinyé » ? Nous rencontrons les deux. Laquelle employez-vous ?",
|
|
'lorye-jonn': "« lorié-jòn » ou « lorye-jonn » ? Nous ne savons pas trancher. Comment l'écrivez-vous ?",
|
|
'pom-dlo':
|
|
"Selon les îles, « pòm-dlo », « pòm-malaka » et « pòm-rose » ne désignent pas le même fruit. Lequel appelez-vous « pòm-dlo » ?"
|
|
};
|
|
|
|
/**
|
|
* Ces questions sont rédigées en français, et c'est un pis-aller assumé : demander en
|
|
* français à des locuteurs kréyòl de corriger du kréyòl est un contresens. Mais rédiger
|
|
* la question en kréyòl reviendrait à produire du kréyòl non relu au moment même où l'on
|
|
* reconnaît ne pas savoir le faire (ADR 0005).
|
|
*
|
|
* Leur traduction en kréyòl fait donc partie des contributions demandées — c'est la
|
|
* première tâche listée dans docs/kreyol-participatif.md.
|
|
*/
|
|
|
|
/** Deux planches Blanco tronquées au même slug : on désambiguïse plutôt que de perdre un média. */
|
|
function idsUniques(medias) {
|
|
const vus = new Map();
|
|
return medias.map((m) => {
|
|
const n = (vus.get(m.id) ?? 0) + 1;
|
|
vus.set(m.id, n);
|
|
return n === 1 ? m : { ...m, id: `${m.id}-${n}` };
|
|
});
|
|
}
|
|
|
|
async function existe(p) {
|
|
try {
|
|
await access(p);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const seed = JSON.parse(await readFile(join(ROOT, 'fruits.seed.json'), 'utf8'));
|
|
const taxa = JSON.parse(await readFile(join(ROOT, 'tools/data/out/taxa-verified.json'), 'utf8'));
|
|
const medias = JSON.parse(await readFile(join(ROOT, 'tools/data/out/commons-candidats.json'), 'utf8'));
|
|
|
|
const parId = (arr) => new Map(arr.map((r) => [r.id, r]));
|
|
const T = parId(taxa.resultats);
|
|
const M = parId(medias.resultats);
|
|
const dateVerif = taxa.genere_le.slice(0, 10);
|
|
|
|
const entrees = [
|
|
...seed.fruits.map((f) => ({ ...f, categorie: 'fruit' })),
|
|
...seed.plantes_dangereuses.map((p) => ({ ...p, categorie: 'plante_dangereuse' }))
|
|
];
|
|
|
|
await mkdir(DEST, { recursive: true });
|
|
let ecrits = 0;
|
|
let ignores = 0;
|
|
|
|
for (const e of entrees) {
|
|
const chemin = join(DEST, `${e.id}.json`);
|
|
if (!FORCE && (await existe(chemin))) {
|
|
console.log(`· ${e.id} — existe déjà, conservé (--force pour écraser)`);
|
|
ignores++;
|
|
continue;
|
|
}
|
|
|
|
const t = T.get(e.id);
|
|
if (!t?.wikidata?.qid || !t?.gbif?.usageKey) {
|
|
console.error(`✘ ${e.id} — taxonomie non vérifiée, entrée non écrite`);
|
|
continue;
|
|
}
|
|
|
|
const candidats = idsUniques(
|
|
(M.get(e.id)?.candidats ?? []).slice(0, MEDIAS_PAR_ENTREE).map((c) => ({
|
|
id: slug(c.titre),
|
|
filiere: 'A',
|
|
type_media: typeMedia(c),
|
|
vue: null,
|
|
usages: [],
|
|
source: 'wikimedia_commons',
|
|
auteur: c.auteur,
|
|
licence: c.licence,
|
|
licence_url: c.licence_url ?? null,
|
|
url_fichier: c.url_fichier,
|
|
url_description: c.url_description,
|
|
largeur: c.largeur ?? null,
|
|
hauteur: c.hauteur ?? null,
|
|
mime: c.mime ?? null,
|
|
description: c.description ?? null,
|
|
date_prise_de_vue: c.date_prise_de_vue ?? null,
|
|
attribution_requise: !SANS_ATTRIBUTION.has(c.licence),
|
|
partage_identique: AVEC_SA.has(c.licence),
|
|
usage_non_commercial: AVEC_NC.has(c.licence),
|
|
composite: false,
|
|
valide: false,
|
|
valide_par: null,
|
|
valide_le: null,
|
|
note_validation: null
|
|
}))
|
|
);
|
|
|
|
const entree = {
|
|
$schema: '../../../../fruits.schema.json',
|
|
id: e.id,
|
|
categorie: e.categorie,
|
|
noms: { ...e.noms, variantes_gcf: [] },
|
|
orthographe: {
|
|
norme: 'gwadloupeyen',
|
|
statut: QUESTIONS_KREYOL[e.id] ? 'en_discussion' : 'a_relire',
|
|
relu_par: null,
|
|
question_ouverte: QUESTIONS_KREYOL[e.id] ?? null
|
|
},
|
|
taxon: {
|
|
scientifique: t.gbif.canonicalName ?? e.taxon.scientifique,
|
|
famille: t.gbif.famille ?? e.taxon.famille,
|
|
wikidata: t.wikidata.qid,
|
|
gbif: t.gbif.usageKey,
|
|
verifie_le: dateVerif,
|
|
verifie_contre: ['wikidata', 'gbif']
|
|
},
|
|
iles: e.iles,
|
|
...(e.categorie === 'fruit' ? { saison_mois: e.saison_mois } : {}),
|
|
// Les cinq plantes du module « Pa touché » sortent à comestible: false.
|
|
// C'est le défaut prudent, pas la vérité complète : l'akée mûre et la pomme-cajou
|
|
// se mangent sous conditions strictes. La nuance vit dans danger.resume et doit
|
|
// être arbitrée par un humain avant publication (voir docs/a-arbitrer.md).
|
|
comestible: e.categorie === 'fruit' ? (e.comestible ?? true) : false,
|
|
danger: normaliserDanger(e.danger),
|
|
indices: e.indices ?? {},
|
|
medias: candidats,
|
|
audio: {
|
|
gcf: null,
|
|
locuteur: null,
|
|
source: null,
|
|
licence: null,
|
|
url_description: null,
|
|
valide: false,
|
|
note: null
|
|
},
|
|
sources_factuelles: [],
|
|
valide: false
|
|
};
|
|
|
|
if (e.$comment) entree.$comment = e.$comment;
|
|
|
|
await writeFile(chemin, JSON.stringify(entree, null, '\t') + '\n');
|
|
console.log(`✔ ${e.id} — ${candidats.length} médias, ${entree.taxon.wikidata} / ${entree.taxon.gbif}`);
|
|
ecrits++;
|
|
}
|
|
|
|
console.log(`\n${ecrits} entrée(s) écrite(s), ${ignores} conservée(s).`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error('✘', err.message);
|
|
process.exit(1);
|
|
});
|