diff --git a/scripts/larvatagger-gui.jl b/scripts/larvatagger-gui.jl
new file mode 100755
index 0000000000000000000000000000000000000000..fed465faba452e064b60529d61e91de3615e08c2
--- /dev/null
+++ b/scripts/larvatagger-gui.jl
@@ -0,0 +1,24 @@
+#!/bin/bash
+#=
+if [ -z "$(which realpath)" ]; then
+macos_realpath=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
+PROJECT_DIR=$(dirname $macos_realpath)
+else
+PROJECT_DIR=$(dirname $(dirname $(realpath "${BASH_SOURCE[0]}")))
+fi
+FLAGS=
+if [ "$1" = "--sysimage" -o "$1" = "-J" ]; then FLAGS="--sysimage $2 "; shift 2; fi
+if [ "${1:0:2}" = "-J" ]; then FLAGS="$1 "; shift; fi
+if [ -n "$1" -a -e "$1" ]; then FLAGS="$FLAGS -iq "; fi
+if [ -z "$JULIA" ]; then JULIA=julia; fi
+    exec $JULIA --project="$PROJECT_DIR" --color=yes --startup-file=no $FLAGS\
+    "${BASH_SOURCE[0]}" "$@"
+=#
+
+projectdir = dirname(Base.active_project())
+
+include(joinpath(projectdir, "src/cli_open.jl")) # defines `main`
+
+using .GUI
+
+main();
diff --git a/scripts/larvatagger-toolkit.jl b/scripts/larvatagger-toolkit.jl
new file mode 100755
index 0000000000000000000000000000000000000000..08988999ad27cc2aba2da750f152c052887d12cc
--- /dev/null
+++ b/scripts/larvatagger-toolkit.jl
@@ -0,0 +1,23 @@
+#!/bin/bash
+#=
+if [ -z "$(which realpath)" ]; then
+macos_realpath=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
+PROJECT_DIR=$(dirname $macos_realpath)
+else
+PROJECT_DIR=$(dirname $(dirname $(realpath "${BASH_SOURCE[0]}")))
+fi
+FLAGS=
+if [ "$1" = "--sysimage" -o "$1" = "-J" ]; then FLAGS="--sysimage $2 "; shift 2; fi
+if [ "${1:0:2}" = "-J" ]; then FLAGS="$1"; shift; fi
+if [ -z "$JULIA" ]; then JULIA=julia; fi
+exec $JULIA --project="$PROJECT_DIR" --color=yes --startup-file=no $FLAGS\
+    "${BASH_SOURCE[0]}" "$@"
+=#
+
+projectdir = dirname(Base.active_project())
+
+include(joinpath(projectdir, "src/cli_toolkit.jl")) # defines `main`
+
+using .Toolkit
+
+main();
diff --git a/scripts/larvatagger.jl b/scripts/larvatagger.jl
index d053125de4a2a6b97e123712cc7a90a19de8f0c9..cb3898615029cc61a58dfdb5249c31ad1980c23c 100755
--- a/scripts/larvatagger.jl
+++ b/scripts/larvatagger.jl
@@ -1,3 +1,105 @@
+#!/bin/bash
+
+# This script is written in Bash and replaces a Julia script whose name (and extension)
+# is kept for backward compatibility.
+
+# Former scripts/larvatagger.jl and src/cli.jl have been split in distinct scripts and
+# source files in an attempt to fix issue https://gitlab.pasteur.fr/nyx/larvatagger.jl/-/issues/61
+
+currentdir=$(dirname "${BASH_SOURCE[0]}")
+
+if [ -z "$1" -o "$1" = "-h" -o "$1" = "--help" ]; then
+cat << "EOT"
+LarvaTagger.jl
+
+Usage:
+  larvatagger.jl open <file-path> [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--manual-label=<label>]
+  larvatagger.jl import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>] [--pixelsize=<μm>] [--overrides=<comma-separated-list>] [--default-label=<label>] [--manual-label=<label>] [--decode]
+  larvatagger.jl train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>]
+  larvatagger.jl predict <backend-path> <model-instance> <data-path> [--make-dataset] [--skip-make-dataset] [--data-isolation]
+  larvatagger.jl merge <input-path> <input-file> [<output-file>] [--manual-label=<label>] [--decode]
+  larvatagger.jl -V | --version
+  larvatagger.jl -h | --help
+
+Options:
+  -h --help            Show this screen.
+  -V --version         Show version.
+  -q --quiet           Do not show instructions.
+  --id=<id>            Run or assay ID, e.g. `date_time`.
+  --framerate=<fps>    Camera frame rate, in frames per second.
+  --pixelsize=<μm>     Camera pixel size, in micrometers.
+  --backends=<path>    Path to backend repository.
+  --port=<number>      Port number the server listens to.
+  --viewer             Disable editing capabilities.
+  --browser            Automatically open a browser tab at the served location.
+  --make-dataset       Perform the make_dataset stage prior to proceeding to predict_model.
+  --skip-make-dataset  Skip the make_dataset stage (default behavior since TaggingBackends==0.10).
+  --data-isolation     Isolate the backend data directories for parallel tagging of multiple data files.
+  --sample-size=<N>    Sample only N track segments from the data repository.
+  --layers=<N>         (MaggotUBA) Number of layers of the classifier.
+  --iterations=<N>     (MaggotUBA) Number of training iterations (can be two integers separated by a comma).
+  --decode             Do not encode the labels into integer indices.
+  --default-label=<label>             Label all untagged data as <label>.
+  --manual-label=<label>              Secondary label for manually labelled data [default: edited].
+  --labels=<comma-separated-list>     Comma-separated list of behavior tags/labels.
+  --class-weights=<csv>               Comma-separated list of floats.
+  --pretrained-model=<instance>       Name of the pretrained encoder (from `pretrained_models` registry).
+  --balancing-strategy=<strategy>     Any of `auto`, `maggotuba`, `none` [default: auto].
+  --overrides=<comma-separated-list>  Comma-separated list of key:value pairs.
+
+
+Commands:
+
+  open      Launch the server-based GUI.
+
+    Backends defined in LarvaTagger project root directory are automatically found. Other
+    backend locations can be specified with the --backends argument.
+
+  import    Generate a label file and store metadata.
+
+    This command is particularly useful to specify missing information such as the camera
+    framerate for FIMTrack v2 table.csv files. A label file is created with a pointer to the
+    original csv file, and a metadata section with the provided information.
+    A second usage applies to label files specifically, and consists in extending the tracks
+    and timesteps on basis of the associated data dependencies and assigning a default label
+    to the data points that are not already defined in the label file.
+    Note: label files should always come as siblings of their data dependencies (located in
+    the same directory).
+
+  merge     Combine the content of label files.
+
+    <input-path> is the path to a manually edited label file whose content is to be augmented
+    with manual editions from a second (sibling) label file <input-file>. If no output file
+    is specified, the resulting content is dumped onto the standard output. All label files
+    must be siblings, i.e. located in the same directory.
+    If no editions are found in the second input file, all the defined labels are copied from
+    the second to the first file.
+
+  train     Train a tagger.
+
+    <data-path> can be a path to a file or directory.
+    --class-weights requires --labels to be defined and the specified comma-separated values
+    should match those given by --labels.
+
+  predict   Automatically label tracking data.
+
+    <data-path> can be a path to a file or directory.
+    <data-path> can also be a .txt file listing data files; one relative path per line.
+
+EOT
+
+elif [ "$1" = "open" ]; then
+    shift
+    $currentdir/larvatagger-gui.jl $@
+
+else
+    $currentdir/larvatagger-toolkit.jl $@
+fi
+
+exit 0
+
+# Equivalent Julia script (slower):
+
 #!/bin/bash
 #=
 if [ -z "$(which realpath)" ]; then
