Spaces:
Running
Running
Upload folder via script
Browse files- Dockerfile +2 -6
- README.md +2 -2
- velai/app_context.py +34 -5
- velai/session.py +5 -2
- velai/storage/storage_endpoint.py +2 -2
Dockerfile
CHANGED
|
@@ -13,10 +13,6 @@ RUN sh /uv-installer.sh && rm /uv-installer.sh
|
|
| 13 |
# Ensure the installed binary is on the PATH
|
| 14 |
ENV PATH="/root/.local/bin:${PATH}"
|
| 15 |
|
| 16 |
-
# Reproducible venv inside the image (no host .venv symlinks; see .dockerignore)
|
| 17 |
-
ENV UV_COMPILE_BYTECODE=1
|
| 18 |
-
ENV UV_LINK_MODE=copy
|
| 19 |
-
|
| 20 |
# Set the work directory inside the container
|
| 21 |
WORKDIR /app
|
| 22 |
|
|
@@ -35,5 +31,5 @@ ENV VELAI_PORT=7860
|
|
| 35 |
|
| 36 |
EXPOSE 7860
|
| 37 |
|
| 38 |
-
#
|
| 39 |
-
CMD ["uv", "run", "--
|
|
|
|
| 13 |
# Ensure the installed binary is on the PATH
|
| 14 |
ENV PATH="/root/.local/bin:${PATH}"
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# Set the work directory inside the container
|
| 17 |
WORKDIR /app
|
| 18 |
|
|
|
|
| 31 |
|
| 32 |
EXPOSE 7860
|
| 33 |
|
| 34 |
+
# Default command to start your app
|
| 35 |
+
CMD ["uv", "run", "--no-sync", "--no-dev", "python", "main.py"]
|
README.md
CHANGED
|
@@ -37,8 +37,8 @@ VELAI is designed for educators, facilitators, and creators who want a lightweig
|
|
| 37 |
- Open the browser at `http://127.0.0.1:7860`. If a password is configured, enter it to access the canvas.
|
| 38 |
|
| 39 |
**Run with Docker**
|
| 40 |
-
- Build the image: `docker build -t
|
| 41 |
-
- Start a container: `docker run -p 7860:7860 -e VELAI_APP_PASSWORD=yourpassword -e VELAI_STORAGE_SECRET=changeme -v $(pwd)/.storage:/app/.storage
|
| 42 |
- Visit `http://127.0.0.1:7860` to begin exploring.
|
| 43 |
|
| 44 |
## First session walkthrough
|
|
|
|
| 37 |
- Open the browser at `http://127.0.0.1:7860`. If a password is configured, enter it to access the canvas.
|
| 38 |
|
| 39 |
**Run with Docker**
|
| 40 |
+
- Build the image: `docker build -t velai .`.
|
| 41 |
+
- Start a container: `docker run -p 7860:7860 -e VELAI_APP_PASSWORD=yourpassword -e VELAI_STORAGE_SECRET=changeme -e VELAI_ADMIN_PASSWORD=youradminpassword -e FAL_KEY=falapikey -v $(pwd)/.storage:/app/.storage velai`.
|
| 42 |
- Visit `http://127.0.0.1:7860` to begin exploring.
|
| 43 |
|
| 44 |
## First session walkthrough
|
velai/app_context.py
CHANGED
|
@@ -77,10 +77,7 @@ async def get_or_create_app_context(client: Client) -> AppContext:
|
|
| 77 |
# init storage
|
| 78 |
app_storage = DocumentStorage(NiceGuiKeyValueStorage[dict[str, Any]](NiceGuiStorageType.General))
|
| 79 |
user_storage = DocumentStorage(NiceGuiKeyValueStorage[dict[str, Any]](NiceGuiStorageType.User))
|
| 80 |
-
|
| 81 |
-
user_id=session_id, root_dir=environment.VELAI_BLOB_STORAGE_PATH, document_storage=user_storage
|
| 82 |
-
)
|
| 83 |
-
async_blob_storage = AsyncBlobStorageAdapter(blob_storage)
|
| 84 |
|
| 85 |
# create and load app config
|
| 86 |
config = _create_config(app_storage)
|
|
@@ -115,10 +112,15 @@ async def current_app_context() -> AppContext:
|
|
| 115 |
raise AppContextNotInitializedError("AppContext not initialized") from exc
|
| 116 |
|
| 117 |
|
| 118 |
-
|
| 119 |
session_id = request.session.get("id")
|
| 120 |
if not session_id:
|
| 121 |
raise AppContextNotInitializedError("missing session")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
|
| 123 |
try:
|
| 124 |
wrapper = await _app_contexts.get(session_id)
|
|
@@ -127,6 +129,33 @@ async def app_context_from_request(request: Request) -> AppContext:
|
|
| 127 |
raise AppContextNotInitializedError("AppContext not initialized") from exc
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
def register_app_context_cleanup(client: Client) -> None:
|
| 131 |
session_id = _browser_session_id()
|
| 132 |
|
|
|
|
| 77 |
# init storage
|
| 78 |
app_storage = DocumentStorage(NiceGuiKeyValueStorage[dict[str, Any]](NiceGuiStorageType.General))
|
| 79 |
user_storage = DocumentStorage(NiceGuiKeyValueStorage[dict[str, Any]](NiceGuiStorageType.User))
|
| 80 |
+
async_blob_storage = create_blob_storage_for_session(session_id)
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
# create and load app config
|
| 83 |
config = _create_config(app_storage)
|
|
|
|
| 112 |
raise AppContextNotInitializedError("AppContext not initialized") from exc
|
| 113 |
|
| 114 |
|
| 115 |
+
def _session_id_from_request(request: Request) -> str:
|
| 116 |
session_id = request.session.get("id")
|
| 117 |
if not session_id:
|
| 118 |
raise AppContextNotInitializedError("missing session")
|
| 119 |
+
return session_id
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
async def app_context_from_request(request: Request) -> AppContext:
|
| 123 |
+
session_id = _session_id_from_request(request)
|
| 124 |
|
| 125 |
try:
|
| 126 |
wrapper = await _app_contexts.get(session_id)
|
|
|
|
| 129 |
raise AppContextNotInitializedError("AppContext not initialized") from exc
|
| 130 |
|
| 131 |
|
| 132 |
+
def create_blob_storage_for_session(session_id: str) -> AsyncBlobStorageAdapter:
|
| 133 |
+
"""Filesystem-backed blob storage for a browser session (no in-memory AppContext required)."""
|
| 134 |
+
user_storage = DocumentStorage(NiceGuiKeyValueStorage[dict[str, Any]](NiceGuiStorageType.User))
|
| 135 |
+
blob_storage: BlobStorage = FileSystemBlobStorage(
|
| 136 |
+
user_id=session_id,
|
| 137 |
+
root_dir=environment.VELAI_BLOB_STORAGE_PATH,
|
| 138 |
+
document_storage=user_storage,
|
| 139 |
+
)
|
| 140 |
+
return AsyncBlobStorageAdapter(blob_storage)
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
async def blob_storage_from_request(request: Request) -> AsyncBlobStorageAdapter:
|
| 144 |
+
"""
|
| 145 |
+
Resolve blob storage for HTTP requests (e.g. <img src="/storage/...">).
|
| 146 |
+
|
| 147 |
+
Image loads are separate HTTP requests and may arrive while the WebSocket is
|
| 148 |
+
reconnecting or on another worker; they must not depend on in-memory AppContext.
|
| 149 |
+
"""
|
| 150 |
+
session_id = _session_id_from_request(request)
|
| 151 |
+
|
| 152 |
+
if await _app_contexts.has(session_id):
|
| 153 |
+
wrapper = await _app_contexts.get(session_id)
|
| 154 |
+
return wrapper.context.blob_storage
|
| 155 |
+
|
| 156 |
+
return create_blob_storage_for_session(session_id)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
def register_app_context_cleanup(client: Client) -> None:
|
| 160 |
session_id = _browser_session_id()
|
| 161 |
|
velai/session.py
CHANGED
|
@@ -152,9 +152,12 @@ class GraphSession:
|
|
| 152 |
return await self.restore_from_server_storage()
|
| 153 |
|
| 154 |
async def save_to_server_storage(self) -> None:
|
| 155 |
-
|
|
|
|
|
|
|
|
|
|
| 156 |
|
| 157 |
-
|
| 158 |
ctx.user_storage[GRAPH_STORAGE_KEY] = data
|
| 159 |
|
| 160 |
async def restore_from_server_storage(self) -> bool:
|
|
|
|
| 152 |
return await self.restore_from_server_storage()
|
| 153 |
|
| 154 |
async def save_to_server_storage(self) -> None:
|
| 155 |
+
try:
|
| 156 |
+
ctx = await app_context.current_app_context()
|
| 157 |
+
except app_context.AppContextNotInitializedError:
|
| 158 |
+
return
|
| 159 |
|
| 160 |
+
data = self.to_json()
|
| 161 |
ctx.user_storage[GRAPH_STORAGE_KEY] = data
|
| 162 |
|
| 163 |
async def restore_from_server_storage(self) -> bool:
|
velai/storage/storage_endpoint.py
CHANGED
|
@@ -65,10 +65,10 @@ def register():
|
|
| 65 |
|
| 66 |
@app.get(storage_endpoint)
|
| 67 |
async def download_blob(request: Request, blob_id: str, original_name: str) -> StreamingResponse:
|
| 68 |
-
|
| 69 |
|
| 70 |
try:
|
| 71 |
-
blob = await
|
| 72 |
except KeyError as exc:
|
| 73 |
raise HTTPException(status_code=404, detail="blob not found") from exc
|
| 74 |
|
|
|
|
| 65 |
|
| 66 |
@app.get(storage_endpoint)
|
| 67 |
async def download_blob(request: Request, blob_id: str, original_name: str) -> StreamingResponse:
|
| 68 |
+
blob_storage = await app_context.blob_storage_from_request(request)
|
| 69 |
|
| 70 |
try:
|
| 71 |
+
blob = await blob_storage.require(blob_id)
|
| 72 |
except KeyError as exc:
|
| 73 |
raise HTTPException(status_code=404, detail="blob not found") from exc
|
| 74 |
|