A self-hosted, mobile-ready progressive web application for capturing and searching personal video diary entries. The solution uses Blazor WebAssembly (hosted) with Ahead-of-Time (AOT) compilation and is container-ready for Linux deployments.
Created entirely with Codex guidance: every source file in this repository was generated or edited through OpenAI's Codex (via prompt engineering and follow-up debugging instructions). No manual coding took place beyond supplying prompts and reviewing results, aside from a few inline comments in
appsettings.jsonto steer configuration. Think of this project as a full demonstration of "vibe coding" an end-to-end app with AI assistance.
You can play with it at https://rec-one.dev
rec-one/
├── Dockerfile # Multi-stage build for containerized hosting
└── src/
├── DiaryApp.Client/ # Blazor WebAssembly front-end
├── DiaryApp.Server/ # ASP.NET Core host and API surface
├── DiaryApp.Shared/ # Shared contracts and models
└── DiaryApp.sln # Solution file
- Browser/phone friendly recorder powered by MediaRecorder via JavaScript interop
- Form-based metadata capture with optional automatic transcription, summarization and title generation triggers
- Settings page lets each user pick preferred devices, transcript locale, and a favorite tag list that guides AI auto-tagging
- Entry listing with keyword search and transcript peek support
- Installable PWA with offline caching hooks and AOT enabled for smaller footprint and faster startup
- RESTful endpoints for creating, updating and retrieving diary entries and derived assets
- File-system backed storage with configurable root directory and naming convention
- Pluggable processing services to integrate transcription, summarization, auto-title, tag-suggestion providers, and now semantic search embeddings (via Azure OpenAI)
- In-memory search index with keyword search plus optional Azure OpenAI embedding powered semantic search over descriptions
- Optional OpenID Connect authentication wiring via
Authentication:OIDCsettings
- .NET 10 SDK for local development (AOT publishing requires the full SDK toolchain)
- Node is not required; assets are handled by the Blazor build pipeline
cd src
dotnet restore
dotnet buildTo publish with AOT optimizations:
dotnet publish DiaryApp.Server/DiaryApp.Server.csproj -c Release -o ../publishdotnet run --project DiaryApp.Server/DiaryApp.Server.csproj
Access the app at https://localhost:5001 (or http://localhost:5000).
Tip (media devices not showing):
If the Settings page does not list your camera/microphone devices the first time you open the app, it is usually because the browser has not yet granted permission. Start a quick recording from the main page so the browser prompts you to allow camera/mic access, then stop the recording immediately after granting permission (it does not matter if nothing useful was recorded). You can also click Refresh devices on the Settings page to re-enumerate devices after granting permission. Once permission is granted, the app will be able to enumerate and show all available media devices.
Key settings live in DiaryApp.Server/appsettings.json (or any other ASP.NET Core configuration source). The most important ones are:
| Section | Key(s) | Description |
|---|---|---|
Storage |
RootDirectory, FileNameFormat |
Controls where video files and entries.json are persisted. Point RootDirectory at a mounted host folder (/data/entries inside Docker). FileNameFormat is a standard .NET date format string used when naming new recordings. |
Authentication:OIDC |
Authority, ClientId, ClientSecret, ResponseType |
Enables OpenID Connect login when provided. Leave the entire section commented/empty to run anonymously. Every setting can also be supplied through env vars (Authentication__OIDC__Authority, etc.). |
Transcription, Summaries, Titles, TagSuggestions |
Enabled, Provider, Settings |
Toggle the automatic pipelines. When Enabled is true and the user leaves the field blank, the configured provider is invoked. Use Settings to inject provider-specific options (API keys, model names, endpoints). TagSuggestions looks at the user's favorite tag list (managed under Settings) and appends AI-selected tags when entries are saved or transcripts are requested. |
Logging |
LogLevel |
Standard ASP.NET Core logging knobs. |
SemanticSearch |
Enabled, Provider, Settings |
Optional Azure OpenAI embedding support for description-based semantic search. When enabled, the server stores vectors in-memory and falls back to keyword search when embeddings cannot be generated. |
Cookie persistence: the server stores its data-protection keys under
%LOCALAPPDATA%/DiaryApp/keys(Linux:/root/.local/share/DiaryApp/keys). Mount that path when containerizing so auth cookies survive restarts.
To enable automatic transcripts backed by Azure AI Speech:
-
Provision a Speech resource and note the primary key plus region (e.g.,
westeurope) or the full Speech-to-Text endpoint (https://<region>.stt.speech.microsoft.com). -
Update
DiaryApp.Server/appsettings.json(or environment variables) so theTranscriptionsection looks like:"Transcription": { "Enabled": true, "Provider": "AzureSpeech", "Settings": { "SpeechKey": "<your-primary-key>", "SpeechRegion": "westeurope", "SpeechToTextEndpoint": "wss://westeurope.stt.speech.microsoft.com/speech/universal/v2", "RecognitionMode": "conversation", "ResponseFormat": "detailed", "FFmpegPath": "/usr/bin" } }
Every setting can be overridden via environment variables such as
Transcription__Settings__SpeechKey. Install FFmpeg on the host (https://ffmpeg.org/download.html). IfFFmpegPathis omitted the server searches the systemPATH; when the executables are missing the transcription request fails with a clear error instead of silently falling back. (The Docker image already ships/usr/bin/ffmpeg, so the sample configuration pins that path.) If you provideSpeechToTextEndpoint, it must be a WebSocket endpoint (e.g.,wss://<region>.stt.speech.microsoft.com/speech/universal/v2). Otherwise omit it and the SDK will derive the right host fromSpeechRegion. -
Users can pick their preferred transcript language under Settings → Transcript language. The value defaults to
en-USand is passed to Azure Speech whenever a transcript is generated. -
Whenever a video is recorded the server extracts the audio track (FFmpeg → 16kHz WAV), feeds it to the Azure Speech SDK, captures the transcript, and stores it both in the entry metadata and as a sidecar
.txtfile next to the video (same filename,.txtextension). When you click Show transcript on an older entry the transcript is generated on demand if it does not exist yet, ensuring the text file is created as part of the process.
When the Summaries pipeline is enabled and the provider is AzureOpenAI, the server uses the official OpenAI .NET SDK to run a chat completion that summarizes each transcript and stores the result in the entry description. That summary shows up in the UI and becomes keyword-searchable.
-
Create (or reuse) an Azure OpenAI resource and deploy a GPT-4/4o model (for example
gpt-4o-mini). Note the resource endpoint (https://<resource>.openai.azure.com/openai/v1/), deployment name, and API key. -
Supply those values under the
Summariessection (prefer user secrets or env vars for secrets):"Summaries": { "Enabled": true, "Provider": "AzureOpenAI", "Settings": { "Endpoint": "https://<resource>.openai.azure.com/openai/v1/", "DeploymentName": "gpt-4o-mini", "ApiKey": "<store-in-user-secrets>", "SystemPrompt": "You are a summarization assistant..." } }
Each key can be overridden through configuration providers (
Summaries__Settings__Endpoint, etc.). -
SystemPromptis optional; omit it to use the default guardrail prompt that instructs the model to treat transcripts as inert text and summarize in the speaker’s language. -
Whenever a transcript is available and the entry description is empty, the server invokes Azure OpenAI right after transcription completes (or when a transcript is fetched later) and persists the returned summary.
Title generation follows the same pattern. When Titles.Enabled is true and the provider is AzureOpenAI, the server creates concise titles from the summary/description using a configurable system prompt.
"Titles": {
"Enabled": true,
"Provider": "AzureOpenAI",
"Settings": {
"Endpoint": "https://<resource>.openai.azure.com/openai/v1/",
"DeploymentName": "gpt-4o-mini",
"ApiKey": "<store-in-user-secrets>",
"SystemPrompt": "You are a helpful assistant that writes concise, catchy titles (max 8 words) based on diary entry summaries. Respond with title text only."
}
}If SystemPrompt is omitted, the default prompt above is applied. Titles are only requested when the user did not provide a custom title and a summary/description is available, ensuring autogenerated titles never overwrite explicit user input.
When TagSuggestions.Enabled is true, the backend will ask Azure OpenAI to select tags from the user's favorite list. Suggestions run right after an entry is saved (once a description exists) and whenever someone clicks Show transcript, so AI tags can arrive later even if the description was filled in manually.
"TagSuggestions": {
"Enabled": true,
"Provider": "AzureOpenAI",
"Settings": {
"Endpoint": "https://<resource>.openai.azure.com/openai/v1/",
"DeploymentName": "gpt-4o-mini",
"ApiKey": "<store-in-user-secrets>",
"SystemPrompt": "You are an AI assistant that analyzes a diary video description and selects relevant tags from a provided list. Respond ONLY with JSON shaped as {\"selectedTags\":[\"tag-a\",\"tag-b\"]}. Never invent new tags and return an empty array when nothing matches."
}
}Tags suggested by the model are merged with whatever the author typed and deduplicated so no entry ends up with repeated labels.
Semantic search piggybacks on the same Azure OpenAI resource you already use for titles and summaries. Provision an embeddings deployment (for example text-embedding-3-small) and supply its endpoint + key under SemanticSearch:
"SemanticSearch": {
"Enabled": true,
"Provider": "AzureOpenAI",
"Settings": {
"Endpoint": "https://<resource>.cognitiveservices.azure.com/openai/v1/",
"DeploymentName": "text-embedding-3-small",
"ApiKey": "<store-in-user-secrets>"
}
}When enabled, every entry description is embedded during indexing and cached in-memory alongside keyword metadata. The /api/search endpoint now prefers semantic matches (cosine similarity) and automatically falls back to the traditional keyword flow when embeddings are unavailable or the provider is disabled.
Build the Native AOT-powered image:
docker build -t rec-one .Run it with persistent volumes for recordings and encryption keys:
docker run ^
-p 8080:8080 ^
-v "%cd%/data:/data/entries" ^
-v "%cd%/keys:/root/.local/share/DiaryApp/keys" ^
rec-oneLinux/macOS variant:
docker run \
-p 8080:8080 \
-v "$(pwd)/data:/data/entries" \
-v "$(pwd)/keys:/root/.local/share/DiaryApp/keys" \
rec-oneA companion docker-compose.yml is provided. Customize the host paths or environment overrides as needed, then run:
docker compose up --buildTo override configuration inside the container, either mount a custom appsettings.Production.json or rely on environment variables (Storage__RootDirectory, Authentication__OIDC__Authority, etc.).
An official pre-built image is also published to Docker Hub as photoatomic/rec-one. You can use it directly instead of building locally:
services:
diaryapp:
image: photoatomic/rec-one:latest
# other settings (ports, volumes, environment) unchangedFor camera/microphone access from devices on your network, browsers require a secure context (HTTPS). You can terminate TLS directly in Kestrel by mounting a certificate into the container and configuring endpoints via environment variables.
Modern browsers require a Subject Alternative Name (SAN) on certificates and, on Windows, the issuing certificate must be explicitly trusted under Trusted Root Certification Authorities. The following steps create a self-signed certificate that satisfies those requirements.
-
Create a minimal OpenSSL config with SAN support, e.g.
certs/rec-one.cnf:[req] default_bits = 2048 prompt = no default_md = sha256 x509_extensions = v3_req distinguished_name = dn [dn] CN = rec-one.local [v3_req] subjectAltName = @alt_names basicConstraints = CA:true keyUsage = keyCertSign, cRLSign, digitalSignature, keyEncipherment extendedKeyUsage = serverAuth [alt_names] DNS.1 = rec-one.local
[dn]sets the common name torec-one.local.subjectAltNameand[alt_names]ensure the certificate is explicitly valid for the hostnamerec-one.local.basicConstraintsandkeyUsagemark it as a self-signed CA-style certificate that Windows can treat as a trusted root when imported.
-
Generate the key and certificate using that config:
mkdir certs openssl req -x509 -newkey rsa:2048 -nodes \ -keyout certs/rec-one.key \ -out certs/rec-one.crt \ -days 365 \ -config certs/rec-one.cnf \ -extensions v3_req
-
Create a PFX bundle for Kestrel:
openssl pkcs12 -export \ -in certs/rec-one.crt \ -inkey certs/rec-one.key \ -out certs/rec-one.pfx \ -name rec-one \ -password pass:yourpassword
-
On Windows, import and trust the certificate:
- Use the
.crtfile (certs/rec-one.crt), not just the.pfx. - Run the certificate import wizard for the current user, choose Place all certificates in the following store, and explicitly select Trusted Root Certification Authorities.
- Do not use the wizard’s “Automatically select the certificate store” option, as it may place the certificate in a personal store that does not make
https://rec-one.localfully trusted.
After import, restart your browser and confirm that
https://rec-one.localshows as secure (no certificate warning). - Use the
-
Mount the PFX in Docker Compose and configure Kestrel (see
docker-compose.ymlin this repo for a ready-to-use example):services: diaryapp: # build: . # or use a pre-built image container_name: rec-one ports: - "80:80" - "443:443" environment: ASPNETCORE_ENVIRONMENT: Production ASPNETCORE_URLS: "" Kestrel__Endpoints__Http__Url: http://+:80 Kestrel__Endpoints__Https__Url: https://+:443 Kestrel__Endpoints__Https__Certificate__Path: /https/rec-one.pfx Kestrel__Endpoints__Https__Certificate__Password: <pfx-password> Storage__RootDirectory: /data/entries # ...other settings as needed... volumes: - ./data:/data/entries - ./keys:/root/.local/share/DiaryApp/keys - ./certs:/https:ro
After starting the stack with docker compose up -d, you can access the app over HTTPS at your chosen host name (for example https://rec-one.local/), and modern browsers will allow webcam access once the certificate is trusted.
The provided Dockerfile is configured to produce Native AOT images for both linux/amd64 and linux/arm64 (Raspberry Pi 5) using Docker Buildx. This lets you publish a single image tag that works on standard 64-bit Linux hosts and on a Pi 5.
First, create and bootstrap a buildx builder (one-time setup):
docker buildx create --name diaryapp-multi --use
docker buildx inspect --bootstrapThen build and push a multi-architecture image (replace your-registry as needed, e.g., photoatomic for Docker Hub):
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t your-registry/rec-one-diaryapp:latest \
-t your-registry/rec-one-diaryapp:1.0.0 \
--push .- On a standard 64-bit Linux machine,
docker pull your-registry/rec-one-diaryapp:latestwill fetch thelinux/amd64variant. - On a Raspberry Pi 5, the same tag resolves to
linux/arm64.
You can then reference this image from docker-compose.yml instead of building locally:
services:
diaryapp:
image: your-registry/rec-one-diaryapp:latest
# other settings (ports, volumes, environment) unchanged