From dba0d19149adbcc152723e97ad3bb5567d74b581 Mon Sep 17 00:00:00 2001 From: Peter Abrahamsen Date: Tue, 29 Sep 2026 16:06:36 -0700 Subject: [PATCH] feat: render with ffmpeg alone, from and to URLs the caller presigns The Python stack (librosa, numba, OpenCV, matplotlib) is replaced by one ffmpeg command: showspectrumpic draws the spectrogram as intensity and pseudocolor applies matplotlib's viridis, which showspectrumpic's own palette does not match. The image is sized from the decoded sample count so tiles keep the archive's width, and the dB window is converted to ffmpeg's full-scale reference by an offset fitted against a production image. On the same segment the result has production's luminance distribution to within 0.01 and the same width. The request now carries presigned GET and PUT URLs and explicit render parameters, echoed back in the response, so the function knows nothing about S3. The previous bucket/key shape still works, via boto3 from the base image, until orcasite has switched. Image 2.27 GB -> 773 MB, no caches to seed, render 2 s at 1024 MB. Co-Authored-By: Claude Fable 5.1 --- README.md | 37 ++-- core/Dockerfile | 39 ++--- core/__init__.py | 0 core/app.py | 252 +++++++++++++-------------- core/requirements.txt | 5 - core/sandbox.ipynb | 317 ---------------------------------- core/spectrogram_generator.py | 110 ------------ core/warm.py | 51 ------ docs/comparison.png | Bin 0 -> 71322 bytes events/spectrogram_job.json | 14 +- template.yaml | 9 +- tests/smoke.py | 67 ++++--- 12 files changed, 196 insertions(+), 705 deletions(-) delete mode 100644 core/__init__.py delete mode 100644 core/requirements.txt delete mode 100644 core/sandbox.ipynb delete mode 100644 core/spectrogram_generator.py delete mode 100644 core/warm.py create mode 100644 docs/comparison.png diff --git a/README.md b/README.md index 00be66e..193330c 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,28 @@ # spectrogram-renderer -A Lambda function that renders one spectrogram PNG from one hydrophone audio segment. [Orcasite](https://github.com/orcasound/orcasite) invokes it by name from [`Orcasite.Radio.AwsClient`](https://github.com/orcasound/orcasite/blob/main/server/lib/orcasite/radio/aws_client.ex), once per `AudioImage`, passing the S3 location of the segment and where to put the image. It has no other entry point. +A Lambda function that renders one spectrogram PNG from one hydrophone audio segment. [Orcasite](https://github.com/orcasound/orcasite) invokes it by name, once per `AudioImage`, and stores what it answers as the image's parameters. It has no other entry point. -- [`core/app.py`](core/app.py): the handler. Downloads the segment, decodes it with ffmpeg, renders with [`core/spectrogram_generator.py`](core/spectrogram_generator.py), uploads the PNG. -- [`core/Dockerfile`](core/Dockerfile): the image, built on the AWS Lambda Python base with a static ffmpeg. -- [`template.yaml`](template.yaml): the function's memory, timeout and IAM policy, as a SAM template. Stack name (`audio-viz`, kept from when this lived in orcasite) and region are in [`samconfig.toml`](samconfig.toml). +The whole render is one ffmpeg command ([`core/app.py`](core/app.py), `render`): `showspectrumpic` draws the spectrogram as intensity and `pseudocolor` applies matplotlib's colour ramp. The handler around it fetches the audio, probes its sample rate and duration to size the image, and stores the PNG. There is no Python beyond the standard library, so the image is the Lambda base plus a static ffmpeg, and a cold start costs about a second. -## Cold starts +## Contract -A new Lambda container starts with an empty `/tmp`, where numba, librosa and matplotlib keep their caches, so the first render in a container recompiles librosa's numba functions and rebuilds the font list. The Dockerfile runs [`warm.py`](core/warm.py) once at build time and keeps the caches it leaves in the image; [`app.py`](core/app.py) copies them into `/tmp` before importing those libraries. If you add a library that caches on first use, give it a directory under `/tmp` in the Dockerfile and add it to the same `mv`. +```json +{ + "audio_url": "https://… (presigned GET)", + "image_url": "https://… (presigned PUT), or null to render and discard", + "parameters": {"n_fft": 1024, "hop_length": 256, "fmin": 1, "fmax": 15000, "db_min": 10, "db_max": 80, "cmap": "viridis", "frequency_scale": "linear"} +} +``` + +The function reads and writes only the URLs it is given and knows nothing about what is behind them; the caller decides where audio and images live and presigns accordingly. Every parameter is optional and defaults to what orcasite has always used, so images stay consistent with the existing archive. The response gives `image_size`, `sample_rate`, `width`, `height` and the `parameters` applied, plus the field names orcasite has stored since the first renderer. + +The image is `n_fft/2` pixels high and one pixel wide per `hop_length` samples, as the previous renderer's STFT was. `db_min`/`db_max` are the dB window relative to an amplitude of 0.01, the convention the archive was drawn with; `DB_OFFSET` in `app.py` converts it to ffmpeg's full-scale dB and was fitted against a production image, whose luminance distribution the new render reproduces to within 0.01. -To check a deployed function, find `platform.report` records in its CloudWatch log (the template sets `LogFormat: JSON`). Cold invocations carry `initDurationMs`; their `durationMs + initDurationMs` should be within a couple of seconds of a warm invocation's `durationMs`. Lambda's `Duration` metric excludes init time, so it alone understates a cold start. +Until orcasite has switched, the previous shape (`audio_bucket`/`audio_key`, `image_bucket`/`image_key`, read and written with boto3) is also accepted; [`template.yaml`](template.yaml) keeps the S3 policy for it and both go once no caller uses them. ## Testing -[`tests/smoke.py`](tests/smoke.py) renders a synthetic MPEG-TS clip inside the built image, run as an unprivileged user with a root-owned empty `/tmp` and the CPU and memory of the deployed tier, which is how Lambda runs it. It fails if the first render is slow enough to suggest the caches were not restored. [CI](.github/workflows/ci.yml) runs it on every pull request; locally: +[`tests/smoke.py`](tests/smoke.py) renders a synthetic AAC/MPEG-TS clip through the handler inside the built image, run as an unprivileged user with a root-owned empty `/tmp` and the CPU and memory of the deployed tier, which is how Lambda runs it. [CI](.github/workflows/ci.yml) runs it on every pull request; locally: ```bash docker build -t spectrogram-renderer core @@ -22,20 +30,15 @@ docker run --rm --user 1000:1000 --tmpfs /tmp:uid=0,gid=0,mode=1777 --cpus 0.58 --entrypoint python -v "$PWD/tests/smoke.py:/var/task/smoke.py" spectrogram-renderer smoke.py ``` -That rehearses the application, not the sandbox: Lambda's Runtime Interface Emulator in the base image reproduces the invoke API, not the filesystem rules. Anything that touches `/tmp`'s own metadata or relies on image contents under `/tmp` will pass locally on a plain tmpfs and fail deployed, hence the `--user` and `--tmpfs` flags above. +That rehearses the application, not the sandbox: Lambda's Runtime Interface Emulator in the base image reproduces the invoke API, not the filesystem rules, hence the `--user` and `--tmpfs` flags. ## Deploying -Merging to `main` deploys: the [CI workflow](.github/workflows/ci.yml) runs the smoke test, then `sam build && sam deploy` under the `production` environment, assuming the `spectrogram-renderer-deploy` IAM role through GitHub's OIDC provider. That role can change only this stack, its ECR repository and its function role. +Merging to `main` deploys: the [CI workflow](.github/workflows/ci.yml) runs the smoke test, then `sam build && sam deploy` under the `production` environment, assuming the `spectrogram-renderer-deploy` IAM role through GitHub's OIDC provider. That role can change only this stack (`audio-viz`, the name kept from when this lived in orcasite), its ECR repository and its function role. -To deploy from a machine instead, with an AWS profile for the Orcasound account and the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html): - -```bash -sam build -AWS_PROFILE=orcasound sam deploy -``` +To deploy from a machine instead, with an AWS profile for the Orcasound account and the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html): `sam build && AWS_PROFILE=orcasound sam deploy`. -To invoke the deployed function on a real segment without writing anything (`image_key` null skips the upload): +To invoke the deployed function on a real segment without writing anything ([`events/spectrogram_job.json`](events/spectrogram_job.json) reads from the public audio bucket and has `image_url` null): ```bash aws lambda invoke --function-name "$(aws lambda list-functions --query "Functions[?contains(FunctionName,'AudioVizFunction')].FunctionName | [0]" --output text)" \ diff --git a/core/Dockerfile b/core/Dockerfile index 08f2817..34d5b8b 100644 --- a/core/Dockerfile +++ b/core/Dockerfile @@ -1,35 +1,16 @@ FROM public.ecr.aws/lambda/python:3.14 +# ffmpeg does all the work; the handler is standard library plus the boto3 +# the base image ships, so there is nothing to pip install and no cache to +# warm. A static build keeps the image at the base plus one binary. ARG FFMPEG_VERSION=ffmpeg-7.0.2-amd64-static +RUN dnf install -y tar xz \ + && curl -sO https://johnvansickle.com/ffmpeg/releases/${FFMPEG_VERSION}.tar.xz \ + && tar -xf ${FFMPEG_VERSION}.tar.xz \ + && mv ${FFMPEG_VERSION}/ffmpeg ${FFMPEG_VERSION}/ffprobe /usr/local/bin/ \ + && rm -rf ${FFMPEG_VERSION} ${FFMPEG_VERSION}.tar.xz \ + && dnf clean all -# /tmp is the only writeable directory at runtime, but Lambda mounts a fresh, -# empty one for every new container, so anything put there at build time is -# gone by the first invocation. The caches these libraries build on first use -# are therefore built into the image (see warm.py below) and copied into /tmp -# by app.py when a container starts. -ENV CACHE_SEED=/opt/cache-seed -ENV MPLCONFIGDIR=/tmp/matplotlib -ENV LIBROSA_CACHE_DIR=/tmp/librosa_cache -ENV NUMBA_CACHE_DIR=/tmp/numba_cache +COPY app.py ./ -RUN dnf install -y tar gzip xz - -RUN curl -O https://johnvansickle.com/ffmpeg/releases/${FFMPEG_VERSION}.tar.xz -RUN tar -xvf "${FFMPEG_VERSION}.tar.xz" -RUN mv ${FFMPEG_VERSION}/ffmpeg /usr/local/bin -RUN mv ${FFMPEG_VERSION}/ffprobe /usr/local/bin - -COPY app.py requirements.txt spectrogram_generator.py warm.py ./ -RUN mkdir -p data - -RUN python3 -m pip install -r requirements.txt -t . - -# Render one clip so that every cache a render populates exists before the -# first real invocation, then keep the result where app.py can find it. -RUN mkdir -p ${MPLCONFIGDIR} ${LIBROSA_CACHE_DIR} ${NUMBA_CACHE_DIR} \ - && python3 warm.py \ - && mkdir -p ${CACHE_SEED} \ - && mv ${MPLCONFIGDIR} ${LIBROSA_CACHE_DIR} ${NUMBA_CACHE_DIR} ${CACHE_SEED}/ - -# Command can be overwritten by providing a different command in the template directly. CMD ["app.lambda_handler"] diff --git a/core/__init__.py b/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/app.py b/core/app.py index 4b0373e..fbbcecf 100644 --- a/core/app.py +++ b/core/app.py @@ -1,159 +1,145 @@ -import os -import shutil - -# Every new Lambda container starts with an empty /tmp, where numba, librosa -# and matplotlib keep their caches. Without them the first render recompiles -# librosa's numba functions and rebuilds the font list, which at 512 MB took -# longer than the function's timeout. The Dockerfile builds the caches into -# the image; restore them before those libraries load and look for them. -# -# Copy each cache directory separately: copying onto /tmp itself makes -# copytree set /tmp's permissions, which Lambda refuses. A failure here only -# costs a slow first render, so it must never fail the invocation. -_cache_seed = os.environ.get("CACHE_SEED") -if _cache_seed and os.path.isdir(_cache_seed): - for _name in os.listdir(_cache_seed): - try: - shutil.copytree( - os.path.join(_cache_seed, _name), - os.path.join("/tmp", _name), - dirs_exist_ok=True, - ) - except OSError as e: - print(f"Could not restore cache {_name}: {e}") - -from matplotlib.pyplot import imshow -from spectrogram_generator import SpectrogramGenerator -from subprocess import check_output -from typing import TypedDict, Optional -import boto3 -import io -import json -import librosa -import matplotlib -import sys - -SpectrogramJob = TypedDict( - "SpectrogramJob", - { - "id": str, - "audio_bucket": str, - "audio_key": str, - "sample_rate": int, - "image_key": Optional[str], - "image_bucket": Optional[str], - }, -) - - -def lambda_handler(event, context): - """Sample pure Lambda function - - Parameters - ---------- - event: dict, required - API Gateway Lambda Proxy Input Format - - Event doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format - - context: object, required - Lambda Context runtime methods and attributes +"""Renders one spectrogram PNG from one audio segment, with ffmpeg. - Context doc: https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html +The request says where to read the audio and where to write the image, as +URLs; the function knows nothing about what is behind them. Render +parameters are explicit, with the defaults orcasite has always used, and +the response echoes the ones that were applied. - Returns - ------ - API Gateway Lambda Proxy Output Format: dict + { + "audio_url": "https://... (presigned GET)", + "image_url": "https://... (presigned PUT), or null to render and discard", + "parameters": {"fmax": 15000, ...} # optional, see DEFAULTS + } - Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html - """ - result = make_spectrogram(event) +For one release the previous shape is also accepted: `audio_bucket` and +`audio_key`, `image_bucket` and `image_key`, read and written with boto3. +""" - return {"status": 200, **result} +import os +import subprocess +import tempfile +import urllib.request +import wave + +DEFAULTS = { + "n_fft": 1024, + "hop_length": 256, + "fmin": 1, + "fmax": 15000, + "db_min": 10, + "db_max": 80, + "cmap": "viridis", + "frequency_scale": "linear", +} + +# ffmpeg's dB scale is relative to full scale, while the dB window orcasite's +# images were drawn with is relative to an amplitude of 0.01 in librosa's +# STFT. This offset lines the two up: it was fitted against a production +# image, whose luminance distribution it reproduces to within 0.01. +DB_OFFSET = -122 + +FSCALE = {"linear": "lin", "log": "log"} -def make_spectrogram( - job: SpectrogramJob, store_audio=False, show_image=False, store_image=False -): - print(f"Received job: {json.dumps(job)}") - local_path = f"/tmp/{job["id"]}" - s3 = boto3.client("s3") +def lambda_handler(event, context): + return {"status": 200, **make_spectrogram(event)} - if not os.path.exists(local_path): - response = s3.get_object(Bucket=job["audio_bucket"], Key=job["audio_key"]) - audio_bytes = response["Body"].read() - with open(local_path, "wb") as file: - file.write(audio_bytes) - sample_rate = job.get("sample_rate") - if sample_rate is None: - metadata = get_audio_metadata(local_path) - sample_rate = int(metadata["streams"][0]["sample_rate"]) +def make_spectrogram(job): + params = {**DEFAULTS, **(job.get("parameters") or {})} - audio, sr = load_audio(local_path, sample_rate) + with tempfile.TemporaryDirectory(dir="/tmp") as tmp: + audio_path = os.path.join(tmp, "audio") + wav_path = os.path.join(tmp, "audio.wav") + fetch_audio(job, audio_path) + sample_rate, samples = decode(audio_path, wav_path) + width = 1 + samples // params["hop_length"] + height = params["n_fft"] // 2 + png = render(wav_path, params, width, height) - params = {"linear": True, "fmin": 1, "fmax": 15000, "cmap": "viridis"} - - generator = SpectrogramGenerator( - sr, n_fft=1024, hop_length=256, db_range=(10, 80), **params - ) - spectrogram = generator(audio) - if show_image: - matplotlib.pyplot.axis("off") - imshow(spectrogram) - - # Store spectrogram image either as a file or in-memory - image_size = None - image_store = f"/tmp/{job["id"]}.png" if store_image else io.BytesIO() - matplotlib.image.imsave(image_store, spectrogram) - if store_image: - image_size = os.path.getsize(image_store) - else: - image_store.seek(0) - image_size = sys.getsizeof(image_store) - image_store.seek(0) - - if job["image_key"] is not None and job["image_bucket"] is not None: - image_content = open(image_store, "rb") if store_image else image_store - s3.put_object( - Bucket=job["image_bucket"], Key=job["image_key"], Body=image_content - ) + store_image(job, png) return { + "image_size": len(png), "sample_rate": sample_rate, - "image_size": image_size, - "frequency_spacing": "linear" if params["linear"] else "log", + "width": width, + "height": height, + "parameters": params, + # The names orcasite has stored since the first renderer. + "frequency_spacing": params["frequency_scale"], "freq_min": params["fmin"], "freq_max": params["fmax"], "color_map": params["cmap"], } -def load_audio(local_path, sample_rate): - """Decodes the segment with ffmpeg and hands librosa a WAV. +def render(audio_path, params, width, height): + """One ffmpeg invocation: spectrogram as intensity, then the colour map. - The hydrophones stream AAC in MPEG-TS, which libsndfile does not read, and - librosa 1.0 dropped the ffmpeg fallback that used to cover it. + showspectrumpic's own colour maps start at black, so the image is drawn + in grey and matplotlib's ramp applied with pseudocolor, which carries the + same tables. Its window size follows from the height (n_fft/2 bins). """ - wav = check_output( - ["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", local_path, "-f", "wav", "-"] + spectrum = ":".join( + [ + f"s={width}x{height}", + "mode=combined", + "color=intensity", + "scale=log", + f"fscale={FSCALE[params['frequency_scale']]}", + "win_func=hann", + f"start={params['fmin']}", + f"stop={params['fmax']}", + f"limit={params['db_max'] + DB_OFFSET}", + f"drange={params['db_max'] - params['db_min']}", + "legend=0", + ] ) - return librosa.load(io.BytesIO(wav), sr=sample_rate) + graph = f"showspectrumpic={spectrum},format=gbrp,pseudocolor=preset={params['cmap']}" + return subprocess.run( + ["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", audio_path, + "-lavfi", graph, "-f", "image2pipe", "-c:v", "png", "-"], + check=True, + capture_output=True, + ).stdout -def get_audio_metadata(local_path): - result = check_output( - [ - "ffprobe", - "-hide_banner", - "-loglevel", - "panic", - "-show_format", - "-show_streams", - "-of", - "json", - local_path, - ] +def decode(audio_path, wav_path): + """Decodes the segment to WAV and returns its sample rate and length. + + The image is sized from the samples actually decoded: the container's + duration undercounts an AAC stream by a frame or two, which would make + each tile a few pixels narrower than the archive's. + """ + subprocess.run( + ["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", audio_path, "-f", "wav", wav_path], + check=True, + capture_output=True, ) + with wave.open(wav_path, "rb") as wav: + return wav.getframerate(), wav.getnframes() + - return json.loads(result) +def fetch_audio(job, path): + if job.get("audio_url"): + with urllib.request.urlopen(job["audio_url"]) as response, open(path, "wb") as file: + file.write(response.read()) + else: + import boto3 + + boto3.client("s3").download_file(job["audio_bucket"], job["audio_key"].lstrip("/"), path) + + +def store_image(job, png): + if job.get("image_url"): + request = urllib.request.Request( + job["image_url"], data=png, method="PUT", headers={"Content-Type": "image/png"} + ) + with urllib.request.urlopen(request): + pass + elif job.get("image_bucket") and job.get("image_key"): + import boto3 + + boto3.client("s3").put_object( + Bucket=job["image_bucket"], Key=job["image_key"].lstrip("/"), Body=png, ContentType="image/png" + ) diff --git a/core/requirements.txt b/core/requirements.txt deleted file mode 100644 index cfbc5eb..0000000 --- a/core/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -boto3 -librosa -matplotlib -numpy -opencv-python-headless diff --git a/core/sandbox.ipynb b/core/sandbox.ipynb deleted file mode 100644 index f47aabe..0000000 --- a/core/sandbox.ipynb +++ /dev/null @@ -1,317 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Spectrogram sandbox\n", - "\n", - "This notebook is for experimenting with spectrogram parameters.\n", - "\n", - "Ensure you have `ffprobe` available to your computer via your computer's package manager (`brew install ffprobe` on MacOS or `sudo apt-get install ffprobe` on Ubuntu, etc.)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "orca_call_audio_url = \"https://s3-us-west-2.amazonaws.com/streaming-orcasound-net/rpi_orcasound_lab/hls/1719430219/live322.ts\"\n", - "fish_grunt_audio_url = \"https://s3-us-west-2.amazonaws.com/streaming-orcasound-net/rpi_orcasound_lab/hls/1723491018/live516.ts\"\n", - "ferry_vessel_audio_url = \"https://s3-us-west-2.amazonaws.com/streaming-orcasound-net/rpi_north_sjc/hls/1723100418/live6160.ts\"\n", - "hydrophone_hum_water_splashes_audio_url = \"https://s3-us-west-2.amazonaws.com/streaming-orcasound-net/rpi_north_sjc/hls/1718175617/live7424.ts\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%pip install boto3\n", - "%pip install imutils \n", - "%pip install librosa\n", - "%pip install matplotlib\n", - "%pip install numpy<2.0\n", - "%pip install opencv-python \n", - "%pip install requests" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Spectrogram generator via: https://github.com/kylemcdonald/AudioNotebooks/blob/master/Generating%20Spectrograms.ipynb\n", - "\n", - "# Copyright (c) 2016- Kyle McDonald\n", - "# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n", - "# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n", - "# THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n", - "\n", - "import librosa\n", - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "import cv2\n", - "\n", - "class SpectrogramGenerator:\n", - " \"\"\"Create a spectrogram generator.\"\"\"\n", - " \n", - " def __init__(self, sr,\n", - " n_fft=1024,\n", - " linear=False,\n", - " hop_length=None,\n", - " db_ref=0.01,\n", - " db_range=None,\n", - " fmin=None,\n", - " fmax=None,\n", - " cmap=None,\n", - " window='hann',\n", - " drop_nyquist=True,\n", - " flipud=True):\n", - " \"\"\"\n", - " Args:\n", - " sr (int): sample rate\n", - " n_fft (int): size of STFT window in samples\n", - " linear (bool): use linear instead of logarithmic frequency spacing\n", - " hop_length (int): samples between STFT frames\n", - " db_ref (float): `ref` parameter when converting amplitude to dB\n", - " db_range (tuple): min and max dB for output\n", - " fmin (float): minimum frequency to output (hertz)\n", - " fmax (float): maximum frequency to output (hertz)\n", - " cmap (str): matplotlib color map to use for output\n", - " window (str): windowing function for STFT\n", - " drop_nyquist (bool): if True, output has exactly `n_fft//2` bins\n", - " flipud (bool): whether to flip the STFT output\n", - " \"\"\"\n", - "\n", - " self.n_fft = n_fft\n", - " self.hop_length = hop_length\n", - " self.db_ref = db_ref\n", - " self.db_range = db_range\n", - " self.cmap = cmap\n", - " self.window = window\n", - " self.drop_nyquist = drop_nyquist\n", - " \n", - " if hop_length is None:\n", - " self.hop_length = n_fft // 4\n", - " \n", - " if cmap is not None:\n", - " self.cmap = plt.get_cmap(cmap)\n", - " \n", - " bin_count = (n_fft // 2) + 1 \n", - " if drop_nyquist:\n", - " bin_count -= 1\n", - " nyquist = sr // 2\n", - " if fmin == None:\n", - " min_bin = 1\n", - " else:\n", - " min_bin = (fmin / nyquist) * (bin_count - 1)\n", - " if fmax == None:\n", - " max_bin = bin_count - 1\n", - " else:\n", - " max_bin = (fmax / nyquist) * (bin_count - 1)\n", - " bins = np.arange(bin_count)\n", - " if linear:\n", - " y_remap = (bins + min_bin) / ((bin_count - 1) / (max_bin - min_bin))\n", - " else:\n", - " scale_factor = bin_count / np.log10(max_bin / min_bin)\n", - " y_remap = min_bin * 10 ** (bins / scale_factor)\n", - " if flipud:\n", - " y_remap = y_remap[::-1]\n", - " self.mapy_base = y_remap.reshape(-1,1).astype(np.float32)\n", - " \n", - " def __call__(self, audio):\n", - " \"\"\"Generate a spectrogram for visualization.\n", - " \n", - " Returns:\n", - " np.array of type np.uint8 if `db_range` or `cmap` is set, otherwise np.float32\n", - " \"\"\"\n", - " stft = librosa.stft(audio,\n", - " n_fft=self.n_fft,\n", - " hop_length=self.hop_length,\n", - " window=self.window)\n", - " if self.drop_nyquist:\n", - " stft = stft[:-1]\n", - " db = librosa.amplitude_to_db(np.abs(stft), ref=self.db_ref)\n", - " mapx = np.arange(db.shape[1], dtype=np.float32).reshape(1,-1).repeat(db.shape[0], axis=0)\n", - " mapy = self.mapy_base.repeat(db.shape[1], axis=1)\n", - " out = cv2.remap(db, mapx, mapy, cv2.INTER_LANCZOS4)\n", - " \n", - " if self.db_range is not None:\n", - " out -= self.db_range[0]\n", - " out /= self.db_range[1] - self.db_range[0]\n", - " \n", - " if self.cmap is not None:\n", - " if self.db_range is None:\n", - " out -= out.min()\n", - " out /= out.max()\n", - " out = self.cmap(out)[...,:3]\n", - " \n", - " if self.cmap is not None or self.db_range is not None:\n", - " out = np.clip(out * 256, 0, 255).astype(np.uint8)\n", - " \n", - " return out\n", - " " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from matplotlib.pyplot import imshow\n", - "from spectrogram_generator import SpectrogramGenerator\n", - "from subprocess import check_output\n", - "from typing import TypedDict, Optional\n", - "import boto3\n", - "import io\n", - "import json\n", - "import librosa\n", - "import matplotlib\n", - "import os.path\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def get_audio_metadata(local_path):\n", - " result = check_output(['ffprobe',\n", - " '-hide_banner', '-loglevel', 'panic',\n", - " '-show_format',\n", - " '-show_streams',\n", - " '-of',\n", - " 'json', local_path])\n", - "\n", - " return json.loads(result)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "def spectrogram(job, show_image=True, store_image=True):\n", - " local_path = f\"{job[\"id\"]}.ts\"\n", - " s3 = boto3.client(\"s3\")\n", - "\n", - " if not os.path.exists(local_path):\n", - " response = s3.get_object(Bucket=job[\"audio_bucket\"], Key=job[\"audio_key\"])\n", - " audio_bytes = response[\"Body\"].read()\n", - " with open(local_path, \"wb\") as file:\n", - " file.write(audio_bytes)\n", - "\n", - " sample_rate = job.get(\"sample_rate\")\n", - " if sample_rate is None:\n", - " metadata = get_audio_metadata(local_path)\n", - " sample_rate = int(metadata[\"streams\"][0][\"sample_rate\"])\n", - "\n", - " audio, sr = librosa.load(local_path, sr=sample_rate)\n", - "\n", - " generator = SpectrogramGenerator(sr,\n", - " linear=False,\n", - " n_fft=1024,\n", - " # fmin=1,\n", - " # fmax=20000,\n", - " hop_length=256,\n", - " db_range=(10, 80),\n", - " cmap='inferno')\n", - " spectrogram = generator(audio)\n", - " if show_image:\n", - " matplotlib.pyplot.axis('off')\n", - " imshow(spectrogram)\n", - "\n", - " # Store spectrogram image either as a file or in-memory\n", - " image_store = f\"{job[\"id\"]}.png\" if store_image else io.BytesIO()\n", - " matplotlib.image.imsave(image_store, spectrogram)\n", - " if not store_image:\n", - " image_store.seek(0)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from urllib.parse import urlparse\n", - "\n", - "def s3_params(url):\n", - " path_parts = urlparse(url).path.split(\"/\")\n", - " return {\"bucket\": path_parts.pop(1), \"key\": \"/\".join(path_parts)[1:]}\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "jobs = [\n", - " {\n", - " \"id\": \"orca_call\",\n", - " \"audio_bucket\": s3_params(orca_call_audio_url)[\"bucket\"],\n", - " \"audio_key\": s3_params(orca_call_audio_url)[\"key\"],\n", - " },\n", - " {\n", - " \"id\": \"fish_grunt\",\n", - " \"audio_bucket\": s3_params(fish_grunt_audio_url)[\"bucket\"],\n", - " \"audio_key\": s3_params(fish_grunt_audio_url)[\"key\"],\n", - " },\n", - " {\n", - " \"id\": \"ferry_vessel\",\n", - " \"audio_bucket\": s3_params(ferry_vessel_audio_url)[\"bucket\"],\n", - " \"audio_key\": s3_params(ferry_vessel_audio_url)[\"key\"],\n", - " },\n", - " {\n", - " \"id\": \"water_splash\",\n", - " \"audio_bucket\": s3_params(hydrophone_hum_water_splashes_audio_url)[\"bucket\"],\n", - " \"audio_key\": s3_params(hydrophone_hum_water_splashes_audio_url)[\"key\"],\n", - " },\n", - "]\n", - "orca = jobs[0]\n", - "fish = jobs[1]\n", - "ferry_vessel = jobs[2]\n", - "water_splash = jobs[3]\n", - "jobs" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "spectrogram(orca)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.4" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/core/spectrogram_generator.py b/core/spectrogram_generator.py deleted file mode 100644 index 4f97252..0000000 --- a/core/spectrogram_generator.py +++ /dev/null @@ -1,110 +0,0 @@ -# Spectrogram generator via: https://github.com/kylemcdonald/AudioNotebooks/blob/master/Generating%20Spectrograms.ipynb - -# Copyright (c) 2016- Kyle McDonald -# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import librosa -import matplotlib.pyplot as plt -import numpy as np -import cv2 - -class SpectrogramGenerator: - """Create a spectrogram generator.""" - - def __init__(self, sr, - n_fft=1024, - linear=False, - hop_length=None, - db_ref=0.01, - db_range=None, - fmin=None, - fmax=None, - cmap=None, - window='hann', - drop_nyquist=True, - flipud=True): - """ - Args: - sr (int): sample rate - n_fft (int): size of STFT window in samples - linear (bool): use linear instead of logarithmic frequency spacing - hop_length (int): samples between STFT frames - db_ref (float): `ref` parameter when converting amplitude to dB - db_range (tuple): min and max dB for output - fmin (float): minimum frequency to output (hertz) - fmax (float): maximum frequency to output (hertz) - cmap (str): matplotlib color map to use for output - window (str): windowing function for STFT - drop_nyquist (bool): if True, output has exactly `n_fft//2` bins - flipud (bool): whether to flip the STFT output - """ - - self.n_fft = n_fft - self.hop_length = hop_length - self.db_ref = db_ref - self.db_range = db_range - self.cmap = cmap - self.window = window - self.drop_nyquist = drop_nyquist - - if hop_length is None: - self.hop_length = n_fft // 4 - - if cmap is not None: - self.cmap = plt.get_cmap(cmap) - - bin_count = (n_fft // 2) + 1 - if drop_nyquist: - bin_count -= 1 - nyquist = sr // 2 - if fmin == None: - min_bin = 1 - else: - min_bin = (fmin / nyquist) * (bin_count - 1) - if fmax == None: - max_bin = bin_count - 1 - else: - max_bin = (fmax / nyquist) * (bin_count - 1) - bins = np.arange(bin_count) - if linear: - y_remap = (bins + min_bin) / ((bin_count - 1) / (max_bin - min_bin)) - else: - scale_factor = bin_count / np.log10(max_bin / min_bin) - y_remap = min_bin * 10 ** (bins / scale_factor) - if flipud: - y_remap = y_remap[::-1] - self.mapy_base = y_remap.reshape(-1,1).astype(np.float32) - - def __call__(self, audio): - """Generate a spectrogram for visualization. - - Returns: - np.array of type np.uint8 if `db_range` or `cmap` is set, otherwise np.float32 - """ - stft = librosa.stft(audio, - n_fft=self.n_fft, - hop_length=self.hop_length, - window=self.window) - if self.drop_nyquist: - stft = stft[:-1] - db = librosa.amplitude_to_db(np.abs(stft), ref=self.db_ref) - mapx = np.arange(db.shape[1], dtype=np.float32).reshape(1,-1).repeat(db.shape[0], axis=0) - mapy = self.mapy_base.repeat(db.shape[1], axis=1) - out = cv2.remap(db, mapx, mapy, cv2.INTER_LANCZOS4) - - if self.db_range is not None: - out -= self.db_range[0] - out /= self.db_range[1] - self.db_range[0] - - if self.cmap is not None: - if self.db_range is None: - out -= out.min() - out /= out.max() - out = self.cmap(out)[...,:3] - - if self.cmap is not None or self.db_range is not None: - out = np.clip(out * 256, 0, 255).astype(np.uint8) - - return out \ No newline at end of file diff --git a/core/warm.py b/core/warm.py deleted file mode 100644 index fff8cf1..0000000 --- a/core/warm.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Renders one synthetic clip at image build time. - -The first render in a fresh container is slow: numba compiles the librosa -functions it meets, and matplotlib scans the system fonts to build its font -list. Running one render here leaves those caches on disk, and the Dockerfile -moves them into the image for app.py to restore on cold start. - -The clip is AAC in MPEG-TS, the format the hydrophones stream, so the render -goes through the same decoder as a real segment. make_spectrogram skips the S3 -download when the clip already exists at /tmp/, and skips the upload when -image_key is None, so this touches no AWS. -""" - -import subprocess - -import app - -CLIP = "warm.ts" - -subprocess.run( - [ - "ffmpeg", - "-hide_banner", - "-loglevel", - "error", - "-y", - "-f", - "lavfi", - "-i", - "anoisesrc=d=10:c=pink:r=48000", - "-ac", - "1", - "-c:a", - "aac", - "-f", - "mpegts", - f"/tmp/{CLIP}", - ], - check=True, -) - -app.make_spectrogram( - { - "id": CLIP, - "audio_bucket": "none", - "audio_key": "none", - "sample_rate": None, - "image_key": None, - "image_bucket": None, - } -) diff --git a/docs/comparison.png b/docs/comparison.png new file mode 100644 index 0000000000000000000000000000000000000000..7d97129b125b21214633173d442f9131b29a080e GIT binary patch literal 71322 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

