Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
beat
beat.editor
Commits
53ec2269
Commit
53ec2269
authored
Jun 17, 2020
by
Flavio TARSETTI
Browse files
Merge branch '185_implement_connection_check' into 'master'
Implement connection check See merge request
!132
parents
c11e99e6
90cabb18
Pipeline
#40520
passed with stages
in 12 minutes and 50 seconds
Changes
11
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
.pre-commit-config.yaml
View file @
53ec2269
...
...
@@ -10,7 +10,7 @@ repos:
rev
:
stable
hooks
:
-
id
:
black
language_version
:
python3.
6
language_version
:
python3.
7
-
repo
:
https://github.com/pre-commit/pre-commit-hooks
rev
:
v2.0.0
hooks
:
...
...
beat/editor/backend/experimentmodel.py
0 → 100644
View file @
53ec2269
# vim: set fileencoding=utf-8 :
###############################################################################
# #
# Copyright (c) 2020 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.editor module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
# or FITNESS FOR A PARTICULAR PURPOSE. #
# #
# You should have received a copy of the GNU Affero Public License along #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# #
###############################################################################
import
typing
from
beat.core.database
import
Database
from
.asset
import
Asset
from
.asset
import
AssetType
class
Connection
:
"""Class representing the connection between the output of a source block
and the input of the corresponding sink.
The information comes from the toolchain.
"""
def
__init__
(
self
,
source
:
str
,
output_name
:
str
,
sink
:
str
,
input_name
:
str
)
->
None
:
self
.
source
=
source
self
.
output_name
=
output_name
self
.
sink
=
sink
self
.
input_name
=
input_name
def
is_used_by_block
(
self
,
block_name
:
str
):
"""Returns whether the given block is concerned by this connection"""
return
self
.
source
==
block_name
or
self
.
sink
==
block_name
def
__repr__
(
self
)
->
str
:
text
=
[
f
"
{
self
.
__class__
.
__name__
}
("
,
f
"from:
{
self
.
source
}
.
{
self
.
output_name
}
"
,
f
"to:
{
self
.
sink
}
.
{
self
.
input_name
}
"
,
")"
,
]
return
"
\n
"
.
join
(
text
)
@
property
def
from_output
(
self
)
->
str
:
return
f
"
{
self
.
source
}
.
{
self
.
output_name
}
"
@
property
def
to_input
(
self
)
->
str
:
return
f
"
{
self
.
sink
}
.
{
self
.
input_name
}
"
class
ExperimentBlock
:
"""Base class experiment blocks representation"""
def
__init__
(
self
,
name
:
str
,
config
:
dict
)
->
None
:
self
.
name
:
str
=
name
self
.
parse
(
config
)
def
parse
(
self
,
config
:
dict
)
->
None
:
raise
NotImplementedError
def
dataformat_for_endpoint
(
self
,
endpoint
:
str
)
->
str
:
raise
NotImplementedError
def
__repr__
(
self
)
->
str
:
return
f
"
{
self
.
__class__
.
__name__
}
(
{
self
.
name
}
)"
class
AlgorithmData
:
"""Class containing the information related to the endpoints of an algorithm
"""
def
__init__
(
self
)
->
None
:
self
.
input_type_map
:
typing
.
Mapping
[
str
,
str
]
=
{}
self
.
output_type_map
:
typing
.
Mapping
[
str
,
str
]
=
{}
# left: alg io right: block io
self
.
input_mapping
:
typing
.
Mapping
[
str
,
str
]
=
{}
self
.
output_mapping
:
typing
.
Mapping
[
str
,
str
]
=
{}
def
parse
(
self
,
config
:
dict
)
->
None
:
prefix
=
config
.
pop
(
"prefix"
)
algorithm_name
=
config
.
get
(
"algorithm"
)
if
algorithm_name
is
not
None
:
algorithm
=
Asset
(
prefix
,
AssetType
.
ALGORITHM
,
algorithm_name
)
for
group
in
algorithm
.
declaration
[
"groups"
]:
for
io_type
,
type_map
in
[
(
"inputs"
,
self
.
input_type_map
),
(
"outputs"
,
self
.
output_type_map
),
]:
for
input_name
,
input_data
in
group
.
get
(
io_type
,
{}).
items
():
type_map
[
input_name
]
=
input_data
[
"type"
]
self
.
input_mapping
=
config
.
get
(
"inputs"
,
{})
self
.
output_mapping
=
config
.
get
(
"outputs"
,
{})
def
dataformat_for_endpoint
(
self
,
endpoint
:
str
)
->
str
:
for
alg_in
,
block_in
in
self
.
input_mapping
.
items
():
if
block_in
==
endpoint
:
return
self
.
input_type_map
[
alg_in
]
for
alg_out
,
block_out
in
self
.
output_mapping
.
items
():
if
block_out
==
endpoint
:
return
self
.
output_type_map
[
alg_out
]
return
""
def
__repr__
(
self
)
->
str
:
representation
=
[
f
"
{
self
.
__class__
.
__name__
}
("
]
if
self
.
input_type_map
:
representation
+=
[
"inputs:"
f
"
{
self
.
input_mapping
}
"
f
"
{
self
.
input_type_map
}
"
]
if
self
.
output_type_map
:
representation
+=
[
"outputs:"
f
"
{
self
.
output_mapping
}
"
f
"
{
self
.
output_type_map
}
"
]
representation
.
append
(
")"
)
return
"
\n
"
.
join
(
representation
)
class
AlgorithmBlock
(
ExperimentBlock
):
"""Class containing the endpoints of an algorithm block"""
def
__init__
(
self
,
name
:
str
,
config
:
dict
)
->
None
:
self
.
algorithm_data
:
AlgorithmData
=
AlgorithmData
()
super
().
__init__
(
name
,
config
)
def
parse
(
self
,
config
:
dict
)
->
None
:
self
.
algorithm_data
.
parse
(
config
)
def
dataformat_for_endpoint
(
self
,
endpoint
:
str
)
->
str
:
return
self
.
algorithm_data
.
dataformat_for_endpoint
(
endpoint
)
def
__repr__
(
self
)
->
str
:
text
=
[
f
"
{
self
.
__class__
.
__name__
}
("
,
f
"name:
{
self
.
name
}
"
,
f
"
{
self
.
algorithm_data
}
"
,
")"
,
]
return
"
\n
"
.
join
(
text
)
class
LoopBlock
(
ExperimentBlock
):
"""Class containing the endpoints of a loop block"""
def
__init__
(
self
,
name
:
str
,
config
:
dict
)
->
None
:
self
.
processor_data
:
AlgorithmData
=
AlgorithmData
()
self
.
evaluator_data
:
AlgorithmData
=
AlgorithmData
()
super
().
__init__
(
name
,
config
)
def
parse
(
self
,
config
:
dict
)
->
None
:
for
algorithm_type
,
algorithm_data
in
(
(
"processor_"
,
self
.
processor_data
),
(
"evaluator_"
,
self
.
evaluator_data
),
):
keys
=
[
key
for
key
in
config
.
keys
()
if
key
.
startswith
(
algorithm_type
)]
algorithm_config
=
{
"prefix"
:
config
[
"prefix"
]}
for
key
in
keys
:
algorithm_config
[
key
[
len
(
algorithm_type
)
:]]
=
config
[
key
]
algorithm_data
.
parse
(
algorithm_config
)
def
dataformat_for_endpoint
(
self
,
endpoint
:
str
)
->
str
:
data_format
=
self
.
processor_data
.
dataformat_for_endpoint
(
endpoint
)
if
not
data_format
:
data_format
=
self
.
evaluator_data
.
dataformat_for_endpoint
(
endpoint
)
return
data_format
def
__repr__
(
self
)
->
str
:
text
=
[
f
"
{
self
.
__class__
.
__name__
}
("
,
f
"name:
{
self
.
name
}
"
,
f
"processor:
{
self
.
processor_data
}
"
,
f
"evaluator:
{
self
.
evaluator_data
}
"
,
")"
,
]
return
"
\n
"
.
join
(
text
)
class
AnalyzerBlock
(
AlgorithmBlock
):
"""Class containing the endpoints of an analyzer block"""
class
DatasetBlock
(
ExperimentBlock
):
"""Class containing the endpoints of a dataset block"""
def
__init__
(
self
,
name
:
str
,
config
:
dict
)
->
None
:
self
.
output_type_map
:
typing
.
Mapping
[
str
,
str
]
=
{}
super
().
__init__
(
name
,
config
)
def
parse
(
self
,
config
:
dict
)
->
None
:
prefix
=
config
.
pop
(
"prefix"
)
database_name
=
config
.
get
(
"database"
)
if
database_name
:
database
=
Database
(
prefix
,
database_name
)
if
database
.
valid
:
set_data
=
database
.
set
(
config
[
"protocol"
],
config
[
"set"
])
self
.
output_type_map
=
set_data
.
get
(
"outputs"
,
{})
def
dataformat_for_endpoint
(
self
,
endpoint
:
str
)
->
str
:
return
self
.
output_type_map
[
endpoint
]
def
__repr__
(
self
)
->
str
:
return
(
f
"
{
self
.
__class__
.
__name__
}
(
\n
"
f
"name:
{
self
.
name
}
\n
"
f
"outputs:
{
self
.
output_type_map
}
\n
"
")
\n
"
)
ErrorMap
=
typing
.
Mapping
[
str
,
typing
.
List
[
str
]]
class
ExperimentModel
:
"""This class contains the connection related information for an experiment.
It's goal is to allow the verification of the compatibility between two
blocks connected endpoints.
"""
def
__init__
(
self
)
->
None
:
self
.
connections
:
typing
.
List
[
Connection
]
=
[]
self
.
blocks
:
typing
.
Mapping
[
str
,
ExperimentBlock
]
=
{}
self
.
experiment
:
Asset
=
None
def
_load_toolchain_info
(
self
)
->
None
:
"""Load the needed information from the toolchain
Currently only the connections are of interest.
"""
toolchain
=
Asset
(
self
.
experiment
.
prefix
,
AssetType
.
TOOLCHAIN
,
"/"
.
join
(
self
.
experiment
.
name
.
split
(
"/"
)[
1
:
4
]),
)
declaration
=
toolchain
.
declaration
# Load connections
connections
=
declaration
[
"connections"
]
for
connection
in
connections
:
source
,
output
=
connection
[
"from"
].
split
(
"."
)
sink
,
input_
=
connection
[
"to"
].
split
(
"."
)
self
.
connections
.
append
(
Connection
(
source
,
output
,
sink
,
input_
))
def
_load_experiment_info
(
self
)
->
None
:
"""Load all the endpoints related information."""
declaration
=
self
.
experiment
.
declaration
for
block_type
,
klass
in
[
(
"blocks"
,
AlgorithmBlock
),
(
"loops"
,
LoopBlock
),
(
"analyzers"
,
AnalyzerBlock
),
(
"datasets"
,
DatasetBlock
),
]:
for
block_name
,
config
in
declaration
.
get
(
block_type
,
{}).
items
():
config
[
"prefix"
]
=
self
.
experiment
.
prefix
self
.
blocks
[
block_name
]
=
klass
(
block_name
,
config
)
def
load_experiment
(
self
,
experiment
:
Asset
)
->
None
:
"""Load the required experiment data"""
self
.
clear
()
self
.
experiment
=
experiment
self
.
_load_toolchain_info
()
self
.
_load_experiment_info
()
def
clear
(
self
)
->
None
:
"""Clear the model content"""
self
.
connections
=
[]
self
.
blocks
=
{}
def
update_block
(
self
,
block_name
:
str
,
config
:
dict
)
->
None
:
"""Update one block content"""
config
[
"prefix"
]
=
self
.
experiment
.
prefix
self
.
blocks
[
block_name
].
parse
(
config
)
def
check_all_blocks
(
self
)
->
ErrorMap
:
"""Check that all blocks connections are compatible"""
declaration
=
self
.
experiment
.
declaration
error_map
:
ErrorMap
=
{}
for
block_type
in
[
"blocks"
,
"loops"
,
"analyzers"
,
"datasets"
]:
for
block_name
in
declaration
.
get
(
block_type
,
{}).
keys
():
error_list
=
self
.
check_block
(
block_name
)
if
error_list
:
error_map
[
block_name
]
=
error_list
return
error_map
def
check_block
(
self
,
block_name
:
str
)
->
typing
.
List
[
str
]:
"""Check that one block connections are compatible"""
connections
=
[
connection
for
connection
in
self
.
connections
if
connection
.
is_used_by_block
(
block_name
)
]
connection_error_list
=
[]
for
connection
in
connections
:
source_block
=
self
.
blocks
[
connection
.
source
]
from_df
=
source_block
.
dataformat_for_endpoint
(
connection
.
output_name
)
sink_block
=
self
.
blocks
[
connection
.
sink
]
to_df
=
sink_block
.
dataformat_for_endpoint
(
connection
.
input_name
)
if
from_df
and
to_df
:
if
from_df
!=
to_df
:
connection_error_list
.
append
(
f
"
{
connection
.
from_output
}
{
from_df
}
is not compatible with
{
connection
.
to_input
}
{
to_df
}
"
)
# else:
# Either side of the connection being unassigned means that the
# connection is "correct".
return
connection_error_list
beat/editor/backend/resourcemodels.py
View file @
53ec2269
...
...
@@ -24,6 +24,7 @@
###############################################################################
import
logging
import
simplejson
as
json
from
PyQt5.QtSql
import
QSqlDatabase
...
...
@@ -112,7 +113,9 @@ class ExperimentResources:
for
algorithm
in
model
.
stringList
():
asset
=
Asset
(
prefix_path
,
AssetType
.
ALGORITHM
,
algorithm
)
if
not
asset
.
is_valid
:
is_valid
,
_
=
asset
.
is_valid
()
if
not
is_valid
:
logger
.
debug
(
"Skipping invalid algorithm {}"
.
format
(
algorithm
))
continue
declaration
=
asset
.
declaration
...
...
@@ -313,10 +316,10 @@ class DatasetResourceModel(QSqlTableModel):
self
.
select
()
def
update
(
self
):
filter_str
=
f
""
filter_str
=
""
if
self
.
_output_count
is
not
None
:
filter_str
+
=
f
"outputs=
{
self
.
_output_count
}
"
filter_str
=
f
"outputs=
{
self
.
_output_count
}
"
self
.
setFilter
(
filter_str
)
...
...
beat/editor/test/conftest.py
View file @
53ec2269
...
...
@@ -56,6 +56,7 @@ def sync_prefix():
prefixes
=
[
pkg_resources
.
resource_filename
(
"beat.backend.python.test"
,
"prefix"
),
pkg_resources
.
resource_filename
(
"beat.core.test"
,
"prefix"
),
pkg_resources
.
resource_filename
(
"beat.editor.test"
,
"prefix"
),
]
for
path
in
prefixes
:
...
...
beat/editor/test/prefix/algorithms/user/string_offsetter/1.json
0 → 100644
View file @
53ec2269
{
"schema_version"
:
3
,
"language"
:
"python"
,
"api_version"
:
2
,
"type"
:
"sequential"
,
"splittable"
:
false
,
"groups"
:
[
{
"name"
:
"main"
,
"inputs"
:
{
"in_data"
:
{
"type"
:
"user/single_string/1"
}
},
"outputs"
:
{
"out_data"
:
{
"type"
:
"user/single_string/1"
}
}
}
],
"parameters"
:
{
"offset"
:
{
"default"
:
0
,
"type"
:
"int8"
,
"description"
:
"Offset to apply"
}
}
}
beat/editor/test/prefix/algorithms/user/string_offsetter/1.py
0 → 100644
View file @
53ec2269
#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
###################################################################################
# #
# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# Redistribution and use in source and binary forms, with or without #
# modification, are permitted provided that the following conditions are met: #
# #
# 1. Redistributions of source code must retain the above copyright notice, this #
# list of conditions and the following disclaimer. #
# #
# 2. Redistributions in binary form must reproduce the above copyright notice, #
# this list of conditions and the following disclaimer in the documentation #
# and/or other materials provided with the distribution. #
# #
# 3. Neither the name of the copyright holder nor the names of its contributors #
# may be used to endorse or promote products derived from this software without #
# specific prior written permission. #
# #
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND #
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED #
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE #
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE #
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL #
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR #
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER #
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, #
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE #
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #
# #
###################################################################################
class
Algorithm
:
def
setup
(
self
,
parameters
):
self
.
offset
=
parameters
[
"offset"
]
return
True
def
process
(
self
,
inputs
,
data_loaders
,
outputs
):
outputs
[
"out_data"
].
write
({
"value"
:
inputs
[
"in_data"
].
data
.
value
+
self
.
offset
})
return
True
beat/editor/test/test_assetwidget.py
View file @
53ec2269
...
...
@@ -591,3 +591,24 @@ class TestAssetWidget:
asset_widget
.
loadAsset
(
asset
)
assert
asset_widget
.
current_asset
.
name
==
"v1/sum/2"
def
test_experiment_error_hinting
(
self
,
qtbot
,
test_prefix
,
beat_context
):
asset_widget
=
AssetWidget
()
qtbot
.
addWidget
(
asset_widget
)
asset_widget
.
set_context
(
beat_context
)
asset_name
=
"user/user/two_loops/1/two_loops"
asset
=
Asset
(
test_prefix
,
AssetType
.
EXPERIMENT
,
asset_name
)
with
qtbot
.
waitSignal
(
asset_widget
.
currentAssetChanged
):
asset_widget
.
loadAsset
(
asset
)
BLOCK_TO_CHANGE
=
"offsetter_for_loop_evaluator"
ALGORITHM_TO_SELECT
=
"user/string_offsetter/1"
editor
=
asset_widget
.
current_editor
.
findEditor
(
BLOCK_TO_CHANGE
)
with
qtbot
.
waitSignal
(
editor
.
dataChanged
):
editor
.
properties_editor
.
algorithm_combobox
.
setCurrentText
(
ALGORITHM_TO_SELECT
)
assert
editor
.
error_label
.
toolTip
()
!=
""
beat/editor/test/test_experimenteditor.py
View file @
53ec2269
...
...
@@ -44,11 +44,11 @@ from ..backend.assetmodel import AssetModel
from
..backend.resourcemodels
import
AlgorithmResourceModel
from
..backend.resourcemodels
import
ExperimentResources
from
..backend.resourcemodels
import
QueueResourceModel
from
..backend.resourcemodels
import
experiment_resources
from
..widgets.experimenteditor
import
AlgorithmParametersEditor
from
..widgets.experimenteditor
import
AnalyzerBlockEditor
from
..widgets.experimenteditor
import
BlockEditor
from
..widgets.experimenteditor
import
DatasetEditor
from
..widgets.experimenteditor
import
DatasetModel
from
..widgets.experimenteditor
import
EnvironmentModel
from
..widgets.experimenteditor
import
ExecutionPropertiesEditor
from
..widgets.experimenteditor
import
ExperimentEditor
...
...
@@ -379,15 +379,8 @@ class TestDatasetEditor:
return
datasets
[
dataset
]
@
pytest
.
fixture
()
def
dataset_model
(
self
,
test_prefix
):
dataset_model
=
DatasetModel
()
dataset_model
.
setPrefixPath
(
test_prefix
)
return
dataset_model
@
pytest
.
fixture
()
def
editor
(
self
,
qtbot
,
test_prefix
,
datasets
,
dataset_model
):
def
editor
(
self
,
qtbot
,
test_prefix
,
datasets
):
editor
=
DatasetEditor
(
"test_block"
,
test_prefix
)
editor
.
setDatasetModel
(
dataset_model
)
qtbot
.
addWidget
(
editor
)
return
editor
...
...
@@ -1130,6 +1123,7 @@ class TestExperimentEditor:
@
pytest
.
fixture
()
def
experiment_editor
(
self
,
qtbot
,
beat_context
,
experiment_declaration
):
experiment_resources
.
setContext
(
beat_context
)
editor
=
ExperimentEditor
()
editor
.
set_context
(
beat_context
)
editor
.
load_json
(
experiment_declaration
)
...
...
@@ -1137,14 +1131,11 @@ class TestExperimentEditor:
return
editor
@
pytest
.
mark
.
parametrize
(
"experiment"
,
get_valid_experiments
(
prefix
))
def
test_load_and_dump
(
self
,
qtbot
,
beat_context
,
test_prefix
,
experiment
):
def
test_load_and_dump
(
self
,
experiment_editor
,
test_prefix
,
experiment
):
experiment_declaration
=
get_experiment_declaration
(
test_prefix
,
experiment
)
editor
=
ExperimentEditor
()
editor
.
set_context
(
beat_context
)
editor
.
load_json
(
experiment_declaration
)
qtbot
.
addWidget
(
editor
)
experiment_editor
.
load_json
(
experiment_declaration
)
assert
editor
.
dump_json
()
==
experiment_declaration
assert
experiment_
editor
.
dump_json
()
==
experiment_declaration
def
test_change_dataset
(
self
,
qtbot
,
experiment_editor
,
experiment_declaration
):
dataset_editor
=
experiment_editor
.
datasets_widget
.
widget_list
[
0
]
...
...
@@ -1307,3 +1298,17 @@ class TestExperimentEditor:
dump
=
experiment_editor
.
dump_json
()
assert
dump
!=
experiment_declaration
assert
"environment"
not
in
dump
[
"blocks"
][
block_editor
.
block_name
]
def
test_showing_error
(
self
,
qtbot
,
experiment_editor
,
experiment_declaration
):
experiment_editor
.
load_json
(
experiment_declaration
)
ERROR_BLOCK
=
"echo"
errors
=
{
ERROR_BLOCK
:
[
"show this error"
]}
experiment_editor
.
setBlockErrors
(
errors
)
editor
=
experiment_editor
.
findEditor
(
ERROR_BLOCK
)
assert
editor
.
error_label
.
toolTip
()
==
"show this error"
experiment_editor
.
clearBlockErrors
()
assert
editor
.
error_label
.
toolTip
()
==
""
beat/editor/test/test_experimentmodel.py
0 → 100644
View file @
53ec2269
# vim: set fileencoding=utf-8 :
###############################################################################
# #
# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.editor module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform is distributed in the hope that it will be useful, but #