timgremore commited on
Commit
6ee6b5e
1 Parent(s): 101a164

chore: Move audio recording state to sidebar button

Browse files
assets/js/app.js CHANGED
@@ -52,6 +52,42 @@ Hooks.ContentEditor = {
52
  },
53
  };
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  let csrfToken = document
56
  .querySelector("meta[name='csrf-token']")
57
  .getAttribute("content");
 
52
  },
53
  };
54
 
55
+ let audioContext = null;
56
+ let pcmNode = null;
57
+
58
+ Hooks.AudioRecorder = {
59
+ mounted() {
60
+ this.handleEvent("start_audio_recording", async () => {
61
+ const audio = this.el;
62
+ const context = new AudioContext({ sampleRate: 16_000 });
63
+
64
+ await context.audioWorklet.addModule("/assets/pcm-processor.js");
65
+
66
+ const node = new AudioWorkletNode(context, "pcm-processor", {
67
+ processorOptions: { chunkSize: 16_000 },
68
+ });
69
+
70
+ node.port.onmessage = (event) => {
71
+ const audioData = new Float32Array(event.data);
72
+ this.pushEvent("audio_chunk", { data: event.data });
73
+ };
74
+
75
+ audioContext = context;
76
+ pcmNode = node;
77
+
78
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
79
+ const source = audioContext.createMediaStreamSource(stream);
80
+
81
+ source.connect(pcmNode);
82
+ pcmNode.connect(audioContext.destination); // Necessary but doesn't output sound to speakers
83
+ });
84
+
85
+ this.handleEvent("stop_audio_recording", async () => {
86
+ audioContext.close();
87
+ });
88
+ },
89
+ };
90
+
91
  let csrfToken = document
92
  .querySelector("meta[name='csrf-token']")
93
  .getAttribute("content");
assets/js/pcm-processor.js ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class PCMProcessor extends AudioWorkletProcessor {
2
+ constructor(options) {
3
+ super();
4
+ this.sampleBuffer = [];
5
+ this.chunkSize = options.processorOptions.chunkSize || 16_000;
6
+ }
7
+
8
+ process(inputs) {
9
+ const input = inputs[0];
10
+ if (input.length > 0) {
11
+ const inputData = input[0];
12
+ for (let i = 0; i < inputData.length; ++i) {
13
+ this.sampleBuffer.push(inputData[i]);
14
+ if (this.sampleBuffer.length >= this.chunkSize) {
15
+ this.port.postMessage(this.sampleBuffer.slice(0, this.chunkSize));
16
+ this.sampleBuffer = this.sampleBuffer.slice(this.chunkSize);
17
+ }
18
+ }
19
+ }
20
+ return true;
21
+ }
22
+ }
23
+
24
+ registerProcessor("pcm-processor", PCMProcessor);
config/config.exs CHANGED
@@ -44,7 +44,7 @@ config :esbuild,
44
  version: "0.17.11",
