timgremore commited on
Commit
deecaf3
1 Parent(s): 8cf56d2

wip: feat: Define show screen for transcriptions

Browse files
lib/medical_transcription/transcription.ex DELETED
@@ -1,9 +0,0 @@
1
- defmodule MedicalTranscription.Transcription do
2
- @moduledoc """
3
- Takes a path to an audio file and transcribes it to text.
4
- """
5
-
6
- def stream_transcription_and_search(audio_file_path) do
7
- MedicalTranscription.TranscriptionSupervisor.start_transcription(audio_file_path)
8
- end
9
- end
 
 
 
 
 
 
 
 
 
 
lib/medical_transcription/transcription_server.ex CHANGED
@@ -20,8 +20,8 @@ defmodule MedicalTranscription.TranscriptionServer do
20
  end
21
 
22
  @impl GenServer
23
- def handle_continue(:start, [file_path: file_path] = state) do
24
- stream_transcription_and_search(file_path)
25
  {:noreply, state}
26
  end
27
 
 
20
  end
21
 
22
  @impl GenServer
23
+ def handle_continue(:start, [transcription: transcription] = state) do
24
+ stream_transcription_and_search(transcription.filename)
25
  {:noreply, state}
26
  end
27
 
lib/medical_transcription/transcription_supervisor.ex CHANGED
@@ -11,8 +11,8 @@ defmodule MedicalTranscription.TranscriptionSupervisor do
11
  DynamicSupervisor.init(strategy: :one_for_one)
12
  end
13
 
14
- def start_transcription(uploaded_file) do
15
- spec = {MedicalTranscription.TranscriptionServer, file_path: uploaded_file}
16
  DynamicSupervisor.start_child(__MODULE__, spec)
17
  end
18
  end
 
11
  DynamicSupervisor.init(strategy: :one_for_one)
12
  end
13
 
14
+ def start_transcription(transcription) do
15
+ spec = {MedicalTranscription.TranscriptionServer, transcription: transcription}
16
  DynamicSupervisor.start_child(__MODULE__, spec)
17
  end
18
  end
lib/medical_transcription/transcriptions.ex CHANGED
@@ -8,6 +8,18 @@ defmodule MedicalTranscription.Transcriptions do
8
 
9
  alias MedicalTranscription.Transcriptions.Transcription
10
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  @doc """
12
  Returns the list of transcriptions.
13
 
 
8
 
9
  alias MedicalTranscription.Transcriptions.Transcription
10
 
11
+ @doc """
12
+ Create transcription record and begin transcribing
13
+
14
+ ## Examples
15
+
16
+ iex> create_and_transcribe_audio("my-audio.mp3")
17
+ %Transcription{audio_file: "my-audio.mp3"}
18
+ """
19
+ def transcribe_audio(transcription) do
20
+ MedicalTranscription.TranscriptionSupervisor.start_transcription(transcription)
21
+ end
22
+
23
  @doc """
24
  Returns the list of transcriptions.
25
 
lib/medical_transcription_web/live/home_live/index.ex CHANGED
@@ -85,19 +85,24 @@ defmodule MedicalTranscriptionWeb.HomeLive.Index do
85
  {:ok, dest}
86
  end)
87
 
88
- uploaded_files
89
- |> Enum.at(0)
90
- |> transcribe_and_tag_audio()
91
-
92
- socket =
93
- socket
94
- |> assign(:status, :loading)
95
- |> assign(:summary_keywords, [])
96
- |> assign(:transcription_rows, [])
97
- |> assign(:uploaded_file_name, filename)
98
- |> assign(:transcriptions, list_transcriptions(socket.assigns.current_user))
99
-
100
- {:noreply, socket}
 
 
 
 
 
101
  end
102
 
103
  @impl true
@@ -208,12 +213,6 @@ defmodule MedicalTranscriptionWeb.HomeLive.Index do
208
  {:noreply, assign(socket, :status, :success)}