@@ -9,7 +111,7 @@ fi
 FLAGS=
 if [ "$1" = "--sysimage" -o "$1" = "-J" ]; then FLAGS="--sysimage $2 "; shift 2; fi
 if [ "${1:0:2}" = "-J" ]; then FLAGS="$1"; shift; fi
-if [ "$1" = "open" ]; then FLAGS="$FLAGS -iq "; fi
+if [ "$1" = "open" -a -n "$2" -a -e "$2" ]; then FLAGS="$FLAGS -iq "; fi
 if [ -z "$JULIA" ]; then JULIA=julia; fi
 exec $JULIA --project="$PROJECT_DIR" --color=yes --startup-file=no $FLAGS\
     "${BASH_SOURCE[0]}" "$@"
diff --git a/src/cli.jl b/src/cli.jl
index 6fb3a1db0710fe6b8fbcd24da3bfdafc860e4aaa..9fdf28250fcc86fb906e1370f79181e432c66bb7 100644
--- a/src/cli.jl
+++ b/src/cli.jl
@@ -1,15 +1,16 @@
-using DocOpt
-using Pkg
-using OrderedCollections
-using PlanarLarvae.Datasets, PlanarLarvae.Formats
-using LarvaTagger, LarvaTagger.Taggers
-using JSServe: JSServe, Server
+
+projectdir = dirname(Base.active_project())
+include(joinpath(projectdir, "src", "cli_open.jl"))
+include(joinpath(projectdir, "src", "cli_toolkit.jl"))
+
+using .GUI
+using .Toolkit
 
 usage = """Larva Tagger.
 
 Usage:
-  larvatagger.jl import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>] [--pixelsize=<μm>] [--overrides=<comma-separated-list>] [--default-label=<label>] [--manual-label=<label>] [--decode]
   larvatagger.jl open <file-path> [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--manual-label=<label>]
+  larvatagger.jl import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>] [--pixelsize=<μm>] [--overrides=<comma-separated-list>] [--default-label=<label>] [--manual-label=<label>] [--decode]
   larvatagger.jl train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>]
   larvatagger.jl predict <backend-path> <model-instance> <data-path> [--make-dataset] [--skip-make-dataset] [--data-isolation]
   larvatagger.jl merge <input-path> <input-file> [<output-file>] [--manual-label=<label>] [--decode]
@@ -83,258 +84,15 @@ Commands:
 
 """
 
