Procesos auxiliares
Whisper.net y LLamaSharp reservan memoria nativa y de GPU que no siempre se devuelve al sistema al liberar el modelo. Para que la aplicación principal no crezca con cada uso, toda la inferencia se ejecuta en procesos hijos: el mismo ejecutable lanzado en modo worker.
Línea de comandos
LoopbackTranscriber.exe --worker <tipo> <ruta-trabajo> <ruta-resultado> <pid-padre>
| Argumento | Significado |
|---|---|
--worker |
Marca el proceso como auxiliar. |
<tipo> |
whisper, summary, whisper-live o translate-live. |
<ruta-trabajo> |
El fichero JSON con el trabajo. |
<ruta-resultado> |
Donde el proceso escribe el resultado. |
<pid-padre> |
El PID de la aplicación, para el vigilante del padre. |
WorkerProcess.CreateStartInfo la construye a partir de Environment.ProcessPath. Si la aplicación se ejecuta con dotnet LoopbackTranscriber.dll, por ejemplo desde el depurador, añade la ruta del ensamblado como primer argumento. El proceso se crea sin ventana y con la salida estándar y la de error redirigidas.
Program.Main reconoce la invocación con WorkerHost.IsWorkerInvocation: el segundo argumento debe ser --worker y debe haber al menos 5. En ese caso ejecuta WorkerHost.Run y termina sin crear App.
| Tipo | Modo | Trabajo | Resultado |
|---|---|---|---|
whisper |
por lotes | WhisperJob |
WhisperJobResult |
summary |
por lotes | SummaryJob |
SummaryJobResult |
whisper-live |
persistente | LiveWhisperConfig |
por stdout |
translate-live |
persistente | LiveTranslatorConfig |
por stdout |
Los ficheros de trabajo se escriben en %TEMP%\LoopbackTranscriber\worker\ con un GUID por nombre: <guid>.job.json y <guid>.result.json, o <guid>.live.json en los persistentes. Se borran al terminar.
Vigilante del padre
Si se pasa <pid-padre>, el worker arranca un hilo en segundo plano que espera a que termine ese proceso. Cuando termina, o si ya no existe, sale con código 2. Así ningún worker sobrevive a la aplicación, aunque esta muera de golpe.
Modo por lotes (whisper, summary)
sequenceDiagram
participant App as Aplicación<br/>WorkerProcess.RunAsync
participant F as Carpeta worker<br/>%TEMP%\LoopbackTranscriber\worker
participant W as Proceso auxiliar<br/>whisper o summary
App->>F: Escribe <guid>.job.json
App->>W: Inicia el proceso
W->>F: Lee el trabajo
loop Mientras trabaja
W-->>App: stdout "@@P 42.5"
end
alt Termina bien
W->>F: Escribe <guid>.result.json
W-->>App: Sale con código 0
else Falla
W->>F: Escribe <guid>.result.json<br/>con Error, ErrorKey y ErrorArgs
W-->>App: Sale con código 1
else Se cierra sin resultado
W-->>App: Sale con cualquier código
end
Note over App: Espera hasta 5 s<br/>a que se vacíe la salida
alt Hay resultado
F->>App: Lee <guid>.result.json
else No hay resultado
Note over App: ErrorBackgroundProcessEnded<br/>y últimas 40 líneas de stderr en app.log
end
App->>F: Borra los dos ficheros
- Progreso: líneas
@@P <número>en la salida estándar, con cultura invariante y hasta 3 decimales (WorkerProtocol.FormatProgress/TryParseProgress). Las demás líneas se ignoran. - Resultado: el worker escribe el JSON y sale con 0.
- Error: si falla, escribe en el fichero de resultado un objeto con
Error(el mensaje) y, si la excepción tenía una clave de localización,ErrorKeyyErrorArgs, y sale con 1. La aplicación lo convierte en una excepción con la misma clave (UserError.FromWorker), así que el usuario ve el mensaje traducido a su idioma. - Sin resultado: si el proceso termina sin escribir nada (porque se cuelga el controlador o falta el runtime de C++), se lanza
ErrorBackgroundProcessEndedcon el código de salida. Las últimas 40 líneas de stderr se escriben enapp.log(WorkerStderrTail). - Cancelación: el
CancellationTokenmata el árbol de procesos. - Cuando el proceso termina, se espera como mucho 5 s a que se vacíe su salida.
Contratos JSON
| Tipo | Campos |
|---|---|
WhisperJob |
WavPath, ModelPath, Language, FallbackLanguage, ThreadCount, StrictNoSpeech, UseGpu |
WhisperJobResult |
Language, Segments (cada uno con StartTicks, EndTicks y Text), Error, ErrorKey, ErrorArgs |
SummaryJob |
TranscriptText, ModelPath, UiLanguage |
SummaryJobResult |
Title, Summary, Error, ErrorKey, ErrorArgs |
Modo persistente (whisper-live, translate-live)
LiveWorkerClient arranca el proceso con la entrada estándar redirigida en UTF-8 sin BOM y habla con él línea a línea:
| Dirección | Formato |
|---|---|
| Aplicación → worker (stdin) | Una línea JSON por petición: LiveWhisperRequest { Id, Audio } (PCM base64) o LiveTranslateRequest { Id, Text, Context, Target }. |
| Worker → aplicación (stdout) | @@L seguido de un LiveWorkerMessage en JSON. |
LiveWorkerMessage:
| Campo | Significado |
|---|---|
Ready |
true en el primer mensaje, cuando el modelo está cargado. |
Id |
Petición a la que responde. 0 con Error indica un fallo al arrancar. |
Error |
Error de esa petición. El worker sigue funcionando. |
Ms |
Milisegundos de inferencia. |
Segments |
Segmentos transcritos (whisper-live). |
Text |
Texto traducido (translate-live). |
- El cliente espera
Readycomo mucho 180 s. - Cada petición tiene su
TaskCompletionSourceen un diccionario porId. Las escrituras en stdin se serializan con un semáforo. - Tiempo máximo por petición: 60 s para transcribir y 30 s para traducir.
- Si el proceso termina, fallan todas las peticiones pendientes con
ErrorBackgroundProcessEnded.LiveWorkerSupervisordecide si reiniciarlo. - Al liberar el cliente, cierra stdin, mata el árbol de procesos y borra el fichero de configuración.
Errores no controlados en el worker
InstallUnhandledExceptionLogging registra en app.log las excepciones no controladas y las tareas no observadas del worker, con su tipo entre corchetes (WorkerHost[whisper]: …). Cada línea del registro lleva el PID, lo que permite distinguir qué proceso la escribió.