209
  end
210
 
211
- defp transcribe_and_tag_audio(audio_file_path) do
212
- MedicalTranscription.Transcription.stream_transcription_and_search(
213
- audio_file_path
214
- )
215
- end
216
-
217
  def error_to_string(:too_large), do: "Too large"
218
  def error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
219
 
 
85
  {:ok, dest}
86
  end)
87
 
88
+ {:ok, transcription} = Transcriptions.create_transcription(%{
89
+ user_id: socket.assigns.current_user.id,
90
+ filename: Enum.at(uploaded_files, 0)
91
+ })
92
+
93
+ Transcriptions.transcribe_audio(transcription)
94
+
95
+ {:noreply, push_navigate(socket, to: ~p"/transcriptions/#{transcription.id}")}
96
+ #
97
+ # socket =
98
+ # socket
99
+ # |> assign(:status, :loading)
100
+ # |> assign(:summary_keywords, [])
101
+ # |> assign(:transcription_rows, [])
102
+ # |> assign(:uploaded_file_name, filename)
103
+ # |> assign(:transcriptions, list_transcriptions(socket.assigns.current_user))
104
+ #
105
+ # {:noreply, socket}
106
  end
107
 
108
  @impl true
 
213
  {:noreply, assign(socket, :status, :success)}
214
  end
215
 
 
 
 
 
 
 
216
  def error_to_string(:too_large), do: "Too large"
217
  def error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
218
 
