File size: 5,121 Bytes
d93863b
cb8dc08
3748deb
cb8dc08
3748deb
 
d93863b
3748deb
d93863b
09e4e72
3748deb
aaa724e
3748deb
59fddbd
d93863b
 
 
7aabb54
d93863b
 
 
 
3748deb
aaa724e
 
7aabb54
 
 
 
5505ef0
7aabb54
 
aaa724e
7aabb54
3748deb
7aabb54
aaa724e
 
7aabb54
aaa724e
d93863b
 
 
 
 
 
7aabb54
 
f290997
d93863b
83bf488
 
7aabb54
5505ef0
7836661
5505ef0
7836661
5505ef0
 
 
83bf488
 
d93863b
83bf488
 
 
046803b
 
83bf488
 
 
 
d93863b
 
 
5505ef0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d93863b
 
 
 
 
5505ef0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e07d058
 
b8bd116
09e4e72
a227b91
 
 
aaa724e
d93863b
09e4e72
 
aaa724e
c3c8822
 
09e4e72
aaa724e
d93863b
83bf488
046803b
 
 
 
 
b8bd116
046803b
83bf488
d93863b
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
defmodule MedicalTranscriptionWeb.Components.TranscriptionTextComponent do
  @moduledoc """
  Represents a portion of transcribed text.

  Once rendered, starts tasks to find relevant codes and keywords for the text.
  """
  use MedicalTranscriptionWeb, :live_component

  import MedicalTranscriptionWeb.Components
  import MedicalTranscriptionWeb.Components.KeywordHighlighter

  alias AudioTagger.KeywordFinder
  alias MedicalTranscription.Coding
  alias MedicalTranscription.Coding.CodeVectorMatch

  @impl Phoenix.LiveComponent
  def update(assigns, socket) do
    socket = assign_initial_state(assigns, socket)

    {:ok, socket}
  end

  defp assign_initial_state(%{start_mark: start_mark, end_mark: end_mark, text: text}, socket) do
    self_pid = self()

    initial_state = %{
      start_mark: start_mark,
      end_mark: end_mark,
      text: text,
      editing: false
    }

    socket
    |> assign(initial_state)
    |> assign_async(:tags, fn -> classify_text(text) end)
    |> assign_async(:keywords, fn -> find_keywords(self_pid, text) end)
  end

  defp assign_initial_state(_assigns, socket), do: socket

  @impl Phoenix.LiveComponent
  def render(assigns) do
    ~H"""
    <div class="flex gap-12 pb-10 border-b border-[#444444]/20">
      <div class="flex-1 flex flex-col gap-4">
        <p class="text-[32px] leading-normal font-semibold">
          <%= if !is_nil(@start_mark) && !is_nil(@end_mark) do %>
            <%= @start_mark %> - <%= @end_mark %>
          <% end %>
        </p>
        <p class="text-[28px] leading-normal text-type-black-tertiary">
          <.async_result :let={keywords} assign={@keywords}>
            <:loading><%= @text %></:loading>
            <%= if @editing do %>
              <textarea class="w-full" phx-blur="reclassify_transcription" phx-target={@myself}><%= @text %></textarea>
            <% else %>
              <div class="text-[28px]" phx-click="edit_transcription" phx-target={@myself}>
                <.highlight text={@text} keywords={keywords} />
              </div>
            <% end %>
          </.async_result>
        </p>

        <div class="flex items-center gap-2 text-sm italic">
          <.async_result assign={@keywords}>
            <:loading>
              <img src={~p"/images/loading.svg"} width="16" class="animate-spin" />
              Finding keywords...
            </:loading>
            <:failed :let={reason}>There was an error finding keywords: <%= reason %></:failed>
          </.async_result>
        </div>
      </div>

      <div class="flex-1 flex flex-col items-stretch gap-3">
        <.async_result :let={tags} assign={@tags}>
          <:loading>
            <img src={~p"/images/loading.svg"} width="16" class="animate-spin" /> Finding codes...
          </:loading>
          <:failed :let={reason}>There was an error finding codes: <%= reason %></:failed>
          <%= for %CodeVectorMatch{id: id, code: code, description: label, cosine_similarity: score, weighting: weighting} <- tags do %>
            <.tag_result
              code_vector_id={id}
              code={code}
              label={label}
              score={score}
              text={@text}
              weighting={weighting}
            />
          <% end %>
        </.async_result>
      </div>
    </div>
    """
  end

  @impl Phoenix.LiveComponent
  def handle_event("edit_transcription", _params, socket) do
    {:noreply, assign(socket, :editing, true)}
  end

  @impl Phoenix.LiveComponent
  def handle_event("reclassify_transcription", %{"value" => value} = _params, socket) do
    self_pid = self()

    socket =
      socket
      |> assign(:text, value)
      |> assign(:editing, false)
      |> assign_async(:tags, fn -> classify_text(value) end)
      |> assign_async(:keywords, fn -> find_keywords(self_pid, value) end)

    {:noreply, socket}
  end

  def classify_text(value) do
    {:ok, %{tags: Coding.process_chunk(value)}}
  end

  defp find_keywords(_live_view_pid, ""), do: {:ok, %{keywords: []}}

  defp find_keywords(live_view_pid, text) do
    # First, we use token classification to determine parts of speech and then retrieve the verb and adjective+noun phrases.
    %{entities: entities} =
      Nx.Serving.batched_run(MedicalTranscription.TokenClassificationServing, text)

    phrases = KeywordFinder.cleanup_phrases(entities)

    # Then, we use one of two processes to determine which to show as keywords:
    # 1. A slower process that looks to classify the text by the extracted phrases.
    # serving = KeywordFinder.prepare_zero_shot_classification_serving(phrases)
    # %{predictions: predictions} = Nx.Serving.run(serving, text)

    # 2. A fast process finding the phrase closest in vector space to the whole text.
    predictions = KeywordFinder.find_most_similar_label(text, phrases, 2)

    # For now, retrieve the top three keywords that have a score of more than 0.25
    keywords =
      predictions
      |> Enum.filter(fn keyword -> keyword.score > 0.25 end)
      |> Enum.take(3)

    send(live_view_pid, {:new_keywords, predictions})

    {:ok, %{keywords: keywords}}
  end
end