timgremore commited on
Commit
8548275
1 Parent(s): 94b2573

feat: Pass chunk to chunk component

Browse files
lib/medical_transcription/classification_server.ex CHANGED
@@ -35,7 +35,7 @@ defmodule MedicalTranscription.ClassificationServer do
35
  end
36
 
37
  @impl GenServer
38
- def handle_info({:chunk_updated, result}, state) do
39
  {:chunk, chunk} = state
40
 
41
  %TranscriptionChunk{id: id} = chunk
@@ -73,7 +73,6 @@ defmodule MedicalTranscription.ClassificationServer do
73
  |> Coding.process_chunk()
74
  |> Enum.each(fn %CodeVectorMatch{
75
  code: code,
76
- description: description,
77
  cosine_similarity: cosine_similarity,
78
  weighting: [weighting]
79
  } ->
 
35
  end
36
 
37
  @impl GenServer
38
+ def handle_info({:chunk_updated, _result}, state) do
39
  {:chunk, chunk} = state
40
 
41
  %TranscriptionChunk{id: id} = chunk
 
73
  |> Coding.process_chunk()
74
  |> Enum.each(fn %CodeVectorMatch{
75
  code: code,
 
76
  cosine_similarity: cosine_similarity,
77
  weighting: [weighting]
78
  } ->
lib/medical_transcription/transcription_server.ex CHANGED
@@ -41,17 +41,18 @@ defmodule MedicalTranscription.TranscriptionServer do
41
 
42
  %Transcription{id: id} = transcription
43
 
44
- Transcriptions.create_chunk(%{
45
- transcription_id: id,
46
- text: result.text,
47
- start_mark: result.start_mark,
48
- end_mark: result.end_mark
49
- })
 
50
 
51
  Phoenix.PubSub.broadcast(
52
  :medicode_pubsub,
53
  "transcriptions:#{id}",
54
- {:transcription_updated, result}
55
  )
56
 
57
  {:noreply, state}
 
41
 
42
  %Transcription{id: id} = transcription
43
 
44
+ {:ok, chunk} =
45
+ Transcriptions.create_chunk(%{
46
+ transcription_id: id,
47
+ text: result.text,
48
+ start_mark: result.start_mark,
49
+ end_mark: result.end_mark
50
+ })
51
 
52
  Phoenix.PubSub.broadcast(
53
  :medicode_pubsub,
54
  "transcriptions:#{id}",
55
+ {:transcription_updated, chunk}
56
  )
57
 
58
  {:noreply, state}
lib/medical_transcription/transcriptions.ex CHANGED
@@ -92,6 +92,32 @@ defmodule MedicalTranscription.Transcriptions do
92
  Repo.get!(query, id)
93
  end
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  @doc """
96
  Creates a transcription.
97
 
 
92
  Repo.get!(query, id)
93
  end
94
 
95
+ @doc """
96
+ Gets a single transcription chunk.
97
+
98
+ Raises `Ecto.NoResultsError` if the TranscriptionChunk does not exist.
99
+
100
+ ## Examples
101
+
102
+ iex> get_transcription_chunk!(123)
103
+ %TranscriptionChunk{}
104
+
105
+ iex> get_transcription_chunk!(456)
106
+ ** (Ecto.NoResultsError)
107
+
108
+ """
109
+ def get_transcription_chunk!(id, preload_transcription_chunk_associations \\ false) do
110
+ query =
111
+ if preload_transcription_chunk_associations do
112
+ TranscriptionChunk
113
+ |> preload([:keywords, :code_vectors])
114
+ else
115
+ TranscriptionChunk
116
+ end
117
+
118
+ Repo.get!(query, id)
119
+ end
120
+
121
  @doc """
122
  Creates a transcription.
123
 
lib/medical_transcription_web/components/transcription_text_component.ex CHANGED
@@ -18,20 +18,16 @@ defmodule MedicalTranscriptionWeb.Components.TranscriptionTextComponent do
18
  def update(assigns, socket) do
19
  self_pid = self()
20
 
21
- text = assigns.text
22
 
23
  initial_assigns = %{
24
  id: assigns.id,
25
- start_mark: assigns.start_mark,
26
- end_mark: assigns.end_mark,
27
- text: assigns.text
28
  }
29
 
30
  socket =
31
  socket
32
  |> assign(initial_assigns)
33
- |> assign_async(:tags, fn -> classify_text(text) end)
34
- |> assign_async(:keywords, fn -> find_keywords(self_pid, text) end)
35
 
36
  {:ok, socket}
37
  end
@@ -43,60 +39,39 @@ defmodule MedicalTranscriptionWeb.Components.TranscriptionTextComponent do
43
  <div id={@id} class="flex gap-12 pb-10 border-b border-[#444444]/20">
44
  <div class="flex-1 flex flex-col gap-4">
45
  <p
46
- :if={!is_nil(@start_mark) && !is_nil(@end_mark)}
47
  class="px-2 text-[32px] leading-normal font-semibold"
48
  >
49
- <%= @start_mark %> - <%= @end_mark %>
50
  </p>
51
 
52
  <div class="flex-1 w-full text-[28px] leading-normal text-type-black-tertiary">
53
- <.async_result :let={keywords} assign={@keywords}>
54
- <:loading>
55
- <p class="h-full min-h-full rounded p-2"><%= @text %></p>
56
- </:loading>
57
- <p
58
- id={"#{@id}-transcription-text"}
59
- contenteditable
60
- role="textbox"
61
- class="h-full min-h-full rounded p-2"
62
- phx-hook="TranscriptionEditor"
63
- phx-target={@myself}
64
- >
65
- <.highlight text={@text} keywords={keywords} />
66
- </p>
67
- </.async_result>
68
- </div>
69
-
70
- <div class="flex items-center gap-2 text-sm italic">
71
- <.async_result assign={@keywords}>
72
- <:loading>
73
- <img src={~p"/images/loading.svg"} width="16" class="animate-spin" />
74
- Finding keywords...
75
- </:loading>
76
- <:failed :let={reason}>There was an error finding keywords: <%= reason %></:failed>
77
- </.async_result>
78
  </div>
79
  </div>
80
 
81
  <div class="flex-1 flex flex-col items-stretch gap-3">
82
- <.async_result :let={tags} assign={@tags}>
83
- <:loading>
84
- <img src={~p"/images/loading.svg"} width="16" class="animate-spin" /> Finding codes...
85
- </:loading>
86
- <:failed :let={reason}>There was an error finding codes: <%= reason %></:failed>
87
- <%= for %CodeVectorMatch{id: id, code: code, description: label, cosine_similarity: score, weighting: weighting} <- tags do %>
88
- <.tag_result
89
- code_vector_id={id}
90
- code={code}
91
- label={label}
92
- score={score}
93
- text={@text}
94
- weighting={weighting}
95
- />
96
- <% end %>
97
-
98
- <.live_component module={CodeSelect} id={"#{@id}-code-select"} text={@text} />
99
- </.async_result>
100
  </div>
101
  </div>
102
  """
@@ -157,8 +132,4 @@ defmodule MedicalTranscriptionWeb.Components.TranscriptionTextComponent do
157
  # 2. A fast process finding the phrase closest in vector space to the whole text.
158
  KeywordFinder.find_most_similar_label(text, phrases, 2)
159
  end
160
-
161
- defp code_selected?(code, finalized_codes) do
162
- Enum.any?(finalized_codes, code)
163
- end
164
  end
 
18
  def update(assigns, socket) do
19
  self_pid = self()
20
 
21
+ text = assigns.chunk.text
22
 
23
  initial_assigns = %{
24
  id: assigns.id,
25
+ chunk: assigns.chunk
 
 
26
  }
27
 
28
  socket =
29
  socket
30
  |> assign(initial_assigns)
 
 
31
 
32
  {:ok, socket}
33
  end
 
39
  <div id={@id} class="flex gap-12 pb-10 border-b border-[#444444]/20">
40
  <div class="flex-1 flex flex-col gap-4">
41
  <p
42
+ :if={!is_nil(@chunk.start_mark) && !is_nil(@chunk.end_mark)}
43
  class="px-2 text-[32px] leading-normal font-semibold"
44
  >
45
+ <%= @chunk.start_mark %> - <%= @chunk.end_mark %>
46
  </p>
47
 
48
  <div class="flex-1 w-full text-[28px] leading-normal text-type-black-tertiary">
49
+ <p class="h-full min-h-full rounded p-2"><%= @chunk.text %></p>
50
+ <p
51
+ id={"#{@id}-transcription-text"}
52
+ contenteditable
53
+ role="textbox"
54
+ class="h-full min-h-full rounded p-2"
55
+ phx-hook="TranscriptionEditor"
56
+ phx-target={@myself}
57
+ >
58
+ <.highlight text={@chunk.text} keywords={@chunk.keywords} />
59
+ </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  </div>
61
  </div>
62
 
63
  <div class="flex-1 flex flex-col items-stretch gap-3">
64
+ <.tag_result
65
+ :for={code_vector <- @chunk.code_vectors}
66
+ code_vector_id={code_vector.id}
67
+ code={code_vector.code}
68
+ label={code_vector.label}
69
+ score={1.0}
70
+ text={@chunk.text}
71
+ weighting={[1.0]}
72
+ />
73
+
74
+ <.live_component module={CodeSelect} id={"#{@id}-code-select"} text={@chunk.text} />
 
 
 
 
 
 
 
75
  </div>
76
  </div>
77
  """
 
132
  # 2. A fast process finding the phrase closest in vector space to the whole text.
133
  KeywordFinder.find_most_similar_label(text, phrases, 2)
134
  end
 
 
 
 
135
  end
lib/medical_transcription_web/live/transcriptions_live/show.ex CHANGED
@@ -4,7 +4,6 @@ defmodule MedicalTranscriptionWeb.TranscriptionsLive.Show do
4
  alias MedicalTranscription.Audio.RecordingPipeline
5
  alias MedicalTranscription.Repo
6
  alias MedicalTranscription.Transcriptions
7
- alias MedicalTranscription.Transcriptions.TranscriptionChunk
8
 
9
  alias MedicalTranscriptionWeb.Components.TranscriptionTextComponent
10
 
@@ -50,16 +49,18 @@ defmodule MedicalTranscriptionWeb.TranscriptionsLive.Show do
50
 
51
  <main class="flex-1 pl-16 pr-16 pt-[25px]">
52
  <div class="flex flex-col h-full mx-auto max-w-5xl">
53
- <.result_heading transcription={@transcription} summary_keywords={@summary_keywords} />
 
 
 
 
54
 
55
  <div id="result_list" class="flex flex flex-col gap-14" phx-update="stream">
56
  <.live_component
57
  :for={{dom_id, chunk} <- @streams.chunks}
58
  module={TranscriptionTextComponent}
59
  id={dom_id}
60
- start_mark={chunk.start_mark}
61
- end_mark={chunk.end_mark}
62
- text={chunk.text}
63
  finalized_codes={@finalized_codes}
64
  />
65
  </div>
@@ -111,7 +112,9 @@ defmodule MedicalTranscriptionWeb.TranscriptionsLive.Show do
111
  end
112
 
113
  @impl true
114
- def handle_info({:transcription_updated, chunk}, socket) do
 
 
115
  # The processing sends a message as each chunk of text is coded. See here for some background and potential
116
  # inspiration for this: https://elixirforum.com/t/liveview-asynchronous-task-patterns/44695
117
  {:noreply, stream_insert(socket, :chunks, chunk)}
@@ -178,9 +181,11 @@ defmodule MedicalTranscriptionWeb.TranscriptionsLive.Show do
178
  end
179
 
180
  defp get_transcription(id) do
181
- id
182
- |> Transcriptions.get_transcription()
183
- |> Repo.preload(:chunks)
 
 
184
  end
185
 
186
  defp list_transcriptions(user), do: Transcriptions.list_transcriptions(user)
 
4
  alias MedicalTranscription.Audio.RecordingPipeline
5
  alias MedicalTranscription.Repo
6
  alias MedicalTranscription.Transcriptions
 
7
 
8
  alias MedicalTranscriptionWeb.Components.TranscriptionTextComponent
9
 
 
49
 
50
  <main class="flex-1 pl-16 pr-16 pt-[25px]">
51
  <div class="flex flex-col h-full mx-auto max-w-5xl">
52
+ <.result_heading
53
+ status={@transcription.status}
54
+ transcription={@transcription}
55
+ summary_keywords={@summary_keywords}
56
+ />
57
 
58
  <div id="result_list" class="flex flex flex-col gap-14" phx-update="stream">
59
  <.live_component
60
  :for={{dom_id, chunk} <- @streams.chunks}
61
  module={TranscriptionTextComponent}
62
  id={dom_id}
63
+ chunk={chunk}
 
 
64
  finalized_codes={@finalized_codes}
65
  />
66
  </div>
 
112
  end
113
 
114
  @impl true
115
+ def handle_info({:transcription_updated, %{id: chunk_id}}, socket) do
116
+ chunk = get_transcription_chunk(chunk_id)
117
+
118
  # The processing sends a message as each chunk of text is coded. See here for some background and potential
119
  # inspiration for this: https://elixirforum.com/t/liveview-asynchronous-task-patterns/44695
120
  {:noreply, stream_insert(socket, :chunks, chunk)}
 
181
  end
182
 
183
  defp get_transcription(id) do
184
+ Transcriptions.get_transcription(id, true)
185
+ end
186
+
187
+ defp get_transcription_chunk(id) do
188
+ Transcriptions.get_transcription_chunk!(id, true)
189
  end
190
 
191
  defp list_transcriptions(user), do: Transcriptions.list_transcriptions(user)
test/medical_transcription/coding_test.exs CHANGED
@@ -1,6 +1,8 @@
1
  defmodule MedicalTranscription.CodingTest do
2
  use MedicalTranscription.DataCase
3
 
 
 
4
  alias MedicalTranscription.Coding
5
  alias MedicalTranscription.Coding.CodeVector
6
  alias MedicalTranscription.Feedback.CodeFeedback
@@ -34,6 +36,9 @@ defmodule MedicalTranscription.CodingTest do
34
  end
35
 
36
  describe "process_chunk/2" do
 
 
 
37
  setup do
38
  create_code_vector!("74685", "Coronary artery anomaly")
39
  create_code_vector!("41412", "Dissection of coronary artery")
@@ -41,28 +46,28 @@ defmodule MedicalTranscription.CodingTest do
41
  create_code_vector!("V812", "Screening for other and unspecified cardiovascular conditions")
42
  create_code_vector!("V4589", "Other postprocedural status")
43
 
44
- :ok
45
- end
46
 
47
- @input_text "This 55-year-old man with known coronary artery disease comes for a follow-up visit today."
48
- @feedback_text "A 42-year-old man arrived for a return check-up who had a past history of coronary artery disease."
49
 
50
- test "Codes can be weighted multiple times when multiple prior feedback items exist for a single code" do
51
  # One negative feedback item moves the code from 1st to 4th (x0.9).
52
  # The net result of 2 negative and 1 positive (x0.891) is to remove the code from the results.
53
  create_code_feedback!(@feedback_text, false, "74685")
54
  create_code_feedback!(@feedback_text, true, "74685")
55
  create_code_feedback!(@feedback_text, false, "74685")
56
- results = Coding.process_chunk(@input_text)
57
 
58
  assert Enum.map(results, & &1.code) == ["41412", "V717", "V812"]
59
  assert Enum.all?(results, &(&1.weighting == [:none]))
60
  end
61
 
62
- test "Weighting can cause codes to appear in results that wouldn't have been included from the initial similarity query" do
63
  # E.g. weighting up a code that isn't in the initial results
64
  create_code_feedback!(@feedback_text, true, "V4589")
65
- results = Coding.process_chunk(@input_text)
66
 
67
  assert Enum.map(results, & &1.code) == ["74685", "41412", "V717", "V812", "V4589"]
68
 
@@ -75,32 +80,32 @@ defmodule MedicalTranscription.CodingTest do
75
  ]
76
  end
77
 
78
- test "Codes are unweighted when no prior feedback" do
79
- results = Coding.process_chunk(@input_text)
80
 
81
  assert Enum.map(results, & &1.code) == ["74685", "41412", "V717", "V812"]
82
  assert Enum.all?(results, &(&1.weighting == [:none]))
83
  end
84
 
85
- test "Code is ranked higher when prior positive feedback" do
86
  create_code_feedback!(@feedback_text, true, "V717")
87
- results = Coding.process_chunk(@input_text)
88
 
89
  assert Enum.map(results, & &1.code) == ["V717", "74685", "41412", "V812"]
90
  assert Enum.map(results, & &1.weighting) == [[:positive], [:none], [:none], [:none]]
91
  end
92
 
93
- test "Code is ranked lower when prior negative feedback" do
94
  create_code_feedback!(@feedback_text, false, "74685")
95
- results = Coding.process_chunk(@input_text)
96
 
97
  assert Enum.map(results, & &1.code) == ["41412", "V717", "V812", "74685"]
98
  assert Enum.map(results, & &1.weighting) == [[:none], [:none], [:none], [:negative]]
99
  end
100
 
101
- test "Code with low similarity score is removed from result when prior negative feedback" do
102
  create_code_feedback!(@feedback_text, false, "V717")
103
- results = Coding.process_chunk(@input_text)
104
 
105
  assert Enum.map(results, & &1.code) == ["74685", "41412", "V812"]
106
  assert Enum.map(results, & &1.weighting) == [[:none], [:none], [:none]]
 
1
  defmodule MedicalTranscription.CodingTest do
2
  use MedicalTranscription.DataCase
3
 
4
+ import MedicalTranscription.TranscriptionChunksFixtures
5
+
6
  alias MedicalTranscription.Coding
7
  alias MedicalTranscription.Coding.CodeVector
8
  alias MedicalTranscription.Feedback.CodeFeedback
 
36
  end
37
 
38
  describe "process_chunk/2" do
39
+ @input_text "This 55-year-old man with known coronary artery disease comes for a follow-up visit today."
40
+ @feedback_text "A 42-year-old man arrived for a return check-up who had a past history of coronary artery disease."
41
+
42
  setup do
43
  create_code_vector!("74685", "Coronary artery anomaly")
44
  create_code_vector!("41412", "Dissection of coronary artery")
 
46
  create_code_vector!("V812", "Screening for other and unspecified cardiovascular conditions")
47
  create_code_vector!("V4589", "Other postprocedural status")
48
 
49
+ input_chunk = transcription_chunk_fixture(%{text: @input_text})
50
+ feedback_chunk = transcription_chunk_fixture(%{text: @feedback_text})
51
 
52
+ {:ok, input_chunk: input_chunk, feedback_chunk: feedback_chunk}
53
+ end
54
 
55
+ test "Codes can be weighted multiple times when multiple prior feedback items exist for a single code", %{input_chunk: chunk} do
56
  # One negative feedback item moves the code from 1st to 4th (x0.9).
57
  # The net result of 2 negative and 1 positive (x0.891) is to remove the code from the results.
58
  create_code_feedback!(@feedback_text, false, "74685")
59
  create_code_feedback!(@feedback_text, true, "74685")
60
  create_code_feedback!(@feedback_text, false, "74685")
61
+ results = Coding.process_chunk(chunk)
62
 
63
  assert Enum.map(results, & &1.code) == ["41412", "V717", "V812"]
64
  assert Enum.all?(results, &(&1.weighting == [:none]))
65
  end
66
 
67
+ test "Weighting can cause codes to appear in results that wouldn't have been included from the initial similarity query", %{input_chunk: chunk} do
68
  # E.g. weighting up a code that isn't in the initial results
69
  create_code_feedback!(@feedback_text, true, "V4589")
70
+ results = Coding.process_chunk(chunk)
71
 
72
  assert Enum.map(results, & &1.code) == ["74685", "41412", "V717", "V812", "V4589"]
73
 
 
80
  ]