-function main(args=ARGS; exit_on_error=true)
-    parsed_args = try
-        docopt(usage, args isa String ? split(args) : args;
-               help=false, exit_on_error=exit_on_error)
-    catch # for exit_on_error==false only
-        print("Parsing error -- ") # help message follows
-        Dict{String, Any}()
+function main(args=ARGS; kwargs...)
+    if args isa String
+        args = split(args)
     end
-
-    if isempty(parsed_args) || parsed_args["--help"]
+    if isempty(args) || "-h" in args || "--help" in args
         print(usage)
-
-    elseif parsed_args["--version"]
-        println(Pkg.project().version)
-
-    elseif parsed_args["import"]
-        infile = parsed_args["<input-path>"]
-        if !isfile(infile)
-            @error "File not found; did you specify a file path after \"import\"?" infile
-            exit()
-        end
-        outfile = parsed_args["<output-file>"]
-        kwargs = Dict{Symbol, Any}()
-        framerate = parsed_args["--framerate"]
-        if !isnothing(framerate)
-            kwargs[:framerate] = parse(Float64, framerate)
-        end
-        pixelsize = parsed_args["--pixelsize"]
-        if !isnothing(pixelsize)
-            kwargs[:pixelsize] = parse(Float64, pixelsize)
-        end
-        file = load(infile; kwargs...)
-        #
-        metadata = file.run.attributes[:metadata]
-        overrides = parsed_args["--overrides"]
-        if !isnothing(overrides)
-            metadata[:overrides] = overrides′= Dict{String, Any}()
-            for override in split(overrides, ',')
-                key, value = split(override, ':'; limit=2)
-                try
-                    value = parse(Int, value)
-                catch
-                    try
-                        value = parse(Float64, value)
-                    catch
-                    end
-                end
-                overrides′[key] = value
-            end
-        end
-        #
-        runid = parsed_args["--id"]
-        run = Run(isnothing(runid) ? file.run.id : runid,
-                  file.run.attributes,
-                  isa(file, Formats.JSONLabels) ? file.run.tracks : OrderedDict{Datasets.TrackID, Track}())
-        if !haskey(run.attributes, :labels)
-            run.attributes[:labels] = String[]
-        end
-        if isa(file, Formats.JSONLabels)
-            for dep in getdependencies(file)
-                Datasets.pushdependency!(run, dep)
-            end
-        else
-            Datasets.pushdependency!(run, infile)
-        end
-        defaultlabel = parsed_args["--default-label"]
-        if !isnothing(defaultlabel)
-            appendlabel!(run, "edited"; ignore=[defaultlabel], ifempty=false)
-            setdefaultlabel!(run, defaultlabel; filepath=infile)
-        end
-        #
-        decode = parsed_args["--decode"]
-        if !decode
-            encodelabels!(run)
-        end
-        if isnothing(outfile)
-            Datasets.write_json(stdout, run)
-        else
-            if '/' in outfile || '\\' in outfile
-                @error "Output file name contains slashes or backslashes (should not be a path)" outfile
-                exit()
-            end
-            outfile = joinpath(dirname(infile), outfile)
-            Datasets.to_json_file(outfile, run)
-            Taggers.check_permissions(outfile)
-        end
-
-    elseif parsed_args["merge"]
-        input_path = parsed_args["<input-path>"]
-        if !isfile(input_path)
-            @error "File not found; did you specify a file path after \"combine\"?" input_path
-            exit()
-        end
-        input_file = parsed_args["<input-file>"]
-        if '/' in input_file || '\\' in input_file
-            @error "Second input file name contains slashes or backslashes (should not be a path)" input_file
-            exit()
-        end
-        second_input = joinpath(dirname(input_path), input_file)
-        if !isfile(second_input)
-            @error "File not found" second_input
-            exit()
-        end
-        #
-        first_input = load(input_path)
-        if !(first_input isa Formats.JSONLabels)
-            @error "Not a JSON label file" first_input
-            exit()
-        end
-        second_input = load(second_input)
-        if !(second_input isa Formats.JSONLabels)
-            @error "Not a JSON label file" second_input
-            exit()
-        end
-        #
-        run = first_input.run
-        run2 = second_input.run
-        if !shareddependencies(run, run2; extend=true)
-            exit()
-        end
-        #
-        attributes = run.attributes
-        attributes2 = run2.attributes
-        edited = parsed_args["--manual-label"]
-        if !(haskey(attributes, :secondarylabels) && edited ∈ attributes[:secondarylabels])
-            @debug "Cannot find manual editions" first_input=basename(first_input.source) edited_label=edited expected_among=get(attributes, :secondarylabels, String[])
-        end
-        if haskey(attributes2, :secondarylabels) && edited ∈ attributes2[:secondarylabels]
-            mergelabels!(run, run2) do labels
-                labels isa Vector && edited ∈ labels
-            end
-        else
-            @warn "Cannot find manual editions" second_input=basename(second_input.source) edited_label=edited expected_among=get(attributes2, :secondarylabels, String[])
-            mergelabels!(run, run2)
-        end
-        #
-        output = run
-        # in the case the first input file was automatically generated, remove the metadata
-        # used elsewhere to distinguish between automatically- and manually-assigned labels
-        metadata = output.attributes[:metadata]
-        if haskey(metadata, :software)
-            software = metadata[:software]
-            tagger = Datasets.coerce(eltype(keys(software)), :tagger)
-            if haskey(software, tagger)
-                delete!(software, tagger)
-                if isempty(software)
-                    delete!(metadata, :software)
-                end
-            end
-        end
-        #
-        decode = parsed_args["--decode"]
-        if !decode
-            encodelabels!(output)
-        end
-        #
-        outfile = parsed_args["<output-file>"]
-        if isnothing(outfile)
-            Datasets.write_json(stdout, output)
-        else
-            if '/' in outfile || '\\' in outfile
-                @error "Output file name contains slashes or backslashes (should not be a path)" outfile
-                exit()
-            end
-            outfile = joinpath(dirname(input_path), outfile)
-            Datasets.to_json_file(outfile, output)
-            Taggers.check_permissions(outfile)
-        end
-
-    elseif parsed_args["open"]
-        verbose = !parsed_args["--quiet"]
-        infile = parsed_args["<file-path>"]
-        if !isfile(infile)
-            @error "File not found; did you specify a file path after \"open\"?" infile
-            exit()
-        end
-        if parsed_args["--viewer"]
-            app = larvaviewer(infile)
-        else
-            kwargs = Dict{Symbol, Any}()
-            backends = parsed_args["--backends"]
-            if !isnothing(backends)
-                kwargs[:backend_directory] = backends
-            end
-            kwargs[:manualtag] = string(parsed_args["--manual-label"])
-            app = larvaeditor(infile; kwargs...)
-        end
-        #
-        port = parsed_args["--port"]
-        port = isnothing(port) ? 9284 : parse(Int, port)
-        server = Server(app, "0.0.0.0", port)
-        if parsed_args["--browser"]
-            JSServe.openurl("http://127.0.0.1:$(port)")
-        elseif verbose
-            @info "The server is ready at http://127.0.0.1:$(port)"
-        end
-        if verbose
-            @info "Press Ctrl+D to stop the server (or Ctrl+C if in PowerShell)"
-        end
-        return server
-
-    elseif parsed_args["train"]
-        backend_path = parsed_args["<backend-path>"]
-        model_instance = parsed_args["<model-instance>"]
-        data_path = parsed_args["<data-path>"]
-        #
-        tagger = Tagger(backend_path, model_instance)
-        Taggers.reset(tagger)
-        Taggers.push(tagger, data_path)
-        kwargs = Dict{Symbol, Any}()
-        layers = parsed_args["--layers"]
-        isnothing(layers) || (kwargs[:layers] = layers)
-        iterations = parsed_args["--iterations"]
-        isnothing(iterations) || (kwargs[:iterations] = iterations)
-        classweights = parsed_args["--class-weights"]
-        isnothing(classweights) || (kwargs[:class_weights] = classweights)
-        train(tagger;
-              pretrained_instance=parsed_args["--pretrained-model"],
-              labels=parsed_args["--labels"],
-              sample_size=parsed_args["--sample-size"],
-              balancing_strategy=parsed_args["--balancing-strategy"],
-              include_all=parsed_args["--manual-label"],
-              kwargs...)
-
-    elseif parsed_args["predict"]
-        backend_path = parsed_args["<backend-path>"]
-        model_instance = parsed_args["<model-instance>"]
-        data_path = parsed_args["<data-path>"]
-        data_isolation = parsed_args["--data-isolation"]
-        #
-        datapath = abspath(data_path)
-        destination = if isfile(datapath)
-            if endswith(datapath, ".txt")
-                # assume that the file lists paths to data files to be processed;
-                # in this case, the path to the txt file is not relevant
-                pwd()
-            else
-                dirname(datapath)
-            end
-        else
-            datapath
-        end
-        #
-        tagger = Tagger(backend_path, model_instance)
-        if data_isolation
-            tagger = Taggers.isolate(tagger)
-            @info "Tagger isolated" sandbox=tagger.sandbox
-        end
-        resetdata(tagger)
-        Taggers.push(tagger, datapath)
-        predict(tagger; skip_make_dataset=parsed_args["--skip-make-dataset"],
-                make_dataset=parsed_args["--make-dataset"])
-        Taggers.pull(tagger, destination)
+    elseif args[1] == "open"
+        GUI.main(args[2:end]; kwargs...)
+    else
+        Toolkit.main(args; kwargs...)
     end
 end
