diff --git a/server/audio_viz/core/app.py b/server/audio_viz/core/app.py index 7b0f32e8..d3d18958 100644 --- a/server/audio_viz/core/app.py +++ b/server/audio_viz/core/app.py @@ -8,6 +8,7 @@ import librosa import matplotlib import os.path +import sys SpectrogramJob = TypedDict( "SpectrogramJob", @@ -43,13 +44,15 @@ def lambda_handler(event, context): Return doc: https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html """ - make_spectrogram(event) + result = make_spectrogram(event) return { - "statusCode": 200, + "status": 200, + "image_size": result["image_size"], + "sample_rate": result["sample_rate"], } -def make_spectrogram(job: SpectrogramJob, store_audio=False, show_image=False, store_image=False) -> None: +def make_spectrogram(job: SpectrogramJob, store_audio=False, show_image=False, store_image=False): local_path = f"/tmp/{job["id"]}" s3 = boto3.client("s3") @@ -80,15 +83,21 @@ def make_spectrogram(job: SpectrogramJob, store_audio=False, show_image=False, s 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 not store_image: + if store_image: + image_size = os.path.getsize(image_store) + else: image_store.seek(0) + image_size = sys.getsizeof(image_store) 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) + return {"sample_rate": sample_rate, "image_size": image_size} + def get_audio_metadata(local_path): result = check_output(['ffprobe', '-hide_banner', '-loglevel', 'panic', diff --git a/server/lib/orcasite/radio/audio_image.ex b/server/lib/orcasite/radio/audio_image.ex index c3fb04f9..848477c8 100644 --- a/server/lib/orcasite/radio/audio_image.ex +++ b/server/lib/orcasite/radio/audio_image.ex @@ -1,13 +1,18 @@ defmodule Orcasite.Radio.AudioImage do + require Ash.Resource.Change.Builtins + require Ash.Resource.Change.Builtins + require Ash.Resource.Change.Builtins + use Ash.Resource, otp_app: :orcasite, domain: Orcasite.Radio, - extensions: [AshGraphql.Resource], + extensions: [AshGraphql.Resource, AshUUID], data_layer: AshPostgres.DataLayer postgres do table "audio_images" repo Orcasite.Repo + custom_indexes do index [:start_time] index [:end_time] @@ -21,10 +26,22 @@ defmodule Orcasite.Radio.AudioImage do attributes do uuid_primary_key :id attribute :image_type, Orcasite.Types.ImageType, public?: true - attribute :status, Orcasite.Types.AudioImageStatus, default: :new, public?: true - attribute :start_time, :utc_datetime_usec, public?: true - attribute :end_time, :utc_datetime_usec, public?: true + attribute :status, Orcasite.Types.AudioImageStatus do + default :new + public? true + allow_nil? false + end + + attribute :start_time, :utc_datetime_usec do + public? true + allow_nil? false + end + + attribute :end_time, :utc_datetime_usec do + public? true + allow_nil? false + end attribute :parameters, :map do public? true @@ -36,13 +53,14 @@ defmodule Orcasite.Radio.AudioImage do attribute :bucket, :string, public?: true attribute :bucket_region, :string, public?: true - attribute :object_path, :string + attribute :object_path, :string, public?: true timestamps() end relationships do belongs_to :feed, Orcasite.Radio.Feed do + allow_nil? false public? true end @@ -58,6 +76,68 @@ defmodule Orcasite.Radio.AudioImage do actions do defaults [:read, :destroy, create: :*, update: :*] + + create :for_feed_segment do + argument :feed_segment_id, :uuid, allow_nil?: false + + argument :image_type, Orcasite.Types.ImageType do + default :spectrogram + end + + change set_attribute(:image_type, arg(:image_type)) + change set_attribute(:bucket, "dev-audio-viz") + change set_attribute(:bucket_region, "us-west-2") + + change before_action(fn change, _context -> + feed_segment_id = Ash.Changeset.get_argument(change, :feed_segment_id) + + Orcasite.Radio.FeedSegment + |> Ash.get(feed_segment_id, authorize?: false) + |> case do + {:ok, feed_segment} -> + change + |> Ash.Changeset.change_attributes(%{ + start_time: feed_segment.start_time, + end_time: feed_segment.end_time + }) + |> Ash.Changeset.manage_relationship(:feed_segments, [feed_segment], + type: :append + ) + |> Ash.Changeset.manage_relationship(:feed, feed_segment.feed_id, + type: :append + ) + + error -> + error + end + end) + + change after_action( + fn record, _context -> + feed = record |> Ash.load(:feed) |> Map.get(:feed) + + record + |> Ash.Changeset.for_update(:update, %{ + object_path: "/#{feed.node_name}/#{record.id}.png" + }) + |> Ash.update(authorize?: false) + end, + prepend?: true + ) + + change after_action(fn record, _context -> + record + |> Ash.Changeset.for_update(:generate_spectrogram) + |> Ash.update(authorize?: false) + end) + end + + update :generate_spectrogram do + change set_attribute(:status, :processing) + change fn change, _context -> + change + end + end end graphql do diff --git a/server/lib/orcasite/radio/audio_image_feed_segment.ex b/server/lib/orcasite/radio/audio_image_feed_segment.ex index 46a84e85..2e1fbacb 100644 --- a/server/lib/orcasite/radio/audio_image_feed_segment.ex +++ b/server/lib/orcasite/radio/audio_image_feed_segment.ex @@ -2,7 +2,7 @@ defmodule Orcasite.Radio.AudioImageFeedSegment do use Ash.Resource, otp_app: :orcasite, domain: Orcasite.Radio, - extensions: [AshGraphql.Resource], + extensions: [AshGraphql.Resource, AshUUID], data_layer: AshPostgres.DataLayer postgres do diff --git a/server/lib/orcasite/radio/aws_client.ex b/server/lib/orcasite/radio/aws_client.ex index b621dde9..b65be6dd 100644 --- a/server/lib/orcasite/radio/aws_client.ex +++ b/server/lib/orcasite/radio/aws_client.ex @@ -1,33 +1,58 @@ defmodule Orcasite.Radio.AwsClient do alias Orcasite.Radio.{Feed, FeedStream} - @default_results %{count: 0, timestamps: []} - - def generate_spectrogram() do - ExAws.Lambda.list_functions() - |> ExAws.request!() - |> Map.get("Functions") - |> Enum.find_value(fn %{"FunctionName" => name} -> - if String.contains?(name, "AudioVizFunction"), do: {:ok, name} - end) + @default_timestamp_results %{count: 0, timestamps: []} + + def generate_spectrogram(%{ + image_id: image_id, + audio_bucket: audio_bucket, + audio_key: audio_key, + image_bucket: image_bucket, + image_key: image_key + }) do + + ExAws.S3.head_object(image_bucket, String.trim_leading(image_key, "/")) + |> ExAws.request() |> case do - {:ok, name} -> - ExAws.Lambda.invoke( - name, - %{ - "id" => "test_image", - "audio_bucket" => "streaming-orcasound-net", - "audio_key" => "rpi_sunset_bay/hls/1654758019/live006.ts", - "image_bucket" => "dev-archive-orcasound-net", - "image_key" => "spectrograms/live006_spect.png" - }, - %{}, - invocation_type: :request_response - ) - |> ExAws.request() + {:ok, %{headers: headers}} -> + # Exists, skip + size = + headers + |> Enum.find(&(elem(&1, 0) == "Content-Length")) + |> elem(1) + |> String.to_integer() + + {:ok, %{size: size}} _ -> - nil + # Doesn't exist, make spectrogram + ExAws.Lambda.list_functions() + |> ExAws.request!() + |> Map.get("Functions") + |> Enum.find_value(fn %{"FunctionName" => name} -> + if String.contains?(name, "AudioVizFunction"), do: {:ok, name} + end) + |> case do + {:ok, name} -> + ExAws.Lambda.invoke( + name, + %{ + "id" => image_id, + "audio_bucket" => audio_bucket, + "audio_key" => String.trim_leading(audio_key, "/"), + "image_bucket" => image_bucket, + "image_key" => String.trim_leading(image_key, "/") + }, + %{}, + invocation_type: :request_response + ) + |> ExAws.request() + + # TODO: Set sample rate, size + + _ -> + nil + end end end @@ -91,7 +116,12 @@ defmodule Orcasite.Radio.AwsClient do end end - def loop_request_timestamp(feed, callback \\ nil, token \\ nil, results \\ @default_results) do + def loop_request_timestamp( + feed, + callback \\ nil, + token \\ nil, + results \\ @default_timestamp_results + ) do request_timestamps(feed, token, callback) |> case do {:ok, diff --git a/server/lib/orcasite/radio/calculations/decode_uuid.ex b/server/lib/orcasite/radio/calculations/decode_uuid.ex index f1215fa1..0f01520a 100644 --- a/server/lib/orcasite/radio/calculations/decode_uuid.ex +++ b/server/lib/orcasite/radio/calculations/decode_uuid.ex @@ -14,15 +14,21 @@ defmodule Orcasite.Radio.Calculations.DecodeUUID do end def decode(record) do - [_, encoded_id] = - record - |> Map.get(:id) - |> String.split("_") + case Ecto.UUID.cast(record.id) do + {:ok, uuid} -> + uuid - with {:ok, uuid} <- AshUUID.Encoder.decode(encoded_id) do - uuid - else - _ -> nil + _ -> + [_, encoded_id] = + record + |> Map.get(:id) + |> String.split("_") + + with {:ok, uuid} <- AshUUID.Encoder.decode(encoded_id) do + uuid + else + _ -> nil + end end end end diff --git a/server/lib/orcasite/radio/feed.ex b/server/lib/orcasite/radio/feed.ex index 5723cd5a..fa9c7808 100644 --- a/server/lib/orcasite/radio/feed.ex +++ b/server/lib/orcasite/radio/feed.ex @@ -26,7 +26,7 @@ defmodule Orcasite.Radio.Feed do end attributes do - uuid_attribute :id, prefix: "feed", public?: true + uuid_attribute :id, public?: true attribute :name, :string, allow_nil?: false, public?: true attribute :node_name, :string, allow_nil?: false, public?: true @@ -67,6 +67,8 @@ defmodule Orcasite.Radio.Feed do :string, {Orcasite.Radio.Calculations.FeedImageUrl, object: "map.png"}, public?: true + + # calculate :uuid, :string, Orcasite.Radio.Calculations.DecodeUUID end aggregates do diff --git a/server/lib/orcasite/types/audio_image_status.ex b/server/lib/orcasite/types/audio_image_status.ex index 6d34f236..38303426 100644 --- a/server/lib/orcasite/types/audio_image_status.ex +++ b/server/lib/orcasite/types/audio_image_status.ex @@ -1,3 +1,3 @@ defmodule Orcasite.Types.AudioImageStatus do - use Ash.Type.Enum, values: [:new, :processing, :complete] + use Ash.Type.Enum, values: [:new, :processing, :complete, :failed] end