Packages
fnord
0.9.30
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/ai/embeddings/script.ex
defmodule AI.Embeddings.Script do
@moduledoc """
Manages the standalone embed.exs script that runs the local embedding model.
The script is stored as a module attribute and written to ~/.fnord/embed.exs
on first use. It uses Bumblebee with all-MiniLM-L12-v2 (384-dim vectors,
mean pooling) and supports both single-input and pooled JSONL streaming modes.
"""
@script_filename "embed.exs"
# The clang workaround wrapper invokes elixir with the CXX override needed
# for Apple clang 17+ to compile EXLA's NIF without hard errors.
@wrapper_filename "embed.sh"
@embed_script ~S"""
#!/usr/bin/env elixir
#-------------------------------------------------------------------------------
# Embedding generator
#
# Generates embedding vectors using Bumblebee with the all-MiniLM-L12-v2
# sentence transformer (384-dimensional vectors, mean pooling).
#
# Two modes, dispatched by System.argv():
#
# Single-input mode:
# elixir embed.exs <file> # embed file contents, output JSON array
# echo "text" | elixir embed.exs - # embed stdin text, output JSON array
#
# Pool mode (JSONL streaming):
# ... | elixir embed.exs -n 4 # read JSONL from stdin, output JSONL
#
# Pool mode loads the model once, then processes a stream of inputs
# via Task.Supervisor.async_stream_nolink with bounded concurrency.
# Each input line is a JSON object with "id" and "text" fields. Each
# output line is a JSON object with "id" and "embedding" fields.
# Results arrive in completion order.
#
# Self-termination: when stdin closes (parent exit / broken pipe), the
# stream ends naturally and the BEAM exits. This prevents orphaned
# elixir processes when the caller is killed.
#
#
# Dependencies: elixir (with Mix.install support)
#
# Compilation notes:
#
# EXLA pins: EXLA 0.10.0 has a duplicate symbol linker error in the
# `fine` library's init functions across multiple translation units.
# Pinned to 0.9.2.
#
# Clang workaround: Apple clang 17+ promotes a template warning to a
# hard error that breaks EXLA's NIF compilation. The caller (embed.sh)
# sets CXX with -Wno-error=missing-template-arg-list-after-template-kw
# before invoking this script.
#-------------------------------------------------------------------------------
# Route Elixir/EXLA log noise to stderr so stdout stays clean for JSON output.
# Level is :critical rather than :warning because at shutdown the stdout pipe
# may close before our async_stream workers finish writing. OTP's default
# writer logs "Writer crashed (:epipe)" at :error level, which users would
# see as spurious indexing noise. The actual write failures are handled
# gracefully in the main loop below.
{:ok, cfg} = :logger.get_handler_config(:default)
cfg = Map.update!(cfg, :config, &Map.put(&1, :type, :standard_error))
:ok = :logger.remove_handler(:default)
:ok = :logger.add_handler(:default, :logger_std_h, cfg)
:ok = :logger.set_primary_config(:level, :critical)
# HuggingFace model weights live under ~/.fnord/models/. Bumblebee picks
# this up via BUMBLEBEE_CACHE_DIR.
cache_dir = Path.join([System.user_home!(), ".fnord", "models"])
File.mkdir_p!(cache_dir)
System.put_env("BUMBLEBEE_CACHE_DIR", cache_dir)
Mix.install([
{:bumblebee, "~> 0.6"},
{:exla, "0.9.2"},
{:jason, "~> 1.4"},
])
Nx.global_default_backend(EXLA.Backend)
defmodule Embed do
@model "sentence-transformers/all-MiniLM-L12-v2"
def load_serving do
model_name = @model
{:ok, model} = Bumblebee.load_model({:hf, model_name})
{:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, model_name})
Bumblebee.Text.TextEmbedding.text_embedding(
model,
tokenizer,
compile: [batch_size: 1, sequence_length: 256],
defn_options: [compiler: EXLA],
output_pool: :mean_pooling,
output_attribute: :hidden_state
)
end
def embed(serving, text) do
%{embedding: tensor} = Nx.Serving.run(serving, text)
Nx.to_flat_list(tensor)
end
end
# Parse arguments to determine mode
{opts, args} =
case System.argv() do
["-n", n | rest] ->
{[pool: String.to_integer(n)], rest}
["-n"] ->
{[pool: 4], []}
other ->
{[], other}
end
# Load model (shared across both modes)
serving = Embed.load_serving()
case {opts[:pool], args} do
# Pool mode: supervised JSONL streaming with stdin-close detection
{concurrency, []} when is_integer(concurrency) ->
{:ok, sup} = Task.Supervisor.start_link()
# When the parent dies or Ctrl-C fires, stdin closes. IO.stream
# ends, Stream.run returns, and the BEAM exits. In-flight tasks
# under the supervisor get :shutdown signals automatically.
#
# Explicit SIGTERM/SIGINT handling: the BEAM's default signal
# handlers shut down the VM cleanly, which stops the supervisor
# and its children. No custom signal handling needed.
# Decoupled producer-worker design.
#
# Why not Task.Supervisor.async_stream_nolink(sup, input_stream, fun)?
# That pattern requires the input stream to signal :eof to flush results
# back to the consumer. When embed.exs is spawned under an Erlang port,
# stdin stays open indefinitely (the parent keeps the write end alive),
# so the stream blocks on IO.binread waiting for the next line and
# completed task results never get yielded. The pipeline stalls after
# the first line.
#
# Solution: run a reader process that pushes lines as messages, and a
# main receive loop that spawns workers and writes results as they come.
# In-flight worker count is capped at `concurrency` to match the old
# max_concurrency behavior.
#
# Why not IO.stream(:stdio, :line)? Under Erlang port spawn, :stdio
# routes through a group leader that hangs on get_line from a piped
# stdin. IO.binread bypasses the group leader.
parent = self()
spawn_link(fn ->
read_loop = fn loop ->
case IO.binread(:stdio, :line) do
:eof ->
send(parent, :reader_eof)
{:error, _reason} ->
send(parent, :reader_eof)
line when is_binary(line) ->
trimmed = String.trim(line)
if trimmed != "", do: send(parent, {:reader_line, trimmed})
loop.(loop)
end
end
read_loop.(read_loop)
end)
# Safe writer: if stdout is closed by the parent (typical at shutdown),
# treat the write failure as a clean exit signal. The default OTP
# writer would log "Writer crashed (:epipe)" to stderr, which users see
# as spurious indexing noise.
write_line = fn line ->
try do
IO.puts(line)
:ok
rescue
_ -> :closed
catch
:exit, _ -> :closed
end
end
process_line = fn line ->
try do
case Jason.decode(line) do
{:ok, %{"id" => id, "text" => text} = input} when is_binary(text) and text != "" ->
try do
embedding = Embed.embed(serving, text)
input
|> Map.delete("text")
|> Map.put("embedding", embedding)
|> Jason.encode!()
rescue
e ->
IO.puts(
:standard_error,
"embed: error computing embedding for id=#{inspect(id)}: " <>
Exception.format(:error, e, __STACKTRACE__)
)
Jason.encode!(%{id: id, error: "embed failure: #{Exception.message(e)}"})
catch
kind, reason ->
IO.puts(
:standard_error,
"embed: #{kind} computing embedding for id=#{inspect(id)}: #{inspect(reason)}"
)
Jason.encode!(%{id: id, error: "embed #{kind}: #{inspect(reason)}"})
end
{:ok, %{"id" => id}} ->
Jason.encode!(%{id: id, error: "missing or empty text field"})
{:error, _} ->
Jason.encode!(%{error: "invalid JSON: #{String.slice(line, 0, 80)}"})
end
rescue
e ->
IO.puts(
:standard_error,
"embed: process_line failed: " <>
Exception.format(:error, e, __STACKTRACE__)
)
Jason.encode!(%{error: "process_line failure: #{Exception.message(e)}"})
end
end
# Receive loop: pull lines from the reader, cap in-flight worker count
# at `concurrency`, write results to stdout as tasks complete. Exits
# when the reader signals EOF and all pending workers have finished.
#
# If write_line returns :closed, stdout is gone (parent exited); we
# halt immediately. The reader co-dies via spawn_link.
# Each Task.Supervisor.async_nolink spawns a task that automatically
# sends its return value back as {ref, result}, plus a {:DOWN, ref, ...}
# on completion. We rely on the automatic message - do NOT also
# send/2 manually, or receive will match the outer {task_ref, {my_ref,
# result}} tuple and pass a tuple where a string is expected.
loop = fn loop, in_flight, reader_done? ->
cond do
reader_done? and in_flight == 0 ->
:ok
in_flight >= concurrency ->
receive do
{ref, json_line} when is_reference(ref) and is_binary(json_line) ->
Process.demonitor(ref, [:flush])
case write_line.(json_line) do
:ok -> loop.(loop, in_flight - 1, reader_done?)
:closed -> :ok
end
{:DOWN, _ref, :process, _pid, _reason} ->
loop.(loop, in_flight - 1, reader_done?)
end
true ->
receive do
{:reader_line, line} ->
Task.Supervisor.async_nolink(sup, fn -> process_line.(line) end)
loop.(loop, in_flight + 1, reader_done?)
:reader_eof ->
loop.(loop, in_flight, true)
{ref, json_line} when is_reference(ref) and is_binary(json_line) ->
Process.demonitor(ref, [:flush])
case write_line.(json_line) do
:ok -> loop.(loop, in_flight - 1, reader_done?)
:closed -> :ok
end
{:DOWN, _ref, :process, _pid, _reason} ->
loop.(loop, in_flight - 1, reader_done?)
end
end
end
loop.(loop, 0, false)
# Single-input mode: embed one text, output bare JSON array
{nil, ["-"]} ->
text = IO.read(:stdio, :eof) |> String.trim()
if text == "" do
IO.puts(:standard_error, "error: empty input")
System.halt(1)
end
Embed.embed(serving, text) |> Jason.encode!() |> IO.puts()
{nil, [path]} ->
case File.read(path) do
{:ok, content} ->
text = String.trim(content)
if text == "" do
IO.puts(:standard_error, "error: empty input")
System.halt(1)
end
Embed.embed(serving, text) |> Jason.encode!() |> IO.puts()
{:error, reason} ->
IO.puts(:standard_error, "error: #{path}: #{:file.format_error(reason)}")
System.halt(1)
end
{nil, []} ->
IO.puts(:standard_error, "Usage: embed <file> | embed - | ... | embed -n <workers>")
System.halt(1)
_ ->
IO.puts(:standard_error, "Usage: embed <file> | embed - | ... | embed -n <workers>")
System.halt(1)
end
"""
@embed_wrapper ~S"""
#!/usr/bin/env bash
#
# Wrapper for embed.exs. Handles three concerns the raw elixir launcher
# can't address on its own:
#
# 1. Clean environment via `env -i`. The `elixir` launcher is a #!/bin/sh
# script; on macOS /bin/sh is bash 3.2 in POSIX mode, which chokes on
# any exported bash 5 functions (BASH_FUNC_*) inherited from the user's
# shell. Stripping to a minimal env avoids silent hangs and cryptic
# startup errors.
#
# 2. CXX workaround for Apple clang 17+. EXLA's NIF build treats
# -Wmissing-template-arg-list-after-template-kw as a hard error on
# first compile. Suppressing -Werror for that specific flag fixes it.
#
# 3. Stderr filter for C++ init noise emitted by XLA/TFRT before absl's
# logging is initialized. These lines can't be silenced via Elixir's
# :logger config. Real errors still pass through.
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CXX_FLAG="${CXX:-c++} -Wno-error=missing-template-arg-list-after-template-kw"
env_args=(
PATH="$PATH"
HOME="$HOME"
CXX="$CXX_FLAG"
TERM="${TERM:-dumb}"
)
exec env -i "${env_args[@]}" elixir "${SCRIPT_DIR}/embed.exs" "$@" \
2> >(grep -v -E '^WARNING: All log messages before absl::InitializeLog|^I[0-9]{4} [0-9:.]+ +[0-9]+ cpu_client\.cc' >&2)
"""
@doc """
Returns the path to the embed.exs script under ~/.fnord/.
"""
@spec script_path() :: String.t()
def script_path do
Path.join(Settings.fnord_home(), @script_filename)
end
@doc """
Returns the path to the embed.sh wrapper script under ~/.fnord/.
"""
@spec wrapper_path() :: String.t()
def wrapper_path do
Path.join(Settings.fnord_home(), @wrapper_filename)
end
@doc """
Ensures the embed.exs script and its shell wrapper exist on disk.
Writes them if missing or if the on-disk content differs from the
compiled-in version (i.e. after an upgrade).
"""
@spec ensure_scripts!() :: :ok
def ensure_scripts!() do
ensure_file!(script_path(), @embed_script)
ensure_file!(wrapper_path(), @embed_wrapper)
File.chmod!(wrapper_path(), 0o755)
:ok
end
# First write vs. subsequent rewrite is useful context for anyone watching
# a fresh install or debugging an unexpected reinstall after an upgrade.
defp ensure_file!(path, content) do
content = String.trim_leading(content, "\n")
case File.read(path) do
{:ok, ^content} ->
:ok
{:ok, _other} ->
File.write!(path, content)
UI.info("[embeddings] Updated", path)
{:error, :enoent} ->
File.write!(path, content)
UI.info("[embeddings] Installed", path)
{:error, reason} ->
File.write!(path, content)
UI.info("[embeddings] Reinstalled after read error", "#{path} (#{inspect(reason)})")
end
end
end