llms-py 2.0.20__py3-none-any.whl → 3.0.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llms/__init__.py +3 -1
- llms/db.py +359 -0
- llms/{ui/Analytics.mjs → extensions/analytics/ui/index.mjs} +254 -327
- llms/extensions/app/README.md +20 -0
- llms/extensions/app/__init__.py +588 -0
- llms/extensions/app/db.py +540 -0
- llms/{ui → extensions/app/ui}/Recents.mjs +99 -73
- llms/{ui/Sidebar.mjs → extensions/app/ui/index.mjs} +139 -68
- llms/extensions/app/ui/threadStore.mjs +440 -0
- llms/extensions/computer/README.md +96 -0
- llms/extensions/computer/__init__.py +59 -0
- llms/extensions/computer/base.py +80 -0
- llms/extensions/computer/bash.py +185 -0
- llms/extensions/computer/computer.py +523 -0
- llms/extensions/computer/edit.py +299 -0
- llms/extensions/computer/filesystem.py +542 -0
- llms/extensions/computer/platform.py +461 -0
- llms/extensions/computer/run.py +37 -0
- llms/extensions/core_tools/CALCULATOR.md +32 -0
- llms/extensions/core_tools/__init__.py +599 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closebrackets.js +201 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/closetag.js +185 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/continuelist.js +101 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchbrackets.js +160 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/matchtags.js +66 -0
- llms/extensions/core_tools/ui/codemirror/addon/edit/trailingspace.js +27 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/active-line.js +72 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/mark-selection.js +119 -0
- llms/extensions/core_tools/ui/codemirror/addon/selection/selection-pointer.js +98 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.css +344 -0
- llms/extensions/core_tools/ui/codemirror/codemirror.js +9884 -0
- llms/extensions/core_tools/ui/codemirror/doc/docs.css +225 -0
- llms/extensions/core_tools/ui/codemirror/doc/source_sans.woff +0 -0
- llms/extensions/core_tools/ui/codemirror/mode/clike/clike.js +942 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/index.html +118 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/javascript.js +962 -0
- llms/extensions/core_tools/ui/codemirror/mode/javascript/typescript.html +62 -0
- llms/extensions/core_tools/ui/codemirror/mode/python/python.js +402 -0
- llms/extensions/core_tools/ui/codemirror/theme/dracula.css +40 -0
- llms/extensions/core_tools/ui/codemirror/theme/mocha.css +135 -0
- llms/extensions/core_tools/ui/index.mjs +650 -0
- llms/extensions/gallery/README.md +61 -0
- llms/extensions/gallery/__init__.py +63 -0
- llms/extensions/gallery/db.py +243 -0
- llms/extensions/gallery/ui/index.mjs +482 -0
- llms/extensions/katex/README.md +39 -0
- llms/extensions/katex/__init__.py +6 -0
- llms/extensions/katex/ui/README.md +125 -0
- llms/extensions/katex/ui/contrib/auto-render.js +338 -0
- llms/extensions/katex/ui/contrib/auto-render.min.js +1 -0
- llms/extensions/katex/ui/contrib/auto-render.mjs +244 -0
- llms/extensions/katex/ui/contrib/copy-tex.js +127 -0
- llms/extensions/katex/ui/contrib/copy-tex.min.js +1 -0
- llms/extensions/katex/ui/contrib/copy-tex.mjs +105 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.js +109 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.min.js +1 -0
- llms/extensions/katex/ui/contrib/mathtex-script-type.mjs +24 -0
- llms/extensions/katex/ui/contrib/mhchem.js +3213 -0
- llms/extensions/katex/ui/contrib/mhchem.min.js +1 -0
- llms/extensions/katex/ui/contrib/mhchem.mjs +3109 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.js +887 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.min.js +1 -0
- llms/extensions/katex/ui/contrib/render-a11y-string.mjs +800 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Main-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Math-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Script-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.ttf +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff +0 -0
- llms/extensions/katex/ui/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- llms/extensions/katex/ui/index.mjs +92 -0
- llms/extensions/katex/ui/katex-swap.css +1230 -0
- llms/extensions/katex/ui/katex-swap.min.css +1 -0
- llms/extensions/katex/ui/katex.css +1230 -0
- llms/extensions/katex/ui/katex.js +19080 -0
- llms/extensions/katex/ui/katex.min.css +1 -0
- llms/extensions/katex/ui/katex.min.js +1 -0
- llms/extensions/katex/ui/katex.min.mjs +1 -0
- llms/extensions/katex/ui/katex.mjs +18547 -0
- llms/extensions/providers/__init__.py +22 -0
- llms/extensions/providers/anthropic.py +260 -0
- llms/extensions/providers/cerebras.py +36 -0
- llms/extensions/providers/chutes.py +153 -0
- llms/extensions/providers/google.py +559 -0
- llms/extensions/providers/nvidia.py +103 -0
- llms/extensions/providers/openai.py +154 -0
- llms/extensions/providers/openrouter.py +74 -0
- llms/extensions/providers/zai.py +182 -0
- llms/extensions/skills/LICENSE +202 -0
- llms/extensions/skills/__init__.py +130 -0
- llms/extensions/skills/errors.py +25 -0
- llms/extensions/skills/models.py +39 -0
- llms/extensions/skills/parser.py +178 -0
- llms/extensions/skills/ui/index.mjs +376 -0
- llms/extensions/skills/ui/skills/create-plan/SKILL.md +74 -0
- llms/extensions/skills/validator.py +177 -0
- llms/extensions/system_prompts/README.md +22 -0
- llms/extensions/system_prompts/__init__.py +45 -0
- llms/extensions/system_prompts/ui/index.mjs +276 -0
- llms/extensions/system_prompts/ui/prompts.json +1067 -0
- llms/extensions/tools/__init__.py +67 -0
- llms/extensions/tools/ui/index.mjs +837 -0
- llms/index.html +36 -62
- llms/llms.json +180 -879
- llms/main.py +4009 -912
- llms/providers-extra.json +394 -0
- llms/providers.json +1 -0
- llms/ui/App.mjs +176 -8
- llms/ui/ai.mjs +156 -20
- llms/ui/app.css +3768 -321
- llms/ui/ctx.mjs +459 -0
- llms/ui/index.mjs +131 -0
- llms/ui/lib/chart.js +14 -0
- llms/ui/lib/charts.mjs +16 -0
- llms/ui/lib/color.js +14 -0
- llms/ui/lib/highlight.min.mjs +1243 -0
- llms/ui/lib/idb.min.mjs +8 -0
- llms/ui/lib/marked.min.mjs +8 -0
- llms/ui/lib/servicestack-client.mjs +1 -0
- llms/ui/lib/servicestack-vue.mjs +37 -0
- llms/ui/lib/vue-router.min.mjs +6 -0
- llms/ui/lib/vue.min.mjs +13 -0
- llms/ui/lib/vue.mjs +18530 -0
- llms/ui/markdown.mjs +25 -14
- llms/ui/modules/chat/ChatBody.mjs +1156 -0
- llms/ui/{SettingsDialog.mjs → modules/chat/SettingsDialog.mjs} +74 -74
- llms/ui/modules/chat/index.mjs +995 -0
- llms/ui/modules/icons.mjs +46 -0
- llms/ui/modules/layout.mjs +271 -0
- llms/ui/modules/model-selector.mjs +811 -0
- llms/ui/tailwind.input.css +560 -78
- llms/ui/typography.css +54 -36
- llms/ui/utils.mjs +221 -92
- llms_py-3.0.18.dist-info/METADATA +49 -0
- llms_py-3.0.18.dist-info/RECORD +194 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/WHEEL +1 -1
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/licenses/LICENSE +1 -2
- llms/ui/Avatar.mjs +0 -28
- llms/ui/Brand.mjs +0 -34
- llms/ui/ChatPrompt.mjs +0 -443
- llms/ui/Main.mjs +0 -740
- llms/ui/ModelSelector.mjs +0 -60
- llms/ui/ProviderIcon.mjs +0 -29
- llms/ui/ProviderStatus.mjs +0 -105
- llms/ui/SignIn.mjs +0 -64
- llms/ui/SystemPromptEditor.mjs +0 -31
- llms/ui/SystemPromptSelector.mjs +0 -36
- llms/ui/Welcome.mjs +0 -8
- llms/ui/threadStore.mjs +0 -524
- llms/ui.json +0 -1069
- llms_py-2.0.20.dist-info/METADATA +0 -931
- llms_py-2.0.20.dist-info/RECORD +0 -36
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/entry_points.txt +0 -0
- {llms_py-2.0.20.dist-info → llms_py-3.0.18.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Gallery Extension
|
|
2
|
+
|
|
3
|
+
The Gallery extension intercepts all generated image, audio & file assets and uploaded files in `~/.llms/cache` file storage whose metadata is maintained in a SQLite database.
|
|
4
|
+
|
|
5
|
+
Dedicated UIs are available for quickly browsing and navigating or generated images / audio files with optimized UIs for viewing portrait, square and landscape images.
|
|
6
|
+
|
|
7
|
+
## Generated Asset Interception
|
|
8
|
+
|
|
9
|
+
The Gallery extension automatically monitors the creation of new cache entries. Whenever a file is saved to the LLMs cache (located at `~/.llms/cache`), the extension captures its metadata and stores it in the gallery database.
|
|
10
|
+
|
|
11
|
+
This includes:
|
|
12
|
+
- **Generated Images**: Images created by AI models (e.g., DALL-E, Stable Diffusion).
|
|
13
|
+
- **Generated Audio**: Audio files generated by text-to-speech or audio models.
|
|
14
|
+
- **Uploaded Files**: Any files uploaded through the UI.
|
|
15
|
+
|
|
16
|
+
All metadata is stored in a dedicated SQLite database located at `~/.llms/user/default/gallery/gallery.sqlite`, in the `media` table.
|
|
17
|
+
|
|
18
|
+
## User Interface
|
|
19
|
+
|
|
20
|
+
The Gallery UI provides a rich, interactive way to explore your generated assets. You can access it via the **Gallery** tab in the sidebar or by navigating to `/gallery`.
|
|
21
|
+
|
|
22
|
+
### Image Gallery
|
|
23
|
+
|
|
24
|
+
The image view offers a responsive grid layout optimized for different aspect ratios.
|
|
25
|
+
|
|
26
|
+
- **Filtering**:
|
|
27
|
+
- **By Format**: Easily switch between **Portrait**, **Square**, and **Landscape** views to see images in their best light.
|
|
28
|
+
- **Search**: Real-time search by prompt, model name, or other metadata.
|
|
29
|
+
- **Interactions**:
|
|
30
|
+
- **Lightbox**: Click any image to view it in full screen.
|
|
31
|
+
- **Details**: View comprehensive metadata including the prompt used, generation model, dimensions, file size, creation date, and generation cost.
|
|
32
|
+
- **Download**: extensive download options.
|
|
33
|
+
- **Remix**: Quickly re-use the prompt and settings of an existing image to generate a new one.
|
|
34
|
+
- **Delete**: Remove unwanted images from the gallery.
|
|
35
|
+
|
|
36
|
+
### Audio Gallery
|
|
37
|
+
|
|
38
|
+
The audio view presents a list layout designed for easy listening and management.
|
|
39
|
+
|
|
40
|
+
- **Playback**: Integrated audio player to preview generated sounds directly in the list.
|
|
41
|
+
- **Metadata**: Displays the caption/prompt, model, and creation time.
|
|
42
|
+
- **Actions**:
|
|
43
|
+
- **Remix**: Regenerate audio using the same prompt.
|
|
44
|
+
- **Delete**: Remove audio files.
|
|
45
|
+
|
|
46
|
+
## Storage Data model
|
|
47
|
+
|
|
48
|
+
The `media` table tracks extensive information about each asset to support the search and filtering capabilities:
|
|
49
|
+
|
|
50
|
+
| Column | Description |
|
|
51
|
+
|---|---|
|
|
52
|
+
| `url` | Relative path to the file in `~/.llms/cache` |
|
|
53
|
+
| `type` | Asset type (image, audio, video) |
|
|
54
|
+
| `prompt` | The prompt used to generate the asset |
|
|
55
|
+
| `model` | The AI model used |
|
|
56
|
+
| `aspect_ratio` | Aspect ratio (e.g., "1:1", "16:9") |
|
|
57
|
+
| `cost` | Generation cost |
|
|
58
|
+
| `metadata` | Additional JSON metadata |
|
|
59
|
+
| `created` | Timestamp of creation |
|
|
60
|
+
|
|
61
|
+
This local database ensures your gallery remains fast and responsive, even with a large collection of generated assets.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from aiohttp import web
|
|
5
|
+
|
|
6
|
+
from .db import GalleryDB
|
|
7
|
+
|
|
8
|
+
g_db = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def install(ctx):
|
|
12
|
+
def get_db():
|
|
13
|
+
global g_db
|
|
14
|
+
if g_db is None and GalleryDB:
|
|
15
|
+
try:
|
|
16
|
+
db_path = os.path.join(ctx.get_user_path(), "gallery", "gallery.sqlite")
|
|
17
|
+
g_db = GalleryDB(ctx, db_path)
|
|
18
|
+
ctx.register_shutdown_handler(g_db.db.close)
|
|
19
|
+
except Exception as e:
|
|
20
|
+
ctx.err("Failed to init GalleryDB", e)
|
|
21
|
+
return g_db
|
|
22
|
+
|
|
23
|
+
if not get_db():
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
def media_dto(row):
|
|
27
|
+
return row and g_db.to_dto(row, ["reactions", "category", "tags", "ratings", "objects", "metadata"])
|
|
28
|
+
|
|
29
|
+
def on_cache_save(context):
|
|
30
|
+
url = context.get("url", None)
|
|
31
|
+
info = context.get("info", {})
|
|
32
|
+
user = context.get("user", None)
|
|
33
|
+
ctx.log(f"cache saved: {url}")
|
|
34
|
+
ctx.dbg(json.dumps(info, indent=2))
|
|
35
|
+
|
|
36
|
+
if "url" not in info:
|
|
37
|
+
info["url"] = url
|
|
38
|
+
g_db.insert_media(info, user=user)
|
|
39
|
+
|
|
40
|
+
ctx.register_cache_saved_filter(on_cache_save)
|
|
41
|
+
|
|
42
|
+
async def query_media(request):
|
|
43
|
+
rows = g_db.query_media(request.query, user=ctx.get_username(request))
|
|
44
|
+
dtos = [media_dto(row) for row in rows]
|
|
45
|
+
return web.json_response(dtos)
|
|
46
|
+
|
|
47
|
+
ctx.add_get("media", query_media)
|
|
48
|
+
|
|
49
|
+
async def media_totals(request):
|
|
50
|
+
rows = g_db.media_totals(user=ctx.get_username(request))
|
|
51
|
+
return web.json_response(rows)
|
|
52
|
+
|
|
53
|
+
ctx.add_get("media/totals", media_totals)
|
|
54
|
+
|
|
55
|
+
async def delete_media(request):
|
|
56
|
+
hash = request.match_info["hash"]
|
|
57
|
+
g_db.delete_media(hash, user=ctx.get_username(request))
|
|
58
|
+
return web.json_response({})
|
|
59
|
+
|
|
60
|
+
ctx.add_delete("media/{hash}", delete_media)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
__install__ = install
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
3
|
+
from typing import Any, Dict
|
|
4
|
+
|
|
5
|
+
from llms.db import DbManager, order_by, to_dto
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def with_user(data, user):
|
|
9
|
+
if user is None:
|
|
10
|
+
if "user" in data:
|
|
11
|
+
del data["user"]
|
|
12
|
+
return data
|
|
13
|
+
else:
|
|
14
|
+
data["user"] = user
|
|
15
|
+
return data
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ratio_format(ratio):
|
|
19
|
+
w, h = ratio.split(":")
|
|
20
|
+
if int(w) < int(h):
|
|
21
|
+
return -1
|
|
22
|
+
if int(w) > int(h):
|
|
23
|
+
return 1
|
|
24
|
+
return 0
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GalleryDB:
|
|
28
|
+
def __init__(self, ctx, db_path=None):
|
|
29
|
+
if db_path is None:
|
|
30
|
+
raise Exception("db_path is required")
|
|
31
|
+
|
|
32
|
+
self.ctx = ctx
|
|
33
|
+
self.db_path = str(db_path)
|
|
34
|
+
dirname = os.path.dirname(self.db_path)
|
|
35
|
+
if dirname:
|
|
36
|
+
os.makedirs(dirname, exist_ok=True)
|
|
37
|
+
|
|
38
|
+
self.db = DbManager(ctx, self.db_path)
|
|
39
|
+
self.columns = {
|
|
40
|
+
"id": "INTEGER",
|
|
41
|
+
"name": "TEXT", # chutes-hunyuan-image-3.png (filename)
|
|
42
|
+
"type": "TEXT", # image|audio|video
|
|
43
|
+
"prompt": "TEXT",
|
|
44
|
+
"model": "TEXT", # gemini-2.5-flash-image
|
|
45
|
+
"created": "TIMESTAMP",
|
|
46
|
+
"cost": "REAL", # 0.03836745
|
|
47
|
+
"seed": "INTEGER", # 1
|
|
48
|
+
"url": "TEXT", # /~cache/23/238841878a0ebeeea8d0034cfdafc82b15d3a6d00c344b0b5e174acbb19572ef.png
|
|
49
|
+
"hash": "TEXT", # 238841878a0ebeeea8d0034cfdafc82b15d3a6d00c344b0b5e174acbb19572ef
|
|
50
|
+
"aspect_ratio": "TEXT", # 9:16
|
|
51
|
+
"width": "INTEGER", # 768
|
|
52
|
+
"height": "INTEGER", # 1344
|
|
53
|
+
"size": "INTEGER", # 1593817 (bytes)
|
|
54
|
+
"duration": "INTEGER", # 100 (secs)
|
|
55
|
+
"user": "TEXT",
|
|
56
|
+
"reactions": "JSON", # {"❤": 1, "👍": 2}
|
|
57
|
+
"caption": "TEXT",
|
|
58
|
+
"description": "TEXT",
|
|
59
|
+
"phash": "TEXT", # 95482f9e1c3f63a1
|
|
60
|
+
"color": "TEXT", # #040609
|
|
61
|
+
"category": "JSON", # {"fantasy": 0.216552734375, "game character": 0.282470703125}
|
|
62
|
+
"tags": "JSON", # {"bug": 0.9706085920333862, "mask": 0.9348311424255371, "glowing": 0.8394700884819031}
|
|
63
|
+
"rating": "TEXT", # "M"
|
|
64
|
+
"ratings": "JSON", # {"predicted_rating":"G","confidence":0.2164306640625,"all_scores":{"G":0.2164306640625,"PG":0.21240234375,"PG-13":0.1915283203125,"M":0.2069091796875,"R":0.2064208984375}}
|
|
65
|
+
"objects": "JSON", # [{"model":"640m","class":"FACE_FEMALE","score":0.5220243334770203,"box":[361,346,367,451]},{"model":"640m","class":"FEMALE_BREAST_EXPOSED","score":0.31755316257476807,"box":[672,1068,212,272]}]
|
|
66
|
+
"variantId": "TEXT", # 1
|
|
67
|
+
"variantName": "TEXT", # 4x Upscaled
|
|
68
|
+
"published": "TIMESTAMP",
|
|
69
|
+
"metadata": "JSON", # {"date":1767111852}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ratios = ctx.aspect_ratios.keys()
|
|
73
|
+
|
|
74
|
+
self.formats = {
|
|
75
|
+
"square": [ratio for ratio in ratios if ratio_format(ratio) == 0],
|
|
76
|
+
"landscape": [ratio for ratio in ratios if ratio_format(ratio) == 1],
|
|
77
|
+
"portrait": [ratio for ratio in ratios if ratio_format(ratio) == -1],
|
|
78
|
+
}
|
|
79
|
+
with self.db.create_writer_connection() as conn:
|
|
80
|
+
self.init_db(conn)
|
|
81
|
+
|
|
82
|
+
def closest_aspect_ratio(self, width, height):
|
|
83
|
+
target_ratio = width / height
|
|
84
|
+
closest_ratio = "1:1"
|
|
85
|
+
min_diff = float("inf")
|
|
86
|
+
|
|
87
|
+
for ratio in self.ctx.aspect_ratios:
|
|
88
|
+
w, h = ratio.split(":")
|
|
89
|
+
diff = abs(target_ratio - (int(w) / int(h)))
|
|
90
|
+
if diff < min_diff:
|
|
91
|
+
min_diff = diff
|
|
92
|
+
closest_ratio = ratio
|
|
93
|
+
|
|
94
|
+
return closest_ratio
|
|
95
|
+
|
|
96
|
+
def get_connection(self):
|
|
97
|
+
return self.db.create_reader_connection()
|
|
98
|
+
|
|
99
|
+
def init_db(self, conn):
|
|
100
|
+
# Create table with all columns
|
|
101
|
+
overrides = {
|
|
102
|
+
"id": "INTEGER PRIMARY KEY AUTOINCREMENT",
|
|
103
|
+
"created": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",
|
|
104
|
+
}
|
|
105
|
+
sql_columns = ",".join([f"{col} {overrides.get(col, dtype)}" for col, dtype in self.columns.items()])
|
|
106
|
+
self.db.exec(
|
|
107
|
+
conn,
|
|
108
|
+
f"""
|
|
109
|
+
CREATE TABLE IF NOT EXISTS media (
|
|
110
|
+
{sql_columns}
|
|
111
|
+
)
|
|
112
|
+
""",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_media_user ON media(user)")
|
|
116
|
+
self.db.exec(conn, "CREATE INDEX IF NOT EXISTS idx_media_type ON media(type)")
|
|
117
|
+
|
|
118
|
+
# Check for missing columns and migrate if necessary
|
|
119
|
+
cur = self.db.exec(conn, "PRAGMA table_info(media)")
|
|
120
|
+
columns = {row[1] for row in cur.fetchall()}
|
|
121
|
+
|
|
122
|
+
for col, dtype in self.columns.items():
|
|
123
|
+
if col not in columns:
|
|
124
|
+
try:
|
|
125
|
+
self.db.exec(conn, f"ALTER TABLE media ADD COLUMN {col} {dtype}")
|
|
126
|
+
except Exception as e:
|
|
127
|
+
self.ctx.err(f"adding column {col}", e)
|
|
128
|
+
|
|
129
|
+
def to_dto(self, row, json_columns):
|
|
130
|
+
return to_dto(self.ctx, row, json_columns)
|
|
131
|
+
|
|
132
|
+
def get_user_filter(self, user=None, params=None):
|
|
133
|
+
if user is None:
|
|
134
|
+
return "WHERE user IS NULL", params or {}
|
|
135
|
+
else:
|
|
136
|
+
args = params.copy() if params else {}
|
|
137
|
+
args.update({"user": user})
|
|
138
|
+
return "WHERE user = :user", args
|
|
139
|
+
|
|
140
|
+
def prepare_media(self, media, id=None, user=None):
|
|
141
|
+
now = datetime.now()
|
|
142
|
+
if id:
|
|
143
|
+
media["id"] = id
|
|
144
|
+
else:
|
|
145
|
+
media["created"] = now
|
|
146
|
+
return with_user(media, user=user)
|
|
147
|
+
|
|
148
|
+
def insert_media(self, info, user=None, callback=None):
|
|
149
|
+
if not info:
|
|
150
|
+
raise Exception("info is required")
|
|
151
|
+
|
|
152
|
+
media = {}
|
|
153
|
+
metadata = {}
|
|
154
|
+
known_columns = self.columns.keys()
|
|
155
|
+
for k in known_columns:
|
|
156
|
+
val = info.get(k, None)
|
|
157
|
+
if k == "metadata":
|
|
158
|
+
continue
|
|
159
|
+
if k == "created" and not val:
|
|
160
|
+
continue
|
|
161
|
+
if k == "type":
|
|
162
|
+
parts = val.split("/")
|
|
163
|
+
if parts[0] == "image" or parts[0] == "video" or parts[0] == "audio":
|
|
164
|
+
media[k] = parts[0]
|
|
165
|
+
else:
|
|
166
|
+
media[k] = self.db.value(val)
|
|
167
|
+
# for items not in known_columns, add to metadata
|
|
168
|
+
for k in info:
|
|
169
|
+
if k not in known_columns:
|
|
170
|
+
metadata[k] = info[k]
|
|
171
|
+
|
|
172
|
+
if not media.get("hash"):
|
|
173
|
+
media["hash"] = media["url"].split("/")[-1].split(".")[0]
|
|
174
|
+
|
|
175
|
+
if "width" in media and "height" in media and media["width"] and media["height"]:
|
|
176
|
+
media["aspect_ratio"] = self.closest_aspect_ratio(int(media["width"]), int(media["height"]))
|
|
177
|
+
|
|
178
|
+
media["metadata"] = self.db.value(metadata)
|
|
179
|
+
media = with_user(media, user=user)
|
|
180
|
+
|
|
181
|
+
insert_keys = list(media.keys())
|
|
182
|
+
insert_body = ", ".join(insert_keys)
|
|
183
|
+
insert_values = ", ".join(["?" for _ in insert_keys])
|
|
184
|
+
|
|
185
|
+
sql = f"INSERT INTO media ({insert_body}) VALUES ({insert_values})"
|
|
186
|
+
|
|
187
|
+
self.db.write(sql, tuple(media[k] for k in insert_keys), callback)
|
|
188
|
+
|
|
189
|
+
def media_totals(self, user=None):
|
|
190
|
+
sql_where, params = self.get_user_filter(user)
|
|
191
|
+
return self.db.all(
|
|
192
|
+
f"SELECT type, COUNT(*) as count FROM media {sql_where} GROUP BY type ORDER BY count DESC",
|
|
193
|
+
params,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def query_media(self, query: Dict[str, Any], user=None):
|
|
197
|
+
try:
|
|
198
|
+
all_columns = self.columns.keys()
|
|
199
|
+
take = query.get("take", 50)
|
|
200
|
+
skip = query.get("skip", 0)
|
|
201
|
+
sort = query.get("sort", "-id")
|
|
202
|
+
|
|
203
|
+
# always filter by user
|
|
204
|
+
sql_where, params = self.get_user_filter(user)
|
|
205
|
+
params.update(
|
|
206
|
+
{
|
|
207
|
+
"take": take,
|
|
208
|
+
"skip": skip,
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
filter = {}
|
|
213
|
+
for k in query:
|
|
214
|
+
if k in self.columns:
|
|
215
|
+
filter[k] = query[k]
|
|
216
|
+
params[k] = query[k]
|
|
217
|
+
|
|
218
|
+
if len(filter) > 0:
|
|
219
|
+
sql_where += " AND " + " AND ".join([f"{k} = :{k}" for k in filter])
|
|
220
|
+
|
|
221
|
+
if "q" in query:
|
|
222
|
+
sql_where += " AND " if sql_where else "WHERE "
|
|
223
|
+
sql_where += "(prompt LIKE :q OR name LIKE :q OR description LIKE :q OR caption LIKE :q)"
|
|
224
|
+
params["q"] = f"%{query['q']}%"
|
|
225
|
+
|
|
226
|
+
if "format" in query:
|
|
227
|
+
sql_where += " AND " if sql_where else "WHERE "
|
|
228
|
+
format_ratios = self.formats.get(query["format"], [])
|
|
229
|
+
ratios = ", ".join([f"'{ratio}'" for ratio in format_ratios])
|
|
230
|
+
sql_where += f"aspect_ratio IN ({ratios})"
|
|
231
|
+
|
|
232
|
+
return self.db.all(
|
|
233
|
+
f"SELECT * FROM media {sql_where} {order_by(all_columns, sort)} LIMIT :take OFFSET :skip",
|
|
234
|
+
params,
|
|
235
|
+
)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
self.ctx.err(f"query_media ({take}, {skip})", e)
|
|
238
|
+
return []
|
|
239
|
+
|
|
240
|
+
def delete_media(self, hash, user=None, callback=None):
|
|
241
|
+
sql_where, params = self.get_user_filter(user)
|
|
242
|
+
params.update({"hash": hash})
|
|
243
|
+
self.db.write(f"DELETE FROM media {sql_where} AND hash = :hash", params, callback)
|