1050 lines
20 KiB
Markdown
1050 lines
20 KiB
Markdown
# MeetVault Desktop
|
||
|
||
MeetVault Desktop is the local desktop client for MeetVault.
|
||
|
||
Its main purpose is to:
|
||
|
||
- Record meeting audio/video
|
||
- Capture microphone and system audio
|
||
- Transcribe speech locally using Whisper
|
||
- Detect and separate speakers in real time using diart
|
||
- Show a real-time transcription overlay on top of other applications
|
||
- Allow users to rename detected speakers during the meeting
|
||
- Upload recordings to SeaweedFS
|
||
- Send the final transcript to the MeetVault backend
|
||
- Receive generated summaries, action items, and reminders from the backend
|
||
|
||
The desktop application should continue to work locally during a meeting even if the backend is temporarily unavailable.
|
||
|
||
---
|
||
|
||
# Core Desktop Architecture
|
||
|
||
```text
|
||
MeetVault Desktop
|
||
|
||
┌─────────────────┐
|
||
│ Desktop UI │
|
||
│ React + Tauri │
|
||
└────────┬────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────┐
|
||
│ Audio Engine │
|
||
│ │
|
||
│ Microphone capture │
|
||
│ System audio capture│
|
||
│ Audio mixing │
|
||
└─────────┬───────────┘
|
||
│
|
||
┌───────────────┴───────────────┐
|
||
│ │
|
||
▼ ▼
|
||
┌───────────────┐ ┌────────────────┐
|
||
│ Whisper │ │ diart │
|
||
│ │ │ │
|
||
│ transcription │ │ diarization │
|
||
│ + timestamps │ │ speaker labels │
|
||
└───────┬───────┘ └───────┬────────┘
|
||
│ │
|
||
└──────────────┬───────────────┘
|
||
▼
|
||
┌────────────────────┐
|
||
│ Segment Merger │
|
||
│ │
|
||
│ text + timestamps │
|
||
│ speaker timeline │
|
||
└─────────┬──────────┘
|
||
│
|
||
┌─────────────┴──────────────┐
|
||
│ │
|
||
▼ ▼
|
||
┌─────────────────┐ ┌──────────────────┐
|
||
│ Live Transcript │ │ Realtime Overlay │
|
||
│ Window │ │ Always-on-top │
|
||
└─────────────────┘ └──────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
# Recommended Desktop Stack
|
||
|
||
```text
|
||
Desktop shell
|
||
Tauri
|
||
|
||
UI
|
||
React + TypeScript
|
||
|
||
Native/local processing
|
||
Rust where required
|
||
|
||
Transcription
|
||
Whisper / whisper.cpp
|
||
|
||
Speaker diarization
|
||
diart
|
||
|
||
Backend API
|
||
MeetVault backend
|
||
|
||
Structured data
|
||
PostgreSQL
|
||
|
||
Media storage
|
||
SeaweedFS
|
||
```
|
||
|
||
Tauri is recommended because the application needs:
|
||
|
||
- Multiple windows
|
||
- Transparent overlay windows
|
||
- Always-on-top behavior
|
||
- Native audio access
|
||
- Local file access
|
||
- Background processes
|
||
- System tray support
|
||
- Global shortcuts
|
||
- Lower memory usage than a full Electron application
|
||
|
||
---
|
||
|
||
# Real-Time Audio Pipeline
|
||
|
||
The application processes meeting audio locally.
|
||
|
||
```text
|
||
Microphone
|
||
│
|
||
├────────────────┐
|
||
│ │
|
||
System Audio │
|
||
│ │
|
||
└──────┬─────────┘
|
||
▼
|
||
Audio Mixer
|
||
│
|
||
▼
|
||
Audio Stream
|
||
│
|
||
┌─────┴─────┐
|
||
│ │
|
||
▼ ▼
|
||
Whisper diart
|
||
│ │
|
||
│ │
|
||
▼ ▼
|
||
text + speaker
|
||
timestamps segments
|
||
│ │
|
||
└─────┬─────┘
|
||
▼
|
||
Segment Merger
|
||
│
|
||
▼
|
||
Live Transcript Event
|
||
│
|
||
┌─────┴────────┐
|
||
│ │
|
||
▼ ▼
|
||
Main Window Overlay Window
|
||
```
|
||
|
||
---
|
||
|
||
# Whisper
|
||
|
||
Whisper is responsible for speech-to-text.
|
||
|
||
For the first version, a lightweight local runtime such as `whisper.cpp` is recommended.
|
||
|
||
The transcription engine should provide:
|
||
|
||
```text
|
||
text
|
||
start timestamp
|
||
end timestamp
|
||
confidence if available
|
||
partial/final state
|
||
```
|
||
|
||
Example:
|
||
|
||
```json
|
||
{
|
||
"start": 18.2,
|
||
"end": 21.7,
|
||
"text": "We should finish the API before Friday.",
|
||
"final": true
|
||
}
|
||
```
|
||
|
||
## Suggested Model Strategy
|
||
|
||
Default:
|
||
|
||
```text
|
||
Whisper small
|
||
```
|
||
|
||
Possible fallback for lower-end hardware:
|
||
|
||
```text
|
||
Whisper base
|
||
```
|
||
|
||
Possible higher-accuracy option:
|
||
|
||
```text
|
||
Whisper medium
|
||
```
|
||
|
||
The selected model should be configurable in Settings.
|
||
|
||
The goal is to balance:
|
||
|
||
```text
|
||
accuracy
|
||
latency
|
||
CPU/GPU usage
|
||
memory usage
|
||
```
|
||
|
||
---
|
||
|
||
# diart
|
||
|
||
diart is responsible for real-time speaker diarization.
|
||
|
||
It does not need to know a person's real identity.
|
||
|
||
It only needs to produce stable speaker labels such as:
|
||
|
||
```text
|
||
Speaker 1
|
||
Speaker 2
|
||
Speaker 3
|
||
```
|
||
|
||
Example diarization result:
|
||
|
||
```json
|
||
{
|
||
"speaker": "speaker_1",
|
||
"start": 18.0,
|
||
"end": 22.0
|
||
}
|
||
```
|
||
|
||
The application combines the diart result with the Whisper result using timestamps.
|
||
|
||
---
|
||
|
||
# Transcript + Speaker Merge
|
||
|
||
Whisper may produce:
|
||
|
||
```text
|
||
18.2 - 21.7
|
||
|
||
"We should finish the API before Friday."
|
||
```
|
||
|
||
diart may produce:
|
||
|
||
```text
|
||
18.0 - 22.0
|
||
|
||
speaker_1
|
||
```
|
||
|
||
The merger produces:
|
||
|
||
```json
|
||
{
|
||
"speaker_id": "speaker_1",
|
||
"speaker_name": "Speaker 1",
|
||
"start": 18.2,
|
||
"end": 21.7,
|
||
"text": "We should finish the API before Friday."
|
||
}
|
||
```
|
||
|
||
This becomes the core live transcript segment.
|
||
|
||
---
|
||
|
||
# Speaker Rename
|
||
|
||
Detected speakers initially appear as:
|
||
|
||
```text
|
||
Speaker 1
|
||
Speaker 2
|
||
Speaker 3
|
||
```
|
||
|
||
The user can rename them at any time.
|
||
|
||
Example:
|
||
|
||
```text
|
||
Speaker 1 → Alice
|
||
Speaker 2 → Bob
|
||
```
|
||
|
||
The underlying diarization ID does not change.
|
||
|
||
```text
|
||
speaker_1
|
||
↓
|
||
display name
|
||
↓
|
||
Alice
|
||
```
|
||
|
||
Recommended local mapping:
|
||
|
||
```json
|
||
{
|
||
"speaker_1": "Alice",
|
||
"speaker_2": "Bob"
|
||
}
|
||
```
|
||
|
||
Existing transcript lines and future overlay lines should immediately use the new display name.
|
||
|
||
---
|
||
|
||
# Real-Time Overlay
|
||
|
||
The overlay is a dedicated transparent Tauri window.
|
||
|
||
It should be:
|
||
|
||
- Always on top
|
||
- Transparent or semi-transparent
|
||
- Resizable
|
||
- Draggable
|
||
- Lockable
|
||
- Optionally click-through
|
||
- Hotkey controlled
|
||
- Able to show only the latest few transcript lines
|
||
|
||
Example:
|
||
|
||
```text
|
||
┌──────────────────────────────────────────────┐
|
||
│ Alice │
|
||
│ We should finish the API before Friday. │
|
||
│ │
|
||
│ Bob │
|
||
│ I'll prepare the deployment plan tonight. │
|
||
└──────────────────────────────────────────────┘
|
||
```
|
||
|
||
The overlay should update continuously while the meeting is running.
|
||
|
||
---
|
||
|
||
# Overlay Update Model
|
||
|
||
The desktop app should distinguish between:
|
||
|
||
```text
|
||
partial transcript
|
||
final transcript
|
||
```
|
||
|
||
Example:
|
||
|
||
```text
|
||
Speaker 1:
|
||
We should finish the...
|
||
```
|
||
|
||
updates to:
|
||
|
||
```text
|
||
Speaker 1:
|
||
We should finish the API before Friday.
|
||
```
|
||
|
||
A partial line may be visually distinguished from finalized text.
|
||
|
||
Recommended behavior:
|
||
|
||
```text
|
||
partial
|
||
→ replace current live line
|
||
|
||
final
|
||
→ commit transcript segment
|
||
→ append to transcript history
|
||
```
|
||
|
||
---
|
||
|
||
# Main UI Screens
|
||
|
||
## Dashboard
|
||
|
||
Purpose:
|
||
|
||
- Start a new meeting
|
||
- View recent meetings
|
||
- Check local transcription state
|
||
- Check SeaweedFS/backend connectivity
|
||
- Open Settings
|
||
|
||
Main actions:
|
||
|
||
```text
|
||
Start Meeting
|
||
Open Meeting
|
||
Preview Overlay
|
||
Settings
|
||
```
|
||
|
||
---
|
||
|
||
# Live Meeting Screen
|
||
|
||
The Live Meeting screen is the main control surface while recording.
|
||
|
||
It should show:
|
||
|
||
- Recording state
|
||
- Meeting timer
|
||
- Live transcript
|
||
- Speaker labels
|
||
- Rename controls
|
||
- Microphone state
|
||
- System audio state
|
||
- Whisper model status
|
||
- Diarization status
|
||
- Overlay toggle
|
||
- Pause
|
||
- Finish meeting
|
||
|
||
Example:
|
||
|
||
```text
|
||
Weekly Product Sync REC 00:18:42
|
||
|
||
Live transcript
|
||
|
||
Alice
|
||
We should finalize the API contract.
|
||
|
||
Bob
|
||
I'll update the database migration.
|
||
|
||
Alice
|
||
Great. I'll prepare the release notes.
|
||
|
||
|
||
Speakers
|
||
|
||
Alice Rename
|
||
Bob Rename
|
||
|
||
|
||
Microphone ON
|
||
System Audio ON
|
||
Whisper Healthy
|
||
diart Healthy
|
||
|
||
[Pause] [Show Overlay] [Finish Meeting]
|
||
```
|
||
|
||
---
|
||
|
||
# Speaker Management
|
||
|
||
The speaker panel should display the diarization speakers detected in the current meeting.
|
||
|
||
Example:
|
||
|
||
```text
|
||
speaker_1 Alice
|
||
speaker_2 Bob
|
||
speaker_3 Speaker 3
|
||
```
|
||
|
||
The user may rename a speaker at any time.
|
||
|
||
The rename should affect:
|
||
|
||
- Current live transcript
|
||
- Overlay
|
||
- Final transcript
|
||
- Meeting review
|
||
|
||
---
|
||
|
||
# Meeting Details Screen
|
||
|
||
After a meeting ends, the Meeting Details screen provides:
|
||
|
||
- Video/audio playback
|
||
- Meeting information
|
||
- Transcript
|
||
- Speaker names
|
||
- Summary
|
||
- Action items
|
||
- Reminders
|
||
- Export options
|
||
|
||
Example layout:
|
||
|
||
```text
|
||
Weekly Product Sync
|
||
Sep 27, 2026 • 48 min
|
||
|
||
┌──────────────────────────┐
|
||
│ Video Player │
|
||
└──────────────────────────┘
|
||
|
||
Transcript | Summary | Action Items | Files
|
||
|
||
Alice 00:18:41
|
||
We should finalize the API contract.
|
||
|
||
Bob 00:18:49
|
||
I'll update the database migration.
|
||
```
|
||
|
||
The transcript timestamp should be clickable so playback can jump to that position.
|
||
|
||
---
|
||
|
||
# Settings
|
||
|
||
Settings should include the following sections.
|
||
|
||
## General
|
||
|
||
```text
|
||
Launch at startup
|
||
Keep running in tray
|
||
Theme
|
||
Global hotkey
|
||
```
|
||
|
||
## Audio
|
||
|
||
```text
|
||
Microphone device
|
||
System audio device
|
||
Input level
|
||
System audio level
|
||
Audio monitoring
|
||
```
|
||
|
||
## Transcription
|
||
|
||
```text
|
||
Whisper model
|
||
Compute device
|
||
Language
|
||
Chunk size
|
||
Voice activity detection
|
||
```
|
||
|
||
Suggested model options:
|
||
|
||
```text
|
||
base
|
||
small
|
||
medium
|
||
```
|
||
|
||
## Diarization
|
||
|
||
```text
|
||
Enable speaker diarization
|
||
Maximum expected speakers
|
||
Speaker clustering sensitivity
|
||
```
|
||
|
||
## Overlay
|
||
|
||
```text
|
||
Enable overlay
|
||
Always on top
|
||
Click-through mode
|
||
Opacity
|
||
Font size
|
||
Maximum visible lines
|
||
Show speaker labels
|
||
Screen position
|
||
```
|
||
|
||
## Storage & Sync
|
||
|
||
```text
|
||
Local recording folder
|
||
Automatic upload
|
||
Upload after meeting
|
||
Delete local media after successful upload
|
||
Backend connection
|
||
SeaweedFS upload status
|
||
```
|
||
|
||
## Account
|
||
|
||
```text
|
||
Signed-in user
|
||
Subscription
|
||
Sign out
|
||
```
|
||
|
||
---
|
||
|
||
# Local Meeting Session Model
|
||
|
||
During a live meeting, the desktop application should maintain a local session object.
|
||
|
||
Example:
|
||
|
||
```json
|
||
{
|
||
"meeting_id": "uuid",
|
||
"started_at": "2026-09-27T09:00:00Z",
|
||
"status": "recording",
|
||
|
||
"speakers": {
|
||
"speaker_1": "Alice",
|
||
"speaker_2": "Bob"
|
||
},
|
||
|
||
"segments": [
|
||
{
|
||
"id": "segment-1",
|
||
"speaker_id": "speaker_1",
|
||
"start": 18.2,
|
||
"end": 21.7,
|
||
"text": "We should finish the API before Friday."
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
This state should be stored locally during recording so a temporary network failure does not lose the transcript.
|
||
|
||
---
|
||
|
||
# Transcript Segment Model
|
||
|
||
A transcript should internally be represented as segments rather than only one large text string.
|
||
|
||
Recommended structure:
|
||
|
||
```json
|
||
{
|
||
"id": "segment-uuid",
|
||
"speaker_id": "speaker_1",
|
||
"speaker_name": "Alice",
|
||
"start": 18.2,
|
||
"end": 21.7,
|
||
"text": "We should finish the API before Friday.",
|
||
"final": true
|
||
}
|
||
```
|
||
|
||
Benefits:
|
||
|
||
- Real-time rendering
|
||
- Speaker rename
|
||
- Timestamp navigation
|
||
- Transcript editing
|
||
- Summary generation
|
||
- Export
|
||
- Search
|
||
|
||
---
|
||
|
||
# Recording Flow
|
||
|
||
```text
|
||
User clicks Start Meeting
|
||
│
|
||
▼
|
||
Create local meeting session
|
||
│
|
||
▼
|
||
Start microphone capture
|
||
│
|
||
├──── Start system audio capture
|
||
│
|
||
▼
|
||
Start recording
|
||
│
|
||
├──── Start Whisper
|
||
│
|
||
└──── Start diart
|
||
│
|
||
▼
|
||
Live transcript events
|
||
│
|
||
┌──────┴──────┐
|
||
▼ ▼
|
||
Main UI Overlay
|
||
```
|
||
|
||
---
|
||
|
||
# Meeting Finish Flow
|
||
|
||
```text
|
||
User clicks Finish Meeting
|
||
│
|
||
▼
|
||
Stop audio capture
|
||
│
|
||
▼
|
||
Finalize Whisper segments
|
||
│
|
||
▼
|
||
Finalize speaker mapping
|
||
│
|
||
▼
|
||
Save local transcript
|
||
│
|
||
├──── upload media ─────► SeaweedFS
|
||
│
|
||
└──── send transcript ──► Backend API
|
||
│
|
||
▼
|
||
PostgreSQL
|
||
│
|
||
▼
|
||
processing_jobs
|
||
│
|
||
┌───────┴────────┐
|
||
▼ ▼
|
||
Summarizer Action Items
|
||
│
|
||
▼
|
||
Reminders
|
||
```
|
||
|
||
---
|
||
|
||
# Upload Strategy
|
||
|
||
Large media should not pass through the backend API.
|
||
|
||
Recommended:
|
||
|
||
```text
|
||
Desktop
|
||
│
|
||
│ request presigned URL
|
||
▼
|
||
Backend
|
||
│
|
||
│ returns temporary upload URL
|
||
▼
|
||
Desktop ───────────────────────► SeaweedFS
|
||
media file
|
||
```
|
||
|
||
The transcript is small and can be sent directly through the backend API.
|
||
|
||
```text
|
||
Desktop
|
||
│
|
||
│ transcript JSON
|
||
▼
|
||
Backend API
|
||
│
|
||
▼
|
||
PostgreSQL
|
||
```
|
||
|
||
---
|
||
|
||
# Offline / Network Failure Behavior
|
||
|
||
The desktop application should not require continuous backend connectivity during a meeting.
|
||
|
||
If the backend becomes unavailable:
|
||
|
||
```text
|
||
recording continues
|
||
transcription continues
|
||
diarization continues
|
||
overlay continues
|
||
local saving continues
|
||
```
|
||
|
||
After the connection returns:
|
||
|
||
```text
|
||
upload recording
|
||
send transcript
|
||
sync meeting metadata
|
||
```
|
||
|
||
This is an important design requirement for MeetVault Desktop.
|
||
|
||
---
|
||
|
||
# Background Components
|
||
|
||
The desktop application can be separated into these logical modules:
|
||
|
||
```text
|
||
MeetVault Desktop
|
||
|
||
├── UI
|
||
│ ├── Dashboard
|
||
│ ├── Live Meeting
|
||
│ ├── Meeting Details
|
||
│ └── Settings
|
||
│
|
||
├── Overlay
|
||
│ └── Realtime subtitle window
|
||
│
|
||
├── Audio Engine
|
||
│ ├── Microphone
|
||
│ ├── System audio
|
||
│ └── Mixer
|
||
│
|
||
├── Speech Engine
|
||
│ ├── Whisper
|
||
│ └── diart
|
||
│
|
||
├── Transcript Engine
|
||
│ ├── Segment merger
|
||
│ ├── Speaker mapping
|
||
│ └── Transcript state
|
||
│
|
||
├── Recorder
|
||
│ └── Media recording
|
||
│
|
||
├── Local Storage
|
||
│ └── Session recovery/cache
|
||
│
|
||
└── Sync Client
|
||
├── Backend API
|
||
└── SeaweedFS upload
|
||
```
|
||
|
||
---
|
||
|
||
# Realtime Event Flow
|
||
|
||
Internally, the UI should consume events rather than directly controlling the models.
|
||
|
||
Example events:
|
||
|
||
```text
|
||
audio.started
|
||
audio.stopped
|
||
|
||
transcript.partial
|
||
transcript.final
|
||
|
||
speaker.detected
|
||
speaker.changed
|
||
|
||
speaker.renamed
|
||
|
||
recording.started
|
||
recording.stopped
|
||
|
||
upload.started
|
||
upload.progress
|
||
upload.completed
|
||
upload.failed
|
||
```
|
||
|
||
Example event:
|
||
|
||
```json
|
||
{
|
||
"type": "transcript.final",
|
||
"payload": {
|
||
"speaker_id": "speaker_1",
|
||
"start": 18.2,
|
||
"end": 21.7,
|
||
"text": "We should finish the API before Friday."
|
||
}
|
||
}
|
||
```
|
||
|
||
Both the main transcript screen and overlay can subscribe to the same event stream.
|
||
|
||
---
|
||
|
||
# Realtime Performance Goals
|
||
|
||
Initial targets:
|
||
|
||
```text
|
||
Overlay update latency
|
||
< 2 seconds
|
||
|
||
Partial transcript refresh
|
||
~ 0.5–1.5 seconds
|
||
|
||
Final segment delay
|
||
~ 1–3 seconds
|
||
|
||
Overlay frame impact
|
||
Minimal
|
||
|
||
Meeting duration
|
||
Several hours without restart
|
||
```
|
||
|
||
Actual latency depends on:
|
||
|
||
- Whisper model
|
||
- CPU/GPU
|
||
- Audio chunk size
|
||
- Number of speakers
|
||
- diart processing speed
|
||
|
||
---
|
||
|
||
# Speaker Diarization Limitations
|
||
|
||
Speaker diarization is not perfect.
|
||
|
||
Expected difficult cases include:
|
||
|
||
- Two people speaking simultaneously
|
||
- Very similar voices
|
||
- Poor microphone quality
|
||
- Strong background noise
|
||
- Echo
|
||
- Speakers far from the microphone
|
||
- Very short speech segments
|
||
|
||
The application should allow users to correct speaker names and, later, reassign individual transcript segments if necessary.
|
||
|
||
No biometric speaker identification is required.
|
||
|
||
MeetVault only needs:
|
||
|
||
```text
|
||
who spoke when
|
||
```
|
||
|
||
not:
|
||
|
||
```text
|
||
who is this person in real life
|
||
```
|
||
|
||
---
|
||
|
||
# Security
|
||
|
||
Permanent SeaweedFS credentials must never be stored in the renderer/frontend.
|
||
|
||
The desktop app should use:
|
||
|
||
```text
|
||
Backend authentication
|
||
+
|
||
temporary presigned upload URLs
|
||
```
|
||
|
||
Sensitive local files should be stored only in the configured MeetVault application data directory.
|
||
|
||
Authentication tokens should use the operating system's secure credential storage when possible.
|
||
|
||
---
|
||
|
||
# MVP Scope
|
||
|
||
The first desktop release should focus on:
|
||
|
||
- Start/stop meeting recording
|
||
- Microphone capture
|
||
- System audio capture
|
||
- Local Whisper transcription
|
||
- Real-time diart speaker separation
|
||
- Speaker 1 / Speaker 2 labels
|
||
- Rename speakers
|
||
- Real-time overlay
|
||
- Local transcript storage
|
||
- Meeting playback
|
||
- Transcript review
|
||
- Upload recording to SeaweedFS
|
||
- Send transcript to backend
|
||
- Receive generated summary
|
||
- View action items
|
||
- View reminders
|
||
|
||
Not required for the first release:
|
||
|
||
- Speaker biometric identification
|
||
- Automatic real-name recognition
|
||
- Cloud transcription
|
||
- Kafka
|
||
- Elasticsearch
|
||
- Kubernetes
|
||
- Vector database
|
||
- Complex microservice architecture
|
||
|
||
---
|
||
|
||
# Final Desktop Architecture
|
||
|
||
```text
|
||
MeetVault Desktop
|
||
|
||
Audio Sources
|
||
/ \
|
||
Microphone System Audio
|
||
\ /
|
||
\ /
|
||
Audio Mixer
|
||
│
|
||
┌─────────┴─────────┐
|
||
│ │
|
||
▼ ▼
|
||
Whisper diart
|
||
│ │
|
||
transcript speakers
|
||
│ │
|
||
└─────────┬─────────┘
|
||
▼
|
||
Segment Merger
|
||
│
|
||
┌─────────────┴──────────────┐
|
||
│ │
|
||
▼ ▼
|
||
Live Meeting UI Realtime Overlay
|
||
│
|
||
▼
|
||
Local Session Store
|
||
│
|
||
meeting completes
|
||
│
|
||
┌───────┴────────┐
|
||
│ │
|
||
▼ ▼
|
||
SeaweedFS Backend API
|
||
media transcript
|
||
│
|
||
▼
|
||
PostgreSQL
|
||
│
|
||
▼
|
||
processing_jobs
|
||
│
|
||
┌────────┴─────────┐
|
||
▼ ▼
|
||
Summary Action Items
|
||
│
|
||
▼
|
||
Reminders
|
||
```
|