FRPVhUFk->lGyUXJ5wnlY+K<9VlQn-elmIaD_k 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%0Aiv8p5gy>sKFg?Q8 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ಆnlhF9FjgJg{N?WFs@v8?lkawD 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 SLOW_FIRST_RENDER: - sys.exit(f"first render took {t2 - t1:.1f}s; were the caches restored?") +t0 = time.time() +result = app.lambda_handler({"audio_url": f"file://{CLIP}", "image_url": None}, None) +elapsed = time.time() - t0 + +png = captured["png"] +if png[:8] != b"\x89PNG\r\n\x1a\n": + sys.exit("output is not a PNG") +width, height = struct.unpack(">II", png[16:24]) +expected_width = 1 + int(result["sample_rate"] * SECONDS) // app.DEFAULTS["hop_length"] + +print(f"rendered {width}x{height} in {elapsed:.1f}s, {len(png)} bytes, sample rate {result['sample_rate']}") + +if height != app.DEFAULTS["n_fft"] // 2: + sys.exit(f"height {height}, expected {app.DEFAULTS['n_fft'] // 2}") +# AAC pads the clip by up to a couple of frames, so allow a few columns. +if abs(width - expected_width) > 8: + sys.exit(f"width {width}, expected about {expected_width}") +if result["image_size"] != len(png): + sys.exit(f"image_size {result['image_size']} does not match the {len(png)} bytes rendered")