81
  end
82
 
83
+ test "Codes are unweighted when no prior feedback", %{input_chunk: chunk} do
84
+ results = Coding.process_chunk(chunk)
85
 
86
  assert Enum.map(results, & &1.code) == ["74685", "41412", "V717", "V812"]
87
  assert Enum.all?(results, &(&1.weighting == [:none]))
88
  end
89
 
90
+ test "Code is ranked higher when prior positive feedback", %{input_chunk: chunk} do
91
  create_code_feedback!(@feedback_text, true, "V717")
92
+ results = Coding.process_chunk(chunk)
93
 
94
  assert Enum.map(results, & &1.code) == ["V717", "74685", "41412", "V812"]
95
  assert Enum.map(results, & &1.weighting) == [[:positive], [:none], [:none], [:none]]
96
  end
97
 
98
+ test "Code is ranked lower when prior negative feedback", %{input_chunk: chunk} do
99
  create_code_feedback!(@feedback_text, false, "74685")
100
+ results = Coding.process_chunk(chunk)
101
 
102
  assert Enum.map(results, & &1.code) == ["41412", "V717", "V812", "74685"]
103
  assert Enum.map(results, & &1.weighting) == [[:none], [:none], [:none], [:negative]]
104
  end
105
 
106
+ test "Code with low similarity score is removed from result when prior negative feedback", %{input_chunk: chunk} do
107
  create_code_feedback!(@feedback_text, false, "V717")
