Skip to content

Commit

Permalink
Onboarding highlight (#196)
Browse files Browse the repository at this point in the history
* Pretty examples.

* Welcome screen mobile fix.

* Onboarding highlighting and other fixes.

* Fix.

* Fix.

* New samples.

* fix user session

* revert kittens

* rewrite hostname on start
  • Loading branch information
makseq authored and niklub committed Jan 21, 2020
1 parent c34b975 commit dba7111
Show file tree
Hide file tree
Showing 23 changed files with 200 additions and 68 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ There is a quick example tutorial on how to do that with simple image classifica

Once you're satisfied with pre-labeling results, you can immediately send prediction requests via REST API:
```bash
curl -X POST -H 'Content-Type: application/json' -d '{"image_url": "https://go.heartex.net/static/samples/kittens.jpg"}' http://localhost:8200/predict
curl -X POST -H 'Content-Type: application/json' -d '{"image_url": "https://go.heartex.net/static/samples/sample.jpg"}' http://localhost:8200/predict
```
Feel free to play around any other models & frameworks apart from image classifiers! (see instructions [here](https://github.com/heartexlabs/pyheartex#advanced-usage))
Expand Down
2 changes: 1 addition & 1 deletion docs/source/guide/ml.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Here is a quick example tutorial on how to do that with simple image classificat

Once you're satisfied with pre-labeling results, you can immediately send prediction requests via REST API:
```bash
curl -X POST -H 'Content-Type: application/json' -d '{"image_url": "https://go.heartex.net/static/samples/kittens.jpg"}' http://localhost:8200/predict
curl -X POST -H 'Content-Type: application/json' -d '{"image_url": "https://go.heartex.net/static/samples/sample.jpg"}' http://localhost:8200/predict
```
Feel free to play around any other models & frameworks apart from image classifiers! (see instructions [here](https://github.com/heartexlabs/pyheartex#advanced-usage))
Expand Down
2 changes: 1 addition & 1 deletion label_studio/examples/dialogue_analysis/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
<Choice value="Good answer"/>
</Choices>

<Header value="Your answer"/>
<Header value="Write your answer and press Enter"/>
<TextArea name="answer"/>
</View>
4 changes: 2 additions & 2 deletions label_studio/examples/image_bbox/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<View>
<Image name="img" value="$image"/>
<RectangleLabels name="tag" toName="img">
<Label value="Planet" background="green"/>
<Label value="Moonwalker" background="blue"/>
<Label value="Airplane" background="green"/>
<Label value="Car" background="blue"/>
</RectangleLabels>
</View>
4 changes: 2 additions & 2 deletions label_studio/examples/image_classification/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<View>
<Image name="img" value="$image_url"/>
<Choices name="choice" toName="img" showInLine="true">
<Choice value="Cat" background="blue"/>
<Choice value="Dog" background="green" />
<Choice value="Boeing" background="blue"/>
<Choice value="Airbus" background="green" />
</Choices>
</View>
4 changes: 2 additions & 2 deletions label_studio/examples/image_keypoints/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<Header>Select label then click on image</Header>
<KeyPointLabels name="tag" toName="img"
strokewidth="5" fillcolor="red">
<Label value="Eye" background="lightgreen"/>
<Label value="Nose" background="lightblue"/>
<Label value="Engine" background="red"/>
<Label value="Tail" background="rgba(0, 255, 0, 0.9)"/>
</KeyPointLabels>
</View>
4 changes: 2 additions & 2 deletions label_studio/examples/image_mixedlabel/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

<RectangleLabels name="tag" toName="img"
canRotate="false">
<Label value="Planet" background="red"/>
<Label value="Moonwalker" background="blue"/>
<Label value="Airplane" background="red"/>
<Label value="Car" background="blue"/>
</RectangleLabels>
</View>

Expand Down
4 changes: 2 additions & 2 deletions label_studio/examples/image_polygons/config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<Image name="img" value="$image"/>

<PolygonLabels name="tag" toName="img" strokewidth="5">
<Label value="Hello" background="red"/>
<Label value="World" background="blue"/>
<Label value="Airplane" background="red"/>
<Label value="Car" background="blue"/>
</PolygonLabels>
</View>
2 changes: 1 addition & 1 deletion label_studio/logger.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
}
},
"root": {
"level": "ERROR",
"level": "DEBUG",
"handlers": [
"console"
]
Expand Down
43 changes: 28 additions & 15 deletions label_studio/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Project(object):
'Audio': ('.wav', '.aiff', '.mp3', '.au', '.flac')
}

def __init__(self, config, name):
def __init__(self, config, name, context=None):
self.config = config
self.name = name

Expand All @@ -49,7 +49,8 @@ def __init__(self, config, name):
self.project_obj = None
self.analytics = None
self.converter = None

self.on_boarding = {}
self.context = context or {}
self.reload()

@property
Expand Down Expand Up @@ -77,8 +78,13 @@ def validate_label_config(self, config_string):
def update_label_config(self, new_label_config):
label_config_file = self.config['label_config']
# save xml label config to file
with io.open(label_config_file, mode='w') as fout:
fout.write(new_label_config)
with io.open(label_config_file, mode='w') as f:
f.write(new_label_config)

# save project config state
self.config['label_config_updated'] = True
with io.open(self.config['config_path'], mode='w') as f:
json.dump(self.config, f)
logger.info('Label config saved to: {path}'.format(path=label_config_file))

def _get_single_input_value(self, input_data_tags):
Expand Down Expand Up @@ -479,9 +485,9 @@ def reload(self):
if collect_analytics is None:
collect_analytics = self.config.get('collect_analytics', True)
if self.analytics is None:
self.analytics = Analytics(self.label_config_line, collect_analytics, self.name)
self.analytics = Analytics(self.label_config_line, collect_analytics, self.name, self.context)
else:
self.analytics.update_info(self.label_config_line, collect_analytics, self.name)
self.analytics.update_info(self.label_config_line, collect_analytics, self.name, self.context)

# configure project
self.project_obj = ProjectObj(label_config=self.label_config_line, label_config_full=self.label_config_full)
Expand Down Expand Up @@ -654,16 +660,17 @@ def _get_config(cls, project_dir, args):
config['label_config'] = os.path.join(config_dir, config['label_config'])
config['input_path'] = os.path.join(config_dir, config['input_path'])
config['output_dir'] = os.path.join(config_dir, config['output_dir'])
config['config_path'] = config_path

return config

@classmethod
def _load_from_dir(cls, project_dir, project_name, args):
def _load_from_dir(cls, project_dir, project_name, args, context):
config = cls._get_config(project_dir, args)
return cls(config, project_name)
return cls(config, project_name, context)

@classmethod
def get(cls, project_name, args):
def get(cls, project_name, args, context):

# If project stored in memory, just return it
if project_name in cls._storage:
Expand All @@ -672,25 +679,31 @@ def get(cls, project_name, args):
# If project directory exists, load project from directory and update in-memory storage
project_dir = cls.get_project_dir(project_name, args)
if os.path.exists(project_dir):
project = cls._load_from_dir(project_dir, project_name, args)
project = cls._load_from_dir(project_dir, project_name, args, context)
cls._storage[project_name] = project

raise KeyError('Project {p} doesn\'t exist'.format(p=project_name))

@classmethod
def create(cls, project_name, args):
def create(cls, project_name, args, context):
# "create" method differs from "get" as it can create new directory with project resources
project_dir = cls.create_project_dir(project_name, args)
project = cls._load_from_dir(project_dir, project_name, args)
project = cls._load_from_dir(project_dir, project_name, args, context)
cls._storage[project_name] = project
return project

@classmethod
def get_or_create(cls, project_name, args):
def get_or_create(cls, project_name, args, context):
try:
project = cls.get(project_name, args)
project = cls.get(project_name, args, context)
logger.info('Get project "' + project_name + '".')
except KeyError:
project = cls.create(project_name, args)
project = cls.create(project_name, args, context)
logger.info('Project "' + project_name + '" created.')
return project

def update_on_boarding_state(self):
self.on_boarding['setup'] = self.config.get('label_config_updated', False)
self.on_boarding['import'] = len(self.tasks) > 0
self.on_boarding['labeled'] = len(os.listdir(self.config['output_dir'])) > 0
return self.on_boarding
56 changes: 46 additions & 10 deletions label_studio/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,22 +42,40 @@
input_args = None


def project_get_or_create():
def project_get_or_create(multi_session_force_recreate=False):
"""
Return existed or create new project based on environment. Currently supported methods:
- "fixed": project is based on "project_name" attribute specified by input args when app starts
- "session": project is based on "project_name" key restored from flask.session object
:return:
"""
if input_args.command == 'start-multi-session':
if 'project_name' in session:
project_name = session['project_name']
else:
project_name = str(uuid4())
session['project_name'] = project_name
return Project.get_or_create(project_name, input_args)
# get user from session
if 'user' not in session:
session['user'] = str(uuid4())
user = session['user']

# get project from session
if 'project' not in session or multi_session_force_recreate:
session['project'] = str(uuid4())
project = session['project']

project_name = user + '/' + project
return Project.get_or_create(project_name, input_args, context={
'user': user,
'project': project,
'multi_session': True,
})
else:
return Project.get_or_create(input_args.project_name, input_args)
if multi_session_force_recreate:
raise NotImplementedError(
'"multi_session_force_recreate" option supported only with "start-multi-session" mode')
user = project = input_args.project_name # in standalone mode, user and project are singletons and consts
return Project.get_or_create(input_args.project_name, input_args, context={
'user': user,
'project': project,
'multi_session': False
})


@app.template_filter('json')
Expand Down Expand Up @@ -129,10 +147,12 @@ def welcome_page():
"""
project = project_get_or_create()
project.analytics.send(getframeinfo(currentframe()).function)
project.update_on_boarding_state()
return flask.render_template(
'welcome.html',
config=project.config,
project=project.project_obj
project=project.project_obj,
on_boarding=project.on_boarding
)


Expand Down Expand Up @@ -176,7 +196,8 @@ def setup_page():
project=project.project_obj,
label_config_full=project.label_config_full,
templates=templates,
input_values=input_values
input_values=input_values,
multi_session=input_args.command == 'start-multi-session'
)


Expand Down Expand Up @@ -455,6 +476,17 @@ def api_generate_next_task():
return make_response('', 404)


@app.route('/api/project/', methods=['POST', 'GET'])
@exception_treatment
def api_project():
""" Project global operation
"""
project = project_get_or_create(multi_session_force_recreate=False)
if request.method == 'POST' and request.args.get('new', False):
project = project_get_or_create(multi_session_force_recreate=True)
return make_response(jsonify({'project_name': project.name}), 201)


@app.route('/api/projects/1/task_ids/', methods=['GET'])
@exception_treatment
def api_all_task_ids():
Expand Down Expand Up @@ -721,10 +753,14 @@ def main():
import threading
import webbrowser

import label_studio.utils.functions

global input_args

input_args = parse_input_args()

label_studio.utils.functions.HOSTNAME = 'http://localhost:' + str(input_args.port)

# On `init` command, create directory args.project_name with initial project state and exit
if input_args.command == 'init':
Project.create_project_dir(input_args.project_name, input_args)
Expand Down
5 changes: 5 additions & 0 deletions label_studio/static/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@ a {
font-weight: bold;
}

#new-project-button:hover {
background: #db2828 !important;
color: white;
}

.btn {
line-height: 1.5;
position: relative;
Expand Down
10 changes: 8 additions & 2 deletions label_studio/static/js/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,15 @@ function message_from_response(result) {
// result is object from XHR, check responseText first, it is always presented
if (!result.responseText) return "Critical error on server";
// grab responseJSON detail
else if (result.responseJSON.hasOwnProperty("detail")) return result.responseJSON["detail"];
else if (result.responseJSON && result.responseJSON.hasOwnProperty("detail")) {
return result.responseJSON["detail"];
}
// something strange inside of responseJSON
else return JSON.stringify(result.responseJSON);
else if (result.hasOwnProperty('responseJSON'))
return JSON.stringify(result.responseJSON);
else {
return 'Critical error on the server side'
}
}

// Take closest form (or form by id) and submit it,
Expand Down
Binary file added label_studio/static/samples/sample-a.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added label_studio/static/samples/sample.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions label_studio/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<img src="/static/images/github.svg" height="22"/></a>

<a href="https://docs.google.com/forms/d/e/1FAIpQLSdLHZx5EeT1J350JPwnY2xLanfmvplJi6VZk65C2R4XSsRBHg/viewform?usp=sf_link"
data-tooltip="If you have any troubles or suggestion just report us to Slack" data-position="bottom right"
target="_blank"><img src="/static/images/slack.png" height="22"/></a>

</ul>
Expand Down
22 changes: 14 additions & 8 deletions label_studio/templates/import.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
<div class="ui form">
<label for="url-input" class="description">Or paste a file URL or task in JSON format here</label><br/>
<div class="ui action input fluid">
<input id="url-input" type="text" name="url" autofocus autocomplete="on"/>
<input id="url-input" type="text" name="url" autofocus autocomplete="on"
placeholder='http://example.com/tasks.json OR {"image_url": "http://my.image.jpg"}'/>
<button style="display: inline-block !important; width: 14%; min-width: 80px !important;"
class="ui positive button" id="url-button" onclick="send_data(event, $('#url-input').val())">Import
</button>
Expand All @@ -60,9 +61,14 @@
<div class="description" id="upload-dialog-msg"></div>
</div>
<div class="actions">
<span id="success-actions" hidden>
<a data-tooltip="You can start the labeling right now" class="ui button positive" href="/">Start Labeling</a>
<a data-tooltip="Explore all imported tasks" class="ui button positive" href="/tasks">Explore Tasks</a>
</span>

<!-- Done button -->
<button class="ui icon button" id="upload-done-button"
hidden onclick="location.reload()" autofocus>Done
hidden onclick="location.reload()" autofocus>Close
</button>
</div>
</div>
Expand Down Expand Up @@ -122,7 +128,8 @@
}).modal('show');

if (success) {
$('#upload-done-button').addClass('positive')
$('#upload-done-button').addClass('positive');
$('#success-actions').show();
} else {
$('#upload-done-button').addClass('red')
}
Expand Down Expand Up @@ -187,12 +194,11 @@

$.ajax(request)
.done(answer => {
let msg = 'Tasks created: ' + answer['task_count'] +
'<br/>Completions created: ' + answer['completion_count'] +
'<br/>Predictions created: ' + answer['prediction_count'] +
'<br/>Duration: ' + answer['duration'].toFixed(2) + ' sec';
let msg = '<h2>Tasks created: ' + answer['task_count'] + '</h2>' +
'Completions created: ' + answer['completion_count'] +
'<br>Predictions created: ' + answer['prediction_count'] +
'<br><br>Duration: ' + answer['duration'].toFixed(2) + ' sec';
stop_wait(msg, true);

})
.fail(answer => {
let msg = "Error: can't upload/process file on server side. Reasons:<br><br>";
Expand Down
Loading

0 comments on commit dba7111

Please sign in to comment.