AIService / database_schema.md
Gaykar's picture
updated db schema
fa63a26
|
Raw History Blame Contribute Delete
3.59 kB
# Database Schema β€” Voice Chat (STT/TTS) System
## 1. `users`
| Column | Type | Notes |
|---|---|---|
| id | PK | |
---
## 2. `threads`
Represents a conversation session β€” groups multiple messages together.
| Column | Type | Notes |
|---|---|---|
| id | PK | internal DB primary key |
| client_thread_id | UUID | client-generated thread identifier β€” `Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True, nullable=False)`. This is what the client sends and what's used to build the `thread_abc`-equivalent folder name in the S3 path below β€” decoupled from `id` so the internal PK never has to be exposed to or generated by the client. |
| user_id | FK β†’ users.id | |
| created_at | timestamp | session start |
---
## 3. `messages`
One row per audio event (STT input turn or TTS output turn). The `type` column determines which direction the conversion runs.
| Column | Type | Notes |
|---|---|---|
| id | PK | internal DB primary key |
| client_msg_id | UUID | client-generated message identifier β€” `Column(UUID(as_uuid=True), default=uuid.uuid4, unique=True, index=True, nullable=False)`. The client generates this *before* uploading, uses it as the S3 filename (`msg_<client_msg_id>.wav`), and sends it along with the DB write β€” this is what enables direct client β†’ S3 presigned-URL uploads, bypassing your backend for the audio bytes entirely. |
| thread_id | FK β†’ threads.id | internal join β€” resolved server-side by looking up the row matching the client's `client_thread_id` |
| user_id | FK β†’ users.id | denormalized β€” avoids a join for ownership checks |
| type | enum: `stt` \| `tts` | direction of conversion |
| audio_storage_path | text | S3/Supabase path β€” built from `client_thread_id` + `client_msg_id`, see layout below |
| text_content | text | transcript (STT output) or source text (TTS input) |
| created_at | timestamp | for ordering turns within a thread |
**How `type` works:**
- `type = stt` β†’ `audio_storage_path` is the input, `text_content` is the model output (transcript)
- `type = tts` β†’ `text_content` is the input, `audio_storage_path` is the model output (generated audio)
**Why two IDs per row (`id` vs. `client_*_id`):**
`id` is the internal relational PK β€” small, sequential, used for all server-side joins and never exposed to or generated by the client. `client_thread_id` / `client_msg_id` are what the client actually generates, sends, and uses to name S3 objects β€” this lets the client build a valid, unique S3 path and request a presigned upload URL *before* your backend has even created a DB row, without ever needing to know or influence the internal PK.
---
## S3 / Supabase Bucket Layout
```
bucket/
└── user_id_123/
β”œβ”€β”€ stt/
β”‚ └── {client_thread_id}/
β”‚ └── msg_{client_msg_id}.wav ← input (spoken query)
β”‚
└── tts/
└── {client_thread_id}/
└── msg_{client_msg_id}.wav ← output (spoken response)
```
- The folder/file names in this path are the client-generated `client_thread_id` and `client_msg_id` β€” not the internal `id` columns.
- `audio_storage_path` in the `messages` table stores this exact path, so it can be used directly for S3 reads/writes without reconstructing it.
- Split by `stt/` and `tts/` at the top level (not by thread) so each pipeline can have independent lifecycle/retention rules.
- `{client_thread_id}/` nested underneath keeps all audio for one conversation together for easy per-thread cleanup or export.
![alt text](image-1.png)