lib/medical_transcription_web/live/transcriptions_live/show.ex ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defmodule MedicalTranscriptionWeb.TranscriptionsLive.Show do
2
+ use MedicalTranscriptionWeb, :live_view
3
+
4
+ alias MedicalTranscription.Audio.RecordingPipeline
5
+ alias MedicalTranscription.Transcriptions
6
+
7
+ @impl Phoenix.LiveView
8
+ def mount(params, session, socket) do
9
+ dbg(params)
10
+ if connected?(socket), do: Phoenix.PubSub.subscribe(MedicalTranscription.PubSub, "transcriptions")
11
+
12
+ # We're storing atoms in `:status` as LiveView's AsyncResult doesn't support modeling a "pending" state, but only
13
+ # loading / ok / failed.
14
+ initial_state = %{
15
+ current_recording_id: 0,
16
+ uploaded_file_name: nil,
17
+ status: :pending,
18
+ audio_pipeline: nil,
19
+ summary_keywords: [],
20
+ transcriptions: list_transcriptions(session["current_user"]),
21
+ finalized_codes: []
22
+ }
23
+
24
+ socket =
25
+ socket
26
+ |> assign(initial_state)
27
+ |> stream_configure(:transcription_rows, dom_id: &"transcription-row-#{&1.id}")
28
+ |> stream(:transcription_rows, [])
29
+ |> allow_upload(:audio, accept: ~w(.mp3), max_entries: 1)
30
+
31
+ {:ok, socket}
32
+ end
33
+
34
+ @impl Phoenix.LiveView
35
+ def render(assigns) do
36
+ ~H"""
37
+ <div class="min-h-[calc(100vh-56px)] flex gap-5">
38
+ <MedicalTranscriptionWeb.Components.SidebarComponent.sidebar
39
+ current_user={@current_user}
40
+ transcriptions={@transcriptions}
41
+ />
42
+
43
+ <main class="flex-1 pl-16 pr-16 pt-[25px]">
44
+ <div class="flex flex-col h-full mx-auto max-w-5xl">
45
+ <div class="flex-1 flex flex-col space-y-6">
46
+ <%= if @status != :pending do %>
47
+ <.live_component
48
+ module={MedicalTranscriptionWeb.Components.ResultListComponent}
49
+ id="result_list"
50
+ rows={@streams.transcription_rows}
51
+ summary_keywords={@summary_keywords}
52
+ finalized_codes={@finalized_codes}
53
+ />
54
+ <% end %>
55
+
56
+ <%= if @status == :pending do %>
57
+ <div class="flex-1 flex flex-col justify-center items-center gap-4">
58
+ <img src={~p"/images/logo.svg"} width="106" />
59
+ <p class="text-2xl leading-normal font-semibold">Medical Code Transcriber</p>
60
+ </div>
61
+ <.upload_form audio_upload={@uploads.audio} />
62
+ <% end %>
63
+ </div>
64
+ </div>
65
+ </main>
66
+ </div>
67
+ """
68
+ end
69
+
70
+ @impl true
71
+ def handle_event("validate", _params, socket) do
72
+ {:noreply, socket}
73
+ end
74
+
75
+ @impl true
76
+ def handle_event("save", _params, socket) do
77
+ filename =
78
+ socket.assigns.uploads.audio.entries
79
+ |> Enum.at(0)
80
+ |> Map.get(:client_name)
81
+
82
+ uploaded_files =
83
+ consume_uploaded_entries(socket, :audio, fn %{path: path}, _entry ->
84
+ dest = Path.join(System.tmp_dir(), Path.basename(path))
85
+ File.cp!(path, dest)
86
+ {:ok, dest}
87
+ end)
88
+
89
+ {:ok, transcription} = Transcriptions.create_transcription(%{
90
+ user_id: socket.assigns.current_user.id,
91
+ filename: Enum.at(uploaded_files, 0)
92
+ })
93
+
94
+ Transcriptions.transcribe_audio(transcription)
95
+
96
+ socket =
97
+ socket
98
+ |> assign(:status, :loading)
99
+ |> assign(:summary_keywords, [])
100
+ |> assign(:transcription_rows, [])
101
+ |> assign(:uploaded_file_name, filename)
102
+ |> assign(:transcriptions, list_transcriptions(socket.assigns.current_user))
103
+
104
+ {:noreply, socket}
105
+ end
106
+
107
+ @impl true
108
+ def handle_event("add_feedback", params, socket) do
109
+ text_vector = MedicalTranscription.Coding.compute_vector_as_list(params["text"])
110
+
111
+ result =
112
+ params
113
+ |> Map.put("text_vector", text_vector)
114
+ |> MedicalTranscription.Feedback.track_response()
115
+
116
+ socket =
117
+ case result do
118
+ {:ok, message} -> put_flash(socket, :info, message)
119
+ {:error, messages} -> put_flash(socket, :error, messages)
120
+ end
121
+
122
+ {:noreply, socket}
123
+ end
124
+
125
+ @impl true
126
+ def handle_event("reset_to_pending", _params, socket) do
127
+ {:noreply, assign(socket, :status, :pending)}
128
+ end
129
+
130
+ @impl true
131
+ def handle_event("toggle_recording", _params, socket) do
132
+ socket =
133
+ if is_nil(socket.assigns.audio_pipeline) do
134
+ pipeline = RecordingPipeline.start_pipeline(self())
135
+
136
+ socket
137
+ |> assign(:status, :streaming_audio)
138
+ |> assign(:audio_pipeline, pipeline)
139
+ else
140
+ RecordingPipeline.stop_pipeline(socket.assigns.audio_pipeline)
141
+
142
+ socket
143
+ |> assign(:status, :success)
144
+ |> assign(:audio_pipeline, nil)
145
+ end
146
+
147
+ {:noreply, socket}
148
+ end
149
+
150
+ @impl true
151
+ def handle_info({:chunk, chunk_result}, socket) do
152
+ # The processing sends a message as each chunk of text is coded. See here for some background and potential
153
+ # inspiration for this: https://elixirforum.com/t/liveview-asynchronous-task-patterns/44695
154
+
155
+ {:noreply, stream_insert(socket, :transcription_rows, chunk_result)}
156
+ end
157
+
158
+ @impl true
159
+ def handle_info({:new_keywords, new_keywords}, socket) do
160
+ socket =
161
+ update(socket, :summary_keywords, fn keywords -> keywords ++ [new_keywords] end)
162
+
163
+ {:noreply, socket}
164
+ end
165
+
166
+ @impl true
167
+ def handle_info({:received_audio_payload, transcribed_text}, socket) do
168
+ tags = MedicalTranscription.Coding.process_chunk(transcribed_text)
169
+
170
+ result = %{
171
+ id: socket.assigns.current_recording_id + 1,
172
+ start_mark: nil,
173
+ end_mark: nil,
174
+ text: transcribed_text,
175
+ tags: tags
176
+ }
177
+
178
+ socket =
179
+ socket
180
+ |> assign(:status, :streaming_audio)
181
+ |> stream_insert(:transcription_rows, result)
182
+ |> assign(:current_recording_id, result.id)
183
+
184
+ {:noreply, socket}
185
+ end
186
+
187
+ @impl Phoenix.LiveView
188
+ def handle_info({"toggle_user_selected_code", {:add, code}}, socket) do
189
+ new_codes =
190
+ if Enum.any?(socket.assigns.finalized_codes, code) do
191
+ socket.assigns.finalized_codes
192
+ else
193
+ socket.assigns.finalized_codes ++ [code]
194
+ end
195
+
196
+ {:noreply, assign(socket, :finalized_codes, new_codes)}
197
+ end
198
+
199
+ @impl Phoenix.LiveView
200
+ def handle_info({"toggle_user_selected_code", {:remove, code}}, socket) do
201
+ new_codes = Enum.reject(socket.assigns.finalized_codes, &(&1 == code))
202
+
203
+ {:noreply, assign(socket, :finalized_codes, new_codes)}
204
+ end
205
+
206
+ @impl true
207
+ def handle_info({ref, _result}, socket) do
208
+ # See this Fly article for the usage of Task.async to start `transcribe_and_tag_audio/2` and handle the end of the
209
+ # task here: https://fly.io/phoenix-files/liveview-async-task/
210
+ Process.demonitor(ref, [:flush])
211
+
212
+ {:noreply, assign(socket, :status, :success)}
213
+ end
214
+
215
+ def error_to_string(:too_large), do: "Too large"
216
+ def error_to_string(:not_accepted), do: "You have selected an unacceptable file type"
217
+
218
+ defp list_transcriptions(user) do
219
+ Transcriptions.list_transcriptions(user)
220
+ end
221
+ end
lib/medical_transcription_web/router.ex CHANGED
@@ -74,6 +74,7 @@ defmodule MedicalTranscriptionWeb.Router do
74
  live "/users/settings", UserSettingsLive, :edit
