Store chat images separately; JPEG for large PNGs; gzip/brotli responses
Images of messages (pasted images, screenshots, generated images) were base64 inside the message JSON, so opening a chat downloaded all of them at once (66 MB for a chat with 120 screenshots). They now live in a `blobs` table, addressed by SHA-256; messages keep a reference (blob, size). The UI loads them from /api/blobs/<hash> lazily with an immutable cache header, the runner fills the bytes in before model requests, JSON exports stay self-contained. - Store::add_message moves inline images out; large opaque PNGs become JPEG (q85) when smaller. Older chats are converted once at start-up in the background, unused images are removed (also after deleting a chat) and the database is vacuumed when that frees 32 MB or more. - Responses are compressed with gzip/brotli (not images, SSE streams or binary downloads). Example (copy of production): chat load 66 MB -> 1.5 MB (370 KB brotli), database 90 MB -> 27 MB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6d656a355d
commit
e8e455b779
27 files changed
+474
-31
No files matched your search
+16
-2
@@ -10,11 +10,25 @@ pub enum Role {
|
||||
Assistant,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ImageData {
|
||||
pub media_type: String,
|
||||
/// Base64-encoded bytes.
|
||||
/// Base64-encoded bytes. Empty when the image is stored separately (`blob`).
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub data: String,
|
||||
/// Server only: SHA-256 of the bytes in the image store; the UI loads
|
||||
/// /api/blobs/<blob> and the server fills `data` in before a model request.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub blob: Option<String>,
|
||||
/// Size in bytes of a stored image (shown in the UI without fetching it).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub size: Option<u64>,
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
pub fn new(media_type: impl Into<String>, data: String) -> Self {
|
||||
Self { media_type: media_type.into(), data, blob: None, size: None }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
|
||||
Reference in new issue
Block a user