timgremore commited on
Commit
cb31b7a
2 Parent(s): 9cee14f 7836661

Merge pull request #11 from headwayio/feature/edit-transcription

Browse files
lib/medical_transcription/transcription.ex CHANGED
@@ -1,18 +1,8 @@
1
  defmodule MedicalTranscription.Transcription do
2
  @moduledoc """
3
- Takes a path to an audio file and transcribes it to text. As each chunk is available, it passes it to the `Coding`
4
- context to look for possible matching codes.
5
  """
6
 
7
- alias MedicalTranscription.Coding
8
-
9
- defp get_tags_and_send_result(chunk, index, live_view_pid) do
10
- tags = Coding.process_chunk(chunk.text)
11
- result = build_result(index, chunk, tags)
12
-
13
- send(live_view_pid, {:transcription_row, result})
14
- end
15
-
16
  # Ideas for future exploration:
17
  # - Instead of storing the long description vectors in a binary file on disk, we could store them within a vector DB
18
  # (such as pgvector or Pinecone.io)
@@ -25,7 +15,7 @@ defmodule MedicalTranscription.Transcription do
25
  audio_file_path
26
  |> stream_transcription()
27
  |> Enum.reduce("", fn {chunk, index}, acc ->
28
- get_tags_and_send_result(chunk, index + 1, live_view_pid)
29
 
30
  acc <> chunk.text
31
  end)
@@ -36,7 +26,7 @@ defmodule MedicalTranscription.Transcription do
36
  end_timestamp_seconds: nil
37
  }
38
 
39
- get_tags_and_send_result(summary_chunk, 0, live_view_pid)
40
  end
41
 
42
  defp stream_transcription(audio_file_path) do
@@ -45,14 +35,15 @@ defmodule MedicalTranscription.Transcription do
45
  |> Stream.with_index()
46
  end
47
 
48
- defp build_result(index, chunk, tags) do
49
- %{
50
  id: index,
51
  start_mark: format_timestamp(chunk.start_timestamp_seconds),
52
  end_mark: format_timestamp(chunk.end_timestamp_seconds),
53
- text: chunk.text,
54
- tags: tags
55
  }
 
 
56
  end
57
 
58
  defp format_timestamp(seconds) when is_nil(seconds), do: nil
 
1
  defmodule MedicalTranscription.Transcription do
2
  @moduledoc """
3
+ Takes a path to an audio file and transcribes it to text.
 
4
  """
5
 
 
 
 
 
 
 
 
 
 
6
  # Ideas for future exploration:
7
  # - Instead of storing the long description vectors in a binary file on disk, we could store them within a vector DB
8
  # (such as pgvector or Pinecone.io)
 
15
  audio_file_path
16
  |> stream_transcription()
17
  |> Enum.reduce("", fn {chunk, index}, acc ->
18
+ send_result(chunk, index + 1, live_view_pid)
19
 
20
  acc <> chunk.text
21
  end)
 
26
  end_timestamp_seconds: nil
27
  }
28
 
29
+ send_result(summary_chunk, 0, live_view_pid)
30
  end
31
 
32
  defp stream_transcription(audio_file_path) do
 
35
  |> Stream.with_index()
36
  end
37
 
38
+ defp send_result(chunk, index, live_view_pid) do
39
+ result = %{
40
  id: index,
41
  start_mark: format_timestamp(chunk.start_timestamp_seconds),
42
  end_mark: format_timestamp(chunk.end_timestamp_seconds),
43
+ text: chunk.text
 
44
  }
45
+
46
+ send(live_view_pid, {:transcription_row, result})
47
  end
48
 
49
  defp format_timestamp(seconds) when is_nil(seconds), do: nil
lib/medical_transcription_web/components/components.ex CHANGED
@@ -143,7 +143,13 @@ defmodule MedicalTranscriptionWeb.Components do
143
  phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
144
  >
145
  <%= for {dom_id, row} <- @rows do %>
146
- <.live_component module={TranscriptionTextComponent} id={dom_id} row={row} />
 
 
 
 
 
 
147
  <% end %>
148
  </div>
149
  """
 
143
  phx-update={match?(%Phoenix.LiveView.LiveStream{}, @rows) && "stream"}
144
  >
145
  <%= for {dom_id, row} <- @rows do %>
146
+ <.live_component
147
+ module={TranscriptionTextComponent}
148
+ id={dom_id}
149
+ start_mark={row.start_mark}
150
+ end_mark={row.end_mark}
151
+ text={row.text}
152
+ />
153
  <% end %>
154
  </div>
155
  """
