Browse Source

Implement demo for named entity recognition

pull/10/head
Hironsan 6 years ago
parent
commit
ad0f3b1944
7 changed files with 433 additions and 3 deletions
  1. 180
      app/server/static/bundle/demo_named_entity.js
  2. 214
      app/server/static/js/demo/demo_named_entity.js
  3. 2
      app/server/templates/base.html
  4. 30
      app/server/templates/demo/demo_named_entity.html
  5. 3
      app/server/urls.py
  6. 4
      app/server/views.py
  7. 3
      app/server/webpack.config.js

180
app/server/static/bundle/demo_named_entity.js
File diff suppressed because it is too large
View File

214
app/server/static/js/demo/demo_named_entity.js

@ -0,0 +1,214 @@
import Vue from 'vue';
import annotationMixin from './demo_mixin';
Vue.use(require('vue-shortkey'), {
prevent: ['input', 'textarea'],
});
Vue.component('annotator', {
template: '<div @click="setSelectedRange">\
<span v-for="r in chunks"\
v-bind:class="{tag: id2label[r.label].text_color}"\
v-bind:style="{ color: id2label[r.label].text_color, backgroundColor: id2label[r.label].background_color }"\
>{{ text.slice(r.start_offset, r.end_offset) }}<button class="delete is-small"\
v-if="id2label[r.label].text_color"\
@click="removeLabel(r)"></button></span>\
</div>',
props: {
labels: Array, // [{id: Integer, color: String, text: String}]
text: String,
entityPositions: Array, // [{'startOffset': 10, 'endOffset': 15, 'label_id': 1}]
},
data() {
return {
startOffset: 0,
endOffset: 0,
};
},
methods: {
setSelectedRange(e) {
let start;
let end;
if (window.getSelection) {
const range = window.getSelection().getRangeAt(0);
const preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(this.$el);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
start = preSelectionRange.toString().length;
end = start + range.toString().length;
} else if (document.selection && document.selection.type !== 'Control') {
const selectedTextRange = document.selection.createRange();
const preSelectionTextRange = document.body.createTextRange();
preSelectionTextRange.moveToElementText(this.$el);
preSelectionTextRange.setEndPoint('EndToStart', selectedTextRange);
start = preSelectionTextRange.text.length;
end = start + selectedTextRange.text.length;
}
this.startOffset = start;
this.endOffset = end;
console.log(start, end);
},
validRange() {
if (this.startOffset === this.endOffset) {
return false;
}
if (this.startOffset > this.text.length || this.endOffset > this.text.length) {
return false;
}
if (this.startOffset < 0 || this.endOffset < 0) {
return false;
}
return true;
},
resetRange() {
this.startOffset = 0;
this.endOffset = 0;
},
addLabel(labelId) {
if (this.validRange()) {
const label = {
start_offset: this.startOffset,
end_offset: this.endOffset,
label: labelId,
};
this.$emit('add-label', label);
}
},
removeLabel(index) {
this.$emit('remove-label', index);
},
makeLabel(startOffset, endOffset) {
const label = {
id: 0,
label: -1,
start_offset: startOffset,
end_offset: endOffset,
};
return label;
},
},
watch: {
entityPositions() {
this.resetRange();
},
},
computed: {
sortedEntityPositions() {
this.entityPositions = this.entityPositions.sort((a, b) => a.start_offset - b.start_offset);
return this.entityPositions;
},
chunks() {
const res = [];
let left = 0;
for (let i = 0; i < this.sortedEntityPositions.length; i++) {
const e = this.sortedEntityPositions[i];
const l = this.makeLabel(left, e.start_offset);
res.push(l);
res.push(e);
left = e.end_offset;
}
const l = this.makeLabel(left, this.text.length);
res.push(l);
return res;
},
id2label() {
let id2label = {};
// default value;
id2label[-1] = {
text_color: '',
background_color: '',
};
for (let i = 0; i < this.labels.length; i++) {
const label = this.labels[i];
id2label[label.id] = label;
}
return id2label;
},
},
});
const vm = new Vue({
el: '#mail-app',
delimiters: ['[[', ']]'],
mixins: [annotationMixin],
data: {
docs: [{
id: 1,
text: 'This is a document for named entity recognition.',
},
{
id: 10,
text: 'This is a sentence.',
},
{
id: 11,
text: 'This is a sentence.',
},
{
id: 12,
text: 'This is a sentence.',
},
{
id: 13,
text: 'This is a sentence.',
},
{
id: 13,
text: 'This is a sentence.',
},
],
labels: [
{
id: 1,
text: 'Negative',
shortcut: 'n',
background_color: '#ff0033',
text_color: '#ffffff',
},
{
id: 2,
text: 'Positive',
shortcut: 'p',
background_color: '#209cee',
text_color: '#ffffff',
},
],
annotations: [
[
{
id: 1,
prob: 0.0,
label: 1,
start_offset: 5,
end_offset: 10,
},
],
[],
[],
[],
[],
[],
],
},
methods: {
annotate(labelId) {
this.$refs.annotator.addLabel(labelId);
},
addLabel(annotation) {
this.annotations[this.pageNumber].push(annotation);
},
},
});

