denckring-fr-data 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,368 @@
1
+ """French lexicon data for denckring.
2
+
3
+ Installing this gives the French pack `lexicon.words`, `lexicon.nouns`,
4
+ `lexicon.glosses` and `lexicon.graded_words`. Nothing branches on whether it is
5
+ present — the same procedures answer the same calls, in a third language.
6
+
7
+ Two sources, both CC BY-SA 4.0: Lexique 3.82 for membership, nouns and frequency,
8
+ and French Wiktionary for definitions. ADR 0032 records why Wikidata Lexemes,
9
+ which supplies German, could not supply French.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import gzip
15
+ import re
16
+ from collections.abc import Mapping, Sequence
17
+ from functools import lru_cache
18
+ from importlib.resources import files
19
+ from pathlib import Path
20
+ from types import MappingProxyType
21
+ from typing import ClassVar, TypeVar
22
+
23
+ from denckring.core.errors import MissingCapability
24
+ from denckring.lang.base import (
25
+ ALPHABET,
26
+ FOLD_DIACRITICS,
27
+ GLOSSES,
28
+ GRADED_WORDS,
29
+ LETTER_SHAPES,
30
+ NOUNS,
31
+ PHONEMES,
32
+ SYLLABLES,
33
+ SYLLABLES_DICTIONARY,
34
+ SYLLABLES_HEURISTIC,
35
+ TOKENS,
36
+ WORDS,
37
+ )
38
+ from denckring.lang.fr import FrenchPack
39
+ from denckring_fr_data.elision import count_line
40
+ from denckring_fr_data.sampa import IPA_VOWELS, to_phonemes
41
+
42
+ #: Orthographic vowel runs, for the estimate a word outside Lexique gets.
43
+ #: Answers "roughly how long is this unknown word" from spelling alone --
44
+ #: deliberately not shared with `elision.py`'s pattern (Task 6), which answers
45
+ #: "does this known word end in a mute e" over a different alphabet of
46
+ #: concerns entirely.
47
+ _VOWEL_RUN = re.compile(r"[aeiouyàâäéèêëîïôöùûüÿœæ]+")
48
+
49
+ #: What `look_up` returns is whatever the table holds, and the caller must get
50
+ #: its own type back. The same shape as `denckring_de_wiktionary.look_up`.
51
+ _V = TypeVar("_V", bound=Sequence[str])
52
+
53
+ WORDS_PATH = Path(str(files("denckring_fr_data") / "data" / "words.txt.gz"))
54
+ NOUNS_PATH = Path(str(files("denckring_fr_data") / "data" / "nouns.txt.gz"))
55
+ GRADED_WORDS_PATH = Path(str(files("denckring_fr_data") / "data" / "graded_words.txt.gz"))
56
+ GLOSSES_PATH = Path(str(files("denckring_fr_data") / "data" / "glosses.txt.gz"))
57
+ SYLLABLES_PATH = Path(str(files("denckring_fr_data") / "data" / "syllables.txt.gz"))
58
+ H_ASPIRE_PATH = Path(str(files("denckring_fr_data") / "data" / "h_aspire.txt.gz"))
59
+
60
+ __version__ = "0.1.0"
61
+
62
+
63
+ def _read(path: Path) -> tuple[str, ...]:
64
+ """Every line of a gzipped list.
65
+
66
+ A truncated file raises rather than returning a short list: a short noun list
67
+ makes N+7 quietly wrong, and a wrong answer is worse than an exception.
68
+ """
69
+ with gzip.open(path, "rt", encoding="utf-8") as handle:
70
+ return tuple(line for line in handle.read().split("\n") if line)
71
+
72
+
73
+ @lru_cache(maxsize=1)
74
+ def noun_list() -> tuple[str, ...]:
75
+ """Every noun form, in dictionary order. N+7 walks this, so order is the point."""
76
+ return _read(NOUNS_PATH)
77
+
78
+
79
+ @lru_cache(maxsize=1)
80
+ def noun_positions() -> dict[str, int]:
81
+ return {word.casefold(): index for index, word in enumerate(noun_list())}
82
+
83
+
84
+ @lru_cache(maxsize=1)
85
+ def known_words() -> frozenset[str]:
86
+ """Membership, from every form Lexique carries."""
87
+ return frozenset(_read(WORDS_PATH))
88
+
89
+
90
+ @lru_cache(maxsize=1)
91
+ def graded_words() -> Mapping[str, int]:
92
+ """Word to band, where **larger means less common** — SCOWL's direction."""
93
+ table: dict[str, int] = {}
94
+ with gzip.open(GRADED_WORDS_PATH, mode="rt", encoding="utf-8") as handle:
95
+ for line in handle:
96
+ word, _, band = line.rstrip("\n").partition("\t")
97
+ if band:
98
+ table[word] = int(band)
99
+ return table
100
+
101
+
102
+ @lru_cache(maxsize=1)
103
+ def gloss_table() -> dict[str, tuple[str, ...]]:
104
+ """Headword to every sense's definition, in Wiktionary's order.
105
+
106
+ Split on `" | "` rather than `"|"`, because a definition may contain a bare
107
+ pipe and the build rejects any that contains the spaced separator.
108
+ """
109
+ with gzip.open(GLOSSES_PATH, mode="rt", encoding="utf-8") as handle:
110
+ return {
111
+ key: tuple(values.split(" | "))
112
+ for key, _, values in (line.rstrip("\n").partition("\t") for line in handle)
113
+ if values
114
+ }
115
+
116
+
117
+ @lru_cache(maxsize=1)
118
+ def syllable_table() -> Mapping[str, tuple[int, str, str]]:
119
+ """spelling -> (citation syllables, Lexique SAMPA, orthographic syllabation).
120
+
121
+ The third element is the segmented spelling (`car-ros-se`), which serves two
122
+ callers: `syllables()` returns its segments, and the mute-e rules compare
123
+ its segment count against `nbsyll` to judge a final `-ent`.
124
+ """
125
+ table: dict[str, tuple[int, str, str]] = {}
126
+ for line in _read(SYLLABLES_PATH):
127
+ ortho, nbsyll, phon, osyll = line.split("\t")
128
+ table[ortho] = (int(nbsyll), phon, osyll)
129
+ return MappingProxyType(table)
130
+
131
+
132
+ def _table_entry(word: str) -> tuple[int, str, str] | None:
133
+ """The table entry for `word`, falling back past a leading elision.
134
+
135
+ Task 6's tokeniser (`elision._TOKEN_RE`) deliberately keeps an
136
+ apostrophe-bearing token whole -- `aujourd'hui` and `prud'homme` are 94
137
+ real Lexique entries, and splitting every apostrophe unconditionally
138
+ would silently misread those as a proclitic plus a word. So the whole
139
+ lowercased form is tried first, exactly as before.
140
+
141
+ But not every apostrophe-bearing form is one of those 94: `d'espoir`,
142
+ `l'amour`, `qu'un` are ordinary elision, the proclitic written onto the
143
+ following word, and were never table entries themselves. A whole-form-only
144
+ lookup made `phonemes`/`rhyme_key`/`rhyme_keys`/`syllable_count` fail on
145
+ them with `MissingCapability`, which is a routine event elsewhere but
146
+ silently starved `rhyme_scheme` and `ghazal` of exactly the line-ending
147
+ words French verse ends on most often (fix round 2). The rhyme- and
148
+ syllable-bearing unit of an elided form is what follows the apostrophe,
149
+ so a whole-form miss retries there -- the segment after the LAST
150
+ apostrophe, in case of a chained elision -- before giving up.
151
+ """
152
+ table = syllable_table()
153
+ lower = word.casefold()
154
+ entry = table.get(lower)
155
+ if entry is not None:
156
+ return entry
157
+ if "'" not in lower and "’" not in lower: # noqa: RUF001
158
+ return None
159
+ tail = re.split(r"['’]", lower)[-1] # noqa: RUF001
160
+ return table.get(tail)
161
+
162
+
163
+ @lru_cache(maxsize=1)
164
+ def h_aspire() -> frozenset[str]:
165
+ """Words whose initial h blocks elision. Lexique cannot answer this."""
166
+ return frozenset(_read(H_ASPIRE_PATH))
167
+
168
+
169
+ def look_up(table: Mapping[str, _V], word: str) -> _V | None:
170
+ """The token as written, then case-flipped.
171
+
172
+ French Wiktionary titles are case-sensitive, and the build stores them as
173
+ written: `France`, `Paris` and `Toulouse` are the titles, not `france`,
174
+ `paris`, `toulouse`. Looking a headword up through `_lemma` — which
175
+ casefolds and drops every non-letter — therefore misses every capitalised
176
+ and every hyphenated title. Measured over the shipped table: 261,638 of
177
+ 510,973 headwords (51.2%) were unreachable that way, 121,388 of them to case
178
+ alone, and proper nouns are the material loss. Trying the flipped case
179
+ recovers them, and the reverse direction recovers a proper noun someone
180
+ lowercased.
181
+
182
+ On the word **as written**, never `_lemma`'d: accents are meaning here, and
183
+ folding them would collide `côte` with `cote`. Same reason ADR 0009 makes
184
+ folding a decision of the procedure rather than of the lexicon. This is the
185
+ shape `denckring_de_wiktionary.look_up` already uses; French diverged from it
186
+ only by oversight, which is the same key/lookup mismatch fixed for the noun
187
+ list in fdad0c8.
188
+ """
189
+ for candidate in (word, word.capitalize(), word.lower(), word.upper()):
190
+ found = table.get(candidate)
191
+ if found:
192
+ return found
193
+ return None
194
+
195
+
196
+ class FrenchDataPack(FrenchPack):
197
+ """French with a lexicon behind it: membership, nouns, glosses and frequency.
198
+
199
+ A fixed `ClassVar`, as on every pack but German's: German's `pack()` factory
200
+ (`denckring_de_data`) exists because two distributions carry German data
201
+ under two different licences and the registry allows only one `de` entry
202
+ point to win. This distribution is the only source of French lexicon data,
203
+ under one licence, so there is nothing for the install to choose between —
204
+ a plain class is the honest shape.
205
+ """
206
+
207
+ capabilities: ClassVar[frozenset[str]] = frozenset(
208
+ {
209
+ TOKENS,
210
+ ALPHABET,
211
+ FOLD_DIACRITICS,
212
+ LETTER_SHAPES,
213
+ NOUNS,
214
+ WORDS,
215
+ GLOSSES,
216
+ GRADED_WORDS,
217
+ SYLLABLES,
218
+ SYLLABLES_DICTIONARY,
219
+ SYLLABLES_HEURISTIC,
220
+ PHONEMES,
221
+ }
222
+ )
223
+
224
+ def is_word(self, word: str) -> bool:
225
+ return self._lemma(word) in known_words()
226
+
227
+ def syllable_count(self, word: str) -> tuple[int, bool]:
228
+ """Lexique's citation count, or a vowel-run estimate outside it.
229
+
230
+ The count is the *citation* one: `femme` is one syllable here and
231
+ frequently two in verse. The line rules, not this method, are where
232
+ verse is answered -- spec D3.
233
+
234
+ `_table_entry` tries the whole form first and only then falls back
235
+ past a leading elision (`d'espoir` -> `espoir`), so `aujourd'hui`
236
+ still resolves whole and an ordinary elided form no longer falls
237
+ through to the vowel-run estimate for no reason (fix round 2).
238
+ """
239
+ entry = _table_entry(word)
240
+ if entry is not None:
241
+ return entry[0], True
242
+ return max(len(_VOWEL_RUN.findall(word.casefold())), 1), False
243
+
244
+ def line_syllables(self, line: str) -> tuple[int, int]:
245
+ """French counts a line, not a bag of words. Spec D3, ADR 0034."""
246
+ return count_line(line, syllable_table(), h_aspire())
247
+
248
+ def syllables(self, word: str) -> list[str]:
249
+ """The orthographic segments, or `MissingCapability` for a word
250
+ Lexique does not carry.
251
+
252
+ Not `KeyError`: a plain `KeyError` here would repeat exactly the
253
+ crash fix-round 1 found in `phonemes` and `rhyme_key`/`rhyme_keys`,
254
+ and `MissingCapability` on an out-of-vocabulary word is a routine
255
+ event in real verse rather than an edge case, the same convention
256
+ every other lookup on this pack follows.
257
+
258
+ Task 6 shipped in ADR 0034 and its line rules do not call this
259
+ method -- `line_syllables` reads `syllable_table()` directly (see
260
+ `elision.count_line`), because it needs `nbsyll` and `phon` together
261
+ and `syllables()` exposes neither. As of the whole-branch review
262
+ (2026-09-01) nothing outside the test suite calls `syllables()` at
263
+ all; it exists to answer the `syllables` capability directly, for a
264
+ caller that wants the segmentation itself rather than a count.
265
+
266
+ It diverges from every other lookup on this pack in one respect:
267
+ it is the only one of the four (`syllables`, `phonemes`,
268
+ `syllable_count`, `rhyme_key`) not routed through `_table_entry`, so
269
+ it does not fall back past a leading elision. `phonemes("d'espoir")`
270
+ succeeds (`_table_entry` retries on `espoir` after the whole form
271
+ misses) while `syllables("d'espoir")` raises `MissingCapability` --
272
+ looking the whole elided form up directly, with no retry. This
273
+ divergence is deliberate, not an oversight: `_table_entry`'s retry
274
+ returns the *following* word's entry, and `syllables()`'s job is to
275
+ segment the word exactly as it was asked for, so returning
276
+ `espoir`'s segments (`es-poir`) for the token `d'espoir` would be
277
+ answering a question that was not asked -- silently dropping the
278
+ `d'` rather than reporting that the whole elided form is not one of
279
+ Lexique's 125,653 orthographic keys. No test exercises this path,
280
+ because nothing calls it outside the test suite.
281
+ """
282
+ entry = syllable_table().get(word.casefold())
283
+ if entry is None:
284
+ raise MissingCapability(f"<word {word!r}>", self.lang, SYLLABLES)
285
+ return entry[2].split("-")
286
+
287
+ def phonemes(self, word: str) -> list[str]:
288
+ """The IPA transcription, or `MissingCapability` for a word Lexique
289
+ does not carry.
290
+
291
+ Not `KeyError`: `core/prosody.py`'s `word_rhyme_keys` and
292
+ `assonance_constraint`/`spoonerism`'s own lookups catch only
293
+ `MissingCapability` around a pack call, reading it as "this word is
294
+ undecidable" rather than letting it propagate. A plain `KeyError`
295
+ went uncaught there -- `check('assonance_constraint', ..., lang='fr')`
296
+ crashed outright on the first unknown word, which is routine in real
297
+ French text, not an edge case. Matches the convention
298
+ `denckring-en-data` and `denckring-de-wiktionary` already established
299
+ for exactly this call.
300
+
301
+ `_table_entry` tries the whole form first and only then falls back
302
+ past a leading elision (fix round 2) -- see its docstring. `rhyme_key`
303
+ and `rhyme_keys` both call this method, so the fallback reaches them
304
+ for free.
305
+ """
306
+ entry = _table_entry(word)
307
+ if entry is None:
308
+ raise MissingCapability(f"<word {word!r}>", self.lang, PHONEMES)
309
+ return to_phonemes(entry[1])
310
+
311
+ def is_vowel_phoneme(self, phoneme: str) -> bool:
312
+ return phoneme in IPA_VOWELS
313
+
314
+ def rhyme_key(self, word: str) -> str:
315
+ """Phonemes from the last vowel to the end of the word.
316
+
317
+ The base definition says "from the last primary-stressed vowel", but
318
+ French has no lexical stress (spec D2), so the final vowel is the
319
+ rhyme-bearing one -- the *rime suffisante* every French prosodist
320
+ assumes. Feminine/masculine alternation is NOT modelled here; this
321
+ project's `rhyme_scheme` models it in no language, and that stays a
322
+ separate, recorded gap rather than something this method papers over.
323
+
324
+ Raises `MissingCapability` for a word Lexique does not carry, via
325
+ `phonemes()` -- a guessed rhyme is worse than an unknown one, and
326
+ `core.prosody.word_rhyme_keys` catches exactly that exception around
327
+ `rhyme_keys` and reads it as "this word is undecidable", which is what
328
+ makes an unknown word in a French poem non-fatal rather than an
329
+ uncaught crash (fix-round 1: it was `KeyError` and unhandled there).
330
+ """
331
+ phones = self.phonemes(word)
332
+ for index in range(len(phones) - 1, -1, -1):
333
+ if self.is_vowel_phoneme(phones[index]):
334
+ return "".join(phones[index:])
335
+ return "".join(phones)
336
+
337
+ def rhyme_keys(self, word: str) -> list[str]:
338
+ """Every pronunciation's rhyme key -- one, since the table holds a
339
+ single transcription per spelling (ADR 0034 records the homograph
340
+ cost that comes with that)."""
341
+ return [self.rhyme_key(word)]
342
+
343
+ def nouns(self) -> tuple[str, ...]:
344
+ return noun_list()
345
+
346
+ def noun_index(self, word: str) -> int | None:
347
+ return noun_positions().get(self._lemma(word))
348
+
349
+ def glosses(self, word: str) -> tuple[str, ...]:
350
+ """Every sense, or nothing. Empty rather than raising, matching English."""
351
+ return look_up(gloss_table(), word) or ()
352
+
353
+ def graded_words(self) -> Mapping[str, int]:
354
+ """A read-only view over the cached table, for the reason English gives:
355
+ the module function is `lru_cache`d and shared, so handing the dict out
356
+ would let one caller's mutation corrupt it for all the others."""
357
+ return MappingProxyType(graded_words())
358
+
359
+ @staticmethod
360
+ def _lemma(word: str) -> str:
361
+ """Casefold, and keep accents.
362
+
363
+ English strips to ASCII here. French must not: `fold_diacritics` would
364
+ collide `côte` with `cote` and `pêcheur` with `pecheur`, and ADR 0009
365
+ makes folding a parameter of the procedure rather than a property of the
366
+ lexicon. German keeps its umlauts for the same reason.
367
+ """
368
+ return "".join(ch for ch in word.casefold() if ch.isalpha())
Binary file
Binary file
@@ -0,0 +1,12 @@
1
+ {
2
+ "counts": {
3
+ "glosses.txt.gz": 510973,
4
+ "graded_words.txt.gz": 125343,
5
+ "h_aspire.txt.gz": 3370,
6
+ "nouns.txt.gz": 44746,
7
+ "syllables.txt.gz": 125653,
8
+ "words.txt.gz": 125343
9
+ },
10
+ "generated": "2026-09-01",
11
+ "source": "Lexique 3.82 (lexique.org), CC BY-SA 4.0, and French Wiktionary, CC BY-SA 4.0 - see LICENSE-LEXIQUE, LICENSE-WIKTIONARY and scripts/build_lexicon.py"
12
+ }
Binary file
Binary file
Binary file
@@ -0,0 +1,329 @@
1
+ """French verse counts a line, not a sequence of words (spec D3).
2
+
3
+ A final mute e is a syllable before a consonant, elides before a vowel or a
4
+ mute h, and never counts at the end of a line. Lexique gives citation forms --
5
+ `femme`, `une`, `belle` and `porte` are all one syllable there and frequently
6
+ two in verse -- so summing them undercounts systematically.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import re
12
+ from collections.abc import Mapping
13
+
14
+ VOWELS = frozenset("aeiouyàâéèêëîïôöùûüœæ")
15
+ _VOWEL_RUN = re.compile(r"[aeiouyàâéèêëîïôöùûüœæ]+")
16
+
17
+ #: SAMPA consonants (Lexique's alphabet, `sampa.SAMPA_TO_IPA` minus the
18
+ #: vowels and the three glides j/w/8). Used only by the diérèse pass below.
19
+ _SAMPA_CONSONANTS = frozenset("bdfgklmnNpRsStvzZGx")
20
+
21
+
22
+ def _dierese_extra(ortho: str, phon: str) -> int:
23
+ """Extra syllables classical diérèse adds to Lexique's citation count.
24
+
25
+ ADR 0034. Classical French diérèse -- whether a Latin/Greek-derived `i`
26
+ or `u` glide (`diadème`, `division`) counts as its own syllable, as
27
+ against a native word's fixed glide (`pied`, `moindre`) that never does
28
+ -- is etymological, not spelling-derived (Task 7 brief). The rule
29
+ shipped here: every `j` after any consonant, single or a cluster, is an
30
+ extra syllable, gated on the word ending in the `-ion(s)` noun suffix
31
+ (the textbook diérèse site: Latin `-io`/`-ionem`) or starting with
32
+ `dia-`. Measured against Racine's *Mithridate* and Hugo's *Hernani*
33
+ (interior lines only; the harness that produced the table below was a
34
+ throwaway script, per spec, and is not shipped -- ADR 0034 D5 carries
35
+ the rule and the table, not the method), this is **Racine 89.3% ->
36
+ 90.3%, Hugo 79.3% -> 80.2%**, against a
37
+ diérèse ceiling of 96.6%/84.8% -- a mechanical rule takes about 1.0 of
38
+ the 7.3 points a perfect per-site oracle would.
39
+
40
+ Three mechanical variants were measured on the way here, none of which
41
+ ships unchanged:
42
+
43
+ - a glide after a consonant CLUSTER ending in a liquid (R, l): worse
44
+ than no rule at all on both texts (84.5%/74.7% against the
45
+ 89.3%/79.3% baseline);
46
+ - every `j` after a single consonant, excluding verb-inflection and
47
+ adjective endings that are reliably synérèse (`-iez`, `-ions`, `-ier`,
48
+ `-ien`, `-iable`, `-iant`, `-iance` and their plurals/feminines) --
49
+ those grammatical endings are the plurality of every "j after a single
50
+ consonant" site in the whole Lexique table, which is why a still
51
+ blunter blanket rule scored a catastrophic 49.5%/36.6%; excluding them
52
+ only brought it to 86.7%/70.0%, still below baseline on Hugo;
53
+ - `j` after a single consonant, narrowed further to the `-ion(s)` noun
54
+ suffix and the `dia-` prefix: the only variant that beat baseline on
55
+ BOTH texts, measured **as tried, with the single-consonant
56
+ restriction still in place, at 90.3%/80.0%**.
57
+
58
+ The `-ion(s)`/`dia-` gate is the shape that third variant kept. Its
59
+ single-consonant restriction was not: fix round 1 found it carried over
60
+ from the first, rejected variant (cluster-ending-in-a-liquid) without
61
+ independent justification, isolated it, and measured it as actively
62
+ costing 0.2 points on Hugo -- for no gain on Racine -- by wrongly
63
+ suppressing `factions` and `predictions`, both genuine `-ion` diérèse
64
+ sites whose `j` follows a `ks`/`ts` cluster. It was dropped; every `j`
65
+ after any consonant now counts once the gate passes, which is the
66
+ 90.3%/80.2% above, not the 90.3%/80.0% the gated variant measured.
67
+
68
+ None of the three reached the 94% accept bar -- this is a STOP, not an
69
+ ACCEPT -- so it ships as the honest ceiling of a three-variant budget,
70
+ not a solved rule: `line_syllables`'s count stays an estimate
71
+ (spec D4), never dictionary-exact. `w` and `8` are left out entirely:
72
+ they overwhelmingly spell native `oi`/`ou`/`ui` digraphs that are never
73
+ split, and no variant that touched them improved on one that ignored
74
+ them.
75
+
76
+ Whole-branch review (2026-09-01) found an undocumented `-iation`/`-iations`
77
+ exclusion on the `-ion(s)` gate -- present in code, named nowhere: not
78
+ here, not in ADR 0034 D5, not in the plan. Re-measured against Racine and
79
+ Hugo interior lines with and without it: **identical to four significant
80
+ figures both ways, 90.3%/80.2%**, because neither corpus contains a
81
+ French `-iation`/`-iations` word at all (the only hits are English
82
+ "appreciation"/"abbreviation"/"associations" in Hugo's front matter,
83
+ outside any verse block). An exclusion with no measured effect and no
84
+ citable justification earns nothing, so it was dropped rather than
85
+ documented: `aviation`, `association` and `initiation` now take diérèse
86
+ like any other `-ion(s)` word, on the same unverified footing as the
87
+ rest of the gate.
88
+ """
89
+ is_ion = ortho.endswith(("ion", "ions"))
90
+ if not (is_ion or ortho.startswith("dia")):
91
+ return 0
92
+ extra = 0
93
+ for index, char in enumerate(phon):
94
+ if char != "j" or index == 0 or phon[index - 1] not in _SAMPA_CONSONANTS:
95
+ continue
96
+ extra += 1
97
+ return extra
98
+
99
+
100
+ #: Proclitics whose own vowel is already elided in the spelling. They carry no
101
+ #: syllable, but they are CONSONANTS for the word in front of them: in
102
+ #: "ne t'attendais" the schwa of `ne` cannot elide across the `t'`. Dropping
103
+ #: them from the token stream cost six points in the go/no-go prototype.
104
+ ELIDED_ZERO: frozenset[str] = frozenset({"l", "d", "j", "n", "m", "t", "s", "c", "qu"})
105
+ #: Same, but these carry syllables: `lorsqu'il` is lors-qu'il.
106
+ ELIDED_WORD: dict[str, str] = {
107
+ "lorsqu": "lorsque",
108
+ "puisqu": "puisque",
109
+ "quoiqu": "quoique",
110
+ "jusqu": "jusque",
111
+ }
112
+
113
+ #: Word-shaped tokens, keeping an internal apostrophe joined rather than
114
+ #: splitting on it -- the same shape as `denckring.lang.base.WORD_RE`. Fix
115
+ #: round 1: splitting unconditionally corrupted the 94 Lexique entries keyed
116
+ #: WITH an internal apostrophe (`aujourd'hui`, `prud'homme`), which are real
117
+ #: single words, not a proclitic plus a word -- `count_line` tries the whole
118
+ #: token against the table first and only falls back to splitting when that
119
+ #: fails (`l'ami` is not a table entry, so it still splits). Hyphens and
120
+ #: whitespace both fall outside `\w` in a Unicode pattern, so a compound like
121
+ #: "dit-elle" tokenises as two tokens without any special case for the hyphen.
122
+ _TOKEN_RE = re.compile(r"[^\W\d_]+(?:['’][^\W\d_]+)*") # noqa: RUF001
123
+
124
+
125
+ def latent_schwa(ortho: str, nbsyll: int, phon: str, orthosyll: str) -> tuple[int, bool]:
126
+ """(syllables excluding any mute e, whether a mute e is pending).
127
+
128
+ Lexique's SAMPA `@` is the nasal /ɑ̃/, NOT a schwa -- `dans` is `d@`.
129
+ Schwa is `2`, which also spells /ø/, so `de` and `deux` are both `d2` and
130
+ only the bare orthographic -e separates them.
131
+ """
132
+ suffix = next((s for s in ("ent", "es", "e") if ortho.endswith(s)), None)
133
+ if suffix is None:
134
+ return nbsyll, False
135
+ if suffix == "e" and phon[-1:] in ("2", "°"):
136
+ # `le`, `je`, `que`: Lexique counted the schwa, so take it back out.
137
+ return max(nbsyll - 1, 0), True
138
+ if len(orthosyll.split("-")) > nbsyll:
139
+ # `bel-le` against 1 shows the mute e; `sou-vent` against 2 does not.
140
+ return nbsyll, True
141
+ if suffix == "ent":
142
+ # orthosyll already judged it. `vient` strips to `vi`, whose one vowel
143
+ # run matches nbsyll 1, so the fallback below would invent a mute e.
144
+ return nbsyll, False
145
+ # orthosyll merges the mute e after a vowel (`vie`, `joie`, `an-née`), so
146
+ # fall back to vowel letter-runs. `qu`/`gu` carry a silent u.
147
+ stripped = ortho[: -len(suffix)].replace("qu", "k").replace("gu", "g")
148
+ return nbsyll, len(_VOWEL_RUN.findall(stripped)) == nbsyll
149
+
150
+
151
+ def _estimate(ortho: str) -> tuple[int, bool]:
152
+ """A vowel-run guess for a word Lexique does not carry.
153
+
154
+ `line_syllables` must degrade gracefully rather than raise -- unlike
155
+ `syllables()`/`phonemes()`, which raise `MissingCapability` for exactly
156
+ this case (Task 5). A real line of French routinely holds a word outside
157
+ Lexique's 125,653 rows, and one unknown word must not blank the count for
158
+ the whole line. Deliberately excludes `-ent` from the suffix check TRAP 3
159
+ warns about: with no `orthosyll` to consult, there is no way to tell
160
+ `chantent` from `vient`, so the safer default is to never invent a mute e
161
+ there at all.
162
+ """
163
+ suffix = next((s for s in ("es", "e") if ortho.endswith(s) and ortho != s), None)
164
+ if suffix is None:
165
+ return max(len(_VOWEL_RUN.findall(ortho)), 1), False
166
+ stripped = ortho[: -len(suffix)]
167
+ return max(len(_VOWEL_RUN.findall(stripped)), 1), True
168
+
169
+
170
+ def _judge(ortho: str, entry: tuple[int, str, str]) -> tuple[int, bool, bool]:
171
+ """(syllables excluding a pending mute e, mute e pending, judged).
172
+
173
+ A table hit's `nbsyll` is a citation fact, not a guess -- Lexique's
174
+ dictionary count for a known spelling is exact. What sits on top of it is
175
+ not: whether a mute e is pending (elides or counts depending on the next
176
+ word) and whether classical diérèse adds a syllable are both editorial
177
+ judgments the spelling alone does not settle (`extraordinaire` is five
178
+ syllables or six depending on the poet, D5's own example). Finding 3 of
179
+ the whole-branch review: spec D4 and ADR 0034 both say the French line
180
+ count is `exact=False`, always, and before this the flag only fired on an
181
+ out-of-vocabulary word -- a line entirely of known words with a pending
182
+ mute e or a diérèse site reported `estimated_words == 0`, confidently
183
+ wrong in the D4/`test_prosody_robustness`-flagged sense. `judged` is True
184
+ when either call was made, so `count_line` can fold it into the same
185
+ `estimated_words` counter it already keeps for out-of-vocabulary words.
186
+ """
187
+ nbsyll, phon, orthosyll = entry
188
+ count, pending = latent_schwa(ortho, nbsyll, phon, orthosyll)
189
+ extra = _dierese_extra(ortho, phon)
190
+ return count + extra, pending, pending or extra > 0
191
+
192
+
193
+ def _lookup(ortho: str, table: Mapping[str, tuple[int, str, str]]) -> tuple[int, bool, bool]:
194
+ """(syllables excluding a pending mute e, mute e pending, estimated).
195
+
196
+ Diérèse (Task 7, ADR 0034) is added here rather than at the caller: it is
197
+ a property of the word's own phonetics, the same footing as the mute-e
198
+ count it sits beside, and an unknown word (`_estimate`) has no `phon` to
199
+ judge it by, so the diérèse pass only ever runs on a table hit.
200
+
201
+ `estimated` covers two different reasons now (Finding 3): a word outside
202
+ Lexique altogether (`_estimate`, always estimated), and a table hit whose
203
+ count rests on a mute-e or diérèse judgment (`_judge`, delegated to it).
204
+ """
205
+ entry = table.get(ortho)
206
+ if entry is None:
207
+ count, pending = _estimate(ortho)
208
+ return count, pending, True
209
+ return _judge(ortho, entry)
210
+
211
+
212
+ def starts_with_vowel(word: str, aspire: frozenset[str]) -> bool:
213
+ """Whether the token that follows lets a preceding mute e elide.
214
+
215
+ A word starting with a vowel letter elides a schwa in front of it, and so
216
+ does a mute h -- silent, letting the vowel behind it show. An aspirated h
217
+ (Task 3's list) behaves as a consonant instead: "je hais" keeps `je`'s
218
+ schwa where "une heure" loses `une`'s.
219
+ """
220
+ if not word:
221
+ return False
222
+ if word[0] in VOWELS:
223
+ return True
224
+ return word[0] == "h" and word not in aspire
225
+
226
+
227
+ def _split_at_apostrophe(word: str) -> tuple[str, str]:
228
+ """(stem before the first apostrophe, everything after it).
229
+
230
+ Called only once the whole token has already failed a table lookup, so
231
+ this never runs on `aujourd'hui` or `prud'homme` -- both are single
232
+ Lexique entries and are used whole (fix round 1).
233
+ """
234
+ for index, ch in enumerate(word):
235
+ if ch in ("'", "’"): # noqa: RUF001
236
+ return word[:index], word[index + 1 :]
237
+ return word, ""
238
+
239
+
240
+ def count_line(
241
+ line: str, table: Mapping[str, tuple[int, str, str]], aspire: frozenset[str]
242
+ ) -> tuple[int, int]:
243
+ """(syllables in the line, words whose count was estimated).
244
+
245
+ "Estimated" covers three cases now, all folded into one counter (Finding
246
+ 3 of the whole-branch review, spec D4): a word outside Lexique
247
+ (`_estimate`'s vowel-run guess), a word carrying a pending mute e, and a
248
+ word whose diérèse gate fired. The first is out-of-vocabulary; the other
249
+ two are in-vocabulary but their contribution to the count rests on an
250
+ editorial judgment the spelling does not settle -- which is exactly what
251
+ D4 means by "estimated, never exact." Before this fix a line built
252
+ entirely from known words could carry a mute-e or diérèse judgment and
253
+ still report `estimated_words == 0`, the confidently-wrong shape D4
254
+ exists to rule out.
255
+
256
+ A token that carries an internal apostrophe is tried WHOLE against the
257
+ table first: 94 Lexique entries are keyed with one (`aujourd'hui`,
258
+ `prud'homme`), and splitting them unconditionally silently returned a
259
+ confidently wrong count with `estimated` left at 0 -- fix round 1's
260
+ finding, and the failure mode this project cares most about, a wrong
261
+ answer reported as certain. Only a token the table does not recognise
262
+ whole falls back to splitting at the apostrophe and treating the stem as
263
+ a proclitic ("t'", "d'", "qu'") -- a zero-syllable, consonant-initial
264
+ token that survives in the stream rather than being dropped, which is
265
+ TRAP 2: dropping it would let the preceding word's schwa see the vowel
266
+ behind the proclitic and elide across it, which it must not.
267
+
268
+ Walks the tokens once, adding one syllable for each pending mute e whose
269
+ successor is consonant-initial. A pending mute e with no successor -- the
270
+ line's last token -- is never added, which is how "the final e never
271
+ counts at the end of a line" falls out of "not last" rather than needing
272
+ a separate rule.
273
+ """
274
+ tokens = _TOKEN_RE.findall(line)
275
+ # Each entry: (syllables carried, mute e pending, was estimated, text
276
+ # used to judge whether the NEXT word starts with a vowel).
277
+ parsed: list[tuple[int, bool, bool, str]] = []
278
+ for token in tokens:
279
+ lower = token.casefold()
280
+ if "'" not in lower and "’" not in lower: # noqa: RUF001
281
+ count, pending, estimated = _lookup(lower, table)
282
+ parsed.append((count, pending, estimated, lower))
283
+ continue
284
+ entry = table.get(lower)
285
+ if entry is not None:
286
+ # `aujourd'hui`, `prud'homme`: a fused word, not a proclitic plus
287
+ # a word -- 94 Lexique entries carry an internal apostrophe.
288
+ count, pending, judged = _judge(lower, entry)
289
+ parsed.append((count, pending, judged, lower))
290
+ continue
291
+ # The whole form isn't a table entry, so it wasn't a fused word like
292
+ # those -- read the apostrophe as an elision boundary instead.
293
+ stem, rest = _split_at_apostrophe(lower)
294
+ if stem in ELIDED_ZERO:
295
+ parsed.append((0, False, False, stem))
296
+ elif (full := ELIDED_WORD.get(stem)) is not None and (
297
+ word_entry := table.get(full)
298
+ ) is not None:
299
+ # `lorsqu'il` etc: already elided in the spelling, so the mute e
300
+ # is gone, not merely pending -- `pending` stays False regardless
301
+ # of what `latent_schwa` would say about `full` on its own. A
302
+ # diérèse site is still a live judgment here, so it alone decides
303
+ # `estimated` (Finding 3).
304
+ nbsyll, phon, orthosyll = word_entry
305
+ count, _pending = latent_schwa(full, nbsyll, phon, orthosyll)
306
+ extra = _dierese_extra(full, phon)
307
+ parsed.append((count + extra, False, extra > 0, stem))
308
+ else:
309
+ # An elided proclitic neither list recognises -- there is no
310
+ # basis for a confident zero, so flag it estimated rather than
311
+ # silently asserting one (fix round 1, same defect as the
312
+ # whole-token case above, now on the proclitic path).
313
+ parsed.append((0, False, True, stem))
314
+ if rest:
315
+ count, pending, estimated = _lookup(rest, table)
316
+ parsed.append((count, pending, estimated, rest))
317
+
318
+ total = 0
319
+ estimated_words = 0
320
+ last = len(parsed) - 1
321
+ for index, (count, pending, was_estimated, _text) in enumerate(parsed):
322
+ total += count
323
+ if was_estimated:
324
+ estimated_words += 1
325
+ if pending and index < last:
326
+ successor = parsed[index + 1][3]
327
+ if not starts_with_vowel(successor, aspire):
328
+ total += 1
329
+ return total, estimated_words
File without changes
@@ -0,0 +1,83 @@
1
+ """Lexique 3.82's SAMPA, in the IPA this project uses everywhere else.
2
+
3
+ Lexique's alphabet is its own: `@` is the nasal /ɑ̃/ rather than a schwa, `§`
4
+ is /ɔ̃/, and `2`/`9` split /ø/ from /œ/. Reading `@` as a schwa costs fifteen
5
+ points of line accuracy and looks like a plausible one-syllable undercount, so
6
+ this table is tested against hand-checked words before anything reads it
7
+ (spec F0).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ #: Two-character sequences do not occur in Lexique's phon column, so a
13
+ #: character-by-character walk is complete.
14
+ SAMPA_TO_IPA: dict[str, str] = {
15
+ # Oral vowels
16
+ "a": "a",
17
+ "A": "ɑ",
18
+ "e": "e",
19
+ "E": "ɛ",
20
+ "i": "i",
21
+ "o": "o",
22
+ "O": "ɔ",
23
+ "u": "u",
24
+ "y": "y",
25
+ "2": "ø",
26
+ "9": "œ",
27
+ "°": "ə",
28
+ # Nasal vowels
29
+ "@": "ɑ̃",
30
+ "5": "ɛ̃",
31
+ "1": "œ̃",
32
+ "§": "ɔ̃",
33
+ # Glides
34
+ "j": "j",
35
+ "w": "w",
36
+ "8": "ɥ",
37
+ # Consonants
38
+ "b": "b",
39
+ "d": "d",
40
+ "f": "f",
41
+ "g": "g",
42
+ "k": "k",
43
+ "l": "l",
44
+ "m": "m",
45
+ "n": "n",
46
+ "N": "ɲ",
47
+ "p": "p",
48
+ "R": "ʁ",
49
+ "s": "s",
50
+ "S": "ʃ",
51
+ "t": "t",
52
+ "v": "v",
53
+ "z": "z",
54
+ "Z": "ʒ",
55
+ "G": "ŋ",
56
+ "x": "x",
57
+ }
58
+
59
+ #: A glide is written with a vowel symbol in some schemes but carries no
60
+ #: syllable of its own, so it is not here. ADR 0030 is the reason this is a
61
+ #: property of the transcription rather than of the phoneme string.
62
+ IPA_VOWELS: frozenset[str] = frozenset(
63
+ {"a", "ɑ", "e", "ɛ", "i", "o", "ɔ", "u", "y", "ø", "œ", "ə", "ɑ̃", "ɛ̃", "œ̃", "ɔ̃"}
64
+ )
65
+
66
+
67
+ def to_ipa(sampa: str) -> str:
68
+ """Convert one Lexique transcription. An unmapped symbol raises.
69
+
70
+ Passing an unknown symbol through would let a scheme leak silently into a
71
+ syllable count, which is the defect ADR 0030 fixed twice in one day.
72
+ """
73
+ return "".join(SAMPA_TO_IPA[ch] for ch in sampa)
74
+
75
+
76
+ def to_phonemes(sampa: str) -> list[str]:
77
+ """The transcription as a list of IPA phonemes.
78
+
79
+ Lexique writes one symbol per phoneme, so the split is the walk itself and
80
+ no re-segmentation of the IPA string is needed -- `d@` is ["d", "ɑ̃"], and
81
+ the nasal stays one phoneme rather than a vowel plus a combining tilde.
82
+ """
83
+ return [SAMPA_TO_IPA[ch] for ch in sampa]
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.5
2
+ Name: denckring-fr-data
3
+ Version: 0.1.0
4
+ Summary: French lexicon and prosody data for denckring: word membership, nouns, frequency bands, glosses, syllables and phonemes
5
+ Author-email: Dennis Senzel <dennis.senzel@posteo.com>
6
+ License-Expression: Apache-2.0 AND CC-BY-SA-4.0
7
+ License-File: LICENSE
8
+ License-File: LICENSE-LEXIQUE
9
+ License-File: LICENSE-WIKTIONARY
10
+ License-File: NOTICE
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Natural Language :: French
13
+ Classifier: Topic :: Text Processing :: Linguistic
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.11
16
+ Requires-Dist: denckring==0.1.0
17
+ Description-Content-Type: text/markdown
18
+
19
+ # denckring-fr-data
20
+
21
+ French lexicon and prosody data for [denckring](https://github.com/senzelden/denckring):
22
+ word membership, an ordered noun list, frequency bands, glosses, a syllable and
23
+ phoneme table, and an aspirated-*h* list.
24
+
25
+ ```console
26
+ pip install denckring[fr]
27
+ ```
28
+
29
+ Installing this gives the French pack `lexicon.words`, `lexicon.nouns`,
30
+ `lexicon.glosses`, `lexicon.graded_words`, `syllables`, `syllables.dictionary`,
31
+ `syllables.heuristic` and `phonemes`. The lexicon capabilities are what
32
+ `charade`, `semordnilap`, `word_square`, `n_plus_7`, `s_plus_7`, `tmesis`,
33
+ `word_ladder`, `kangaroo_word` and the two definitional rows need in order to
34
+ run in French — and what `apply anagram` needs in order to generate in it. The
35
+ prosody capabilities, plus the line-level elision rules `FrenchDataPack`
36
+ overrides `line_syllables` with, are what the 23 syllabic rows
37
+ (`alexandrine`, `syllable_count`, `rhyme_scheme`, `haiku`, `hemeling`, and the
38
+ rest — ADR 0034) need in order to run in French. Without this package, all of
39
+ it raises `MissingCapability` for `fr`, exactly as for any unmet capability.
40
+ French still has no lexical stress, so the 18 rows that need `stress` stay
41
+ blocked regardless — see ADR 0034 D3.
42
+
43
+ **125,653 syllable/phoneme rows and 3,370 aspirated-*h* entries**, alongside
44
+ the lexicon: 46,947 lemmas, 48,234 noun forms and a Wiktionary gloss slice.
45
+ Both sources are **CC BY-SA 4.0** — attribution *and* share-alike — Lexique
46
+ 3.82 for the lexicon and the prosody table, French Wiktionary for the glosses
47
+ and the aspirated-*h* list. See `LICENSE-LEXIQUE` and `LICENSE-WIKTIONARY`.
48
+ Regenerate the vendored files with `scripts/build_lexicon.py`.
49
+
50
+ Accents are preserved rather than folded. `côte` and `cote` are different words,
51
+ and whether they should be treated alike is a decision ADR 0009 leaves to the
52
+ procedure, not to the lexicon.
53
+
54
+ The French line count is **estimated, never exact** (ADR 0034 D4): a final
55
+ mute *e* elides or counts depending on what follows it, and classical
56
+ diérèse is not decidable from spelling alone. Every syllabic report carries
57
+ `estimated_words` accordingly.
@@ -0,0 +1,19 @@
1
+ denckring_fr_data/__init__.py,sha256=xYZIJzAUm8RCG4CsXQNRtu9aewtOQkrFb797ZGgUZ8Q,16141
2
+ denckring_fr_data/elision.py,sha256=FGoUF2iVhJvn1t1CSYneWGXBZDLlU6zbpvKLxLZhArQ,16839
3
+ denckring_fr_data/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ denckring_fr_data/sampa.py,sha256=o8APJPRtynzfPoailbcZ-Ag3fcP2fikMyQDabCDAU5Q,2279
5
+ denckring_fr_data/data/glosses.txt.gz,sha256=SVt_oCYayaOfK3EJkxihPJqhpL7umpn8ABbCXZKmWPk,15270003
6
+ denckring_fr_data/data/graded_words.txt.gz,sha256=r3X-d0OH05x_wpVXpatsDFJ3eU3b8-fqFm0OwYIdpa0,413181
7
+ denckring_fr_data/data/h_aspire.txt.gz,sha256=LX8DhJQ0GsnYKpJZZyx61mArKrW-Y4qVOwiAY4yyypk,10124
8
+ denckring_fr_data/data/metadata.json,sha256=qoTCjj-_wyK83ehXNfcUTAG3c25Pcf6FehQN3yy1-Uk,395
9
+ denckring_fr_data/data/nouns.txt.gz,sha256=HtExMdJqsFWQmUda4ckDf_aHwkvE6sxJPP1BWti7x18,126711
10
+ denckring_fr_data/data/syllables.txt.gz,sha256=X-H3_yulQKQGmAe9SbI9HP2u6q6QOQbSXCWToqn8YxI,1001801
11
+ denckring_fr_data/data/words.txt.gz,sha256=TzUtEd2knOAN54sbkG5IyFsoqRs3xYIK_29HL8VX87w,329843
12
+ denckring_fr_data-0.1.0.dist-info/METADATA,sha256=rpIqeMUgIeb9M-zyjKAmjGV6f0Cyhv8_SUPdlBSyHKw,2782
13
+ denckring_fr_data-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
14
+ denckring_fr_data-0.1.0.dist-info/entry_points.txt,sha256=1Bg8TfE8IxB3sS1w4XHjy757vrSflnYqCja7xDkgQTY,55
15
+ denckring_fr_data-0.1.0.dist-info/licenses/LICENSE,sha256=qsc7MUj20dcRHbyjIJn2jSbGRMaBOuHk8F9leaomY_4,11360
16
+ denckring_fr_data-0.1.0.dist-info/licenses/LICENSE-LEXIQUE,sha256=c-BZFUDwVcpZH5TlVEsrLKAIAte34uH4_3ULVvmH10w,879
17
+ denckring_fr_data-0.1.0.dist-info/licenses/LICENSE-WIKTIONARY,sha256=HnQjmLvaM6hU9xrrNknD0_U5vIyniYqR5vVkgXRRjqc,691
18
+ denckring_fr_data-0.1.0.dist-info/licenses/NOTICE,sha256=rSQ_EtW_ZbopAnsN1JnN42bSnJaKGzSmINsBCaDP4eY,607
19
+ denckring_fr_data-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [denckring.lang]
2
+ fr = denckring_fr_data:FrenchDataPack
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ https://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ https://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
@@ -0,0 +1,20 @@
1
+ The French word list, noun list and frequency bands in this distribution are
2
+ derived from Lexique 3.82.
3
+
4
+ New, B., Pallier, C., Ferrand, L., Matos, R. (2001). Une base de données
5
+ lexicales du français contemporain sur internet: LEXIQUE. L'Année
6
+ Psychologique, 101, 447-462.
7
+ http://www.lexique.org
8
+
9
+ Lexique is licensed under the Creative Commons Attribution-ShareAlike 4.0
10
+ International licence, as stated in the README bundled with Lexique382.zip:
11
+
12
+ https://creativecommons.org/licenses/by-sa/4.0/
13
+
14
+ Attribution: Boris New and Christophe Pallier, and the Lexique contributors.
15
+
16
+ Share-alike: the derived data files in this distribution
17
+ (`src/denckring_fr_data/data/`) are offered under the same CC BY-SA 4.0 licence.
18
+ The code in this distribution is Apache-2.0 and is not a derivative of the data.
19
+
20
+ Regenerate the vendored files with scripts/build_lexicon.py.
@@ -0,0 +1,17 @@
1
+ The French glosses in this distribution are derived from French Wiktionary
2
+ (https://fr.wiktionary.org), specifically from the
3
+ `frwiktionary-latest-pages-articles` XML dump published by the Wikimedia
4
+ Foundation.
5
+
6
+ Wiktionary text is licensed under the Creative Commons
7
+ Attribution-ShareAlike 4.0 International licence:
8
+
9
+ https://creativecommons.org/licenses/by-sa/4.0/
10
+
11
+ Attribution: the French Wiktionary contributors. Page histories, which name
12
+ them, are at https://fr.wiktionary.org/wiki/<headword> under "Historique".
13
+
14
+ Share-alike: the derived data files in this distribution are offered under the
15
+ same CC BY-SA 4.0 licence.
16
+
17
+ Regenerate the vendored files with scripts/build_lexicon.py.
@@ -0,0 +1,15 @@
1
+ denckring-fr-data
2
+ Copyright 2026 senzelden
3
+
4
+ The code in this distribution is licensed under the Apache License, Version 2.0.
5
+ See LICENSE.
6
+
7
+ The data in this distribution comes from two sources, both licensed under the
8
+ Creative Commons Attribution-ShareAlike 4.0 International licence:
9
+
10
+ - Lexique 3.82 (lexique.org) — see LICENSE-LEXIQUE
11
+ - French Wiktionary — see LICENSE-WIKTIONARY
12
+
13
+ Both impose attribution and share-alike. That they agree is why this is one
14
+ distribution where German needs two: ADR 0013 quarantines a data licence per
15
+ distribution, and there is only one licence here to quarantine.