From 410abf266a7b08c06ac00c14e6e20d35162c9f51 Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Tue, 9 Jan 2024 16:22:12 -0800 Subject: [PATCH 01/12] Updates for pre-commit --- .gitignore | 1 + .pre-commit-config.yaml | 41 +- README.md | 23 +- bats_ai/core/fixtures/species.json | 840 ++++++++++++++--------------- dev/.env.docker-compose | 2 +- dev/django.Dockerfile | 24 +- dev/export-env.sh | 22 +- 7 files changed, 510 insertions(+), 443 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3fa33f..da8c6e0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,22 +1,61 @@ -files: ^(bats_ai)/ +exclude: | + (?x)( + ^.github/ | + ^terraform/ | + ^staticfiles/ | + ^client/ + ) repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: check-case-conflict - id: check-docstring-first + - id: check-executables-have-shebangs - id: check-json - id: check-merge-conflict + - id: check-shebang-scripts-are-executable - id: check-toml - id: check-yaml + args: ['--unsafe'] - id: debug-statements - id: destroyed-symlinks - id: detect-private-key - id: double-quote-string-fixer - id: end-of-file-fixer - id: mixed-line-ending + args: ['--fix=lf'] + - id: pretty-format-json + name: Format JSON files + args: ["--autofix"] - id: trailing-whitespace + - repo: https://github.com/hadolint/hadolint + rev: v2.12.1-beta + hooks: + - id: hadolint-docker + name: Hadolint for Dockerfiles + + - repo: https://github.com/lovesegfault/beautysh + rev: v6.2.1 + hooks: + - id: beautysh + name: Beautysh for Bash scripts + args: ["--indent-size", "4"] + + - repo: https://github.com/igorshubovych/markdownlint-cli + rev: v0.38.0 + hooks: + - id: markdownlint-fix-docker + name: MarkdownLint for Markdown files + args: ["--fix", "--disable", "MD013"] + + - repo: https://github.com/sirwart/ripsecrets + rev: v0.1.7 + hooks: + - id: ripsecrets + name: RIPSecrets for API keys and secrets + - repo: https://github.com/psf/black rev: 23.9.1 hooks: diff --git a/README.md b/README.md index 9b5871c..069b140 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,50 @@ # bats-ai ## Develop with Docker (recommended quickstart) + This is the simplest configuration for developers to start with. ### Initial Setup + 1. Run `docker compose run --rm django ./manage.py migrate` 2. Run `docker compose run --rm django ./manage.py createsuperuser` and follow the prompts to create your own user ### Run Application + 1. Run `docker compose up` -2. Access the site, starting at http://localhost:8000/admin/ +2. Access the site, starting at 3. When finished, use `Ctrl+C` ### Application Maintenance + Occasionally, new package dependencies or schema changes will necessitate maintenance. To non-destructively update your development stack at any time: + 1. Run `docker compose pull` 2. Run `docker compose build --pull --no-cache` 3. Run `docker compose run --rm django ./manage.py migrate` 3. Run `docker compose run --rm django ./manage.py createsuperuser` 4. Run `docker compose run --rm django ./manage.py loaddata species` to load species data into the database 5. Run `docker compose run --rm django ./manage.py makeclient --username your.super.user@email.address --uri http://localhost:3000/` + ## Develop Natively (advanced) + This configuration still uses Docker to run attached services in the background, but allows developers to run Python code on their native system. ### Dev Tool Endpoints + 1. Main Site Interface [http://localhost:3000/](http://localhost:3000/) 2. Site Administration [http://localhost:8000/admin/](http://localhost:8000/admin/) 3. Swagger API (These are default swagger endpoints using Django-REST) [http://localhost:8000/api/docs/swagger/](http://localhost:8000/api/docs/swagger/) 4. Django Ninja API [http://localhost:8000/api/v1/docs#/](http://localhost:8000/api/v1/docs#/) -5. MinIO (S3 local management) [http://localhost:9001/browser](http://localhost:9001/browser) +5. MinIO (S3 local management) [http://localhost:9001/browser](http://localhost:9001/browser) Username: 'minioAccessKey' Password: 'minioSecretKey' ### Initial Setup + 1. Run `docker compose -f ./docker-compose.yml up -d` 2. Install Python 3.10 3. Install @@ -47,7 +56,8 @@ but allows developers to run Python code on their native system. 8. Run `./manage.py createsuperuser` and follow the prompts to create your own user ### Run Application -1. Ensure `docker compose -f ./docker-compose.yml up -d` is still active + +1. Ensure `docker compose -f ./docker-compose.yml up -d` is still active 2. Run: 1. `source ./dev/export-env.sh` 2. `./manage.py runserver` @@ -58,15 +68,18 @@ but allows developers to run Python code on their native system. 5. To destroy the stack and start fresh, run `docker compose down -v` ## Remap Service Ports (optional) + Attached services may be exposed to the host system via alternative ports. Developers who work on multiple software projects concurrently may find this helpful to avoid port conflicts. To do so, before running any `docker compose` commands, set any of the environment variables: + * `DOCKER_POSTGRES_PORT` * `DOCKER_RABBITMQ_PORT` * `DOCKER_MINIO_PORT` The Django server must be informed about the changes: + * When running the "Develop with Docker" configuration, override the environment variables: * `DJANGO_MINIO_STORAGE_ENDPOINT`, using the port from `DOCKER_MINIO_PORT`. * When running the "Develop Natively" configuration, override the environment variables: @@ -78,7 +91,9 @@ Since most of Django's environment variables contain additional content, use the the appropriate `dev/.env.docker-compose*` file as a baseline for overrides. ## Testing + ### Initial Setup + tox is used to execute all tests. tox is installed automatically with the `dev` package extra. @@ -86,11 +101,13 @@ When running the "Develop with Docker" configuration, all tox commands must be r `docker-compose run --rm django tox`; extra arguments may also be appended to this form. ### Running Tests + Run `tox` to launch the full test suite. Individual test environments may be selectively run. This also allows additional options to be be added. Useful sub-commands include: + * `tox -e lint`: Run only the style checks * `tox -e type`: Run only the type checks * `tox -e test`: Run only the pytest-driven tests diff --git a/bats_ai/core/fixtures/species.json b/bats_ai/core/fixtures/species.json index 53f0208..a38a935 100644 --- a/bats_ai/core/fixtures/species.json +++ b/bats_ai/core/fixtures/species.json @@ -1,1010 +1,1010 @@ [ { - "model": "core.Species", - "pk": "67", "fields": { - "species_code": "25k", + "common_name": "Various species with pulses that have a minimum frequency of approximately 25 kHz.", "family": "", "genus": "", "species": "", - "common_name": "Various species with pulses that have a minimum frequency of approximately 25 kHz.", + "species_code": "25k", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "67" }, { - "model": "core.Species", - "pk": "68", "fields": { - "species_code": "40k", + "common_name": "Various species with pulses that have a minimum frequency in the range of 35-45 kHz.", "family": "", "genus": "", "species": "", - "common_name": "Various species with pulses that have a minimum frequency in the range of 35-45 kHz.", + "species_code": "40k", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "68" }, { - "model": "core.Species", - "pk": "65", "fields": { - "species_code": "NOISE", + "common_name": "Not a bat/Noise", "family": "", "genus": "", "species": "Not a bat", - "common_name": "Not a bat/Noise", + "species_code": "NOISE", "species_code_6": "NOTBAT" - } + }, + "model": "core.Species", + "pk": "65" }, { - "model": "core.Species", - "pk": "1", "fields": { - "species_code": "ANPA", + "common_name": "Pallid bat", "family": "Vespertilionidae", "genus": "Antrozous", "species": "Antrozous pallidus", - "common_name": "Pallid bat", + "species_code": "ANPA", "species_code_6": "ANTPAL" - } + }, + "model": "core.Species", + "pk": "1" }, { - "model": "core.Species", - "pk": "55", "fields": { - "species_code": "ANPAEPFU", + "common_name": "Pallid bat/Big brown bat", "family": "", "genus": "", "species": "Antrozous pallidus/Eptesicus fuscus", - "common_name": "Pallid bat/Big brown bat", + "species_code": "ANPAEPFU", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "55" }, { - "model": "core.Species", - "pk": "45", "fields": { - "species_code": "ARJA", + "common_name": "Jamaican fruit-eating bat", "family": "Phyllostomidae", "genus": "Artibeus", "species": "Artibeus jamaicensis", - "common_name": "Jamaican fruit-eating bat", + "species_code": "ARJA", "species_code_6": "ARTJAM" - } + }, + "model": "core.Species", + "pk": "45" }, { - "model": "core.Species", - "pk": "46", "fields": { - "species_code": "BRCA", + "common_name": "Antillean fruit-eating bat", "family": "Phyllostomidae", "genus": "Brachyphylla", "species": "Brachyphylla cavernarum", - "common_name": "Antillean fruit-eating bat", + "species_code": "BRCA", "species_code_6": "BRACAV" - } + }, + "model": "core.Species", + "pk": "46" }, { - "model": "core.Species", - "pk": "2", "fields": { - "species_code": "CHME", + "common_name": "Mexican long-tongued bat", "family": "Phyllostomidae", "genus": "Choeronycteris", "species": "Choeronycteris mexicana", - "common_name": "Mexican long-tongued bat", + "species_code": "CHME", "species_code_6": "CHOMEX" - } + }, + "model": "core.Species", + "pk": "2" }, { - "model": "core.Species", - "pk": "3", "fields": { - "species_code": "CORA", + "common_name": "Rafinesque's big-eared bat", "family": "Vespertilionidae", "genus": "Corynorhinus", "species": "Corynorhinus rafinesquii", - "common_name": "Rafinesque's big-eared bat", + "species_code": "CORA", "species_code_6": "CORRAF" - } + }, + "model": "core.Species", + "pk": "3" }, { - "model": "core.Species", - "pk": "47", "fields": { - "species_code": "DIEC", + "common_name": "Hairy-legged vampire bat", "family": "Phyllostomidae", "genus": "Diphylla", "species": "Diphylla ecaudata", - "common_name": "Hairy-legged vampire bat", + "species_code": "DIEC", "species_code_6": "DIPECA" - } + }, + "model": "core.Species", + "pk": "47" }, { - "model": "core.Species", - "pk": "5", "fields": { - "species_code": "EPFU", + "common_name": "Big brown bat", "family": "Vespertilionidae", "genus": "Eptesicus", "species": "Eptesicus fuscus", - "common_name": "Big brown bat", + "species_code": "EPFU", "species_code_6": "EPTFUS" - } + }, + "model": "core.Species", + "pk": "5" }, { - "model": "core.Species", - "pk": "82", "fields": { - "species_code": "EPFULABO", + "common_name": "Big brown bat/Eastern red bat", "family": "", "genus": "", "species": "Eptesicus fuscus/Lasiurus borealis", - "common_name": "Big brown bat/Eastern red bat", + "species_code": "EPFULABO", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "82" }, { - "model": "core.Species", - "pk": "56", "fields": { - "species_code": "EPFULANO", + "common_name": "Big brown bat/Silver-haired bat", "family": "", "genus": "", "species": "Eptesicus fuscus/Lasionycteris noctivagans", - "common_name": "Big brown bat/Silver-haired bat", + "species_code": "EPFULANO", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "56" }, { - "model": "core.Species", - "pk": "85", "fields": { - "species_code": "EPFUMYLU", + "common_name": "Big brown bat/Little brown bat", "family": "", "genus": "Eptesicus/Myotis", "species": "Eptesicus fuscus/Myotis lucifugus", - "common_name": "Big brown bat/Little brown bat", + "species_code": "EPFUMYLU", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "85" }, { - "model": "core.Species", - "pk": "7", "fields": { - "species_code": "EUFL", + "common_name": "Florida bonneted bat", "family": "Molossidae", "genus": "Eumops", "species": "Eumops floridanus", - "common_name": "Florida bonneted bat", + "species_code": "EUFL", "species_code_6": "EUMFLO" - } + }, + "model": "core.Species", + "pk": "7" }, { - "model": "core.Species", - "pk": "6", "fields": { - "species_code": "EUMA", + "common_name": "Spotted bat", "family": "Vespertilionidae", "genus": "Euderma", "species": "Euderma maculatum", - "common_name": "Spotted bat", + "species_code": "EUMA", "species_code_6": "EUDMAC" - } + }, + "model": "core.Species", + "pk": "6" }, { - "model": "core.Species", - "pk": "8", "fields": { - "species_code": "EUPE", + "common_name": "Greater mastiff bat", "family": "Molossidae", "genus": "Eumops", "species": "Eumops perotis", - "common_name": "Greater mastiff bat", + "species_code": "EUPE", "species_code_6": "EUMPER" - } + }, + "model": "core.Species", + "pk": "8" }, { - "model": "core.Species", - "pk": "9", "fields": { - "species_code": "EUUN", + "common_name": "Underwood's mastiff bat", "family": "Molossidae", "genus": "Eumops", "species": "Eumops underwoodi", - "common_name": "Underwood's mastiff bat", + "species_code": "EUUN", "species_code_6": "EUMUND" - } + }, + "model": "core.Species", + "pk": "9" }, { - "model": "core.Species", - "pk": "71", "fields": { - "species_code": "HighF", + "common_name": "Various species with pulses having a minimum frequency higher than 30 kHz.", "family": "", "genus": "", "species": "", - "common_name": "Various species with pulses having a minimum frequency higher than 30 kHz.", + "species_code": "HighF", "species_code_6": "HiF" - } + }, + "model": "core.Species", + "pk": "71" }, { - "model": "core.Species", - "pk": "10", "fields": { - "species_code": "IDPH", + "common_name": "Allen's big-eared bat", "family": "Vespertilionidae", "genus": "Idionycteris", "species": "Idionycteris phyllotis", - "common_name": "Allen's big-eared bat", + "species_code": "IDPH", "species_code_6": "IDIPHY" - } + }, + "model": "core.Species", + "pk": "10" }, { - "model": "core.Species", - "pk": "12", "fields": { - "species_code": "LABL", + "common_name": "Western red bat", "family": "Vespertilionidae", "genus": "Lasiurus", "species": "Lasiurus blossevillii", - "common_name": "Western red bat", + "species_code": "LABL", "species_code_6": "LASBLO" - } + }, + "model": "core.Species", + "pk": "12" }, { - "model": "core.Species", - "pk": "57", "fields": { - "species_code": "LABLPAHE", + "common_name": "Western red bat/Canyon bat", "family": "", "genus": "", "species": "Lasiurus blossevillii/Parastrellus hesperus", - "common_name": "Western red bat/Canyon bat", + "species_code": "LABLPAHE", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "57" }, { - "model": "core.Species", - "pk": "13", "fields": { - "species_code": "LABO", + "common_name": "Eastern red bat", "family": "Vespertilionidae", "genus": "Lasiurus", "species": "Lasiurus borealis", - "common_name": "Eastern red bat", + "species_code": "LABO", "species_code_6": "LASBOR" - } + }, + "model": "core.Species", + "pk": "13" }, { - "model": "core.Species", - "pk": "60", "fields": { - "species_code": "LABOLASE", + "common_name": "Eastern red bat/Seminole bat", "family": "", "genus": "", "species": "Lasiurus borealis/Lasiurus seminolus", - "common_name": "Eastern red bat/Seminole bat", + "species_code": "LABOLASE", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "60" }, { - "model": "core.Species", - "pk": "59", "fields": { - "species_code": "LABOMYLU", + "common_name": "Eastern red bat/Little brown bat", "family": "", "genus": "", "species": "Lasiurus borealis/Myotis lucifugus", - "common_name": "Eastern red bat/Little brown bat", + "species_code": "LABOMYLU", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "59" }, { - "model": "core.Species", - "pk": "83", "fields": { - "species_code": "LABONYHU", + "common_name": "Eastern red bat/Evening bat", "family": "", "genus": "", "species": "Lasiurus borealis/Nycticeius humeralis", - "common_name": "Eastern red bat/Evening bat", + "species_code": "LABONYHU", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "83" }, { - "model": "core.Species", - "pk": "58", "fields": { - "species_code": "LABOPESU", + "common_name": "Eastern red bat/Tricolored bat", "family": "", "genus": "", "species": "Lasiurus borealis/Perimyotis subflavus", - "common_name": "Eastern red bat/Tricolored bat", + "species_code": "LABOPESU", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "58" }, { - "model": "core.Species", - "pk": "14", "fields": { - "species_code": "LACI", + "common_name": "Hoary bat", "family": "Vespertilionidae", "genus": "Lasiurus", "species": "Lasiurus cinereus", - "common_name": "Hoary bat", + "species_code": "LACI", "species_code_6": "LASCIN" - } + }, + "model": "core.Species", + "pk": "14" }, { - "model": "core.Species", - "pk": "72", "fields": { - "species_code": "LACILANO", + "common_name": "Hoary bat/Silver-haired bat", "family": "", "genus": "", "species": "Lasiurus cinereus/Lasionycteris noctivagans", - "common_name": "Hoary bat/Silver-haired bat", + "species_code": "LACILANO", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "72" }, { - "model": "core.Species", - "pk": "62", "fields": { - "species_code": "LACITABR", + "common_name": "Hoary bat/Mexican free-tailed bat", "family": "", "genus": "", "species": "Lasiurus cinereus/Tadarida brasiliensis", - "common_name": "Hoary bat/Mexican free-tailed bat", + "species_code": "LACITABR", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "62" }, { - "model": "core.Species", - "pk": "15", "fields": { - "species_code": "LAEG", + "common_name": "Southern yellow bat", "family": "Vespertilionidae", "genus": "Lasiurus", "species": "Lasiurus ega", - "common_name": "Southern yellow bat", + "species_code": "LAEG", "species_code_6": "LASEGA" - } + }, + "model": "core.Species", + "pk": "15" }, { - "model": "core.Species", - "pk": "16", "fields": { - "species_code": "LAIN", + "common_name": "Northern yellow bat", "family": "Vespertilionidae", "genus": "Lasiurus", "species": "Lasiurus intermedius", - "common_name": "Northern yellow bat", + "species_code": "LAIN", "species_code_6": "LASINT" - } + }, + "model": "core.Species", + "pk": "16" }, { - "model": "core.Species", - "pk": "48", "fields": { - "species_code": "LAMI", + "common_name": "Minor red bat", "family": "Vespertilionidae", "genus": "Lasiurus", "species": "Lasiurus minor", - "common_name": "Minor red bat", + "species_code": "LAMI", "species_code_6": "LASMIN" - } + }, + "model": "core.Species", + "pk": "48" }, { - "model": "core.Species", - "pk": "11", "fields": { - "species_code": "LANO", + "common_name": "Silver-haired bat", "family": "Vespertilionidae", "genus": "Lasionycteris", "species": "Lasionycteris noctivagans", - "common_name": "Silver-haired bat", + "species_code": "LANO", "species_code_6": "LASNOC" - } + }, + "model": "core.Species", + "pk": "11" }, { - "model": "core.Species", - "pk": "61", "fields": { - "species_code": "LANOTABR", + "common_name": "Silver-haired bat/Mexican free-tailed bat", "family": "", "genus": "", "species": "Lasiurus noctivagans/Tadarida brasiliensis", - "common_name": "Silver-haired bat/Mexican free-tailed bat", + "species_code": "LANOTABR", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "61" }, { - "model": "core.Species", - "pk": "17", "fields": { - "species_code": "LASE", + "common_name": "Seminole bat", "family": "Vespertilionidae", "genus": "Lasiurus", "species": "Lasiurus seminolus", - "common_name": "Seminole bat", + "species_code": "LASE", "species_code_6": "LASSEM" - } + }, + "model": "core.Species", + "pk": "17" }, { - "model": "core.Species", - "pk": "18", "fields": { - "species_code": "LAXA", + "common_name": "Western yellow bat", "family": "Vespertilionidae", "genus": "Lasiurus", "species": "Lasiurus xanthinus", - "common_name": "Western yellow bat", + "species_code": "LAXA", "species_code_6": "LASXAN" - } + }, + "model": "core.Species", + "pk": "18" }, { - "model": "core.Species", - "pk": "19", "fields": { - "species_code": "LENI", + "common_name": "Greater long-nosed bat", "family": "Phyllostomidae", "genus": "Leptonycteris", "species": "Leptonycteris nivalis", - "common_name": "Greater long-nosed bat", + "species_code": "LENI", "species_code_6": "LEPNIV" - } + }, + "model": "core.Species", + "pk": "19" }, { - "model": "core.Species", - "pk": "74", "fields": { - "species_code": "LESP", + "common_name": "Unidentifiable species of the Leptonycteris genus", "family": "", "genus": "Leptonycteris", "species": "", - "common_name": "Unidentifiable species of the Leptonycteris genus", + "species_code": "LESP", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "74" }, { - "model": "core.Species", - "pk": "20", "fields": { - "species_code": "LEYE", + "common_name": "Lesser long-nosed bat", "family": "Phyllostomidae", "genus": "Leptonycteris", "species": "Leptonycteris yerbabuenae", - "common_name": "Lesser long-nosed bat", + "species_code": "LEYE", "species_code_6": "LEPYER" - } + }, + "model": "core.Species", + "pk": "20" }, { - "model": "core.Species", - "pk": "70", "fields": { - "species_code": "LowF", + "common_name": "Various species with pulses having a minimum frequency lower than 30 kHz.", "family": "", "genus": "", "species": "", - "common_name": "Various species with pulses having a minimum frequency lower than 30 kHz.", + "species_code": "LowF", "species_code_6": "LoF" - } + }, + "model": "core.Species", + "pk": "70" }, { - "model": "core.Species", - "pk": "75", "fields": { - "species_code": "LUSO", + "common_name": "Little brown bat/Indiana bat", "family": "", "genus": "Myotis", "species": "Myotis lucifugus/Myotis sodalis", - "common_name": "Little brown bat/Indiana bat", + "species_code": "LUSO", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "75" }, { - "model": "core.Species", - "pk": "21", "fields": { - "species_code": "MACA", + "common_name": "California leaf-nosed bat", "family": "Phyllostomidae", "genus": "Macrotus", "species": "Macrotus californicus", - "common_name": "California leaf-nosed bat", + "species_code": "MACA", "species_code_6": "MACCAL" - } + }, + "model": "core.Species", + "pk": "21" }, { - "model": "core.Species", - "pk": "23", "fields": { - "species_code": "MOME", + "common_name": "Ghost-faced bat", "family": "Mormoopidae", "genus": "Mormoops", "species": "Mormoops megalophylla", - "common_name": "Ghost-faced bat", + "species_code": "MOME", "species_code_6": "MORMEG" - } + }, + "model": "core.Species", + "pk": "23" }, { - "model": "core.Species", - "pk": "22", "fields": { - "species_code": "MOMO", + "common_name": "Pallas's mastiff bat", "family": "Molossidae", "genus": "Molossus", "species": "Molossus molossus", - "common_name": "Pallas's mastiff bat", + "species_code": "MOMO", "species_code_6": "MOLMOL" - } + }, + "model": "core.Species", + "pk": "22" }, { - "model": "core.Species", - "pk": "29", "fields": { - "species_code": "MYGR", + "common_name": "Gray bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis grisescens", - "common_name": "Gray bat", + "species_code": "MYGR", "species_code_6": "MYOGRI" - } + }, + "model": "core.Species", + "pk": "29" }, { - "model": "core.Species", - "pk": "32", "fields": { - "species_code": "MYLU", + "common_name": "Little brown bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis lucifugus", - "common_name": "Little brown bat", + "species_code": "MYLU", "species_code_6": "MYOLUC" - } + }, + "model": "core.Species", + "pk": "32" }, { - "model": "core.Species", - "pk": "84", "fields": { - "species_code": "MYLUMYSE", + "common_name": "Little brown bat/Northern long-eared bat", "family": "", "genus": "", "species": "Myotis lucifugus/Myotis septentrionalis", - "common_name": "Little brown bat/Northern long-eared bat", + "species_code": "MYLUMYSE", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "84" }, { - "model": "core.Species", - "pk": "33", "fields": { - "species_code": "MYSE", + "common_name": "Northern long-eared bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis septentrionalis", - "common_name": "Northern long-eared bat", + "species_code": "MYSE", "species_code_6": "MYOSEP" - } + }, + "model": "core.Species", + "pk": "33" }, { - "model": "core.Species", - "pk": "34", "fields": { - "species_code": "MYSO", + "common_name": "Indiana bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis sodalis", - "common_name": "Indiana bat", + "species_code": "MYSO", "species_code_6": "MYOSOD" - } + }, + "model": "core.Species", + "pk": "34" }, { - "model": "core.Species", - "pk": "24", "fields": { - "species_code": "MYAR", + "common_name": "Southwestern bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis auriculus", - "common_name": "Southwestern bat", + "species_code": "MYAR", "species_code_6": "MYOAUR" - } + }, + "model": "core.Species", + "pk": "24" }, { - "model": "core.Species", - "pk": "69", "fields": { - "species_code": "MYSP", + "common_name": "General Myotis Spcies Code", "family": "Vespertilionidae", "genus": "Myotis", "species": "", - "common_name": "General Myotis Spcies Code", + "species_code": "MYSP", "species_code_6": "40kMyo" - } + }, + "model": "core.Species", + "pk": "69" }, { - "model": "core.Species", - "pk": "25", "fields": { - "species_code": "MYAU", + "common_name": "Southeastern bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis austroriparius", - "common_name": "Southeastern bat", + "species_code": "MYAU", "species_code_6": "MYOAUS" - } + }, + "model": "core.Species", + "pk": "25" }, { - "model": "core.Species", - "pk": "26", "fields": { - "species_code": "MYCA", + "common_name": "California bat ", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis californicus", - "common_name": "California bat ", + "species_code": "MYCA", "species_code_6": "MYOCAL" - } + }, + "model": "core.Species", + "pk": "26" }, { - "model": "core.Species", - "pk": "4", "fields": { - "species_code": "COTO", + "common_name": "Townsend's big-eared bat", "family": "Vespertilionidae", "genus": "Corynorhinus", "species": "Corynorhinus townsendii", - "common_name": "Townsend's big-eared bat", + "species_code": "COTO", "species_code_6": "CORTOW" - } + }, + "model": "core.Species", + "pk": "4" }, { - "model": "core.Species", - "pk": "86", "fields": { - "species_code": "COTV", + "common_name": "Virginia big-eared bat", "family": "Vespertilionidae", "genus": "Corynorhinus", "species": "Corynorhinus townsendii virginianus", - "common_name": "Virginia big-eared bat", + "species_code": "COTV", "species_code_6": "COTOVI" - } + }, + "model": "core.Species", + "pk": "86" }, { - "model": "core.Species", - "pk": "66", "fields": { - "species_code": "NoID", + "common_name": "Bat but no grouping or user-defined category applies", "family": "", "genus": "", "species": "", - "common_name": "Bat but no grouping or user-defined category applies", + "species_code": "NoID", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "66" }, { - "model": "core.Species", - "pk": "53", "fields": { - "species_code": "NOLE", + "common_name": "Greater bulldog bat", "family": "Noctilionidae", "genus": "Noctilio", "species": "Noctilio leporinus", - "common_name": "Greater bulldog bat", + "species_code": "NOLE", "species_code_6": "NOCLEP" - } + }, + "model": "core.Species", + "pk": "53" }, { - "model": "core.Species", - "pk": "40", "fields": { - "species_code": "NYFE", + "common_name": "Pocketed free-tailed bat", "family": "Molossidae", "genus": "Nyctinomops", "species": "Nyctinomops femorosaccus", - "common_name": "Pocketed free-tailed bat", + "species_code": "NYFE", "species_code_6": "NYCFEM" - } + }, + "model": "core.Species", + "pk": "40" }, { - "model": "core.Species", - "pk": "39", "fields": { - "species_code": "NYHU", + "common_name": "Evening bat", "family": "Vespertilionidae", "genus": "Nycticeius", "species": "Nycticeius humeralis", - "common_name": "Evening bat", + "species_code": "NYHU", "species_code_6": "NYCHUM" - } + }, + "model": "core.Species", + "pk": "39" }, { - "model": "core.Species", - "pk": "41", "fields": { - "species_code": "NYMA", + "common_name": "Big free-tailed bat", "family": "Molossidae", "genus": "Nyctinomops", "species": "Nyctinomops macrotis", - "common_name": "Big free-tailed bat", + "species_code": "NYMA", "species_code_6": "NYCMAC" - } + }, + "model": "core.Species", + "pk": "41" }, { - "model": "core.Species", - "pk": "42", "fields": { - "species_code": "PAHE", + "common_name": "Canyon bat", "family": "Vespertilionidae", "genus": "Parastrellus", "species": "Parastrellus hesperus", - "common_name": "Canyon bat", + "species_code": "PAHE", "species_code_6": "PARHES" - } + }, + "model": "core.Species", + "pk": "42" }, { - "model": "core.Species", - "pk": "43", "fields": { - "species_code": "PESU", + "common_name": "Tricolored bat", "family": "Vespertilionidae", "genus": "Perimyotis", "species": "Perimyotis subflavus", - "common_name": "Tricolored bat", + "species_code": "PESU", "species_code_6": "PERSUB" - } + }, + "model": "core.Species", + "pk": "43" }, { - "model": "core.Species", - "pk": "54", "fields": { - "species_code": "STRU", + "common_name": "Red fruit bat", "family": "Phyllostomidae", "genus": "Stenoderma", "species": "Stenoderma rufum", - "common_name": "Red fruit bat", + "species_code": "STRU", "species_code_6": "STERUF" - } + }, + "model": "core.Species", + "pk": "54" }, { - "model": "core.Species", - "pk": "44", "fields": { - "species_code": "TABR", + "common_name": "Brazilian free-tailed bat", "family": "Molossidae", "genus": "Tadarida", "species": "Tadarida brasiliensis", - "common_name": "Brazilian free-tailed bat", + "species_code": "TABR", "species_code_6": "TADBRA" - } + }, + "model": "core.Species", + "pk": "44" }, { - "model": "core.Species", - "pk": "63", "fields": { - "species_code": "LEMY", + "common_name": "Western long-eared myotis/Keen's myotis/Northern long-eared bat", "family": "", "genus": "", "species": "Myotis evotis/Myotis septentrionalis", - "common_name": "Western long-eared myotis/Keen's myotis/Northern long-eared bat", + "species_code": "LEMY", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "63" }, { - "model": "core.Species", - "pk": "89", "fields": { - "species_code": "NoBat", + "common_name": "Code used for surveys that recorded 0 bats", "family": "", "genus": "", "species": "", - "common_name": "Code used for surveys that recorded 0 bats", + "species_code": "NoBat", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "89" }, { - "model": "core.Species", - "pk": "78", "fields": { - "species_code": "MYCAMYCI", + "common_name": "California bat/Western small-footed bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis californicus/Myotis ciliolabrum", - "common_name": "California bat/Western small-footed bat", + "species_code": "MYCAMYCI", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "78" }, { - "model": "core.Species", - "pk": "64", "fields": { - "species_code": "MYCAMYYU", + "common_name": "California bat/Yuma bat", "family": "", "genus": "", "species": "Myotis californicus/Myotis yumanensis", - "common_name": "California bat/Yuma bat", + "species_code": "MYCAMYYU", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "64" }, { - "model": "core.Species", - "pk": "27", "fields": { - "species_code": "MYCI", + "common_name": "Western small-footed bat ", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis ciliolabrum", - "common_name": "Western small-footed bat ", + "species_code": "MYCI", "species_code_6": "MYOCIL" - } + }, + "model": "core.Species", + "pk": "27" }, { - "model": "core.Species", - "pk": "73", "fields": { - "species_code": "MYCIMYVO", + "common_name": "Western small-footed bat/Long-legged bat", "family": "", "genus": "", "species": "Myotis ciliolabrum/Myotis volans", - "common_name": "Western small-footed bat/Long-legged bat", + "species_code": "MYCIMYVO", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "73" }, { - "model": "core.Species", - "pk": "28", "fields": { - "species_code": "MYEV", + "common_name": "Western long-eared bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis evotis", - "common_name": "Western long-eared bat", + "species_code": "MYEV", "species_code_6": "MYOEVO" - } + }, + "model": "core.Species", + "pk": "28" }, { - "model": "core.Species", - "pk": "80", "fields": { - "species_code": "MYEVMYTH", + "common_name": "Western long-eared bat/Fringed bat", "family": "", "genus": "Myotis", "species": "Myotis evotis/Myotis thysanodes", - "common_name": "Western long-eared bat/Fringed bat", + "species_code": "MYEVMYTH", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "80" }, { - "model": "core.Species", - "pk": "31", "fields": { - "species_code": "MYLE", + "common_name": "Eastern small-footed bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis leibii", - "common_name": "Eastern small-footed bat", + "species_code": "MYLE", "species_code_6": "MYOLEI" - } + }, + "model": "core.Species", + "pk": "31" }, { - "model": "core.Species", - "pk": "81", "fields": { - "species_code": "MYLUMYCI", + "common_name": "Little brown bat/Western small-footed bat", "family": "", "genus": "Myotis", "species": "Myotis lucifugus/Myotis ciliolabrum", - "common_name": "Little brown bat/Western small-footed bat", + "species_code": "MYLUMYCI", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "81" }, { - "model": "core.Species", - "pk": "79", "fields": { - "species_code": "MYLUMYVO", + "common_name": "Little brown bat/Long-legged bat", "family": "", "genus": "Myotis", "species": "Myotis lucifugus/Myotis volans", - "common_name": "Little brown bat/Long-legged bat", + "species_code": "MYLUMYVO", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "79" }, { - "model": "core.Species", - "pk": "52", "fields": { - "species_code": "MYOC", + "common_name": "Arizona bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis occultus", - "common_name": "Arizona bat", + "species_code": "MYOC", "species_code_6": "MYOOCC" - } + }, + "model": "core.Species", + "pk": "52" }, { - "model": "core.Species", - "pk": "35", "fields": { - "species_code": "MYTH", + "common_name": "Fringed bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis thysanodes", - "common_name": "Fringed bat", + "species_code": "MYTH", "species_code_6": "MYOTHY" - } + }, + "model": "core.Species", + "pk": "35" }, { - "model": "core.Species", - "pk": "36", "fields": { - "species_code": "MYVE", + "common_name": "Cave bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis velifer", - "common_name": "Cave bat", + "species_code": "MYVE", "species_code_6": "MYOVEL" - } + }, + "model": "core.Species", + "pk": "36" }, { - "model": "core.Species", - "pk": "37", "fields": { - "species_code": "MYVO", + "common_name": "Long-legged bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis volans", - "common_name": "Long-legged bat", + "species_code": "MYVO", "species_code_6": "MYOVOL" - } + }, + "model": "core.Species", + "pk": "37" }, { - "model": "core.Species", - "pk": "38", "fields": { - "species_code": "MYYU", + "common_name": "Yuma bat ", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis yumanensis", - "common_name": "Yuma bat ", + "species_code": "MYYU", "species_code_6": "MYOYUM" - } + }, + "model": "core.Species", + "pk": "38" }, { - "model": "core.Species", - "pk": "88", "fields": { - "species_code": "MYYUMYLU", + "common_name": "Yuma bat/Little brown bat", "family": "Vespertilionidae", "genus": "Myotis", "species": "Myotis yumanensis/Myotis lucifugus", - "common_name": "Yuma bat/Little brown bat", + "species_code": "MYYUMYLU", "species_code_6": "" - } + }, + "model": "core.Species", + "pk": "88" }, { - "model": "core.Species", - "pk": "90", "fields": { - "species_code": "COTI", + "common_name": "Ozark big-eared bat", "family": "Vespertilionidae", "genus": "Corynorhinus", "species": "Corynorhinus townsendii ingens", - "common_name": "Ozark big-eared bat", + "species_code": "COTI", "species_code_6": "COTOIN" - } + }, + "model": "core.Species", + "pk": "90" }, { - "model": "core.Species", - "pk": "77", "fields": { - "species_code": "NYSP", + "common_name": "Unidentifiable species of the Nyctinomops genus", "family": "Molossidae", "genus": "Nyctinomops", "species": "", - "common_name": "Unidentifiable species of the Nyctinomops genus", + "species_code": "NYSP", "species_code_6": "NYCSPP" - } + }, + "model": "core.Species", + "pk": "77" } ] diff --git a/dev/.env.docker-compose b/dev/.env.docker-compose index 1e26fae..be89c27 100644 --- a/dev/.env.docker-compose +++ b/dev/.env.docker-compose @@ -6,4 +6,4 @@ DJANGO_MINIO_STORAGE_ACCESS_KEY=minioAccessKey DJANGO_MINIO_STORAGE_SECRET_KEY=minioSecretKey DJANGO_STORAGE_BUCKET_NAME=django-storage DJANGO_MINIO_STORAGE_ENDPOINT=minio:9000 -DJANGO_CORS_ORIGIN_WHITELIST=http://localhost:3000 \ No newline at end of file +DJANGO_CORS_ORIGIN_WHITELIST=http://localhost:3000 diff --git a/dev/django.Dockerfile b/dev/django.Dockerfile index 4e2723d..7afea88 100644 --- a/dev/django.Dockerfile +++ b/dev/django.Dockerfile @@ -1,20 +1,30 @@ FROM python:3.10-slim -# Install system libraries for Python packages: -# * psycopg2 -RUN apt-get update && \ - apt-get install --no-install-recommends --yes \ - libpq-dev gcc libc6-dev libgdal32 && \ - rm -rf /var/lib/apt/lists/* ENV PYTHONDONTWRITEBYTECODE 1 + ENV PYTHONUNBUFFERED 1 +# Install system libraries for Python packages: +# * psycopg2 +# hadolint ignore=DL3008 +RUN set -ex \ + && apt-get update \ + && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + gcc \ + libc6-dev \ + libgdal32 \ + libpq-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + # Only copy the setup.py, it will still force all install_requires to be installed, # but find_packages() will find nothing (which is fine). When Docker Compose mounts the real source # over top of this directory, the .egg-link in site-packages resolves to the mounted directory # and all package modules are importable. COPY ./setup.py /opt/django-project/setup.py -RUN pip install --editable /opt/django-project[dev] # Use a directory name which will never be an import name, as isort considers this as first-party. WORKDIR /opt/django-project + +RUN set -ex \ + && pip install --no-cache-dir -e .[dev] diff --git a/dev/export-env.sh b/dev/export-env.sh index ee0e3b0..6ffbeba 100644 --- a/dev/export-env.sh +++ b/dev/export-env.sh @@ -3,18 +3,18 @@ # This file must be sourced, not run. if [ -n "$1" ]; then - # If an argument was provided, use it as the .env file - _dotenv_file="$1" + # If an argument was provided, use it as the .env file + _dotenv_file="$1" else - # Otherwise, use the default .env file - if [ -n "$ZSH_VERSION" ]; then - # ZSH has a different way to get the directory of the current script - _dotenv_dir="$0:A:h" - else - # Assume this is Bash - _dotenv_dir="$( dirname "${BASH_SOURCE[0]}" )" - fi - _dotenv_file="${_dotenv_dir}/.env.docker-compose-native" + # Otherwise, use the default .env file + if [ -n "$ZSH_VERSION" ]; then + # ZSH has a different way to get the directory of the current script + _dotenv_dir="$0:A:h" + else + # Assume this is Bash + _dotenv_dir="$( dirname "${BASH_SOURCE[0]}" )" + fi + _dotenv_file="${_dotenv_dir}/.env.docker-compose-native" fi # Export all assignments in the $_dotenv_file From 36a1fd4775806f6637394d4eea9e6216491d2f2c Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Wed, 10 Jan 2024 10:42:58 -0800 Subject: [PATCH 02/12] Merge changes after rebase --- .pre-commit-config.yaml | 2 +- README.md | 88 +++++++++++++++++++++++++++------ bats_ai/api.py | 23 +++++---- bats_ai/core/views/recording.py | 28 +++-------- client/README.md | 19 +++++-- client/yarn.lock | 11 +++-- docker-compose.override.yml | 4 +- 7 files changed, 119 insertions(+), 56 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index da8c6e0..e7d0be1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,7 +48,7 @@ repos: hooks: - id: markdownlint-fix-docker name: MarkdownLint for Markdown files - args: ["--fix", "--disable", "MD013"] + args: ["--fix"] - repo: https://github.com/sirwart/ripsecrets rev: v0.1.7 diff --git a/README.md b/README.md index 069b140..a1ebfd8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,9 @@ This is the simplest configuration for developers to start with. 1. Run `docker compose run --rm django ./manage.py migrate` 2. Run `docker compose run --rm django ./manage.py createsuperuser` and follow the prompts to create your own user +3. Run `docker compose run --rm django ./manage.py makeclient \ + --username your.super.user@email.address \ + --uri http://localhost:3000/` ### Run Application @@ -24,9 +27,12 @@ maintenance. To non-destructively update your development stack at any time: 1. Run `docker compose pull` 2. Run `docker compose build --pull --no-cache` 3. Run `docker compose run --rm django ./manage.py migrate` -3. Run `docker compose run --rm django ./manage.py createsuperuser` -4. Run `docker compose run --rm django ./manage.py loaddata species` to load species data into the database -5. Run `docker compose run --rm django ./manage.py makeclient --username your.super.user@email.address --uri http://localhost:3000/` +4. Run `docker compose run --rm django ./manage.py createsuperuser` +5. Run `docker compose run --rm django ./manage.py loaddata species` to load species + data into the database +6. Run `docker compose run --rm django ./manage.py makeclient \ + --username your.super.user@email.address \ + --uri http://localhost:3000/` ## Develop Natively (advanced) @@ -43,7 +49,7 @@ but allows developers to run Python code on their native system. Username: 'minioAccessKey' Password: 'minioSecretKey' -### Initial Setup +### Initial Setup (Natively) 1. Run `docker compose -f ./docker-compose.yml up -d` 2. Install Python 3.10 @@ -55,7 +61,7 @@ but allows developers to run Python code on their native system. 7. Run `./manage.py migrate` 8. Run `./manage.py createsuperuser` and follow the prompts to create your own user -### Run Application +### Run Application (Natively) 1. Ensure `docker compose -f ./docker-compose.yml up -d` is still active 2. Run: @@ -69,10 +75,12 @@ but allows developers to run Python code on their native system. ## Remap Service Ports (optional) -Attached services may be exposed to the host system via alternative ports. Developers who work -on multiple software projects concurrently may find this helpful to avoid port conflicts. +Attached services may be exposed to the host system via alternative ports. Developers +who work on multiple software projects concurrently may find this helpful to avoid +port conflicts. -To do so, before running any `docker compose` commands, set any of the environment variables: +To do so, before running any `docker compose` commands, set any of the environment +variables: * `DOCKER_POSTGRES_PORT` * `DOCKER_RABBITMQ_PORT` @@ -80,25 +88,29 @@ To do so, before running any `docker compose` commands, set any of the environme The Django server must be informed about the changes: -* When running the "Develop with Docker" configuration, override the environment variables: +* When running the "Develop with Docker" configuration, override the environment + variables: * `DJANGO_MINIO_STORAGE_ENDPOINT`, using the port from `DOCKER_MINIO_PORT`. -* When running the "Develop Natively" configuration, override the environment variables: +* When running the "Develop Natively" configuration, override the environment + variables: * `DJANGO_DATABASE_URL`, using the port from `DOCKER_POSTGRES_PORT` * `DJANGO_CELERY_BROKER_URL`, using the port from `DOCKER_RABBITMQ_PORT` * `DJANGO_MINIO_STORAGE_ENDPOINT`, using the port from `DOCKER_MINIO_PORT` -Since most of Django's environment variables contain additional content, use the values from -the appropriate `dev/.env.docker-compose*` file as a baseline for overrides. +Since most of Django's environment variables contain additional content, use the +values from the appropriate `dev/.env.docker-compose*` file as a baseline for +overrides. ## Testing -### Initial Setup +### Initial Setup (Testing) tox is used to execute all tests. tox is installed automatically with the `dev` package extra. -When running the "Develop with Docker" configuration, all tox commands must be run as -`docker-compose run --rm django tox`; extra arguments may also be appended to this form. +When running the "Develop with Docker" configuration, all tox commands must be run +as `docker-compose run --rm django tox`; extra arguments may also be appended to +this form. ### Running Tests @@ -114,3 +126,49 @@ Useful sub-commands include: To automatically reformat all code to comply with some (but not all) of the style checks, run `tox -e format`. + +## Code Formatting + +Any contributed code should be compliant with `PEP8`, which is enforced by +`flake8` via `pre-commit`. It's recommended that you use `pre-commit` to ensure +linting procedures are run on any commit you make. See the +[installation instructions](https://pre-commit.com/#install) for your OS/platform +to install. + +After you have the software installed, run `pre-commit install` on the command line. +Now every time you commit to this project's code base the linter procedures will +automatically run over the changed files. + +To install the linting Python dependencies, run: + +```bash +pip install -r pre-commit +``` + +To run pre-commit on files preemptively from the command line use: + +```bash + git add . + pre-commit run + + # or + + pre-commit run --all-files +``` + +See `.pre-commit-config.yaml` for a list of configured linters and fixers. + +To add `pre-commit` as a Git hook, run the following: + +```bash +pre-commit install +``` + +This will run the `pre-commit` update prior to finalizing a local commit. This +is preferred because once the commit is created locally, you will need to rebase +or otherwise rewrite the commit to make adjustments if done after the fact. + +Lastly, the GitLab CI/CD infrastructure runs the same `pre-commit` configuration +on all pipelines for new MRs. The automated checks in GitLab are optional, but +it is highly recommended to perform these checks locally prior to pushing new +commits. diff --git a/bats_ai/api.py b/bats_ai/api.py index 9d77141..f82b7b7 100644 --- a/bats_ai/api.py +++ b/bats_ai/api.py @@ -1,7 +1,6 @@ import logging from ninja import NinjaAPI -from ninja.security import HttpBearer from oauth2_provider.models import AccessToken from bats_ai.core.views import RecordingRouter, SpeciesRouter @@ -9,17 +8,21 @@ logger = logging.getLogger(__name__) -class GlobalAuth(HttpBearer): - def authenticate(self, request, token): - logger.warning(f'Checking Token: {token}') - print(token) - logger.warning(AccessToken.objects.get(token=token)) - if AccessToken.objects.get(token=token): - logger.warning('returning token') - return token +def global_auth(request): + if request.user.is_anonymous: + token = request.headers.get('Authorization', '').replace('Bearer ', '') + if len(token) > 0: + try: + access_token = AccessToken.objects.get(token=token) + except AccessToken.DoesNotExist: + access_token = None + if access_token and access_token.user: + if not access_token.user.is_anonymous: + request.user = access_token.user + return not request.user.is_anonymous -api = NinjaAPI() +api = NinjaAPI(auth=global_auth) api.add_router('/recording/', RecordingRouter) api.add_router('/species/', SpeciesRouter) diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index f77aae0..78afcae 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -5,10 +5,8 @@ from django.core.files.storage import default_storage from django.http import HttpRequest from ninja import File, Form, Schema -from ninja.errors import HttpError from ninja.files import UploadedFile from ninja.pagination import RouterPaginated -from oauth2_provider.models import AccessToken from bats_ai.core.models import Annotations, Recording, Species from bats_ai.core.views.species import SpeciesSchema @@ -75,11 +73,10 @@ def get_user(request: HttpRequest): def create_recording( request: HttpRequest, payload: Form[RecordingUploadSchema], audio_file: File[UploadedFile] ): - user_id = get_user(request).pk converted_date = datetime.strptime(payload.recorded_date, '%Y-%m-%d') recording = Recording( name=payload.name, - owner_id=user_id, + owner_id=request.user.pk, audio_file=audio_file, recorded_date=converted_date, equipment=payload.equipment, @@ -92,11 +89,8 @@ def create_recording( @router.get('/') def get_recordings(request: HttpRequest): - # Check if the user is authenticated and get userId - user_id = get_user(request) - # Filter recordings based on the owner's id - recordings = Recording.objects.filter(owner=user_id).values() + recordings = Recording.objects.filter(owner=request.user).values() for recording in recordings: user = User.objects.get(id=recording['owner_id']) @@ -129,10 +123,8 @@ def get_spectrogram(request: HttpRequest, id: int): @router.get('/{id}/annotations') def get_annotations(request: HttpRequest, id: int): - user_id = get_user(request) - try: - recording = Recording.objects.get(pk=id, owner=user_id) + recording = Recording.objects.get(pk=id, owner=request.user) except Recording.DoesNotExist: return {'error': 'Recording not found'} @@ -154,17 +146,15 @@ def put_annotation( annotation: AnnotationSchema, species_ids: list[int], ): - user = get_user(request) - try: - recording = Recording.objects.get(pk=id, owner=user.pk) + recording = Recording.objects.get(pk=id, owner=request.user) except Recording.DoesNotExist: return {'error': 'Recording not found'} # Create a new annotation new_annotation = Annotations.objects.create( recording=recording, - owner=user, + owner=request.user, start_time=annotation.start_time, end_time=annotation.end_time, low_freq=annotation.low_freq, @@ -192,10 +182,8 @@ def patch_annotation( annotation: UpdateAnnotationsSchema, species_ids: list[int], ): - user_id = get_user(request) - try: - recording = Recording.objects.get(pk=recording_id, owner=user_id) + recording = Recording.objects.get(pk=recording_id, owner=request.user) annotation_instance = Annotations.objects.get(pk=id, recording=recording) except Recording.DoesNotExist: return {'error': 'Recording not found'} @@ -232,10 +220,8 @@ def patch_annotation( @router.delete('/{recording_id}/annotations/{id}') def delete_annotation(request, recording_id: int, id: int): - user_id = get_user(request) - try: - recording = Recording.objects.get(pk=recording_id, owner=user_id) + recording = Recording.objects.get(pk=recording_id, owner=request.user) annotation_instance = Annotations.objects.get(pk=id, recording=recording) except Recording.DoesNotExist: return {'error': 'Recording not found'} diff --git a/client/README.md b/client/README.md index b30c861..35a6427 100644 --- a/client/README.md +++ b/client/README.md @@ -1,7 +1,8 @@ # Vue Project Template -This is a project boilerplate template designed for building SPAs that will serve as -front-ends to Girder 4-based servers. The following features are included: +This is a project boilerplate template designed for building SPAs that +will serve as front-ends to Girder 4-based servers. The following features +are included: * `vuetify` installation and configuration * `axios` installation and configuration @@ -20,7 +21,8 @@ front-ends to Girder 4-based servers. The following features are included: > If you don't want to use GitHub, you can instead clone the repository, `rm -rf .git/` in it, and `git init` the copy. -2. After you have your copy, replace occurrences of `CHANGEME` in the codebase. +2. After you have your copy, replace occurrences of `CHANGEME` in the + codebase. ```bash git grep CHANGEME @@ -28,8 +30,15 @@ git grep CHANGEME ## Recommended IDE Setup -- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) +- [VSCode](https://code.visualstudio.com/) +- [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar) ## Type Support For `.vue` Imports in TS -Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` from VSCode command palette. +Since TypeScript cannot handle type information for `.vue` imports, they are +shimmed to be a generic Vue component type by default. In most cases this is +fine if you don't really care about component prop types outside of templates. +However, if you wish to get actual prop types in `.vue` imports (for example +to get props validation when using manual `h(...)` calls), you can enable +Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` +from VSCode command palette. diff --git a/client/yarn.lock b/client/yarn.lock index 8700ec4..8bbe048 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -3040,10 +3040,10 @@ es-module-lexer@^1.2.1: resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz" integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== -esbuild-linux-64@0.14.26: +esbuild-darwin-arm64@0.14.26: version "0.14.26" - resolved "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.26.tgz" - integrity sha512-aPku1lCxxXmBr5LkENSlGIbY33jjQExDzaSrNV+dDA5bHXhFnpI9UkSe+vQzrSkxgO66vNjSTNDcxg3pOXBaBw== + resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.26.tgz" + integrity sha512-eYW+cmP3BGVPDp+wd9bRI5CN5HjkZnrMQtj46Mj//UsSh4SRvflAp3pjs3ooA+MCpIa9xZ8091HqLqpYi7KFWA== esbuild@^0.14.14: version "0.14.26" @@ -3405,6 +3405,11 @@ fs.realpath@^1.0.0: resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + function-bind@^1.1.1, function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 499dcac..bb63aa0 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -4,7 +4,9 @@ services: build: context: . dockerfile: ./dev/django.Dockerfile - command: ["./manage.py", "runserver", "0.0.0.0:8000"] + entrypoint: ["/bin/bash"] + command: "" + # command: ["./manage.py", "runserver", "0.0.0.0:8000"] # Log printing via Rich is enhanced by a TTY tty: true env_file: ./dev/.env.docker-compose From 7bc1dbc9892cfadb1cdd3c3e94d2ba67de0e2a7d Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Thu, 11 Jan 2024 00:24:32 -0800 Subject: [PATCH 03/12] Preliminary integration of spectogram conversion --- README.md | 5 ++ bats_ai/core/models/recording.py | 115 ++++++++++++++++++++++--------- bats_ai/core/views/recording.py | 5 +- dev/django.Dockerfile | 1 + docker-compose.override.yml | 2 +- 5 files changed, 91 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index a1ebfd8..dda410f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,11 @@ This is the simplest configuration for developers to start with. --username your.super.user@email.address \ --uri http://localhost:3000/` +### Run Vue Frontend + +1. Run `npm install` +2. Run `npm run dev` + ### Run Application 1. Run `docker compose up` diff --git a/bats_ai/core/models/recording.py b/bats_ai/core/models/recording.py index 1942730..89855eb 100644 --- a/bats_ai/core/models/recording.py +++ b/bats_ai/core/models/recording.py @@ -2,6 +2,10 @@ from django.contrib.gis.db import models from django_extensions.db.models import TimeStampedModel +FREQ_MIN = 5e3 +FREQ_MAX = 200e3 +FREQ_PAD = 2e3 + # TimeStampedModel also provides "created" and "modified" fields class Recording(TimeStampedModel, models.Model): @@ -17,54 +21,99 @@ class Recording(TimeStampedModel, models.Model): def generate_spectrogram(self): import base64 + import io from io import BytesIO + import math + from PIL import Image + import django import librosa - import librosa.display import matplotlib.pyplot as plt import numpy as np - # Load audio file - bytefile = self.audio_file.read() + wav_file = self.audio_file + try: + if isinstance(wav_file, django.db.models.fields.files.FieldFile): + sig, sr = librosa.load(BytesIO(wav_file.read()), sr=None) + wav_file.name + else: + sig, sr = librosa.load(wav_file, sr=None) + + duration = len(sig) / sr + except Exception as e: + print(e) + return None + + # Function to take a signal and return a spectrogram. + size = int(0.001 * sr) # 1.0ms resolution + size = 2 ** math.ceil(math.log(size, 2)) + + # Short-time Fourier Transform + window = librosa.stft(sig, n_fft=size, window='hamming') + + # Calculating and processing data for the spectrogram. + window = np.abs(window) ** 2 + window = librosa.power_to_db(window) + + # Denoise spectrogram + window -= np.median(window, axis=1, keepdims=True) + window -= np.median(window, axis=0, keepdims=True) + + bands = librosa.fft_frequencies(sr=sr, n_fft=size) + for index in range(len(bands)): + band_min = bands[index] + band_max = bands[index + 1] if index < len(bands) - 1 else np.inf + if band_max <= FREQ_MIN or FREQ_MAX <= band_min: + window[index, :] = -1 + + window = np.clip(window, 0, None) + + try: + alpha = 8 + h, w = window.shape + figsize = (int(math.ceil(w / h)) * alpha, alpha) + fig = plt.figure(figsize=figsize, facecolor='black', dpi=100) + ax = plt.axes() + plt.margins(0) - y, sr = librosa.load(BytesIO(bytefile)) + # Plot + hop_length = int(size / 4) + librosa.display.specshow( + window, sr=sr, hop_length=hop_length, x_axis='s', y_axis='linear', ax=ax + ) - # Generate spectrogram - D = librosa.amplitude_to_db(np.abs(librosa.stft(y)), ref=np.max) + freq_low = FREQ_MIN - FREQ_PAD + freq_high = FREQ_MAX + FREQ_PAD + ax.set_ylim(freq_low, freq_high) - # Plot and save the spectrogram - plt.figure(figsize=(10, 4)) - librosa.display.specshow(D, sr=sr, x_axis='time', y_axis='linear') - plt.colorbar(format='%+2.0f dB') - plt.title('Spectrogram') - plt.xlabel('Time') - plt.ylabel('Frequency') - plt.tight_layout() + ax.axis('off') - # Convert the plot to base64 - buffer = BytesIO() - plt.savefig(buffer, format='png') - buffer.seek(0) - base64_image = base64.b64encode(buffer.read()).decode('utf-8') + buf = io.BytesIO() + fig.savefig(buf, bbox_inches='tight', pad_inches=0) + buf.seek(0) + img = Image.open(buf) + w, h = img.size - plt.close() + import IPython + IPython.embed() + img.save('temp.png') - start_time = 0.0 - end_time = librosa.get_duration(y=y, sr=sr) * 1000 # in milliseconds - low_frequency = 0 # Set your desired low frequency - high_frequency = sr / 2 # Set your desired high frequency - image_width = 10 * 100 # 10 inches at 100 dpi - image_height = 4 * 100 # 4 inches at 100 dpi + buf = io.BytesIO() + img.save(buf, format='JPEG') + img_base64 = base64.b64encode(buf.getvalue()) + finally: + fig.clf() + plt.close() # Return dictionary with all required fields return { - 'base64_spectrogram': base64_image, + 'base64_spectrogram': img_base64, 'spectroInfo': { - 'width': image_width, - 'height': image_height, - 'start_time': start_time, - 'end_time': end_time, - 'low_freq': low_frequency, - 'high_freq': high_frequency, + 'width': w, + 'height': h, + 'start_time': 0.0, + 'end_time': int(math.ceil(duration * 1000.0)), # in milliseconds + 'low_freq': freq_low, + 'high_freq': freq_high, }, } diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index 78afcae..bedf371 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -103,7 +103,6 @@ def get_recordings(request: HttpRequest): @router.get('/{id}/spectrogram') def get_spectrogram(request: HttpRequest, id: int): - user_id = get_user(request) try: recording = Recording.objects.get(pk=id) except Recording.DoesNotExist: @@ -111,7 +110,7 @@ def get_spectrogram(request: HttpRequest, id: int): spectro_data = recording.generate_spectrogram() - annotations_qs = Annotations.objects.filter(recording=recording, owner=user_id) + annotations_qs = Annotations.objects.filter(recording=recording, owner=request.user) # Serialize the annotations using AnnotationSchema annotations_data = [ @@ -129,7 +128,7 @@ def get_annotations(request: HttpRequest, id: int): return {'error': 'Recording not found'} # Query annotations associated with the recording - annotations_qs = Annotations.objects.filter(recording=recording, owner=user_id) + annotations_qs = Annotations.objects.filter(recording=recording, owner=request.user) # Serialize the annotations using AnnotationSchema annotations_data = [ diff --git a/dev/django.Dockerfile b/dev/django.Dockerfile index 7afea88..7fa3378 100644 --- a/dev/django.Dockerfile +++ b/dev/django.Dockerfile @@ -14,6 +14,7 @@ RUN set -ex \ libc6-dev \ libgdal32 \ libpq-dev \ + libsndfile1-dev \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/docker-compose.override.yml b/docker-compose.override.yml index bb63aa0..207f319 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -6,7 +6,7 @@ services: dockerfile: ./dev/django.Dockerfile entrypoint: ["/bin/bash"] command: "" - # command: ["./manage.py", "runserver", "0.0.0.0:8000"] + # command: ["./manage.py", "runserver", "0.0.0.0:8000", "--noreload"] # Log printing via Rich is enhanced by a TTY tty: true env_file: ./dev/.env.docker-compose From 18e244922940897d5720c57170997ebef125afe7 Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Fri, 12 Jan 2024 08:52:27 -0800 Subject: [PATCH 04/12] Preliminary spectrogram visualization --- bats_ai/core/models/recording.py | 7 ++++--- docker-compose.override.yml | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/bats_ai/core/models/recording.py b/bats_ai/core/models/recording.py index 89855eb..8724421 100644 --- a/bats_ai/core/models/recording.py +++ b/bats_ai/core/models/recording.py @@ -94,9 +94,10 @@ def generate_spectrogram(self): img = Image.open(buf) w, h = img.size - import IPython - IPython.embed() - img.save('temp.png') + # import IPython + + # IPython.embed() + # img.save('temp.png') buf = io.BytesIO() img.save(buf, format='JPEG') diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 207f319..2e12974 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -4,9 +4,9 @@ services: build: context: . dockerfile: ./dev/django.Dockerfile - entrypoint: ["/bin/bash"] - command: "" - # command: ["./manage.py", "runserver", "0.0.0.0:8000", "--noreload"] + command: ["./manage.py", "runserver", "0.0.0.0:8000", "--noreload"] + # entrypoint: ["/bin/bash"] + # command: "" # Log printing via Rich is enhanced by a TTY tty: true env_file: ./dev/.env.docker-compose From 7ae352019385ce55fcc764914df600c18408ee35 Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Fri, 19 Jan 2024 04:10:45 -0800 Subject: [PATCH 05/12] Updated NPM packages --- client/package-lock.json | 368 ++++++++++++++++++++++----------------- client/yarn.lock | 222 ++++++++++++----------- 2 files changed, 327 insertions(+), 263 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index d05e0be..8671cb2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -174,37 +174,29 @@ } }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "devOptional": true, "bin": { "semver": "bin/semver.js" } }, "node_modules/@babel/generator": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz", - "integrity": "sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "devOptional": true, "dependencies": { - "@babel/types": "^7.17.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" + "@babel/types": "^7.23.6", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/generator/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "devOptional": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@babel/helper-annotate-as-pure": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", @@ -1994,20 +1986,20 @@ } }, "node_modules/@babel/traverse": { - "version": "7.17.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", - "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", "devOptional": true, "dependencies": { - "@babel/code-frame": "^7.16.7", - "@babel/generator": "^7.17.3", - "@babel/helper-environment-visitor": "^7.16.7", - "@babel/helper-function-name": "^7.16.7", - "@babel/helper-hoist-variables": "^7.16.7", - "@babel/helper-split-export-declaration": "^7.16.7", - "@babel/parser": "^7.17.3", - "@babel/types": "^7.17.0", - "debug": "^4.1.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -2055,6 +2047,21 @@ "node": ">=0.8.0" } }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.0.tgz", @@ -2412,8 +2419,7 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", - "optional": true, - "peer": true, + "devOptional": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -2436,8 +2442,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "optional": true, - "peer": true, + "devOptional": true, "engines": { "node": ">=6.0.0" } @@ -5119,9 +5124,9 @@ "peer": true }, "node_modules/esbuild": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.26.tgz", - "integrity": "sha512-v0zIYlFB9NZ82/hFljhvpA7f8rob66r68ymB7juMz6TYAAMYjKGoW+hrMfRRvic5MAOI2wE/SuykFvsELLa6eA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", "devOptional": true, "hasInstallScript": true, "bin": { @@ -5131,32 +5136,33 @@ "node": ">=12" }, "optionalDependencies": { - "esbuild-android-64": "0.14.26", - "esbuild-android-arm64": "0.14.26", - "esbuild-darwin-64": "0.14.26", - "esbuild-darwin-arm64": "0.14.26", - "esbuild-freebsd-64": "0.14.26", - "esbuild-freebsd-arm64": "0.14.26", - "esbuild-linux-32": "0.14.26", - "esbuild-linux-64": "0.14.26", - "esbuild-linux-arm": "0.14.26", - "esbuild-linux-arm64": "0.14.26", - "esbuild-linux-mips64le": "0.14.26", - "esbuild-linux-ppc64le": "0.14.26", - "esbuild-linux-riscv64": "0.14.26", - "esbuild-linux-s390x": "0.14.26", - "esbuild-netbsd-64": "0.14.26", - "esbuild-openbsd-64": "0.14.26", - "esbuild-sunos-64": "0.14.26", - "esbuild-windows-32": "0.14.26", - "esbuild-windows-64": "0.14.26", - "esbuild-windows-arm64": "0.14.26" + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" } }, "node_modules/esbuild-android-64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.26.tgz", - "integrity": "sha512-HIyJ3VvigHfseaI0D+vsD8zKQ4roDUD962/vtO/KXzav6wR//Y//Qx1HUX8k5bQeQ7/0yCXlltY9VBw1MFnWFQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", "cpu": [ "x64" ], @@ -5169,9 +5175,9 @@ } }, "node_modules/esbuild-android-arm64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.26.tgz", - "integrity": "sha512-TxRCLxyU5yj3U8Bud9fCg3IxzIXXKaWcmDbvURm8JkRr0WvCAmwZBdLi5T8BasT1v5vrVE//M0KSHZod6HC6lA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", "cpu": [ "arm64" ], @@ -5184,9 +5190,9 @@ } }, "node_modules/esbuild-darwin-64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.26.tgz", - "integrity": "sha512-oZJBN+CkR47Fc7KB1vowZy2kb5r+WSnsBjVEw7aI8HmR6louAgTr4bs1NwzaF6MbLi41ajaw6RdipfsM1H9PvQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", "cpu": [ "x64" ], @@ -5199,9 +5205,9 @@ } }, "node_modules/esbuild-darwin-arm64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.26.tgz", - "integrity": "sha512-eYW+cmP3BGVPDp+wd9bRI5CN5HjkZnrMQtj46Mj//UsSh4SRvflAp3pjs3ooA+MCpIa9xZ8091HqLqpYi7KFWA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", "cpu": [ "arm64" ], @@ -5214,9 +5220,9 @@ } }, "node_modules/esbuild-freebsd-64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.26.tgz", - "integrity": "sha512-Q+Hs27fSBkNfUHNhphSyWfF5lxl3o9S6LFlzkC5KofxLCnCESP+7YTzAWTosYGANsPT2mvYFOraFeYEokG+5DA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", "cpu": [ "x64" ], @@ -5229,9 +5235,9 @@ } }, "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.26.tgz", - "integrity": "sha512-MT+FuC/63oz6j/jvWOMCNqnHBYm/bNhGPArUgQX8GRhofFCeqe0NRmJbhtlHZaEeErIIjHPZQ/nXs34mfiqo/Q==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", "cpu": [ "arm64" ], @@ -5244,9 +5250,9 @@ } }, "node_modules/esbuild-linux-32": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.26.tgz", - "integrity": "sha512-9gqSfJ8qMDvz7wXZoinNoe9/ekPpbT+/ZgVfZEeB72ETITVPHvMbG8i0E12wG366G01vMXtlxbD9IYJsMVhe6w==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", "cpu": [ "ia32" ], @@ -5259,9 +5265,9 @@ } }, "node_modules/esbuild-linux-64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.26.tgz", - "integrity": "sha512-aPku1lCxxXmBr5LkENSlGIbY33jjQExDzaSrNV+dDA5bHXhFnpI9UkSe+vQzrSkxgO66vNjSTNDcxg3pOXBaBw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", "cpu": [ "x64" ], @@ -5274,9 +5280,9 @@ } }, "node_modules/esbuild-linux-arm": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.26.tgz", - "integrity": "sha512-m7ozLQozfgBmh9l3HWxDEVYEEG8GuTqzRoFuf9iX0xAlbtqmhhlm7M4zNMa2eyPEG+ejgHndAuvuB1hcOWvdJw==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", "cpu": [ "arm" ], @@ -5289,9 +5295,9 @@ } }, "node_modules/esbuild-linux-arm64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.26.tgz", - "integrity": "sha512-S0boyzv5Yx+IN1A8253nEPzHqn/W/y+CRcLYFZ1E5DscqkY7EvBao6rhff3ZxaHU9Zrkn0pLVqlJdMx3rm6D4Q==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", "cpu": [ "arm64" ], @@ -5304,9 +5310,9 @@ } }, "node_modules/esbuild-linux-mips64le": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.26.tgz", - "integrity": "sha512-TyMRc2ctQV1g9ruHg1Y793e18uDigKKsgzcZPzfxZi2z+hGK1uaSdaejGdULEJBJVMXt3/NC1T1yq0vCTiYYgg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", "cpu": [ "mips64el" ], @@ -5319,9 +5325,9 @@ } }, "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.26.tgz", - "integrity": "sha512-aOJPP80m2gV8CyDqEMGbwZaGKuR45tZU1qYZ0+Cy8lWV4CWmd9iBWhCLP3eI9d7163m6t+0YO/6N3iLSVlNnpA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", "cpu": [ "ppc64" ], @@ -5334,9 +5340,9 @@ } }, "node_modules/esbuild-linux-riscv64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.26.tgz", - "integrity": "sha512-2E5xK7SNZFXhFzRbZGtUqg3MbHnrx5XzqHaGLOLdHBqOSWIAdJKB3w6WtjpLkZvPuWrKeh51XnRpk1jm0TsUjQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", "cpu": [ "riscv64" ], @@ -5349,9 +5355,9 @@ } }, "node_modules/esbuild-linux-s390x": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.26.tgz", - "integrity": "sha512-kfSuFea857mTYMp/RAFmMp9TBjf1T8F/dTRqLn2p+g8Ok30Cp1+mI2+YCmxz5Uw2JOfxyvpND0Ek1PGPMo1UsQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", "cpu": [ "s390x" ], @@ -5364,9 +5370,9 @@ } }, "node_modules/esbuild-netbsd-64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.26.tgz", - "integrity": "sha512-tWhLwfOOqdZRwvaSYIWuic9Cj+WRRCLHe//Bmlf0ThBur9/EssRTtVh6/rC2Okp7Eb4QcerA/1wjWLYLECYD7g==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", "cpu": [ "x64" ], @@ -5379,9 +5385,9 @@ } }, "node_modules/esbuild-openbsd-64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.26.tgz", - "integrity": "sha512-Xj7IWpsPn/hgKNzwjLpnf6wMtV0lfw5bzn7N9vmiCKx9TBA28L2hI8G15O0s9atLKny4HpmCGwZWmReNF1Ui6g==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", "cpu": [ "x64" ], @@ -5394,9 +5400,9 @@ } }, "node_modules/esbuild-sunos-64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.26.tgz", - "integrity": "sha512-5odPsuhghCUYc3c1gEtz6pGq9cuGRDq1+iNdLBjZcz6IUebd0ay/AVORWchs5WddzyJA9hguxrKsPjECxX6OzQ==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", "cpu": [ "x64" ], @@ -5409,9 +5415,9 @@ } }, "node_modules/esbuild-windows-32": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.26.tgz", - "integrity": "sha512-xSVyGV6xGQlAC/K+oBXC9YiGGqoKqQGXVEFQKlDGXD6rxHGK5Fch0ynuvkjaYWW/p8OWqxGVYcof5BvGjY49RA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", "cpu": [ "ia32" ], @@ -5424,9 +5430,9 @@ } }, "node_modules/esbuild-windows-64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.26.tgz", - "integrity": "sha512-Q0Bm42+wIqHJ8yF96T7nXosILXROegRtMmuI1L0kry0YBHnCFMtjNRTyUwv8yi7o2XvVYh7DF0NHLDL4N34MuA==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", "cpu": [ "x64" ], @@ -5439,9 +5445,9 @@ } }, "node_modules/esbuild-windows-arm64": { - "version": "0.14.26", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.26.tgz", - "integrity": "sha512-+l0DB0VV4LiSoDfNsGviK/2M88IR+/fOUfQoQx08RPu7OZ7gv9BqhRLZCSCT4qHT351OTH1nPv7avRXX6JRQcg==", + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", "cpu": [ "arm64" ], @@ -5999,9 +6005,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", "funding": [ { "type": "individual", @@ -6679,9 +6685,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -7613,9 +7619,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "devOptional": true, "bin": { "semver": "bin/semver.js" @@ -7692,9 +7698,9 @@ } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "devOptional": true, "dependencies": { "brace-expansion": "^1.1.7" @@ -7704,10 +7710,13 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "optional": true + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/mousetrap": { "version": "1.6.5", @@ -7727,9 +7736,15 @@ "dev": true }, "node_modules/nanoid": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz", - "integrity": "sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -8021,20 +8036,30 @@ "license": "MIT" }, "node_modules/postcss": { - "version": "8.4.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz", - "integrity": "sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ==", + "version": "8.4.33", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz", + "integrity": "sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "dependencies": { - "nanoid": "^3.3.1", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, "engines": { "node": "^10 || ^12 || >=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" } }, "node_modules/postcss-value-parser": { @@ -8121,6 +8146,12 @@ "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8293,6 +8324,12 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -8931,14 +8968,15 @@ } }, "node_modules/tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", "dev": true, "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", - "universalify": "^0.1.2" + "universalify": "^0.2.0", + "url-parse": "^1.5.3" }, "engines": { "node": ">=6" @@ -9076,9 +9114,9 @@ } }, "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", "dev": true, "engines": { "node": ">= 4.0.0" @@ -9133,6 +9171,16 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9169,15 +9217,15 @@ } }, "node_modules/vite": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz", - "integrity": "sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug==", + "version": "2.9.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz", + "integrity": "sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA==", "devOptional": true, "dependencies": { - "esbuild": "^0.14.14", - "postcss": "^8.4.6", + "esbuild": "^0.14.27", + "postcss": "^8.4.13", "resolve": "^1.22.0", - "rollup": "^2.59.0" + "rollup": ">=2.59.0 <2.78.0" }, "bin": { "vite": "bin/vite.js" @@ -9589,9 +9637,9 @@ "integrity": "sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw==" }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "engines": { "node": ">=0.10.0" diff --git a/client/yarn.lock b/client/yarn.lock index 8bbe048..c580659 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -9,7 +9,7 @@ dependencies: "@jridgewell/trace-mapping" "^0.3.0" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.13": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.22.13", "@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz" integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== @@ -43,14 +43,15 @@ json5 "^2.1.2" semver "^6.3.0" -"@babel/generator@^7.17.3", "@babel/generator@^7.7.2": - version "7.17.3" - resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz" - integrity sha512-+R6Dctil/MgUsZsZAkYgK+ADNSZzJRRy0TvY65T71z/CR854xHQ1EweBYXdfT+HNeN7w0cSJJEzgxZMv40pxsg== +"@babel/generator@^7.17.3", "@babel/generator@^7.23.6", "@babel/generator@^7.7.2": + version "7.23.6" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz" + integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== dependencies: - "@babel/types" "^7.17.0" + "@babel/types" "^7.23.6" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" - source-map "^0.5.0" "@babel/helper-annotate-as-pure@^7.22.5": version "7.22.5" @@ -112,12 +113,12 @@ lodash.debounce "^4.0.8" resolve "^1.14.2" -"@babel/helper-environment-visitor@^7.16.7", "@babel/helper-environment-visitor@^7.22.20": +"@babel/helper-environment-visitor@^7.22.20": version "7.22.20" resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz" integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== -"@babel/helper-function-name@^7.16.7", "@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": +"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": version "7.23.0" resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz" integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== @@ -125,7 +126,7 @@ "@babel/template" "^7.22.15" "@babel/types" "^7.23.0" -"@babel/helper-hoist-variables@^7.16.7", "@babel/helper-hoist-variables@^7.22.5": +"@babel/helper-hoist-variables@^7.22.5": version "7.22.5" resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz" integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== @@ -201,7 +202,7 @@ dependencies: "@babel/types" "^7.22.5" -"@babel/helper-split-export-declaration@^7.16.7", "@babel/helper-split-export-declaration@^7.22.6": +"@babel/helper-split-export-declaration@^7.22.6": version "7.22.6" resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz" integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== @@ -250,7 +251,7 @@ chalk "^2.4.2" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.17.3", "@babel/parser@^7.20.15", "@babel/parser@^7.21.3", "@babel/parser@^7.22.15": +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.17.3", "@babel/parser@^7.20.15", "@babel/parser@^7.21.3", "@babel/parser@^7.22.15", "@babel/parser@^7.23.6": version "7.23.6" resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz" integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== @@ -938,22 +939,22 @@ "@babel/types" "^7.22.15" "@babel/traverse@^7.17.0", "@babel/traverse@^7.17.3", "@babel/traverse@^7.7.2": - version "7.17.3" - resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz" - integrity sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw== + version "7.23.7" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz" + integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg== dependencies: - "@babel/code-frame" "^7.16.7" - "@babel/generator" "^7.17.3" - "@babel/helper-environment-visitor" "^7.16.7" - "@babel/helper-function-name" "^7.16.7" - "@babel/helper-hoist-variables" "^7.16.7" - "@babel/helper-split-export-declaration" "^7.16.7" - "@babel/parser" "^7.17.3" - "@babel/types" "^7.17.0" - debug "^4.1.0" + "@babel/code-frame" "^7.23.5" + "@babel/generator" "^7.23.6" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.6" + "@babel/types" "^7.23.6" + debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.17.0", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": +"@babel/types@^7.0.0", "@babel/types@^7.17.0", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.23.6", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": version "7.23.6" resolved "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz" integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== @@ -1195,7 +1196,7 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.3.0": +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": version "0.3.3" resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz" integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== @@ -1227,7 +1228,7 @@ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== -"@jridgewell/trace-mapping@^0.3.0", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.9": +"@jridgewell/trace-mapping@^0.3.0", "@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.9": version "0.3.20" resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz" integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== @@ -2893,7 +2894,7 @@ de-indent@^1.0.2: resolved "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz" integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== -debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@4: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@4: version "4.3.3" resolved "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz" integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== @@ -3040,36 +3041,37 @@ es-module-lexer@^1.2.1: resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz" integrity sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w== -esbuild-darwin-arm64@0.14.26: - version "0.14.26" - resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.26.tgz" - integrity sha512-eYW+cmP3BGVPDp+wd9bRI5CN5HjkZnrMQtj46Mj//UsSh4SRvflAp3pjs3ooA+MCpIa9xZ8091HqLqpYi7KFWA== +esbuild-darwin-arm64@0.14.54: + version "0.14.54" + resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz" + integrity sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw== -esbuild@^0.14.14: - version "0.14.26" - resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.26.tgz" - integrity sha512-v0zIYlFB9NZ82/hFljhvpA7f8rob66r68ymB7juMz6TYAAMYjKGoW+hrMfRRvic5MAOI2wE/SuykFvsELLa6eA== +esbuild@^0.14.27: + version "0.14.54" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz" + integrity sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA== optionalDependencies: - esbuild-android-64 "0.14.26" - esbuild-android-arm64 "0.14.26" - esbuild-darwin-64 "0.14.26" - esbuild-darwin-arm64 "0.14.26" - esbuild-freebsd-64 "0.14.26" - esbuild-freebsd-arm64 "0.14.26" - esbuild-linux-32 "0.14.26" - esbuild-linux-64 "0.14.26" - esbuild-linux-arm "0.14.26" - esbuild-linux-arm64 "0.14.26" - esbuild-linux-mips64le "0.14.26" - esbuild-linux-ppc64le "0.14.26" - esbuild-linux-riscv64 "0.14.26" - esbuild-linux-s390x "0.14.26" - esbuild-netbsd-64 "0.14.26" - esbuild-openbsd-64 "0.14.26" - esbuild-sunos-64 "0.14.26" - esbuild-windows-32 "0.14.26" - esbuild-windows-64 "0.14.26" - esbuild-windows-arm64 "0.14.26" + "@esbuild/linux-loong64" "0.14.54" + esbuild-android-64 "0.14.54" + esbuild-android-arm64 "0.14.54" + esbuild-darwin-64 "0.14.54" + esbuild-darwin-arm64 "0.14.54" + esbuild-freebsd-64 "0.14.54" + esbuild-freebsd-arm64 "0.14.54" + esbuild-linux-32 "0.14.54" + esbuild-linux-64 "0.14.54" + esbuild-linux-arm "0.14.54" + esbuild-linux-arm64 "0.14.54" + esbuild-linux-mips64le "0.14.54" + esbuild-linux-ppc64le "0.14.54" + esbuild-linux-riscv64 "0.14.54" + esbuild-linux-s390x "0.14.54" + esbuild-netbsd-64 "0.14.54" + esbuild-openbsd-64 "0.14.54" + esbuild-sunos-64 "0.14.54" + esbuild-windows-32 "0.14.54" + esbuild-windows-64 "0.14.54" + esbuild-windows-arm64 "0.14.54" escalade@^3.1.1: version "3.1.1" @@ -3373,9 +3375,9 @@ flatted@^3.1.0: integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== follow-redirects@^1.13.3, follow-redirects@^1.15.0: - version "1.15.3" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz" - integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + version "1.15.5" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz" + integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw== form-data@^3.0.0: version "3.0.1" @@ -4515,9 +4517,9 @@ mimic-fn@^2.1.0: integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== dependencies: brace-expansion "^1.1.7" @@ -4529,9 +4531,9 @@ minimatch@^9.0.3: brace-expansion "^2.0.1" minimist@^1.2.0: - version "1.2.5" - resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz" - integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + version "1.2.8" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== mousetrap@^1.6.5: version "1.6.5" @@ -4548,10 +4550,10 @@ muggle-string@^0.3.1: resolved "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz" integrity sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg== -nanoid@^3.3.1: - version "3.3.1" - resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.1.tgz" - integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== natural-compare@^1.4.0: version "1.4.0" @@ -4745,12 +4747,12 @@ postcss-value-parser@^4.2.0: resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== -postcss@^8.1.0, postcss@^8.1.10, postcss@^8.4.6: - version "8.4.8" - resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.8.tgz" - integrity sha512-2tXEqGxrjvAO6U+CJzDL2Fk2kPHTv1jQsYkSoMeOis2SsYaXRO2COxTdQp99cYvif9JTXaAk9lYGc3VhJt7JPQ== +postcss@^8.1.0, postcss@^8.1.10, postcss@^8.4.13: + version "8.4.33" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.33.tgz" + integrity sha512-Kkpbhhdjw2qQs2O2DGX+8m5OVqEcbB9HRBvuYM9pgrjEFUg30A9LmXNlTAUj4S9kgtGyrMbTzVjH7E+s5Re2yg== dependencies: - nanoid "^3.3.1" + nanoid "^3.3.7" picocolors "^1.0.0" source-map-js "^1.0.2" @@ -4804,6 +4806,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" @@ -4897,6 +4904,11 @@ require-directory@^2.1.1: resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" @@ -4945,7 +4957,7 @@ robust-predicates@^3.0.0: resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz" integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg== -rollup@^2.59.0: +"rollup@>=2.59.0 <2.78.0": version "2.70.1" resolved "https://registry.npmjs.org/rollup/-/rollup-2.70.1.tgz" integrity sha512-CRYsI5EuzLbXdxC6RnYhOuRdtz4bhejPMSWjsFLfVM/7w/85n2szZv6yExqUXsBdz5KT8eoubeyDUDjhLHEslA== @@ -5010,14 +5022,14 @@ seedrandom@3.0.5: integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== semver@^6.0.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^6.3.0: - version "6.3.0" - resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^6.3.1: version "6.3.1" @@ -5094,11 +5106,6 @@ source-map-support@^0.5.6, source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.5.0: - version "0.5.7" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== - source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" @@ -5290,13 +5297,14 @@ to-regex-range@^5.0.1: is-number "^7.0.0" tough-cookie@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz" - integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + version "4.1.3" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== dependencies: psl "^1.1.33" punycode "^2.1.1" - universalify "^0.1.2" + universalify "^0.2.0" + url-parse "^1.5.3" tr46@^2.1.0: version "2.1.0" @@ -5381,10 +5389,10 @@ unicode-property-aliases-ecmascript@^2.0.0: resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz" integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== -universalify@^0.1.2: - version "0.1.2" - resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" - integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== upath@^2.0.1: version "2.0.1" @@ -5406,6 +5414,14 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -5435,14 +5451,14 @@ vite-plugin-vuetify@^1.0.0-alpha.12, vite-plugin-vuetify@^1.0.2: upath "^2.0.1" vite@^2.5.10, vite@^2.7.0, "vite@^2.7.0 || ^3.0.0 || ^4.0.0", vite@^2.8.0: - version "2.8.6" - resolved "https://registry.npmjs.org/vite/-/vite-2.8.6.tgz" - integrity sha512-e4H0QpludOVKkmOsRyqQ7LTcMUDF3mcgyNU4lmi0B5JUbe0ZxeBBl8VoZ8Y6Rfn9eFKYtdXNPcYK97ZwH+K2ug== + version "2.9.16" + resolved "https://registry.npmjs.org/vite/-/vite-2.9.16.tgz" + integrity sha512-X+6q8KPyeuBvTQV8AVSnKDvXoBMnTx8zxh54sOwmmuOdxkjMmEJXH2UEchA+vTMps1xw9vL64uwJOWryULg7nA== dependencies: - esbuild "^0.14.14" - postcss "^8.4.6" + esbuild "^0.14.27" + postcss "^8.4.13" resolve "^1.22.0" - rollup "^2.59.0" + rollup ">=2.59.0 <2.78.0" optionalDependencies: fsevents "~2.3.2" @@ -5634,9 +5650,9 @@ wkt-parser@^1.3.3: integrity sha512-ZnV3yH8/k58ZPACOXeiHaMuXIiaTk1t0hSUVisbO0t4RjA5wPpUytcxeyiN2h+LZRrmuHIh/1UlrR9e7DHDvTw== word-wrap@^1.2.3, word-wrap@~1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.5" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== worker-loader@3.0.8: version "3.0.8" From 80b4db97c0a4857a0579147f9039f19d3a2f7f11 Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Fri, 19 Jan 2024 04:11:33 -0800 Subject: [PATCH 06/12] Added Flower and OpenCV --- docker-compose.yml | 21 +++++++++++++++++++++ setup.py | 2 ++ 2 files changed, 23 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 17fc684..52df5c6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,27 @@ services: volumes: - minio:/data + flower: + build: + context: . + dockerfile: ./dev/django.Dockerfile + command: [ + "celery", + "--app", "bats_ai.celery", + "flower" + ] + tty: false + env_file: ./dev/.env.docker-compose + volumes: + - .:/opt/django-project + ports: + - ${DOCKER_FLOWER_PORT-5555}:5555 + depends_on: + - postgres + - rabbitmq + - minio + - celery + volumes: postgres: sourcedb: diff --git a/setup.py b/setup.py index 7678b04..a8deb60 100644 --- a/setup.py +++ b/setup.py @@ -51,9 +51,11 @@ 'django-composed-configuration[prod]>=0.20', 'django-s3-file-field[boto3]<1', 'gunicorn', + 'flower', 'librosa', 'matplotlib', 'numpy', + 'opencv-python-headless', ], extras_require={ 'dev': [ From d65a5aa13a599fa21c954cfc2fba34cddd6f138b Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Fri, 19 Jan 2024 04:11:55 -0800 Subject: [PATCH 07/12] Added example WAV file with LFS --- .gitattributes | 1 + assets/example.wav | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 .gitattributes create mode 100644 assets/example.wav diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..84fe760 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +assets/example.wav filter=lfs diff=lfs merge=lfs -text diff --git a/assets/example.wav b/assets/example.wav new file mode 100644 index 0000000..b08d0b6 --- /dev/null +++ b/assets/example.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:391efce5433d1057caddb4ce07b9712c523d6a815e4ee9e64b62973569982925 +size 4999428 From cccb6ce45cd7c054ec2fc0d52b248b5474e19dd4 Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Fri, 19 Jan 2024 04:12:37 -0800 Subject: [PATCH 08/12] Added spectogram visualization for Recordings --- bats_ai/core/admin/__init__.py | 4 +- bats_ai/core/admin/recording.py | 31 ++- bats_ai/core/admin/spectrogram.py | 31 +++ bats_ai/core/migrations/0004_spectrogram.py | 53 ++++ bats_ai/core/models/__init__.py | 4 +- bats_ai/core/models/recording.py | 114 ++------- bats_ai/core/models/spectrogram.py | 265 ++++++++++++++++++++ bats_ai/core/tasks.py | 10 +- bats_ai/core/views/recording.py | 59 +++-- 9 files changed, 458 insertions(+), 113 deletions(-) create mode 100644 bats_ai/core/admin/spectrogram.py create mode 100644 bats_ai/core/migrations/0004_spectrogram.py create mode 100644 bats_ai/core/models/spectrogram.py diff --git a/bats_ai/core/admin/__init__.py b/bats_ai/core/admin/__init__.py index d949e53..68eb9b8 100644 --- a/bats_ai/core/admin/__init__.py +++ b/bats_ai/core/admin/__init__.py @@ -1,9 +1,11 @@ from .annotations import AnnotationsAdmin from .image import ImageAdmin from .recording import RecordingAdmin +from .spectrogram import SpectrogramAdmin __all__ = [ + 'AnnotationsAdmin', 'ImageAdmin', 'RecordingAdmin', - 'AnnotationsAdmin', + 'SpectrogramAdmin', ] diff --git a/bats_ai/core/admin/recording.py b/bats_ai/core/admin/recording.py index f07d854..0a6174b 100644 --- a/bats_ai/core/admin/recording.py +++ b/bats_ai/core/admin/recording.py @@ -1,6 +1,11 @@ -from django.contrib import admin +from django.contrib import admin, messages +from django.db.models import QuerySet +from django.http import HttpRequest +from django.urls import reverse +from django.utils.safestring import mark_safe from bats_ai.core.models import Recording +from bats_ai.core.tasks import recording_compute_spectrogram @admin.register(Recording) @@ -9,6 +14,7 @@ class RecordingAdmin(admin.ModelAdmin): 'id', 'name', 'audio_file', + 'spectrogram_status', 'owner', 'recorded_date', 'equipment', @@ -21,6 +27,29 @@ class RecordingAdmin(admin.ModelAdmin): # list_select_related = ['owner'] search_fields = ['name'] + actions = ['compute_spectrograms'] autocomplete_fields = ['owner'] readonly_fields = ['created', 'modified'] + + @admin.display( + description='Spectrogram', + empty_value='Not computed', + ) + def spectrogram_status(self, recording: Recording): + if recording.has_spectrogram: + spectrogram = recording.spectrogram + href = reverse('admin:core_spectrogram_change', args=(spectrogram.pk,)) + description = str(spectrogram) + link = mark_safe(f'{description}') + return link + return None + + @admin.action(description='Compute Spectrograms') + def compute_spectrograms(self, request: HttpRequest, queryset: QuerySet): + counter = 0 + for recording in queryset: + if not recording.has_spectrogram: + recording_compute_spectrogram.delay(recording.pk) + counter += 1 + self.message_user(request, f'{counter} recordings queued', messages.SUCCESS) diff --git a/bats_ai/core/admin/spectrogram.py b/bats_ai/core/admin/spectrogram.py new file mode 100644 index 0000000..2586ebb --- /dev/null +++ b/bats_ai/core/admin/spectrogram.py @@ -0,0 +1,31 @@ +from django.contrib import admin + +from bats_ai.core.models import Spectrogram + + +@admin.register(Spectrogram) +class SpectrogramAdmin(admin.ModelAdmin): + list_display = [ + 'pk', + 'recording', + 'image_file', + 'width', + 'height', + 'duration', + 'frequency_min', + 'frequency_max', + ] + list_display_links = ['pk', 'recording'] + list_select_related = True + autocomplete_fields = ['recording'] + readonly_fields = [ + 'recording', + 'image_file', + 'created', + 'modified', + 'width', + 'height', + 'duration', + 'frequency_min', + 'frequency_max', + ] diff --git a/bats_ai/core/migrations/0004_spectrogram.py b/bats_ai/core/migrations/0004_spectrogram.py new file mode 100644 index 0000000..d573437 --- /dev/null +++ b/bats_ai/core/migrations/0004_spectrogram.py @@ -0,0 +1,53 @@ +# Generated by Django 4.1.13 on 2024-01-19 08:48 + +from django.db import migrations, models +import django.db.models.deletion +import django_extensions.db.fields + + +class Migration(migrations.Migration): + dependencies = [ + ('core', '0003_annotations'), + ] + + operations = [ + migrations.CreateModel( + name='Spectrogram', + fields=[ + ( + 'id', + models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name='ID' + ), + ), + ( + 'created', + django_extensions.db.fields.CreationDateTimeField( + auto_now_add=True, verbose_name='created' + ), + ), + ( + 'modified', + django_extensions.db.fields.ModificationDateTimeField( + auto_now=True, verbose_name='modified' + ), + ), + ('image_file', models.FileField(upload_to='')), + ('width', models.IntegerField()), + ('height', models.IntegerField()), + ('duration', models.IntegerField()), + ('frequency_min', models.IntegerField()), + ('frequency_max', models.IntegerField()), + ( + 'recording', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to='core.recording' + ), + ), + ], + options={ + 'get_latest_by': 'modified', + 'abstract': False, + }, + ), + ] diff --git a/bats_ai/core/models/__init__.py b/bats_ai/core/models/__init__.py index 5610854..3dfd89d 100644 --- a/bats_ai/core/models/__init__.py +++ b/bats_ai/core/models/__init__.py @@ -3,11 +3,13 @@ from .recording import Recording from .recording_annotation_status import RecordingAnnotationStatus from .species import Species +from .spectrogram import Spectrogram __all__ = [ + 'Annotations', 'Image', 'Recording', 'RecordingAnnotationStatus', 'Species', - 'Annotations', + 'Spectrogram', ] diff --git a/bats_ai/core/models/recording.py b/bats_ai/core/models/recording.py index 8724421..28a2bd8 100644 --- a/bats_ai/core/models/recording.py +++ b/bats_ai/core/models/recording.py @@ -2,10 +2,6 @@ from django.contrib.gis.db import models from django_extensions.db.models import TimeStampedModel -FREQ_MIN = 5e3 -FREQ_MAX = 200e3 -FREQ_PAD = 2e3 - # TimeStampedModel also provides "created" and "modified" fields class Recording(TimeStampedModel, models.Model): @@ -19,102 +15,30 @@ class Recording(TimeStampedModel, models.Model): grts_cell_id = models.IntegerField(blank=True, null=True) grts_cell = models.IntegerField(blank=True, null=True) - def generate_spectrogram(self): - import base64 - import io - from io import BytesIO - import math - - from PIL import Image - import django - import librosa - import matplotlib.pyplot as plt - import numpy as np - - wav_file = self.audio_file - try: - if isinstance(wav_file, django.db.models.fields.files.FieldFile): - sig, sr = librosa.load(BytesIO(wav_file.read()), sr=None) - wav_file.name - else: - sig, sr = librosa.load(wav_file, sr=None) - - duration = len(sig) / sr - except Exception as e: - print(e) - return None - - # Function to take a signal and return a spectrogram. - size = int(0.001 * sr) # 1.0ms resolution - size = 2 ** math.ceil(math.log(size, 2)) - - # Short-time Fourier Transform - window = librosa.stft(sig, n_fft=size, window='hamming') - - # Calculating and processing data for the spectrogram. - window = np.abs(window) ** 2 - window = librosa.power_to_db(window) - - # Denoise spectrogram - window -= np.median(window, axis=1, keepdims=True) - window -= np.median(window, axis=0, keepdims=True) - - bands = librosa.fft_frequencies(sr=sr, n_fft=size) - for index in range(len(bands)): - band_min = bands[index] - band_max = bands[index + 1] if index < len(bands) - 1 else np.inf - if band_max <= FREQ_MIN or FREQ_MAX <= band_min: - window[index, :] = -1 - - window = np.clip(window, 0, None) - - try: - alpha = 8 - h, w = window.shape - figsize = (int(math.ceil(w / h)) * alpha, alpha) - fig = plt.figure(figsize=figsize, facecolor='black', dpi=100) - ax = plt.axes() - plt.margins(0) + @property + def has_spectrogram(self): + return len(self.spectrograms) > 0 - # Plot - hop_length = int(size / 4) - librosa.display.specshow( - window, sr=sr, hop_length=hop_length, x_axis='s', y_axis='linear', ax=ax - ) + @property + def spectrograms(self): + from bats_ai.core.models import Spectrogram - freq_low = FREQ_MIN - FREQ_PAD - freq_high = FREQ_MAX + FREQ_PAD - ax.set_ylim(freq_low, freq_high) + query = Spectrogram.objects.filter(recording=self).order_by('-created') + return query.all() - ax.axis('off') + @property + def spectrogram(self): + from bats_ai.core.models import Spectrogram - buf = io.BytesIO() - fig.savefig(buf, bbox_inches='tight', pad_inches=0) - buf.seek(0) - img = Image.open(buf) - w, h = img.size + spectrograms = self.spectrograms - # import IPython + if len(spectrograms) == 0: + Spectrogram.generate(self) - # IPython.embed() - # img.save('temp.png') + spectrograms = self.spectrograms + assert len(spectrograms) == 1 - buf = io.BytesIO() - img.save(buf, format='JPEG') - img_base64 = base64.b64encode(buf.getvalue()) - finally: - fig.clf() - plt.close() + assert len(spectrograms) >= 1 + spectrogram = spectrograms[0] # most recently created - # Return dictionary with all required fields - return { - 'base64_spectrogram': img_base64, - 'spectroInfo': { - 'width': w, - 'height': h, - 'start_time': 0.0, - 'end_time': int(math.ceil(duration * 1000.0)), # in milliseconds - 'low_freq': freq_low, - 'high_freq': freq_high, - }, - } + return spectrogram diff --git a/bats_ai/core/models/spectrogram.py b/bats_ai/core/models/spectrogram.py new file mode 100644 index 0000000..dafe19f --- /dev/null +++ b/bats_ai/core/models/spectrogram.py @@ -0,0 +1,265 @@ +import base64 +import io +import math + +from PIL import Image +import cv2 +from django.core.files import File +from django.db import models +from django.db.models.fields.files import FieldFile +from django_extensions.db.models import TimeStampedModel +import librosa +import matplotlib.pyplot as plt +import numpy as np +import scipy + +from bats_ai.core.models import Recording + +FREQ_MIN = 5e3 +FREQ_MAX = 200e3 +FREQ_PAD = 2e3 + + +# TimeStampedModel also provides "created" and "modified" fields +class Spectrogram(TimeStampedModel, models.Model): + recording = models.ForeignKey(Recording, on_delete=models.CASCADE) + image_file = models.FileField() + width = models.IntegerField() # pixels + height = models.IntegerField() # pixels + duration = models.IntegerField() # milliseconds + frequency_min = models.IntegerField() # hz + frequency_max = models.IntegerField() # hz + + @classmethod + def generate(cls, recording): + wav_file = recording.audio_file + try: + if isinstance(wav_file, FieldFile): + sig, sr = librosa.load(io.BytesIO(wav_file.read()), sr=None) + wav_file.name + else: + sig, sr = librosa.load(wav_file, sr=None) + + duration = len(sig) / sr + except Exception as e: + print(e) + return None + + # Function to take a signal and return a spectrogram. + size = int(0.001 * sr) # 1.0ms resolution + size = 2 ** math.ceil(math.log(size, 2)) + hop_length = int(size / 4) + + # Short-time Fourier Transform + window = librosa.stft(sig, n_fft=size, window='hamming') + + # Calculating and processing data for the spectrogram. + window = np.abs(window) ** 2 + window = librosa.power_to_db(window) + + # Denoise spectrogram + # Subtract median frequency + window -= np.median(window, axis=1, keepdims=True) + + # Subtract mean amplitude + window_ = window[window > 0] + thresh = np.median(window_) + window[window <= thresh] = 0 + + bands = librosa.fft_frequencies(sr=sr, n_fft=size) + for index in range(len(bands)): + band_min = bands[index] + band_max = bands[index + 1] if index < len(bands) - 1 else np.inf + if band_max <= FREQ_MIN or FREQ_MAX <= band_min: + window[index, :] = -1 + + window = np.clip(window, 0, None) + + freq_low = int(FREQ_MIN - FREQ_PAD) + freq_high = int(FREQ_MAX + FREQ_PAD) + dpi = 300 + + chunksize = int(1e4) + arange = np.arange(chunksize, window.shape[1], chunksize) + chunks = np.array_split(window, arange, axis=1) + + imgs = [] + for chunk in chunks: + h, w = chunk.shape + alpha = 3 + figsize = (int(math.ceil(w / h)) * alpha + 1, alpha) + fig = plt.figure(figsize=figsize, facecolor='black', dpi=dpi) + ax = plt.axes() + plt.margins(0) + + # Plot + librosa.display.specshow( + chunk, sr=sr, hop_length=hop_length, x_axis='s', y_axis='linear', ax=ax + ) + + ax.set_ylim(freq_low, freq_high) + ax.axis('off') + + buf = io.BytesIO() + fig.savefig(buf, bbox_inches='tight', pad_inches=0) + + fig.clf() + plt.close() + + buf.seek(0) + img = Image.open(buf) + + img = np.array(img) + mask = img[:, :, -1] + flags = np.where(np.sum(mask != 0, axis=0) == 0)[0] + index = flags.min() if len(flags) > 0 else img.shape[1] + img = img[:, :index, :3] + + imgs.append(img) + + img = np.hstack(imgs) + img = Image.fromarray(img, 'RGB') + + w, h = img.size + # ratio = dpi / h + # w_ = int(round(w * ratio)) + w_ = int(4.0 * duration * 1e3) + h_ = int(dpi) + img = img.resize((w_, h_), resample=Image.Resampling.LANCZOS) + w, h = img.size + + # img.save('temp.jpg') + + buf = io.BytesIO() + img.save(buf, format='JPEG', quality=80) + buf.seek(0) + + name = 'spectrogram.jpg' + image_file = File(buf, name=name) + + spectrogram = cls( + recording_id=recording.pk, + image_file=image_file, + width=w, + height=h, + duration=math.ceil(duration * 1e3), + frequency_min=freq_low, + frequency_max=freq_high, + ) + spectrogram.save() + + @property + def compressed(self): + img = self.image_np + + canvas = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + canvas = canvas.astype(np.float32) + + amplitude = canvas.max(axis=0) + amplitude -= amplitude.min() + amplitude /= amplitude.max() + amplitude[amplitude < 0.5] = 0.0 + amplitude[amplitude > 0] = 1.0 + amplitude = amplitude.reshape(1, -1) + + canvas -= canvas.min() + canvas /= canvas.max() + canvas *= 255.0 + canvas *= amplitude + canvas = np.around(canvas).astype(np.uint8) + + mask = canvas.max(axis=0) + mask = scipy.signal.medfilt(mask, 3) + mask[0] = 0 + mask[-1] = 0 + starts = [] + stops = [] + for index in range(1, len(mask) - 1): + value_pre = mask[index - 1] + value = mask[index] + value_post = mask[index + 1] + if value != 0: + if value_pre == 0: + starts.append(index) + if value_post == 0: + stops.append(index) + assert len(starts) == len(stops) + + starts = [val - 40 for val in starts] # 10 ms buffer + stops = [val + 40 for val in stops] # 10 ms buffer + ranges = list(zip(starts, stops)) + + while True: + found = False + merged = [] + index = 0 + while index < len(ranges) - 1: + start1, stop1 = ranges[index] + start2, stop2 = ranges[index + 1] + if stop1 >= start2: + found = True + merged.append((start1, stop2)) + index += 2 + else: + merged.append((start1, stop1)) + index += 1 + if index == len(ranges) - 1: + merged.append((start2, stop2)) + ranges = merged + if not found: + for index in range(1, len(ranges)): + start1, stop1 = ranges[index - 1] + start2, stop2 = ranges[index] + assert start1 < stop1 + assert start2 < stop2 + assert start1 < start2 + assert stop1 < stop2 + assert stop1 < start2 + break + + segments = [] + starts_ = [] + stops_ = [] + domain = img.shape[1] + for start, stop in ranges: + segment = img[:, start:stop] + segments.append(segment) + + starts_.append(int(round(self.duration * (start / domain)))) + stops_.append(int(round(self.duration * (stop / domain)))) + + # buffer = np.zeros((len(img), 20, 3), dtype=img.dtype) + # segments.append(buffer) + # segments = segments[:-1] + + canvas = np.hstack(segments) + canvas = Image.fromarray(canvas, 'RGB') + + # canvas.save('temp.jpg') + + buf = io.BytesIO() + canvas.save(buf, format='JPEG', quality=80) + buf.seek(0) + img_base64 = base64.b64encode(buf.getvalue()).decode('utf-8') + + return img_base64, starts_, stops_ + + @property + def image_np(self): + return np.array(self.image) + + @property + def image_pil(self): + return self.image + + @property + def image(self): + img = Image.open(self.image_file) + return img + + @property + def base64(self): + img = self.image_file.read() + img_base64 = base64.b64encode(img).decode('utf-8') + + return img_base64 diff --git a/bats_ai/core/tasks.py b/bats_ai/core/tasks.py index 24bb987..a2ffdc3 100644 --- a/bats_ai/core/tasks.py +++ b/bats_ai/core/tasks.py @@ -1,6 +1,6 @@ from celery import shared_task -from bats_ai.core.models import Image +from bats_ai.core.models import Image, Recording @shared_task @@ -8,3 +8,11 @@ def image_compute_checksum(image_id: int): image = Image.objects.get(pk=image_id) image.compute_checksum() image.save() + + +@shared_task +def recording_compute_spectrogram(recording_id: int): + recording = Recording.objects.get(pk=recording_id) + if not recording.has_spectrogram: + recording.spectrogram # compute by simply referenceing the attribute + assert recording.has_spectrogram diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index bedf371..a6b4fd8 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -56,19 +56,6 @@ class UpdateAnnotationsSchema(Schema): id: int | None -def get_user(request: HttpRequest): - auth_header = request.headers.get('Authorization', None) - if auth_header is not None: - token = request.headers.get('Authorization').replace('Bearer ', '') - token_found = AccessToken.objects.get(token=token) - if not token_found: - raise HttpError(401, 'Authentication credentials were not provided.') - return token_found.user - elif request.user: - logger.warning(f'User: {request.user}') - return request.user - - @router.post('/') def create_recording( request: HttpRequest, payload: Form[RecordingUploadSchema], audio_file: File[UploadedFile] @@ -108,7 +95,51 @@ def get_spectrogram(request: HttpRequest, id: int): except Recording.DoesNotExist: return {'error': 'Recording not found'} - spectro_data = recording.generate_spectrogram() + spectrogram = recording.spectrogram + + spectro_data = { + 'base64_spectrogram': spectrogram.base64, + 'spectroInfo': { + 'width': spectrogram.width, + 'height': spectrogram.height, + 'start_time': 0, + 'end_time': spectrogram.duration, + 'low_freq': spectrogram.frequency_min, + 'high_freq': spectrogram.frequency_max, + }, + } + + annotations_qs = Annotations.objects.filter(recording=recording, owner=request.user) + + # Serialize the annotations using AnnotationSchema + annotations_data = [ + AnnotationSchema.from_orm(annotation).dict() for annotation in annotations_qs + ] + spectro_data['annotations'] = annotations_data + return spectro_data + + +@router.get('/{id}/spectrogram/compressed') +def get_spectrogram_compressed(request: HttpRequest, id: int): + try: + recording = Recording.objects.get(pk=id) + except Recording.DoesNotExist: + return {'error': 'Recording not found'} + + spectrogram = recording.spectrogram + compressed, starts, ends = spectrogram.compressed + + spectro_data = { + 'base64_spectrogram': compressed, + 'spectroInfo': { + 'width': spectrogram.width, + 'height': spectrogram.height, + 'start_times': starts, + 'end_times': ends, + 'low_freq': spectrogram.frequency_min, + 'high_freq': spectrogram.frequency_max, + }, + } annotations_qs = Annotations.objects.filter(recording=recording, owner=request.user) From 856b620fadec09545d4b8f81e7f17d480af00a86 Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Fri, 19 Jan 2024 14:24:58 -0800 Subject: [PATCH 09/12] Bug fixes --- bats_ai/core/models/spectrogram.py | 8 +++++--- docker-compose.override.yml | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/bats_ai/core/models/spectrogram.py b/bats_ai/core/models/spectrogram.py index dafe19f..bcc9cf2 100644 --- a/bats_ai/core/models/spectrogram.py +++ b/bats_ai/core/models/spectrogram.py @@ -16,7 +16,7 @@ from bats_ai.core.models import Recording FREQ_MIN = 5e3 -FREQ_MAX = 200e3 +FREQ_MAX = 120e3 FREQ_PAD = 2e3 @@ -77,9 +77,11 @@ def generate(cls, recording): freq_low = int(FREQ_MIN - FREQ_PAD) freq_high = int(FREQ_MAX + FREQ_PAD) + vmin = window.min() + vmax = window.max() dpi = 300 - chunksize = int(1e4) + chunksize = int(5e3) arange = np.arange(chunksize, window.shape[1], chunksize) chunks = np.array_split(window, arange, axis=1) @@ -94,7 +96,7 @@ def generate(cls, recording): # Plot librosa.display.specshow( - chunk, sr=sr, hop_length=hop_length, x_axis='s', y_axis='linear', ax=ax + chunk, sr=sr, hop_length=hop_length, x_axis='s', y_axis='linear', ax=ax, vmin=vmin, vmax=vmax, ) ax.set_ylim(freq_low, freq_high) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 2e12974..6186b2d 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -4,9 +4,10 @@ services: build: context: . dockerfile: ./dev/django.Dockerfile - command: ["./manage.py", "runserver", "0.0.0.0:8000", "--noreload"] - # entrypoint: ["/bin/bash"] - # command: "" + # command: ["./manage.py", "runserver", "0.0.0.0:8000"] + # ./manage.py runserver 0.0.0.0:8000 --noreload + entrypoint: ["/bin/bash"] + command: "" # Log printing via Rich is enhanced by a TTY tty: true env_file: ./dev/.env.docker-compose From 57e4f0114fd6cc94aa3794c0cf4203dbb9180382 Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Fri, 19 Jan 2024 14:28:43 -0800 Subject: [PATCH 10/12] Linting updates --- bats_ai/core/models/spectrogram.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bats_ai/core/models/spectrogram.py b/bats_ai/core/models/spectrogram.py index bcc9cf2..223ff82 100644 --- a/bats_ai/core/models/spectrogram.py +++ b/bats_ai/core/models/spectrogram.py @@ -96,7 +96,14 @@ def generate(cls, recording): # Plot librosa.display.specshow( - chunk, sr=sr, hop_length=hop_length, x_axis='s', y_axis='linear', ax=ax, vmin=vmin, vmax=vmax, + chunk, + sr=sr, + hop_length=hop_length, + x_axis='s', + y_axis='linear', + ax=ax, + vmin=vmin, + vmax=vmax, ) ax.set_ylim(freq_low, freq_high) From d809c13cf563c2ad00ed5aa1126370a5a110dbd1 Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Fri, 19 Jan 2024 14:35:07 -0800 Subject: [PATCH 11/12] Revert docker-compose override --- docker-compose.override.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 6186b2d..70355ce 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -4,10 +4,10 @@ services: build: context: . dockerfile: ./dev/django.Dockerfile - # command: ["./manage.py", "runserver", "0.0.0.0:8000"] + command: ["./manage.py", "runserver", "0.0.0.0:8000"] # ./manage.py runserver 0.0.0.0:8000 --noreload - entrypoint: ["/bin/bash"] - command: "" + # entrypoint: ["/bin/bash"] + # command: "" # Log printing via Rich is enhanced by a TTY tty: true env_file: ./dev/.env.docker-compose From 77f23067fca89f4f488f0ee3e65658418d01fda9 Mon Sep 17 00:00:00 2001 From: Jason Parham Date: Fri, 19 Jan 2024 15:32:25 -0800 Subject: [PATCH 12/12] Improve stability of compressed spectrogram generation --- bats_ai/core/models/spectrogram.py | 168 ++++++++++++++++------------- 1 file changed, 94 insertions(+), 74 deletions(-) diff --git a/bats_ai/core/models/spectrogram.py b/bats_ai/core/models/spectrogram.py index 223ff82..72e61e1 100644 --- a/bats_ai/core/models/spectrogram.py +++ b/bats_ai/core/models/spectrogram.py @@ -161,87 +161,107 @@ def generate(cls, recording): def compressed(self): img = self.image_np - canvas = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) - canvas = canvas.astype(np.float32) - - amplitude = canvas.max(axis=0) - amplitude -= amplitude.min() - amplitude /= amplitude.max() - amplitude[amplitude < 0.5] = 0.0 - amplitude[amplitude > 0] = 1.0 - amplitude = amplitude.reshape(1, -1) - - canvas -= canvas.min() - canvas /= canvas.max() - canvas *= 255.0 - canvas *= amplitude - canvas = np.around(canvas).astype(np.uint8) - - mask = canvas.max(axis=0) - mask = scipy.signal.medfilt(mask, 3) - mask[0] = 0 - mask[-1] = 0 - starts = [] - stops = [] - for index in range(1, len(mask) - 1): - value_pre = mask[index - 1] - value = mask[index] - value_post = mask[index + 1] - if value != 0: - if value_pre == 0: - starts.append(index) - if value_post == 0: - stops.append(index) - assert len(starts) == len(stops) - - starts = [val - 40 for val in starts] # 10 ms buffer - stops = [val + 40 for val in stops] # 10 ms buffer - ranges = list(zip(starts, stops)) - + threshold = 0.5 while True: - found = False - merged = [] - index = 0 - while index < len(ranges) - 1: - start1, stop1 = ranges[index] - start2, stop2 = ranges[index + 1] - if stop1 >= start2: - found = True - merged.append((start1, stop2)) - index += 2 - else: - merged.append((start1, stop1)) - index += 1 + canvas = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) + canvas = canvas.astype(np.float32) + + amplitude = canvas.max(axis=0) + amplitude -= amplitude.min() + amplitude /= amplitude.max() + amplitude[amplitude < threshold] = 0.0 + amplitude[amplitude > 0] = 1.0 + amplitude = amplitude.reshape(1, -1) + + canvas -= canvas.min() + canvas /= canvas.max() + canvas *= 255.0 + canvas *= amplitude + canvas = np.around(canvas).astype(np.uint8) + + mask = canvas.max(axis=0) + mask = scipy.signal.medfilt(mask, 3) + mask[0] = 0 + mask[-1] = 0 + starts = [] + stops = [] + for index in range(1, len(mask) - 1): + value_pre = mask[index - 1] + value = mask[index] + value_post = mask[index + 1] + if value != 0: + if value_pre == 0: + starts.append(index) + if value_post == 0: + stops.append(index) + assert len(starts) == len(stops) + + starts = [val - 40 for val in starts] # 10 ms buffer + stops = [val + 40 for val in stops] # 10 ms buffer + ranges = list(zip(starts, stops)) + + while True: + found = False + merged = [] + index = 0 + while index < len(ranges) - 1: + start1, stop1 = ranges[index] + start2, stop2 = ranges[index + 1] + + start1 = min(max(start1, 0), len(mask)) + start2 = min(max(start2, 0), len(mask)) + stop1 = min(max(stop1, 0), len(mask)) + stop2 = min(max(stop2, 0), len(mask)) + + if stop1 >= start2: + found = True + merged.append((start1, stop2)) + index += 2 + else: + merged.append((start1, stop1)) + index += 1 if index == len(ranges) - 1: merged.append((start2, stop2)) - ranges = merged - if not found: - for index in range(1, len(ranges)): - start1, stop1 = ranges[index - 1] - start2, stop2 = ranges[index] - assert start1 < stop1 - assert start2 < stop2 - assert start1 < start2 - assert stop1 < stop2 - assert stop1 < start2 + ranges = merged + if not found: + for index in range(1, len(ranges)): + start1, stop1 = ranges[index - 1] + start2, stop2 = ranges[index] + assert start1 < stop1 + assert start2 < stop2 + assert start1 < start2 + assert stop1 < stop2 + assert stop1 < start2 + break + + segments = [] + starts_ = [] + stops_ = [] + domain = img.shape[1] + for start, stop in ranges: + segment = img[:, start:stop] + segments.append(segment) + + starts_.append(int(round(self.duration * (start / domain)))) + stops_.append(int(round(self.duration * (stop / domain)))) + + # buffer = np.zeros((len(img), 20, 3), dtype=img.dtype) + # segments.append(buffer) + # segments = segments[:-1] + + if len(segments) > 0: break - segments = [] - starts_ = [] - stops_ = [] - domain = img.shape[1] - for start, stop in ranges: - segment = img[:, start:stop] - segments.append(segment) - - starts_.append(int(round(self.duration * (start / domain)))) - stops_.append(int(round(self.duration * (stop / domain)))) + threshold -= 0.05 + if threshold < 0: + segments = None + break - # buffer = np.zeros((len(img), 20, 3), dtype=img.dtype) - # segments.append(buffer) - # segments = segments[:-1] + if segments is None: + canvas = img.copy() + else: + canvas = np.hstack(segments) - canvas = np.hstack(segments) canvas = Image.fromarray(canvas, 'RGB') # canvas.save('temp.jpg')