lib/medical_transcription_web/components/layouts/app.html.heex CHANGED
@@ -1,5 +1,5 @@
1
  <div class="min-h-[calc(100vh-56px)] flex gap-5">
2
- <header class="w-[335px] min-w-[335px] pt-6 border-r border-gray-200 flex flex-col gap-12">
3
  <div class="flex items-center gap-[10.5px]">
4
  <img src={~p"/images/logo.svg"} width="48" />
5
 
 
1
  <div class="min-h-[calc(100vh-56px)] flex gap-5">
2
+ <header class="hidden w-[335px] min-w-[335px] pt-6 border-r border-gray-200 lg:flex flex-col gap-12">
3
  <div class="flex items-center gap-[10.5px]">
4
  <img src={~p"/images/logo.svg"} width="48" />
5
 
lib/medical_transcription_web/components/transcription_text_component.ex CHANGED
@@ -1,30 +1,42 @@
1
  defmodule MedicalTranscriptionWeb.Components.TranscriptionTextComponent do
2
  @moduledoc """
3
- Represents a portion of transcribed text and its codes and starts a task to determine keywords within the text.
4
- """
5
 
 
 
6
  use MedicalTranscriptionWeb, :live_component
 
7
  import MedicalTranscriptionWeb.Components
8
  import MedicalTranscriptionWeb.Components.KeywordHighlighter
 
9
  alias AudioTagger.KeywordFinder
 
10
  alias MedicalTranscription.Coding.CodeVectorMatch
11
 
12
  @impl Phoenix.LiveComponent
13
  def update(assigns, socket) do
14
- socket = assign_for_row(assigns, socket)
15
 
16
  {:ok, socket}
17
  end
18
 
19
- defp assign_for_row(%{row: row}, socket) do
20
  self_pid = self()
21
 
 
 
 
 
 
 
 
22
  socket
23
- |> assign(:row, row)
24
- |> assign_async(:keywords, fn -> find_keywords(self_pid, row.text) end)
 
25
  end
26
 
27
- defp assign_for_row(_assigns, socket), do: socket
28
 
29
  @impl Phoenix.LiveComponent
30
  def render(assigns) do
@@ -32,14 +44,20 @@ defmodule MedicalTranscriptionWeb.Components.TranscriptionTextComponent do
32
  <div class="flex gap-12 pb-10 border-b border-[#444444]/20">
33
  <div class="flex-1 flex flex-col gap-4">
34
  <p class="text-[32px] leading-normal font-semibold">
35
- <%= if !is_nil(@row.start_mark) && !is_nil(@row.end_mark) do %>
36
- <%= @row.start_mark %> - <%= @row.end_mark %>
37
  <% end %>
38
  </p>
39
  <p class="text-[28px] leading-normal text-type-black-tertiary">
40
  <.async_result :let={keywords} assign={@keywords}>
41
- <:loading><%= @row.text %></:loading>
42
- <.highlight text={@row.text} keywords={keywords} />
 
 
 
 
 
 
43
  </.async_result>
44
  </p>
45
 
@@ -55,21 +73,50 @@ defmodule MedicalTranscriptionWeb.Components.TranscriptionTextComponent do
55
  </div>
56
 
57
  <div class="flex-1 flex flex-col items-stretch gap-3">
58
- <%= for %CodeVectorMatch{id: id, code: code, description: label, cosine_similarity: score, weighting: weighting} <- @row.tags do %>
59
- <.tag_result
60
- code_vector_id={id}
61
- code={code}
62
- label={label}
63
- score={score}
64
- text={@row.text}
65
- weighting={weighting}
66
- />
67
- <% end %>
 
 
 
 
 
 
68
  </div>
69
  </div>
70
  """
71
  end
72
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  defp find_keywords(_live_view_pid, ""), do: {:ok, %{keywords: []}}
74
 
75
  defp find_keywords(live_view_pid, text) do
 
1
  defmodule MedicalTranscriptionWeb.Components.TranscriptionTextComponent do
2
  @moduledoc """
3
+ Represents a portion of transcribed text.
 
4
 
