You've already forked algora-tv
							
							
				mirror of
				https://github.com/algora-io/tv.git
				synced 2025-10-30 23:07:56 +02:00 
			
		
		
		
	add vector search for vods (#28)
This commit is contained in:
		| @@ -89,7 +89,7 @@ | ||||
|           # If you don't want TODO comments to cause `mix credo` to fail, just | ||||
|           # set this value to 0 (zero). | ||||
|           # | ||||
|           {Credo.Check.Design.TagTODO, [exit_status: 2]}, | ||||
|           {Credo.Check.Design.TagTODO, [exit_status: 0]}, | ||||
|  | ||||
|           # | ||||
|           ## Readability Checks | ||||
|   | ||||
| @@ -7,6 +7,11 @@ AWS_ENDPOINT_URL_S3="https://fly.storage.tigris.dev" | ||||
| AWS_REGION="auto" | ||||
| AWS_ACCESS_KEY_ID="" | ||||
| AWS_SECRET_ACCESS_KEY="" | ||||
| BUCKET_NAME="" | ||||
|  | ||||
| BUCKET_MEDIA="" | ||||
| BUCKET_ML="" | ||||
|  | ||||
| REPLICATE_API_TOKEN="" | ||||
| HF_TOKEN="" | ||||
|  | ||||
| EVENT_SINK_URL="" | ||||
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -38,4 +38,4 @@ npm-debug.log | ||||
| # Ignore local files | ||||
| /tmp | ||||
| /.local | ||||
| /priv/cache | ||||
| /priv/cache | ||||
|   | ||||
							
								
								
									
										2
									
								
								.iex.exs
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								.iex.exs
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| import Ecto.Query | ||||
| import Ecto.Changeset | ||||
|  | ||||
| alias Algora.{Accounts, Library, Repo, Storage} | ||||
| alias Algora.{Accounts, Library, Repo, Storage, Cache, ML} | ||||
|  | ||||
| IEx.configure(inspect: [charlists: :as_lists]) | ||||
|  | ||||
|   | ||||
| @@ -18,7 +18,7 @@ ARG RUNNER_IMAGE="debian:bookworm-20231009-slim" | ||||
| FROM ${BUILDER_IMAGE} as builder | ||||
|  | ||||
| # install build dependencies | ||||
| RUN apt-get update -y && apt-get install -y build-essential git && apt-get clean && rm -f /var/lib/apt/lists/*_* | ||||
| RUN apt-get update -y && apt-get install -y build-essential git curl && apt-get clean && rm -f /var/lib/apt/lists/*_* | ||||
|  | ||||
| # prepare build dir | ||||
| WORKDIR /app | ||||
|   | ||||
| @@ -16,7 +16,7 @@ | ||||
|     @apply border-gray-800; | ||||
|   } | ||||
|   body { | ||||
|     @apply bg-gray-950 text-white; | ||||
|     @apply bg-[radial-gradient(ellipse_at_top_left,_#1d1e3a_0%,_#050217_40%,_#050217_60%,_#1d1e3a_100%)] text-white; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -144,9 +144,15 @@ const Hooks = { | ||||
|         const { player } = detail; | ||||
|         this.player.options({ | ||||
|           techOrder: [player.type === "video/youtube" ? "youtube" : "html5"], | ||||
|           ...(player.currentTime && player.type === "video/youtube" | ||||
|             ? { youtube: { customVars: { start: player.currentTime } } } | ||||
|             : {}), | ||||
|         }); | ||||
|         this.player.src({ src: player.src, type: player.type }); | ||||
|         this.player.play(); | ||||
|         if (player.currentTime && player.type !== "video/youtube") { | ||||
|           this.player.currentTime(player.currentTime); | ||||
|         } | ||||
|         this.player.el().parentElement.classList.remove("hidden"); | ||||
|         this.player.el().parentElement.classList.add("flex"); | ||||
|  | ||||
| @@ -275,11 +281,33 @@ let liveSocket = new LiveSocket("/live", Socket, { | ||||
| let routeUpdated = () => { | ||||
|   // TODO: uncomment | ||||
|   // Focus.focusMain(); | ||||
|  | ||||
|   const player = document.querySelector("#video-player")?.parentElement; | ||||
|   if (!player) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   const pipClasses = [ | ||||
|     "fixed", | ||||
|     "bottom-0", | ||||
|     "right-0", | ||||
|     "z-[1000]", | ||||
|     "w-[100vw]", | ||||
|     "sm:w-[30vw]", | ||||
|   ]; | ||||
|  | ||||
|   if (/^\/[^\/]+\/\d+$/.test(new URL(window.location.href).pathname)) { | ||||
|     player.classList.add("lg:pr-[24rem]"); | ||||
|     player.classList.remove(...pipClasses); | ||||
|   } else { | ||||
|     player.classList.remove("lg:pr-[24rem]"); | ||||
|     player.classList.add(...pipClasses); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // Show progress bar on live navigation and form submits | ||||
| topbar.config({ | ||||
|   barColors: { 0: "rgba(147, 51, 234, 1)" }, | ||||
|   barColors: { 0: "rgba(79, 70, 229, 1)" }, | ||||
|   shadowColor: "rgba(0, 0, 0, .3)", | ||||
| }); | ||||
| window.addEventListener("phx:page-loading-start", (info) => | ||||
|   | ||||
| @@ -33,8 +33,7 @@ config :algora, AlgoraWeb.Embed.Endpoint, | ||||
|   ] | ||||
|  | ||||
| config :algora, Oban, | ||||
|   repo: Algora.Repo, | ||||
|   notifier: Oban.Notifiers.PG, | ||||
|   repo: Algora.Repo.Local, | ||||
|   queues: [default: 10] | ||||
|  | ||||
| config :esbuild, | ||||
| @@ -66,6 +65,8 @@ config :logger, :console, | ||||
| # Use Jason for JSON parsing in Phoenix | ||||
| config :phoenix, :json_library, Jason | ||||
|  | ||||
| config :nx, default_backend: EXLA.Backend | ||||
|  | ||||
| # Import environment specific config. This must remain at the bottom | ||||
| # of this file so it overrides the configuration defined above. | ||||
| import_config "#{config_env()}.exs" | ||||
|   | ||||
| @@ -1,6 +1,10 @@ | ||||
| import Config | ||||
|  | ||||
| config :algora, :files, bucket: System.get_env("BUCKET_NAME") | ||||
| config :algora, mode: :dev | ||||
|  | ||||
| config :algora, :buckets, | ||||
|   media: System.get_env("BUCKET_MEDIA"), | ||||
|   ml: System.get_env("BUCKET_ML") | ||||
|  | ||||
| config :algora, :github, | ||||
|   client_id: System.get_env("GITHUB_CLIENT_ID"), | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import Config | ||||
|  | ||||
| config :algora, mode: :prod | ||||
|  | ||||
| # For production, don't forget to configure the url host | ||||
| # to something meaningful, Phoenix uses this information | ||||
| # when generating URLs. | ||||
|   | ||||
| @@ -12,6 +12,12 @@ if System.get_env("PHX_SERVER") && System.get_env("RELEASE_NAME") do | ||||
|   config :algora, AlgoraWeb.Embed.Endpoint, server: true | ||||
| end | ||||
|  | ||||
| config :algora, | ||||
|   hf_token: System.get_env("HF_TOKEN") | ||||
|  | ||||
| config :replicate, | ||||
|   replicate_api_token: System.get_env("REPLICATE_API_TOKEN") | ||||
|  | ||||
| if config_env() == :prod do | ||||
|   database_url = | ||||
|     System.get_env("DATABASE_URL") || | ||||
| @@ -80,7 +86,9 @@ if config_env() == :prod do | ||||
|     ], | ||||
|     secret_key_base: secret_key_base | ||||
|  | ||||
|   config :algora, :files, bucket: System.fetch_env!("BUCKET_NAME") | ||||
|   config :algora, :buckets, | ||||
|     media: System.get_env("BUCKET_MEDIA"), | ||||
|     ml: System.get_env("BUCKET_ML") | ||||
|  | ||||
|   config :algora, :github, | ||||
|     client_id: System.fetch_env!("GITHUB_CLIENT_ID"), | ||||
|   | ||||
							
								
								
									
										38
									
								
								lib/algora/cache.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/algora/cache.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| defmodule Algora.Cache do | ||||
|   def refetch(key, f) do | ||||
|     result = f.() | ||||
|     key |> path() |> write(result) | ||||
|     result | ||||
|   end | ||||
|  | ||||
|   def fetch(key, f) do | ||||
|     case key |> path() |> read() do | ||||
|       {:ok, result} -> result | ||||
|       {:error, _} -> refetch(key, f) | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def path(key) do | ||||
|     path = key |> String.split("/") |> Enum.map(&Slug.slugify/1) | ||||
|  | ||||
|     dir = | ||||
|       case Algora.config([:mode]) do | ||||
|         :prod -> "/data" | ||||
|         _ -> :code.priv_dir(:algora) | ||||
|       end | ||||
|  | ||||
|     Path.join([dir, "cache"] ++ path) | ||||
|   end | ||||
|  | ||||
|   defp write(path, content) do | ||||
|     File.mkdir_p!(Path.dirname(path)) | ||||
|     File.write(path, :erlang.term_to_binary(content)) | ||||
|   end | ||||
|  | ||||
|   defp read(path) do | ||||
|     case File.read(path) do | ||||
|       {:ok, binary} -> {:ok, :erlang.binary_to_term(binary)} | ||||
|       {:error, error} -> {:error, error} | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -7,8 +7,8 @@ defmodule Algora.Library do | ||||
|   import Ecto.Query, warn: false | ||||
|   import Ecto.Changeset | ||||
|   alias Algora.Accounts.User | ||||
|   alias Algora.{Repo, Accounts, Storage} | ||||
|   alias Algora.Library.{Channel, Video, Events, Subtitle} | ||||
|   alias Algora.{Repo, Accounts, Storage, Cache, ML} | ||||
|   alias Algora.Library.{Channel, Video, Events, Subtitle, Segment} | ||||
|  | ||||
|   @pubsub Algora.PubSub | ||||
|  | ||||
| @@ -183,19 +183,44 @@ defmodule Algora.Library do | ||||
|       join: u in User, | ||||
|       on: v.user_id == u.id, | ||||
|       where: u.id == ^user.id, | ||||
|       select_merge: %{channel_handle: u.handle, channel_name: u.name}, | ||||
|       select_merge: %{ | ||||
|         channel_handle: u.handle, | ||||
|         channel_name: u.name, | ||||
|         channel_avatar_url: u.avatar_url | ||||
|       }, | ||||
|       order_by: [desc: v.inserted_at], | ||||
|       limit: 1 | ||||
|     ) | ||||
|     |> Repo.one() | ||||
|   end | ||||
|  | ||||
|   def transcribe_video(%Video{} = video, cb) do | ||||
|     dir = Path.join(System.tmp_dir!(), video.uuid) | ||||
|     File.mkdir_p!(dir) | ||||
|     mp3_local_path = Path.join(dir, "index.mp3") | ||||
|  | ||||
|     cb.(%{stage: :transmuxing, done: 1, total: 1}) | ||||
|     System.cmd("ffmpeg", ["-i", video.url, "-vn", mp3_local_path]) | ||||
|  | ||||
|     Storage.upload_from_filename(mp3_local_path, "#{video.uuid}/index.mp3", cb) | ||||
|  | ||||
|     File.rm!(mp3_local_path) | ||||
|  | ||||
|     Cache.fetch("#{Video.slug(video)}/transcription", fn -> | ||||
|       ML.transcribe_video_async("#{video.url_root}/index.mp3") | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   def get_mp4_video(id) do | ||||
|     from(v in Video, | ||||
|       where: v.format == :mp4 and (v.transmuxed_from_id == ^id or v.id == ^id), | ||||
|       join: u in User, | ||||
|       on: v.user_id == u.id, | ||||
|       select_merge: %{channel_handle: u.handle, channel_name: u.name} | ||||
|       select_merge: %{ | ||||
|         channel_handle: u.handle, | ||||
|         channel_name: u.name, | ||||
|         channel_avatar_url: u.avatar_url | ||||
|       } | ||||
|     ) | ||||
|     |> Repo.one() | ||||
|   end | ||||
| @@ -394,12 +419,41 @@ defmodule Algora.Library do | ||||
|           v.visibility == :public and | ||||
|           is_nil(v.vertical_thumbnail_url) and | ||||
|           (v.is_live == true or v.duration >= 120 or v.type == :vod), | ||||
|       select_merge: %{channel_handle: u.handle, channel_name: u.name} | ||||
|       select_merge: %{ | ||||
|         channel_handle: u.handle, | ||||
|         channel_name: u.name, | ||||
|         channel_avatar_url: u.avatar_url | ||||
|       } | ||||
|     ) | ||||
|     |> order_by_inserted(:desc) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   def list_videos_by_ids(ids) do | ||||
|     videos = | ||||
|       from(v in Video, | ||||
|         join: u in User, | ||||
|         on: v.user_id == u.id, | ||||
|         select_merge: %{ | ||||
|           channel_handle: u.handle, | ||||
|           channel_name: u.name, | ||||
|           channel_avatar_url: u.avatar_url | ||||
|         }, | ||||
|         where: v.id in ^ids | ||||
|       ) | ||||
|       |> Repo.all() | ||||
|  | ||||
|     video_by_id = fn id -> | ||||
|       videos | ||||
|       |> Enum.find(fn s -> s.id == id end) | ||||
|     end | ||||
|  | ||||
|     ids | ||||
|     |> Enum.reduce([], fn id, acc -> [video_by_id.(id) | acc] end) | ||||
|     |> Enum.filter(& &1) | ||||
|     |> Enum.reverse() | ||||
|   end | ||||
|  | ||||
|   def list_shorts(limit \\ 100) do | ||||
|     from(v in Video, | ||||
|       join: u in User, | ||||
| @@ -409,7 +463,11 @@ defmodule Algora.Library do | ||||
|         not is_nil(v.url) and | ||||
|           is_nil(v.transmuxed_from_id) and v.visibility == :public and | ||||
|           not is_nil(v.vertical_thumbnail_url), | ||||
|       select_merge: %{channel_handle: u.handle, channel_name: u.name} | ||||
|       select_merge: %{ | ||||
|         channel_handle: u.handle, | ||||
|         channel_name: u.name, | ||||
|         channel_avatar_url: u.avatar_url | ||||
|       } | ||||
|     ) | ||||
|     |> order_by_inserted(:desc) | ||||
|     |> Repo.all() | ||||
| @@ -420,7 +478,11 @@ defmodule Algora.Library do | ||||
|       limit: ^limit, | ||||
|       join: u in User, | ||||
|       on: v.user_id == u.id, | ||||
|       select_merge: %{channel_handle: u.handle, channel_name: u.name}, | ||||
|       select_merge: %{ | ||||
|         channel_handle: u.handle, | ||||
|         channel_name: u.name, | ||||
|         channel_avatar_url: u.avatar_url | ||||
|       }, | ||||
|       where: | ||||
|         not is_nil(v.url) and | ||||
|           is_nil(v.transmuxed_from_id) and | ||||
| @@ -435,10 +497,11 @@ defmodule Algora.Library do | ||||
|       limit: ^limit, | ||||
|       join: u in assoc(v, :user), | ||||
|       left_join: m in assoc(v, :messages), | ||||
|       group_by: [v.id, u.handle, u.name], | ||||
|       group_by: [v.id, u.handle, u.name, u.avatar_url], | ||||
|       select_merge: %{ | ||||
|         channel_handle: u.handle, | ||||
|         channel_name: u.name, | ||||
|         channel_avatar_url: u.avatar_url, | ||||
|         messages_count: count(m.id) | ||||
|       }, | ||||
|       where: | ||||
| @@ -489,7 +552,11 @@ defmodule Algora.Library do | ||||
|         where: v.id == ^id, | ||||
|         join: u in User, | ||||
|         on: v.user_id == u.id, | ||||
|         select_merge: %{channel_handle: u.handle, channel_name: u.name} | ||||
|         select_merge: %{ | ||||
|           channel_handle: u.handle, | ||||
|           channel_name: u.name, | ||||
|           channel_avatar_url: u.avatar_url | ||||
|         } | ||||
|       ) | ||||
|       |> Repo.one!() | ||||
|  | ||||
| @@ -513,6 +580,22 @@ defmodule Algora.Library do | ||||
|  | ||||
|   def topic_studio(), do: "studio" | ||||
|  | ||||
|   def list_segments(%Video{} = video) do | ||||
|     from(s in Segment, where: s.video_id == ^video.id, order_by: [asc: s.start]) | ||||
|     |> Repo.all() | ||||
|   end | ||||
|  | ||||
|   def list_segments_by_ids(ids) do | ||||
|     segments = from(s in Segment, where: s.id in ^ids) |> Repo.all() | ||||
|  | ||||
|     segment_by_id = fn id -> segments |> Enum.find(fn s -> s.id == id end) end | ||||
|  | ||||
|     ids | ||||
|     |> Enum.reduce([], fn id, acc -> [segment_by_id.(id) | acc] end) | ||||
|     |> Enum.filter(& &1) | ||||
|     |> Enum.reverse() | ||||
|   end | ||||
|  | ||||
|   def list_subtitles(%Video{} = video) do | ||||
|     from(s in Subtitle, where: s.video_id == ^video.id, order_by: [asc: s.start]) | ||||
|     |> Repo.all() | ||||
|   | ||||
							
								
								
									
										42
									
								
								lib/algora/library/segment.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								lib/algora/library/segment.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| defmodule Algora.Library.Segment do | ||||
|   alias Algora.Library | ||||
|   alias Algora.Library.{Segment, Subtitle} | ||||
|   use Ecto.Schema | ||||
|   import Ecto.Changeset | ||||
|  | ||||
|   schema "segments" do | ||||
|     field :start, :float | ||||
|     field :end, :float | ||||
|     field :body, :string | ||||
|     field :embedding, {:array, :float} | ||||
|     belongs_to :video, Library.Video | ||||
|     belongs_to :starting_subtitle, Library.Subtitle | ||||
|     belongs_to :ending_subtitle, Library.Subtitle | ||||
|  | ||||
|     timestamps() | ||||
|   end | ||||
|  | ||||
|   @doc false | ||||
|   def changeset(segment, attrs) do | ||||
|     segment | ||||
|     |> cast(attrs, [:body, :start, :end]) | ||||
|     |> validate_required([:body, :start, :end]) | ||||
|   end | ||||
|  | ||||
|   def init([]), do: nil | ||||
|  | ||||
|   def init(subtitles) do | ||||
|     body = subtitles |> Enum.map_join("", fn %Subtitle{body: body} -> body end) | ||||
|     starting_subtitle = subtitles |> Enum.at(0) | ||||
|     ending_subtitle = subtitles |> Enum.at(-1) | ||||
|  | ||||
|     %Segment{ | ||||
|       body: body, | ||||
|       start: starting_subtitle.start, | ||||
|       end: ending_subtitle.end, | ||||
|       video_id: starting_subtitle.video_id, | ||||
|       starting_subtitle_id: starting_subtitle.id, | ||||
|       ending_subtitle_id: ending_subtitle.id | ||||
|     } | ||||
|   end | ||||
| end | ||||
| @@ -24,6 +24,7 @@ defmodule Algora.Library.Video do | ||||
|     field :filename, :string | ||||
|     field :channel_handle, :string, virtual: true | ||||
|     field :channel_name, :string, virtual: true | ||||
|     field :channel_avatar_url, :string, virtual: true | ||||
|     field :messages_count, :integer, virtual: true, default: 0 | ||||
|     field :visibility, Ecto.Enum, values: [public: 1, unlisted: 2] | ||||
|     field :remote_path, :string | ||||
| @@ -48,15 +49,26 @@ defmodule Algora.Library.Video do | ||||
|     put_assoc(changeset, :user, user) | ||||
|   end | ||||
|  | ||||
|   def put_video_uuid(%Ecto.Changeset{} = changeset) do | ||||
|     if changeset.valid? do | ||||
|       uuid = Ecto.UUID.generate() | ||||
|  | ||||
|       changeset | ||||
|       |> put_change(:uuid, uuid) | ||||
|       |> put_change(:url_root, url_root(uuid)) | ||||
|     else | ||||
|       changeset | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def put_video_meta(%Ecto.Changeset{} = changeset, format, basename \\ "index") | ||||
|       when format in [:mp4, :hls] do | ||||
|     if changeset.valid? do | ||||
|       uuid = Ecto.UUID.generate() | ||||
|       filename = "#{basename}#{fileext(format)}" | ||||
|  | ||||
|       changeset | ||||
|       |> put_video_uuid() | ||||
|       |> put_change(:filename, filename) | ||||
|       |> put_change(:uuid, uuid) | ||||
|     else | ||||
|       changeset | ||||
|     end | ||||
| @@ -70,7 +82,6 @@ defmodule Algora.Library.Video do | ||||
|  | ||||
|       changeset | ||||
|       |> put_change(:url, url(uuid, filename)) | ||||
|       |> put_change(:url_root, url_root(uuid)) | ||||
|       |> put_change(:remote_path, "#{uuid}/#{filename}") | ||||
|     else | ||||
|       changeset | ||||
| @@ -81,10 +92,14 @@ defmodule Algora.Library.Video do | ||||
|   defp fileext(:hls), do: ".m3u8" | ||||
|  | ||||
|   defp url_root(uuid) do | ||||
|     bucket = Algora.config([:files, :bucket]) | ||||
|     bucket = Algora.config([:buckets, :media]) | ||||
|     %{scheme: scheme, host: host} = Application.fetch_env!(:ex_aws, :s3) |> Enum.into(%{}) | ||||
|     "#{scheme}#{host}/#{bucket}/#{uuid}" | ||||
|   end | ||||
|  | ||||
|   defp url(uuid, filename), do: "#{url_root(uuid)}/#{filename}" | ||||
|  | ||||
|   def slug(%Video{} = video), do: Slug.slugify("#{video.id}-#{video.title}") | ||||
|  | ||||
|   def id_from_slug(slug), do: slug |> String.split("-") |> Enum.at(0) | ||||
| end | ||||
|   | ||||
							
								
								
									
										199
									
								
								lib/algora/ml.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								lib/algora/ml.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| defmodule Algora.ML do | ||||
|   alias Replicate.Predictions | ||||
|   alias Replicate.Predictions.Prediction | ||||
|   alias Algora.{Storage, Library} | ||||
|  | ||||
|   @chunk_size 128 | ||||
|  | ||||
|   @mistral "mistralai/Mixtral-8x7B-Instruct-v0.1" | ||||
|   @mpnet "replicate/all-mpnet-base-v2" | ||||
|   @whisper "vaibhavs10/incredibly-fast-whisper" | ||||
|  | ||||
|   # @mistral_version "" | ||||
|   @mpnet_version "b6b7585c9640cd7a9572c6e129c9549d79c9c31f0d3fdce7baac7c67ca38f305" | ||||
|   @whisper_version "3ab86df6c8f54c11309d4d1f930ac292bad43ace52d10c80d87eb258b3c9f79c" | ||||
|  | ||||
|   def get!(id), do: Predictions.get!(id) | ||||
|  | ||||
|   defp index_local_dir(), do: Path.join(System.tmp_dir!(), "algora/hnswlib") | ||||
|   defp index_local_path(), do: Path.join(index_local_dir(), "index") | ||||
|  | ||||
|   def save_index(index) do | ||||
|     local_path = index_local_path() | ||||
|     HNSWLib.Index.save_index(index, local_path) | ||||
|     Storage.upload_from_filename_to_bucket(local_path, "index", :ml) | ||||
|   end | ||||
|  | ||||
|   def load_index!() do | ||||
|     local_path = index_local_path() | ||||
|  | ||||
|     if !File.exists?(local_path) do | ||||
|       File.mkdir_p!(index_local_dir()) | ||||
|  | ||||
|       {:ok, _} = | ||||
|         ExAws.S3.download_file(Algora.config([:buckets, :ml]), "index", local_path) | ||||
|         |> ExAws.request() | ||||
|     end | ||||
|  | ||||
|     load_index_from_disk!(local_path) | ||||
|   end | ||||
|  | ||||
|   defp load_index_from_disk!(path) do | ||||
|     case HNSWLib.Index.load_index(:cosine, 768, path, max_elements: 100_000) do | ||||
|       {:ok, index} -> | ||||
|         index | ||||
|  | ||||
|       {:error, _} -> | ||||
|         {:ok, index} = HNSWLib.Index.new(:cosine, 768, 1_000_000) | ||||
|         save_index(index) | ||||
|         index | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def add_embeddings(index, segments) do | ||||
|     for %Library.Segment{id: id, embedding: embedding} <- segments do | ||||
|       HNSWLib.Index.add_items(index, Nx.tensor(embedding), ids: [id]) | ||||
|     end | ||||
|  | ||||
|     save_index(index) | ||||
|   end | ||||
|  | ||||
|   def get_relevant_chunks(index, embedding) do | ||||
|     {:ok, labels, _dist} = | ||||
|       HNSWLib.Index.knn_query(index, Nx.tensor(embedding), k: 100) | ||||
|  | ||||
|     labels |> Nx.to_flat_list() |> Library.list_segments_by_ids() | ||||
|   end | ||||
|  | ||||
|   def transcribe_video_async(path) do | ||||
|     run_async( | ||||
|       @whisper, | ||||
|       @whisper_version, | ||||
|       audio: path, | ||||
|       language: "english", | ||||
|       timestamp: "chunk", | ||||
|       batch_size: 64, | ||||
|       diarise_audio: false | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def transcribe_video(path) do | ||||
|     run( | ||||
|       @whisper, | ||||
|       @whisper_version, | ||||
|       audio: path, | ||||
|       language: "english", | ||||
|       timestamp: "chunk", | ||||
|       batch_size: 64, | ||||
|       diarise_audio: false | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def create_embedding(text) do | ||||
|     run(@mpnet, @mpnet_version, text: text) | ||||
|   end | ||||
|  | ||||
|   def create_embeddings(segments) do | ||||
|     text_batch = | ||||
|       segments | ||||
|       |> Enum.map(fn %Library.Segment{body: body} -> body end) | ||||
|       |> Jason.encode!() | ||||
|  | ||||
|     run(@mpnet, @mpnet_version, text_batch: text_batch) | ||||
|   end | ||||
|  | ||||
|   def create_embeddings_async(segments) do | ||||
|     text_batch = | ||||
|       segments | ||||
|       |> Enum.map(fn %Library.Segment{body: body} -> body end) | ||||
|       |> Jason.encode!() | ||||
|  | ||||
|     run_async(@mpnet, @mpnet_version, text_batch: text_batch) | ||||
|   end | ||||
|  | ||||
|   def test do | ||||
|     Regex.named_captures( | ||||
|       ~r/^(?P<model>[^\/]+\/[^:]+):(?P<version>.+)$/, | ||||
|       "replicate/all-mpnet-base-v2" | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   def run(model, version, input) do | ||||
|     Replicate.run("#{model}:#{version}", input) | ||||
|   end | ||||
|  | ||||
|   def run_async(model, version, input) do | ||||
|     model = Replicate.Models.get!(model) | ||||
|     version = Replicate.Models.get_version!(model, version) | ||||
|  | ||||
|     case Predictions.create(version, input) do | ||||
|       {:ok, %Prediction{} = prediction} -> prediction | ||||
|       {:error, message} -> {:error, message} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def fetch_output!(%Prediction{output: output}) do | ||||
|     {:ok, resp} = Finch.build(:get, output) |> Finch.request(Algora.Finch) | ||||
|     Jason.decode!(resp.body) | ||||
|   end | ||||
|  | ||||
|   def load_tokenizer!() do | ||||
|     {:ok, tokenizer} = | ||||
|       Bumblebee.load_tokenizer({:hf, @mistral, auth_token: Algora.config([:hf_token])}, [ | ||||
|         {:type, :llama} | ||||
|       ]) | ||||
|  | ||||
|     tokenizer | ||||
|   end | ||||
|  | ||||
|   def tokenize_and_measure(%Library.Segment{body: body}, tokenizer) do | ||||
|     %{"input_ids" => tensor} = Bumblebee.apply_tokenizer(tokenizer, body) | ||||
|     {1, len} = Nx.shape(tensor) | ||||
|     len | ||||
|   end | ||||
|  | ||||
|   def tokenize_and_measure(subtitles, tokenizer) do | ||||
|     Library.Segment.init(subtitles) |> tokenize_and_measure(tokenizer) | ||||
|   end | ||||
|  | ||||
|   def format_segment(%Library.Segment{start: start, body: body} = segment), | ||||
|     do: | ||||
|       "#{Library.to_hhmmss(start)} - [#{segment.starting_subtitle_id}, #{segment.ending_subtitle_id}]\n#{body}" | ||||
|  | ||||
|   def chunk(video) do | ||||
|     subtitles = Library.list_subtitles(video) | ||||
|  | ||||
|     chunk(load_tokenizer!(), [], [], subtitles) | ||||
|     |> Enum.map(&Library.Segment.init/1) | ||||
|   end | ||||
|  | ||||
|   def chunk(_, chunks, [], []), do: Enum.reverse(chunks) | ||||
|  | ||||
|   def chunk(tokenizer, chunks, chunk, []), do: chunk(tokenizer, [chunk | chunks], [], []) | ||||
|  | ||||
|   def chunk(tokenizer, chunks, chunk, [subtitle | subtitles]) do | ||||
|     new_chunk = [subtitle | chunk] | ||||
|     valid? = tokenize_and_measure(new_chunk, tokenizer) <= @chunk_size | ||||
|  | ||||
|     cond do | ||||
|       valid? -> | ||||
|         chunk(tokenizer, chunks, new_chunk, subtitles) | ||||
|  | ||||
|       chunk == [] -> | ||||
|         chunk( | ||||
|           tokenizer, | ||||
|           chunks, | ||||
|           [], | ||||
|           subtitles | ||||
|         ) | ||||
|  | ||||
|       true -> | ||||
|         chunk( | ||||
|           tokenizer, | ||||
|           [Enum.reverse(chunk) | chunks], | ||||
|           chunk |> Enum.take(min(2, length(chunk) - 1)), | ||||
|           [subtitle | subtitles] | ||||
|         ) | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -83,13 +83,19 @@ defmodule Algora.Storage do | ||||
|     {:ok, state} | ||||
|   end | ||||
|  | ||||
|   def upload(contents, remote_path, opts \\ []) do | ||||
|     Algora.config([:files, :bucket]) | ||||
|   def upload_to_bucket(contents, remote_path, bucket, opts \\ []) do | ||||
|     Algora.config([:buckets, bucket]) | ||||
|     |> ExAws.S3.put_object(remote_path, contents, opts) | ||||
|     |> ExAws.request([]) | ||||
|   end | ||||
|  | ||||
|   def upload_from_filename(local_path, remote_path, cb \\ fn _ -> nil end, opts \\ []) do | ||||
|   def upload_from_filename_to_bucket( | ||||
|         local_path, | ||||
|         remote_path, | ||||
|         bucket, | ||||
|         cb \\ fn _ -> nil end, | ||||
|         opts \\ [] | ||||
|       ) do | ||||
|     %{size: size} = File.stat!(local_path) | ||||
|  | ||||
|     chunk_size = 5 * 1024 * 1024 | ||||
| @@ -99,10 +105,24 @@ defmodule Algora.Storage do | ||||
|       cb.(%{stage: :persisting, done: chunk_size, total: size}) | ||||
|       chunk | ||||
|     end) | ||||
|     |> ExAws.S3.upload(Algora.config([:files, :bucket]), remote_path, opts) | ||||
|     |> ExAws.S3.upload(Algora.config([:buckets, bucket]), remote_path, opts) | ||||
|     |> ExAws.request([]) | ||||
|   end | ||||
|  | ||||
|   def upload(contents, remote_path, opts \\ []) do | ||||
|     upload_to_bucket(contents, remote_path, :media, opts) | ||||
|   end | ||||
|  | ||||
|   def upload_from_filename(local_path, remote_path, cb \\ fn _ -> nil end, opts \\ []) do | ||||
|     upload_from_filename_to_bucket( | ||||
|       local_path, | ||||
|       remote_path, | ||||
|       :media, | ||||
|       cb, | ||||
|       opts | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   defp broadcast!(topic, msg) do | ||||
|     Phoenix.PubSub.broadcast!(@pubsub, topic, {__MODULE__, msg}) | ||||
|   end | ||||
|   | ||||
							
								
								
									
										86
									
								
								lib/algora/util.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								lib/algora/util.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| defmodule Algora.Util do | ||||
|   @common_words [ | ||||
|     "a", | ||||
|     "add", | ||||
|     "again", | ||||
|     "air", | ||||
|     "also", | ||||
|     "an", | ||||
|     "and", | ||||
|     "are", | ||||
|     "as", | ||||
|     "ask", | ||||
|     "at", | ||||
|     "be", | ||||
|     "but", | ||||
|     "by", | ||||
|     "can", | ||||
|     "do", | ||||
|     "does", | ||||
|     "each", | ||||
|     "end", | ||||
|     "even", | ||||
|     "for", | ||||
|     "from", | ||||
|     "get", | ||||
|     "got", | ||||
|     "had", | ||||
|     "have", | ||||
|     "he", | ||||
|     "here", | ||||
|     "his", | ||||
|     "how", | ||||
|     "i", | ||||
|     "if", | ||||
|     "in", | ||||
|     "is", | ||||
|     "it", | ||||
|     "kind", | ||||
|     "men", | ||||
|     "must", | ||||
|     "my", | ||||
|     "near", | ||||
|     "need", | ||||
|     "of", | ||||
|     "off", | ||||
|     "on", | ||||
|     "one", | ||||
|     "or", | ||||
|     "other", | ||||
|     "our", | ||||
|     "out", | ||||
|     "put", | ||||
|     "said", | ||||
|     "self", | ||||
|     "set", | ||||
|     "some", | ||||
|     "such", | ||||
|     "tell", | ||||
|     "that", | ||||
|     "the", | ||||
|     "their", | ||||
|     "they", | ||||
|     "this", | ||||
|     "to", | ||||
|     "try", | ||||
|     "us", | ||||
|     "use", | ||||
|     "want", | ||||
|     "was", | ||||
|     "we're", | ||||
|     "we", | ||||
|     "well", | ||||
|     "went", | ||||
|     "were", | ||||
|     "what", | ||||
|     "which", | ||||
|     "why", | ||||
|     "will", | ||||
|     "with", | ||||
|     "you're", | ||||
|     "you", | ||||
|     "your" | ||||
|   ] | ||||
|  | ||||
|   def is_common_word(s), do: Enum.member?(@common_words, s) | ||||
| end | ||||
							
								
								
									
										75
									
								
								lib/algora/workers/transcriber.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								lib/algora/workers/transcriber.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| defmodule Algora.Workers.Transcriber do | ||||
|   use Oban.Worker, queue: :default, max_attempts: 1, unique: [period: 86_400] | ||||
|  | ||||
|   alias Algora.Library | ||||
|   import Ecto.Query, warn: false | ||||
|  | ||||
|   require Logger | ||||
|  | ||||
|   @impl Oban.Worker | ||||
|   def perform(%Oban.Job{args: %{"video_id" => video_id}} = job) do | ||||
|     video = Library.get_video!(video_id) | ||||
|     build_transcriber(job, video) | ||||
|     await_transcriber(video) | ||||
|   end | ||||
|  | ||||
|   defp build_transcriber(job, %Library.Video{} = video) do | ||||
|     job_pid = self() | ||||
|  | ||||
|     Task.async(fn -> | ||||
|       try do | ||||
|         prediction = | ||||
|           Library.transcribe_video(video, fn progress -> | ||||
|             send(job_pid, {:progress, progress}) | ||||
|           end) | ||||
|  | ||||
|         output = | ||||
|           await_prediction(prediction.id, fn progress -> | ||||
|             send(job_pid, {:progress, progress}) | ||||
|           end) | ||||
|  | ||||
|         dbg(output) | ||||
|  | ||||
|         send(job_pid, {:complete, video}) | ||||
|       rescue | ||||
|         e -> | ||||
|           send(job_pid, {:error, e, job}) | ||||
|           reraise e, __STACKTRACE__ | ||||
|       end | ||||
|     end) | ||||
|   end | ||||
|  | ||||
|   defp await_prediction(id, cb) do | ||||
|     case Replicate.Predictions.get(id) do | ||||
|       {:ok, %Replicate.Predictions.Prediction{status: "succeeded", output: output}} -> | ||||
|         {:ok, resp} = Finch.build(:get, output) |> Finch.request(Algora.Finch) | ||||
|         Jason.decode!(resp.body) | ||||
|  | ||||
|       {:ok, %Replicate.Predictions.Prediction{logs: logs}} -> | ||||
|         cb.(%{stage: logs |> String.split("\n") |> Enum.at(-1), done: 1, total: 1}) | ||||
|         :timer.sleep(1000) | ||||
|         await_prediction(id, cb) | ||||
|  | ||||
|       error -> | ||||
|         error | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   defp await_transcriber(video, stage \\ :retrieving, done \\ 0) do | ||||
|     receive do | ||||
|       {:progress, %{stage: stage_now, done: done_now, total: total}} -> | ||||
|         Library.broadcast_processing_progressed!(stage, video, min(1, done / total)) | ||||
|         done_total = if(stage == stage_now, do: done, else: 0) | ||||
|         await_transcriber(video, stage_now, done_total + done_now) | ||||
|  | ||||
|       {:complete, video} -> | ||||
|         Library.broadcast_processing_progressed!(stage, video, 1) | ||||
|         Library.broadcast_processing_completed!(:transcription, video, video.url) | ||||
|         {:ok, video.url} | ||||
|  | ||||
|       {:error, e, %Oban.Job{attempt: attempt, max_attempts: max_attempts}} -> | ||||
|         Library.broadcast_processing_failed!(video, attempt, max_attempts) | ||||
|         {:error, e} | ||||
|     end | ||||
|   end | ||||
| end | ||||
| @@ -133,7 +133,9 @@ defmodule AlgoraWeb.CoreComponents do | ||||
|  | ||||
|   def short_entry(assigns) do | ||||
|     ~H""" | ||||
|     <.link class="cursor-pointer truncate" navigate={~p"/#{@video.channel_handle}/#{@video.id}"}> | ||||
|     <%!-- HACK: should use navigate instead of href here --%> | ||||
|     <%!-- but it breaks navigating from youtube video to another video --%> | ||||
|     <.link class="cursor-pointer truncate" href={~p"/#{@video.channel_handle}/#{@video.id}"}> | ||||
|       <.short_thumbnail video={@video} class="rounded-2xl" /> | ||||
|       <div class="pt-2 text-base font-semibold truncate"><%= @video.title %></div> | ||||
|       <div class="text-gray-300 text-sm font-medium"><%= @video.channel_name %></div> | ||||
| @@ -146,7 +148,9 @@ defmodule AlgoraWeb.CoreComponents do | ||||
|  | ||||
|   def video_entry(assigns) do | ||||
|     ~H""" | ||||
|     <.link class="cursor-pointer truncate" navigate={~p"/#{@video.channel_handle}/#{@video.id}"}> | ||||
|     <%!-- HACK: should use navigate instead of href here --%> | ||||
|     <%!-- but it breaks navigating from youtube video to another video --%> | ||||
|     <.link class="cursor-pointer truncate" href={~p"/#{@video.channel_handle}/#{@video.id}"}> | ||||
|       <.video_thumbnail video={@video} class="rounded-2xl" /> | ||||
|       <div class="pt-2 text-base font-semibold truncate"><%= @video.title %></div> | ||||
|       <div class="text-gray-300 text-sm font-medium"><%= @video.channel_name %></div> | ||||
|   | ||||
| @@ -10,17 +10,37 @@ | ||||
|       <%= assigns[:page_title] || "Algora TV" %> | ||||
|     </.live_title> | ||||
|  | ||||
|     <%= if assigns[:page_image] do %> | ||||
|       <meta name="description" content={assigns[:page_description]} /> | ||||
|       <meta name="twitter:image:src" content={assigns[:page_image]} /> | ||||
|       <meta name="twitter:site" content="@algoraio" /> | ||||
|       <meta name="twitter:card" content="summary_large_image" /> | ||||
|       <meta name="twitter:title" content={"#{assigns[:page_title]} | Algora TV"} /> | ||||
|       <meta name="twitter:description" content={assigns[:page_description]} /> | ||||
|       <meta property="og:image" content={assigns[:page_image]} /> | ||||
|       <meta property="og:image:width" content="1200" /> | ||||
|       <meta property="og:image:height" content="630" /> | ||||
|       <meta property="og:site_name" content="Algora TV" /> | ||||
|       <meta property="og:type" content="website" /> | ||||
|       <meta property="og:title" content={"#{assigns[:page_title]} | Algora TV"} /> | ||||
|       <meta property="og:url" content={"#{assigns[:page_url]}"} /> | ||||
|       <meta property="og:description" content={assigns[:page_description]} /> | ||||
|     <% end %> | ||||
|  | ||||
|     <%= if assigns[:channel_handle] && assigns[:channel_name] && assigns[:channel_tagline] do %> | ||||
|       <meta name="description" content={assigns[:channel_tagline]} /> | ||||
|       <meta | ||||
|         name="twitter:image:src" | ||||
|         content={"#{AlgoraWeb.Endpoint.url()}/images/og/default.png"} | ||||
|         content={assigns[:page_image] || "#{AlgoraWeb.Endpoint.url()}/images/og/default.png"} | ||||
|       /> | ||||
|       <meta name="twitter:site" content="@algoraio" /> | ||||
|       <meta name="twitter:card" content="summary_large_image" /> | ||||
|       <meta name="twitter:title" content={"#{assigns[:channel_name]} | Algora TV"} /> | ||||
|       <meta name="twitter:description" content={assigns[:channel_tagline]} /> | ||||
|       <meta property="og:image" content={"#{AlgoraWeb.Endpoint.url()}/images/og/default.png"} /> | ||||
|       <meta | ||||
|         property="og:image" | ||||
|         content={assigns[:page_image] || "#{AlgoraWeb.Endpoint.url()}/images/og/default.png"} | ||||
|       /> | ||||
|       <meta property="og:image:width" content="1200" /> | ||||
|       <meta property="og:image:height" content="630" /> | ||||
|       <meta property="og:site_name" content="Algora TV" /> | ||||
|   | ||||
| @@ -14,13 +14,16 @@ | ||||
|       <meta name="description" content={assigns[:channel_tagline]} /> | ||||
|       <meta | ||||
|         name="twitter:image:src" | ||||
|         content={"#{AlgoraWeb.Endpoint.url()}/images/og/default.png"} | ||||
|         content={assigns[:page_image] || "#{AlgoraWeb.Endpoint.url()}/images/og/default.png"} | ||||
|       /> | ||||
|       <meta name="twitter:site" content="@algoraio" /> | ||||
|       <meta name="twitter:card" content="summary_large_image" /> | ||||
|       <meta name="twitter:title" content={"#{assigns[:channel_name]} | Algora TV"} /> | ||||
|       <meta name="twitter:description" content={assigns[:channel_tagline]} /> | ||||
|       <meta property="og:image" content={"#{AlgoraWeb.Endpoint.url()}/images/og/default.png"} /> | ||||
|       <meta | ||||
|         property="og:image" | ||||
|         content={assigns[:page_image] || "#{AlgoraWeb.Endpoint.url()}/images/og/default.png"} | ||||
|       /> | ||||
|       <meta property="og:image:width" content="1200" /> | ||||
|       <meta property="og:image:height" content="630" /> | ||||
|       <meta property="og:site_name" content="Algora TV" /> | ||||
|   | ||||
							
								
								
									
										300
									
								
								lib/algora_web/live/cossgpt_live.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										300
									
								
								lib/algora_web/live/cossgpt_live.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,300 @@ | ||||
| defmodule AlgoraWeb.COSSGPTLive do | ||||
|   use AlgoraWeb, :live_view | ||||
|   alias Algora.{Library, ML, Cache, Util} | ||||
|  | ||||
|   @impl true | ||||
|   def render(assigns) do | ||||
|     ~H""" | ||||
|     <div class="px-4 py-4 lg:py-8 text-white min-h-screen max-w-7xl mx-auto overflow-hidden"> | ||||
|       <h1 class="flex items-center justify-center gap-2 sm:gap-4 text-4xl sm:text-6xl font-bold font-mono text-purple-400 [text-shadow:#000_10px_5px_10px]"> | ||||
|         <svg | ||||
|           xmlns="http://www.w3.org/2000/svg" | ||||
|           width="24" | ||||
|           height="24" | ||||
|           viewBox="0 0 24 24" | ||||
|           fill="none" | ||||
|           stroke="currentColor" | ||||
|           stroke-width="2" | ||||
|           stroke-linecap="round" | ||||
|           stroke-linejoin="round" | ||||
|           class="h-8 w-8 sm:h-16 sm:w-16 text-purple-300" | ||||
|         > | ||||
|           <path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z" /> | ||||
|         </svg> | ||||
|         <span class="text-purple-300">COSS</span><span class="text-purple-400">gpt</span> | ||||
|         <svg | ||||
|           xmlns="http://www.w3.org/2000/svg" | ||||
|           width="24" | ||||
|           height="24" | ||||
|           viewBox="0 0 24 24" | ||||
|           fill="none" | ||||
|           stroke="currentColor" | ||||
|           stroke-width="2" | ||||
|           stroke-linecap="round" | ||||
|           stroke-linejoin="round" | ||||
|           class="h-8 w-8 sm:h-16 sm:w-16 text-purple-400" | ||||
|         > | ||||
|           <path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z" /> | ||||
|         </svg> | ||||
|       </h1> | ||||
|       <form class="mt-4 sm:mt-8 max-w-lg mx-auto" phx-submit="search"> | ||||
|         <label for="query" class="mb-2 text-sm font-medium sr-only text-white"> | ||||
|           Search | ||||
|         </label> | ||||
|         <div class="relative"> | ||||
|           <div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"> | ||||
|             <svg | ||||
|               class="w-4 h-4 text-purple-300" | ||||
|               aria-hidden="true" | ||||
|               xmlns="http://www.w3.org/2000/svg" | ||||
|               fill="none" | ||||
|               viewBox="0 0 20 20" | ||||
|             > | ||||
|               <path | ||||
|                 stroke="currentColor" | ||||
|                 stroke-linecap="round" | ||||
|                 stroke-linejoin="round" | ||||
|                 stroke-width="2" | ||||
|                 d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" | ||||
|               /> | ||||
|             </svg> | ||||
|           </div> | ||||
|           <input | ||||
|             type="search" | ||||
|             id="query" | ||||
|             name="query" | ||||
|             value={@query} | ||||
|             autocomplete="off" | ||||
|             class="w-full p-4 ps-10 text-sm border rounded-lg border-purple-500 bg-white/[5%] placeholder-purple-300 text-white ring-purple-500 ring-1 focus:ring-2 focus:ring-purple-500 focus:outline-none" | ||||
|             placeholder="Ask anything about COSS..." | ||||
|             required | ||||
|           /> | ||||
|           <button | ||||
|             type="submit" | ||||
|             class="text-white absolute end-2.5 bottom-2.5 focus:ring-4 focus:outline-none font-medium rounded-lg text-sm px-4 py-2 bg-purple-600 hover:bg-purple-700 focus:ring-purple-800" | ||||
|           > | ||||
|             Learn | ||||
|           </button> | ||||
|         </div> | ||||
|       </form> | ||||
|       <div class="mt-4 sm:mt-8"> | ||||
|         <div class="uppercase text-center text-gray-300 tracking-tight text-xs font-semibold"> | ||||
|           Suggestions | ||||
|         </div> | ||||
|         <div class="mt-4 flex flex-wrap gap-2 justify-center mx-auto"> | ||||
|           <div | ||||
|             :for={ | ||||
|               suggestion_group <- [ | ||||
|                 [ | ||||
|                   "Benefits of going open source", | ||||
|                   "Business models and pricing", | ||||
|                   "Choosing a license", | ||||
|                   "How to hire engineers" | ||||
|                 ], | ||||
|                 [ | ||||
|                   "How to get your first customers", | ||||
|                   "B2B startup metrics", | ||||
|                   "Setting KPIs and goals", | ||||
|                   "How to fundraise", | ||||
|                   "Developer marketing" | ||||
|                 ] | ||||
|               ] | ||||
|             } | ||||
|             class="-ml-2 -mt-2 p-2 z-10 flex md:justify-center whitespace-nowrap md:flex-wrap gap-4 overflow-x-auto md:overflow-x-hidden scrollbar-thin" | ||||
|           > | ||||
|             <button | ||||
|               :for={suggestion <- suggestion_group} | ||||
|               phx-click="search" | ||||
|               phx-value-query={suggestion} | ||||
|               class={[ | ||||
|                 "text-gray-200 font-medium text-sm px-3 py-2 ring-1 hover:ring-2 ring-white/20 shadow-inner inline-flex rounded-lg hover:ring-white/25 hover:bg-white/5 hover:text-white transition-colors", | ||||
|                 if(suggestion == @query, | ||||
|                   do: "bg-white/5 ring-white/25 hover:ring-white/25", | ||||
|                   else: "bg-white/10 ring-white/20" | ||||
|                 ) | ||||
|               ]} | ||||
|             > | ||||
|               <%= suggestion %> | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="space-y-4 lg:space-y-8 mt-4 lg:mt-8"> | ||||
|         <div :if={@task} class="flex-1 space-y-4 lg:space-y-8"> | ||||
|           <div :for={_ <- 1..2} class="gap-8 hidden lg:flex"> | ||||
|             <div class="w-1/2 rounded-2xl aspect-video bg-white/20 animate-pulse"></div> | ||||
|             <div class="w-1/2 rounded-2xl aspect-video bg-white/20 animate-pulse"></div> | ||||
|           </div> | ||||
|           <div :for={_ <- 1..3} class="gap-8 lg:hidden flex"> | ||||
|             <div class="w-full rounded-2xl aspect-video bg-white/20 animate-pulse lg:hidden"></div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div :if={@results} class="flex-1 space-y-8"> | ||||
|           <div | ||||
|             :for={%{video: video, segments: segments} <- @results} | ||||
|             class="flex flex-col lg:flex-row gap-8" | ||||
|           > | ||||
|             <.link navigate={video_url(video, Enum.at(segments, 0))} class="w-full shrink-0 lg:shrink"> | ||||
|               <.video_thumbnail video={video} class="w-full rounded-2xl" /> | ||||
|             </.link> | ||||
|             <div> | ||||
|               <div> | ||||
|                 <.link | ||||
|                   navigate={video_url(video, Enum.at(segments, 0))} | ||||
|                   class="text-lg font-bold line-clamp-2" | ||||
|                 > | ||||
|                   <%= video.title %> | ||||
|                 </.link> | ||||
|                 <p class="text-sm text-gray-300"><%= Timex.from_now(video.inserted_at) %></p> | ||||
|                 <.link navigate={"/#{video.channel_handle}"} class="mt-2 flex items-center gap-2"> | ||||
|                   <span class="relative flex items-center h-8 w-8 shrink-0 overflow-hidden rounded-full"> | ||||
|                     <img | ||||
|                       class="aspect-square h-full w-full" | ||||
|                       alt={video.channel_name} | ||||
|                       src={video.channel_avatar_url} | ||||
|                     /> | ||||
|                   </span> | ||||
|                   <span class="text-sm text-gray-300"><%= video.channel_name %></span> | ||||
|                 </.link> | ||||
|               </div> | ||||
|               <div class="mt-4 relative"> | ||||
|                 <div class="w-full h-full pointer-events-none absolute bg-gradient-to-r from-transparent from-[75%] to-gray-900 rounded-xl"> | ||||
|                 </div> | ||||
|                 <div class="bg-white/[7.5%] border border-white/[20%] p-4 rounded-xl flex gap-8 w-[calc(100vw-2rem)] md:hidden lg:flex lg:w-[22rem] xl:w-[40rem] overflow-x-auto pb-4 -mb-4 scrollbar-thin"> | ||||
|                   <.link | ||||
|                     :for={segment <- segments} | ||||
|                     class="space-x-2" | ||||
|                     navigate={video_url(video, segment)} | ||||
|                   > | ||||
|                     <div class="w-[66vw] lg:w-[20rem] xl:w-[28rem]"> | ||||
|                       <p class="text-base font-semibold text-green-400"> | ||||
|                         <%= Library.to_hhmmss(segment.start) %> | ||||
|                       </p> | ||||
|                       <p class="mt-2 text-sm"> | ||||
|                         <span | ||||
|                           :for={word <- segment.body |> String.split(~r/\s/)} | ||||
|                           class={[matches_query?(@query_words, word) && "text-green-300 font-medium"]} | ||||
|                         > | ||||
|                           <%= word %> | ||||
|                         </span> | ||||
|                       </p> | ||||
|                     </div> | ||||
|                   </.link> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   defp video_url(video, segment) do | ||||
|     params = | ||||
|       case segment do | ||||
|         nil -> "" | ||||
|         s -> "?t=#{trunc(s.start)}" | ||||
|       end | ||||
|  | ||||
|     "/#{video.channel_handle}/#{video.id}#{params}" | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def mount(_params, _session, socket) do | ||||
|     {:ok, socket} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_params(params, _url, socket) do | ||||
|     {:noreply, socket |> apply_action(socket.assigns.live_action, params)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("search", %{"query" => query}, socket) do | ||||
|     {:noreply, socket |> push_patch(to: ~p"/cossgpt?#{%{query: query}}")} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info({ref, result}, socket) when socket.assigns.task.ref == ref do | ||||
|     {:noreply, assign(socket, task: nil, results: result)} | ||||
|   end | ||||
|  | ||||
|   def handle_info(_, socket) do | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   defp apply_action(socket, :index, params) do | ||||
|     socket = | ||||
|       case params["query"] || "" do | ||||
|         "" -> | ||||
|           socket | ||||
|           |> assign( | ||||
|             query: nil, | ||||
|             query_words: nil, | ||||
|             task: nil, | ||||
|             results: nil | ||||
|           ) | ||||
|  | ||||
|         query -> | ||||
|           socket | ||||
|           |> assign( | ||||
|             query: query, | ||||
|             query_words: query |> String.split(~r/\s/) |> Enum.map(&normalize_word/1), | ||||
|             task: Task.async(fn -> fetch_results(query) end), | ||||
|             results: nil | ||||
|           ) | ||||
|       end | ||||
|  | ||||
|     socket | ||||
|     |> assign( | ||||
|       page_title: "COSSgpt", | ||||
|       page_description: "Learn how to build a commercial open source software company", | ||||
|       page_url: "https://tv.algora.io/cossgpt", | ||||
|       page_image: "#{AlgoraWeb.Endpoint.url()}/images/og/cossgpt.png" | ||||
|     ) | ||||
|   end | ||||
|  | ||||
|   defp fetch_results(query) do | ||||
|     [%{"embedding" => embedding}] = | ||||
|       Cache.fetch("embeddings/#{Slug.slugify(query)}", fn -> | ||||
|         ML.create_embedding(query) | ||||
|       end) | ||||
|  | ||||
|     index = ML.load_index!() | ||||
|  | ||||
|     segments = ML.get_relevant_chunks(index, embedding) | ||||
|  | ||||
|     to_result = fn video -> | ||||
|       %{ | ||||
|         video: video, | ||||
|         segments: segments |> Enum.filter(fn s -> s.video_id == video.id end) | ||||
|       } | ||||
|     end | ||||
|  | ||||
|     segments | ||||
|     |> Enum.map(fn %Library.Segment{video_id: video_id} -> video_id end) | ||||
|     |> Enum.uniq() | ||||
|     |> Library.list_videos_by_ids() | ||||
|     |> Enum.map(to_result) | ||||
|   end | ||||
|  | ||||
|   defp normalize_word(s) do | ||||
|     s | ||||
|     |> String.replace(~r/[^A-Za-z0-9]/, "") | ||||
|     |> String.downcase() | ||||
|   end | ||||
|  | ||||
|   defp matches_query?(query_words, s) do | ||||
|     query_words | ||||
|     |> Enum.any?(fn s2 -> | ||||
|       s1 = normalize_word(s) | ||||
|  | ||||
|       String.length(s1) >= 3 and | ||||
|         String.length(s2) >= 3 and | ||||
|         (String.contains?(s1, s2) or String.contains?(s2, s1)) and | ||||
|         !Util.is_common_word(s1) and | ||||
|         !Util.is_common_word(s2) | ||||
|     end) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										275
									
								
								lib/algora_web/live/cossgpt_og_live.ex
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										275
									
								
								lib/algora_web/live/cossgpt_og_live.ex
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,275 @@ | ||||
| defmodule AlgoraWeb.COSSGPTOGLive do | ||||
|   use AlgoraWeb, :live_view | ||||
|   alias Algora.{Library, ML, Cache, Util} | ||||
|  | ||||
|   @impl true | ||||
|   def render(assigns) do | ||||
|     ~H""" | ||||
|     <div class="px-4 py-8 text-white mx-auto overflow-hidden flex gap-[20rem]"> | ||||
|       <div class="ml-[18rem] h-[calc(100vh-88px)] flex flex-col items-center justify-center scale-150"> | ||||
|         <h1 class="flex items-center gap-4 text-8xl font-bold font-mono text-green-300 [text-shadow:#000_10px_5px_10px]"> | ||||
|           <svg | ||||
|             xmlns="http://www.w3.org/2000/svg" | ||||
|             width="24" | ||||
|             height="24" | ||||
|             viewBox="0 0 24 24" | ||||
|             fill="none" | ||||
|             stroke="currentColor" | ||||
|             stroke-width="2" | ||||
|             stroke-linecap="round" | ||||
|             stroke-linejoin="round" | ||||
|             class="h-20 w-20 text-green-300" | ||||
|           > | ||||
|             <path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z" /> | ||||
|           </svg> | ||||
|           <span class="text-green-300">COSS</span><span class="text-green-400">gpt</span> | ||||
|           <svg | ||||
|             xmlns="http://www.w3.org/2000/svg" | ||||
|             width="24" | ||||
|             height="24" | ||||
|             viewBox="0 0 24 24" | ||||
|             fill="none" | ||||
|             stroke="currentColor" | ||||
|             stroke-width="2" | ||||
|             stroke-linecap="round" | ||||
|             stroke-linejoin="round" | ||||
|             class="h-20 w-20 text-green-400" | ||||
|           > | ||||
|             <path stroke="none" d="M0 0h24v24H0z" fill="none" /><path d="M16 18a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm0 -12a2 2 0 0 1 2 2a2 2 0 0 1 2 -2a2 2 0 0 1 -2 -2a2 2 0 0 1 -2 2zm-7 12a6 6 0 0 1 6 -6a6 6 0 0 1 -6 -6a6 6 0 0 1 -6 6a6 6 0 0 1 6 6z" /> | ||||
|           </svg> | ||||
|         </h1> | ||||
|         <form class="mt-10 w-full max-w-lg mx-auto scale-[1.3]" phx-submit="search"> | ||||
|           <label for="query" class="mb-2 text-sm font-medium sr-only text-white"> | ||||
|             Search | ||||
|           </label> | ||||
|           <div class="relative"> | ||||
|             <div class="absolute inset-y-0 start-0 flex items-center ps-4 pointer-events-none"> | ||||
|               <svg | ||||
|                 class="w-8 h-8 text-green-200" | ||||
|                 aria-hidden="true" | ||||
|                 xmlns="http://www.w3.org/2000/svg" | ||||
|                 fill="none" | ||||
|                 viewBox="0 0 20 20" | ||||
|               > | ||||
|                 <path | ||||
|                   stroke="currentColor" | ||||
|                   stroke-linecap="round" | ||||
|                   stroke-linejoin="round" | ||||
|                   stroke-width="2" | ||||
|                   d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z" | ||||
|                 /> | ||||
|               </svg> | ||||
|             </div> | ||||
|             <input | ||||
|               type="search" | ||||
|               id="query" | ||||
|               name="query" | ||||
|               value={@query} | ||||
|               autocomplete="off" | ||||
|               class="block w-full p-4 ps-14 border rounded-lg border-green-400 bg-white/[5%] placeholder-gray-400 text-green-100 ring-4 ring-green-400 focus:outline-none text-2xl font-medium" | ||||
|               placeholder="Anything about commercial open-source software..." | ||||
|               required | ||||
|             /> | ||||
|             <button | ||||
|               type="submit" | ||||
|               class="text-green-950 text-2xl absolute end-2.5 bottom-2.5 focus:ring-4 focus:outline-none font-bold rounded-lg px-4 py-2 bg-green-200 hover:bg-green-700 focus:ring-green-800" | ||||
|             > | ||||
|               Learn | ||||
|             </button> | ||||
|           </div> | ||||
|         </form> | ||||
|         <div class="mt-20 scale-[1.37]"> | ||||
|           <div class="uppercase text-center text-gray-300 tracking-tight text-xs font-semibold"> | ||||
|             Suggestions | ||||
|           </div> | ||||
|           <div class="mt-2 flex flex-wrap gap-4 justify-center max-w-2xl mx-auto"> | ||||
|             <button | ||||
|               :for={ | ||||
|                 suggestion <- [ | ||||
|                   "Business models and pricing", | ||||
|                   "Choosing a license", | ||||
|                   "How to hire engineers" | ||||
|                 ] | ||||
|               } | ||||
|               phx-click="search" | ||||
|               phx-value-query={suggestion} | ||||
|               class="bg-white/10 text-gray-200 font-medium text-sm px-3 py-2 ring-1 ring-white/20 shadow-inner inline-flex rounded-lg hover:ring-white/25 hover:bg-white/5 hover:text-white transition-colors" | ||||
|             > | ||||
|               <%= suggestion %> | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="flex"> | ||||
|         <div :if={@results} class="flex-1 space-y-8"> | ||||
|           <div | ||||
|             :for={%{video: video, segments: segments} <- @results} | ||||
|             class="flex flex-col lg:flex-row gap-8" | ||||
|           > | ||||
|             <.link | ||||
|               navigate={video_url(video, Enum.at(segments, 0))} | ||||
|               class="w-full shrink-0 max-w-[40rem]" | ||||
|             > | ||||
|               <.video_thumbnail video={video} class="w-full rounded-2xl" /> | ||||
|             </.link> | ||||
|             <div> | ||||
|               <div> | ||||
|                 <.link | ||||
|                   navigate={video_url(video, Enum.at(segments, 0))} | ||||
|                   class="text-lg font-bold line-clamp-2" | ||||
|                 > | ||||
|                   <%= video.title %> | ||||
|                 </.link> | ||||
|                 <p class="text-sm text-gray-300"><%= Timex.from_now(video.inserted_at) %></p> | ||||
|                 <.link navigate={"/#{video.channel_handle}"} class="mt-2 flex items-center gap-2"> | ||||
|                   <span class="relative flex items-center h-8 w-8 shrink-0 overflow-hidden rounded-full"> | ||||
|                     <img | ||||
|                       class="aspect-square h-full w-full" | ||||
|                       alt={video.channel_name} | ||||
|                       src={video.channel_avatar_url} | ||||
|                     /> | ||||
|                   </span> | ||||
|                   <span class="text-sm text-gray-300"><%= video.channel_name %></span> | ||||
|                 </.link> | ||||
|               </div> | ||||
|               <div class="mt-4 relative"> | ||||
|                 <div class="w-full h-full pointer-events-none absolute bg-gradient-to-r from-transparent from-[75%] to-gray-900 rounded-xl"> | ||||
|                 </div> | ||||
|                 <div class="bg-white/[7.5%] border border-white/[20%] p-4 rounded-xl flex gap-8 w-[calc(100vw-2rem)] lg:w-[22rem] xl:w-[40rem] overflow-x-auto pb-4 -mb-4 scrollbar-thin"> | ||||
|                   <.link | ||||
|                     :for={segment <- segments} | ||||
|                     class="space-x-2" | ||||
|                     navigate={video_url(video, segment)} | ||||
|                   > | ||||
|                     <div class="w-[66vw] lg:w-[20rem] xl:w-[28rem]"> | ||||
|                       <p class="text-base font-semibold text-green-400"> | ||||
|                         <%= Library.to_hhmmss(segment.start) %> | ||||
|                       </p> | ||||
|                       <p class="mt-2 text-sm"> | ||||
|                         <span | ||||
|                           :for={word <- segment.body |> String.split(~r/\s/)} | ||||
|                           class={[matches_query?(@query_words, word) && "text-green-300 font-medium"]} | ||||
|                         > | ||||
|                           <%= word %> | ||||
|                         </span> | ||||
|                       </p> | ||||
|                     </div> | ||||
|                   </.link> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div :if={@task} class="flex-1 space-y-8"> | ||||
|           <div :for={_ <- 1..2} class="flex gap-8"> | ||||
|             <div class="w-1/2 rounded-2xl aspect-video bg-white/20 animate-pulse"></div> | ||||
|             <div class="w-1/2 rounded-2xl aspect-video bg-white/20 animate-pulse"></div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   defp video_url(video, segment) do | ||||
|     params = | ||||
|       case segment do | ||||
|         nil -> "" | ||||
|         s -> "?t=#{trunc(s.start)}" | ||||
|       end | ||||
|  | ||||
|     "/#{video.channel_handle}/#{video.id}#{params}" | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def mount(_params, _session, socket) do | ||||
|     {:ok, socket} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_params(params, _url, socket) do | ||||
|     {:noreply, socket |> apply_action(socket.assigns.live_action, params)} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_event("search", %{"query" => query}, socket) do | ||||
|     {:noreply, socket |> push_patch(to: ~p"/og/cossgpt?#{%{query: query}}")} | ||||
|   end | ||||
|  | ||||
|   @impl true | ||||
|   def handle_info({ref, result}, socket) when socket.assigns.task.ref == ref do | ||||
|     {:noreply, assign(socket, task: nil, results: result)} | ||||
|   end | ||||
|  | ||||
|   def handle_info(_, socket) do | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   defp apply_action(socket, :index, params) do | ||||
|     socket = | ||||
|       case params["query"] || "" do | ||||
|         "" -> | ||||
|           socket | ||||
|           |> assign( | ||||
|             query: nil, | ||||
|             query_words: nil, | ||||
|             task: nil, | ||||
|             results: nil | ||||
|           ) | ||||
|  | ||||
|         query -> | ||||
|           socket | ||||
|           |> assign( | ||||
|             query: query, | ||||
|             query_words: query |> String.split(~r/\s/) |> Enum.map(&normalize_word/1), | ||||
|             task: Task.async(fn -> fetch_results(query) end), | ||||
|             results: nil | ||||
|           ) | ||||
|       end | ||||
|  | ||||
|     socket |> assign(:page_title, "COSSgpt") | ||||
|   end | ||||
|  | ||||
|   defp fetch_results(query) do | ||||
|     [%{"embedding" => embedding}] = | ||||
|       Cache.fetch("embeddings/#{Slug.slugify(query)}", fn -> | ||||
|         ML.create_embedding(query) | ||||
|       end) | ||||
|  | ||||
|     index = ML.load_index!() | ||||
|  | ||||
|     segments = ML.get_relevant_chunks(index, embedding) | ||||
|  | ||||
|     to_result = fn video -> | ||||
|       %{ | ||||
|         video: video, | ||||
|         segments: segments |> Enum.filter(fn s -> s.video_id == video.id end) | ||||
|       } | ||||
|     end | ||||
|  | ||||
|     segments | ||||
|     |> Enum.map(fn %Library.Segment{video_id: video_id} -> video_id end) | ||||
|     |> Enum.uniq() | ||||
|     |> Library.list_videos_by_ids() | ||||
|     |> Enum.map(to_result) | ||||
|   end | ||||
|  | ||||
|   defp normalize_word(s) do | ||||
|     s | ||||
|     |> String.replace(~r/[^A-Za-z0-9]/, "") | ||||
|     |> String.downcase() | ||||
|   end | ||||
|  | ||||
|   defp matches_query?(query_words, s) do | ||||
|     query_words | ||||
|     |> Enum.any?(fn s2 -> | ||||
|       s1 = normalize_word(s) | ||||
|  | ||||
|       String.length(s1) >= 3 and | ||||
|         String.length(s2) >= 3 and | ||||
|         (String.contains?(s1, s2) or String.contains?(s2, s1)) and | ||||
|         !Util.is_common_word(s1) and | ||||
|         !Util.is_common_word(s2) | ||||
|     end) | ||||
|   end | ||||
| end | ||||
| @@ -10,7 +10,7 @@ defmodule AlgoraWeb.PlayerLive do | ||||
|         <video | ||||
|           id="video-player" | ||||
|           phx-hook="VideoPlayer" | ||||
|           class="video-js vjs-default-skin min-w-xl max-w-2xl aspect-video vjs-fluid h-full w-full flex-1 rounded-2xl overflow-hidden" | ||||
|           class="video-js vjs-default-skin aspect-video h-full w-full flex-1 rounded-2xl overflow-hidden" | ||||
|           controls | ||||
|         /> | ||||
|       </div> | ||||
|   | ||||
| @@ -10,7 +10,7 @@ defmodule AlgoraWeb.SignInLive do | ||||
|         </h2> | ||||
|         <a | ||||
|           href={Algora.Github.authorize_url()} | ||||
|           class="mt-8 w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-400" | ||||
|           class="mt-8 w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-400" | ||||
|         > | ||||
|           Sign in with GitHub | ||||
|         </a> | ||||
|   | ||||
| @@ -252,21 +252,31 @@ defmodule AlgoraWeb.StudioLive do | ||||
|       {:noreply, redirect(socket, external: mp4_video.url)} | ||||
|     else | ||||
|       video = Library.get_video!(id) | ||||
|       send(self(), {Library, %Library.Events.ProcessingQueued{video: video}}) | ||||
|  | ||||
|       %{video_id: id} | ||||
|       |> Workers.MP4Transmuxer.new() | ||||
|       |> Oban.insert() | ||||
|  | ||||
|       send(self(), {Library, %Library.Events.ProcessingQueued{video: video}}) | ||||
|       {:noreply, socket} | ||||
|     end | ||||
|   end | ||||
|  | ||||
|   def handle_event("transcribe_video", %{"id" => id}, socket) do | ||||
|     video = Library.get_video!(id) | ||||
|  | ||||
|     %{video_id: id} | ||||
|     |> Workers.Transcriber.new() | ||||
|     |> Oban.insert() | ||||
|  | ||||
|     send(self(), {Library, %Library.Events.ProcessingQueued{video: video}}) | ||||
|     {:noreply, socket} | ||||
|   end | ||||
|  | ||||
|   def handle_event("upload_videos", _params, socket) do | ||||
|     _videos = | ||||
|       consume_uploaded_entries(socket, :video, fn %{path: path}, entry -> | ||||
|         video = Library.init_mp4!(entry, path, socket.assigns.current_user) | ||||
|         send(self(), {Library, %Library.Events.ProcessingQueued{video: video}}) | ||||
|  | ||||
|         # TODO: add to oban queue instead | ||||
|         # ensure that the worker runs in the same machine where the upload is consumed | ||||
| @@ -275,6 +285,7 @@ defmodule AlgoraWeb.StudioLive do | ||||
|         # |> Oban.insert() | ||||
|         Library.transmux_to_hls(video, fn _ -> nil end) | ||||
|  | ||||
|         send(self(), {Library, %Library.Events.ProcessingQueued{video: video}}) | ||||
|         {:ok, video} | ||||
|       end) | ||||
|  | ||||
|   | ||||
| @@ -346,7 +346,11 @@ defmodule AlgoraWeb.VideoLive do | ||||
|     """ | ||||
|   end | ||||
|  | ||||
|   def mount(%{"channel_handle" => channel_handle, "video_id" => video_id}, _session, socket) do | ||||
|   def mount( | ||||
|         %{"channel_handle" => channel_handle, "video_id" => video_id} = params, | ||||
|         _session, | ||||
|         socket | ||||
|       ) do | ||||
|     %{current_user: current_user} = socket.assigns | ||||
|  | ||||
|     channel = | ||||
| @@ -378,11 +382,10 @@ defmodule AlgoraWeb.VideoLive do | ||||
|       |> Jason.encode(pretty: true) | ||||
|  | ||||
|     types = %{subtitles: :string} | ||||
|     params = %{subtitles: encoded_subtitles} | ||||
|  | ||||
|     changeset = | ||||
|       {data, types} | ||||
|       |> Ecto.Changeset.cast(params, Map.keys(types)) | ||||
|       |> Ecto.Changeset.cast(%{subtitles: encoded_subtitles}, Map.keys(types)) | ||||
|  | ||||
|     socket = | ||||
|       socket | ||||
| @@ -398,7 +401,7 @@ defmodule AlgoraWeb.VideoLive do | ||||
|       |> stream(:videos, videos) | ||||
|       |> stream(:presences, Presence.list_online_users(channel_handle)) | ||||
|  | ||||
|     if connected?(socket), do: send(self(), {:play, video}) | ||||
|     if connected?(socket), do: send(self(), {:play, {video, params["t"]}}) | ||||
|  | ||||
|     {:ok, socket} | ||||
|   end | ||||
| @@ -408,11 +411,17 @@ defmodule AlgoraWeb.VideoLive do | ||||
|     {:noreply, socket |> apply_action(socket.assigns.live_action, params)} | ||||
|   end | ||||
|  | ||||
|   def handle_info({:play, video}, socket) do | ||||
|   def handle_info({:play, {video, t}}, socket) do | ||||
|     socket = | ||||
|       socket | ||||
|       |> push_event("play_video", %{ | ||||
|         detail: %{player: %{src: video.url, type: Library.player_type(video)}} | ||||
|         detail: %{ | ||||
|           player: %{ | ||||
|             src: video.url, | ||||
|             type: Library.player_type(video), | ||||
|             currentTime: t | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|       |> push_event("join_chat", %{id: video.id}) | ||||
|  | ||||
|   | ||||
| @@ -82,6 +82,8 @@ defmodule AlgoraWeb.Router do | ||||
|     live_session :default, on_mount: [{AlgoraWeb.UserAuth, :current_user}, AlgoraWeb.Nav] do | ||||
|       live "/", HomeLive, :show | ||||
|       live "/auth/login", SignInLive, :index | ||||
|       live "/cossgpt", COSSGPTLive, :index | ||||
|       live "/og/cossgpt", COSSGPTOGLive, :index | ||||
|       live "/:channel_handle", ChannelLive, :show | ||||
|       live "/:channel_handle/:video_id", VideoLive, :show | ||||
|     end | ||||
|   | ||||
							
								
								
									
										7
									
								
								mix.exs
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								mix.exs
									
									
									
									
									
								
							| @@ -32,14 +32,17 @@ defmodule Algora.MixProject do | ||||
|   # Type `mix help deps` for examples and options. | ||||
|   defp deps do | ||||
|     [ | ||||
|       {:bumblebee, "~> 0.5.3"}, | ||||
|       {:castore, "~> 0.1.13"}, | ||||
|       {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, | ||||
|       {:dialyxir, "~> 1.3", only: [:dev], runtime: false}, | ||||
|       {:dns_cluster, "~> 0.1.1"}, | ||||
|       {:ecto_network, "~> 1.3.0"}, | ||||
|       {:ecto_sql, "~> 3.6"}, | ||||
|       {:elixir_make, "~> 0.7.0", runtime: false}, | ||||
|       {:esbuild, "~> 0.8", runtime: Mix.env() == :dev}, | ||||
|       {:ex_m3u8, "~> 0.9.0"}, | ||||
|       {:exla, ">= 0.0.0"}, | ||||
|       {:exsync, "~> 0.2", only: :dev}, | ||||
|       {:ffmpex, "~> 0.10.0"}, | ||||
|       {:finch, "~> 0.13"}, | ||||
| @@ -47,6 +50,7 @@ defmodule Algora.MixProject do | ||||
|       {:fly_postgres, "~> 0.3.0"}, | ||||
|       {:gettext, "~> 0.18"}, | ||||
|       {:heroicons, "~> 0.5.0"}, | ||||
|       {:hnswlib, "~> 0.1.0"}, | ||||
|       {:jason, "~> 1.2"}, | ||||
|       {:libcluster, "~> 3.3.1"}, | ||||
|       {:membrane_core, "~> 1.0"}, | ||||
| @@ -55,13 +59,14 @@ defmodule Algora.MixProject do | ||||
|       {:mint, "~> 1.0"}, | ||||
|       {:oban, "~> 2.16"}, | ||||
|       {:phoenix_ecto, "~> 4.4"}, | ||||
|       {:phoenix_html, "~> 4.0", override: true}, | ||||
|       {:phoenix_html_helpers, "~> 1.0"}, | ||||
|       {:phoenix_html, "~> 4.0", override: true}, | ||||
|       {:phoenix_live_dashboard, "~> 0.8.3"}, | ||||
|       {:phoenix_live_reload, "~> 1.2", only: :dev}, | ||||
|       {:phoenix_live_view, "~> 0.20.2"}, | ||||
|       {:phoenix, "~> 1.7.11"}, | ||||
|       {:plug_cowboy, "~> 2.5"}, | ||||
|       {:replicate, "~> 1.2.0"}, | ||||
|       {:slugify, "~> 1.3"}, | ||||
|       {:swoosh, "~> 1.3"}, | ||||
|       {:tailwind, "~> 0.2", runtime: Mix.env() == :dev}, | ||||
|   | ||||
							
								
								
									
										20
									
								
								mix.lock
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								mix.lock
									
									
									
									
									
								
							| @@ -1,13 +1,17 @@ | ||||
| %{ | ||||
|   "axon": {:hex, :axon, "0.6.1", "1d042fdba1c1b4413a3d65800524feebd1bc8ed218f8cdefe7a97510c3f427f3", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:kino_vega_lite, "~> 0.1.7", [hex: :kino_vega_lite, repo: "hexpm", optional: true]}, {:nx, "~> 0.6.0 or ~> 0.7.0", [hex: :nx, repo: "hexpm", optional: false]}, {:polaris, "~> 0.1", [hex: :polaris, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "d6b0ae2f0dd284f6bf702edcab71e790d6c01ca502dd06c4070836554f5a48e1"}, | ||||
|   "bimap": {:hex, :bimap, "1.3.0", "3ea4832e58dc83a9b5b407c6731e7bae87458aa618e6d11d8e12114a17afa4b3", [:mix], [], "hexpm", "bf5a2b078528465aa705f405a5c638becd63e41d280ada41e0f77e6d255a10b4"}, | ||||
|   "bumblebee": {:hex, :bumblebee, "0.5.3", "151c215fd6014958dbfc322fe5f31b44d170293f69cfdca419936c81e39b1f64", [:mix], [{:axon, "~> 0.6.1", [hex: :axon, repo: "hexpm", optional: false]}, {:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.7.0", [hex: :nx, repo: "hexpm", optional: false]}, {:nx_image, "~> 0.1.0", [hex: :nx_image, repo: "hexpm", optional: false]}, {:nx_signal, "~> 0.2.0", [hex: :nx_signal, repo: "hexpm", optional: false]}, {:progress_bar, "~> 3.0", [hex: :progress_bar, repo: "hexpm", optional: false]}, {:safetensors, "~> 0.1.3", [hex: :safetensors, repo: "hexpm", optional: false]}, {:tokenizers, "~> 0.4", [hex: :tokenizers, repo: "hexpm", optional: false]}, {:unpickler, "~> 0.1.0", [hex: :unpickler, repo: "hexpm", optional: false]}, {:unzip, "~> 0.10.0", [hex: :unzip, repo: "hexpm", optional: false]}], "hexpm", "5518f11e424c431a9cbedc80e0d26525368f0b6e50572a674ff247ec3b26bdd7"}, | ||||
|   "bunch": {:hex, :bunch, "1.6.1", "5393d827a64d5f846092703441ea50e65bc09f37fd8e320878f13e63d410aec7", [:mix], [], "hexpm", "286cc3add551628b30605efbe2fca4e38cc1bea89bcd0a1a7226920b3364fe4a"}, | ||||
|   "bunch_native": {:hex, :bunch_native, "0.5.0", "8ac1536789a597599c10b652e0b526d8833348c19e4739a0759a2bedfd924e63", [:mix], [{:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "24190c760e32b23b36edeb2dc4852515c7c5b3b8675b1a864e0715bdd1c8f80d"}, | ||||
|   "bundlex": {:hex, :bundlex, "1.4.5", "ea06cb441af636baaf5232dced24c6b1ee5ccbe7a7cad8a348eb3100fa1d7b52", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:elixir_uuid, "~> 1.2", [hex: :elixir_uuid, repo: "hexpm", optional: false]}, {:qex, "~> 0.5", [hex: :qex, repo: "hexpm", optional: false]}, {:req, "~> 0.4.0", [hex: :req, repo: "hexpm", optional: false]}, {:zarex, "~> 1.0", [hex: :zarex, repo: "hexpm", optional: false]}], "hexpm", "bd4136100d3120740bf8eaa73ad74859d5ccd659cf0b27aa1645590a67a0172b"}, | ||||
|   "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, | ||||
|   "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"}, | ||||
|   "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, | ||||
|   "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, | ||||
|   "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, | ||||
|   "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, | ||||
|   "complex": {:hex, :complex, "0.5.0", "af2d2331ff6170b61bb738695e481b27a66780e18763e066ee2cd863d0b1dd92", [:mix], [], "hexpm", "2683bd3c184466cfb94fad74cbfddfaa94b860e27ad4ca1bffe3bff169d91ef1"}, | ||||
|   "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"}, | ||||
|   "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, | ||||
|   "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, | ||||
| @@ -21,6 +25,7 @@ | ||||
|   "ecto": {:hex, :ecto, "3.9.6", "2f420c173efcb2e22fa4f8fc41e75e02b3c5bd4cffef12085cae5418c12e530d", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df17bc06ba6f78a7b764e4a14ef877fe5f4499332c5a105ace11fe7013b72c84"}, | ||||
|   "ecto_network": {:hex, :ecto_network, "1.3.0", "1e77fa37c20e0f6a426d3862732f3317b0fa4c18f123d325f81752a491d7304e", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:phoenix_html, ">= 0.0.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.14.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "053a5e46ef2837e8ea5ea97c82fa0f5494699209eddd764e663c85f11b2865bd"}, | ||||
|   "ecto_sql": {:hex, :ecto_sql, "3.9.0", "2bb21210a2a13317e098a420a8c1cc58b0c3421ab8e3acfa96417dab7817918c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8f3f720073b8b1ac4c978be25fa7960ed7fd44997420c304a4a2e200b596453"}, | ||||
|   "elixir_make": {:hex, :elixir_make, "0.7.8", "505026f266552ee5aabca0b9f9c229cbb496c689537c9f922f3eb5431157efc7", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "7a71945b913d37ea89b06966e1342c85cfe549b15e6d6d081e8081c493062c07"}, | ||||
|   "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, | ||||
|   "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, | ||||
|   "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, | ||||
| @@ -28,6 +33,7 @@ | ||||
|   "ex_aws_s3": {:hex, :ex_aws_s3, "2.5.3", "422468e5c3e1a4da5298e66c3468b465cfd354b842e512cb1f6fbbe4e2f5bdaf", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm", "4f09dd372cc386550e484808c5ac5027766c8d0cd8271ccc578b82ee6ef4f3b8"}, | ||||
|   "ex_doc": {:hex, :ex_doc, "0.29.4", "6257ecbb20c7396b1fe5accd55b7b0d23f44b6aa18017b415cb4c2b91d997729", [:mix], [{:earmark_parser, "~> 1.4.31", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "2c6699a737ae46cb61e4ed012af931b57b699643b24dabe2400a8168414bc4f5"}, | ||||
|   "ex_m3u8": {:hex, :ex_m3u8, "0.9.0", "54a12463320236aab09402bc69676f665e692636235a2b186a22df507ebc5643", [:mix], [{:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:typed_struct, "~> 0.3.0", [hex: :typed_struct, repo: "hexpm", optional: false]}], "hexpm", "d57939a90d8da5956264d27a516c5e2ac80b09c8adbe4e3199d7d14c79549b5c"}, | ||||
|   "exla": {:hex, :exla, "0.7.0", "27fac40a580f0d3816fe3bf35c50dfc2f99597d26ac7e2aca4a3c62b89bb427f", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.7.0", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.6.0", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "d3bfc622deb52cec95efc9d76063891afc7cd33e38eddbb01f3385c53e043c40"}, | ||||
|   "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"}, | ||||
|   "exsync": {:hex, :exsync, "0.3.0", "39ab8b3d4e5fe779a34ad930135145283ebf56069513dfdfaad4e30a04b158c7", [:mix], [{:file_system, "~> 0.2", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "2030d085a14fa5f685d53d97171a21345dddaf2b67a0927263efc6b2cd2bb09f"}, | ||||
|   "ffmpex": {:hex, :ffmpex, "0.10.0", "ce29281eac60bf109c05acb4342eecf813a3cd3f08c1bce350423caad86128af", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:rambo, "~> 0.3.0", [hex: :rambo, repo: "hexpm", optional: false]}], "hexpm", "de8d81f8c51cc258dcee9a3e0b1568b0659c97be004557d9af47795206cff53b"}, | ||||
| @@ -39,7 +45,9 @@ | ||||
|   "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"}, | ||||
|   "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, | ||||
|   "heroicons": {:hex, :heroicons, "0.5.3", "ee8ae8335303df3b18f2cc07f46e1cb6e761ba4cf2c901623fbe9a28c0bc51dd", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "a210037e8a09ac17e2a0a0779d729e89c821c944434c3baa7edfc1f5b32f3502"}, | ||||
|   "hnswlib": {:hex, :hnswlib, "0.1.5", "750bea8627ea60dfdea67421aef34478c31bf1254495ebd43a6a100aaf0523c0", [:make, :mix], [{:cc_precompiler, "~> 0.1.0", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "833a3dcfd917236a4e3dd1917725c32d68f65cbf37888cb2af8d38a4e547f8f3"}, | ||||
|   "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, | ||||
|   "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, | ||||
|   "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, | ||||
|   "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, | ||||
|   "libcluster": {:hex, :libcluster, "3.3.1", "e7a4875cd1290cee7a693d6bd46076863e9e433708b01339783de6eff5b7f0aa", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b575ca63c1cd84e01f3fa0fc45e6eb945c1ee7ae8d441d33def999075e9e5398"}, | ||||
| @@ -76,6 +84,9 @@ | ||||
|   "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, | ||||
|   "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, | ||||
|   "numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"}, | ||||
|   "nx": {:hex, :nx, "0.7.1", "5f6376e3d18408116e8a84b8f4ac851fb07dfe61764a5410ebf0b5dcb69c1b7e", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e3ddd6a3f2a9bac79c67b3933368c25bb5ec814a883fc68aba8fd8a236751777"}, | ||||
|   "nx_image": {:hex, :nx_image, "0.1.2", "0c6e3453c1dc30fc80c723a54861204304cebc8a89ed3b806b972c73ee5d119d", [:mix], [{:nx, "~> 0.4", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "9161863c42405ddccb6dbbbeae078ad23e30201509cc804b3b3a7c9e98764b81"}, | ||||
|   "nx_signal": {:hex, :nx_signal, "0.2.0", "e1ca0318877b17c81ce8906329f5125f1e2361e4c4235a5baac8a95ee88ea98e", [:mix], [{:nx, "~> 0.6", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "7247e5e18a177a59c4cb5355952900c62fdeadeb2bad02a9a34237b68744e2bb"}, | ||||
|   "oban": {:hex, :oban, "2.17.3", "ddfd5710aadcd550d2e174c8d73ce5f1865601418cf54a91775f20443fb832b7", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "452eada8bfe0d0fefd0740ab5fa8cf3ef6c375df0b4a3c3805d179022a04738a"}, | ||||
|   "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, | ||||
|   "phoenix": {:hex, :phoenix, "1.7.11", "1d88fc6b05ab0c735b250932c4e6e33bfa1c186f76dcf623d8dd52f07d6379c7", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "b1ec57f2e40316b306708fe59b92a16b9f6f4bf50ccfa41aa8c7feb79e0ec02a"}, | ||||
| @@ -90,12 +101,17 @@ | ||||
|   "plug": {:hex, :plug, "1.15.3", "712976f504418f6dff0a3e554c40d705a9bcf89a7ccef92fc6a5ef8f16a30a97", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4365a3c010a56af402e0809208873d113e9c38c401cabd88027ef4f5c01fd2"}, | ||||
|   "plug_cowboy": {:hex, :plug_cowboy, "2.7.0", "3ae9369c60641084363b08fe90267cbdd316df57e3557ea522114b30b63256ea", [:mix], [{:cowboy, "~> 2.7.0 or ~> 2.8.0 or ~> 2.9.0 or ~> 2.10.0", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d85444fb8aa1f2fc62eabe83bbe387d81510d773886774ebdcb429b3da3c1a4a"}, | ||||
|   "plug_crypto": {:hex, :plug_crypto, "2.0.0", "77515cc10af06645abbfb5e6ad7a3e9714f805ae118fa1a70205f80d2d70fe73", [:mix], [], "hexpm", "53695bae57cc4e54566d993eb01074e4d894b65a3766f1c43e2c61a1b0f45ea9"}, | ||||
|   "polaris": {:hex, :polaris, "0.1.0", "dca61b18e3e801ecdae6ac9f0eca5f19792b44a5cb4b8d63db50fc40fc038d22", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "13ef2b166650e533cb24b10e2f3b8ab4f2f449ba4d63156e8c569527f206e2c2"}, | ||||
|   "postgrex": {:hex, :postgrex, "0.16.5", "fcc4035cc90e23933c5d69a9cd686e329469446ef7abba2cf70f08e2c4b69810", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "edead639dc6e882618c01d8fc891214c481ab9a3788dfe38dd5e37fd1d5fb2e8"}, | ||||
|   "progress_bar": {:hex, :progress_bar, "3.0.0", "f54ff038c2ac540cfbb4c2bfe97c75e7116ead044f3c2b10c9f212452194b5cd", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "6981c2b25ab24aecc91a2dc46623658e1399c21a2ae24db986b90d678530f2b7"}, | ||||
|   "qex": {:hex, :qex, "0.5.1", "0d82c0f008551d24fffb99d97f8299afcb8ea9cf99582b770bd004ed5af63fd6", [:mix], [], "hexpm", "935a39fdaf2445834b95951456559e9dc2063d0a055742c558a99987b38d6bab"}, | ||||
|   "rambo": {:hex, :rambo, "0.3.4", "8962ac3bd1a633ee9d0e8b44373c7913e3ce3d875b4151dcd060886092d2dce7", [:mix], [], "hexpm", "0cc54ed089fbbc84b65f4b8a774224ebfe60e5c80186fafc7910b3e379ad58f1"}, | ||||
|   "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, | ||||
|   "ratio": {:hex, :ratio, "3.0.2", "60a5976872a4dc3d873ecc57eed1738589e99d1094834b9c935b118231297cfb", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "3a13ed5a30ad0bfd7e4a86bf86d93d2b5a06f5904417d38d3f3ea6406cdfc7bb"}, | ||||
|   "replicate": {:hex, :replicate, "1.2.0", "802d6826a89a11aded0d3586d6d0418db3c20590f5cc04727b9951e0efe566e9", [:mix], [{:httpoison, "~> 2.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6eeeef65de231784937b0f4c0336fe247caf5845930d2ee667d2b3ed5cec888d"}, | ||||
|   "req": {:hex, :req, "0.4.8", "2b754a3925ddbf4ad78c56f30208ced6aefe111a7ea07fb56c23dccc13eb87ae", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7146e51d52593bb7f20d00b5308a5d7d17d663d6e85cd071452b613a8277100c"}, | ||||
|   "rustler_precompiled": {:hex, :rustler_precompiled, "0.7.1", "ecadf02cc59a0eccbaed6c1937303a5827fbcf60010c541595e6d3747d3d0f9f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "b9e4657b99a1483ea31502e1d58c464bedebe9028808eda45c3a429af4550c66"}, | ||||
|   "safetensors": {:hex, :safetensors, "0.1.3", "7ff3c22391e213289c713898481d492c9c28a49ab1d0705b72630fb8360426b2", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "fe50b53ea59fde4e723dd1a2e31cfdc6013e69343afac84c6be86d6d7c562c14"}, | ||||
|   "shmex": {:hex, :shmex, "0.5.0", "7dc4fb1a8bd851085a652605d690bdd070628717864b442f53d3447326bcd3e8", [:mix], [{:bunch_native, "~> 0.5.0", [hex: :bunch_native, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "b67bb1e22734758397c84458dbb746519e28eac210423c267c7248e59fc97bdc"}, | ||||
|   "slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"}, | ||||
|   "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, | ||||
| @@ -108,12 +124,16 @@ | ||||
|   "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, | ||||
|   "thumbnex": {:hex, :thumbnex, "0.5.0", "9f3c20c8c70d17e108710830e1495548b45c7433f30dc318f1075d76eb6f7f00", [:mix], [{:ffmpex, "~> 0.10.0", [hex: :ffmpex, repo: "hexpm", optional: false]}, {:mogrify, "~> 0.9.0", [hex: :mogrify, repo: "hexpm", optional: false]}], "hexpm", "a187948110e2de8dc2e9a73d5a3489398ba6a44d285293c174b6285717c5e5fc"}, | ||||
|   "timex": {:hex, :timex, "3.7.11", "bb95cb4eb1d06e27346325de506bcc6c30f9c6dea40d1ebe390b262fad1862d1", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.20", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "8b9024f7efbabaf9bd7aa04f65cf8dcd7c9818ca5737677c7b76acbc6a94d1aa"}, | ||||
|   "tokenizers": {:hex, :tokenizers, "0.4.0", "140283ca74a971391ddbd83cd8cbdb9bd03736f37a1b6989b82d245a95e1eb97", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "ef1a9824f5a893cd3b831c0e5b3d72caa250d2ec462035cc6afef6933b13a82e"}, | ||||
|   "turbojpeg": {:hex, :turbojpeg, "0.4.0", "02616e44a70788e40bfc1fdbbdd8bc4e4615cd7d5ced5614b2aefb60a8acbda7", [:mix], [{:bundlex, "~> 1.4.0", [hex: :bundlex, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_raw_video_format, "~> 0.3.0", [hex: :membrane_raw_video_format, repo: "hexpm", optional: false]}, {:unifex, "~> 1.1.0", [hex: :unifex, repo: "hexpm", optional: false]}], "hexpm", "53759d41f6e7d63805dc014db11b5c8e9274c5e67caea46d8b7f314dcdf51431"}, | ||||
|   "typed_struct": {:hex, :typed_struct, "0.3.0", "939789e3c1dca39d7170c87f729127469d1315dcf99fee8e152bb774b17e7ff7", [:mix], [], "hexpm", "c50bd5c3a61fe4e198a8504f939be3d3c85903b382bde4865579bc23111d1b6d"}, | ||||
|   "tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"}, | ||||
|   "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, | ||||
|   "unifex": {:hex, :unifex, "1.1.0", "26b1bcb6c3b3454e1ea15f85b2e570aaa5b5c609566aa9f5c2e0a8b213379d6b", [:mix], [{:bunch, "~> 1.0", [hex: :bunch, repo: "hexpm", optional: false]}, {:bundlex, "~> 1.0", [hex: :bundlex, repo: "hexpm", optional: false]}, {:shmex, "~> 0.5.0", [hex: :shmex, repo: "hexpm", optional: false]}], "hexpm", "d8f47e9e3240301f5b20eec5792d1d4341e1a3a268d94f7204703b48da4aaa06"}, | ||||
|   "unpickler": {:hex, :unpickler, "0.1.0", "c2262c0819e6985b761e7107546cef96a485f401816be5304a65fdd200d5bd6a", [:mix], [], "hexpm", "e2b3f61e62406187ac52afead8a63bfb4e49394028993f3c4c42712743cab79e"}, | ||||
|   "unzip": {:hex, :unzip, "0.10.0", "374e0059e48e982076f3fd22cd4817ab11016c1bae3f09421511901ddda95c5c", [:mix], [], "hexpm", "101c06b0fa97a858a83beb618f4bc20370624f73ab3954f756d9b52194056de6"}, | ||||
|   "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, | ||||
|   "websock_adapter": {:hex, :websock_adapter, "0.5.5", "9dfeee8269b27e958a65b3e235b7e447769f66b5b5925385f5a569269164a210", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "4b977ba4a01918acbf77045ff88de7f6972c2a009213c515a445c48f224ffce9"}, | ||||
|   "xla": {:hex, :xla, "0.6.0", "67bb7695efa4a23b06211dc212de6a72af1ad5a9e17325e05e0a87e4c241feb8", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "dd074daf942312c6da87c7ed61b62fb1a075bced157f1cc4d47af2d7c9f44fb7"}, | ||||
|   "zarex": {:hex, :zarex, "1.0.5", "58239e3ee5d75f343262bb4df5cf466555a1c689f920e5d3651a9333972f7c7e", [:mix], [], "hexpm", "9fb72ef0567c2b2742f5119a1ba8a24a2fabb21b8d09820aefbf3e592fa9a46a"}, | ||||
| } | ||||
|   | ||||
							
								
								
									
										21
									
								
								priv/repo/migrations/20240408000415_create_segments.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								priv/repo/migrations/20240408000415_create_segments.exs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| defmodule Algora.Repo.Local.Migrations.CreateSegments do | ||||
|   use Ecto.Migration | ||||
|  | ||||
|   def change do | ||||
|     create table(:segments) do | ||||
|       add :body, :text | ||||
|       add :start, :float | ||||
|       add :end, :float | ||||
|       add :embedding, :map | ||||
|       add :starting_subtitle_id, references(:subtitles, on_delete: :nothing), null: false | ||||
|       add :ending_subtitle_id, references(:subtitles, on_delete: :nothing), null: false | ||||
|       add :video_id, references(:videos, on_delete: :nothing), null: false | ||||
|  | ||||
|       timestamps() | ||||
|     end | ||||
|  | ||||
|     create index(:segments, [:video_id]) | ||||
|     create index(:segments, [:starting_subtitle_id]) | ||||
|     create index(:segments, [:ending_subtitle_id]) | ||||
|   end | ||||
| end | ||||
							
								
								
									
										
											BIN
										
									
								
								priv/static/images/og/cossgpt.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								priv/static/images/og/cossgpt.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 547 KiB | 
							
								
								
									
										295
									
								
								scripts/cossgpt.livemd
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										295
									
								
								scripts/cossgpt.livemd
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,295 @@ | ||||
| # COSSgpt | ||||
|  | ||||
| ```elixir | ||||
| import Ecto.Query | ||||
| import Ecto.Changeset | ||||
|  | ||||
| alias Algora.{Accounts, Library, Repo, Storage, Cache, ML} | ||||
|  | ||||
| IEx.configure(inspect: [charlists: :as_lists]) | ||||
|  | ||||
| if Code.ensure_loaded?(ExSync) && function_exported?(ExSync, :register_group_leader, 0) do | ||||
|   ExSync.register_group_leader() | ||||
| end | ||||
| ``` | ||||
|  | ||||
| ## Section | ||||
|  | ||||
| ```elixir | ||||
| defmodule COSSgpt do | ||||
|   @dir "/home/zaf/Desktop/podcast audio" | ||||
|  | ||||
|   alias Algora.Library.Video | ||||
|  | ||||
|   def transcribe_video(id, filename) do | ||||
|     video = Library.get_video!(id) | ||||
|  | ||||
|     index = ML.load_index!() | ||||
|  | ||||
|     IO.puts("\n⌛ processing #{video.title}") | ||||
|  | ||||
|     video = | ||||
|       case video.uuid do | ||||
|         nil -> | ||||
|           video |> change() |> Video.put_video_uuid() |> Repo.update!() | ||||
|  | ||||
|         _ -> | ||||
|           video | ||||
|       end | ||||
|  | ||||
|     slug = Video.slug(video) | ||||
|  | ||||
|     IO.puts("- uploading mp3 to tigris") | ||||
|  | ||||
|     Cache.fetch("#{slug}/upload", fn -> | ||||
|       Storage.upload_from_filename("#{@dir}/#{filename}", "#{video.uuid}/index.mp3") | ||||
|       :ok | ||||
|     end) | ||||
|  | ||||
|     IO.puts("- transcribing audio") | ||||
|  | ||||
|     Cache.fetch("#{slug}/transcription", fn -> | ||||
|       transcription = ML.transcribe_video("#{video.url_root}/index.mp3") | ||||
|  | ||||
|       :ok = | ||||
|         transcription["chunks"] | ||||
|         |> Enum.map(fn %{"text" => text, "timestamp" => [tstart, tend]} -> | ||||
|           %Library.Subtitle{ | ||||
|             body: text, | ||||
|             start: :erlang.float(tstart), | ||||
|             end: :erlang.float(tend || video.duration), | ||||
|             video_id: video.id | ||||
|           } | ||||
|         end) | ||||
|         |> Enum.each(&Repo.insert!/1) | ||||
|  | ||||
|       transcription | ||||
|     end) | ||||
|  | ||||
|     IO.puts("- chunking transcript") | ||||
|  | ||||
|     chunks = ML.chunk(video) | ||||
|  | ||||
|     IO.puts("- creating embeddings") | ||||
|  | ||||
|     embeddings = | ||||
|       Cache.fetch("#{slug}/embeddings", fn -> | ||||
|         ML.create_embeddings(chunks) | ||||
|       end) | ||||
|  | ||||
|     IO.puts("- creating segments") | ||||
|  | ||||
|     segments = | ||||
|       Cache.fetch("#{slug}/segments", fn -> | ||||
|         segments = | ||||
|           Enum.zip(chunks, embeddings) | ||||
|           |> Enum.map(fn {chunk, embedding} -> | ||||
|             %Library.Segment{chunk | embedding: embedding["embedding"]} | ||||
|           end) | ||||
|           |> Enum.map(&Repo.insert!/1) | ||||
|  | ||||
|         ML.add_embeddings(index, segments) | ||||
|  | ||||
|         segments | ||||
|       end) | ||||
|  | ||||
|     segments | ||||
|   end | ||||
| end | ||||
| ``` | ||||
|  | ||||
| ```elixir | ||||
| videos = [ | ||||
|   {6333, "Supertokens Interview Highlights.mp3"}, | ||||
|   {6339, "Hanko full interview.mp3"}, | ||||
|   {6407, "OpenBB Full Interview.mp3"}, | ||||
|   {6422, "Signoz Interview Highlights.mp3"}, | ||||
|   {6390, "Remotion Interview Highlights.mp3"}, | ||||
|   {6305, "Nango Interview Highlights.mp3"}, | ||||
|   {6347, "Linen Interview Highlights.mp3"}, | ||||
|   {6429, "Medplum Full Interview.mp3"}, | ||||
|   {6393, "Windmill Interview Highlights.mp3"}, | ||||
|   {6419, "Elementary Data Highlights.mp3"}, | ||||
|   {6400, "Sematic_Highlights_final.mp3"}, | ||||
|   {6300, "Infisical Interview Highlights.mp3"}, | ||||
|   {6421, "Novu Interview Highlights.mp3"}, | ||||
|   {6330, "Hydra Full Interview.mp3"}, | ||||
|   {6309, "shuttle.rs Interview Highlights.mp3"}, | ||||
|   {6402, "Ivy Interview Highlights.mp3"}, | ||||
|   {6411, "Nextcloud Interview Highlights.mp3"}, | ||||
|   {6425, "Okteto Interview Highlights .mp3"}, | ||||
|   {6405, "Tigris Data Interview Highlights.mp3"}, | ||||
|   {6415, "Refine Interview Highlights.mp3"}, | ||||
|   {6413, "Cal.com Interview Highlights.mp3"}, | ||||
|   {6302, "Hoppscotch Interview Highlights.mp3"}, | ||||
|   {6334, "MAIN-Documenso.mp3"}, | ||||
|   {6331, "GiteaFull.mp3"}, | ||||
|   {6336, "maybefull.mp3"}, | ||||
|   {6391, "NuxtFull.mp3"}, | ||||
|   {6327, "OramaFull.mp3"}, | ||||
|   {6430, "FinalTimeplusFullInterview.mp3"}, | ||||
|   {6295, "tursofull.mp3"}, | ||||
|   {6324, "unkeyfull.mp3"}, | ||||
|   {8540, "Tauri-Full-Final.mp3"}, | ||||
|   {8541, "TailcallFull-correct.mp3"}, | ||||
|   {8539, "millionfull.mp3"}, | ||||
|   {6426, "Scalarfullinterview.mp3"}, | ||||
|   {6387, "IHP Interview Highlights.mp3"}, | ||||
|   {8196, "peer-1.mp3"}, | ||||
|   {8236, "peer-2.mp3"}, | ||||
|   {8412, "peer-3.mp3"}, | ||||
|   {8428, "peer-4.mp3"}, | ||||
|   {8231, "andreas-1.mp3"}, | ||||
|   {8411, "andreas-2.mp3"}, | ||||
|   {8426, "andreas-3.mp3"} | ||||
| ] | ||||
|  | ||||
| for {id, filename} <- videos do | ||||
|   COSSgpt.transcribe_video(id, filename) | ||||
| end | ||||
| ``` | ||||
|  | ||||
| ```elixir | ||||
| obsolete_segments = [ | ||||
|   937, | ||||
|   938, | ||||
|   939, | ||||
|   940, | ||||
|   941, | ||||
|   942, | ||||
|   932, | ||||
|   933, | ||||
|   934, | ||||
|   935, | ||||
|   936, | ||||
|   1572, | ||||
|   1574, | ||||
|   1575, | ||||
|   1573, | ||||
|   1275, | ||||
|   1276, | ||||
|   1277, | ||||
|   1278, | ||||
|   1279, | ||||
|   1280, | ||||
|   1281, | ||||
|   1282, | ||||
|   1283, | ||||
|   1284, | ||||
|   1289, | ||||
|   1294, | ||||
|   1299, | ||||
|   1285, | ||||
|   1290, | ||||
|   1295, | ||||
|   1300, | ||||
|   1286, | ||||
|   1291, | ||||
|   1296, | ||||
|   1301, | ||||
|   1306, | ||||
|   1311, | ||||
|   1316, | ||||
|   1321, | ||||
|   1326, | ||||
|   1331, | ||||
|   1336, | ||||
|   1341, | ||||
|   1346, | ||||
|   1287, | ||||
|   1292, | ||||
|   1297, | ||||
|   1288, | ||||
|   1293, | ||||
|   1298, | ||||
|   1302, | ||||
|   1303, | ||||
|   1304, | ||||
|   1305, | ||||
|   1307, | ||||
|   1308, | ||||
|   1309, | ||||
|   1310, | ||||
|   1312, | ||||
|   1313, | ||||
|   1314, | ||||
|   1320, | ||||
|   1319, | ||||
|   1324, | ||||
|   1329, | ||||
|   1334, | ||||
|   1339, | ||||
|   1344, | ||||
|   1349, | ||||
|   1354, | ||||
|   1359, | ||||
|   1364, | ||||
|   1369, | ||||
|   1315, | ||||
|   1325, | ||||
|   1330, | ||||
|   1335, | ||||
|   1340, | ||||
|   1345, | ||||
|   1350, | ||||
|   1355, | ||||
|   1360, | ||||
|   1365, | ||||
|   1370, | ||||
|   1382, | ||||
|   1317, | ||||
|   1322, | ||||
|   1327, | ||||
|   1332, | ||||
|   1337, | ||||
|   1342, | ||||
|   1347, | ||||
|   1352, | ||||
|   1357, | ||||
|   1362, | ||||
|   1367, | ||||
|   1372, | ||||
|   1377, | ||||
|   1318, | ||||
|   1323, | ||||
|   1328, | ||||
|   1333, | ||||
|   1338, | ||||
|   1343, | ||||
|   1348, | ||||
|   1353, | ||||
|   1358, | ||||
|   1363, | ||||
|   1368, | ||||
|   1373, | ||||
|   1351, | ||||
|   1356, | ||||
|   1361, | ||||
|   1366, | ||||
|   1371, | ||||
|   1376, | ||||
|   1381, | ||||
|   1386, | ||||
|   1391, | ||||
|   1374, | ||||
|   1379, | ||||
|   1384, | ||||
|   1389, | ||||
|   1375, | ||||
|   1380, | ||||
|   1385, | ||||
|   1390, | ||||
|   1378, | ||||
|   1383, | ||||
|   1388, | ||||
|   1387 | ||||
| ] | ||||
|  | ||||
| index = ML.load_index!() | ||||
|  | ||||
| for id <- obsolete_segments do | ||||
|   :ok = HNSWLib.Index.mark_deleted(index, id) | ||||
| end | ||||
|  | ||||
| ML.save_index(index) | ||||
| ``` | ||||
		Reference in New Issue
	
	Block a user