diff --git a/src/cli_open.jl b/src/cli_open.jl
new file mode 100644
index 0000000000000000000000000000000000000000..023b432a6a9c20d9bfb4657c44b6cabcf268ee97
--- /dev/null
+++ b/src/cli_open.jl
@@ -0,0 +1,80 @@
+module GUI
+
+using DocOpt
+using LarvaTagger
+using JSServe: JSServe, Server
+
+export main
+
+usage = """LarvaTagger.jl - launch the server-based GUI.
+
+Usage:
+  larvatagger-gui.jl <file-path> [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--manual-label=<label>]
+  larvatagger-gui.jl -h | --help
+
+Options:
+  -h --help               Show this screen.
+  -q --quiet              Do not show instructions.
+  --backends=<path>       Path to backend repository.
+  --port=<number>         Port number the server listens to.
+  --viewer                Disable editing capabilities.
+  --browser               Automatically open a browser tab at the served location.
+  --manual-label=<label>  Secondary label for manually labelled data [default: edited].
+
+Backends defined in LarvaTagger project root directory are automatically found. Other
+backend locations can be specified with the --backends argument.
+"""
+
+function main(args=ARGS; exit_on_error=false)
+    if isempty(args)
+        print(usage)
+        exit()
+    end
+
+    parsed_args = try
+        docopt(usage, args isa String ? split(args) : args;
+               help=false, exit_on_error=exit_on_error)
+    catch # for exit_on_error==false only
+        print("Parsing error -- ") # help message follows
+        true # docopt returns true if no input arguments are passed
+    end
+
+    if parsed_args === true || parsed_args["--help"]
+        print(usage)
+        exit()
+    end
+
+    verbose = !parsed_args["--quiet"]
+    infile = parsed_args["<file-path>"]
+    if !isfile(infile)
+        @error "File not found; did you specify a file path?" infile
+        exit()
+    end
+    if parsed_args["--viewer"]
+        app = larvaviewer(infile)
+    else
+        kwargs = Dict{Symbol, Any}()
+        backends = parsed_args["--backends"]
+        if !isnothing(backends)
+            kwargs[:backend_directory] = backends
+        end
+        kwargs[:manualtag] = string(parsed_args["--manual-label"])
+        app = larvaeditor(infile; kwargs...)
+    end
+    #
+    port = parsed_args["--port"]
+    port = isnothing(port) ? 9284 : parse(Int, port)
+    server = Server(app, "0.0.0.0", port)
+    if parsed_args["--browser"]
+        JSServe.openurl("http://127.0.0.1:$(port)")
+    elseif verbose
+        @info "The server is ready at http://127.0.0.1:$(port)"
+    end
+    if verbose
+        @info "Press Ctrl+D to stop the server (or Ctrl+C if in PowerShell)"
+    end
+    return server
+
+end
+
+end
diff --git a/src/cli_toolkit.jl b/src/cli_toolkit.jl
new file mode 100644
index 0000000000000000000000000000000000000000..62bd46e828f99c8d2399bcc0a090310cad536b7f
--- /dev/null
+++ b/src/cli_toolkit.jl
@@ -0,0 +1,304 @@
+module Toolkit
+
+using DocOpt
+using Pkg
+using OrderedCollections
+using PlanarLarvae.Datasets, PlanarLarvae.Formats
+
+include("Taggers.jl")
+using .Taggers
+
+export main
+
+usage = """Larva Tagger.
+
+Usage:
+  larvatagger-toolkit.jl import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>] [--pixelsize=<μm>] [--overrides=<comma-separated-list>] [--default-label=<label>] [--manual-label=<label>] [--decode]
+  larvatagger-toolkit.jl train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>]
+  larvatagger-toolkit.jl predict <backend-path> <model-instance> <data-path> [--make-dataset] [--skip-make-dataset] [--data-isolation]
+  larvatagger-toolkit.jl merge <input-path> <input-file> [<output-file>] [--manual-label=<label>] [--decode]
+  larvatagger-toolkit.jl -V | --version
+  larvatagger-toolkit.jl -h | --help
+
+Options:
+  -h --help            Show this screen.
+  -V --version         Show version.
+  --id=<id>            Run or assay ID, e.g. `date_time`.
+  --framerate=<fps>    Camera frame rate, in frames per second.
+  --pixelsize=<μm>     Camera pixel size, in micrometers.
+  --make-dataset       Perform the make_dataset stage prior to proceeding to predict_model.
+  --skip-make-dataset  Skip the make_dataset stage (default behavior since TaggingBackends==0.10).
+  --data-isolation     Isolate the backend data directories for parallel tagging of multiple data files.
+  --sample-size=<N>    Sample only N track segments from the data repository.
+  --layers=<N>         (MaggotUBA) Number of layers of the classifier.
+  --iterations=<N>     (MaggotUBA) Number of training iterations (can be two integers separated by a comma).
+  --decode             Do not encode the labels into integer indices.
+  --default-label=<label>             Label all untagged data as <label>.
+  --manual-label=<label>              Secondary label for manually labelled data [default: edited].
+  --labels=<comma-separated-list>     Comma-separated list of behavior tags/labels.
+  --class-weights=<csv>               Comma-separated list of floats.
+  --pretrained-model=<instance>       Name of the pretrained encoder (from `pretrained_models` registry).
+  --balancing-strategy=<strategy>     Any of `auto`, `maggotuba`, `none` [default: auto].
+  --overrides=<comma-separated-list>  Comma-separated list of key:value pairs.
+
+
+Commands:
+
+  import    Generate a label file and store metadata.
+
+    This command is particularly useful to specify missing information such as the camera
+    framerate for FIMTrack v2 table.csv files. A label file is created with a pointer to the
+    original csv file, and a metadata section with the provided information.
+    A second usage applies to label files specifically, and consists in extending the tracks
+    and timesteps on basis of the associated data dependencies and assigning a default label
+    to the data points that are not already defined in the label file.
+    Note: label files should always come as siblings of their data dependencies (located in
+    the same directory).
+
+  merge     Combine the content of label files.
+
+    <input-path> is the path to a manually edited label file whose content is to be augmented
+    with manual editions from a second (sibling) label file <input-file>. If no output file
+    is specified, the resulting content is dumped onto the standard output. All label files
+    must be siblings, i.e. located in the same directory.
+    If no editions are found in the second input file, all the defined labels are copied from
+    the second to the first file.
+
+  train     Train a tagger.
+
+    <data-path> can be a path to a file or directory.
+    --class-weights requires --labels to be defined and the specified comma-separated values
+    should match those given by --labels.
+
+  predict   Automatically label tracking data.
+
+    <data-path> can be a path to a file or directory.
+    <data-path> can also be a .txt file listing data files; one relative path per line.
+
+"""
+
+function main(args=ARGS; exit_on_error=true)
+    parsed_args = try
+        docopt(usage, args isa String ? split(args) : args;
+               help=false, exit_on_error=exit_on_error)
+    catch # for exit_on_error==false only
+        print("Parsing error -- ") # help message follows
+        Dict{String, Any}()
+    end
+
+    if isempty(parsed_args) || parsed_args["--help"]
+        print(usage)
+
+    elseif parsed_args["--version"]
+        println(Pkg.project().version)
+
+    elseif parsed_args["import"]
+        infile = parsed_args["<input-path>"]
+        if !isfile(infile)
+            @error "File not found; did you specify a file path after \"import\"?" infile
+            exit()
+        end
+        outfile = parsed_args["<output-file>"]
+        kwargs = Dict{Symbol, Any}()
+        framerate = parsed_args["--framerate"]
+        if !isnothing(framerate)
+            kwargs[:framerate] = parse(Float64, framerate)
+        end
+        pixelsize = parsed_args["--pixelsize"]
+        if !isnothing(pixelsize)
+            kwargs[:pixelsize] = parse(Float64, pixelsize)
+        end
+        file = load(infile; kwargs...)
+        #
+        metadata = file.run.attributes[:metadata]
+        overrides = parsed_args["--overrides"]
+        if !isnothing(overrides)
+            metadata[:overrides] = overrides′= Dict{String, Any}()
+            for override in split(overrides, ',')
+                key, value = split(override, ':'; limit=2)
+                try
+                    value = parse(Int, value)
+                catch
+                    try
+                        value = parse(Float64, value)
+                    catch
+                    end
+                end
+                overrides′[key] = value
+            end
+        end
+        #
+        runid = parsed_args["--id"]
+        run = Run(isnothing(runid) ? file.run.id : runid,
+                  file.run.attributes,
+                  isa(file, Formats.JSONLabels) ? file.run.tracks : OrderedDict{Datasets.TrackID, Track}())
+        if !haskey(run.attributes, :labels)
+            run.attributes[:labels] = String[]
+        end
+        if isa(file, Formats.JSONLabels)
+            for dep in getdependencies(file)
+                Datasets.pushdependency!(run, dep)
+            end
+        else
+            Datasets.pushdependency!(run, infile)
+        end
+        defaultlabel = parsed_args["--default-label"]
+        if !isnothing(defaultlabel)
+            appendlabel!(run, "edited"; ignore=[defaultlabel], ifempty=false)
+            setdefaultlabel!(run, defaultlabel; filepath=infile)
+        end
+        #
+        decode = parsed_args["--decode"]
+        if !decode
+            encodelabels!(run)
+        end
+        if isnothing(outfile)
+            Datasets.write_json(stdout, run)
+        else
+            if '/' in outfile || '\\' in outfile
+                @error "Output file name contains slashes or backslashes (should not be a path)" outfile
+                exit()
+            end
+            outfile = joinpath(dirname(infile), outfile)
+            Datasets.to_json_file(outfile, run)
+            Taggers.check_permissions(outfile)
+        end
+
+    elseif parsed_args["merge"]
+        input_path = parsed_args["<input-path>"]
+        if !isfile(input_path)
+            @error "File not found; did you specify a file path after \"combine\"?" input_path
+            exit()
+        end
+        input_file = parsed_args["<input-file>"]
+        if '/' in input_file || '\\' in input_file
+            @error "Second input file name contains slashes or backslashes (should not be a path)" input_file
+            exit()
+        end
+        second_input = joinpath(dirname(input_path), input_file)
+        if !isfile(second_input)
+            @error "File not found" second_input
+            exit()
+        end
+        #
+        first_input = load(input_path)
+        if !(first_input isa Formats.JSONLabels)
+            @error "Not a JSON label file" first_input
+            exit()
+        end
+        second_input = load(second_input)
+        if !(second_input isa Formats.JSONLabels)
+            @error "Not a JSON label file" second_input
+            exit()
+        end
+        #
+        run = first_input.run
+        run2 = second_input.run
+        if !shareddependencies(run, run2; extend=true)
+            exit()
+        end
+        #
+        attributes = run.attributes
+        attributes2 = run2.attributes
+        edited = parsed_args["--manual-label"]
+        if !(haskey(attributes, :secondarylabels) && edited ∈ attributes[:secondarylabels])
+            @debug "Cannot find manual editions" first_input=basename(first_input.source) edited_label=edited expected_among=get(attributes, :secondarylabels, String[])
+        end
+        if haskey(attributes2, :secondarylabels) && edited ∈ attributes2[:secondarylabels]
+            mergelabels!(run, run2) do labels
+                labels isa Vector && edited ∈ labels
+            end
+        else
+            @warn "Cannot find manual editions" second_input=basename(second_input.source) edited_label=edited expected_among=get(attributes2, :secondarylabels, String[])
+            mergelabels!(run, run2)
+        end
+        #
+        output = run
+        # in the case the first input file was automatically generated, remove the metadata
+        # used elsewhere to distinguish between automatically- and manually-assigned labels
+        metadata = output.attributes[:metadata]
+        if haskey(metadata, :software)
+            software = metadata[:software]
+            tagger = Datasets.coerce(eltype(keys(software)), :tagger)
+            if haskey(software, tagger)
+                delete!(software, tagger)
+                if isempty(software)
+                    delete!(metadata, :software)
+                end
+            end
+        end
+        #
+        decode = parsed_args["--decode"]
+        if !decode
+            encodelabels!(output)
+        end
+        #
+        outfile = parsed_args["<output-file>"]
+        if isnothing(outfile)
+            Datasets.write_json(stdout, output)
+        else
+            if '/' in outfile || '\\' in outfile
+                @error "Output file name contains slashes or backslashes (should not be a path)" outfile
+                exit()
+            end
+            outfile = joinpath(dirname(input_path), outfile)
+            Datasets.to_json_file(outfile, output)
+            Taggers.check_permissions(outfile)
+        end
+
+    elseif parsed_args["train"]
+        backend_path = parsed_args["<backend-path>"]
+        model_instance = parsed_args["<model-instance>"]
+        data_path = parsed_args["<data-path>"]
+        #
+        tagger = Tagger(backend_path, model_instance)
+        Taggers.reset(tagger)
+        Taggers.push(tagger, data_path)
+        kwargs = Dict{Symbol, Any}()
+        layers = parsed_args["--layers"]
+        isnothing(layers) || (kwargs[:layers] = layers)
+        iterations = parsed_args["--iterations"]
+        isnothing(iterations) || (kwargs[:iterations] = iterations)
+        classweights = parsed_args["--class-weights"]
+        isnothing(classweights) || (kwargs[:class_weights] = classweights)
+        train(tagger;
+              pretrained_instance=parsed_args["--pretrained-model"],
+              labels=parsed_args["--labels"],
+              sample_size=parsed_args["--sample-size"],
+              balancing_strategy=parsed_args["--balancing-strategy"],
+              include_all=parsed_args["--manual-label"],
+              kwargs...)
+
+    elseif parsed_args["predict"]
+        backend_path = parsed_args["<backend-path>"]
+        model_instance = parsed_args["<model-instance>"]
+        data_path = parsed_args["<data-path>"]
+        data_isolation = parsed_args["--data-isolation"]
+        #
+        datapath = abspath(data_path)
+        destination = if isfile(datapath)
+            if endswith(datapath, ".txt")
+                # assume that the file lists paths to data files to be processed;
+                # in this case, the path to the txt file is not relevant
+                pwd()
+            else
+                dirname(datapath)
+            end
+        else
+            datapath
+        end
+        #
+        tagger = Tagger(backend_path, model_instance)
+        if data_isolation
+            tagger = Taggers.isolate(tagger)
+            @info "Tagger isolated" sandbox=tagger.sandbox
+        end
+        resetdata(tagger)
+        Taggers.push(tagger, datapath)
+        predict(tagger; skip_make_dataset=parsed_args["--skip-make-dataset"],
+                make_dataset=parsed_args["--make-dataset"])
+        Taggers.pull(tagger, destination)
+    end
+end
+
+end