diff --git a/README.md b/README.md index 9047e22bb65e8176e2529e317fb3f06ae58c646f..f2e573d28335aac59d9794f04867cdcc518246d1 100644 --- a/README.md +++ b/README.md @@ -1,59 +1,53 @@ -# discut22 +# Project DisCut22 *still WIP* +A tool for Discourse Segmentation. Inheritor of ToNy and DisCut, segmentors for DISRPT 2019 and 2021. The goal of this version is to be easy to use with or without IT knowledge. +__2021__ +*[Multi-lingual Discourse Segmentation and Connective Identification: MELODI at Disrpt2021](https://aclanthology.org/2021.disrpt-1.3.pdf)* +Code: https://gitlab.irit.fr/melodi/andiamo/discoursesegmentation/discut -## Getting started +__2019__ +*[ToNy: Contextual embeddings for accurate multilingual discourse segmentation of full documents](https://www.aclweb.org/anthology/W19-2715.pdf)* +Code: https://gitlab.inria.fr/andiamo/tony -To make it easy for you to get started with GitLab, here's a list of recommended next steps. -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! -## Add your files +# Usage -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: +## Content description +[TBD : xplain directories automatically created during scripts run] +- `data/` Contains input data, raw and/or pre-processed format(s). +- `data/results/` Contains output data, scores and post-processed data. (Also logs of allennlp) +- `code/` Contains main scripts. +- `code/utils` Contains useful scripts to be called. +- `model/` Contains loaded or created model. +- `doc.pdf` Contains detailed documentation (TBD?) +- `code/config.json` A file to be completed (or a dir with choise between simple use_case configs and a template for a custom config) + +## Set up environnement +- Conda stuff pour python 3.7 (TBD ?) +- Install all librairies required with the following command: ``` -cd existing_repo -git remote add origin https://gitlab.irit.fr/melodi/andiamo/discoursesegmentation/discut22.git -git branch -M main -git push -uf origin main +pip install -r <dir?>requirements.txt ``` -## Integrate with your tools +## Configuration file: to chose or to complete +- `code/config_1.json` Config for usecase_1 : take a sentence splited text, apply ToNy, output same text but with EDU brackets. +- [TBD : train models config and all sort of cool options] -- [ ] [Set up project integrations](https://gitlab.irit.fr/melodi/andiamo/discoursesegmentation/discut22/-/settings/integrations) +## Run usecase 1 +(go to `code` directory) +Run this command: +``` +python discut22.py --config config_1.json +``` -## Collaborate with your team -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Automatically merge when pipeline succeeds](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +<!--- ## Test and Deploy -Use the built-in continuous integration in GitLab. - -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing(SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - -*** - -# Editing this README - -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thank you to [makeareadme.com](https://www.makeareadme.com/) for this template. - -## Suggestions for a good README -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. - -## Name -Choose a self-explaining name for your project. - ## Description Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. @@ -83,10 +77,8 @@ For people who want to make changes to your project, it's helpful to have some d You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. ## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. - ## License -For open source projects, say how it is licensed. - ## Project status If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. + +---> \ No newline at end of file diff --git a/code/classes_def.py b/code/classes_def.py new file mode 100644 index 0000000000000000000000000000000000000000..4835185d0abda8a6b0f564e4052e95ebe60e4b58 --- /dev/null +++ b/code/classes_def.py @@ -0,0 +1,21 @@ +# Classes for discut22 + + + +class Input: + def __init__(self, infos): + self.name = infos['name'] + self.lang = infos['language'] + self.path = infos['folder_path'] + self.file = infos['file'] + self.form = infos['format'] + self.gold = infos['gold'] + self.resu = infos['results_path'] + + +class Process: + def __init__(self, infos, data): + self.toke = infos['pre-processing']['tokenization'] + self.data = data + self.model = infos['discourse_segmenter']['model'] # ezpz for Tony + \ No newline at end of file diff --git a/code/config_1.json b/code/config_1.json new file mode 100644 index 0000000000000000000000000000000000000000..a33547577166bc4a4db8f1023e41bade4674c8ca --- /dev/null +++ b/code/config_1.json @@ -0,0 +1,32 @@ +{ + "usecase_description": "Config file for usecase_1 : from a text, get the same text but with EDU bracket using ToNy segmenter.", + "input": { + "name": "chaperontest", + "file": ".ss", + "folder_path": "../data/chaperontest", + "format": "raw_sentences", + "language": "fr", + "gold": false, + "results_path": "../data/chaperontest/results" + }, + "output": { + "format": "bracket", + "framework": "sdrt" + }, + "steps":{ + "pre-processing": { + "tokenization": true, + "sentence_split": true, + "syntactic_parsing": false, + "NER_format_initialisation": true + }, + "discourse_segmenter": { + "model": "tony" + }, + "post-processing": { + "json_to_tab": true, + "tab_to_bracket":true + } + } +} + diff --git a/code/discut22_1.py b/code/discut22_1.py new file mode 100644 index 0000000000000000000000000000000000000000..87f13eb0627230e9ce1192017f72a07af8d38051 --- /dev/null +++ b/code/discut22_1.py @@ -0,0 +1,109 @@ +###################################### +###### DISCOURSE SEGMENTOR 2022 ###### +###################################### +""" This the main script + And the only one to run, + after completion of config.json """ + +import os +import sys +import argparse +import pandas as pd # for futur clean output in df +import json + +from classes_def import Input, Process +import utils.fr_tokenize as tk +import utils.conv2ner as c2n +import utils.json2conll as j2c +import utils.conll2bracket as c2bracket + + +# fonction to get config stuffs +def get_config_infos(config_file): + + with open(config_file) as f: + infos = json.load(f) + data_in = Input(infos['input']) + actions = Process(infos['steps'], data_in) + print("data to be process : {}".format(data_in.name)) + return actions + + +# fonction to load existing model -> only tony for now +def get_model(model_name): + name = model_name + + if name == "tony": + arch = "french_tokens.tar.gz" + if not os.path.isfile("../model/{}".format(arch)): + dl = "wget https://zenodo.org/record/4235850/files/french_tokens.tar.gz -P ../model --progress=bar" + os.system(dl) + else: + print("Tony already in place !") + + return "../model/{}".format(arch) + + + +# main call +def main(config): + + steps = get_config_infos(config) # on obtient la liste des trucs + # à faire, donnée par la classe Process + #print([x for x in enumerate(steps)]) + #suivant la liste ordonnée, faire les trucs (for now simple usecase1): + + #### Split text into sentence : not in usecase1 + + + #### Tokenization du text # #python ${SEG_DIR}/code/utils/fr_tokenize.py $RAW > ${RAW}.tok + data_in = "{}/{}{}".format(steps.data.path, steps.data.name, steps.data.file) + data_tok = "{}/{}.tok".format(steps.data.path, steps.data.name) +# sys.exit("check path") + print("Starting Tokenization...to {}".format(data_tok)) + tk.main(data_in, data_tok) # .ss -> .tok + + + #### Conversion en NER pb # #python $RUNTIME/conv2ner.py ${RAW}.tok > ${RAW}.ner.tok + data_ner = "{}/{}.ner.tok".format(steps.data.path, steps.data.name) + print("Starting conversion to NER format...to {}".format(data_ner)) + c2n.main(data_tok, data_ner) + + + #### Appliquer le model choisi, sortir le JSON avec les predictions :score, proba, tags + # #allennlp predict --use-dataset-reader --output-file ${RESULT_DIR}/${FILE}.json ${MODEL} ${RAW}.ner.tok + print("Checking for model...{}".format(steps.model)) + model_path = get_model(steps.model) + data_json = "{}/{}.json".format(steps.data.resu, steps.data.name) + cmd = "allennlp predict --use-dataset-reader --output-file {} {} {} &> {}/logs.txt".format(data_json, model_path, data_ner, steps.data.resu) + if not os.path.isdir(steps.data.resu): + print(" result does not exist") + os.mkdir(steps.data.resu) + print("Starting Prediction...") + os.system(cmd) + #### ------------------------------- TBD do the same but with python script (or JIANT ??) + + + #### Appliquer les predictions au texte et sortir le texte tokenisé avec la colone des tags-prédis # #python $RUNTIME/json2conll.py ${RESULT_DIR}/${FILE}.json split.tok > ${RESULT_DIR}/${FILE}.split.tok + data_conll = "{}/{}.split.tok".format(steps.data.resu, steps.data.name) + format = "split.tok" # to retrive from config file !!! + print("Starting Formating from json to tok format...to {}".format(data_conll)) + j2c.main(data_json, format, data_conll) + + + ####prendre le texte tokénisé+tags-prédits et sortir le texte en plain (format du d'ebut, for now en suite de phrases) avec les brackets # #python $RUNTIME/conll2bracket.py ${RESULT_DIR}/${FILE}.split.tok > ${RESULT_DIR}/${FILE}.split.tok.bracket + data_bracket = "{}/{}.split.tok.bracket".format(steps.data.resu, steps.data.name) + print("Starting formating into bracket text...to {}".format(data_bracket)) + c2bracket.main(data_conll, data_bracket) + + + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--config', help='Config file in JSON') + args = parser.parse_args() + config = args.config + + main(config) + print("Done.") \ No newline at end of file diff --git a/code/utils/__init__.py b/code/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/code/utils/conll2bracket.py b/code/utils/conll2bracket.py new file mode 100644 index 0000000000000000000000000000000000000000..f1dac8bea3e9b5c3197be8ebe8ad4c040c5ea463 --- /dev/null +++ b/code/utils/conll2bracket.py @@ -0,0 +1,57 @@ +"""24brièvement________ + +1ok_______BeginSeg=Yes +2bonjour________ +3tout________ +4le________ +5monde________ +6je________ +7suis________ +8Ilyes________ +9Rebai________ +10euh________ +11je________ +12suis________ +13un________ +14ingénieur________ +15de________ +16recherche________ +17chez________ +18Linagora________ + +1bonjour_______BeginSeg=Yes +""" + + +import sys +import codecs + +#input = open(sys.argv[1],encoding="utf8").readlines() + +def conll2brackets(in_f, out_f): + start = True + input = in_f + + with open(out_f, 'w') as file_out: + with open(in_f, 'r') as input: + for line in input: + if line.strip()=="": + file_out.write("]") + file_out.write("\n\n") + start = True + else: + n, word, *junk, tag = line.split() + if tag=="BeginSeg=Yes": + if not(start): + file_out.write("] ") + file_out.write(f"[ {word} ") + else: + file_out.write(f"{word} ") + start = False + file_out.write("]\n\n") + +def main(f_in, f_out): + input = f_in + output = f_out + + conll2brackets(input, output) \ No newline at end of file diff --git a/code/utils/conv2ner.py b/code/utils/conv2ner.py new file mode 100644 index 0000000000000000000000000000000000000000..5265f4608b90dedb64a15435395c796f46986f08 --- /dev/null +++ b/code/utils/conv2ner.py @@ -0,0 +1,127 @@ +""" +Convert to ner Connl format to use allennlp dataset reader + +basically, just skip lines between docs, strip to 4 fields with words as 1st and tag as last, and format as BIO + +TODO: try BIOUL (L=last, U=unit entity = 1 token) +""" +import sys +import argparse + +maptags = {"_":"O", + "BeginSeg=Yes": "B-S", + "Seg=B-Conn":"B-Conn", + "Seg=I-Conn":"I-Conn", + "SpaceAfter=No":"O", + "Typo=Yes":"O", + } +# +parameters = { + "LEMMATIZE": False, + "MARK_END": False, + "SPLIT_TOO_LONG": False, + "THRESHOLD": int(180), + "input_format": "tok" + } + +LEMMATIZE=False +MARK_END=False +SPLIT_TOO_LONG=False +THRESHOLD=int(180) +input_format="tok" + +""" def get_param(): + + #parser = argparse.ArgumentParser() + #parser.add_argument("filepath", help="path to file to convert") + #parser.add_argument("--lemmatize", default=False, action='store_true', help="to use with conll input: replace token with its lemma (useful for turk)") + #parser.add_argument("--mark-end", default=False, action='store_true', help="add explicit label for end of segment") + #parser.add_argument("--split-too-long", default=[False,180], help="split sentences longer than threshold",nargs=2) + #parser.add_argument("--input-format",default="tok",help="input format: tok, split.tok, conll ner") + #args = parser.parse_args() + #MARK_END = args.mark_end + # take lemmas instead of token forms (useful for turkish) + # also tag all proper nouns with same token + #LEMMATIZE = args.lemmatize + # split for too long sentences (default 180) for bert + #SPLIT_TOO_LONG= args.split_too_long[0] + ##THRESHOLD = int(args.split_too_long[1]) + + ## for now all params set to default + parameters = { + "LEMMATIZE": False, + "MARK_END": False, + "SPLIT_TOO_LONG": False, + "THRESHOLD": int(180), + "input_format": "tok" + } + +#filepath = sys.argv[1] +#filepath = args.filepath + +input_format = args.input_format """ + + +if SPLIT_TOO_LONG: + print("warning: too-long sentence splitting mode = ON ",file=sys.stderr) + +token_number=0 + +def conversion2ner(input, output, params=None): + + with open(output, 'w') as out_f: + with open(input, 'r') as f: + start_doc = True + res = [] + for line in f: + if input_format=="ner": + token_number +=1 + if SPLIT_TOO_LONG and token_number>THRESHOLD: + # sentence too long: insert a newline to make a separate sequence + res.append([]) + token_number = 0 + res.append(line.split()) + elif "\t" not in line: + if not(start_doc): + res.append([]) # [line.strip()]) + start_doc = True + #elif line.strip()=="": + # res.append([]) + # start_doc = True + else: + fields = line.strip().split() + #print(fields,file=sys.stderr) + token_number = int(fields[0].split("-")[0]) + if SPLIT_TOO_LONG and token_number>THRESHOLD: + # sentence too long: insert a newline to make a separate sequence + res.append([]) + w = fields[1] if not(LEMMATIZE) else fields[2] + label = fields[-1].split("|")[0] + if input_format=="conll": + if LEMMATIZE and fields[3]=="PROPN": + w = "NAME" + pos = "NN" + else: + pos = "NN" + tag = maptags.get(label,"O") + #if start_doc: + # tag = "B-S" + if not(start_doc) and MARK_END and tag=="B-S" and res[-1][-1]!="B-S": + # then, previous token label is set to B-E to signal end of previous segment + res[-1][-1] = "B-E" + start_doc = False + if label not in maptags: + print("warning, strange label ",label,file=sys.stderr) + res.append([w,pos,"O",tag]) + + for line in res: + out_f.write("\t".join(line)) + out_f.write("\n") + + + +def main(f_in, f_out): + input = f_in + output = f_out + #param = get_param() + conversion2ner(input, output) # add param \ No newline at end of file diff --git a/code/utils/fr_tokenize.py b/code/utils/fr_tokenize.py new file mode 100644 index 0000000000000000000000000000000000000000..0efedec7e7da6b8bd636bd34ee47438bea49e3c2 --- /dev/null +++ b/code/utils/fr_tokenize.py @@ -0,0 +1,91 @@ +"""take a French document and + + 1) use spacy to tokenize it + 2) format it as disrpt input + +TODO: conll option to output spacy analysis +""" + +import sys +import codecs +import spacy + +fr = spacy.load('fr_core_news_sm') +#extra_fields = 8 +# WIP ... +CONLL = False + +#input_file = sys.argv[1] +#input = codecs.open(input_file,encoding="utf8").read() + + + +# watch the max length because of Bert restrictions to 512 subword units +#current = max_length = 0 +#cutoff = 200 + + +# build conll stuff. bits from spacy_conll, adapted +# WIP +tagmap = fr.Defaults.tag_map + +def get_morphology(self, tag): + if not self.tagmap or tag not in self.tagmap: + return '_' + else: + feats = [f'{prop}={val}' for prop, val in self.tagmap[tag].items() if not Spacy2ConllParser._is_number(prop)] + if feats: + return '|'.join(feats) + else: + return '_' + +def head_idx(idx,word): + if word.dep_.lower().strip() == 'root': + head_idx = 0 + else: + head_idx = word.head.i + 1 - sent[0].i + return head_idx + +def word_tuple(idx,word): + return (idx, + word.text, + word.lemma_, + word.pos_, + word.tag_, + get_morphology(word.tag_), + head_idx(idx,word), + word.dep_, + '_', + '_' + ) +###################### + + +def main(f_in, f_out): + + with open(f_out, 'w') as file: + + input_file = f_in + input = codecs.open(input_file,encoding="utf8").read() + + + doc = fr(input) + + current = max_length = 0 + extra_fields = 8 + + # raw doc + for i,token in enumerate(doc): + if token.text.strip()!="": + line = [str(i),token.text]+["_"]*extra_fields + file.write("\t".join(line)) + current = current + 1 + #if current>cutoff: + # print() + file.write("\n") + else: + file.write("\n") + max_length = max(max_length,current) + current = 0 + + print("max length sequence = %s"%max_length,file=sys.stderr) diff --git a/code/utils/json2conll.py b/code/utils/json2conll.py new file mode 100644 index 0000000000000000000000000000000000000000..c657f89b763874719740a4149fd7323c4c6933f4 --- /dev/null +++ b/code/utils/json2conll.py @@ -0,0 +1,44 @@ +""" +reexports allennlp predictions from json to +conll format +""" + +import json +import sys + +#filepath = sys.argv[1] +#config = sys.argv[2] +# conll ou tok + +map = {"O":"_", + "B-S":"BeginSeg=Yes", + "U-S":"BeginSeg=Yes", + "U-Conn":"Seg=B-Conn", + "L-Conn":"Seg=I-Conn", + "I-Conn":"Seg=I-Conn", + "B-Conn":"Seg=B-Conn", + "B-E":"_", + "U-E":"_", + } + + +def js2conll(filepath, fileoutpath, config): + data = [] + for line in open(filepath, 'r'): + data.append(json.loads(line)) + + with open(fileoutpath, 'w') as f_out: + for doc in data: + tokens = zip(doc["words"],doc["tags"]) + out = "\n".join(("%s\t%s\t%s%s"%(i+1,word,"_\t"*7,map.get(tag,tag)) for (i,(word,tag)) in enumerate(tokens))) + if config=="tok": + print("# blabla") + f_out.write(f'{out}\n') + f_out.write("\n") + #print() + +def main(f_in, form, f_out): + input = f_in + output = f_out + forma = form + js2conll(input, output, forma) \ No newline at end of file diff --git a/data/chaperontest/chaperontest.ss b/data/chaperontest/chaperontest.ss new file mode 100644 index 0000000000000000000000000000000000000000..4a87d03968535cc2b73bfb518c1c27939fd9d50f --- /dev/null +++ b/data/chaperontest/chaperontest.ss @@ -0,0 +1,132 @@ +# newdoc id = chaperonrouge +Il était une fois une petite fille que tout le monde aimait bien, surtout sa grand-mère. +Elle ne savait qu'entreprendre pour lui faire plaisir. +Un jour, elle lui offrit un petit bonnet de velours rouge, qui lui allait si bien qu'elle ne voulut plus en porter d'autre. +Du coup, on l'appela Chaperon Rouge. +Un jour, sa mère lui dit: “Viens voir, Chaperon Rouge : voici un morceau de gâteau et une bouteille de vin. +Porte +-les à ta grand-mère; +elle est malade et faible; +elle s'en délectera; +fais vite, avant qu'il ne fasse trop chaud. +Et quand tu seras en chemin, sois bien sage et ne t'écarte pas de ta route, sinon tu casserais la bouteille et ta grand-mère n'aurait plus rien. +Et quand tu arriveras chez elle, n'oublie pas de dire “Bonjour” et ne va pas fureter dans tous les coins. +” “Je ferai tout comme il faut,” dit le Petit Chaperon Rouge à sa mère. +La fillette lui dit au revoir. +La grand-mère habitait loin, au milieu de la forêt, à une demi-heure du village. +Lorsque le Petit Chaperon Rouge arriva dans le bois, il rencontra le Loup. +Mais il ne savait pas que c'était une vilaine bête et ne le craignait point. +“ +Bonjour, Chaperon Rouge,” dit le Loup. +“Bonjour, Loup,” dit le Chaperon Rouge. +“Où donc vas-tu si tôt, Chaperon Rouge? +” - “Chez ma grand-mère. +” +- “Que portes +-tu dans ton panier? +” - “Du gâteau et du vin. +Hier nous avons fait de la pâtisserie, et ça fera du bien à ma grand-mère. +Ça la fortifiera. +” +- “Où habite donc ta grand-mère, Chaperon Rouge? +” - “Oh! +à un bon quart d'heure d'ici, dans la forêt. +Sa maison se trouve sous les trois gros chênes. +En dessous, il y a une haie de noisetiers, tu sais bien? +” dit le petit Chaperon Rouge. +Le Loup se dit: +“Voilà un mets bien jeune et bien tendre, un vrai régal! +Il sera encore bien meilleur que la vieille. +Il faut que je m'y prenne adroitement pour les attraper toutes les eux! +” Il l'accompagna un bout de chemin et dit: “Chaperon Rouge, vois ces belles fleurs autour de nous. +Pourquoi ne les regardes +-tu pas? +J'ai l'impression que tu n'écoutes même pas comme les oiseaux chantent joliment. +Tu marches comme si tu allais à l'école, alors que tout est si beau, ici, dans la forêt! +” Le Petit Chaperon Rouge ouvrit les yeux et lorsqu'elle vit comment les rayons du soleil dansaient de-ci, de-là à travers les arbres, et combien tout était plein de fleurs, elle pensa: +“Si j'apportais à ma grand- mère un beau bouquet de fleurs, ça lui ferait bien plaisir. +Il est encore si tôt que j'arriverai bien à l'heure. +” Elle quitta le chemin, pénétra dans le bois et cueillit des fleurs. +Et, chaque fois qu'elle en avait cueilli une, elle se disait: “Plus loin, j'en vois une plus belle,” et elle y allait et s'enfonçait toujours plus profondément dans la forêt. +Le Loup lui, courait tout droit vers la maison de la grand-mère. +Il frappa à la porte. +“Qui est là? +” +- “C'est le Petit Chaperon Rouge qui t'apporte du gâteau et du vin. +” +- “Tire la chevillette,” dit la grand-mère. +“Je suis trop faible et ne peux me lever. +” +Le Loup tire la chevillette, la porte s'ouvre et sans dire un mot, il s'approche du lit de la grand-mère et l'avale. +Il enfile ses habits, met sa coiffe, se couche dans son lit et tire les rideaux. +Pendant ce temps, le petit Chaperon Rouge avait fait la chasse aux fleurs. +Lorsque la fillette en eut tant qu'elle pouvait à peine les porter, elle se souvint soudain de sa grand-mère et reprit la route pour se rendre auprès d'elle. +Elle fut très étonnée de voir la porte ouverte. +Et lorsqu'elle entra dans la chambre, cela lui sembla si curieux qu'elle se dit: “Mon dieu, comme je suis craintive aujourd'hui. +Et, cependant, d'habitude, je suis si contente d'être auprès de ma grand-mère! +” Elle s'écria: “Bonjour! +” Mais nulle réponse. +Elle s'approcha du lit et tira les rideaux. +La grand-mère y était couchée, sa coiffe tirée très bas sur son visage. +Elle avait l'air bizarre. +“Oh, grand-mère, comme tu as de grandes oreilles. +” +- “C'est pour mieux t'entendre! +” +- “Oh! grand-mère, comme tu as de grands yeux! +” +- “C'est pour mieux te voir! +” - “Oh! grand-mère, comme tu as de grandes mains! +” +- “C'est pour mieux t'étreindre! +” +- “Mais, grand-mère, comme tu as une horrible et grande bouche! +” +- “C'est pour mieux te manger! +” À peine le Loup eut-il prononcé ces mots, qu'il bondit hors du lit et avala le pauvre Petit Chaperon Rouge. +Lorsque le Loup eut apaisé sa faim, il se recoucha, s'endormit et commença à ronfler bruyamment. +Un chasseur passait justement devant la maison. +Il se dit: “Comme cette vieille femme ronfle! +Il faut que je voie si elle a besoin de quelque chose. +” Il entre dans la chambre et quand il arrive devant le lit, il voit que c'est un Loup qui y est couché. +“Ah! +c'est toi, bandit! +” dit-il. +“Voilà bien longtemps que je te cherche. +” Il se prépare à faire feu lorsque tout à coup l'idée lui vient que le Loup pourrait bien avoir avalé la grand-mère et qu'il serait peut-être encore possible de la sauver. +Il ne tire pas, mais prend des ciseaux et commence à ouvrir le ventre du Loup endormi. +À peine avait-il donné quelques coups de ciseaux qu'il aperçoit le Chaperon Rouge. +Quelques coups encore et la voilà qui sort du Loup et dit: “Ah! +comme j'ai eu peur! +Comme il faisait sombre dans le ventre du Loup! +” Et voilà que la grand-mère sort à son tour, pouvant à peine respirer. +Le Petit Chaperon Rouge se hâte de chercher de grosses pierres. +Ils en remplissent le ventre du Loup. +Lorsque celui-ci se réveilla, il voulut s'enfuir. +Mais les pierres étaient si lourdes qu'il s'écrasa par terre et mourut. +Ils étaient bien contents tous les trois: le chasseur dépouilla le Loup et l'emporta chez lui. +La grand- mère mangea le gâteau et but le vin que le Petit Chaperon Rouge avait apportés. +Elle s'en trouva toute ragaillardie. +Le Petit Chaperon Rouge cependant pensait: “Je ne quitterai plus jamais mon chemin pour aller me promener dans la forêt, quand ma maman me l'aura interdit. +” On raconte encore qu’une autre fois, quand le Petit Chaperon Rouge apportait de nouveau de la galette à sa vieille grand-mère, un autre loup essaya de la distraire et de la faire sortir du chemin. +Mais elle s’en garda bien et continua à marcher tout droit. +Arrivée chez sa grand-mère, elle lui raconta bien vite que le loup était venu à sa rencontre et qu’il lui avait souhaité le bonjour, mais qu’il l’avait regardée avec des yeux si méchants: “Si je n’avais pas été sur la grand-route, il m’aurait dévorée! +” ajouta-t’elle. +“Viens,” lui dit sa grand-mère, “nous allons fermer la porte et bien la cadenasser pour qu’il ne puisse pas entrer ici. +” +Peu après, le loup frappait à la porte et criait: +“ +Ouvre-moi, grand-mère! +c’est moi, le Petit Chaperon Rouge, qui t’apporte des gâteaux! +” Mais les deux gardèrent le silence et n’ouvrirent point la porte. +Tête +-Grise fit alors plusieurs fois le tour de la maison à pas feutrés, et, pour finir, il sauta sur le toit, décidé à attendre jusqu’au soir, quand le Petit Chaperon Rouge sortirait, pour profiter de l’obscurité et l’engloutir. +Mais la grand-mère se douta bien de ses intentions. +“Prends le seau, mon enfant,” dit-elle au Petit Chaperon Rouge, “j’ai fait cuire des saucisses hier, et tu vas porter l’eau de cuisson dans la grande auge de pierre qui est devant l’entrée de la maison. +” Le Petit Chaperon Rouge en porta tant et tant de seaux que, pour finir, l’auge était pleine. +Alors la bonne odeur de la saucisse vint caresser les narines du loup jusque sur le toit. +Il se pencha si bien en tendant le cou, qu’à la fin il glissa et ne put plus se retenir. +Il glissa du toit et tomba droit dans l’auge de pierre où il se noya. +Allègrement, le Petit Chaperon Rouge regagna sa maison, et personne ne lui fit le moindre mal. +FIN +