{"id":13482652,"url":"https://github.com/turbopape/postagga","last_synced_at":"2025-04-07T12:09:43.021Z","repository":{"id":55652945,"uuid":"84552491","full_name":"turbopape/postagga","owner":"turbopape","description":"A Library to parse natural language in pure Clojure and ClojureScript","archived":false,"fork":false,"pushed_at":"2020-12-15T05:51:02.000Z","size":18510,"stargazers_count":159,"open_issues_count":12,"forks_count":16,"subscribers_count":18,"default_branch":"master","last_synced_at":"2024-11-12T11:16:45.907Z","etag":null,"topics":["bots","clojure","clojurescript","natural-language-processing","parser","pos-tagger","viterbi-algorithm"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/turbopape.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-03-10T11:19:05.000Z","updated_at":"2024-05-31T07:44:22.000Z","dependencies_parsed_at":"2022-08-15T05:40:16.780Z","dependency_job_id":null,"html_url":"https://github.com/turbopape/postagga","commit_stats":null,"previous_names":["fekr/postagga"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/turbopape%2Fpostagga","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/turbopape%2Fpostagga/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/turbopape%2Fpostagga/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/turbopape%2Fpostagga/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/turbopape","download_url":"https://codeload.github.com/turbopape/postagga/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247284957,"owners_count":20913704,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bots","clojure","clojurescript","natural-language-processing","parser","pos-tagger","viterbi-algorithm"],"created_at":"2024-07-31T17:01:04.120Z","updated_at":"2025-04-07T12:09:42.983Z","avatar_url":"https://github.com/turbopape.png","language":"Clojure","funding_links":["https://opencollective.com/postagga"],"categories":["函式庫","Text Processing","Packages"],"sub_categories":["書籍","Libraries"],"readme":"# postagga\n\n[![Backers on Open Collective](https://opencollective.com/postagga/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/postagga/sponsors/badge.svg)](#sponsors) [![License MIT](https://img.shields.io/badge/License-MIT-blue.svg)](http://opensource.org/licenses/MIT)\n[![Gratipay](https://img.shields.io/gratipay/turbopape.svg)](https://gratipay.com/turbopape/)\n[![Clojars Project](https://img.shields.io/clojars/v/postagga.svg)](https://clojars.org/postagga)\n\n\u003cimg src=\"./logo.png\" alt=\"postagga logo\" title=\"postagga on da mix\" align=\"right\"/\u003e\n \n\u003e \"But if thought corrupts language, language can also corrupt thought.\"\n- George Orwell, 1984\n\n**postagga** is a suite of tools to assist you in generating \nefficient and self-contained natural language processors. You can use **postagga** \nto process annotated text samples into full fledged parsers capable of\nunderstanding \"*free speech*\" input as structured data. Ah - and\nyou'll be able to do this easily. You're welcome.\n\n# Getting postagga\n\nYou can refer **postagga** as a lib in your Clojure project. Grab it\nfrom clojars - in your dependencies in **project.clj**, just add:\n\n[![Clojars Project](https://img.shields.io/clojars/v/postagga.svg)](https://clojars.org/postagga)\n\nYou can also clone the project and walk around the source and models:\n\n```ssh\ngit clone https://github.com/turbopape/postagga.git\n```\n\nThe models are included under the [models folder](https://github.com/turbopape/postagga/blob/master/models). \n\nIn JVM Clojure, provided you have cloned the repository:\n```clojure\n;; ...\n (def fr-model (load-edn \"models/fr_tb_v_model.edn\")) ;; for French for instance\n;; ... \n```\nWe also shipped two light models as vars defined in namespaces: one for\nFrench and one for English. As for JavaScript, the artifacts size are\na concern. You can use these models by requiring the two namespaces:\n\n```clojure\n  (ns your-cool.bot\n   (: require [postagga.en-fn-v-model :refer [en-model]] ;; for English\n              [postagga.fr-tb-v-model :refer [fr-model]])) ;; for French \n   ;; ...\n   \n```\nThese namespaces make it easy for you to ship parsers for ClojureScript.\n\nYou can see an example on how to work with this model, all while\nmaking sure your code is compatible across Clojure AND ClojureScript\n(thanks to readers' conditional) in the [Test File](https://github.com/turbopape/postagga/blob/master/test/postagga/core_test.cljc).\n\n# How does it work?\n\nTo do its magic, **postagga** extracts the *phrase structure* of your input and tries to find how does this structure compare to its many semantic rules and if it finds a match, where in this structure shall it extract meaningful information.\n\nLet's study a simple example. Look at the next sentence:\n\n\u003e \"Rafik loves apples\"\n\nThat is our \"Natural language input.\"\n\nFirst step in understanding this sentence is to extract some structure from it so it is easier to interpret. One common way to do this is extracting its grammatical phrase structure, which is close enough to what \"function\" words are actually meant to provide:\n\n\u003e Noun Verb Noun\n\nThat was the phrase structure analysis, or as we call it POS (Part Of Speech) Tagging. These \"Tags\" qualify parts of the sentence, as the name implies, and will be used as a hi-fidelity mechanism to write rules for parsers of such phrases.\n\n**postagga** has tools that enable you to train POS Taggers for any language you want, without relying on external libs. Actually, it does not care about the meaning of the tags at all. However, you should be consistent and clear enough when annotating your input data samples with tags. On one hand, your parser will be more reliable. On the other hand you'll do yourself a great favor maintaining your parser.\n\nNow comes the parser part. Actually, **postagga** offers a parser that needs semantic **rules** to be able to map a particular phrase structure into data. In our example, we know that the first **Noun** depicts a subject carrying out some action. This action is represented by the **Verb** following it. Finally, the **Noun** coming after the **Verb** will undergo this action.\n\n**postagga** parsers lets you express such rules so it can extract the data for you. You literally tell it to take the first **Noun**, call it **Subject**, take the verb, label it **action**, take the last **Noun**, call it the **Object**, finally packaging it all into the following data structure:\n\n```clojure\n{:Subject \"Rafik\" :Action \"Loves\" :Object \"Apples\"}\n```\nNaturally, **postagga** can handle much more complex sentences!\n\n**postagga** parsers are eventually compiled into self-contained packages, with no single third party dependency. From there it can easily run on servers (Clojure version) and on the browser (ClojureScript). Now your bots can really get what you're trying to tell them!\n\n# The postagga workflow\n\n## Training a POS Tagger\nFirst of all, you need to train a POS Tagger that can qualify parts of\nyour natural text. **postagga** relies on Hidden Markov Models,\ncomputed with\nthe\n[Viterbi  Algorithm](https://en.wikipedia.org/wiki/Viterbi_algorithm). This\nalgorithm makes use of a set of matrices, like what states (the POS Tags)\nwe have, how likely we transition from one POS tag to another,\netc...\n\nAll of these constitute a **model**. These are computed out of what we\ncall an **annotated text corpus**. The **postagga.trainer** namespace is used create models\nout of such annotated text corpus.\nTo train a model, make sure you have an annotated corpus like so:\n\n```clojure\n[ ;; A vector of sentences like this one:\n[[\"-\" \"PONCT\"] [\"guerre\" \"NC\"] [\"d'\" \"P\"] [\"indochine\" \"NPP\"]] [[\"-\" \"PONCT\"] [\"colloque\" \"NC\"] [\"sur\" \"P\"] [\"les\" \"DET\"] [\"fraudes\" \"NC\"]] [[\"-\" \"PONCT\"] [\"dernier\" \"ADJ\"] [\"résumé\" \"NC\"] [\":\" \"PONCT\"] [\"l'\" \"DET\"] [\"\\\"\" \"PONCT\"] [\"affaire\" \"NC\"] [\"des\" \"P+D\"] [\"piastres\" \"NC\"] [\"\\\"\" \"PONCT\"]] [[\"catégories\" \"NC\"] [\":\" \"PONCT\"] [\"guerre\" \"NC\"] [\"d'\" \"P\"] [\"indochine\" \"NPP\"] [\".\" \"PONCT\"]] [[\"indochine\" \"NPP\"] [\"française\" \"ADJ\"] [\".\" \"PONCT\"]] [[\"quatrième\" \"ADJ\"] [\"république\" \"NC\"] [\".\" \"PONCT\"]\n;; etc...\n]\n```\n\nSay you have this corpus - that is: a vector of annotated sentences\nin a var unsurprisingly named **corpus**. To train a **model**, just issue:\n\n```clojure\n(require '[postagga.trainer :refer [train]]\n\n(def model (train corpus)) ;;\u003c- Beware, these can be large vars so avoid realizing all of them like printing in your REPL!!!\n```\n\nWe processed one annotated corpus for English:\n- [postagga-fn-en.edn](https://github.com/turbopape/postagga/blob/master/models/postagga-fn-en.edn)\n  Generated from\n  the\n  [Framenet Project](https://framenet.icsi.berkeley.edu/fndrupal/)\n\nWe also processed two annotated corpora for French:\n- [postagga-sequoia-fr.edn](https://github.com/turbopape/postagga/blob/master/models/postagga-sequoia-fr.edn)\n    Generated from\n    the\n    [Sequoia Corpus from INRIA](https://www.rocq.inria.fr/alpage-wiki/tiki-index.php?page=CorpusSequoia).\n    \n- [postagga-tb-fr.edn](https://github.com/turbopape/postagga/blob/master/models/fr_tb_v_model.edn)\n    Generated from\n    the\n    [Free French tree Bank](https://github.com/nicolashernandez/free-french-treebank).\n    \nWe exposed two of these models as Clojure namespaces so you can embed\nthem without using the **resource** functionality - as it is specific\nto Clojure(JVM). We chose the two lightest ones to limit the possibility\ncause network issues:\n\n- [French Model as a namespace: postagga.fr_tb_model](https://github.com/turbopape/postagga/blob/master/src/postagga/fr_tb_model.cljc)\n- [English Model as a namespace: postagga.en_fn_model](https://github.com/turbopape/postagga/blob/master/src/postagga/en_fn_model.cljc)\n\nThe suite of tools used to process these two corpora are in\nthe [corpuscule project](https://github.com/turbopape/corpuscule). \n**Please refer to the licensing of these corpora to see what\nextent you can use work derived from them.**\n\nWe then trained a  model out of the above English corpus:\n\n- [en_fn_v_model.edn](https://github.com/turbopape/postagga/blob/master/models/en_fn_v_model.edn)\n\n... and two models out of these two French corpora:\n- [fr_sequoia_pos_v_model.edn](https://github.com/turbopape/postagga/blob/master/models/fr_sequoia_pos_v_model.edn)\n- [fr_tb_v_model.edn](https://github.com/turbopape/postagga/blob/master/models/fr_tb_v_model.edn)\n      \n    \nNow you can use that **model** to assign POS tags to speech:\n\n(**Note:** sentences must be fed in the form of a vector of all small-case\ntokens)\n```clojure\n(require '[postagga.tagger :refer [viterbi]])\n\n(viterbi model [\"je\" \"suis\" \"heureux\"])\n;;=\u003e [\"CLS\" \"V\" \"ADJ\"]\n```\n### Patching Viterbi's output\n\nWhen the tagger encounters a word it doesn't know about- that is, was\nnot in the corpus used to generate the viterbi models - it arbitrarily\nassigns it a tag - more or less randomly picked by the algorithm. To \nsomehow enhance the detection, it is possible to *patch* the output,\nthat is, look it up in a dictionary of terms of a known type and force the tags\naccordingly. For instance, if you have a dictionary for\nproper nouns in a given language, you can patch your HMM generated\nPOS-tags by forcing every word happening to be an entry in this\ndictionary to have the \"NPP\" tag.\n\nWe provide two dictionaries for proper nouns:\n- [fr_tr_names.cljc](https://github.com/turbopape/postagga/blob/master/src/postagga/fr_tr_names.cljc) for French,\n- [en_tr_names.cljc](https://github.com/turbopape/postagga/blob/master/src/postagga/en_tr_names.cljc) for English. \n\nYou can see how you can integrate patching in the parsing phase\nhereafter.\n\nTechnically, dictionaries are [tries](https://en.wikipedia.org/wiki/Trie) to speed up lookup for multiple\nentries. But this may evolve during time and should be considered as\nmere details implementation.\n\n### Meaning of tags\nA reference to the meaning of tags is provided:\n- For [English](https://github.com/turbopape/postagga/blob/master/models/en_penn_tb_tags.md)\n\n## Using the tagger to parse free speech\n\nNow that you have your tagger trained, you can use a parser to drill the\ninformation from your sentences. For our last example say you want\n**postagga** to understand how you currently feel, or how you look. It can be done by detecting\nthe first token as being a Subject - **CLS**, doing a Verb - **V** and\nthen having an Adjective - **ADJ**. We want to detect who is having what\nadjective in our sentence.\nFor this, we'll use the **postagga.parser** namespace.\n\nFirst of all, require the namespace:\n\n```clojure\n(require '[postagga.parser :refer [parse-tags-rules]])\n```\n\nThen, you'll need to specify rules for the parser. We want to grab the\nword tagged as **CLS** and the word tagged as **ADJ** as our\ninformation. Here's what the parser rules look like:\n\n```clojure\n(def sample-rules [{;;Rule TB French \"je suis heureux.\"\n                    :id :sample-rule-tb-french\n                    :optional-steps []\n                    :rule [\n                           :qui       ;;\u003c----- A atep\n                           #{:get-value #{\"CLS\"}} ;;\u003c----- A state in the parse machine\n                                           ;;i.e, a set of possible sets of POS TAGS                           \n                           :!OR!\n                           \n                           :product\n                           #{#{\"DET\"}}\n                           #{:get-value #{\"NC\"}} ;;\u003c--- an alternate possible\n                                                 ;; state at this step\n                                                 \n                           \n                           :mood              ;;\u003c--- Another step\n                           #{#{\"V\"}}\n                           #{:get-value #{\"ADJ\"}}]}]\n```\nThis deserves some explanation before we carry on with our example.\n\nThe parser is basically a state machine. It goes through **steps** *([:qui, :mood])*, with each step encompassing multiple\n**states** *([#{#{\"V}} ...])*. A **state** basically refers to words; it is matched with tag sets\n(a word can relate to multiple tags, if your preferred tagger wants to!). \nDifferent tag sets can be assigned to a **state**. For instance, to say that in some **state** we require either a *Noun(\"NPP\")* or a *Verb(\"V\")*, you might put:\n\n```clojure\n;...\n#{#{\"V\"} #{\"NPP\"}}\n;...\n```\n\nPutting the keyword **:get-value** in a **state** tells the parser to grab the word having\nled to this state, put in the yielded parse map, and finally assign it to a key representing\nthe **step** in which that state was in. Confusing, isn't it? :confused:\n\nYou'll get it with an example.\n\nLet's say that somewhere we have:\n\n```clojure\n[:qui ; \u003c-- A step\n;;...\n   {:get-value #{\"CLS\"}} ;;\u003c-- A state with :get-value under the :qui step\n;;...\n]\n```\nThe value of the word that yielded the tag **CLS** (which is **je** in our example) will be reflected on the\noutput map as an entry in some vector, associated with the related step, which is **qui**:\n\n```clojure\n{:qui [\"je\"]}\n```\nThis is what the **postagga** parser is all about: you tell it where to extract information and how you want it structured for upstream processing.\n\nIf we had multiple states with **:get-value** flag on, we'll find multiple words in the corresponding entry in the output. This is why the **step** key is referring a vector of words in the output map.\n\nIt is also possible to say that a state can be encountered repeatedly\nusing the **:multi** keyword. If you say in certain state:\n```clojure\n:some-step\n;...\n#{:get-value :multi #{\"ADJ\"}\n;...\n```\nand if you feed **postagga** the following tokenized sentence:\n\n```clojure\n[\"il\" \"parait\" \"beau\" \"grand\" \"heureux\"]\n```\n\nyou'll find in the parse map:\n```clojure\n{:some-step [\"beau\" \"grand\" \"heureux\"]}\n```\n\nThe **:optional-steps** stanza tells the parser not to raise an error if a step\nbelonging to this vector is not present.\n\nAt any step you can specify multiple alternatives for capturing\ndifferent sets of information. In the above example, you can say that\nthe first step in your sentence might talk about a person captured\nthrough the *CLS* attribute or a product captured by specified a\n*DET* than a *NC*. You specify such alternatives via the :!OR! keyword.\n\n\nYou'll also need to tell the parser how to break down a line of text\ninto a vector of words. We call this a **tokenizer**. Waiting to\ndevelop a full fledged couple with language-specific rules, we can\njust start by a naive one that splits strings using space characters:\n\n```clojure\n;; Hey, this one works only on Clojure (JVM) version !!\n(def sample-tokenizer-fn #(clojure.string/split % #\"\\s\"))\n```\n\nBack to our sample. With **sample-rules** holding a set of rules as defined above,\nyou can parse your sentence like so:\n\n```clojure\n(def parse-result (parse-tags-rules \n                   sample-tokenizer-fn      ;; The tokenizer function.\n                   (partial viterbi model)  ;; The tagger function - curried with a model\n                   sample-rules             ;; The parser rules.\n                   \"je suis heureux\"))      ;; The sentence to parse. \n```\nand you'd have a detailed result like so:\n\n```clojure\n{:errors nil ;;\u003c- The error if any\n :result {:rule :sample-rule-tb-french ;; \u003c- Which rule was detected \n          :data {:qui [\"je\"],          ;; \u003c- The data structure drilled\n                                       ;;    down from the input.\n                 :mood [\"heureux\"]}}}\n```\n\nThe errors will be reported as a collection mapping each rule to what\nstep and state the parser failed. This can be quite large, so be\ncareful not to spit the contents of the result directly into your REPL - \nyou can test on the **:errors** being _nil_ and work with the\n**:data** value:\n\n```clojure\n;; Do something with\n(:data parse-result)\n \n```\n\nTo integrate patching, as discussed above to the parsing, you can\nproceed as follows:\n\n```clojure\n(def patch-fr-tagger-w-name ;;\u003c- a function that wraps viterbi into a\n                            ;; \"patched\" version\n  #(patch-w-entity  0.9 % en-names-trie\n                    ;; Takes a sentence, computes tags with viterbi\n                    ;; and afterwards, looks if the words are close\n                    ;; enough to entries in the French names\n                    ;; dictionary, in which case it will force them to\n                    ;; have \"NC\" tag \n                    (viterbi fr-model %)\n                    \"NC\"))\n;;=\u003e #'postagga.core-test/patch-fr-tagger-w-name                    \n\n(-\u003e (parse-tags-rules sample-tokenizer-fn \n                      patch-fr-tagger-w-name \n                      sample-rules \"nicolas est heureux\")\n              (get-in [:result :data]))\n;;=\u003e {:qui[\"nicolas\"] :mood [\"heureux\"]}              \n```\n\n# Complete list of features\nYou can see some of this workflow (other than the training) in the\n[Tests](https://github.com/turbopape/postagga/blob/master/test/postagga/core_test.cljc).\n\nPlease refer to the [Changelog](https://github.com/turbopape/postagga/blob/master/CHANGELOG.md) to see included features per version.\n\n# TODO and contributing\n\n**postagga** can make great use of great contributors like you! I'll\ntrack the enhancements, bugs, features, etc., in the [project issues](https://github.com/turbopape/postagga/issues)\ntab and please feel free to send your PRs!\n\n# Code Of Conduct\n\nPlease note that this project is released with a [Contributor Code of Conduct](./CODE_OF_CONDUCT.md). By\nparticipating in this project, you agree to abide by its terms.\n\n## Contributors\n\nThis project exists thanks to all the people who contribute. [[Contribute]](CONTRIBUTING.md).\n\u003ca href=\"graphs/contributors\"\u003e\u003cimg src=\"https://opencollective.com/postagga/contributors.svg?width=890\" /\u003e\u003c/a\u003e\n\n\n## Backers\n\nThank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/postagga#backer)]\n\n\u003ca href=\"https://opencollective.com/postagga#backers\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/backers.svg?width=890\"\u003e\u003c/a\u003e\n\n\n## Sponsors\n\nSupport this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/postagga#sponsor)]\n\n\u003ca href=\"https://opencollective.com/postagga/sponsor/0/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/0/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/postagga/sponsor/1/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/1/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/postagga/sponsor/2/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/2/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/postagga/sponsor/3/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/3/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/postagga/sponsor/4/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/4/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/postagga/sponsor/5/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/5/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/postagga/sponsor/6/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/6/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/postagga/sponsor/7/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/7/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/postagga/sponsor/8/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/8/avatar.svg\"\u003e\u003c/a\u003e\n\u003ca href=\"https://opencollective.com/postagga/sponsor/9/website\" target=\"_blank\"\u003e\u003cimg src=\"https://opencollective.com/postagga/sponsor/9/avatar.svg\"\u003e\u003c/a\u003e\n\n\n\n# License and Credits\n\nCopyright (c) 2017 [Rafik Naccache](mailto:rafik@fekr.tech).\n\nHappily brought to you by [fekr](http://www.fekr.tech).\n\nThe Logo is created by my talented friend the great [Chakib Daoud](https://www.facebook.com/3amettaher/?fref=ts)\n\nDistributed under the terms of the [MIT License](\"http://opensource.org/licenses/MIT).\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fturbopape%2Fpostagga","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fturbopape%2Fpostagga","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fturbopape%2Fpostagga/lists"}