108
+ results = Coding.process_chunk(chunk)
109
 
110
  assert Enum.map(results, & &1.code) == ["74685", "41412", "V812"]
111
  assert Enum.map(results, & &1.weighting) == [[:none], [:none], [:none]]
test/medical_transcription_web/live/transcriptions_live_show_test.exs CHANGED
@@ -25,7 +25,7 @@ defmodule MedicalTranscriptionWeb.TranscriptionsLive.ShowTest do
25
  conn = get(conn, "/transcriptions/#{transcription.id}")
26
  assert html_response(conn, 200) =~ "Medical Code Transcriber"
27
 
28
- {:ok, view, html} = live(conn)
29
  assert html =~ "my-audio.mp3"
30
  assert html =~ "Foo"
31
  assert html =~ "Bar"
 
25
  conn = get(conn, "/transcriptions/#{transcription.id}")
26
  assert html_response(conn, 200) =~ "Medical Code Transcriber"
27
 
28
+ {:ok, _view, html} = live(conn)
29
  assert html =~ "my-audio.mp3"
30
  assert html =~ "Foo"
31
  assert html =~ "Bar"
test/support/fixtures/code_vectors_fixtures.ex CHANGED
@@ -7,7 +7,7 @@ defmodule MedicalTranscription.CodeVectorsFixtures do
7
  @doc """
8
  Insert code vectors from cached csv file.
9
  """
10
- def insert_code_vector_fixtures(attrs \\ %{}) do
11
  code_vectors =
12
  "../../../code_vectors.csv"
13
  |> Path.expand(__DIR__)
 
7
  @doc """
8
  Insert code vectors from cached csv file.
9
  """
10
+ def insert_code_vector_fixtures() do
11
  code_vectors =
12
  "../../../code_vectors.csv"
13
  |> Path.expand(__DIR__)