ruby-spacy 0.6.0 → 0.7.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +28 -5
- data/CHANGELOG.md +22 -0
- data/Gemfile +7 -0
- data/README.md +51 -1
- data/docs/syntax_trees.md +55 -0
- data/examples/rsyntaxtree/outputs/tree_ar_projection.png +0 -0
- data/examples/rsyntaxtree/outputs/tree_de_projection.png +0 -0
- data/examples/rsyntaxtree/outputs/tree_en_chunks.png +0 -0
- data/examples/rsyntaxtree/outputs/tree_en_morphology.png +0 -0
- data/examples/rsyntaxtree/outputs/tree_en_projection.png +0 -0
- data/examples/rsyntaxtree/outputs/tree_ja_chunks.png +0 -0
- data/examples/rsyntaxtree/outputs/tree_ja_projection.png +0 -0
- data/examples/rsyntaxtree/outputs/tree_ru_morphology.png +0 -0
- data/examples/rsyntaxtree/outputs/tree_ru_projection.png +0 -0
- data/examples/rsyntaxtree/outputs/tree_zh_projection.png +0 -0
- data/examples/rsyntaxtree/syntax_tree_ar.rb +36 -0
- data/examples/rsyntaxtree/syntax_tree_de.rb +19 -0
- data/examples/rsyntaxtree/syntax_tree_en.rb +26 -0
- data/examples/rsyntaxtree/syntax_tree_ja.rb +21 -0
- data/examples/rsyntaxtree/syntax_tree_ru.rb +23 -0
- data/examples/rsyntaxtree/syntax_tree_zh.rb +21 -0
- data/lib/ruby-spacy/syntax_tree.rb +304 -0
- data/lib/ruby-spacy/version.rb +1 -1
- data/lib/ruby-spacy.rb +75 -1
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: eff16e19adc4863d93ac22803e63e5ac2ab8bba077eca7cbc8c2403174704aac
|
|
4
|
+
data.tar.gz: 1766dc8fbb6c2c4b10314c925ddae484907f6677f9fb314ede756c9b3752923e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b1ef879141c89307e0fa05d5130fa997c0e1746ab0fc4a466dca032a6c4f1007341e8e6a5d1369d1f5a260070e161df8df60f87042343870c862d931faaa7789
|
|
7
|
+
data.tar.gz: 54baa96c6f475f147b374d8b27a80c6aa792937d759793178fd4f78592588f93d121f4e1edbfb89fdb7e8da5cd6b74abf8b1ccc404ef8bd6f836cc88b55fa407
|
data/.github/workflows/ci.yml
CHANGED
|
@@ -33,6 +33,10 @@ jobs:
|
|
|
33
33
|
env:
|
|
34
34
|
# PyCall locates the Python interpreter via this variable
|
|
35
35
|
PYTHON: python
|
|
36
|
+
# rsyntaxtree (used by the syntax_tree tests) is in the :rendering
|
|
37
|
+
# bundler group and needs pango/rsvg system libraries; install it only
|
|
38
|
+
# on representative jobs
|
|
39
|
+
BUNDLE_WITHOUT: ${{ matrix.python != '3.13' && 'rendering' || '' }}
|
|
36
40
|
steps:
|
|
37
41
|
- uses: actions/checkout@v7
|
|
38
42
|
|
|
@@ -42,6 +46,12 @@ jobs:
|
|
|
42
46
|
with:
|
|
43
47
|
python-version: ${{ matrix.python }}
|
|
44
48
|
|
|
49
|
+
# Must precede ruby/setup-ruby: bundler-cache installs the pango/rsvg2
|
|
50
|
+
# gems, which compile against these system libraries
|
|
51
|
+
- name: Install rsyntaxtree system dependencies (representative jobs)
|
|
52
|
+
if: matrix.python == '3.13'
|
|
53
|
+
run: sudo apt-get update && sudo apt-get install -y libpango1.0-dev librsvg2-dev libgirepository1.0-dev
|
|
54
|
+
|
|
45
55
|
- uses: ruby/setup-ruby@v1
|
|
46
56
|
with:
|
|
47
57
|
ruby-version: ${{ matrix.ruby }}
|
|
@@ -53,22 +63,35 @@ jobs:
|
|
|
53
63
|
# Cache the language models only (en_core_web_lg alone is several hundred
|
|
54
64
|
# MB). spaCy itself is intentionally installed fresh on every run so that
|
|
55
65
|
# new releases are exercised immediately. Bump the key suffix when
|
|
56
|
-
# upgrading the models.
|
|
66
|
+
# upgrading the models. The ja/ru/de/zh models are installed only on the
|
|
67
|
+
# python 3.13 jobs (where the syntax_tree tests run); listing them here
|
|
68
|
+
# for other jobs is harmless
|
|
57
69
|
- uses: actions/cache@v6
|
|
58
70
|
with:
|
|
59
71
|
path: |
|
|
60
72
|
${{ steps.site-packages.outputs.path }}/en_core_web_sm*
|
|
61
73
|
${{ steps.site-packages.outputs.path }}/en_core_web_lg*
|
|
62
|
-
|
|
74
|
+
${{ steps.site-packages.outputs.path }}/ja_core_news_sm*
|
|
75
|
+
${{ steps.site-packages.outputs.path }}/ru_core_news_sm*
|
|
76
|
+
${{ steps.site-packages.outputs.path }}/de_core_news_sm*
|
|
77
|
+
${{ steps.site-packages.outputs.path }}/zh_core_web_sm*
|
|
78
|
+
key: spacy-models-${{ matrix.python }}-v2
|
|
63
79
|
|
|
64
|
-
# en_core_web_lg
|
|
65
|
-
#
|
|
80
|
+
# en_core_web_lg and the ja/ru/de/zh models are large; install them only
|
|
81
|
+
# on representative jobs (where the syntax_tree tests run). Tests that
|
|
82
|
+
# need them are skipped automatically where they are absent.
|
|
66
83
|
- name: Install spaCy and language models
|
|
67
84
|
run: |
|
|
68
85
|
python -m pip install --upgrade pip
|
|
69
86
|
python -m pip install --upgrade spacy
|
|
70
87
|
python -m spacy download en_core_web_sm
|
|
71
|
-
if [ "${{ matrix.python }}" = "3.13" ]; then
|
|
88
|
+
if [ "${{ matrix.python }}" = "3.13" ]; then
|
|
89
|
+
python -m spacy download en_core_web_lg
|
|
90
|
+
python -m spacy download ja_core_news_sm
|
|
91
|
+
python -m spacy download ru_core_news_sm
|
|
92
|
+
python -m spacy download de_core_news_sm
|
|
93
|
+
python -m spacy download zh_core_web_sm
|
|
94
|
+
fi
|
|
72
95
|
|
|
73
96
|
- name: Run tests
|
|
74
97
|
run: bundle exec rake test
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Change Log
|
|
2
2
|
|
|
3
|
+
## 0.7.0 - 2026-09-03
|
|
4
|
+
### Added
|
|
5
|
+
- `Doc#syntax_tree` and `Span#syntax_tree` — turn a parse into rsyntaxtree
|
|
6
|
+
bracket notation, or draw it as SVG, PNG, PDF, TikZ, or JSON. Two styles:
|
|
7
|
+
`:projection` builds a phrase-structure-like tree from the head words,
|
|
8
|
+
`:chunks` lays the noun chunks out flat. Noun chunks are shaded, named
|
|
9
|
+
entities that coincide with one are labeled, and `morphology: true` attaches
|
|
10
|
+
a table of features to every leaf. rsyntaxtree (>= 2.4.0) is an optional
|
|
11
|
+
dependency, required on first use rather than at load time
|
|
12
|
+
- `Language.new(py_nlp:)` — wrap a Python pipeline built elsewhere, for the
|
|
13
|
+
languages spaCy ships no trained model for. Every right-to-left language is
|
|
14
|
+
one of them, and their trees are drawn mirrored so the leaves run in reading
|
|
15
|
+
order. A package such as spacy-stanza builds the pipeline; ruby-spacy gains
|
|
16
|
+
no dependency
|
|
17
|
+
- `docs/syntax_trees.md` — a gallery of trees in six languages, generated by
|
|
18
|
+
the scripts in `examples/rsyntaxtree/`
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- CI runs the syntax tree tests against English, Japanese, Russian, German, and
|
|
22
|
+
Chinese models, which between them cover the paths that differ: with and
|
|
23
|
+
without noun chunks, two-line entity labels, and a mirrored right-to-left tree
|
|
24
|
+
|
|
3
25
|
## 0.6.0 - 2026-08-31
|
|
4
26
|
### Added
|
|
5
27
|
- GitHub Actions CI — Ruby 3.2 to 4.0 (plus ruby-head) and Python 3.11 to 3.14,
|
data/Gemfile
CHANGED
|
@@ -9,3 +9,10 @@ group :development do
|
|
|
9
9
|
gem "github-markup"
|
|
10
10
|
gem "redcarpet"
|
|
11
11
|
end
|
|
12
|
+
|
|
13
|
+
# Used only for rendering in Doc#syntax_tree / Span#syntax_tree tests.
|
|
14
|
+
# Kept in its own group so CI jobs without the pango/rsvg system libraries
|
|
15
|
+
# can exclude it with BUNDLE_WITHOUT=rendering.
|
|
16
|
+
group :rendering do
|
|
17
|
+
gem "rsyntaxtree", ">= 2.4.0", require: false
|
|
18
|
+
end
|
data/README.md
CHANGED
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
| ✅ | Part-of-speech tagging and dependency parsing |
|
|
11
11
|
| ✅ | Named entity recognition |
|
|
12
12
|
| ✅ | Syntactic dependency visualization |
|
|
13
|
+
| ✅ | Syntax tree visualization (via rsyntaxtree) |
|
|
13
14
|
| ✅ | Access to pre-trained word vectors |
|
|
14
15
|
| ✅ | LLM integration: OpenAI, Anthropic (Claude), and local models |
|
|
15
16
|
|
|
16
|
-
Current Version: `0.
|
|
17
|
+
Current Version: `0.7.0`
|
|
17
18
|
|
|
18
19
|
- Ruby 3.2 to 4.0 supported (PyCall 1.5.3 or later required)
|
|
19
20
|
- spaCy 3.8 supported
|
|
21
|
+
- Syntax trees drawn with rsyntaxtree, including right-to-left languages
|
|
20
22
|
- Multi-provider LLM API: OpenAI, Anthropic (Claude), and local models via Ollama or any OpenAI-compatible server
|
|
21
23
|
- Structured outputs (JSON Schema) support
|
|
22
24
|
- Block-based LLM API with linguistic analysis
|
|
@@ -77,6 +79,18 @@ Or install it yourself as:
|
|
|
77
79
|
|
|
78
80
|
See [Examples](#examples) below.
|
|
79
81
|
|
|
82
|
+
### Using an External Pipeline
|
|
83
|
+
|
|
84
|
+
`Spacy::Language.new` normally loads an installed model by name. To use a language spaCy has no model for, or a pipeline you built yourself, pass an existing Python `Language` object instead:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
py_nlp = PyCall.import_module("spacy_stanza").load_pipeline("ar")
|
|
88
|
+
nlp = Spacy::Language.new(py_nlp: py_nlp)
|
|
89
|
+
nlp.read("...").tokens # works like any other nlp
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Third-party packages such as [spacy-stanza](https://github.com/explosion/spacy-stanza) or [spacy-udpipe](https://github.com/TakeLab/spacy-udpipe) are not ruby-spacy dependencies; install them yourself. `model` and `py_nlp:` are mutually exclusive.
|
|
93
|
+
|
|
80
94
|
## Examples
|
|
81
95
|
|
|
82
96
|
Many of the following examples are Python-to-Ruby translations of code snippets in [spaCy 101](https://spacy.io/usage/spacy-101). For more examples, look inside the `examples` directory.
|
|
@@ -286,6 +300,42 @@ Output:
|
|
|
286
300
|
|
|
287
301
|

|
|
288
302
|
|
|
303
|
+
### Syntax Trees
|
|
304
|
+
|
|
305
|
+
→ [rsyntaxtree](https://github.com/yohasebe/rsyntaxtree)
|
|
306
|
+
|
|
307
|
+
`Doc#syntax_tree` (and `Span#syntax_tree`) converts the parse into rsyntaxtree bracket notation and can render it as an image. rsyntaxtree (>= 2.4.0) is an optional dependency: `gem install rsyntaxtree`.
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
require "ruby-spacy"
|
|
311
|
+
|
|
312
|
+
nlp = Spacy::Language.new("en_core_web_sm")
|
|
313
|
+
doc = nlp.read("The quick brown fox jumped over the lazy dog near the river.")
|
|
314
|
+
|
|
315
|
+
doc.syntax_tree # => "[S [%NP [DET The] [ADJ quick] ...] ...]"
|
|
316
|
+
File.binwrite("tree.png", doc.syntax_tree(format: :png))
|
|
317
|
+
doc.syntax_tree(style: :chunks) # shallow tree with noun chunks
|
|
318
|
+
doc.syntax_tree(morphology: true) # attach morphology tables to the leaves
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
<img src="https://github.com/yohasebe/ruby-spacy/blob/main/examples/rsyntaxtree/outputs/tree_en_projection.png" alt="English projection tree" width="560">
|
|
322
|
+
|
|
323
|
+
<img src="https://github.com/yohasebe/ruby-spacy/blob/main/examples/rsyntaxtree/outputs/tree_en_morphology.png" alt="English projection tree with morphology tables" width="760">
|
|
324
|
+
|
|
325
|
+
<img src="https://github.com/yohasebe/ruby-spacy/blob/main/examples/rsyntaxtree/outputs/tree_ja_projection.png" alt="Japanese projection tree with named entities" width="500">
|
|
326
|
+
|
|
327
|
+
Two styles are available: `:projection` (default; a phrase-structure-like tree projected from the head words) and `:chunks` (a shallow tree with noun chunks). In both, noun chunks are shaded grey, and a chunk that is also a named entity is shaded orange and labeled with the entity type (`entities: false` to disable). The blue and green are rsyntaxtree's default palette, not marking of any kind; only the shading carries meaning. Punctuation is omitted (`punctuation: true` to keep it). The `format:` option accepts `:bracket` (default), `:svg`, `:png`, `:pdf`, `:tikz`, and `:json`; any other keywords are passed through to rsyntaxtree (e.g. `fontsize: 12`).
|
|
328
|
+
|
|
329
|
+
The notation is rsyntaxtree-flavored: it can contain AVMs (`#(...#)`), region backgrounds (`%`), and in-word space joins (`<>`). A doc must hold a single sentence; for a multi-sentence doc, use `doc.sents.map { |s| s.syntax_tree }`.
|
|
330
|
+
|
|
331
|
+
**Annotation schemes differ between models.** spaCy's English models make the preposition the head of its phrase, so projection trees contain `PP` nodes. UD-style models (Japanese, Russian, Chinese, and most others) attach prepositions and particles to the noun instead, so no `PP` appears. Phrase labels come from the head's POS tag and follow whatever scheme the model uses.
|
|
332
|
+
|
|
333
|
+
**Entity highlighting requires noun chunks.** An entity gets its colored background only where it coincides with a noun chunk. In languages whose models have no noun chunk iterator (Russian, Chinese, Korean, Polish, ...) `entities: true` highlights nothing and `style: :chunks` raises `ArgumentError`.
|
|
334
|
+
|
|
335
|
+
**Right-to-left languages.** spaCy ships no pipelines for Arabic, Hebrew, or other RTL languages, so one has to come from outside (see [Using an External Pipeline](#using-an-external-pipeline)). Their trees are drawn mirrored automatically; pass `mirror: "off"` to disable.
|
|
336
|
+
|
|
337
|
+
See the [syntax tree gallery](docs/syntax_trees.md) for rendered trees in six languages, and `examples/rsyntaxtree/` for the scripts that generate them.
|
|
338
|
+
|
|
289
339
|
### Named Entity Recognition
|
|
290
340
|
|
|
291
341
|
→ [spaCy: Named entities](https://spacy.io/usage/spacy-101#annotations-ner)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Syntax Tree Gallery
|
|
2
|
+
|
|
3
|
+
Trees drawn by `Doc#syntax_tree` with the [rsyntaxtree](https://github.com/yohasebe/rsyntaxtree) gem. The scripts in `examples/rsyntaxtree/` generate every image here.
|
|
4
|
+
|
|
5
|
+
Noun chunks are shaded grey; a chunk that is also a named entity is shaded orange and labeled with the entity type. The blue and green are rsyntaxtree's default palette, not marking of any kind. Phrase labels come from the head's POS tag, so they follow whatever annotation scheme the model uses. Both are explained in the [README](../README.md#syntax-trees).
|
|
6
|
+
|
|
7
|
+
## English (`en_core_web_sm`)
|
|
8
|
+
|
|
9
|
+
Projection, the default style:
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
Chunks style:
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
With morphology (a shorter sentence — the tables are tall):
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
## Japanese (`ja_core_news_sm`)
|
|
22
|
+
|
|
23
|
+
Projection, with PERSON and GPE highlighted:
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
Chunks style:
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
## Russian (`ru_core_news_sm`)
|
|
32
|
+
|
|
33
|
+
No noun chunk iterator, so `style: :chunks` is unavailable.
|
|
34
|
+
|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
With morphology — Russian marks case, gender, animacy, aspect, and voice:
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
## German (`de_core_news_sm`)
|
|
42
|
+
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
## Chinese (`zh_core_web_sm`)
|
|
46
|
+
|
|
47
|
+
No noun chunk iterator, so `style: :chunks` is unavailable.
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
## Arabic (right-to-left)
|
|
52
|
+
|
|
53
|
+
spaCy ships no Arabic pipeline, so this tree was parsed with [Stanza](https://stanfordnlp.github.io/stanza/) via [spacy-stanza](https://github.com/explosion/spacy-stanza) and wrapped with `Spacy::Language.new(py_nlp:)`. Right-to-left languages are drawn mirrored automatically. This pipeline has no noun chunks, so nothing is shaded. See `examples/rsyntaxtree/syntax_tree_ar.rb`.
|
|
54
|
+
|
|
55
|
+

|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Arabic via an external pipeline. spaCy ships no trained pipeline for
|
|
4
|
+
# Arabic (or any other right-to-left language), so this example builds one
|
|
5
|
+
# with Stanza through spacy-stanza. These are NOT ruby-spacy dependencies;
|
|
6
|
+
# install them yourself first:
|
|
7
|
+
#
|
|
8
|
+
# pip install spacy-stanza
|
|
9
|
+
# pip install --upgrade "stanza>=1.10" # spacy-stanza's stanza pin is old and conflicts with current torch
|
|
10
|
+
# python -c "import stanza; stanza.download('ar')"
|
|
11
|
+
#
|
|
12
|
+
# Note: at exit, spacy-stanza under PyCall prints a harmless
|
|
13
|
+
# multiprocessing.resource_tracker LoadError to stderr (sys.executable
|
|
14
|
+
# points to ruby, not python); the results are unaffected.
|
|
15
|
+
|
|
16
|
+
# add path to ruby-spacy lib to load path
|
|
17
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
18
|
+
|
|
19
|
+
require "ruby-spacy"
|
|
20
|
+
require "fileutils"
|
|
21
|
+
|
|
22
|
+
# Requires the rsyntaxtree gem (>= 2.4.0): gem install rsyntaxtree
|
|
23
|
+
|
|
24
|
+
py_nlp = PyCall.import_module("spacy_stanza")
|
|
25
|
+
.load_pipeline("ar", processors: "tokenize,pos,lemma,depparse", verbose: false)
|
|
26
|
+
nlp = Spacy::Language.new(py_nlp: py_nlp)
|
|
27
|
+
doc = nlp.read("قرأ الطالب الجديد كتابا مثيرا في المكتبة أمس.")
|
|
28
|
+
|
|
29
|
+
puts doc.syntax_tree
|
|
30
|
+
|
|
31
|
+
# Right-to-left languages are drawn mirrored automatically (pass
|
|
32
|
+
# mirror: "off" to disable)
|
|
33
|
+
output_dir = File.join(File.dirname(__FILE__), "outputs")
|
|
34
|
+
FileUtils.mkdir_p(output_dir)
|
|
35
|
+
File.binwrite(File.join(output_dir, "tree_ar_projection.png"),
|
|
36
|
+
doc.syntax_tree(format: :png))
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# add path to ruby-spacy lib to load path
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
5
|
+
|
|
6
|
+
require "ruby-spacy"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
# Requires the rsyntaxtree gem (>= 2.4.0): gem install rsyntaxtree
|
|
10
|
+
|
|
11
|
+
nlp = Spacy::Language.new("de_core_news_sm")
|
|
12
|
+
doc = nlp.read("Der alte Professor las gestern ein interessantes Buch.")
|
|
13
|
+
|
|
14
|
+
puts doc.syntax_tree
|
|
15
|
+
|
|
16
|
+
output_dir = File.join(File.dirname(__FILE__), "outputs")
|
|
17
|
+
FileUtils.mkdir_p(output_dir)
|
|
18
|
+
File.binwrite(File.join(output_dir, "tree_de_projection.png"),
|
|
19
|
+
doc.syntax_tree(format: :png))
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# add path to ruby-spacy lib to load path
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
5
|
+
|
|
6
|
+
require "ruby-spacy"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
# Requires the rsyntaxtree gem (>= 2.4.0): gem install rsyntaxtree
|
|
10
|
+
|
|
11
|
+
nlp = Spacy::Language.new("en_core_web_sm")
|
|
12
|
+
doc = nlp.read("The quick brown fox jumped over the lazy dog near the river.")
|
|
13
|
+
|
|
14
|
+
puts doc.syntax_tree
|
|
15
|
+
|
|
16
|
+
output_dir = File.join(File.dirname(__FILE__), "outputs")
|
|
17
|
+
FileUtils.mkdir_p(output_dir)
|
|
18
|
+
File.binwrite(File.join(output_dir, "tree_en_projection.png"),
|
|
19
|
+
doc.syntax_tree(format: :png))
|
|
20
|
+
File.binwrite(File.join(output_dir, "tree_en_chunks.png"),
|
|
21
|
+
doc.syntax_tree(style: :chunks, format: :png))
|
|
22
|
+
# A shorter sentence for the morphology tables: every leaf becomes a box, so a
|
|
23
|
+
# long sentence draws a figure too wide to read
|
|
24
|
+
compact = nlp.read("He was sitting under the old tree.")
|
|
25
|
+
File.binwrite(File.join(output_dir, "tree_en_morphology.png"),
|
|
26
|
+
compact.syntax_tree(format: :png, morphology: true))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# add path to ruby-spacy lib to load path
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
5
|
+
|
|
6
|
+
require "ruby-spacy"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
# Requires the rsyntaxtree gem (>= 2.4.0): gem install rsyntaxtree
|
|
10
|
+
|
|
11
|
+
nlp = Spacy::Language.new("ja_core_news_sm")
|
|
12
|
+
doc = nlp.read("太郎は昨日、東京で花子に古い本を渡した。")
|
|
13
|
+
|
|
14
|
+
puts doc.syntax_tree
|
|
15
|
+
|
|
16
|
+
output_dir = File.join(File.dirname(__FILE__), "outputs")
|
|
17
|
+
FileUtils.mkdir_p(output_dir)
|
|
18
|
+
File.binwrite(File.join(output_dir, "tree_ja_projection.png"),
|
|
19
|
+
doc.syntax_tree(format: :png))
|
|
20
|
+
File.binwrite(File.join(output_dir, "tree_ja_chunks.png"),
|
|
21
|
+
doc.syntax_tree(style: :chunks, format: :png))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# add path to ruby-spacy lib to load path
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
5
|
+
|
|
6
|
+
require "ruby-spacy"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
# Requires the rsyntaxtree gem (>= 2.4.0): gem install rsyntaxtree
|
|
10
|
+
|
|
11
|
+
nlp = Spacy::Language.new("ru_core_news_sm")
|
|
12
|
+
doc = nlp.read("Старый профессор читал интересную книгу в библиотеке.")
|
|
13
|
+
|
|
14
|
+
puts doc.syntax_tree
|
|
15
|
+
|
|
16
|
+
# ru_core_news_sm has no noun chunk iterator, so style: :chunks raises
|
|
17
|
+
# ArgumentError for this language; Russian morphology is rich, though
|
|
18
|
+
output_dir = File.join(File.dirname(__FILE__), "outputs")
|
|
19
|
+
FileUtils.mkdir_p(output_dir)
|
|
20
|
+
File.binwrite(File.join(output_dir, "tree_ru_projection.png"),
|
|
21
|
+
doc.syntax_tree(format: :png))
|
|
22
|
+
File.binwrite(File.join(output_dir, "tree_ru_morphology.png"),
|
|
23
|
+
doc.syntax_tree(format: :png, morphology: true))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# add path to ruby-spacy lib to load path
|
|
4
|
+
$LOAD_PATH.unshift(File.expand_path("../../lib", __dir__))
|
|
5
|
+
|
|
6
|
+
require "ruby-spacy"
|
|
7
|
+
require "fileutils"
|
|
8
|
+
|
|
9
|
+
# Requires the rsyntaxtree gem (>= 2.4.0): gem install rsyntaxtree
|
|
10
|
+
|
|
11
|
+
nlp = Spacy::Language.new("zh_core_web_sm")
|
|
12
|
+
doc = nlp.read("老教授昨天在图书馆读了一本有趣的书。")
|
|
13
|
+
|
|
14
|
+
puts doc.syntax_tree
|
|
15
|
+
|
|
16
|
+
# zh_core_web_sm has no noun chunk iterator, so style: :chunks raises
|
|
17
|
+
# ArgumentError for this language
|
|
18
|
+
output_dir = File.join(File.dirname(__FILE__), "outputs")
|
|
19
|
+
FileUtils.mkdir_p(output_dir)
|
|
20
|
+
File.binwrite(File.join(output_dir, "tree_zh_projection.png"),
|
|
21
|
+
doc.syntax_tree(format: :png))
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spacy
|
|
4
|
+
# Converts spaCy parse results into rsyntaxtree bracket notation and renders
|
|
5
|
+
# them with the rsyntaxtree gem. This is an internal implementation module;
|
|
6
|
+
# the public API is {Doc#syntax_tree} and {Span#syntax_tree}.
|
|
7
|
+
#
|
|
8
|
+
# rsyntaxtree is a soft dependency: it is required (>= 2.4.0, for
|
|
9
|
+
# `RSyntaxTree.escape`) on the first call, not at load time.
|
|
10
|
+
module SyntaxTree
|
|
11
|
+
FORMATS = %i[bracket svg png pdf tikz json].freeze
|
|
12
|
+
STYLES = %i[projection chunks].freeze
|
|
13
|
+
|
|
14
|
+
# rsyntaxtree drawing defaults chosen for the wide, shallow trees produced
|
|
15
|
+
# here. User-supplied options take precedence (except hyphen, which the
|
|
16
|
+
# escaping depends on).
|
|
17
|
+
RENDER_DEFAULTS = {
|
|
18
|
+
hyphen: "literal",
|
|
19
|
+
polyline: "on",
|
|
20
|
+
tidy: "medium",
|
|
21
|
+
leafstyle: "nothing",
|
|
22
|
+
color: "modern"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Phrase label mapping from the head's POS tag
|
|
26
|
+
PHRASE_LABEL = {
|
|
27
|
+
"NOUN" => "NP", "PROPN" => "NP", "PRON" => "NP", "NUM" => "NP",
|
|
28
|
+
"VERB" => "VP", "AUX" => "VP", "ADP" => "PP", "ADJ" => "AdjP",
|
|
29
|
+
"ADV" => "AdvP", "DET" => "DP", "SCONJ" => "CP", "CCONJ" => "ConjP"
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
ENT_BACKGROUND = "orange"
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
# @param source [Doc, Span] the document or span to convert
|
|
36
|
+
# @return [String] the bracket notation (format: :bracket), the rendered
|
|
37
|
+
# output (SVG/TikZ/JSON text, or PNG/PDF binary), depending on format
|
|
38
|
+
def generate(source, format: :bracket, style: :projection, morphology: false,
|
|
39
|
+
entities: true, punctuation: false, **render_opts)
|
|
40
|
+
ensure_rsyntaxtree!
|
|
41
|
+
format = format.to_sym
|
|
42
|
+
style = style.to_sym
|
|
43
|
+
validate_options!(format, style, render_opts)
|
|
44
|
+
|
|
45
|
+
bracket = bracket_for(source, style: style, morphology: morphology,
|
|
46
|
+
entities: entities, punctuation: punctuation)
|
|
47
|
+
return bracket if format == :bracket
|
|
48
|
+
|
|
49
|
+
# Right-to-left scripts are drawn mirrored (leaves run right to left)
|
|
50
|
+
# unless the caller says otherwise. The notation itself is unaffected
|
|
51
|
+
render_opts = { mirror: "on" }.merge(render_opts) if rtl?(source)
|
|
52
|
+
render(bracket, format, render_opts)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# True when the source's language writes right-to-left. Asked from the
|
|
58
|
+
# pipeline itself (`Defaults.writing_system`) rather than a hardcoded
|
|
59
|
+
# language list, so external pipelines (spacy-stanza, spacy-udpipe) work
|
|
60
|
+
# too. Any failure (e.g. no `Defaults`) falls back to left-to-right;
|
|
61
|
+
# note this also swallows a genuine detection failure, so if an RTL
|
|
62
|
+
# tree ever renders unmirrored, this fallback is the first place to check
|
|
63
|
+
def rtl?(source)
|
|
64
|
+
py_nlp = source.is_a?(Spacy::Span) ? source.doc.py_nlp : source.py_nlp
|
|
65
|
+
direction = Spacy::Builtins.getattr(py_nlp.Defaults, "writing_system")["direction"]
|
|
66
|
+
direction.to_s == "rtl"
|
|
67
|
+
rescue StandardError
|
|
68
|
+
false
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ensure_rsyntaxtree!
|
|
72
|
+
return if @loaded
|
|
73
|
+
|
|
74
|
+
begin
|
|
75
|
+
require "rsyntaxtree"
|
|
76
|
+
rescue LoadError
|
|
77
|
+
raise LoadError, "syntax_tree requires the rsyntaxtree gem (>= 2.4.0): gem install rsyntaxtree"
|
|
78
|
+
end
|
|
79
|
+
unless RSyntaxTree.respond_to?(:escape)
|
|
80
|
+
raise LoadError, "syntax_tree requires rsyntaxtree >= 2.4.0 (found #{RSyntaxTree::VERSION})"
|
|
81
|
+
end
|
|
82
|
+
@loaded = true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate_options!(format, style, render_opts)
|
|
86
|
+
unless FORMATS.include?(format)
|
|
87
|
+
raise ArgumentError, "unknown format: #{format.inspect} (expected one of: #{FORMATS.join(', ')})"
|
|
88
|
+
end
|
|
89
|
+
unless STYLES.include?(style)
|
|
90
|
+
raise ArgumentError, "unknown style: #{style.inspect} (expected one of: #{STYLES.join(', ')})"
|
|
91
|
+
end
|
|
92
|
+
if render_opts.key?(:hyphen)
|
|
93
|
+
raise ArgumentError, "hyphen: cannot be overridden (the notation is escaped for hyphen: :literal)"
|
|
94
|
+
end
|
|
95
|
+
unknown = render_opts.keys.map(&:to_sym) - ::DEFAULT_OPTS.keys
|
|
96
|
+
unless unknown.empty?
|
|
97
|
+
raise ArgumentError,
|
|
98
|
+
"unknown rsyntaxtree option(s): #{unknown.join(', ')} (valid: #{::DEFAULT_OPTS.keys.join(', ')})"
|
|
99
|
+
end
|
|
100
|
+
if format == :bracket && !render_opts.empty?
|
|
101
|
+
raise ArgumentError,
|
|
102
|
+
"drawing options (#{render_opts.keys.join(', ')}) have no effect with format: :bracket"
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def render(bracket, format, render_opts)
|
|
107
|
+
params = RENDER_DEFAULTS.merge(render_opts).merge(data: bracket)
|
|
108
|
+
gen = RSyntaxTree::RSGenerator.new(params)
|
|
109
|
+
case format
|
|
110
|
+
when :svg then gen.draw_svg
|
|
111
|
+
when :json then gen.draw_json
|
|
112
|
+
when :tikz then gen.draw_tikz
|
|
113
|
+
when :png then gen.draw_png.force_encoding(Encoding::BINARY)
|
|
114
|
+
when :pdf then gen.draw_pdf.force_encoding(Encoding::BINARY)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Escapes a string for the bracket notation. `as:` follows
|
|
119
|
+
# `RSyntaxTree.escape` (:word for leaf words, :label for entity labels,
|
|
120
|
+
# :cell for AVM cells).
|
|
121
|
+
def escape(text, as:, context: nil)
|
|
122
|
+
RSyntaxTree.escape(text, as: as, hyphen: :literal, apostrophe: :keep)
|
|
123
|
+
rescue ArgumentError => e
|
|
124
|
+
where = context ? " (#{context})" : ""
|
|
125
|
+
raise ArgumentError, "syntax_tree: cannot escape#{where}: #{e.message}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def bracket_for(source, style:, morphology:, entities:, punctuation:)
|
|
129
|
+
tokens, root, root_label = tree_scope(source)
|
|
130
|
+
chunks = chunk_spans(source)
|
|
131
|
+
if chunks.nil? && style == :chunks
|
|
132
|
+
raise ArgumentError,
|
|
133
|
+
"noun chunks are not available for this language/model, so style: :chunks cannot be used"
|
|
134
|
+
end
|
|
135
|
+
chunks ||= []
|
|
136
|
+
ents = entities ? ent_map(source) : {}
|
|
137
|
+
|
|
138
|
+
case style
|
|
139
|
+
when :chunks
|
|
140
|
+
chunks_bracket(tokens, chunks, ents, morphology: morphology, punctuation: punctuation)
|
|
141
|
+
else
|
|
142
|
+
projection_bracket(root, chunks, ents, root_label: root_label,
|
|
143
|
+
morphology: morphology, punctuation: punctuation)
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Returns [tokens, root_token, root_label]. All positions are doc-based
|
|
148
|
+
# (Span#tokens / Token#i are doc-based, which matches chunk and entity
|
|
149
|
+
# offsets).
|
|
150
|
+
def tree_scope(source)
|
|
151
|
+
case source
|
|
152
|
+
when Spacy::Doc
|
|
153
|
+
py_doc = source.py_doc
|
|
154
|
+
unless py_doc.has_annotation("DEP")
|
|
155
|
+
raise ArgumentError, "syntax_tree requires a dependency parse (the pipeline has no parser)"
|
|
156
|
+
end
|
|
157
|
+
if py_doc.has_annotation("SENT_START") && source.sents.size > 1
|
|
158
|
+
raise ArgumentError,
|
|
159
|
+
"syntax_tree requires a single sentence; " \
|
|
160
|
+
"use doc.sents.map { |s| s.syntax_tree } for a multi-sentence doc"
|
|
161
|
+
end
|
|
162
|
+
tokens = source.tokens
|
|
163
|
+
# The root is the token that is its own head. Comparing dep strings
|
|
164
|
+
# would tie this to an annotation scheme (spaCy's trained pipelines
|
|
165
|
+
# use "ROOT" while UD-style pipelines such as Stanza use "root")
|
|
166
|
+
root = tokens.find { |t| t.head.i == t.i }
|
|
167
|
+
raise ArgumentError, "syntax_tree: no root token found (no dependency parse)" unless root
|
|
168
|
+
|
|
169
|
+
[tokens, root, "S"]
|
|
170
|
+
when Spacy::Span
|
|
171
|
+
unless source.py_span.doc.has_annotation("DEP")
|
|
172
|
+
raise ArgumentError, "syntax_tree requires a dependency parse (the pipeline has no parser)"
|
|
173
|
+
end
|
|
174
|
+
tokens = source.tokens
|
|
175
|
+
raise ArgumentError, "syntax_tree: empty span" if tokens.empty?
|
|
176
|
+
|
|
177
|
+
first_i = tokens.first.i
|
|
178
|
+
last_i = tokens.last.i
|
|
179
|
+
roots = tokens.select { |t| t.head.i == t.i || t.head.i < first_i || t.head.i > last_i }
|
|
180
|
+
unless roots.size == 1
|
|
181
|
+
raise ArgumentError,
|
|
182
|
+
"syntax_tree requires a span with a single root (e.g. a sentence from doc.sents)"
|
|
183
|
+
end
|
|
184
|
+
root = roots.first
|
|
185
|
+
unless root.left_edge.i == first_i && root.right_edge.i == last_i
|
|
186
|
+
raise ArgumentError,
|
|
187
|
+
"syntax_tree requires a span that is a complete subtree " \
|
|
188
|
+
"(e.g. a sentence from doc.sents or a noun chunk)"
|
|
189
|
+
end
|
|
190
|
+
is_sentence = source.py_span.sent.start == source.py_span.start &&
|
|
191
|
+
source.py_span.sent.end == source.py_span.end
|
|
192
|
+
[tokens, root, is_sentence ? "S" : PHRASE_LABEL.fetch(root.pos, "XP")]
|
|
193
|
+
else
|
|
194
|
+
raise ArgumentError, "syntax_tree expects a Spacy::Doc or Spacy::Span"
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Noun chunk spans as doc-based [start, end) pairs, or nil when the
|
|
199
|
+
# language/model has no noun chunk iterator (spaCy error E894).
|
|
200
|
+
def chunk_spans(source)
|
|
201
|
+
source.noun_chunks.map { |c| [c.py_span.start, c.py_span.end] }
|
|
202
|
+
rescue PyCall::PyError => e
|
|
203
|
+
raise unless e.message.include?("E894")
|
|
204
|
+
|
|
205
|
+
nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Entity spans as a doc-based [start, end) => label map
|
|
209
|
+
def ent_map(source)
|
|
210
|
+
source.ents.to_h { |e| [[e.py_span.start, e.py_span.end], e.label] }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def leaf(token, morphology:)
|
|
214
|
+
word = escape(token.text, as: :word, context: "token #{token.text.inspect}")
|
|
215
|
+
return "[#{token.pos} #{word}]" unless morphology
|
|
216
|
+
|
|
217
|
+
rows = ["pos\\t#{escape(token.pos, as: :cell)}"]
|
|
218
|
+
morph = token.morphology(hash: false)
|
|
219
|
+
unless morph.empty?
|
|
220
|
+
morph.split("|").each do |kv|
|
|
221
|
+
k, v = kv.split("=", 2)
|
|
222
|
+
rows << "#{escape(k, as: :cell)}\\t#{escape(v, as: :cell)}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
"[#(#{rows.join('\n')}#) #{word}]"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# "%NP" for a bare chunk; "%@orange:NP\nLABEL" for one matching an entity
|
|
229
|
+
def chunk_label(base, ent_label)
|
|
230
|
+
return "%#{base}" unless ent_label
|
|
231
|
+
|
|
232
|
+
"%@#{ENT_BACKGROUND}:#{base}\\n#{escape(ent_label, as: :label, context: 'entity label')}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# A shallow tree: [S ...] with chunks as [%NP [POS w] ...] and other
|
|
236
|
+
# tokens as plain leaves. Chunks matching an entity get a colored
|
|
237
|
+
# background and a second label line.
|
|
238
|
+
def chunks_bracket(tokens, chunks, ents, morphology:, punctuation:)
|
|
239
|
+
i = 0
|
|
240
|
+
parts = []
|
|
241
|
+
while i < tokens.size
|
|
242
|
+
token = tokens[i]
|
|
243
|
+
chunk = chunks.find { |s, _e| s == token.i }
|
|
244
|
+
if chunk
|
|
245
|
+
s, e = chunk
|
|
246
|
+
chunk_tokens = tokens.select { |t| t.i >= s && t.i < e }
|
|
247
|
+
label = chunk_label("NP", ents[[s, e]])
|
|
248
|
+
parts << "[#{label} #{chunk_tokens.map { |t| leaf(t, morphology: morphology) }.join(' ')}]"
|
|
249
|
+
i += chunk_tokens.size
|
|
250
|
+
else
|
|
251
|
+
parts << leaf(token, morphology: morphology) unless token.pos == "PUNCT" && !punctuation
|
|
252
|
+
i += 1
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
"[S #{parts.join(' ')}]"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Head projection: each head projects a phrase node over its dependents
|
|
259
|
+
# and itself in word order. A phrase whose span coincides with a noun
|
|
260
|
+
# chunk gets a background; when the chunk containing the head is
|
|
261
|
+
# narrower than the projection (NP -> NP PP), the chunk's tokens are
|
|
262
|
+
# wrapped in an inner [%NP ...]. A single-token chunk (a frequent case
|
|
263
|
+
# for named entities) is wrapped as [%NP leaf] so that the background
|
|
264
|
+
# and entity label are not lost.
|
|
265
|
+
def projection_bracket(token, chunks, ents, root_label:, morphology:, punctuation:)
|
|
266
|
+
kids = token.children.to_a
|
|
267
|
+
kids = kids.reject { |k| k.pos == "PUNCT" } unless punctuation
|
|
268
|
+
if kids.empty?
|
|
269
|
+
node = leaf(token, morphology: morphology)
|
|
270
|
+
single = [token.i, token.i + 1]
|
|
271
|
+
return node unless chunks.include?(single)
|
|
272
|
+
|
|
273
|
+
return "[#{chunk_label('NP', ents[single])} #{node}]"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
label = root_label || PHRASE_LABEL.fetch(token.pos, "XP")
|
|
277
|
+
span = [token.left_edge.i, token.right_edge.i + 1]
|
|
278
|
+
label = chunk_label(label, ents[span]) if chunks.include?(span)
|
|
279
|
+
|
|
280
|
+
ordered = (kids + [token]).sort_by(&:i)
|
|
281
|
+
render_node = lambda do |t|
|
|
282
|
+
if t.i == token.i
|
|
283
|
+
leaf(t, morphology: morphology)
|
|
284
|
+
else
|
|
285
|
+
projection_bracket(t, chunks, ents, root_label: nil,
|
|
286
|
+
morphology: morphology, punctuation: punctuation)
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
inner = chunks.find { |s, e| (s...e).cover?(token.i) && [s, e] != span }
|
|
291
|
+
if inner
|
|
292
|
+
s, e = inner
|
|
293
|
+
inside, = ordered.partition { |t| (s...e).cover?(t.i) }
|
|
294
|
+
inner_node = "[#{chunk_label('NP', ents[inner])} #{inside.map(&render_node).join(' ')}]"
|
|
295
|
+
parts = ordered.map { |t| inside.include?(t) ? (t.equal?(inside.first) ? inner_node : nil) : render_node.call(t) }
|
|
296
|
+
parts = parts.compact
|
|
297
|
+
else
|
|
298
|
+
parts = ordered.map(&render_node)
|
|
299
|
+
end
|
|
300
|
+
"[#{label} #{parts.join(' ')}]"
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
end
|
data/lib/ruby-spacy/version.rb
CHANGED
data/lib/ruby-spacy.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "ruby-spacy/openai_client"
|
|
|
6
6
|
require_relative "ruby-spacy/anthropic_client"
|
|
7
7
|
require_relative "ruby-spacy/openai_helper"
|
|
8
8
|
require_relative "ruby-spacy/anthropic_helper"
|
|
9
|
+
require_relative "ruby-spacy/syntax_tree"
|
|
9
10
|
require "pycall"
|
|
10
11
|
require "json"
|
|
11
12
|
require "base64"
|
|
@@ -344,6 +345,40 @@ module Spacy
|
|
|
344
345
|
PyDisplacy.render(py_doc, style: style, options: { compact: compact }, jupyter: false)
|
|
345
346
|
end
|
|
346
347
|
|
|
348
|
+
# Generates a syntax tree in rsyntaxtree bracket notation, or renders it
|
|
349
|
+
# with the rsyntaxtree gem.
|
|
350
|
+
#
|
|
351
|
+
# Requires the rsyntaxtree gem (>= 2.4.0) at call time (it is a soft
|
|
352
|
+
# dependency; install it with `gem install rsyntaxtree`). The doc must
|
|
353
|
+
# contain a single sentence; for a multi-sentence doc, use
|
|
354
|
+
# `doc.sents.map { |s| s.syntax_tree }`.
|
|
355
|
+
#
|
|
356
|
+
# Note that the bracket notation is rsyntaxtree-flavored: it may contain
|
|
357
|
+
# rsyntaxtree-specific markup such as AVMs (`#(...#)`), region backgrounds
|
|
358
|
+
# (`%`), and in-word space joins (`<>`).
|
|
359
|
+
#
|
|
360
|
+
# @param format [Symbol] `:bracket` (default; the notation string),
|
|
361
|
+
# `:svg`, `:png`, `:pdf`, `:tikz`, or `:json`. `:png` and `:pdf` return
|
|
362
|
+
# a binary string
|
|
363
|
+
# @param style [Symbol] `:projection` (default; head-projection
|
|
364
|
+
# phrase-structure-like tree) or `:chunks` (shallow tree with noun chunks)
|
|
365
|
+
# @param morphology [Boolean] attach a morphology AVM to each leaf
|
|
366
|
+
# @param entities [Boolean] mark chunks that match a named entity with a
|
|
367
|
+
# colored background and the entity label
|
|
368
|
+
# @param punctuation [Boolean] keep punctuation tokens
|
|
369
|
+
# @param render_opts [Hash] rsyntaxtree drawing options (e.g. `fontsize:`).
|
|
370
|
+
# `hyphen:` cannot be overridden
|
|
371
|
+
# @return [String] the bracket notation or the rendered output
|
|
372
|
+
# @example
|
|
373
|
+
# doc.syntax_tree # => "[S [%NP [DET The] ...] ...]"
|
|
374
|
+
# doc.syntax_tree(format: :svg) # => "<svg ..."
|
|
375
|
+
# doc.syntax_tree(format: :png, fontsize: 12)
|
|
376
|
+
def syntax_tree(format: :bracket, style: :projection, morphology: false,
|
|
377
|
+
entities: true, punctuation: false, **render_opts)
|
|
378
|
+
SyntaxTree.generate(self, format: format, style: style, morphology: morphology,
|
|
379
|
+
entities: entities, punctuation: punctuation, **render_opts)
|
|
380
|
+
end
|
|
381
|
+
|
|
347
382
|
# Generates a JSON string summarizing the linguistic analysis of the document.
|
|
348
383
|
# Designed to be passed as context to an LLM (e.g., via {OpenAIHelper#chat}).
|
|
349
384
|
#
|
|
@@ -614,6 +649,12 @@ module Spacy
|
|
|
614
649
|
end
|
|
615
650
|
end
|
|
616
651
|
|
|
652
|
+
# Sentinel for "the caller gave no model argument". Distinct from nil so
|
|
653
|
+
# that an explicit nil (e.g. an unset ENV var passed straight through)
|
|
654
|
+
# still fails validation instead of silently loading the default model
|
|
655
|
+
NO_MODEL = Object.new.freeze
|
|
656
|
+
private_constant :NO_MODEL
|
|
657
|
+
|
|
617
658
|
# Creates a language model instance, which is conventionally referred to by a variable named `nlp`.
|
|
618
659
|
# @param model [String] A language model installed in the system
|
|
619
660
|
# @param timeout [Numeric, nil] Seconds to wait for the model to load before
|
|
@@ -622,7 +663,31 @@ module Spacy
|
|
|
622
663
|
# because Ruby's `Timeout` cannot fire while PyCall holds the GVL. When it
|
|
623
664
|
# fires, the loading thread is left running as a daemon until the process
|
|
624
665
|
# exits (accepted: timeouts are an abnormal path).
|
|
625
|
-
|
|
666
|
+
# @param py_nlp [Object, nil] an existing Python `Language` pipeline to wrap
|
|
667
|
+
# instead of loading a model. For languages spaCy ships no trained
|
|
668
|
+
# pipeline for (e.g. Arabic and other right-to-left languages) or for
|
|
669
|
+
# self-built pipelines, create one with a third-party package such as
|
|
670
|
+
# spacy-stanza or spacy-udpipe and pass it here. Mutually exclusive with
|
|
671
|
+
# `model`; model name validation, timeout, and retries are skipped
|
|
672
|
+
# @example Load an installed spaCy model
|
|
673
|
+
# nlp = Spacy::Language.new("en_core_web_sm")
|
|
674
|
+
# @example Wrap an external pipeline (requires: pip install spacy-stanza)
|
|
675
|
+
# py_nlp = PyCall.import_module("spacy_stanza").load_pipeline("ar")
|
|
676
|
+
# nlp = Spacy::Language.new(py_nlp: py_nlp)
|
|
677
|
+
def initialize(model = NO_MODEL, max_retrial: MAX_RETRIAL, timeout: 60, py_nlp: nil)
|
|
678
|
+
if py_nlp
|
|
679
|
+
raise ArgumentError, "model and py_nlp: are mutually exclusive" unless model.equal?(NO_MODEL)
|
|
680
|
+
unless Builtins.isinstance(py_nlp, PyLanguage)
|
|
681
|
+
raise ArgumentError,
|
|
682
|
+
"py_nlp: must be a spaCy Language pipeline " \
|
|
683
|
+
"(e.g. from spacy.load or spacy_stanza.load_pipeline)"
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
@py_nlp = py_nlp
|
|
687
|
+
return
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
model = "en_core_web_sm" if model.equal?(NO_MODEL)
|
|
626
691
|
unless model.to_s.match?(/\A[a-zA-Z0-9_\-\.\/]+\z/)
|
|
627
692
|
raise ArgumentError, "Invalid model name: #{model.inspect}"
|
|
628
693
|
end
|
|
@@ -1003,6 +1068,15 @@ module Spacy
|
|
|
1003
1068
|
Doc.new(@doc.py_nlp, text: text)
|
|
1004
1069
|
end
|
|
1005
1070
|
|
|
1071
|
+
# Generates a syntax tree for the span in rsyntaxtree bracket notation, or
|
|
1072
|
+
# renders it with the rsyntaxtree gem. The span must have a single root
|
|
1073
|
+
# (e.g. a sentence from {Doc#sents}). See {Doc#syntax_tree} for the
|
|
1074
|
+
# available options.
|
|
1075
|
+
# @return [String] the bracket notation or the rendered output
|
|
1076
|
+
def syntax_tree(**options)
|
|
1077
|
+
SyntaxTree.generate(self, **options)
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1006
1080
|
# Returns tokens conjugated to the root of the span.
|
|
1007
1081
|
# @return [Array<Token>] an array of tokens
|
|
1008
1082
|
def conjuncts
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby-spacy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yoichiro Hasebe
|
|
@@ -169,6 +169,7 @@ files:
|
|
|
169
169
|
- Rakefile
|
|
170
170
|
- bin/console
|
|
171
171
|
- bin/setup
|
|
172
|
+
- docs/syntax_trees.md
|
|
172
173
|
- examples/get_started/lexeme.rb
|
|
173
174
|
- examples/get_started/linguistic_annotations.rb
|
|
174
175
|
- examples/get_started/morphology.rb
|
|
@@ -230,6 +231,22 @@ files:
|
|
|
230
231
|
- examples/openai_integration/openai_query_2.rb
|
|
231
232
|
- examples/openai_integration/openai_query_3.rb
|
|
232
233
|
- examples/openai_integration/openai_query_4.rb
|
|
234
|
+
- examples/rsyntaxtree/outputs/tree_ar_projection.png
|
|
235
|
+
- examples/rsyntaxtree/outputs/tree_de_projection.png
|
|
236
|
+
- examples/rsyntaxtree/outputs/tree_en_chunks.png
|
|
237
|
+
- examples/rsyntaxtree/outputs/tree_en_morphology.png
|
|
238
|
+
- examples/rsyntaxtree/outputs/tree_en_projection.png
|
|
239
|
+
- examples/rsyntaxtree/outputs/tree_ja_chunks.png
|
|
240
|
+
- examples/rsyntaxtree/outputs/tree_ja_projection.png
|
|
241
|
+
- examples/rsyntaxtree/outputs/tree_ru_morphology.png
|
|
242
|
+
- examples/rsyntaxtree/outputs/tree_ru_projection.png
|
|
243
|
+
- examples/rsyntaxtree/outputs/tree_zh_projection.png
|
|
244
|
+
- examples/rsyntaxtree/syntax_tree_ar.rb
|
|
245
|
+
- examples/rsyntaxtree/syntax_tree_de.rb
|
|
246
|
+
- examples/rsyntaxtree/syntax_tree_en.rb
|
|
247
|
+
- examples/rsyntaxtree/syntax_tree_ja.rb
|
|
248
|
+
- examples/rsyntaxtree/syntax_tree_ru.rb
|
|
249
|
+
- examples/rsyntaxtree/syntax_tree_zh.rb
|
|
233
250
|
- examples/rule_based_matching/creating_spans_from_matches.rb
|
|
234
251
|
- examples/rule_based_matching/matcher.rb
|
|
235
252
|
- lib/ruby-spacy.rb
|
|
@@ -238,6 +255,7 @@ files:
|
|
|
238
255
|
- lib/ruby-spacy/llm_client_base.rb
|
|
239
256
|
- lib/ruby-spacy/openai_client.rb
|
|
240
257
|
- lib/ruby-spacy/openai_helper.rb
|
|
258
|
+
- lib/ruby-spacy/syntax_tree.rb
|
|
241
259
|
- lib/ruby-spacy/version.rb
|
|
242
260
|
- ruby-spacy.gemspec
|
|
243
261
|
homepage: https://github.com/yohasebe/ruby-spacy
|