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
|