75
  live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
76
  live "/", HomeLive.Index
 
77
  end
78
  end
79
 
 
74
  live "/users/settings", UserSettingsLive, :edit
75
  live "/users/settings/confirm_email/:token", UserSettingsLive, :confirm_email
76
  live "/", HomeLive.Index
77
+ live "/transcriptions/:id", TranscriptionsLive.Show
78
  end
79
  end
80
 
test/medical_transcription/transcription_server_test.exs CHANGED
@@ -5,6 +5,8 @@ defmodule MedicalTranscription.TranscriptionServerTest do
5
 
6
  use MedicalTranscription.DataCase
7
 
 
 
8
  alias MedicalTranscription.TranscriptionServer
9
 
10
  setup do
@@ -13,11 +15,12 @@ defmodule MedicalTranscription.TranscriptionServerTest do
13
  |> Path.join("../../medasrdemo-Paul.mp3")
14
  |> Path.expand()
15
 
16
- %{sample_file: sample_file}
 
17
  end
18
 
19
- test "transcribe and tag audio", %{sample_file: sample_file} do
20
- spec = {TranscriptionServer, file_path: sample_file}
21
 
22
  {:ok, pid} = start_supervised(spec)
23
 
 
5
 
6
  use MedicalTranscription.DataCase
7
 