2
app/server/templates/base.html

@ -58,7 +58,7 @@
<a href="{% url 'demo-text-classification' %}" class="navbar-item">
Text Classification
</a>
<a class="navbar-item">
<a href="{% url 'demo-named-entity-recognition' %}" class="navbar-item">
Named Entity Recognition
</a>
<a class="navbar-item">

30
app/server/templates/demo/demo_named_entity.html

@ -0,0 +1,30 @@
{% extends "annotation/annotation_base.html" %}
{% load static %}
{% block annotation-area %}
<div class="card">
<header class="card-header">
<div class="card-header-title" style="padding:1.5rem;background-color:royalblue;">
<div class="field is-grouped is-grouped-multiline">
<div class="control" v-for="label in labels">
<div class="tags has-addons">
<a class="tag is-medium" v-bind:style="{ color: label.text_color, backgroundColor: label.background_color }" v-on:click="annotate(label.id)"
v-shortkey.once="[ label.shortcut ]" @shortkey="annotate(label.id)">
[[ label.text ]]
</a>
<span class="tag is-medium">[[ label.shortcut ]]</span>
</div>
</div>
</div>
</div>
</header>
<div class="card-content">
<div class="content" v-if="docs[pageNumber] && annotations[pageNumber]">
<annotator ref="annotator" v-bind:labels="labels" v-bind:entity-positions="annotations[pageNumber]" v-bind:text="docs[pageNumber].text"
@remove-label="removeLabel" @add-label="addLabel"></annotator>
</div>
</div>
</div>
{% endblock %}
{% block footer %}
<script src="{% static 'bundle/demo_named_entity.js' %}"></script>
{% endblock %}

3
app/server/urls.py

@ -4,7 +4,7 @@ from rest_framework import routers
from .views import IndexView
from .views import ProjectView, DatasetView, DataUpload, LabelView, StatsView
from .views import ProjectsView, DataDownload
from .views import DemoTextClassification
from .views import DemoTextClassification, DemoNamedEntityRecognition
from .api import ProjectViewSet, LabelList, ProjectStatsAPI, LabelDetail, \
AnnotationList, AnnotationDetail, DocumentList
@ -28,4 +28,5 @@ urlpatterns = [
path('projects/<int:project_id>/labels/', LabelView.as_view(), name='label-management'),
path('projects/<int:project_id>/stats/', StatsView.as_view(), name='stats'),
path('demo/text-classification/', DemoTextClassification.as_view(), name='demo-text-classification'),
path('demo/named-entity-recognition/', DemoNamedEntityRecognition.as_view(), name='demo-named-entity-recognition'),
]

4
app/server/views.py

@ -81,3 +81,7 @@ class DataDownload(SuperUserMixin, LoginRequiredMixin, View):
class DemoTextClassification(TemplateView):
template_name = 'demo/demo_text_classification.html'
class DemoNamedEntityRecognition(TemplateView):
template_name = 'demo/demo_named_entity.html'

3
app/server/webpack.config.js

@ -9,7 +9,8 @@ module.exports = {
'projects': './static/js/projects.js',
'stats': './static/js/stats.js',
'label': './static/js/label.js',
'demo_text_classification': './static/js/demo/demo_text_classification.js'
'demo_text_classification': './static/js/demo/demo_text_classification.js',
'demo_named_entity': './static/js/demo/demo_named_entity.js'
},
output: {
path: __dirname + '/static/bundle',

Loading…
Cancel
Save