feat(cmd): buildTranscriber -- the base-engine setting turns on the two-engine ensemble
Default is the single-engine chain exactly as before. With TAPIR_RECONCILE_BASE_MODEL set, the base engine and the primary run on the same WAV and a LOCAL model (config has already refused any non-local alias) reconciles them, at temperature 0 with a short reply -- the conditions its accuracy was measured under. The cloud fallback is unchanged and runs only when both local engines fail. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
+40
-10
@@ -7,10 +7,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.d-ma.be/mathias/tapir/internal/adapters/audio"
|
"git.d-ma.be/mathias/tapir/internal/adapters/audio"
|
||||||
|
"git.d-ma.be/mathias/tapir/internal/adapters/llm"
|
||||||
"git.d-ma.be/mathias/tapir/internal/adapters/transcribe"
|
"git.d-ma.be/mathias/tapir/internal/adapters/transcribe"
|
||||||
"git.d-ma.be/mathias/tapir/internal/config"
|
"git.d-ma.be/mathias/tapir/internal/config"
|
||||||
|
"git.d-ma.be/mathias/tapir/internal/reconcile"
|
||||||
"git.d-ma.be/mathias/tapir/internal/uploads"
|
"git.d-ma.be/mathias/tapir/internal/uploads"
|
||||||
"git.d-ma.be/mathias/tapir/internal/web"
|
"git.d-ma.be/mathias/tapir/internal/web"
|
||||||
)
|
)
|
||||||
@@ -52,16 +55,7 @@ func buildUploads(ctx context.Context, cfg config.Config, st uploadsStore, log *
|
|||||||
log.Warn("uploads: marked interrupted by a restart (audio is not retained; they must be re-uploaded)", "count", n)
|
log.Warn("uploads: marked interrupted by a restart (audio is not retained; they must be re-uploaded)", "count", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No overall client timeout: a 90 minute recording takes minutes. The deadline
|
worker := uploads.NewWorker(st, &audio.FFmpeg{Bin: cfg.FFmpegBin}, buildTranscriber(cfg),
|
||||||
// is the worker's per-job context.
|
|
||||||
client := &http.Client{}
|
|
||||||
primary := transcribe.NewGateway(cfg.GatewayURL, cfg.GatewayKey, cfg.TranscribeModel, client)
|
|
||||||
var fallback transcribe.Backend
|
|
||||||
if cfg.TranscribeFallbackModel != "" && cfg.TranscribeFallbackModel != cfg.TranscribeModel {
|
|
||||||
fallback = transcribe.NewGateway(cfg.GatewayURL, cfg.GatewayKey, cfg.TranscribeFallbackModel, client)
|
|
||||||
}
|
|
||||||
|
|
||||||
worker := uploads.NewWorker(st, &audio.FFmpeg{Bin: cfg.FFmpegBin}, transcribe.NewChain(primary, fallback),
|
|
||||||
cfg.UploadTmpDir, cfg.TranscribeTimeout)
|
cfg.UploadTmpDir, cfg.TranscribeTimeout)
|
||||||
intake := uploads.NewIntake(st, &audio.FFprobe{Bin: cfg.FFprobeBin}, worker,
|
intake := uploads.NewIntake(st, &audio.FFprobe{Bin: cfg.FFprobeBin}, worker,
|
||||||
uploadLimits(cfg), cfg.UploadTmpDir)
|
uploadLimits(cfg), cfg.UploadTmpDir)
|
||||||
@@ -98,3 +92,39 @@ func wireUploads(app *web.App, cfg config.Config, in *uploads.Intake) {
|
|||||||
app.UploadChatMaxChars = cfg.MaxUploadedTranscriptChars
|
app.UploadChatMaxChars = cfg.MaxUploadedTranscriptChars
|
||||||
app.UploadChatModel = cfg.UploadChatModel
|
app.UploadChatModel = cfg.UploadChatModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adjudicatorTimeout bounds one adjudication call. A call takes ~2 s on gemma4-26b and ~10 s
|
||||||
|
// on 31b; this is the wait before an unresponsive model is counted as an error.
|
||||||
|
const adjudicatorTimeout = 2 * time.Minute
|
||||||
|
|
||||||
|
// buildTranscriber builds what the worker transcribes with. By default that is the
|
||||||
|
// single-engine chain (the primary, then the optional cloud fallback). When
|
||||||
|
// TAPIR_RECONCILE_BASE_MODEL is set it is the two-engine ensemble (ADR-034): the base
|
||||||
|
// engine and the primary run on the same WAV and a LOCAL model reconciles them. Config
|
||||||
|
// has already refused any non-local model, so nothing here can send private audio or
|
||||||
|
// text to a cloud model for reconciliation; the cloud fallback is unchanged.
|
||||||
|
func buildTranscriber(cfg config.Config) uploads.Transcriber {
|
||||||
|
// No overall client timeout: a 90 minute recording takes minutes. The deadline is
|
||||||
|
// the worker's per-job context.
|
||||||
|
client := &http.Client{}
|
||||||
|
primary := transcribe.NewGateway(cfg.GatewayURL, cfg.GatewayKey, cfg.TranscribeModel, client)
|
||||||
|
var fallback transcribe.Backend
|
||||||
|
if cfg.TranscribeFallbackModel != "" && cfg.TranscribeFallbackModel != cfg.TranscribeModel {
|
||||||
|
fallback = transcribe.NewGateway(cfg.GatewayURL, cfg.GatewayKey, cfg.TranscribeFallbackModel, client)
|
||||||
|
}
|
||||||
|
if cfg.ReconcileBaseModel == "" {
|
||||||
|
return transcribe.NewChain(primary, fallback)
|
||||||
|
}
|
||||||
|
|
||||||
|
base := transcribe.NewGateway(cfg.GatewayURL, cfg.GatewayKey, cfg.ReconcileBaseModel, client)
|
||||||
|
// Temperature 0 and a short reply: the adjudicator's accuracy was measured that way.
|
||||||
|
chat := llm.New(cfg.GatewayURL, cfg.GatewayKey, cfg.ReconcileModel, adjudicatorTimeout,
|
||||||
|
llm.WithTemperature(0), llm.WithMaxTokens(200))
|
||||||
|
var adj reconcile.Adjudicator = reconcile.NewLLMAdjudicator(chat)
|
||||||
|
if cfg.ReconcileCheckOrder {
|
||||||
|
adj = reconcile.NewOrderChecked(adj)
|
||||||
|
}
|
||||||
|
return transcribe.NewEnsemble(base, primary, fallback, adj, reconcile.Options{
|
||||||
|
BaseEngine: cfg.ReconcileBaseModel, AltEngine: cfg.TranscribeModel, Reconciler: cfg.ReconcileModel,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user