45
  default: [
46
  args:
47
- ~w(js/app.js js/storybook.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
48
  cd: Path.expand("../assets", __DIR__),
49
  env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
50
  ]
 
44
  version: "0.17.11",
45
  default: [
46
  args:
47
+ ~w(js/app.js js/storybook.js js/pcm-processor.js --bundle --target=es2017 --outdir=../priv/static/assets --external:/fonts/* --external:/images/*),
48
  cd: Path.expand("../assets", __DIR__),
49
  env: %{"NODE_PATH" => Path.expand("../deps", __DIR__)}
50
  ]
lib/medicode/application.ex CHANGED
@@ -23,7 +23,6 @@ defmodule Medicode.Application do
23
  transcription_spec(),
24
  token_classification_spec(),
25
  text_embedding_spec(),
26
- {Registry, keys: :unique, name: :transcription_registry},
27
  {
28
  Medicode.TranscriptionSupervisor,
29
  strategy: :one_for_one, max_restarts: 1
 
23
  transcription_spec(),
24
  token_classification_spec(),
25
  text_embedding_spec(),
 
26
  {
27
  Medicode.TranscriptionSupervisor,
28
  strategy: :one_for_one, max_restarts: 1
lib/medicode/transcription_server.ex CHANGED
@@ -7,10 +7,13 @@ defmodule Medicode.TranscriptionServer do
7
  alias Medicode.Transcriptions
8
  alias Medicode.Transcriptions.Transcription
9
 
10
- @registry :transcription_registry
11
-
12
- def start_link(%{transcription: transcription, name: name}) do
13
- GenServer.start_link(__MODULE__, {:transcription, transcription}, name: via_tuple(name))
 
 
 
14
  end
15
 
16
  @doc """
@@ -38,7 +41,19 @@ defmodule Medicode.TranscriptionServer do
38
  end
39
 
40
  @impl GenServer
41
- def handle_continue(:start, {:transcription, transcription}) do
 
 
 
 
 
 
 
 
 
 
 
 
42
  {:ok, transcription} =
43
  Transcriptions.update_transcription(transcription, %{status: :transcribing})
44
 
@@ -50,12 +65,12 @@ defmodule Medicode.TranscriptionServer do
50
 
51
  stream_transcription_and_search(transcription.filename)
52
 
53
- {:noreply, {:transcription, transcription}}
54
  end
55
 
56
  @impl GenServer
57
  def handle_info({:chunk, result}, state) do
58
- {:transcription, transcription} = state
59
 
60
  %Transcription{id: id} = transcription
61
 
@@ -85,7 +100,7 @@ defmodule Medicode.TranscriptionServer do
85
 
86
  @impl GenServer
87
  def terminate(reason, state) do
88
- {:transcription, transcription} = state
89
 
90
  {:ok, transcription} =
91
  Transcriptions.update_transcription(transcription, %{status: :finished})
@@ -101,6 +116,40 @@ defmodule Medicode.TranscriptionServer do
101
  reason
102
  end
103
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  # Ideas for future exploration:
105
  # - A potential improvement would be to not code each chunk of transcribed audio separately, but to instead gather
106
  # complete sentences based on punctuation. We may want to suggest codes for the entire audio as a single piece as
@@ -128,6 +177,8 @@ defmodule Medicode.TranscriptionServer do
128
  seconds |> round() |> Time.from_seconds_after_midnight() |> Time.to_string()
129
  end
130
 
131
- defp via_tuple(name),
132
- do: {:via, Registry, {@registry, name}}
 
 
133
  end
 
7
  alias Medicode.Transcriptions
8
  alias Medicode.Transcriptions.Transcription
9
 
10
+ def start_link(%{
11
+ transcription: transcription,
12
+ transcription_id: transcription_id
13
+ }) do
14
+ GenServer.start_link(__MODULE__, %{transcription: transcription},
15
+ name: via_tuple(transcription_id)
16
+ )
17
  end
18
 
19
  @doc """
 
41
  end
42
 
43
  @impl GenServer
44
+ def handle_continue(:start, %{transcription: %{status: :recording}} = state) do
45
+ %{transcription: transcription} = state
46
+
47
+ Phoenix.PubSub.broadcast(
48
+ :medicode_pubsub,
49
+ "transcriptions:#{transcription.id}",
50
+ {:transcription_started, transcription.id}
51
+ )
52
+
53
+ {:noreply, %{transcription: transcription}}
54
+ end
55
+
56
+ def handle_continue(:start, %{transcription: transcription}) do
57
  {:ok, transcription} =
58
  Transcriptions.update_transcription(transcription, %{status: :transcribing})
59
 
 
65
 
66
  stream_transcription_and_search(transcription.filename)
67
 
68
+ {:noreply, %{transcription: transcription}}
69
  end
70
 
71
  @impl GenServer
72
  def handle_info({:chunk, result}, state) do
73
+ %{transcription: transcription} = state
74
 
75
  %Transcription{id: id} = transcription
76
 
 
100
 
101
  @impl GenServer
102
  def terminate(reason, state) do
103
+ %{transcription: transcription} = state
104
 
105
  {:ok, transcription} =
106
  Transcriptions.update_transcription(transcription, %{status: :finished})
 
116
  reason
117
  end
118
 
119
+ @impl GenServer
120
+ # def handle_call({:recording, %{data: data}}, _from, state) do
121
+ def handle_cast({:recording, %{data: data}}, state) do
122
+ tensor =
123
+ Nx.tensor(data)
124
+ |> Nx.stack()
125
+ |> Nx.reshape({:auto, 1})
126
+ |> Nx.mean(axes: [1])
127
+
128
+ # audio transcription + semantic search
129
+ Medicode.TranscriptionServing
130
+ |> Nx.Serving.batched_run(tensor)
131
+ |> Enum.each(fn chunk ->
132
+ dbg(chunk)
133
+
134
+ result = %{
135
+ start_mark: format_timestamp(chunk.start_timestamp_seconds),
136
+ end_mark: format_timestamp(chunk.end_timestamp_seconds),
137
+ text: chunk.text
138
+ }
139
+
140
+ send(self(), {:chunk, result})
141
+ end)
142
+
143
+ {:noreply, state}
144
+ end
145
+
146
+ def process_recording_data(transcription_id, data) do
147
+ GenServer.cast(
148
+ {:via, :gproc, {:n, :l, {:transcription, transcription_id}}},
149
+ {:recording, %{data: data}}
150
+ )
151
+ end
152
+
153
  # Ideas for future exploration:
154
  # - A potential improvement would be to not code each chunk of transcribed audio separately, but to instead gather
155
  # complete sentences based on punctuation. We may want to suggest codes for the entire audio as a single piece as
 
177
  seconds |> round() |> Time.from_seconds_after_midnight() |> Time.to_string()
178
  end
179
 
180
+ defp via_tuple(transcription_id),
181
+ # NOTE: gproc requires keys that are a tuple of {type, scope, key}
182
+ # See https://www.brianstorti.com/process-registry-in-elixir
183
+ do: {:via, :gproc, {:n, :l, {:transcription, transcription_id}}}
184
  end
lib/medicode/transcription_supervisor.ex CHANGED
@@ -17,7 +17,7 @@ defmodule Medicode.TranscriptionSupervisor do
17
  def start_transcription(transcription) do
18
  spec = {
19
  Medicode.TranscriptionServer,
20
- %{transcription: transcription, name: "transcription:#{transcription.id}"}
21
  }
22
 
23
  DynamicSupervisor.start_child(__MODULE__, spec)
 
17
  def start_transcription(transcription) do
18
  spec = {
19
  Medicode.TranscriptionServer,
20
+ %{transcription: transcription, transcription_id: transcription.id}
21
  }
22
 
23
  DynamicSupervisor.start_child(__MODULE__, spec)
lib/medicode/transcriptions/transcription.ex CHANGED
@@ -13,7 +13,7 @@ defmodule Medicode.Transcriptions.Transcription do
13
  field :recording_length_in_seconds, :integer
14
 
15
  field :status, Ecto.Enum,
16
- values: [new: 0, waiting: 1, transcribing: 2, finished: 3, failed: 4]
17
 
18
  belongs_to :user, Medicode.Accounts.User
19
 
 
13
  field :recording_length_in_seconds, :integer
14
 
15
  field :status, Ecto.Enum,
16
+ values: [new: 0, waiting: 1, transcribing: 2, finished: 3, failed: 4, recording: 5]
17
 
18
  belongs_to :user, Medicode.Accounts.User
19
 
lib/medicode_web/components/components.ex CHANGED
@@ -102,8 +102,6 @@ defmodule MedicodeWeb.Components do
102
  class="mr-[17px]"
103
  />
104
 
105
- <.record_button_in_heading status={@transcription.status} />
106
-
107
  <div class="px-[14px] py-3 flex items-center gap-3 bg-brand rounded-lg text-white mr-4 overflow-hidden">
108
  <img src={~p"/images/document.svg"} width="20" />
109
  <p
@@ -165,33 +163,6 @@ defmodule MedicodeWeb.Components do
165
  """
166
  end
167
 
168
- # Renders the record button within the result_heading component
169
- defp record_button_in_heading(assigns) when assigns.status == :streaming_audio do
170
- ~H"""
171
- <button
172
- disabled
173
- phx-click="toggle_recording"
174
- title="Not available"
175
- class="cursor-not-allowed mr-6 px-4 py-3 bg-red-200 rounded-lg"
176
- >
177
- <.icon name="hero-microphone" class="animate-pulse" />
178
- </button>
179
- """
180
- end
181
-
182
- defp record_button_in_heading(assigns) do
183
- ~H"""
184
- <button
185
- disabled
186
- phx-click="toggle_recording"
187
- title="Not available"
188
- class="cursor-not-allowed mr-6 px-4 py-3 bg-emerald-200 rounded-lg"
189
- >
190
- <.icon name="hero-microphone" />
191
- </button>
192
- """
193
- end
194
-
195
  # Formats keywords for display in the result_heading component
196
  defp format_keywords(keyword_predictions) do
197
  keyword_predictions
 
102
  class="mr-[17px]"
103
  />
104
 
 
 
105
  <div class="px-[14px] py-3 flex items-center gap-3 bg-brand rounded-lg text-white mr-4 overflow-hidden">
106
  <img src={~p"/images/document.svg"} width="20" />
107
  <p
 
163
  """
164
  end
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  # Formats keywords for display in the result_heading component
167
  defp format_keywords(keyword_predictions) do
168
  keyword_predictions
lib/medicode_web/components/sidebar_component.ex CHANGED
@@ -22,6 +22,7 @@ defmodule MedicodeWeb.Components.SidebarComponent do
22
 
23
  socket =
24
  socket
 
25
  |> assign(:current_user, assigns.current_user)
26
  |> assign(:transcriptions, list_transcriptions(assigns.current_user))
27
 
@@ -49,14 +50,18 @@ defmodule MedicodeWeb.Components.SidebarComponent do
49
  <.icon name="hero-document-plus" />
50
  <span>Upload Audio File</span>
51
  </.link>
52
- <.link
 
 
 
 
53
  navigate={~p"/transcriptions/new"}
54
  class="text-[0.8125rem] flex justify-center gap-2 w-full text-white font-semibold hover:text-slate-300 px-3 py-2 bg-emerald-600 rounded-lg"
55
  >
56
- <.icon name="hero-microphone" />
 
57
  <span>Record Patient Notes</span>
58
- </.link>
59
- <p class="text-xs leading-normal tracking-[0.2em] font-semibold uppercase">Today</p>
60
  <div class="flex flex-col gap-4">
61
  <.link
62
  :for={transcription <- @transcriptions}
@@ -119,6 +124,31 @@ defmodule MedicodeWeb.Components.SidebarComponent do
119
  """
120
  end
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  @impl Phoenix.LiveView
123
  def handle_info({:transcription_created, _transcription_id}, socket) do
124
  transcriptions = list_transcriptions(socket.assigns.current_user)
 
22
 
23
  socket =
24
  socket
25
+ |> assign(:status, nil)
26
  |> assign(:current_user, assigns.current_user)
27
  |> assign(:transcriptions, list_transcriptions(assigns.current_user))
28
 
 
50
  <.icon name="hero-document-plus" />
51
  <span>Upload Audio File</span>
52
  </.link>
53
+ <button
54
+ id="audio-recorder-button"
55
+ phx-hook="AudioRecorder"
56
+ phx-click="record_transcription"
57
+ phx-target={@myself}
58
  navigate={~p"/transcriptions/new"}
59
  class="text-[0.8125rem] flex justify-center gap-2 w-full text-white font-semibold hover:text-slate-300 px-3 py-2 bg-emerald-600 rounded-lg"
60
  >
61
+ <.icon :if={@status == :streaming_audio} name="hero-speaker-wave" />
62
+ <.icon :if={@status != :streaming_audio} name="hero-microphone" />
63
  <span>Record Patient Notes</span>
64
+ </button>
 
65
  <div class="flex flex-col gap-4">
66
  <.link
67
  :for={transcription <- @transcriptions}
 
124
  """
125
  end
126
 
127
+ @impl Phoenix.LiveView
128
+ def handle_event("record_transcription", _params, socket) do
129
+ socket =
130
+ if socket.assigns.status == :streaming_audio do
131
+ socket
132
+ |> push_event("stop_audio_recording", %{})
133
+ |> assign(:status, nil)
134
+ else
135
+ {:ok, new_transcription} =
136
+ Transcriptions.create_transcription(%{
137
+ user_id: socket.assigns.current_user.id,
138
+ filename: "Untitled",
139
+ status: :recording
140
+ })
141
+
142
+ Medicode.TranscriptionSupervisor.start_transcription(new_transcription)
143
+
144
+ socket
145
+ |> assign(:status, :streaming_audio)
146
+ |> push_patch(to: ~p"/transcriptions/#{new_transcription.id}")
147
+ end
148
+
149
+ {:noreply, socket}
150
+ end
151
+
152
  @impl Phoenix.LiveView
153
  def handle_info({:transcription_created, _transcription_id}, socket) do
154
  transcriptions = list_transcriptions(socket.assigns.current_user)
lib/medicode_web/components/transcription_text_component.ex CHANGED
@@ -45,7 +45,8 @@ defmodule MedicodeWeb.Components.TranscriptionTextComponent do
45
  # NOTE: This assumes that if a process matches by transcription chunk ID that
46
  # the chunk is in the process of loading. Specific state inspection could happen
47
  # with the :sys module (ie :sys.get_state/1)
48
- case Registry.lookup(:transcription_registry, "transcription_chunk:#{assigns.chunk.id}") do
 
49
  [] -> false
50
  [{_pid, _}] -> true
51
  end
 
45
  # NOTE: This assumes that if a process matches by transcription chunk ID that
46
  # the chunk is in the process of loading. Specific state inspection could happen
47
  # with the :sys module (ie :sys.get_state/1)
48
+ # case Registry.lookup({:via, :gproc, {:n, :l, {:transcription, transcription_id}}}) do
49
+ case [] do
50
  [] -> false
51
  [{_pid, _}] -> true
52
  end
lib/medicode_web/live/transcriptions_live/show.ex CHANGED
@@ -1,22 +1,36 @@
1
  defmodule MedicodeWeb.TranscriptionsLive.Show do
2
  use MedicodeWeb, :live_view
3
 
4
- alias Medicode.Audio.RecordingPipeline
5
  alias Medicode.Transcriptions
6
 
7
  alias MedicodeWeb.Components.TranscriptionTextComponent
8
  alias MedicodeWeb.Components.TranscriptionTextCodingsComponent
9
 
10
  @impl Phoenix.LiveView
11
- def mount(params, session, socket) do
12
- transcription = get_transcription(params["id"])
13
-
14
  timezone = get_connect_params(socket)["timezone"] || "Etc/UTC"
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  if is_nil(transcription), do: raise(Medicode.Fallback)
17
 
18
  if connected?(socket) do
19
- Phoenix.PubSub.subscribe(:medicode_pubsub, "medicode:#{session["current_user"].id}")
20
  Phoenix.PubSub.subscribe(:medicode_pubsub, "transcriptions:#{transcription.id}")
21
  end
22
 
@@ -26,24 +40,24 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
26
  finalized_codes = Transcriptions.list_transcription_finalized_codes(transcription.id)
27
 
28
  initial_state = %{
29
- current_recording_id: 0,
30
- uploaded_file_name: nil,
31
- status: :pending,
32
- audio_pipeline: nil,
33
  summary_keywords: summary_keywords,
34
  transcription: transcription,
35
- transcriptions: list_transcriptions(session["current_user"]),
36
- finalized_codes: finalized_codes,
37
- timezone: timezone
38
  }
39
 
40
  socket =
41
  socket
42
  |> assign(initial_state)
43
- |> stream_configure(:chunk_ids, dom_id: &"transcription-chunk-#{&1.id}")
44
- |> stream(:chunk_ids, transcription_chunk_ids)
45
 
46
- {:ok, socket}
 
 
 
 
 
 
 
47
  end
48
 
49
  @impl Phoenix.LiveView
@@ -59,6 +73,7 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
59
  <main class="flex-1 pl-16 pr-16 pt-[25px]">
60
  <div class="flex flex-col h-full mx-auto max-w-5xl">
61
  <.result_heading
 
62
  id="transcription-name"
63
  target={self()}
64
  transcription={@transcription}
@@ -68,7 +83,7 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
68
  />
69
 
70
  <img
71
- :if={@transcription.status == :transcribing}
72
  src={~p"/images/loading.svg"}
73
  width="36"
74
  class="m-8 animate-spin"
@@ -112,23 +127,39 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
112
 
113
  def handle_event("toggle_recording", _params, socket) do
114
  socket =
115
- if is_nil(socket.assigns.audio_pipeline) do
116
- pipeline = RecordingPipeline.start_pipeline(self())
117
-
118
  socket
119
- |> assign(:status, :streaming_audio)
120
- |> assign(:audio_pipeline, pipeline)
121
  else
122
- RecordingPipeline.stop_pipeline(socket.assigns.audio_pipeline)
 
 
 
 
 
 
123
 
124
  socket
125
- |> assign(:status, :success)
126
- |> assign(:audio_pipeline, nil)
 
 
 
 
 
 
 
127
  end
128
 
129
  {:noreply, socket}
130
  end
131
 
 
 
 
 
 
132
  def handle_event("rename_transcription", %{"value" => new_name}, socket) do
133
  new_name_trimmed = String.trim(new_name)
134
 
@@ -150,17 +181,7 @@ defmodule MedicodeWeb.TranscriptionsLive.Show do
150
  end
151
  end
152
 
153
- @impl true
154
- def handle_info({:transcription_created, _transcription_id}, socket) do
155
- transcriptions = list_transcriptions(socket.assigns.current_user)
156
-
157
- socket =
158
- socket
159
- |> assign(:transcriptions, transcriptions)
160
-
161
- {:noreply, socket}
162
- end
163
-
164
  def handle_info({:transcription_updated, %{id: chunk_id}}, socket) do
165
  transcription = get_transcription(socket.assigns.transcription.id)
166
 
 
1
  defmodule MedicodeWeb.TranscriptionsLive.Show do
2
  use MedicodeWeb, :live_view
3
 
 
4
  alias Medicode.Transcriptions
5
 
6
  alias MedicodeWeb.Components.TranscriptionTextComponent
7
  alias MedicodeWeb.Components.TranscriptionTextCodingsComponent
8
 
9
  @impl Phoenix.LiveView
10
+ def mount(_params, session, socket) do
 
 
11
  timezone = get_connect_params(socket)["timezone"] || "Etc/UTC"
12
 
13
+ socket =
14
+ socket
15
+ |> assign(:status, nil)
16
+ |> assign(:current_user, Medicode.Accounts.get_user!(session["current_user"].id))
17
+ |> assign(:timezone, timezone)
18
+ |> stream_configure(:chunk_ids, dom_id: &"transcription-chunk-#{&1.id}")
19
+
20
+ {:ok, socket}
21
+ end
22
+
23
+ @impl Phoenix.LiveView
24
+ def handle_params(params, _url, socket) do
25
+ if connected?(socket) do
26
+ Phoenix.PubSub.subscribe(:medicode_pubsub, "medicode:#{socket.assigns.current_user.id}")
27
+ end
28
+
29
+ transcription = get_transcription(params["id"])
30
+
31
  if is_nil(transcription), do: raise(Medicode.Fallback)
32
 
33
  if connected?(socket) do
 
34
  Phoenix.PubSub.subscribe(:medicode_pubsub, "transcriptions:#{transcription.id}")
35
  end
36
 
 
40
  finalized_codes = Transcriptions.list_transcription_finalized_codes(transcription.id)
41
 
42
  initial_state = %{
 
 
 
 
43
  summary_keywords: summary_keywords,
44
  transcription: transcription,
45
+ finalized_codes: finalized_codes
 
 
46
  }
47
 
48
  socket =
49
  socket
50
  |> assign(initial_state)
51
+ |> stream(:chunk_ids, transcription_chunk_ids, reset: true)
 
52
 
53
+ socket =
54
+ if transcription.status == :recording do
55
+ push_event(socket, "start_audio_recording", %{})
56
+ else
57
+ socket
58
+ end
59
+
60
+ {:noreply, socket}
61
  end
62
 
63
  @impl Phoenix.LiveView
 
73
  <main class="flex-1 pl-16 pr-16 pt-[25px]">
74
  <div class="flex flex-col h-full mx-auto max-w-5xl">
75
  <.result_heading
76
+ :if={@transcription}
77
  id="transcription-name"
78
  target={self()}
79
  transcription={@transcription}
 
83
  />
84
 
85
  <img
86
+ :if={@transcription && @transcription.status == :transcribing}
87
  src={~p"/images/loading.svg"}
88
  width="36"
89
  class="m-8 animate-spin"
 
127
 
128
  def handle_event("toggle_recording", _params, socket) do
129
  socket =
130
+ if socket.assigns.status == :streaming_audio do
 
 
131
  socket
132
+ |> push_event("stop_audio_recording", %{})
133
+ |> assign(:status, nil)
134
  else
135
+ {:ok, new_transcription} =
136
+ Transcriptions.create_transcription(%{
137
+ user_id: socket.assigns.current_user.id,
138
+ filename: "Untitled"
139
+ })
140
+
141
+ Medicode.TranscriptionSupervisor.start_transcription(new_transcription, true)
142
 
143
  socket
144
+ |> assign(:status, :streaming_audio)
145
+ |> push_event("start_audio_recording", %{})
146
+ |> push_patch(to: "/transcriptions/#{new_transcription.id}")
147
+
148
+ #
149
+ # socket
150
+ # |> assign(:transcription, transcription)
151
+ # |> assign(:summary_keywords, [])
152
+ # |> stream(:chunk_ids, [], reset: true)
153
  end
154
 
155
  {:noreply, socket}
156
  end
157
 
158
+ def handle_event("audio_chunk", %{"data" => data}, socket) do
159
+ Medicode.TranscriptionServer.process_recording_data(socket.assigns.transcription.id, data)
160
+ {:noreply, socket}
161
+ end
162
+
163
  def handle_event("rename_transcription", %{"value" => new_name}, socket) do
164
  new_name_trimmed = String.trim(new_name)
165
 
 
181
  end
182
  end
183
 
184
+ @impl Phoenix.LiveView
 
 
 
 
 
 
 
 
 
 
185
  def handle_info({:transcription_updated, %{id: chunk_id}}, socket) do
186
  transcription = get_transcription(socket.assigns.transcription.id)
187
 
lib/medicode_web/live/user_settings_live.ex CHANGED
@@ -8,9 +8,10 @@ defmodule MedicodeWeb.UserSettingsLive do
8
  def render(assigns) do
9
  ~H"""
10
  <div class="min-h-[calc(100vh-56px)] flex gap-5">
11
- <MedicodeWeb.Components.SidebarComponent.sidebar
 
 
12
  current_user={@current_user}
13
- transcriptions={@transcriptions}
14
  />
15
 
16
  <div class="w-full flex justify-center">
 
8
  def render(assigns) do
9
  ~H"""
10
  <div class="min-h-[calc(100vh-56px)] flex gap-5">
11
+ <.live_component
12
+ id="sidebar"
13
+ module={MedicodeWeb.Components.SidebarComponent}
14
  current_user={@current_user}
 
15
  />
16
 
17
  <div class="w-full flex justify-center">
mix.exs CHANGED
@@ -73,7 +73,8 @@ defmodule Medicode.MixProject do
73
  {:sentry, "~> 8.0"},
74
  {:ecto_psql_extras, "~> 0.6"},
75
  {:circular_buffer, "~> 0.4.0"},
76
- {:tzdata, "~> 1.1"}
 
77
  # {:membrane_portaudio_plugin, "~> 0.18.0"}
78
  ]
79
  end
 
73
  {:sentry, "~> 8.0"},
74
  {:ecto_psql_extras, "~> 0.6"},
75
  {:circular_buffer, "~> 0.4.0"},
76
+ {:tzdata, "~> 1.1"},
77
+ {:gproc, "~> 1.0.0"}
78
  # {:membrane_portaudio_plugin, "~> 0.18.0"}
79
  ]
80
  end
mix.lock CHANGED
@@ -40,6 +40,7 @@
40
  "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
41
  "fss": {:hex, :fss, "0.1.1", "9db2344dbbb5d555ce442ac7c2f82dd975b605b50d169314a20f08ed21e08642", [:mix], [], "hexpm", "78ad5955c7919c3764065b21144913df7515d52e228c09427a004afe9c1a16b0"},
42
  "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
 
43
  "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
44
  "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
45
  "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
 
40
  "floki": {:hex, :floki, "0.35.2", "87f8c75ed8654b9635b311774308b2760b47e9a579dabf2e4d5f1e1d42c39e0b", [:mix], [], "hexpm", "6b05289a8e9eac475f644f09c2e4ba7e19201fd002b89c28c1293e7bd16773d9"},
41
  "fss": {:hex, :fss, "0.1.1", "9db2344dbbb5d555ce442ac7c2f82dd975b605b50d169314a20f08ed21e08642", [:mix], [], "hexpm", "78ad5955c7919c3764065b21144913df7515d52e228c09427a004afe9c1a16b0"},
42
  "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
43
+ "gproc": {:hex, :gproc, "1.0.0", "aa9ec57f6c9ff065b16d96924168d7c7157cd1fd457680efe4b1274f456fa500", [:rebar3], [], "hexpm", "109f253c2787de8a371a51179d4973230cbec6239ee673fa12216a5ce7e4f902"},
44
  "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
45
  "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
46
  "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},