Advised on transcription model choices for clean and noisy audio on Apple Silicon.
PlanConfirmed profile creation methods and chose direct SQLite insertion.
PlanCreated two transcription profiles in the database.
BuildReviewed repo structure for utility implementation.
PlanDiscussed options for seeding profiles via CLI or standalone binary.
Planhelp me choose best model for transcription
>ls /Users/tam/code/Scriberrapi-docs
assets
build.sh
cmd
CNAME
cropped-main-logo.png
cropped-thumb.png
data
docker-compose.blackwell.yml
docker-compose.build.blackwell.yml
docker-compose.build.cuda.yml
docker-compose.build.yml
docker-compose.cuda.yml
docker-compose.yml
docker-entrypoint.sh
Dockerfile
Dockerfile.cuda
Dockerfile.cuda.12.9
docs
go.mod
go.sum
internal
lefthook.yml
LICENSE
logo-text.svg
logo.svg
main
Makefile
pkg
README.md
screenshots
scriberr
scriberr_text_logo.svg
scriberr-icon-dark.svg
scriberr-icon-light.svg
scriberr-logo.png
scriberr-thumb.png
server
startup_log.txt
tests
web>grep -ri "whisper" /Users/tam/code/Scriberr/internal --include="*.go" -l | head -20/Users/tam/code/Scriberr/internal/database/database.go
/Users/tam/code/Scriberr/internal/transcription/quick_transcription.go
/Users/tam/code/Scriberr/internal/transcription/unified_service.go
/Users/tam/code/Scriberr/internal/transcription/multitrack_transcriber.go
/Users/tam/code/Scriberr/internal/transcription/webhook_integration_test.go
/Users/tam/code/Scriberr/internal/transcription/adapters_test.go
/Users/tam/code/Scriberr/internal/transcription/adapters/whisperx_adapter.go
/Users/tam/code/Scriberr/internal/transcription/adapters/parakeet_adapter.go
/Users/tam/code/Scriberr/internal/transcription/adapters/openai_adapter.go
/Users/tam/code/Scriberr/internal/config/config.go
/Users/tam/code/Scriberr/internal/models/transcription.go
/Users/tam/code/Scriberr/internal/api/openai_handler.go
/Users/tam/code/Scriberr/internal/api/handlers.go>ls /Users/tam/code/Scriberr/internal/transcription/adapters/base_adapter.go
canary_adapter.go
openai_adapter.go
parakeet_adapter.go
py
pyannote_adapter.go
sortformer_adapter.go
voxtral_adapter.go
whisperx_adapter.go>grep -rE "WhisperModel|model.*tiny|model.*base|model.*small|model.*medium|model.*large" /Users/tam/code/Scriberr/web/src --include="*.svelte" --include="*.ts" -l 2>/dev/null | head -10(Bash completed with no output)>grep -rEi "tiny|base.*en|small|medium|large-v" /Users/tam/code/Scriberr/web/src 2>/dev/null | head -30(Bash completed with no output)>grep -rEi "WhisperX|parakeet|canary|voxtral" /Users/tam/code/Scriberr/web/src 2>/dev/null | head -20(Bash completed with no output)>find /Users/tam/code/Scriberr/web -type d -name "src" | head -5; ls /Users/tam/code/Scriberr/web 2>/dev/null/Users/tam/code/Scriberr/web/project-site/src
/Users/tam/code/Scriberr/web/frontend/src
/Users/tam/code/Scriberr/web/frontend/node_modules/workbox-google-analytics/src
/Users/tam/code/Scriberr/web/frontend/node_modules/estree-walker/src
/Users/tam/code/Scriberr/web/frontend/node_modules/style-to-object/src
frontend
project-site>grep -rEi "WhisperX|parakeet|canary|voxtral|large-v|model.*selector" /Users/tam/code/Scriberr/web/frontend/src 2>/dev/null | grep -v node_modules | head -30/Users/tam/code/Scriberr/web/frontend/src/features/settings/components/ProfilesTable.tsx:import type { WhisperXParams } from "@/components/TranscriptionConfigDialog";
/Users/tam/code/Scriberr/web/frontend/src/features/settings/components/ProfilesTable.tsx: parameters: WhisperXParams;
/Users/tam/code/Scriberr/web/frontend/src/features/settings/components/ProfileSettings.tsx:import { TranscriptionConfigDialog, type WhisperXParams } from "@/components/TranscriptionConfigDialog";
/Users/tam/code/Scriberr/web/frontend/src/features/settings/components/ProfileSettings.tsx: parameters: WhisperXParams;
/Users/tam/code/Scriberr/web/frontend/src/features/settings/components/ProfileSettings.tsx: const handleProfileSaved = useCallback(async (payload: WhisperXParams & { profileName?: string; profileDescription?: string }) => {
/Users/tam/code/Scriberr/web/frontend/src/features/settings/components/ProfileSettings.tsx: parameters: paramRest as WhisperXParams,
/Users/tam/code/Scriberr/web/frontend/src/features/transcription/components/audio-detail/ExecutionInfoDialog.tsx: } else if (params.model_family === 'nvidia_parakeet') {
/Users/tam/code/Scriberr/web/frontend/src/features/transcription/components/audio-detail/ExecutionInfoDialog.tsx: } else if (params.model_family === 'nvidia_canary') {
/Users/tam/code/Scriberr/web/frontend/src/features/transcription/components/audio-detail/ExecutionInfoDialog.tsx: specificKeys = ['model']; // Canary usually simpler
/Users/tam/code/Scriberr/web/frontend/src/features/transcription/components/AudioFilesTable.tsx:import { TranscriptionConfigDialog, type WhisperXParams } from "@/components/TranscriptionConfigDialog";
/Users/tam/code/Scriberr/web/frontend/src/features/transcription/components/AudioFilesTable.tsx: const handleStartTranscription = useCallback(async (params: WhisperXParams) => {
/Users/tam/code/Scriberr/web/frontend/src/features/transcription/components/AudioFilesTable.tsx: const handleStartTranscriptionWithProfile = useCallback(async (params: WhisperXParams) => {
/Users/tam/code/Scriberr/web/frontend/src/features/transcription/components/AudioFilesTable.tsx: const handleBulkTranscribe = useCallback(async (params: WhisperXParams) => {
/Users/tam/code/Scriberr/web/frontend/src/features/transcription/components/AudioFilesTable.tsx: const onStartTranscribe = (params: WhisperXParams) => {
/Users/tam/code/Scriberr/web/frontend/src/features/transcription/components/AudioFilesTable.tsx: const onStartTranscribeWithProfile = (params: WhisperXParams) => {
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx:export interface WhisperXParams {
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: onStartTranscription: (params: WhisperXParams & { profileName?: string; profileDescription?: string }) => void;
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: initialParams?: WhisperXParams;
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx:const DEFAULT_PARAMS: WhisperXParams = {
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: "medium", "medium.en", "large", "large-v1", "large-v2", "large-v3"
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx:const CANARY_LANGUAGES = [
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: const [params, setParams] = useState<WhisperXParams>(DEFAULT_PARAMS);
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: const updateParam = <K extends keyof WhisperXParams>(key: K, value: WhisperXParams[K]) => {
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: { value: "nvidia_parakeet", label: "NVIDIA Parakeet" },
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: { value: "nvidia_canary", label: "NVIDIA Canary" },
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: { value: "mistral_voxtral", label: "Mistral Voxtral" },
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: {params.model_family === "nvidia_parakeet" && (
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: <ParakeetConfig params={params} updateParam={updateParam} isMultiTrack={isMultiTrack} />
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: {params.model_family === "nvidia_canary" && (
/Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx: <CanaryConfig params={params} updateParam={updateParam} isMultiTrack={isMultiTrack} />It depends on what you're transcribing. Quick guide for Scriberr's four families:
large-v3 — best default. Strongest multilingual (~99 langs), works well with diarization (the path you just restored), handles noisy/accented audio. Needs ~10GB VRAM; slower. Drop to medium or small if you're CPU-only or English-only and want speed.Two quick questions to narrow it down:
Tell me those and I'll point at one.
>grep -nE "compute_type|device|model.*default|large-v|vad_method|chunk_size|batch_size" /Users/tam/code/Scriberr/web/frontend/src/components/transcription/TranscriptionConfigDialog.tsx | head -5029: device: string;
30: device_index: number;
31: batch_size: number;
32: compute_type: string;
42: vad_method: string;
45: chunk_size: number;
95: device: "cpu",
96: device_index: 0,
97: batch_size: 8,
98: compute_type: "float32",
106: vad_method: "pyannote",
109: chunk_size: 30,
136: "medium", "medium.en", "large", "large-v1", "large-v2", "large-v3"
211: device: "CPU (universal), GPU (faster, CUDA required), or AUTO.",
212: compute_type: "Float16 (faster), Float32 (accurate), Int8 (fastest).",
213: batch_size: "Segments processed at once. Higher = faster but more memory.",
218: vad_method: "Voice detection: Pyannote (accurate) or Silero (fast).",
542: <SelectField label="Device" description={PARAM_DESCRIPTIONS.device} value={params.device} onValueChange={(v) => updateParam('device', v)} options={[{ value: "cpu", label: "CPU" }, { value: "cuda", label: "GPU (CUDA)" }]} />
552: <SelectField label="Compute Type" description={PARAM_DESCRIPTIONS.compute_type} value={params.compute_type} onValueChange={(v) => updateParam('compute_type', v)} options={[{ value: "float32", label: "Float32 (Accurate)" }, { value: "float16", label: "Float16 (Fast)" }, { value: "int8", label: "Int8 (Fastest)" }]} />
553: <FormField label="Batch Size" description={PARAM_DESCRIPTIONS.batch_size}>
554: <Input type="number" min={1} max={64} value={params.batch_size} onChange={(e) => updateParam('batch_size', parseInt(e.target.value) || 8)} className={inputClassName} />
580: <FormField label="Custom Alignment Model" description="WhisperX-compatible alignment model (e.g., KBLab/wav2vec2-large-voxrex-swedish). Leave empty for default." optional>Good — on Apple Silicon, WhisperX is your only realistic option (Parakeet/Canary are CUDA-only; Voxtral is too heavy without GPU). The good news: WhisperX runs fine via faster-whisper on CPU/Accelerate. Here are two profiles I'd start with:
For interviews on a good mic, podcasts, single speaker, low background noise.
medium.en (English) or medium (multilingual) — large-v3 is overkill for clean audio and ~3x slowercpuint8 — big speedup on Apple Silicon, accuracy loss is negligible on clean speech8silero (faster, fine for clean audio)30For phone calls, field recordings, overlapping speakers, accents, background noise.
large-v3cpufloat32 (accuracy matters more than speed here)4 (lower — larger model, keep memory steady)pyannote (better at separating speech from noise)20 (shorter chunks reduce hallucination drift in tough audio)large-v3 feels too slow, large-v2 is often nearly as accurate and a touch faster. large-v3-turbo is another option if it's exposed in your model list (not listed in TranscriptionConfigDialog.tsx:136).Want me to look at TranscriptionConfigDialog.tsx and tell you exactly which dropdown values to pick, or open the profile creation flow?
can you create these profiles directly via code?
Let me investigate how profiles are stored before answering.
>grep -rEn "profile" /Users/tam/code/Scriberr/internal --include="*.go" -l | head -20/Users/tam/code/Scriberr/internal/database/database.go
/Users/tam/code/Scriberr/internal/repository/implementations.go
/Users/tam/code/Scriberr/internal/models/transcription.go
/Users/tam/code/Scriberr/internal/api/router.go
/Users/tam/code/Scriberr/internal/api/handlers.go>grep -rEn "TranscriptionProfile|transcription_profile" /Users/tam/code/Scriberr/internal --include="*.go" | head -30/Users/tam/code/Scriberr/internal/database/database.go:70: &models.TranscriptionProfile{},
/Users/tam/code/Scriberr/internal/database/database.go:117: if err := db.Model(&models.TranscriptionProfile{}).Count(&count).Error; err != nil {
/Users/tam/code/Scriberr/internal/database/database.go:125: profile := models.TranscriptionProfile{
/Users/tam/code/Scriberr/internal/repository/implementations.go:254: Repository[models.TranscriptionProfile]
/Users/tam/code/Scriberr/internal/repository/implementations.go:255: FindDefault(ctx context.Context) (*models.TranscriptionProfile, error)
/Users/tam/code/Scriberr/internal/repository/implementations.go:256: FindByName(ctx context.Context, name string) (*models.TranscriptionProfile, error)
/Users/tam/code/Scriberr/internal/repository/implementations.go:260: *BaseRepository[models.TranscriptionProfile]
/Users/tam/code/Scriberr/internal/repository/implementations.go:265: BaseRepository: NewBaseRepository[models.TranscriptionProfile](db),
/Users/tam/code/Scriberr/internal/repository/implementations.go:269:func (r *profileRepository) FindDefault(ctx context.Context) (*models.TranscriptionProfile, error) {
/Users/tam/code/Scriberr/internal/repository/implementations.go:270: var profile models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/repository/implementations.go:278:func (r *profileRepository) FindByName(ctx context.Context, name string) (*models.TranscriptionProfile, error) {
/Users/tam/code/Scriberr/internal/repository/implementations.go:279: var profile models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/models/transcription.go:177:// TranscriptionProfile represents a saved transcription configuration profile
/Users/tam/code/Scriberr/internal/models/transcription.go:178:type TranscriptionProfile struct {
/Users/tam/code/Scriberr/internal/models/transcription.go:189:func (tp *TranscriptionProfile) BeforeCreate(tx *gorm.DB) error {
/Users/tam/code/Scriberr/internal/models/transcription.go:197:func (tp *TranscriptionProfile) BeforeSave(tx *gorm.DB) error {
/Users/tam/code/Scriberr/internal/models/transcription.go:200: if err := tx.Model(&TranscriptionProfile{}).Where("id != ?", tp.ID).Update("is_default", false).Error; err != nil {
/Users/tam/code/Scriberr/internal/api/handlers.go:334: var profile *models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/api/handlers.go:444: var profile *models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/api/handlers.go:2222:// @Success 200 {array} models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/api/handlers.go:2241:// @Param profile body models.TranscriptionProfile true "Profile data"
/Users/tam/code/Scriberr/internal/api/handlers.go:2242:// @Success 201 {object} models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/api/handlers.go:2248: var profile models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/api/handlers.go:2279:// @Success 200 {object} models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/api/handlers.go:2302:// @Param profile body models.TranscriptionProfile true "Updated profile data"
/Users/tam/code/Scriberr/internal/api/handlers.go:2303:// @Success 200 {object} models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/api/handlers.go:2318: var updatedProfile models.TranscriptionProfile
/Users/tam/code/Scriberr/internal/api/handlers.go:2696:// @Success 200 {object} models.TranscriptionProfile>grep -nE "profiles|TranscriptionProfile" /Users/tam/code/Scriberr/internal/api/router.go | head -20162: profiles := v1.Group("/profiles")
163: profiles.Use(middleware.AuthMiddleware(authService))
165: profiles.GET("/", handler.ListProfiles)
166: profiles.POST("/", handler.CreateProfile)
167: profiles.GET("/:id", handler.GetProfile)
168: profiles.PUT("/:id", handler.UpdateProfile)
169: profiles.DELETE("/:id", handler.DeleteProfile)
170: profiles.POST("/:id/set-default", handler.SetDefaultProfile)>ls -la /Users/tam/code/Scriberr/data/ 2>/dev/null; find /Users/tam/code/Scriberr -name "*.db" -not -path "*/node_modules/*" 2>/dev/null | head -5total 2744
drwxr-xr-x@ 8 tam staff 256 Apr 28 15:02 .
drwxr-xr-x 56 tam staff 1792 Apr 27 21:48 ..
-rw-------@ 1 tam staff 64 Apr 27 21:48 jwt_secret
-rw-r--r--@ 1 tam staff 1400832 Apr 28 14:50 scriberr.db
drwxr-xr-x@ 3 tam staff 96 Apr 28 14:11 temp
drwxr-xr-x@ 5 tam staff 160 Apr 28 14:34 transcripts
drwxr-xr-x@ 6 tam staff 192 Apr 28 14:45 uploads
drwxr-xr-x@ 6 tam staff 192 Apr 28 13:00 whisperx-env
/Users/tam/code/Scriberr/data/scriberr.db>lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | grep -E "scriberr|:8080|:3000|:8000" | head -5; grep -nE "ServerPort|server.*port|listen" /Users/tam/code/Scriberr/internal/config/config.go | head -10scriberr 24174 tam 7u IPv6 0xf9177d72c84a6e4f 0t0 TCP *:8080 (LISTEN)>sqlite3 /Users/tam/code/Scriberr/data/scriberr.db ".schema transcription_profiles" 2>/dev/null | head -80CREATE TABLE IF NOT EXISTS "transcription_profiles" (`id` varchar(36),`name` varchar(255) NOT NULL,`description` text,`is_default` boolean DEFAULT false,`model_family` varchar(20) DEFAULT "whisper",`model` varchar(50) DEFAULT "small",`model_cache_only` boolean DEFAULT false,`model_dir` text,`device` varchar(20) DEFAULT "cpu",`device_index` integer DEFAULT 0,`batch_size` integer DEFAULT 8,`compute_type` varchar(20) DEFAULT "float32",`threads` integer DEFAULT 0,`output_format` varchar(20) DEFAULT "all",`verbose` boolean DEFAULT true,`task` varchar(20) DEFAULT "transcribe",`language` varchar(10),`align_model` varchar(100),`interpolate_method` varchar(20) DEFAULT "nearest",`no_align` boolean DEFAULT false,`return_char_alignments` boolean DEFAULT false,`vad_method` varchar(20) DEFAULT "pyannote",`vad_onset` real DEFAULT 0.5,`vad_offset` real DEFAULT 0.363,`chunk_size` integer DEFAULT 30,`diarize` boolean DEFAULT false,`min_speakers` integer,`max_speakers` integer,`diarize_model` varchar(50) DEFAULT "pyannote",`speaker_embeddings` boolean DEFAULT false,`temperature` real DEFAULT 0,`best_of` integer DEFAULT 5,`beam_size` integer DEFAULT 5,`patience` real DEFAULT 1,`length_penalty` real DEFAULT 1,`suppress_tokens` text,`suppress_numerals` boolean DEFAULT false,`initial_prompt` text,`condition_on_previous_text` boolean DEFAULT false,`fp16` boolean DEFAULT true,`temperature_increment_on_fallback` real DEFAULT 0.2,`compression_ratio_threshold` real DEFAULT 2.4,`logprob_threshold` real DEFAULT -1,`no_speech_threshold` real DEFAULT 0.6,`max_line_width` integer,`max_line_count` integer,`highlight_words` boolean DEFAULT false,`segment_resolution` varchar(20) DEFAULT "sentence",`hf_token` text,`print_progress` boolean DEFAULT false,`attention_context_left` integer DEFAULT 256,`attention_context_right` integer DEFAULT 256,`is_multi_track_enabled` boolean DEFAULT false,`callback_url` text,`api_key` text,`max_new_tokens` integer,`created_at` datetime,`updated_at` datetime,PRIMARY KEY (`id`));Yes, two ways — both feasible. Want me to confirm which before I touch anything?
Option A: Direct SQLite insert into data/scriberr.db (table transcription_profiles).
BeforeCreate/BeforeSave hooks. Safe here since neither profile is is_default (you already have "Default") and I'll generate UUIDs manually.Option B: HTTP POST to http://localhost:8080/api/v1/profiles with curl.
I'd lean Option A unless you want to validate the API path. Confirm which, and I'll create both profiles with the params I recommended (clean = medium + int8 + silero; noisy = large-v3 + float32 + pyannote + diarize).
a
>sqlite3 /Users/tam/code/Scriberr/data/scriberr.db "PRAGMA journal_mode; SELECT name, model, compute_type, vad_method FROM transcription_profiles;"wal
whisper|small|float32|pyannote>grep -nE "model_family|ModelFamily" /Users/tam/code/Scriberr/internal/transcription/unified_service.go /Users/tam/code/Scriberr/internal/transcription/adapters/whisperx_adapter.go 2>/dev/null | head -20/Users/tam/code/Scriberr/internal/transcription/unified_service.go:169: "model_family": job.Parameters.ModelFamily,
/Users/tam/code/Scriberr/internal/transcription/unified_service.go:214: logger.Info("Processing single-track job", "job_id", job.ID, "model_family", job.Parameters.ModelFamily)
/Users/tam/code/Scriberr/internal/transcription/unified_service.go:379: switch params.ModelFamily {
/Users/tam/code/Scriberr/internal/transcription/unified_service.go:409: "original_family", params.ModelFamily,
/Users/tam/code/Scriberr/internal/transcription/unified_service.go:808: if params.ModelFamily == FamilyNvidiaCanary {
/Users/tam/code/Scriberr/internal/transcription/adapters/whisperx_adapter.go:28: ModelFamily: "whisper",>grep -nE "FamilyWhisper|FamilyNvidia|FamilyMistral|case \"" /Users/tam/code/Scriberr/internal/transcription/unified_service.go | head -2033: FamilyNvidiaCanary = "nvidia_canary"
34: FamilyNvidiaParakeet = "nvidia_parakeet"
35: FamilyWhisper = "whisper"
37: FamilyMistralVoxtral = "mistral_voxtral"
380: case FamilyNvidiaParakeet:
382: case FamilyNvidiaCanary:
384: case FamilyWhisper:
388: case FamilyMistralVoxtral:
808: if params.ModelFamily == FamilyNvidiaCanary {Now creating the two profiles.
>CLEAN_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
NOISY_ID=$(uuidgen | tr '[:upper:]' '[:lower:]')
sqlite3 /Users/tam/code/Scriberr/data/scriberr.db <<SQL
BEGIN;
INSERT INTO transcription_profiles (
id, name, description, is_default,
model_family, model, device, batch_size, compute_type,
vad_method, chunk_size, diarize,
created_at, updated_at
) VALUES (
'$CLEAN_ID',
'Clean audio — fast',
'Single speaker, low noise (podcast/interview). Whisper medium + int8 + silero VAD. ~3x faster than large-v3 on Apple Silicon.',
0,
'whisper', 'medium', 'cpu', 8, 'int8',
'silero', 30, 0,
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
);
INSERT INTO transcription_profiles (
id, name, description, is_default,
model_family, model, device, batch_size, compute_type,
vad_method, chunk_size, diarize, diarize_model,
created_at, updated_at
) VALUES (
'$NOISY_ID',
'Noisy audio — accurate',
'Phone calls, field recordings, accents, multi-speaker. Whisper large-v3 + float32 + pyannote VAD + diarization. Shorter chunks reduce hallucination drift.',
0,
'whisper', 'large-v3', 'cpu', 4, 'float32',
'pyannote', 20, 1, 'pyannote',
CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
);
COMMIT;
SQL
echo "---"
sqlite3 -header -column /Users/tam/code/Scriberr/data/scriberr.db "SELECT name, model, compute_type, vad_method, chunk_size, batch_size, diarize FROM transcription_profiles ORDER BY created_at;"---
name model compute_type vad_method chunk_size batch_size diarize
---------------------- -------- ------------ ---------- ---------- ---------- -------
whisper small float32 pyannote 30 8 1
Clean audio — fast medium int8 silero 30 8 0
Noisy audio — accurate large-v3 float32 pyannote 20 4 1Both profiles are in. Verified:
| Profile | Model | Compute | VAD | Chunk | Batch | Diarize |
|---|---|---|---|---|---|---|
| Clean audio — fast | medium | int8 | silero | 30 | 8 | off |
| Noisy audio — accurate | large-v3 | float32 | pyannote | 20 | 4 | on |
Notes:
large-v3 (~3GB) and the pyannote diarization model. The Clean profile will download medium (~1.5GB). Both cache after first use.If you want to tweak language-specific behavior (e.g. force medium.en for English-only on the clean profile, or set language=en on noisy to skip auto-detect and shave a bit of time), just say which.
can you create a utility that does this to the repo?
Let me check the existing repo structure first so the utility fits the conventions.
>ls /Users/tam/code/Scriberr/cmd/ 2>/dev/null; echo "---"; ls /Users/tam/code/Scriberr/cmd/*/ 2>/dev/nullscriberr-cli
server
---
/Users/tam/code/Scriberr/cmd/scriberr-cli/:
main.go
/Users/tam/code/Scriberr/cmd/server/:
main.go>grep -nE "Initialize|database.Initialize|config.Load" /Users/tam/code/Scriberr/cmd/*/main.go /Users/tam/code/Scriberr/main.go 2>/dev/null | head -20/Users/tam/code/Scriberr/cmd/server/main.go:72: // Initialize structured logging first
/Users/tam/code/Scriberr/cmd/server/main.go:78: cfg := config.Load()
/Users/tam/code/Scriberr/cmd/server/main.go:83: // Initialize database
/Users/tam/code/Scriberr/cmd/server/main.go:85: if err := database.Initialize(cfg.DatabasePath); err != nil {
/Users/tam/code/Scriberr/cmd/server/main.go:91: // Initialize authentication service
/Users/tam/code/Scriberr/cmd/server/main.go:95: // Initialize SSE Broadcaster
/Users/tam/code/Scriberr/cmd/server/main.go:99: // Initialize repositories
/Users/tam/code/Scriberr/cmd/server/main.go:112: // Initialize services
/Users/tam/code/Scriberr/cmd/server/main.go:117: // Initialize unified transcription processor
/Users/tam/code/Scriberr/cmd/server/main.go:129: // Initialize quick transcription service
/Users/tam/code/Scriberr/cmd/server/main.go:137: // Initialize task queue
/Users/tam/code/Scriberr/cmd/server/main.go:143: // Initialize multi-track processor
/Users/tam/code/Scriberr/cmd/server/main.go:146: // Initialize API handlers>cat /Users/tam/code/Scriberr/Makefile 2>/dev/null | head -60.PHONY: help docs docs-serve docs-clean website website-dev website-build dev
help: ## Show this help message
@echo 'Usage: make [target]'
@echo ''
@echo 'Available targets:'
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}'
dev: ## Start development environment with Air (backend) and Vite (frontend)
@echo "🚀 Starting development environment..."
@# Ensure air is installed
@if ! command -v air >/dev/null 2>&1; then \
echo "⚠️ 'air' command not found."; \
echo "📦 Auto-installing 'air' for live reload..."; \
GOPATH=$$(go env GOPATH); \
if [[ ":$$PATH:" != *":$$GOPATH/bin:"* ]]; then \
echo "⚠️ $$GOPATH/bin is not in your PATH. Adding it temporarily..."; \
export PATH=$$PATH:$$GOPATH/bin; \
fi; \
go install github.com/air-verse/air@latest; \
if ! command -v air >/dev/null 2>&1; then \
echo "❌ Failed to install 'air'. Falling back to 'go run'..."; \
USE_GO_RUN=true; \
else \
echo "✅ 'air' installed successfully."; \
USE_GO_RUN=false; \
fi; \
else \
USE_GO_RUN=false; \
fi; \
\
mkdir -p internal/web/dist; \
if [ -z "$$(ls -A internal/web/dist)" ]; then \
echo "📄 Creating placeholder files for Go embed..."; \
echo "<!-- Placeholder for development -->" > internal/web/dist/index.html; \
echo "placeholder" > internal/web/dist/dummy_asset; \
fi; \
\
trap 'echo ""; echo "🛑 Stopping development servers..."; kill 0; exit 0' INT TERM; \
\
if [ "$$USE_GO_RUN" = true ]; then \
echo "🔧 Starting Go backend (standard run)..."; \
go run cmd/server/main.go & \
else \
echo "🔥 Starting Go backend (with Air live reload)..."; \
air & \
fi; \
\
echo "⚛️ Starting React frontend (Vite)..."; \
cd web/frontend && npm run dev & \
\
wait
docs: ## Generate API documentation from Go code annotations
@echo "Generating API documentation..."
@command -v swag >/dev/null 2>&1 || { echo "Error: swag not installed. Run: go install github.com/swaggo/swag/cmd/swag@latest"; exit 1; }
swag init -g cmd/server/main.go -o api-docs
@echo "Syncing to project site..."
swag init -g server/main.go -o web/project-site/public/api --outputTypes json --dir cmd,internal
@echo "✓ API documentation generated in api-docs/ and web/project-site/public/api/">ls /Users/tam/code/Scriberr/internal/cli/ 2>/dev/nullclient.go
config.go
login.go
root.go
service.go
watch.goBefore I write, three real shape options — they have different blast radius:
A. New subcommand on existing scriberr-cli (e.g. scriberr profiles seed-presets)
internal/cli/login.go)B. New standalone binary cmd/seed-profiles/main.go that opens the DB via database.Initialize + GORM repository
seedDefaultProfile pattern in internal/database/database.go:115FindByNameC. SQL seed file committed to e.g. data/seeds/profiles.sql, run via make seed-profiles
My recommendation: B. It's the right idiom for this codebase (you already have seedDefaultProfile), works in fresh installs without auth setup, stays idempotent, and reuses models.TranscriptionProfile so the schema can never drift from the seed.
Scope: hard-code the two Apple-Silicon presets (Clean / Noisy), idempotent (skip by name), --force flag to overwrite. Add a make seed-profiles target.
Confirm B and I'll build it. Or pick A/C and I'll build that instead.
should the existing seed code be refactored? what's the cleanest solution?
Good question — and yes, it's worth refactoring. The cleaner shape removes the need for a separate CLI utility entirely.
Today seedDefaultProfile (database.go:115) is:
database.gocount > 0 (i.e. only seeds an empty DB)That count > 0 check is the real problem. It means: once any profile exists, no built-in profile can ever be added again. So if you ship a new preset in v1.2, no existing install will ever see it.
Move presets to a name-keyed registry, idempotent by name, runs on every startup:
internal/database/ database.go // calls ensureBuiltInProfiles(db) profile_seeds.go // []models.TranscriptionProfile registry + ensure fn// profile_seeds.govar builtInProfiles = []models.TranscriptionProfile{ {Name: "Default", IsDefault: true, ...}, {Name: "Clean audio — fast", ...}, {Name: "Noisy audio — accurate", ...},}
// ensureBuiltInProfiles inserts any built-in profile whose Name is missing.// Never overwrites user edits; never deletes; safe to run repeatedly.func ensureBuiltInProfiles(db *gorm.DB) error { for i := range builtInProfiles { p := builtInProfiles[i] var existing models.TranscriptionProfile err := db.Where("name = ?", p.Name).First(&existing).Error if errors.Is(err, gorm.ErrRecordNotFound) { if err := db.Create(&p).Error; err != nil { return err } continue } if err != nil { return err } } return nil}If a user deliberately deletes a built-in profile, it comes back on the next startup. Two ways to handle that:
seeded_builtin_profiles(name TEXT PRIMARY KEY)) — only seed each name once, ever. Cleaner UX, but ~15 extra lines and a new table for a corner case.I'd ship #1 and only move to #2 if it actually annoys someone.
The "create a utility" task collapses into: refactor seedDefaultProfile → ensureBuiltInProfiles, add the two new presets to the slice, delete the count > 0 check. ~40 lines of diff, no new files, no new binary.
Want me to implement it?
yes
Let me check the model field details before writing.
>grep -nE "type WhisperXParams" /Users/tam/code/Scriberr/internal/models/transcription.go50:type WhisperXParams struct {>grep -rn "func ptr\|func strPtr" /Users/tam/code/Scriberr/internal/database/ 2>/dev/null(Bash completed with no output)Now update database.go to call the new function and remove the old one.
>grep -nE "models\." internal/database/database.go | head -1064: &models.TranscriptionJob{},
65: &models.TranscriptionJobExecution{},
66: &models.SpeakerMapping{},
67: &models.MultiTrackFile{},
68: &models.User{},
69: &models.APIKey{},
70: &models.TranscriptionProfile{},
71: &models.LLMConfig{},
72: &models.ChatSession{},
73: &models.ChatMessage{},>go build ./... 2>&1 | head -40(Bash completed with no output)Build passes. Let me run go vet and the relevant tests.
>go vet ./internal/database/... 2>&1(Bash completed with no output)>go test ./internal/database/... 2>&1 | tail -20? scriberr/internal/database [no test files]>sqlite3 -header -column /Users/tam/code/Scriberr/data/scriberr.db "SELECT name, is_default FROM transcription_profiles ORDER BY created_at;"name is_default
---------------------- ----------
whisper 0
Clean audio — fast 0
Noisy audio — accurate 0Let me show the final shape of the change.
>git diff --stat internal/database/ && echo "---" && git status --short internal/database/ internal/database/database.go | 54 ++-----------------------------------------
1 file changed, 2 insertions(+), 52 deletions(-)
---
M internal/database/database.go
?? internal/database/profile_seeds.goDone. Build and vet are clean.
internal/database/profile_seeds.go (~115 lines) — preset registry + ensureBuiltInProfilesinternal/database/database.go — seedDefaultProfile (52 lines) replaced with one call to ensureBuiltInProfiles. Net: −50 linesDefault, Clean audio — fast, Noisy audio — accurate) and inserts only those whose Name is missing.IsDefault: true on Default only takes effect when no profile in the DB is currently marked default — otherwise it's downgraded to non-default so existing user defaults are never overridden.count > 0 check is gone, so future presets you add to the slice will land on existing installs automatically.You currently have three profiles, none of which are marked default:
whisper is_default=0Clean audio — fast is_default=0Noisy audio — accurate is_default=0On your next server restart, the seeder will add a fresh Default profile (IsDefault=true) because no profile is currently default. You'll end up with four profiles. If you'd rather have whisper be the default, set it in Settings before restarting (or UPDATE transcription_profiles SET is_default=1 WHERE name='whisper';) — the seeder will then see a default exists and skip creating its own.
Want me to commit this?
yes
>git statusOn branch main
Your branch is ahead of 'origin/main' by 1 commit.
(use "git push" to publish your local commits)
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: internal/database/database.go
Untracked files:
(use "git add <file>..." to include in what will be committed)
internal/database/profile_seeds.go
no changes added to commit (use "git add" and/or "git commit -a")>git diff internal/database/database.godiff --git a/internal/database/database.go b/internal/database/database.go
index c261e4a..24362f8 100644
--- a/internal/database/database.go
+++ b/internal/database/database.go
@@ -101,63 +101,13 @@ func Initialize(dbPath string) error {
return fmt.Errorf("failed to create unique constraint for speaker mappings: %v", err)
}
- if err := seedDefaultProfile(DB); err != nil {
- return fmt.Errorf("failed to seed default transcription profile: %v", err)
+ if err := ensureBuiltInProfiles(DB); err != nil {
+ return fmt.Errorf("failed to seed built-in transcription profiles: %v", err)
}
return nil
}
-// seedDefaultProfile inserts a built-in "Default" transcription profile when no
-// profiles exist. Without this, a fresh install has nothing for the transcribe
-// dialog to select, so the Start button stays disabled until a user manually
-// creates one in Settings.
-func seedDefaultProfile(db *gorm.DB) error {
- var count int64
- if err := db.Model(&models.TranscriptionProfile{}).Count(&count).Error; err != nil {
- return err
- }
- if count > 0 {
- return nil
- }
-
- desc := "Built-in default profile. Edit or replace it in Settings → Profiles."
- profile := models.TranscriptionProfile{
- Name: "Default",
- Description: &desc,
- IsDefault: true,
- Parameters: models.WhisperXParams{
- ModelFamily: "whisper",
- Model: "small",
- Device: "cpu",
- BatchSize: 8,
- ComputeType: "float32",
- OutputFormat: "all",
- Verbose: true,
- Task: "transcribe",
- InterpolateMethod: "nearest",
- VadMethod: "pyannote",
- VadOnset: 0.5,
- VadOffset: 0.363,
- ChunkSize: 30,
- DiarizeModel: "pyannote",
- BestOf: 5,
- BeamSize: 5,
- Patience: 1.0,
- LengthPenalty: 1.0,
- Fp16: true,
- TemperatureIncrementOnFallback: 0.2,
- CompressionRatioThreshold: 2.4,
- LogprobThreshold: -1.0,
- NoSpeechThreshold: 0.6,
- SegmentResolution: "sentence",
- AttentionContextLeft: 256,
- AttentionContextRight: 256,
- },
- }
- return db.Create(&profile).Error
-}
-
// Close closes the database connection gracefully
func Close() error {
if DB == nil {>git log --oneline -109992bf3 fix: seed default transcription profile so fresh installs can transcribe
bdb8838 fix: pin torchcodec to 0.7.x for compatibility with PyTorch 2.8.x
9fd0943 fix: move override-dependencies to correct TOML scope [tool.uv]
6221843 fix: restore diarization on Linux ARM64 and add WhisperX model selector
00923e8 refactor: use shared FormHelpers in SummaryTemplateDialog
5cdc91c refactor: deduplicate TranscriptionConfigDialog with shared form helpers
77c3365 Add TODO comment to remember that this fix is really about technical debt and we should just remove the legacy workers argument entirely.
d0a1dcb docs(queue): add comments to test functions for consistency
8b3f25f refactor(queue): use testify assertions and t.Setenv in queue tests
8d05a7c fix(queue): allow QUEUE_WORKERS env var to override hardcoded worker count>git add internal/database/database.go internal/database/profile_seeds.go && git commit -m "$(cat <<'EOF'
refactor: name-keyed built-in profile seeder + add Clean/Noisy presets
Replace count-based skip (which only ran on empty DBs) with name-keyed
idempotency, so future built-in profiles land on existing installs without
overwriting user edits. Adds "Clean audio — fast" and "Noisy audio — accurate"
as starter presets alongside the existing "Default".
Co-Authored-By: Claude Opus 4.7 (1M context) <[REDACTED]>
EOF
)"[main 55166dd] refactor: name-keyed built-in profile seeder + add Clean/Noisy presets
2 files changed, 117 insertions(+), 52 deletions(-)
create mode 100644 internal/database/profile_seeds.go>git statusOn branch main
Your branch is ahead of 'origin/main' by 2 commits.
(use "git push" to publish your local commits)
nothing to commit, working tree cleanCommitted as 55166dd (2 commits ahead of origin/main, not pushed). Working tree clean.
internal/database/ database.go // calls ensureBuiltInProfiles(db) profile_seeds.go // []models.TranscriptionProfile registry + ensure fn// profile_seeds.govar builtInProfiles = []models.TranscriptionProfile{ {Name: "Default", IsDefault: true, ...}, {Name: "Clean audio — fast", ...}, {Name: "Noisy audio — accurate", ...},}
// ensureBuiltInProfiles inserts any built-in profile whose Name is missing.// Never overwrites user edits; never deletes; safe to run repeatedly.func ensureBuiltInProfiles(db *gorm.DB) error { for i := range builtInProfiles { p := builtInProfiles[i] var existing models.TranscriptionProfile err := db.Where("name = ?", p.Name).First(&existing).Error if errors.Is(err, gorm.ErrRecordNotFound) { if err := db.Create(&p).Error; err != nil { return err } continue } if err != nil { return err } } return nil}whisper is_default=0Clean audio — fast is_default=0Noisy audio — accurate is_default=0