Edit on GitHub

Setting up Diarist with a coding agent

This document is for a coding agent (Claude Code, Codex or similar) that a user has asked to set up Diarist on their Windows PC. It is a procedure: follow the steps in order, run the PowerShell exactly as given, and compare the output with what each step says it means. It does what Diarist's own first-run screen does, from a terminal, and leaves a result you can check: the app installed, the runtime and models in place with their SHA-256 verified, and a test transcript the user has seen.

People without an agent need none of this: the README quick start is three steps.

Contents

  1. Read this first
  2. Ask the user
  3. Check the PC
  4. Decide the package and models
  5. Install Diarist
  6. Place the runtime and models
  7. Verify
  8. Updating later

0. Read this first

Diarist is a local meeting transcriber: it records or imports a meeting and writes a transcript where every line says who spoke. The speech and speaker models are NVIDIA's Nemotron models, run by NVIDIA's NeMo-Speech.cpp runtime. The installer holds only the app; the runtime (a zip) and the models (.gguf files) go into the app's data folder, which is what steps 4 and 5 do.

Rules. They hold for the whole procedure, whatever a command prints:

How to run the commands:

1. Ask the user

Ask these three questions together, and wait for the answers:

  1. "Are your meetings in English only, or also in other languages?" This picks the speech model (step 3).
  2. "Do you use headphones for calls?" Diarist records the microphone and the system audio separately. With loudspeakers the microphone also hears the other people, and their words can end up under the user's name. Recommend headphones for live recording for now. The answer changes nothing in the setup; do not change any audio setting.
  3. "Diarist keeps its data (transcripts, recordings, the runtime and the models) in %LOCALAPPDATA%\app.diarist.desktop. Is that fine, or do you want it in another folder, for example on another drive?" Plan about 2 GB for the runtime and models plus about 90 MB per recorded hour (recordings are kept 30 days by default). Another folder is set in step 4b.

2. Check the PC

Run this block. It only reads.