5
+ Once rendered, starts tasks to find relevant codes and keywords for the text.
6
+ """
7
  use MedicalTranscriptionWeb, :live_component
8
+
9
  import MedicalTranscriptionWeb.Components
10
  import MedicalTranscriptionWeb.Components.KeywordHighlighter
11
+
12
  alias AudioTagger.KeywordFinder
13
+ alias MedicalTranscription.Coding
14
  alias MedicalTranscription.Coding.CodeVectorMatch
15
 
16
  @impl Phoenix.LiveComponent
17
  def update(assigns, socket) do
18
+ socket = assign_initial_state(assigns, socket)
19
 
20
  {:ok, socket}
21
  end
22
 
23
+ defp assign_initial_state(%{start_mark: start_mark, end_mark: end_mark, text: text}, socket) do
24
  self_pid = self()
25
 
26
+ initial_state = %{
27
+ start_mark: start_mark,
28
+ end_mark: end_mark,
29
+ text: text,
30
+ editing: false
31
+ }
32
+
33
  socket
34
+ |> assign(initial_state)
35
+ |> assign_async(:tags, fn -> classify_text(text) end)
36
+ |> assign_async(:keywords, fn -> find_keywords(self_pid, text) end)
37
  end
38
 
39
+ defp assign_initial_state(_assigns, socket), do: socket
40
 
41
  @impl Phoenix.LiveComponent
42
  def render(assigns) do
 
44
  <div class="flex gap-12 pb-10 border-b border-[#444444]/20">
45
  <div class="flex-1 flex flex-col gap-4">
46
  <p class="text-[32px] leading-normal font-semibold">
47
+ <%= if !is_nil(@start_mark) && !is_nil(@end_mark) do %>
48
+ <%= @start_mark %> - <%= @end_mark %>
49
  <% end %>
50
  </p>
51
  <p class="text-[28px] leading-normal text-type-black-tertiary">
52
  <.async_result :let={keywords} assign={@keywords}>
53
+ <:loading><%= @text %></:loading>
54
+ <%= if @editing do %>
55
+ <textarea class="w-full" phx-blur="reclassify_transcription" phx-target={@myself}><%= @text %></textarea>
56
+ <% else %>
57
+ <div class="text-[28px]" phx-click="edit_transcription" phx-target={@myself}>
58
+ <.highlight text={@text} keywords={keywords} />
59
+ </div>
60
+ <% end %>
61
  </.async_result>
62
  </p>
63
 
 
73
  </div>
74
 
75
  <div class="flex-1 flex flex-col items-stretch gap-3">
76
+ <.async_result :let={tags} assign={@tags}>
77
+ <:loading>
78
+ <img src={~p"/images/loading.svg"} width="16" class="animate-spin" /> Finding codes...
79
+ </:loading>
80
+ <:failed :let={reason}>There was an error finding codes: <%= reason %></:failed>
81
+ <%= for %CodeVectorMatch{id: id, code: code, description: label, cosine_similarity: score, weighting: weighting} <- tags do %>
82
+ <.tag_result
83
+ code_vector_id={id}
84
+ code={code}
85
+ label={label}
86
+ score={score}
87
+ text={@text}
88
+ weighting={weighting}
89
+ />
90
+ <% end %>
91
+ </.async_result>
92
  </div>
93
  </div>
94
  """
95
  end
96
 
97
+ @impl Phoenix.LiveComponent
98
+ def handle_event("edit_transcription", _params, socket) do
99
+ {:noreply, assign(socket, :editing, true)}
100
+ end
101
+
102
+ @impl Phoenix.LiveComponent
103
+ def handle_event("reclassify_transcription", %{"value" => value} = _params, socket) do
104
+ self_pid = self()
105
+
106
+ socket =
107
+ socket
108
+ |> assign(:text, value)
109
+ |> assign(:editing, false)
110
+ |> assign_async(:tags, fn -> classify_text(value) end)
111
+ |> assign_async(:keywords, fn -> find_keywords(self_pid, value) end)
112
+
113
+ {:noreply, socket}
114
+ end
115
+
116
+ def classify_text(value) do
117
+ {:ok, %{tags: Coding.process_chunk(value)}}
118
+ end
119
+
120
  defp find_keywords(_live_view_pid, ""), do: {:ok, %{keywords: []}}
121
 
122
  defp find_keywords(live_view_pid, text) do