whisper live
This commit is contained in:
134
whisper_live/lib/whisper_live_web/channels/audio_channel.ex
Normal file
134
whisper_live/lib/whisper_live_web/channels/audio_channel.ex
Normal file
@ -0,0 +1,134 @@
|
||||
defmodule WhisperLiveWeb.AudioChannel do
|
||||
use Phoenix.Channel
|
||||
require Logger
|
||||
alias WhisperLive.AudioBuffer
|
||||
|
||||
def join("audio:lobby", _payload, socket) do
|
||||
ref = socket_id(socket)
|
||||
Logger.info("Cliente conectado al canal audio:lobby")
|
||||
{:ok, _} = AudioBuffer.start_link(ref)
|
||||
{:ok, _} = WhisperLive.Transcriber.start_link(ref)
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def handle_in("audio_chunk", %{"data" => base64_audio, "sample_rate" => sample_rate}, socket) do
|
||||
# 1. Decodificas el audio base64
|
||||
{:ok, bin} = Base.decode64(base64_audio)
|
||||
|
||||
# 2. Guardas o procesas el chunk de audio
|
||||
# Podrías escribirlo en un archivo temporal para enviar a Whisper
|
||||
tmpfile = tmp_path("chunk_#{socket.assigns.ref}")
|
||||
:ok = File.write!(tmpfile, encode_wav(bin, sample_rate))
|
||||
|
||||
# 3. Llamas a la transcripción del chunk (podría ser sync o async)
|
||||
case send_to_whisper(tmpfile) do
|
||||
{:ok, transcription} ->
|
||||
# 4. Envías el texto parcial por PubSub o Push a LiveView/cliente
|
||||
Phoenix.PubSub.broadcast(YourApp.PubSub, "transcription:#{socket.assigns.ref}", {:transcription, transcription})
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Error en transcripción parcial: #{inspect(reason)}")
|
||||
end
|
||||
|
||||
File.rm(tmpfile)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
|
||||
def handle_in("stop_audio", _payload, socket) do
|
||||
Logger.info("🛑 Grabación detenida por cliente")
|
||||
|
||||
ref = socket_id(socket)
|
||||
|
||||
case AudioBuffer.get_all(ref) do
|
||||
[{rate, _} | _] = chunks ->
|
||||
merged = chunks |> Enum.map(fn {_, bin} -> bin end) |> IO.iodata_to_binary()
|
||||
filename = "recordings/recording_#{System.system_time(:millisecond)}.wav"
|
||||
File.mkdir_p!("recordings")
|
||||
File.write!(filename, encode_wav(merged, rate))
|
||||
Logger.info("💾 Audio guardado en #{filename}")
|
||||
|
||||
# 🔁 Transcribir automáticamente
|
||||
case send_to_whisper(filename) do
|
||||
{:ok, response} ->
|
||||
Logger.info("📝 Transcripción recibida: #{response}")
|
||||
{:error, reason} ->
|
||||
Logger.error("❌ Error al transcribir: #{inspect(reason)}")
|
||||
end
|
||||
|
||||
_ ->
|
||||
Logger.warning("⚠️ No se recibieron chunks de audio")
|
||||
end
|
||||
|
||||
AudioBuffer.stop(ref)
|
||||
WhisperLive.Transcriber.stop(ref)
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp socket_id(socket), do: socket.transport_pid |> :erlang.pid_to_list() |> List.to_string()
|
||||
|
||||
defp encode_wav(data, sample_rate) do
|
||||
num_channels = 1
|
||||
bits_per_sample = 16
|
||||
byte_rate = sample_rate * num_channels * div(bits_per_sample, 8)
|
||||
block_align = div(bits_per_sample * num_channels, 8)
|
||||
data_size = byte_size(data)
|
||||
riff_size = 36 + data_size
|
||||
|
||||
<<
|
||||
"RIFF",
|
||||
<<riff_size::little-size(32)>>,
|
||||
"WAVE",
|
||||
"fmt ",
|
||||
<<16::little-size(32)>>,
|
||||
<<1::little-size(16)>>,
|
||||
<<num_channels::little-size(16)>>,
|
||||
<<sample_rate::little-size(32)>>,
|
||||
<<byte_rate::little-size(32)>>,
|
||||
<<block_align::little-size(16)>>,
|
||||
<<bits_per_sample::little-size(16)>>,
|
||||
"data",
|
||||
<<data_size::little-size(32)>>
|
||||
>> <> data
|
||||
end
|
||||
|
||||
defp send_to_whisper(filepath) do
|
||||
url = "http://localhost:4000/infer"
|
||||
|
||||
{:ok, file_bin} = File.read(filepath)
|
||||
filename = Path.basename(filepath)
|
||||
|
||||
headers = [
|
||||
{'Content-Type', 'multipart/form-data; boundary=----ElixirBoundary'}
|
||||
]
|
||||
|
||||
body =
|
||||
[
|
||||
"------ElixirBoundary\r\n",
|
||||
"Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n",
|
||||
"Content-Type: audio/wav\r\n\r\n",
|
||||
file_bin,
|
||||
"\r\n------ElixirBoundary--\r\n"
|
||||
]
|
||||
|
||||
:httpc.request(:post, {url, headers, 'multipart/form-data; boundary=----ElixirBoundary', body}, [], [])
|
||||
|> case do
|
||||
{:ok, {{_, 200, _}, _headers, body}} ->
|
||||
{:ok, to_string(body)}
|
||||
|
||||
{:ok, {{_, status, _}, _, body}} ->
|
||||
{:error, {:http_error, status, to_string(body)}}
|
||||
|
||||
error ->
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp tmp_path(prefix) do
|
||||
unique = :erlang.unique_integer([:positive]) |> Integer.to_string()
|
||||
filename = prefix <> "_" <> unique <> ".wav"
|
||||
Path.join(System.tmp_dir!(), filename)
|
||||
end
|
||||
|
||||
end
|
14
whisper_live/lib/whisper_live_web/channels/user_socket.ex
Normal file
14
whisper_live/lib/whisper_live_web/channels/user_socket.ex
Normal file
@ -0,0 +1,14 @@
|
||||
defmodule WhisperLiveWeb.UserSocket do
|
||||
use Phoenix.Socket
|
||||
|
||||
## Canales que acepta este socket:
|
||||
channel "audio:*", WhisperLiveWeb.AudioChannel
|
||||
|
||||
transport :websocket, Phoenix.Transports.WebSocket
|
||||
|
||||
def connect(_params, socket, _connect_info) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def id(_socket), do: nil
|
||||
end
|
Reference in New Issue
Block a user