# Windows and memory
"Windows version: " + [System.Environment]::OSVersion.Version
"64-bit Windows: " + [System.Environment]::Is64BitOperatingSystem
"RAM: {0:N1} GB" -f ((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB)

# NVIDIA GPU: name, VRAM, driver, compute capability, CUDA (needs an NVIDIA driver)
$smi = Get-Command nvidia-smi.exe -ErrorAction SilentlyContinue
if ($smi) {
    & $smi.Source --query-gpu=name,memory.total,driver_version --format=csv,noheader
    & $smi.Source --query-gpu=compute_cap --format=csv,noheader
    & $smi.Source | Select-String 'CUDA.*Version'
} else {
    "nvidia-smi: not found"
}

# Every display adapter, any vendor, and its dedicated memory
Get-CimInstance Win32_VideoController | Select-Object Name, DriverVersion, AdapterRAM | Format-Table -AutoSize
Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Class\{4d36e968-e325-11ce-bfc1-08002be10318}\0*' -ErrorAction SilentlyContinue |
    Select-Object DriverDesc, @{ n = 'VRAM_GB'; e = { [math]::Round($_.'HardwareInformation.qwMemorySize' / 1GB, 1) } } |
    Format-Table -AutoSize

# Free space per drive
Get-PSDrive -PSProvider FileSystem | Select-Object Name, Root, @{ n = 'FreeGB'; e = { [math]::Round($_.Free / 1GB, 1) } } | Format-Table -AutoSize

# What could block an unsigned program, and the downloader
$sac = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\CI\Policy' -Name VerifiedAndReputablePolicyState -ErrorAction SilentlyContinue).VerifiedAndReputablePolicyState
"Smart App Control: " + $(switch ($sac) { 0 { 'off' } 1 { 'on' } 2 { 'evaluation' } default { 'not present' } })
"curl.exe present: " + (Test-Path "$env:SystemRoot\System32\curl.exe")

Then this one, which looks for an existing Diarist and its data folder the way the app finds it (portable marker, then the registry, then the default):

$loc = (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Diarist' -ErrorAction SilentlyContinue).InstallLocation
$app = if ($loc) { $loc.Trim('"') } else { "$env:LOCALAPPDATA\Diarist" }
$exe = Join-Path $app 'diarist.exe'
"Program folder: $app"
"Diarist installed: " + (Test-Path $exe)
if (Test-Path $exe) { "Installed version: " + (Get-Item $exe).VersionInfo.ProductVersion }
"Diarist running: " + [bool](Get-Process diarist -ErrorAction SilentlyContinue)

$marker = Join-Path $app 'portable.txt'
$reg = (Get-ItemProperty 'HKCU:\Software\Diarist' -Name DataRoot -ErrorAction SilentlyContinue).DataRoot
if (Test-Path $marker) {
    $line = Get-Content $marker | Where-Object { $_.Trim() } | Select-Object -First 1
    $data = if (-not $line) { Join-Path $app 'data' }
            elseif ([System.IO.Path]::IsPathRooted($line.Trim())) { $line.Trim() }
            else { Join-Path $app $line.Trim() }
    "Data folder (portable mode): $data"
} elseif ($reg) {
    $data = $reg
    "Data folder (moved, HKCU\Software\Diarist\DataRoot): $data"
} else {
    $data = "$env:LOCALAPPDATA\app.diarist.desktop"
    "Data folder (default): $data"
}
"Data folder exists: " + (Test-Path $data)
"Transcripts database exists: " + (Test-Path (Join-Path $data 'diarist.db'))
Get-ChildItem (Join-Path $data 'engine'), (Join-Path $data 'models') -Recurse -Include nemo-speech.exe, *.gguf, *.onnx -ErrorAction SilentlyContinue |
    ForEach-Object { '{0,11}  {1}' -f $_.Length, $_.FullName }

# Data from Diarist's earlier name, meet-assist
$old = (Get-ItemProperty 'HKCU:\Software\meet-assist' -Name DataRoot -ErrorAction SilentlyContinue).DataRoot
if (-not $old) { $old = "$env:LOCALAPPDATA\app.meetassist.desktop" }
"meet-assist data: " + (Test-Path (Join-Path $old 'meet-assist.db'))

What the output means:

Line Meaning
Windows version: 10.0.<build> The build (third number) must be 17134 or higher (Windows 10 1803, or any Windows 11). Lower: stop, Diarist does not run there. 64-bit Windows must be True.
nvidia-smi line, e.g. NVIDIA GeForce RTX 3080, 10240 MiB, 610.88 GPU name, VRAM, driver version. A driver of 580 or higher supports CUDA 13, which the CUDA package needs.
Compute capability, e.g. 8.6 The CUDA package has kernels for Turing (7.5), Ampere (8.6), Ada (8.9) and Blackwell (12.0). Below 7.5 (GTX 10 series and older) use Vulkan. An error here instead of a number means a driver too old to answer: Vulkan.
CUDA Version: 13.0 or CUDA UMD Version: 13.3 The highest CUDA version the driver supports (newer drivers print the second form). 13.0 or higher confirms the CUDA package.
nvidia-smi: not found No NVIDIA driver. Use the adapter table.
Adapter table Any AMD or Intel adapter, or an NVIDIA one without a working driver, means Vulkan. Only "Microsoft Basic Display Adapter" or nothing means no usable GPU: CPU. AdapterRAM is a 32-bit field and stops at 4 GB (4293918720), so read VRAM from the VRAM_GB table (or nvidia-smi) instead.
FreeGB The drive of the data folder needs about 2 GB free, plus about 90 MB per recorded hour. The setup itself also puts about 7 MB into %TEMP%.
Smart App Control: on Windows may block the unsigned installer and the unsigned NVIDIA runtime. Tell the user before going on; do not change the setting. off, evaluation and not present are fine.
curl.exe present: False Windows older than 1803; see the first row.
Program folder %LOCALAPPDATA%\Diarist unless the user picked another folder in the installer; an update installs into the same folder.
Diarist installed: True Step 4 updates it in place; the data folder is kept.
Diarist running: True Ask the user to quit it (tray icon, Quit) before step 4. Do not stop it yourself.
Transcripts database exists: True The user already has Diarist data. Keep it: do not set another data folder in step 4b (the app's Settings, Storage, Move… does that safely). Step 5 skips files that are already there.
Engine and model paths listed Already installed. Step 5 checks them and skips them.
meet-assist data: True The user ran Diarist under its earlier name. At its first start Diarist asks once whether to move that data in, and the move needs empty engine\ and models\ folders. Do step 4, skip step 5, start the app and let the user answer; afterwards run step 5 for whatever is still missing.

3. Decide the package and models

The runtime package, from step 2:

What step 2 showed Package Why
NVIDIA GPU, compute capability 7.5 or higher, driver 580 or newer (CUDA 13.0 or higher) cuda The tested package; measured on an RTX 3080, about 27 times faster than real time.
AMD or Intel GPU, an NVIDIA GPU with an older driver, or compute capability below 7.5 vulkan Runs on any current GPU driver; shipped by NVIDIA but untested by Diarist. With an older NVIDIA driver, the user may prefer to update it from nvidia.com (they do that, not you) and use cuda.
No usable GPU cpu Always works, much slower than real time; untested by Diarist.

The models. Always nemotron-3-diarization (who spoke when, up to 8 speakers). The speech model, from question 1 and the VRAM:

Situation Install In the app afterwards (step 6b)
English only, 6 GB VRAM or more nemotron-3-diarization, nemotron-en Nothing: both are selected automatically.
English only, less than 6 GB VRAM nemotron-3-diarization, nemotron-en, nemotron-3.5 Turn on Low-VRAM mode: imports then use Nemotron 3.5, live recording keeps the English model.
Other or mixed languages nemotron-3-diarization, nemotron-3.5 Nothing: Nemotron 3.5 is selected when it is the only speech model.
Both English-only and other meetings all three The English model is selected by default; the user switches in Settings, Models, Speech to text.
CPU package as by language above Expect a long wait for each transcript.

Never install parakeet-tdt. It is experimental: it punctuates, but it silently skipped a 46-second span of a 77-minute recording.

The VRAM figures behind the table, measured on an RTX 3080 10 GB, not counting the desktop's own use (about 1.5 GB):

Scenario VRAM
Live recording, two channels, Nemotron 3.5 and the diarizer 1.05 GB idle, 1.26 GB with two sessions, spikes of 0.5 to 0.6 GB per session; budget 2 GB (the English model sits at 1.18 GB)
File transcription, Nemotron 3.5 1.82 GB peak (77-minute file)
File transcription, English model 2.6 GB peak
Diarization pass alone 0.7 to 0.85 GB
Experimental Parakeet 4.9 GB peak

Estimated, not measured: a 4 GB card handles file transcription with Nemotron 3.5 (with Low-VRAM mode on), and live recording is borderline next to the desktop's own use; 6 GB runs live and file mode comfortably; 8 GB and up runs everything.

Now tell the user the plan and wait for a yes. For example:

I'll install Diarist 0.1.0-beta.1 (7 MB, per user, no administrator rights) into %LOCALAPPDATA%\Diarist, then download the CUDA runtime (98 MB, from NVIDIA's GitHub releases) and two models from Hugging Face, Nemotron 3 Diarization (102 MB) and Nemotron Speech Streaming EN 0.6B (668 MB), into %LOCALAPPDATA%\app.diarist.desktop. Every file is checked against the SHA-256 Diarist publishes. Nothing else on the PC changes. Go ahead?

Take the version from step 4a's output and the sizes from step 5a's if you want them exact; the numbers above are for 0.1.0-beta.1.

4. Install Diarist

4a. Find the newest release and download the installer

Diarist releases are pre-releases for now, and the GitHub API's releases/latest skips those, so this reads the full list. The installer's SHA-256 is in the release notes; GitHub also publishes its own digest of each asset. Both must agree.

$ErrorActionPreference = 'Stop'
$stage = "$env:TEMP\diarist-setup"
New-Item -ItemType Directory -Force $stage | Out-Null
$curl = "$env:SystemRoot\System32\curl.exe"

$all = Invoke-RestMethod -Uri 'https://api.github.com/repos/ebt55/diarist/releases' -Headers @{ 'User-Agent' = 'diarist-agent-setup' }
$rel = $all | Where-Object { -not $_.draft } | Sort-Object { [datetime]$_.published_at } -Descending | Select-Object -First 1
$asset = $rel.assets | Where-Object { $_.name -match '^Diarist_.+_x64-setup\.exe$' } | Select-Object -First 1
if (-not $asset) { throw 'The newest release has no Diarist_*_x64-setup.exe. Stop and tell the user.' }

$fromNotes = [regex]::Match("$($rel.body)", 'SHA-256 of\W+' + [regex]::Escape($asset.name) + '\W+([0-9a-fA-F]{64})').Groups[1].Value.ToLower()
$fromGitHub = ("$($asset.digest)" -replace '^sha256:', '').ToLower()
if (-not $fromNotes -and -not $fromGitHub) { throw 'No SHA-256 for the installer. Stop and tell the user.' }
if ($fromNotes -and $fromGitHub -and $fromNotes -ne $fromGitHub) { throw "Release notes say $fromNotes, GitHub says $fromGitHub. Stop and tell the user." }
$sha = if ($fromNotes) { $fromNotes } else { $fromGitHub }

$installer = Join-Path $stage $asset.name
& $curl -L --fail --retry 3 -sS -o "$installer.part" $asset.browser_download_url
if ($LASTEXITCODE -ne 0) { throw "Download failed (curl exit code $LASTEXITCODE)." }
$got = (Get-FileHash "$installer.part" -Algorithm SHA256).Hash.ToLower()
if ($got -ne $sha) { Remove-Item "$installer.part"; throw "SHA-256 mismatch: got $got, expected $sha. The file was deleted." }
Move-Item "$installer.part" $installer -Force
Unblock-File $installer

[ordered]@{ version = $rel.tag_name.TrimStart('v'); tag = $rel.tag_name; installer = $installer; sha256 = $sha } |
    ConvertTo-Json | Set-Content "$stage\release.json"
"Release notes SHA-256: $fromNotes"
"GitHub SHA-256:        $fromGitHub"
"Downloaded SHA-256:    $got"
Get-Content "$stage\release.json"

The last lines must show three equal hashes (or two, when one source is missing) and the release's version and tag. The installer is not code-signed: Unblock-File removes the "downloaded from the internet" mark so SmartScreen does not stop the silent run (curl does not set that mark, but a copy the user fetched with a browser has it; the hash check is what makes running it safe).

If the API answers 403 (it allows 60 requests an hour without a login), wait and try again later. The app's own update check reads the newest version from https://raw.githubusercontent.com/ebt55/diarist/main/updater/latest.json; that file has the installer's URL but no SHA-256, so it is no substitute for the hash.

4b. Only if the user wants another data folder

Skip this unless the user answered question 3 with another folder and step 2 showed Transcripts database exists: False. It points Diarist at the new folder before its first start, the same registry value the app's own "Move data folder" writes.

$ErrorActionPreference = 'Stop'
$folder = 'D:\Diarist'   # the folder the user named: new or empty
if (Test-Path "$env:LOCALAPPDATA\app.diarist.desktop\diarist.db") { throw 'Diarist already has data in the default folder. Use Settings, Storage, Move... in the app instead.' }
if ((Test-Path $folder) -and (Get-ChildItem $folder -Force | Select-Object -First 1)) { throw "$folder is not empty. Ask the user for a new or empty folder." }
New-Item -ItemType Directory -Force $folder | Out-Null
if (-not (Test-Path 'HKCU:\Software\Diarist')) { New-Item -Path 'HKCU:\Software\Diarist' | Out-Null }
Set-ItemProperty -Path 'HKCU:\Software\Diarist' -Name DataRoot -Value $folder
"Data folder: " + (Get-ItemProperty 'HKCU:\Software\Diarist').DataRoot

From here on, the data folder is this folder. The installer later adds a subkey HKCU\Software\Diarist\Diarist (its own install path); leave both alone.

4c. Run the installer

The installer is an NSIS setup that installs per user into %LOCALAPPDATA%\Diarist without administrator rights; /S runs it silently. Run over an existing install, it updates the program in its folder and keeps the data folder.

$ErrorActionPreference = 'Stop'
$stage = "$env:TEMP\diarist-setup"
$rel = Get-Content "$stage\release.json" -Raw | ConvertFrom-Json
if ((Get-FileHash $rel.installer -Algorithm SHA256).Hash.ToLower() -ne $rel.sha256) { throw 'The installer changed since it was checked. Run 4a again.' }
if (Get-Process diarist -ErrorAction SilentlyContinue) { throw 'Diarist is running. Ask the user to quit it (tray icon, Quit), then run this again.' }
$p = Start-Process -FilePath $rel.installer -ArgumentList '/S' -Wait -PassThru
"Installer exit code: $($p.ExitCode)"
$loc = (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Diarist' -ErrorAction SilentlyContinue).InstallLocation
$app = if ($loc) { $loc.Trim('"') } else { "$env:LOCALAPPDATA\Diarist" }
$exe = Join-Path $app 'diarist.exe'
"diarist.exe present: " + (Test-Path $exe) + " ($exe)"
if (Test-Path $exe) { "Version: " + (Get-Item $exe).VersionInfo.ProductVersion }

Expected: exit code 0, diarist.exe present: True, and the release's version. The program folder holds diarist.exe and uninstall.exe. The installer does not start the app. If WebView2 is missing (rare on Windows 10 and 11), the installer fetches it from Microsoft. The installer contains only the app; as of 0.1.0-beta.1 there is no command-line tool to call.

5. Place the runtime and models

The app can do all of this itself: its first-run screen downloads the same files from the same addresses and checks the same hashes. This step saves the user the clicks and gives you a result you can verify before the app starts. The runtime goes into engine\<package name>\ (the folder holding bin\nemo-speech.exe), each model into models\<repo>\<revision>\<file>, the same folder the app's own download uses. The app recognises a model anywhere under models\ by its file name, size and SHA-256, so the folder is for tidiness; the hash is what counts.

5a. Fetch the manifests of this release

The URLs, sizes and hashes come from the manifests at the release's tag, so they always match the installed version.

$ErrorActionPreference = 'Stop'
$stage = "$env:TEMP\diarist-setup"
$rel = Get-Content "$stage\release.json" -Raw | ConvertFrom-Json
$curl = "$env:SystemRoot\System32\curl.exe"
foreach ($f in 'engine.json', 'models.json') {
    & $curl -L --fail --retry 3 -sS -o "$stage\$f" "https://raw.githubusercontent.com/ebt55/diarist/$($rel.tag)/manifests/$f"
    if ($LASTEXITCODE -ne 0) { throw "Download of $f failed (curl exit code $LASTEXITCODE)." }
}
$engines = Get-Content "$stage\engine.json" -Raw | ConvertFrom-Json
$models = Get-Content "$stage\models.json" -Raw | ConvertFrom-Json
$engines | Select-Object name, backend, version, tested, @{ n = 'MB'; e = { [math]::Ceiling($_.size_bytes / 1MB - 0.1) } } | Format-Table -AutoSize
$models | Select-Object name, role, required, experimental, @{ n = 'MB'; e = { [math]::Ceiling($_.size_bytes / 1MB - 0.1) } } | Format-Table -AutoSize

The two tables list every runtime package and model with its size, rounded as the app shows it. For 0.1.0-beta.1: cuda 98, vulkan 19, cpu 6; nemotron-3-diarization 102, nemotron-en 668, nemotron-3.5 708, parakeet-tdt 681. The names in the name column are the ones step 3 uses.

5b. The runtime

Set the data folder (from step 2, or the folder from step 4b) and the package from step 3, then run:

$ErrorActionPreference = 'Stop'
$data = "$env:LOCALAPPDATA\app.diarist.desktop"   # from step 2 (or 4b)
$backend = 'cuda'                                 # from step 3: cuda, vulkan or cpu

$stage = "$env:TEMP\diarist-setup"
$curl = "$env:SystemRoot\System32\curl.exe"
$engines = Get-Content "$stage\engine.json" -Raw | ConvertFrom-Json
$e = $engines | Where-Object { $_.backend -eq $backend } |
    Sort-Object @{ e = { [bool]$_.tested }; Descending = $true }, @{ e = { [version]$_.version }; Descending = $true } |
    Select-Object -First 1
if (-not $e) { throw "No $backend package in the manifest." }
$root = Join-Path $data 'engine'
$target = Join-Path $root $e.name
if (Test-Path "$target\bin\nemo-speech.exe") {
    "Already installed: $target"
} else {
    New-Item -ItemType Directory -Force "$root\downloads" | Out-Null
    $zip = "$root\downloads\$($e.name).zip"
    & $curl -L --fail --retry 3 -sS -o "$zip.part" $e.url
    if ($LASTEXITCODE -ne 0) { throw "Download failed (curl exit code $LASTEXITCODE): $($e.url)" }
    $got = (Get-FileHash "$zip.part" -Algorithm SHA256).Hash.ToLower()
    if ($got -ne $e.sha256 -or (Get-Item "$zip.part").Length -ne $e.size_bytes) {
        Remove-Item "$zip.part"
        throw "SHA-256 mismatch for $($e.name): got $got, expected $($e.sha256). The file was deleted."
    }
    Move-Item "$zip.part" $zip -Force
    "SHA-256 OK: $($e.name).zip"
    $tmp = "$target.agent-tmp"
    if (Test-Path $tmp) { throw "$tmp is left over from an earlier attempt of yours; delete it and run this again." }
    Expand-Archive -LiteralPath $zip -DestinationPath $tmp
    $inner = if (Test-Path "$tmp\bin\nemo-speech.exe") { $tmp } else {
        Get-ChildItem $tmp -Directory | Where-Object { Test-Path "$($_.FullName)\bin\nemo-speech.exe" } |
            Select-Object -First 1 -ExpandProperty FullName
    }
    if (-not $inner) { throw "The zip has no bin\nemo-speech.exe; left as it is in $tmp." }
    Move-Item -LiteralPath $inner -Destination $target
    if (Test-Path $tmp) { Remove-Item -LiteralPath $tmp -Recurse -Force }
    Remove-Item -LiteralPath $zip
    if (-not (Get-ChildItem "$root\downloads")) { Remove-Item "$root\downloads" }
    "Installed: $target"
}
& "$target\bin\nemo-speech.exe" --json doctor

The last command asks the runtime about this PC. In its JSON, version is the runtime version (0.2.0 for 0.1.0-beta.1). For the CUDA package, driver_runtime_compatible must be true and devices must list the GPU with "type": "gpu"; if not, the driver is too old or the GPU is not supported, and the Vulkan package is the fallback. If Windows refuses to run nemo-speech.exe at all, Smart App Control or an antivirus blocked it: tell the user.

5c. The models

Set the data folder and the model names from step 3, then run. Each model takes a minute or more; run one name at a time if your command timeout is short.

$ErrorActionPreference = 'Stop'
$data = "$env:LOCALAPPDATA\app.diarist.desktop"            # from step 2 (or 4b)
$want = @('nemotron-3-diarization', 'nemotron-en')         # from step 3

$stage = "$env:TEMP\diarist-setup"
$curl = "$env:SystemRoot\System32\curl.exe"
$models = Get-Content "$stage\models.json" -Raw | ConvertFrom-Json
foreach ($name in $want) {
    $m = $models | Where-Object { $_.name -eq $name }
    if (-not $m) { throw "No model named $name in the manifest." }
    $dir = Join-Path (Join-Path $data 'models') ((@($m.repo -split '/') + $m.revision) -join '\')
    $file = Join-Path $dir $m.file
    if (Test-Path $file) {
        $have = (Get-FileHash $file -Algorithm SHA256).Hash.ToLower()
        if ($have -eq $m.sha256) { "Already there, SHA-256 OK: $file"; continue }
        throw "$file exists with SHA-256 $have instead of $($m.sha256). Not touched; tell the user."
    }
    New-Item -ItemType Directory -Force $dir | Out-Null
    & $curl -L --fail --retry 3 -sS -o "$file.part" $m.url
    if ($LASTEXITCODE -ne 0) { throw "Download failed (curl exit code $LASTEXITCODE): $($m.url)" }
    $got = (Get-FileHash "$file.part" -Algorithm SHA256).Hash.ToLower()
    if ($got -ne $m.sha256 -or (Get-Item "$file.part").Length -ne $m.size_bytes) {
        Remove-Item "$file.part"
        throw "SHA-256 mismatch for ${name}: got $got, expected $($m.sha256). The file was deleted."
    }
    Move-Item "$file.part" $file
    "SHA-256 OK: $file"
}

Every model must end with SHA-256 OK or Already there, SHA-256 OK. The app checks each file again at its first start (it hashes a file placed by hand and writes the result next to it as <file>.sha256) and uses it as the recommended model only when the hash matches.

5d. What is in place

$data = "$env:LOCALAPPDATA\app.diarist.desktop"   # from step 2 (or 4b)
Get-ChildItem (Join-Path $data 'engine'), (Join-Path $data 'models') -Recurse -Include nemo-speech.exe, *.gguf |
    ForEach-Object { '{0,11}  {1}' -f $_.Length, $_.FullName.Substring($data.Length + 1) }

One engine\<package>\bin\nemo-speech.exe per installed package and one models\nvidia\...\<file>.gguf per model from step 3, with the size in bytes from the manifest (for example 107012128 models\nvidia\Nemotron-3-Diarization\f667...\Nemotron-3-Diarization.q8_0.gguf).

6. Verify

6a. Start the app

$loc = (Get-ItemProperty 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\Diarist' -ErrorAction SilentlyContinue).InstallLocation
$app = if ($loc) { $loc.Trim('"') } else { "$env:LOCALAPPDATA\Diarist" }
Start-Process (Join-Path $app 'diarist.exe')

Wait about 15 seconds, then read the end of the app log:

$data = "$env:LOCALAPPDATA\app.diarist.desktop"   # from step 2 (or 4b)
Get-Content (Join-Path $data 'logs\diarist.log') -Tail 40
Get-ChildItem (Join-Path $data 'models') -Recurse -Filter *.sha256 | Select-Object Name

The newest start line reads Diarist <version> started; data root <folder> (Default), or (Registry) after step 4b; the folder must be the one you placed the files in. A .sha256 file next to each model means the app has hashed it at its first scan. A log line model scan: ... not used as ... means a file has the right name and size but other bytes; tell the user. If the app shows a dialog "Move your existing meet-assist data to Diarist?", that is the user's decision (see step 2); do not answer it.

6b. What the user should see, and what to click

Tell the user what to look at; you cannot click for them.

Then, as step 3 decided:

6c. Smoke test

This writes a test recording of about 15 seconds with Windows' built-in text to speech (Windows PowerShell 5.1 only; PowerShell 7 lacks System.Speech):

Add-Type -AssemblyName System.Speech
$wav = Join-Path ([Environment]::GetFolderPath('Desktop')) 'diarist-test.wav'
$s = New-Object System.Speech.Synthesis.SpeechSynthesizer
$en = $s.GetInstalledVoices() | Where-Object { $_.Enabled -and $_.VoiceInfo.Culture.Name -like 'en-*' } | Select-Object -First 1
if ($en) { $s.SelectVoice($en.VoiceInfo.Name) }
$fmt = New-Object System.Speech.AudioFormat.SpeechAudioFormatInfo(16000, [System.Speech.AudioFormat.AudioBitsPerSample]::Sixteen, [System.Speech.AudioFormat.AudioChannel]::Mono)
$s.SetOutputToWaveFile($wav, $fmt)
$s.Speak('This is a test of Diarist on this computer. The weekly meeting starts at ten in the morning. We will talk about the budget, the new office and the plan for next year. Please send your notes before Friday. Thank you all for coming.')
$s.Dispose()
"Wrote $wav, about {0:N0} seconds, voice: {1}" -f (((Get-Item $wav).Length - 44) / 32000), $(if ($en) { $en.VoiceInfo.Name } else { 'default' })

The last line should say about 16 seconds and name an English voice (Microsoft David Desktop or Microsoft Zira Desktop on most PCs).

Ask the user to press Ctrl+O in Diarist (the dialog is titled "Import recordings") and pick diarist-test.wav on their desktop, or drag the file onto the window. A correct result: a meeting called "diarist-test" appears in the library as "16 s, 1 speaker", within a few seconds with the CUDA package (longer with Vulkan or CPU; the app log says import done: ... in <seconds> s). Opened, it shows one speaker, "Speaker 1", and the text above split into about five lines at the pauses, near word for word ("Diarist" may come out as "diarist"). On real meetings the Nemotron models punctuate much less than on this clean synthetic voice. With no English voice installed the test speaks another language, and only Nemotron 3.5 transcribes that.

The user can delete the meeting (Delete key) and the WAV afterwards. You may delete %TEMP%\diarist-setup, which only holds the installer and the manifests you downloaded.

If anything is off

Ask the user to open Diagnostics (Ctrl+Shift+D). Why isn't it working? lists every check with a fix button on each failing row; Check again re-runs them. For a bug report, Copy diagnostics copies a text report to paste into an issue at https://github.com/ebt55/diarist/issues, together with what you ran and what it printed. The app log is logs\diarist.log in the data folder.

7. Updating later

In the app: Settings, section About, Check for updates, then Install and restart. Updates are signed; the app checks the signature, and the data folder is not touched.

From a terminal: run steps 4a and 4c again (the new installer updates the old install in place), then 5a, 5b and 5c with the same choices. Files whose hashes still match are skipped; a new runtime or model version lands in its own folder next to the old one. Never remove the old ones yourself: the user can delete them once the new ones work.