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