8
+ import MedicalTranscription.TranscriptionsFixtures
9
+
10
  alias MedicalTranscription.TranscriptionServer
11
 
12
  setup do
 
15
  |> Path.join("../../medasrdemo-Paul.mp3")
16
  |> Path.expand()
17
 
18
+ transcription = transcription_fixture(%{filename: sample_file})
19
+ %{transcription: transcription}
20
  end
21
 
22
+ test "transcribe and tag audio", %{transcription: transcription} do
23
+ spec = {TranscriptionServer, transcription: transcription}
24
 
25
  {:ok, pid} = start_supervised(spec)
26
 
test/medical_transcription_web/live/home_live_test.exs CHANGED
@@ -2,17 +2,20 @@ defmodule MedicalTranscriptionWeb.HomeLiveTest do
2
  use MedicalTranscriptionWeb.ConnCase, async: true
3
 
4
  import Phoenix.LiveViewTest
5
-
6
  import MedicalTranscription.AccountsFixtures
 
 
 
 
7
 
8
  setup %{conn: conn} do
9
  password = valid_user_password()
10
  user = user_fixture(%{password: password})
11
- %{conn: log_in_user(conn, user)}
12
  end
13
 
14
  describe "/" do
15
- test "renders upload screen", %{conn: conn} do
16
  # 1. Find file input
17
  # 2. Upload file
18
  # 3. Click submit
@@ -42,28 +45,20 @@ defmodule MedicalTranscriptionWeb.HomeLiveTest do
42
  }
43
  ])
44
 
45
- # assert audio
46
- # |> render_upload("sample.mp3") =~ "Submitted: sample.mp3"
47
- #
48
- # refute view
49
- # |> form("#audio-form")
50
- # |> render_submit() =~ "Submitted: sample.mp3"
51
-
52
  render_upload(audio, "sample.mp3")
53
 
54
- assert view
55
- |> form("#audio-form")
56
- |> render_submit() =~ "Transcribing and tagging audio file..."
57
 
58
- # TODO: Test that transcribed text appears
59
- # TODO: Test that codes appear
60
- # assert render_async(view, 5_000) =~ "Coronary artery anomaly"
61
- end
 
 
62
 
63
- test "renders transcription text", %{conn: conn} do
64
- {:ok, view, html} = live(conn, "/")
65
- html = view |> element("result_list")
66
- assert html_response(conn, 200)
67
  end
68
  end
69
  end
 
2
  use MedicalTranscriptionWeb.ConnCase, async: true
3
 
4
  import Phoenix.LiveViewTest
 
5
  import MedicalTranscription.AccountsFixtures
6
+ import Ecto.Query
7
+
8
+ alias MedicalTranscription.Transcriptions.Transcription
9
+ alias MedicalTranscription.Repo
10
 
11
  setup %{conn: conn} do
12
  password = valid_user_password()
13
  user = user_fixture(%{password: password})
14
+ %{conn: log_in_user(conn, user), current_user: user}
15
  end
16
 
17
  describe "/" do
18
+ test "renders upload screen", %{conn: conn, current_user: current_user} do
19
  # 1. Find file input
20
  # 2. Upload file
21
  # 3. Click submit
 
45
  }
46
  ])
47
 
 
 
 
 
 
 
 
48
  render_upload(audio, "sample.mp3")
49
 
50
+ view
51
+ |> form("#audio-form")
52
+ |> render_submit()
53
 
54
+ transcription =
55
+ Transcription
56
+ |> where([t], t.user_id == ^current_user.id)
57
+ |> order_by(desc: :inserted_at)
58
+ |> limit(1)
59
+ |> Repo.one()
60
 
61
+ assert_redirected view, ~p"/transcriptions/#{transcription.id}"
 
 
 
62
  end
63
  end
64
  end