Spaces:
Sleeping
Sleeping
transition to python backend
Browse files- .github/workflows/ci.yml +73 -13
- README.md +1 -1
- backend/go.mod +0 -47
- backend/go.sum +0 -111
- backend/handlers/caption.go +0 -65
- backend/handlers/metadata.go +0 -26
- backend/handlers/upload.go +0 -118
- backend/internal/captioner/stub.go +0 -16
- backend/internal/storage/local.go +0 -36
- backend/internal/storage/s3.go +0 -40
- backend/internal/storage/storage.go +0 -16
- backend/main.go +0 -88
- backend/migrations/0001_init.sql +0 -51
- backend/migrations/0002_lookup.sql +0 -37
- backend/migrations/0003_placeholder.sql +0 -22
- backend/server/server.go +0 -15
- docker-compose.yml +34 -5
- frontend/src/pages/UploadPage.tsx +9 -6
- frontend/vite.config.ts +4 -4
- py_backend/alembic.ini +3 -0
- py_backend/alembic/env.py +38 -0
- py_backend/alembic/script.py.mako +0 -0
- py_backend/alembic/versions/0002_seed.py +87 -0
- py_backend/alembic/versions/ad38fd571716_init_schema.py +113 -0
- py_backend/app/config.py +15 -0
- py_backend/app/crud.py +56 -0
- py_backend/app/database.py +17 -0
- py_backend/app/main.py +27 -0
- py_backend/app/models.py +68 -0
- py_backend/app/routers/caption.py +50 -0
- py_backend/app/routers/metadata.py +24 -0
- py_backend/app/routers/upload.py +33 -0
- py_backend/app/schemas.py +42 -0
- py_backend/app/storage.py +47 -0
- py_backend/requirements.txt +9 -0
.github/workflows/ci.yml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
name: CI
|
| 2 |
|
| 3 |
on: [push, pull_request]
|
| 4 |
|
|
@@ -6,22 +6,82 @@ jobs:
|
|
| 6 |
build-and-test:
|
| 7 |
runs-on: ubuntu-latest
|
| 8 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
steps:
|
| 10 |
-
-
|
|
|
|
| 11 |
|
| 12 |
# --- Front-end: React ---
|
| 13 |
-
-
|
| 14 |
-
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
-
|
|
|
|
| 17 |
cd frontend
|
| 18 |
-
npm ci
|
| 19 |
-
npm run build
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
- run: |
|
| 26 |
-
cd backend
|
| 27 |
-
go test ./... # 6) run Go tests
|
|
|
|
| 1 |
+
name: CI
|
| 2 |
|
| 3 |
on: [push, pull_request]
|
| 4 |
|
|
|
|
| 6 |
build-and-test:
|
| 7 |
runs-on: ubuntu-latest
|
| 8 |
|
| 9 |
+
# start postgres & minio for the Python backend
|
| 10 |
+
services:
|
| 11 |
+
postgres:
|
| 12 |
+
image: postgres:16
|
| 13 |
+
env:
|
| 14 |
+
POSTGRES_USER: promptaid
|
| 15 |
+
POSTGRES_PASSWORD: promptaid
|
| 16 |
+
POSTGRES_DB: promptaid
|
| 17 |
+
ports:
|
| 18 |
+
- 5433:5432
|
| 19 |
+
options: >-
|
| 20 |
+
--health-cmd "pg_isready -U promptaid -d promptaid"
|
| 21 |
+
--health-interval 10s
|
| 22 |
+
--health-timeout 5s
|
| 23 |
+
--health-retries 5
|
| 24 |
+
|
| 25 |
+
minio:
|
| 26 |
+
image: minio/minio
|
| 27 |
+
env:
|
| 28 |
+
MINIO_ROOT_USER: promptaid
|
| 29 |
+
MINIO_ROOT_PASSWORD: promptaid
|
| 30 |
+
ports:
|
| 31 |
+
- 9000:9000
|
| 32 |
+
options: >-
|
| 33 |
+
--health-cmd "curl -f http://localhost:9000/minio/health/live || exit 1"
|
| 34 |
+
--health-interval 10s
|
| 35 |
+
--health-timeout 5s
|
| 36 |
+
--health-retries 5
|
| 37 |
+
|
| 38 |
steps:
|
| 39 |
+
- name: Check out code
|
| 40 |
+
uses: actions/checkout@v4
|
| 41 |
|
| 42 |
# --- Front-end: React ---
|
| 43 |
+
- name: Setup Node.js
|
| 44 |
+
uses: actions/setup-node@v4
|
| 45 |
+
with:
|
| 46 |
+
node-version: "20"
|
| 47 |
|
| 48 |
+
- name: Install & build front-end
|
| 49 |
+
run: |
|
| 50 |
cd frontend
|
| 51 |
+
npm ci
|
| 52 |
+
npm run build
|
| 53 |
+
env:
|
| 54 |
+
CI: true
|
| 55 |
+
|
| 56 |
+
# --- Back-end: Python/FastAPI ---
|
| 57 |
+
- name: Setup Python
|
| 58 |
+
uses: actions/setup-python@v4
|
| 59 |
+
with:
|
| 60 |
+
python-version: "3.11"
|
| 61 |
+
|
| 62 |
+
- name: Install Python dependencies
|
| 63 |
+
run: |
|
| 64 |
+
cd py_backend
|
| 65 |
+
python -m venv .venv
|
| 66 |
+
source .venv/bin/activate
|
| 67 |
+
pip install --upgrade pip
|
| 68 |
+
pip install -r requirements.txt
|
| 69 |
+
|
| 70 |
+
- name: Run database migrations
|
| 71 |
+
env:
|
| 72 |
+
DATABASE_URL: postgresql://promptaid:promptaid@localhost:5433/promptaid
|
| 73 |
+
S3_ENDPOINT: http://localhost:9000
|
| 74 |
+
S3_ACCESS_KEY: promptaid
|
| 75 |
+
S3_SECRET_KEY: promptaid
|
| 76 |
+
S3_BUCKET: promptaid
|
| 77 |
+
run: |
|
| 78 |
+
cd py_backend
|
| 79 |
+
source .venv/bin/activate
|
| 80 |
+
alembic upgrade head
|
| 81 |
|
| 82 |
+
- name: Run back-end tests
|
| 83 |
+
run: |
|
| 84 |
+
cd py_backend
|
| 85 |
+
source .venv/bin/activate
|
| 86 |
+
pytest
|
| 87 |
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -2,4 +2,4 @@
|
|
| 2 |
UCL IXN summer project - IFRC
|
| 3 |
|
| 4 |
System architecture
|
| 5 |
-
React =>
|
|
|
|
| 2 |
UCL IXN summer project - IFRC
|
| 3 |
|
| 4 |
System architecture
|
| 5 |
+
React => Python => postgreSQL => fly.io
|
backend/go.mod
DELETED
|
@@ -1,47 +0,0 @@
|
|
| 1 |
-
module github.com/SCGR-1/promptaid-backend
|
| 2 |
-
|
| 3 |
-
go 1.24.5
|
| 4 |
-
|
| 5 |
-
require (
|
| 6 |
-
github.com/gin-gonic/gin v1.10.1
|
| 7 |
-
github.com/google/uuid v1.6.0
|
| 8 |
-
github.com/lib/pq v1.10.9
|
| 9 |
-
github.com/minio/minio-go/v7 v7.0.95
|
| 10 |
-
)
|
| 11 |
-
|
| 12 |
-
require (
|
| 13 |
-
github.com/bytedance/sonic v1.11.6 // indirect
|
| 14 |
-
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
| 15 |
-
github.com/cloudwego/base64x v0.1.4 // indirect
|
| 16 |
-
github.com/cloudwego/iasm v0.2.0 // indirect
|
| 17 |
-
github.com/dustin/go-humanize v1.0.1 // indirect
|
| 18 |
-
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
| 19 |
-
github.com/gin-contrib/sse v0.1.0 // indirect
|
| 20 |
-
github.com/go-ini/ini v1.67.0 // indirect
|
| 21 |
-
github.com/go-playground/locales v0.14.1 // indirect
|
| 22 |
-
github.com/go-playground/universal-translator v0.18.1 // indirect
|
| 23 |
-
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
| 24 |
-
github.com/goccy/go-json v0.10.5 // indirect
|
| 25 |
-
github.com/json-iterator/go v1.1.12 // indirect
|
| 26 |
-
github.com/klauspost/compress v1.18.0 // indirect
|
| 27 |
-
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
| 28 |
-
github.com/leodido/go-urn v1.4.0 // indirect
|
| 29 |
-
github.com/mattn/go-isatty v0.0.20 // indirect
|
| 30 |
-
github.com/minio/crc64nvme v1.0.2 // indirect
|
| 31 |
-
github.com/minio/md5-simd v1.1.2 // indirect
|
| 32 |
-
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
| 33 |
-
github.com/modern-go/reflect2 v1.0.2 // indirect
|
| 34 |
-
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
| 35 |
-
github.com/philhofer/fwd v1.2.0 // indirect
|
| 36 |
-
github.com/rs/xid v1.6.0 // indirect
|
| 37 |
-
github.com/tinylib/msgp v1.3.0 // indirect
|
| 38 |
-
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
| 39 |
-
github.com/ugorji/go/codec v1.2.12 // indirect
|
| 40 |
-
golang.org/x/arch v0.8.0 // indirect
|
| 41 |
-
golang.org/x/crypto v0.39.0 // indirect
|
| 42 |
-
golang.org/x/net v0.41.0 // indirect
|
| 43 |
-
golang.org/x/sys v0.33.0 // indirect
|
| 44 |
-
golang.org/x/text v0.26.0 // indirect
|
| 45 |
-
google.golang.org/protobuf v1.34.1 // indirect
|
| 46 |
-
gopkg.in/yaml.v3 v3.0.1 // indirect
|
| 47 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/go.sum
DELETED
|
@@ -1,111 +0,0 @@
|
|
| 1 |
-
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
| 2 |
-
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
| 3 |
-
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
| 4 |
-
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
| 5 |
-
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
| 6 |
-
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
| 7 |
-
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
| 8 |
-
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
| 9 |
-
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 10 |
-
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
| 11 |
-
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
| 12 |
-
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
| 13 |
-
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
| 14 |
-
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
| 15 |
-
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
| 16 |
-
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
| 17 |
-
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
| 18 |
-
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
| 19 |
-
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
| 20 |
-
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
| 21 |
-
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
| 22 |
-
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
| 23 |
-
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
| 24 |
-
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
| 25 |
-
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
| 26 |
-
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
| 27 |
-
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
| 28 |
-
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
| 29 |
-
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
| 30 |
-
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
| 31 |
-
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
| 32 |
-
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
| 33 |
-
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
| 34 |
-
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
| 35 |
-
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
| 36 |
-
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
| 37 |
-
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
| 38 |
-
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
| 39 |
-
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
| 40 |
-
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
| 41 |
-
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
| 42 |
-
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
| 43 |
-
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
| 44 |
-
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
| 45 |
-
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
| 46 |
-
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
| 47 |
-
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
| 48 |
-
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
| 49 |
-
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
| 50 |
-
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
| 51 |
-
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
| 52 |
-
github.com/minio/crc64nvme v1.0.2 h1:6uO1UxGAD+kwqWWp7mBFsi5gAse66C4NXO8cmcVculg=
|
| 53 |
-
github.com/minio/crc64nvme v1.0.2/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
| 54 |
-
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
| 55 |
-
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
| 56 |
-
github.com/minio/minio-go/v7 v7.0.95 h1:ywOUPg+PebTMTzn9VDsoFJy32ZuARN9zhB+K3IYEvYU=
|
| 57 |
-
github.com/minio/minio-go/v7 v7.0.95/go.mod h1:wOOX3uxS334vImCNRVyIDdXX9OsXDm89ToynKgqUKlo=
|
| 58 |
-
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 59 |
-
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
| 60 |
-
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
| 61 |
-
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
| 62 |
-
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
| 63 |
-
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
| 64 |
-
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
| 65 |
-
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
| 66 |
-
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
| 67 |
-
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
| 68 |
-
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
| 69 |
-
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
| 70 |
-
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
| 71 |
-
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
| 72 |
-
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
| 73 |
-
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
| 74 |
-
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
| 75 |
-
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
| 76 |
-
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 77 |
-
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
| 78 |
-
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
| 79 |
-
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
| 80 |
-
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
| 81 |
-
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
| 82 |
-
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
| 83 |
-
github.com/tinylib/msgp v1.3.0 h1:ULuf7GPooDaIlbyvgAxBV/FI7ynli6LZ1/nVUNu+0ww=
|
| 84 |
-
github.com/tinylib/msgp v1.3.0/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
| 85 |
-
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
| 86 |
-
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
| 87 |
-
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
| 88 |
-
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
| 89 |
-
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
| 90 |
-
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
| 91 |
-
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
| 92 |
-
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
| 93 |
-
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
| 94 |
-
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
|
| 95 |
-
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
| 96 |
-
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
| 97 |
-
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
| 98 |
-
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
| 99 |
-
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
| 100 |
-
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
| 101 |
-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
| 102 |
-
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
| 103 |
-
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
| 104 |
-
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
| 105 |
-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
| 106 |
-
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
| 107 |
-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 108 |
-
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
| 109 |
-
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
| 110 |
-
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
| 111 |
-
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/handlers/caption.go
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
package handlers
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"time"
|
| 5 |
-
"net/http"
|
| 6 |
-
|
| 7 |
-
"github.com/gin-gonic/gin"
|
| 8 |
-
"github.com/google/uuid"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
// POST /api/maps/:id/caption
|
| 12 |
-
func (d *UploadDeps) CreateCaption(c *gin.Context) {
|
| 13 |
-
ctx := c.Request.Context()
|
| 14 |
-
mapID := c.Param("id")
|
| 15 |
-
|
| 16 |
-
// 1) look up the mapβs file key
|
| 17 |
-
var key string
|
| 18 |
-
if err := d.DB.QueryRowContext(ctx,
|
| 19 |
-
`SELECT file_key FROM maps WHERE map_id = $1`, mapID).Scan(&key); err != nil {
|
| 20 |
-
c.JSON(http.StatusNotFound, gin.H{"error": "map not found"})
|
| 21 |
-
return
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
// 2) generate placeholder caption
|
| 25 |
-
text, model := d.Cap.Generate(ctx, key)
|
| 26 |
-
|
| 27 |
-
// 3) insert caption row
|
| 28 |
-
capID := uuid.New()
|
| 29 |
-
if _, err := d.DB.ExecContext(ctx, `
|
| 30 |
-
INSERT INTO captions (cap_id, map_id, generated, model, raw_json, created_at)
|
| 31 |
-
VALUES ($1,$2,$3,$4,$5,NOW())`,
|
| 32 |
-
capID, mapID, text, model, []byte("{}")); err != nil {
|
| 33 |
-
c.JSON(http.StatusInternalServerError, gin.H{"error": "db insert failed"})
|
| 34 |
-
return
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
// 4) respond to the frontβend
|
| 38 |
-
c.JSON(http.StatusOK, gin.H{
|
| 39 |
-
"captionId": capID,
|
| 40 |
-
"generated": text,
|
| 41 |
-
})
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
func (d *UploadDeps) GetCaption(c *gin.Context) {
|
| 45 |
-
ctx := c.Request.Context()
|
| 46 |
-
capID := c.Param("id")
|
| 47 |
-
|
| 48 |
-
var key, text string
|
| 49 |
-
if err := d.DB.QueryRowContext(ctx, `
|
| 50 |
-
SELECT m.file_key, c.generated
|
| 51 |
-
FROM captions c
|
| 52 |
-
JOIN maps m ON c.map_id = m.id
|
| 53 |
-
WHERE c.id = $1`, capID).Scan(&key, &text); err != nil {
|
| 54 |
-
c.JSON(http.StatusNotFound, gin.H{"error": "caption not found"})
|
| 55 |
-
return
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
// turn object key into a 24βhour presigned URL
|
| 59 |
-
url, _ := d.Storage.Link(ctx, key, 24*time.Hour)
|
| 60 |
-
|
| 61 |
-
c.JSON(http.StatusOK, gin.H{
|
| 62 |
-
"imageUrl": url,
|
| 63 |
-
"generated": text,
|
| 64 |
-
})
|
| 65 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/handlers/metadata.go
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
package handlers
|
| 2 |
-
|
| 3 |
-
import "github.com/gin-gonic/gin"
|
| 4 |
-
|
| 5 |
-
// PUT /api/maps/:id/metadata
|
| 6 |
-
func (d *UploadDeps) UpdateMapMetadata(c *gin.Context) {
|
| 7 |
-
mapID := c.Param("id")
|
| 8 |
-
var req struct {
|
| 9 |
-
Source string `json:"source"`
|
| 10 |
-
Region string `json:"region"`
|
| 11 |
-
Category string `json:"category"`
|
| 12 |
-
}
|
| 13 |
-
if err := c.BindJSON(&req); err != nil {
|
| 14 |
-
c.JSON(400, gin.H{"error": "invalid json"})
|
| 15 |
-
return
|
| 16 |
-
}
|
| 17 |
-
_, err := d.DB.Exec(`UPDATE maps
|
| 18 |
-
SET source=$1, region=$2, category=$3
|
| 19 |
-
WHERE id=$4`,
|
| 20 |
-
req.Source, req.Region, req.Category, mapID)
|
| 21 |
-
if err != nil {
|
| 22 |
-
c.JSON(500, gin.H{"error": "db update failed"})
|
| 23 |
-
return
|
| 24 |
-
}
|
| 25 |
-
c.Status(204)
|
| 26 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/handlers/upload.go
DELETED
|
@@ -1,118 +0,0 @@
|
|
| 1 |
-
package handlers
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"bytes"
|
| 5 |
-
"crypto/sha256"
|
| 6 |
-
"database/sql"
|
| 7 |
-
"encoding/hex"
|
| 8 |
-
"io"
|
| 9 |
-
"log"
|
| 10 |
-
"net/http"
|
| 11 |
-
"time"
|
| 12 |
-
|
| 13 |
-
"github.com/gin-gonic/gin"
|
| 14 |
-
"github.com/google/uuid"
|
| 15 |
-
"github.com/lib/pq"
|
| 16 |
-
"github.com/SCGR-1/promptaid-backend/internal/storage"
|
| 17 |
-
"github.com/SCGR-1/promptaid-backend/internal/captioner"
|
| 18 |
-
)
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
// wire this in main.go: r.POST("/maps", deps.UploadMap)
|
| 22 |
-
type UploadDeps struct {
|
| 23 |
-
DB *sql.DB
|
| 24 |
-
Storage storage.ObjectStore
|
| 25 |
-
Bucket string
|
| 26 |
-
RegionOK map[string]bool
|
| 27 |
-
Cap captioner.Captioner
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
func (d *UploadDeps) UploadMap(c *gin.Context) {
|
| 31 |
-
// ---- 1. Parse multipart form ----------------------------------------
|
| 32 |
-
file, fileHdr, err := c.Request.FormFile("file")
|
| 33 |
-
if err != nil {
|
| 34 |
-
c.JSON(http.StatusBadRequest, gin.H{"error": "file required"})
|
| 35 |
-
return
|
| 36 |
-
}
|
| 37 |
-
defer file.Close()
|
| 38 |
-
|
| 39 |
-
params := struct {
|
| 40 |
-
Source string `form:"source" binding:"required"`
|
| 41 |
-
Region string `form:"region" binding:"required"`
|
| 42 |
-
Category string `form:"category" binding:"required"`
|
| 43 |
-
Countries []string `form:"countries"` // optional multiβselect
|
| 44 |
-
}{}
|
| 45 |
-
if err := c.ShouldBind(¶ms); err != nil {
|
| 46 |
-
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
| 47 |
-
return
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
// ---- 2. Validate lookup codes ---------------------------------------
|
| 51 |
-
if !d.RegionOK[params.Region] {
|
| 52 |
-
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid region"})
|
| 53 |
-
return
|
| 54 |
-
}
|
| 55 |
-
// repeat for source / category / countriesβ¦
|
| 56 |
-
|
| 57 |
-
// ---- 3. Read file + hash it ----------------------------------------
|
| 58 |
-
var buf []byte
|
| 59 |
-
buf, err = io.ReadAll(file)
|
| 60 |
-
if err != nil {
|
| 61 |
-
c.JSON(http.StatusInternalServerError, gin.H{"error": "read failed"})
|
| 62 |
-
return
|
| 63 |
-
}
|
| 64 |
-
sha := sha256.Sum256(buf)
|
| 65 |
-
shaHex := hex.EncodeToString(sha[:])
|
| 66 |
-
|
| 67 |
-
// choose a deterministic object key
|
| 68 |
-
objKey := "maps/" + time.Now().Format("2006/01/02/") + shaHex + ".png"
|
| 69 |
-
|
| 70 |
-
// ---- 4. Upload to object storage -----------------------------------
|
| 71 |
-
ctx := c.Request.Context()
|
| 72 |
-
if err := d.Storage.Put(
|
| 73 |
-
ctx, objKey,
|
| 74 |
-
bytes.NewReader(buf), int64(len(buf)),
|
| 75 |
-
fileHdr.Header.Get("Content-Type"),
|
| 76 |
-
); err != nil {
|
| 77 |
-
c.JSON(http.StatusInternalServerError, gin.H{"error": "storage failed"})
|
| 78 |
-
return
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
// ---- 5. Insert into maps -------------------------------------------
|
| 83 |
-
mapID := uuid.New()
|
| 84 |
-
res, err := d.DB.Exec(`
|
| 85 |
-
INSERT INTO maps
|
| 86 |
-
(map_id, file_key, sha256, source, region, category, created_at)
|
| 87 |
-
VALUES ($1,$2,$3,$4,$5,$6,NOW())`,
|
| 88 |
-
mapID, objKey, shaHex,
|
| 89 |
-
params.Source, params.Region, params.Category,
|
| 90 |
-
)
|
| 91 |
-
if err != nil {
|
| 92 |
-
log.Printf("π΄ maps INSERT error: %v", err)
|
| 93 |
-
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
| 94 |
-
return
|
| 95 |
-
}
|
| 96 |
-
rows, _ := res.RowsAffected()
|
| 97 |
-
log.Printf("π’ maps INSERT succeeded, rows affected: %d", rows)
|
| 98 |
-
|
| 99 |
-
// ---- 6. Insert any countries ---------------------------------------
|
| 100 |
-
if len(params.Countries) > 0 {
|
| 101 |
-
_, err = d.DB.Exec(`
|
| 102 |
-
INSERT INTO map_countries (map_id, c_code)
|
| 103 |
-
SELECT $1, UNNEST($2::char(2)[])
|
| 104 |
-
ON CONFLICT DO NOTHING`,
|
| 105 |
-
mapID, pq.Array(params.Countries),
|
| 106 |
-
)
|
| 107 |
-
if err != nil {
|
| 108 |
-
c.JSON(http.StatusInternalServerError, gin.H{"error": "country insert failed"})
|
| 109 |
-
return
|
| 110 |
-
}
|
| 111 |
-
}
|
| 112 |
-
|
| 113 |
-
// ---- 7. Return success ---------------------------------------------
|
| 114 |
-
c.JSON(http.StatusOK, gin.H{
|
| 115 |
-
"mapId": mapID,
|
| 116 |
-
// frontβend can show a presigned preview if you want
|
| 117 |
-
})
|
| 118 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/internal/captioner/stub.go
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
package captioner
|
| 2 |
-
|
| 3 |
-
import "context"
|
| 4 |
-
|
| 5 |
-
// A pluggable captioner interface (so you can swap to GPTβ4o later).
|
| 6 |
-
type Captioner interface {
|
| 7 |
-
Generate(ctx context.Context, objectKey string) (text, model string)
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
// Minimal stub: always returns the same caption.
|
| 11 |
-
type Stub struct{}
|
| 12 |
-
|
| 13 |
-
func (Stub) Generate(_ context.Context, _ string) (string, string) {
|
| 14 |
-
return "π§ Stub caption: replace me with GPTβ4o", "GPTβ4O"
|
| 15 |
-
}
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/internal/storage/local.go
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
package storage
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
"io"
|
| 6 |
-
"os"
|
| 7 |
-
"path/filepath"
|
| 8 |
-
"time"
|
| 9 |
-
)
|
| 10 |
-
|
| 11 |
-
type LocalStore struct{ root string }
|
| 12 |
-
|
| 13 |
-
func NewLocalStore(root string) *LocalStore { return &LocalStore{root: root} }
|
| 14 |
-
|
| 15 |
-
func (l *LocalStore) Put(_ context.Context, key string, r io.Reader, _ int64, _ string) error {
|
| 16 |
-
full := filepath.Join(l.root, key)
|
| 17 |
-
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
| 18 |
-
return err
|
| 19 |
-
}
|
| 20 |
-
f, err := os.Create(full)
|
| 21 |
-
if err != nil {
|
| 22 |
-
return err
|
| 23 |
-
}
|
| 24 |
-
defer f.Close()
|
| 25 |
-
_, err = io.Copy(f, r)
|
| 26 |
-
return err
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
func (l *LocalStore) Link(_ context.Context, key string, _ time.Duration) (string, error) {
|
| 30 |
-
// Served by Gin static handler: router.Static("/static", "./uploads")
|
| 31 |
-
return "/static/" + key, nil
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
func (l *LocalStore) Root() string {
|
| 35 |
-
return l.root
|
| 36 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/internal/storage/s3.go
DELETED
|
@@ -1,40 +0,0 @@
|
|
| 1 |
-
package storage
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
"time"
|
| 6 |
-
"io"
|
| 7 |
-
|
| 8 |
-
"github.com/minio/minio-go/v7"
|
| 9 |
-
"github.com/minio/minio-go/v7/pkg/credentials"
|
| 10 |
-
)
|
| 11 |
-
|
| 12 |
-
type S3Store struct {
|
| 13 |
-
cli *minio.Client
|
| 14 |
-
bucket string
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
func NewS3Store(endpoint, accessKey, secretKey, bucket string, useSSL bool) (*S3Store, error) {
|
| 18 |
-
cli, err := minio.New(endpoint, &minio.Options{
|
| 19 |
-
Creds: credentials.NewStaticV4(accessKey, secretKey, ""),
|
| 20 |
-
Secure: useSSL,
|
| 21 |
-
})
|
| 22 |
-
if err != nil {
|
| 23 |
-
return nil, err
|
| 24 |
-
}
|
| 25 |
-
return &S3Store{cli: cli, bucket: bucket}, nil
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
func (s *S3Store) Put(ctx context.Context, key string, r io.Reader, size int64, ctype string) error {
|
| 29 |
-
_, err := s.cli.PutObject(ctx, s.bucket, key, r, size,
|
| 30 |
-
minio.PutObjectOptions{ContentType: ctype})
|
| 31 |
-
return err
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
func (s *S3Store) Link(ctx context.Context, key string, ttl time.Duration) (string, error) {
|
| 35 |
-
u, err := s.cli.PresignedGetObject(ctx, s.bucket, key, ttl, nil)
|
| 36 |
-
if err != nil {
|
| 37 |
-
return "", err
|
| 38 |
-
}
|
| 39 |
-
return u.String(), nil
|
| 40 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/internal/storage/storage.go
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
package storage
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"context"
|
| 5 |
-
"time"
|
| 6 |
-
"io"
|
| 7 |
-
)
|
| 8 |
-
|
| 9 |
-
// ObjectStore is a minimal interface for saving and later linking to blobs.
|
| 10 |
-
type ObjectStore interface {
|
| 11 |
-
// Put permanently stores the object under key.
|
| 12 |
-
Put(ctx context.Context, key string, r io.Reader, size int64, contentType string) error
|
| 13 |
-
|
| 14 |
-
// Link returns a URL valid for roughly ttl (ignored by LocalStore).
|
| 15 |
-
Link(ctx context.Context, key string, ttl time.Duration) (string, error)
|
| 16 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/main.go
DELETED
|
@@ -1,88 +0,0 @@
|
|
| 1 |
-
package main
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"database/sql"
|
| 5 |
-
"log"
|
| 6 |
-
"os"
|
| 7 |
-
|
| 8 |
-
"github.com/gin-gonic/gin"
|
| 9 |
-
_ "github.com/lib/pq"
|
| 10 |
-
|
| 11 |
-
"github.com/SCGR-1/promptaid-backend/internal/storage"
|
| 12 |
-
"github.com/SCGR-1/promptaid-backend/handlers"
|
| 13 |
-
"github.com/SCGR-1/promptaid-backend/internal/captioner"
|
| 14 |
-
)
|
| 15 |
-
|
| 16 |
-
type Config struct {
|
| 17 |
-
S3Bucket string
|
| 18 |
-
UploadDir string
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
func loadConfig() Config {
|
| 22 |
-
return Config{
|
| 23 |
-
S3Bucket: os.Getenv("S3_BUCKET"),
|
| 24 |
-
UploadDir: os.Getenv("UPLOAD_DIR"),
|
| 25 |
-
}
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
func main() {
|
| 30 |
-
|
| 31 |
-
// ---- 1. connect DB ----
|
| 32 |
-
dsn := os.Getenv("DATABASE_URL")
|
| 33 |
-
if dsn == "" {
|
| 34 |
-
dsn = "postgres://promptaid:promptaid@localhost:5432/promptaid?sslmode=disable"
|
| 35 |
-
}
|
| 36 |
-
db, err := sql.Open("postgres", dsn)
|
| 37 |
-
if err != nil {
|
| 38 |
-
log.Fatal(err)
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
// ---- 2. choose storage driver ----
|
| 43 |
-
var store storage.ObjectStore
|
| 44 |
-
switch os.Getenv("STORAGE_DRIVER") {
|
| 45 |
-
case "s3":
|
| 46 |
-
store, err = storage.NewS3Store(
|
| 47 |
-
os.Getenv("S3_ENDPOINT"),
|
| 48 |
-
os.Getenv("S3_KEY"),
|
| 49 |
-
os.Getenv("S3_SECRET"),
|
| 50 |
-
os.Getenv("S3_BUCKET"),
|
| 51 |
-
os.Getenv("S3_SSL") == "true",
|
| 52 |
-
)
|
| 53 |
-
if err != nil { log.Fatal(err) }
|
| 54 |
-
default: // local
|
| 55 |
-
uploadDir := os.Getenv("UPLOAD_DIR")
|
| 56 |
-
if uploadDir == "" { uploadDir = "./uploads" }
|
| 57 |
-
store = storage.NewLocalStore(uploadDir)
|
| 58 |
-
}
|
| 59 |
-
|
| 60 |
-
uploadDeps := handlers.UploadDeps{
|
| 61 |
-
DB: db,
|
| 62 |
-
Storage: store,
|
| 63 |
-
Bucket: os.Getenv("S3_BUCKET"),
|
| 64 |
-
RegionOK: make(map[string]bool),
|
| 65 |
-
Cap: captioner.Stub{},
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
// ---- 3. build server ----
|
| 69 |
-
r := gin.Default()
|
| 70 |
-
|
| 71 |
-
if l, ok := store.(*storage.LocalStore); ok {
|
| 72 |
-
r.Static("/static", l.Root()) // add Root() getter or hardcode "./uploads"
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
api := r.Group("/api")
|
| 76 |
-
|
| 77 |
-
api.POST("/maps", uploadDeps.UploadMap)
|
| 78 |
-
api.POST("/maps/:id/caption", uploadDeps.CreateCaption)
|
| 79 |
-
api.PUT ("/maps/:id/metadata", uploadDeps.UpdateMapMetadata)
|
| 80 |
-
api.GET ("/captions/:id", uploadDeps.GetCaption)
|
| 81 |
-
|
| 82 |
-
uploadDeps.RegionOK = map[string]bool{
|
| 83 |
-
"_TBD_REGION": true,
|
| 84 |
-
"AFR": true, "AMR": true, "APA": true, "EUR": true, "MENA": true,
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
log.Fatal(r.Run(":8080"))
|
| 88 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/migrations/0001_init.sql
DELETED
|
@@ -1,51 +0,0 @@
|
|
| 1 |
-
-- +goose Up
|
| 2 |
-
-- Initial schema
|
| 3 |
-
|
| 4 |
-
CREATE TABLE sources (s_code TEXT PRIMARY KEY, label TEXT NOT NULL);
|
| 5 |
-
CREATE TABLE region (r_code TEXT PRIMARY KEY, label TEXT NOT NULL);
|
| 6 |
-
CREATE TABLE category (cat_code TEXT PRIMARY KEY, label TEXT NOT NULL);
|
| 7 |
-
CREATE TABLE country (c_code CHAR(2) PRIMARY KEY, label TEXT NOT NULL);
|
| 8 |
-
CREATE TABLE model (m_code TEXT PRIMARY KEY, label TEXT NOT NULL);
|
| 9 |
-
|
| 10 |
-
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
|
| 11 |
-
|
| 12 |
-
CREATE TABLE maps (
|
| 13 |
-
map_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
| 14 |
-
file_key TEXT NOT NULL,
|
| 15 |
-
sha256 TEXT NOT NULL,
|
| 16 |
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
| 17 |
-
source TEXT NOT NULL REFERENCES sources(s_code),
|
| 18 |
-
region TEXT NOT NULL REFERENCES region(r_code),
|
| 19 |
-
category TEXT NOT NULL REFERENCES category(cat_code)
|
| 20 |
-
);
|
| 21 |
-
|
| 22 |
-
CREATE TABLE map_countries (
|
| 23 |
-
map_id UUID REFERENCES maps(map_id) ON DELETE CASCADE,
|
| 24 |
-
c_code CHAR(2) REFERENCES country(c_code),
|
| 25 |
-
PRIMARY KEY (map_id, c_code)
|
| 26 |
-
);
|
| 27 |
-
|
| 28 |
-
CREATE TABLE captions (
|
| 29 |
-
cap_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
| 30 |
-
map_id UUID UNIQUE REFERENCES maps(map_id) ON DELETE CASCADE,
|
| 31 |
-
model TEXT NOT NULL REFERENCES model(m_code),
|
| 32 |
-
raw_json JSONB NOT NULL,
|
| 33 |
-
generated TEXT NOT NULL,
|
| 34 |
-
edited TEXT,
|
| 35 |
-
accuracy SMALLINT CHECK (accuracy BETWEEN 0 AND 100),
|
| 36 |
-
context SMALLINT CHECK (context BETWEEN 0 AND 100),
|
| 37 |
-
usability SMALLINT CHECK (usability BETWEEN 0 AND 100),
|
| 38 |
-
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
| 39 |
-
updated_at TIMESTAMPTZ
|
| 40 |
-
);
|
| 41 |
-
|
| 42 |
-
-- +goose Down
|
| 43 |
-
DROP TABLE IF EXISTS captions;
|
| 44 |
-
DROP TABLE IF EXISTS map_countries;
|
| 45 |
-
DROP TABLE IF EXISTS maps;
|
| 46 |
-
DROP TABLE IF EXISTS model;
|
| 47 |
-
DROP TABLE IF EXISTS country;
|
| 48 |
-
DROP TABLE IF EXISTS category;
|
| 49 |
-
DROP TABLE IF EXISTS region;
|
| 50 |
-
DROP TABLE IF EXISTS sources;
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/migrations/0002_lookup.sql
DELETED
|
@@ -1,37 +0,0 @@
|
|
| 1 |
-
-- +goose Up
|
| 2 |
-
|
| 3 |
-
-- Sources
|
| 4 |
-
INSERT INTO sources (s_code, label) VALUES
|
| 5 |
-
('WFP_ADAM', 'WFP ADAM β Automated Disaster Analysis & Mapping'),
|
| 6 |
-
('PDC', 'Pacific Disaster Center (PDC)'),
|
| 7 |
-
('GDACS', 'GDACS β Global Disaster Alert & Coordination System'),
|
| 8 |
-
('GFL_HUB', 'Google Flood Hub'),
|
| 9 |
-
('GFL_GENCAST', 'Google GenCast'),
|
| 10 |
-
('USGS', 'USGS β United States Geological Survey')
|
| 11 |
-
ON CONFLICT (s_code) DO NOTHING;
|
| 12 |
-
|
| 13 |
-
-- Regions
|
| 14 |
-
INSERT INTO region (r_code, label) VALUES
|
| 15 |
-
('AFR','Africa'), ('AMR','Americas'), ('APA','AsiaβPacific'),
|
| 16 |
-
('EUR','Europe'), ('MENA','Middleβ―East & Northβ―Africa')
|
| 17 |
-
ON CONFLICT (r_code) DO NOTHING;
|
| 18 |
-
|
| 19 |
-
-- Categories
|
| 20 |
-
INSERT INTO category (cat_code, label) VALUES
|
| 21 |
-
('FLOOD','Flood'), ('WILDFIRE','Wildfire'), ('EARTHQUAKE','Earthquake'),
|
| 22 |
-
('CYCLONE','Cyclone'), ('DROUGHT','Drought'), ('LANDSLIDE','Landslide'),
|
| 23 |
-
('TORNADO','Tornado'), ('VOLCANO','Volcano'), ('OTHER','Other')
|
| 24 |
-
ON CONFLICT (cat_code) DO NOTHING;
|
| 25 |
-
|
| 26 |
-
-- Models
|
| 27 |
-
INSERT INTO model (m_code, label) VALUES
|
| 28 |
-
('GPTβ4O','GPTβ4o Vision'), ('GEMINI15','GeminiΒ 1.5 Pro'), ('CLAUDE3','ClaudeΒ 3 Sonnet')
|
| 29 |
-
ON CONFLICT (m_code) DO NOTHING;
|
| 30 |
-
|
| 31 |
-
-- +goose Down
|
| 32 |
-
DELETE FROM sources WHERE s_code IN ('WFP_ADAM','PDC','GDACS','GFL_HUB','GFL_GENCAST','USGS');
|
| 33 |
-
DELETE FROM region WHERE r_code IN ('AFR','AMR','APA','EUR','MENA');
|
| 34 |
-
DELETE FROM category WHERE cat_code IN ('FLOOD','WILDFIRE','EARTHQUAKE','CYCLONE','DROUGHT','LANDSLIDE','TORNADO','VOLCANO','OTHER');
|
| 35 |
-
DELETE FROM model WHERE m_code IN ('GPTβ4O','GEMINI15','CLAUDE3');
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/migrations/0003_placeholder.sql
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
-- +goose Up
|
| 2 |
-
|
| 3 |
-
INSERT INTO sources (s_code, label)
|
| 4 |
-
VALUES
|
| 5 |
-
('_TBD_SOURCE', 'TBD placeholder')
|
| 6 |
-
ON CONFLICT (s_code) DO NOTHING;
|
| 7 |
-
|
| 8 |
-
INSERT INTO region (r_code, label)
|
| 9 |
-
VALUES
|
| 10 |
-
('_TBD_REGION', 'TBD placeholder')
|
| 11 |
-
ON CONFLICT (r_code) DO NOTHING;
|
| 12 |
-
|
| 13 |
-
INSERT INTO category (cat_code, label)
|
| 14 |
-
VALUES
|
| 15 |
-
('_TBD_CATEGORY', 'TBD placeholder')
|
| 16 |
-
ON CONFLICT (cat_code) DO NOTHING;
|
| 17 |
-
|
| 18 |
-
-- +goose Down
|
| 19 |
-
|
| 20 |
-
DELETE FROM category WHERE cat_code = '_TBD_CATEGORY';
|
| 21 |
-
DELETE FROM region WHERE r_code = '_TBD_REGION';
|
| 22 |
-
DELETE FROM sources WHERE s_code = '_TBD_SOURCE';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
backend/server/server.go
DELETED
|
@@ -1,15 +0,0 @@
|
|
| 1 |
-
package server //
|
| 2 |
-
|
| 3 |
-
import (
|
| 4 |
-
"database/sql"
|
| 5 |
-
"github.com/SCGR-1/promptaid-backend/internal/storage"
|
| 6 |
-
)
|
| 7 |
-
|
| 8 |
-
type Server struct {
|
| 9 |
-
db *sql.DB
|
| 10 |
-
store storage.ObjectStore
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
func NewServer(db *sql.DB, store storage.ObjectStore) *Server {
|
| 14 |
-
return &Server{db: db, store: store}
|
| 15 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docker-compose.yml
CHANGED
|
@@ -1,15 +1,44 @@
|
|
|
|
|
|
|
|
| 1 |
services:
|
| 2 |
-
|
| 3 |
image: postgres:16
|
| 4 |
-
restart:
|
| 5 |
environment:
|
| 6 |
POSTGRES_USER: promptaid
|
| 7 |
POSTGRES_PASSWORD: promptaid
|
| 8 |
POSTGRES_DB: promptaid
|
| 9 |
ports:
|
| 10 |
-
- "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
volumes:
|
| 12 |
-
-
|
|
|
|
|
|
|
| 13 |
|
| 14 |
volumes:
|
| 15 |
-
|
|
|
|
|
|
| 1 |
+
version: "3.8"
|
| 2 |
+
|
| 3 |
services:
|
| 4 |
+
postgres:
|
| 5 |
image: postgres:16
|
| 6 |
+
restart: always
|
| 7 |
environment:
|
| 8 |
POSTGRES_USER: promptaid
|
| 9 |
POSTGRES_PASSWORD: promptaid
|
| 10 |
POSTGRES_DB: promptaid
|
| 11 |
ports:
|
| 12 |
+
- "5433:5432"
|
| 13 |
+
volumes:
|
| 14 |
+
- pgdata:/var/lib/postgresql/data
|
| 15 |
+
|
| 16 |
+
pgadmin:
|
| 17 |
+
image: dpage/pgadmin4
|
| 18 |
+
restart: always
|
| 19 |
+
environment:
|
| 20 |
+
PGADMIN_DEFAULT_EMAIL: [email protected]
|
| 21 |
+
PGADMIN_DEFAULT_PASSWORD: admin
|
| 22 |
+
ports:
|
| 23 |
+
- "5050:80"
|
| 24 |
+
depends_on:
|
| 25 |
+
- postgres
|
| 26 |
+
|
| 27 |
+
minio:
|
| 28 |
+
image: minio/minio
|
| 29 |
+
restart: always
|
| 30 |
+
command: server /data
|
| 31 |
+
environment:
|
| 32 |
+
MINIO_ROOT_USER: promptaid
|
| 33 |
+
MINIO_ROOT_PASSWORD: promptaid
|
| 34 |
+
ports:
|
| 35 |
+
- "9000:9000"
|
| 36 |
+
- "9001:9001"
|
| 37 |
volumes:
|
| 38 |
+
- minio_data:/data
|
| 39 |
+
depends_on:
|
| 40 |
+
- postgres
|
| 41 |
|
| 42 |
volumes:
|
| 43 |
+
pgdata:
|
| 44 |
+
minio_data:
|
frontend/src/pages/UploadPage.tsx
CHANGED
|
@@ -9,18 +9,19 @@ import {
|
|
| 9 |
UploadCloudLineIcon,
|
| 10 |
ArrowRightLineIcon,
|
| 11 |
} from '@ifrc-go/icons';
|
| 12 |
-
import { Link
|
| 13 |
|
| 14 |
export default function UploadPage() {
|
| 15 |
const [step, setStep] = useState<1 | 2>(1);
|
| 16 |
const [preview, setPreview] = useState<string | null>(null);
|
| 17 |
/* ---------------- local state ----------------- */
|
| 18 |
-
const navigate = useNavigate();
|
| 19 |
|
| 20 |
const PH_SOURCE = "_TBD_SOURCE";
|
| 21 |
const PH_REGION = "_TBD_REGION";
|
| 22 |
const PH_CATEGORY = "_TBD_CATEGORY";
|
| 23 |
|
|
|
|
|
|
|
| 24 |
const [file, setFile] = useState<File | null>(null);
|
| 25 |
//const [source, setSource] = useState('');
|
| 26 |
//const [region, setRegion] = useState('');
|
|
@@ -29,7 +30,6 @@ export default function UploadPage() {
|
|
| 29 |
const [region, setRegion] = useState(PH_REGION);
|
| 30 |
const [category, setCategory] = useState(PH_CATEGORY);
|
| 31 |
const [countries, setCountries] = useState<string[]>([]);
|
| 32 |
-
const [captionId, setCaptionId] = useState<string | null>(null);
|
| 33 |
|
| 34 |
// Wrapper functions to handle OptionKey to string conversion
|
| 35 |
const handleSourceChange = (value: any) => setSource(String(value));
|
|
@@ -89,20 +89,23 @@ export default function UploadPage() {
|
|
| 89 |
|
| 90 |
try {
|
| 91 |
/* 1) upload */
|
| 92 |
-
const mapRes
|
| 93 |
const mapJson = await readJsonSafely(mapRes);
|
| 94 |
if (!mapRes.ok) throw new Error(mapJson.error || 'Upload failed');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
/* 2) caption */
|
| 97 |
const capRes = await fetch(
|
| 98 |
-
`/api/maps/${
|
| 99 |
{ method: 'POST' },
|
| 100 |
);
|
| 101 |
const capJson = await readJsonSafely(capRes);
|
| 102 |
if (!capRes.ok) throw new Error(capJson.error || 'Caption failed');
|
| 103 |
|
| 104 |
/* 3) continue workflow */
|
| 105 |
-
setCaptionId(capJson.captionId);
|
| 106 |
setDraft(capJson.generated);
|
| 107 |
setStep(2);
|
| 108 |
} catch (err) {
|
|
|
|
| 9 |
UploadCloudLineIcon,
|
| 10 |
ArrowRightLineIcon,
|
| 11 |
} from '@ifrc-go/icons';
|
| 12 |
+
import { Link } from 'react-router-dom';
|
| 13 |
|
| 14 |
export default function UploadPage() {
|
| 15 |
const [step, setStep] = useState<1 | 2>(1);
|
| 16 |
const [preview, setPreview] = useState<string | null>(null);
|
| 17 |
/* ---------------- local state ----------------- */
|
|
|
|
| 18 |
|
| 19 |
const PH_SOURCE = "_TBD_SOURCE";
|
| 20 |
const PH_REGION = "_TBD_REGION";
|
| 21 |
const PH_CATEGORY = "_TBD_CATEGORY";
|
| 22 |
|
| 23 |
+
const [mapId, setMapId] = useState<string | null>(null);
|
| 24 |
+
|
| 25 |
const [file, setFile] = useState<File | null>(null);
|
| 26 |
//const [source, setSource] = useState('');
|
| 27 |
//const [region, setRegion] = useState('');
|
|
|
|
| 30 |
const [region, setRegion] = useState(PH_REGION);
|
| 31 |
const [category, setCategory] = useState(PH_CATEGORY);
|
| 32 |
const [countries, setCountries] = useState<string[]>([]);
|
|
|
|
| 33 |
|
| 34 |
// Wrapper functions to handle OptionKey to string conversion
|
| 35 |
const handleSourceChange = (value: any) => setSource(String(value));
|
|
|
|
| 89 |
|
| 90 |
try {
|
| 91 |
/* 1) upload */
|
| 92 |
+
const mapRes = await fetch('/api/maps/', { method: 'POST', body: fd });
|
| 93 |
const mapJson = await readJsonSafely(mapRes);
|
| 94 |
if (!mapRes.ok) throw new Error(mapJson.error || 'Upload failed');
|
| 95 |
+
|
| 96 |
+
const mapIdVal = mapJson.map_id;
|
| 97 |
+
if (!mapIdVal) throw new Error('Upload failed: map_id not found');
|
| 98 |
+
setMapId(mapIdVal);
|
| 99 |
|
| 100 |
/* 2) caption */
|
| 101 |
const capRes = await fetch(
|
| 102 |
+
`/api/maps/${mapIdVal}/caption/`,
|
| 103 |
{ method: 'POST' },
|
| 104 |
);
|
| 105 |
const capJson = await readJsonSafely(capRes);
|
| 106 |
if (!capRes.ok) throw new Error(capJson.error || 'Caption failed');
|
| 107 |
|
| 108 |
/* 3) continue workflow */
|
|
|
|
| 109 |
setDraft(capJson.generated);
|
| 110 |
setStep(2);
|
| 111 |
} catch (err) {
|
frontend/vite.config.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
import { defineConfig } from 'vite'
|
| 4 |
import react from '@vitejs/plugin-react'
|
| 5 |
|
| 6 |
export default defineConfig({
|
|
|
|
| 7 |
server: {
|
| 8 |
-
port: 5173,
|
| 9 |
proxy: {
|
|
|
|
| 10 |
'/api': {
|
| 11 |
target: 'http://localhost:8080',
|
| 12 |
changeOrigin: true,
|
| 13 |
secure: false,
|
|
|
|
| 14 |
},
|
| 15 |
},
|
| 16 |
},
|
| 17 |
-
plugins: [react()],
|
| 18 |
})
|
| 19 |
|
| 20 |
|
|
|
|
| 1 |
+
// vite.config.ts
|
|
|
|
| 2 |
import { defineConfig } from 'vite'
|
| 3 |
import react from '@vitejs/plugin-react'
|
| 4 |
|
| 5 |
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
server: {
|
|
|
|
| 8 |
proxy: {
|
| 9 |
+
// proxy any /api/* request to localhost:8080
|
| 10 |
'/api': {
|
| 11 |
target: 'http://localhost:8080',
|
| 12 |
changeOrigin: true,
|
| 13 |
secure: false,
|
| 14 |
+
// rewrite: (path) => path.replace(/^\/api/, '/api'), // not needed if same prefix
|
| 15 |
},
|
| 16 |
},
|
| 17 |
},
|
|
|
|
| 18 |
})
|
| 19 |
|
| 20 |
|
py_backend/alembic.ini
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[alembic]
|
| 2 |
+
script_location = alembic
|
| 3 |
+
sqlalchemy.url = postgresql://promptaid:promptaid@localhost:5433/promptaid
|
py_backend/alembic/env.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
load_dotenv()
|
| 5 |
+
|
| 6 |
+
# Add the project root (one level up from alembic/) to PYTHONPATH
|
| 7 |
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 8 |
+
|
| 9 |
+
from alembic import context
|
| 10 |
+
from sqlalchemy import engine_from_config, pool
|
| 11 |
+
from app.models import Base
|
| 12 |
+
|
| 13 |
+
target_metadata = Base.metadata
|
| 14 |
+
config = context.config
|
| 15 |
+
|
| 16 |
+
def run_migrations_offline():
|
| 17 |
+
url = config.get_main_option("sqlalchemy.url")
|
| 18 |
+
context.configure(
|
| 19 |
+
url=url, target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}
|
| 20 |
+
)
|
| 21 |
+
with context.begin_transaction():
|
| 22 |
+
context.run_migrations()
|
| 23 |
+
|
| 24 |
+
def run_migrations_online():
|
| 25 |
+
connectable = engine_from_config(
|
| 26 |
+
config.get_section(config.config_ini_section),
|
| 27 |
+
prefix='sqlalchemy.',
|
| 28 |
+
poolclass=pool.NullPool,
|
| 29 |
+
)
|
| 30 |
+
with connectable.connect() as connection:
|
| 31 |
+
context.configure(connection=connection, target_metadata=target_metadata)
|
| 32 |
+
with context.begin_transaction():
|
| 33 |
+
context.run_migrations()
|
| 34 |
+
|
| 35 |
+
if context.is_offline_mode():
|
| 36 |
+
run_migrations_offline()
|
| 37 |
+
else:
|
| 38 |
+
run_migrations_online()
|
py_backend/alembic/script.py.mako
ADDED
|
File without changes
|
py_backend/alembic/versions/0002_seed.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# py_backend/alembic/versions/0002_seed_lookups.py
|
| 2 |
+
|
| 3 |
+
"""seed lookup tables
|
| 4 |
+
|
| 5 |
+
Revision ID: 0002seed
|
| 6 |
+
Revises: ad38fd571716
|
| 7 |
+
Create Date: 2025-07-25 14:00:00.000000
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
|
| 12 |
+
# revision identifiers, used by Alembic.
|
| 13 |
+
revision = '0002seed'
|
| 14 |
+
down_revision = 'ad38fd571716'
|
| 15 |
+
branch_labels = None
|
| 16 |
+
depends_on = None
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def upgrade():
|
| 20 |
+
# 1) sources
|
| 21 |
+
op.execute("""
|
| 22 |
+
INSERT INTO sources (s_code, label) VALUES
|
| 23 |
+
('WFP_ADAM', 'WFP ADAM β Automated Disaster Analysis & Mapping'),
|
| 24 |
+
('PDC', 'Pacific Disaster Center (PDC)'),
|
| 25 |
+
('GDACS', 'GDACS β Global Disaster Alert & Coordination System'),
|
| 26 |
+
('GFL_HUB', 'Google Flood Hub'),
|
| 27 |
+
('GFL_GENCAST', 'Google GenCast'),
|
| 28 |
+
('USGS', 'USGS β United States Geological Survey'),
|
| 29 |
+
('_TBD_SOURCE','TBD placeholder')
|
| 30 |
+
ON CONFLICT (s_code) DO NOTHING;
|
| 31 |
+
""")
|
| 32 |
+
|
| 33 |
+
# 2) region
|
| 34 |
+
op.execute("""
|
| 35 |
+
INSERT INTO region (r_code, label) VALUES
|
| 36 |
+
('AFR','Africa'),
|
| 37 |
+
('AMR','Americas'),
|
| 38 |
+
('APA','AsiaβPacific'),
|
| 39 |
+
('EUR','Europe'),
|
| 40 |
+
('MENA','Middleβ―East & Northβ―Africa'),
|
| 41 |
+
('_TBD_REGION','TBD placeholder')
|
| 42 |
+
ON CONFLICT (r_code) DO NOTHING;
|
| 43 |
+
""")
|
| 44 |
+
|
| 45 |
+
# 3) category
|
| 46 |
+
op.execute("""
|
| 47 |
+
INSERT INTO category (cat_code, label) VALUES
|
| 48 |
+
('FLOOD','Flood'),
|
| 49 |
+
('WILDFIRE','Wildfire'),
|
| 50 |
+
('EARTHQUAKE','Earthquake'),
|
| 51 |
+
('CYCLONE','Cyclone'),
|
| 52 |
+
('DROUGHT','Drought'),
|
| 53 |
+
('LANDSLIDE','Landslide'),
|
| 54 |
+
('TORNADO','Tornado'),
|
| 55 |
+
('VOLCANO','Volcano'),
|
| 56 |
+
('OTHER','Other'),
|
| 57 |
+
('_TBD_CATEGORY','TBD placeholder')
|
| 58 |
+
ON CONFLICT (cat_code) DO NOTHING;
|
| 59 |
+
""")
|
| 60 |
+
|
| 61 |
+
# 4) model
|
| 62 |
+
op.execute("""
|
| 63 |
+
INSERT INTO model (m_code, label) VALUES
|
| 64 |
+
('GPT-4O', 'GPTβ4o Vision'),
|
| 65 |
+
('GEMINI15', 'GeminiΒ 1.5 Pro'),
|
| 66 |
+
('CLAUDE3', 'ClaudeΒ 3 Sonnet'),
|
| 67 |
+
('STUB_MODEL','Stub Captioner')
|
| 68 |
+
ON CONFLICT (m_code) DO NOTHING;
|
| 69 |
+
""")
|
| 70 |
+
|
| 71 |
+
# 5) country (example set; add more ISOβ2 codes as needed)
|
| 72 |
+
op.execute("""
|
| 73 |
+
INSERT INTO country (c_code, label) VALUES
|
| 74 |
+
('PH','Philippines'),
|
| 75 |
+
('ID','Indonesia'),
|
| 76 |
+
('VN','Vietnam')
|
| 77 |
+
ON CONFLICT (c_code) DO NOTHING;
|
| 78 |
+
""")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def downgrade():
|
| 82 |
+
# reverse in roughly the opposite order
|
| 83 |
+
op.execute("DELETE FROM country WHERE c_code IN ('PH','ID','VN');")
|
| 84 |
+
op.execute("DELETE FROM model WHERE m_code IN ('GPT-4O','GEMINI15','CLAUDE3','STUB_MODEL');")
|
| 85 |
+
op.execute("DELETE FROM category WHERE cat_code IN ('FLOOD','WILDFIRE','EARTHQUAKE','CYCLONE','DROUGHT','LANDSLIDE','TORNADO','VOLCANO','OTHER','_TBD_CATEGORY');")
|
| 86 |
+
op.execute("DELETE FROM region WHERE r_code IN ('AFR','AMR','APA','EUR','MENA','_TBD_REGION');")
|
| 87 |
+
op.execute("DELETE FROM sources WHERE s_code IN ('WFP_ADAM','PDC','GDACS','GFL_HUB','GFL_GENCAST','USGS','_TBD_SOURCE');")
|
py_backend/alembic/versions/ad38fd571716_init_schema.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""init schema
|
| 2 |
+
|
| 3 |
+
Revision ID: ad38fd571716
|
| 4 |
+
Revises:
|
| 5 |
+
Create Date: 2025-07-24 15:30:00.000000
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from alembic import op
|
| 9 |
+
import sqlalchemy as sa
|
| 10 |
+
|
| 11 |
+
# revision identifiers, used by Alembic.
|
| 12 |
+
revision = 'ad38fd571716'
|
| 13 |
+
down_revision = None
|
| 14 |
+
branch_labels = None
|
| 15 |
+
depends_on = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def upgrade():
|
| 19 |
+
# 1) Enable pgcrypto extension for gen_random_uuid()
|
| 20 |
+
op.execute('CREATE EXTENSION IF NOT EXISTS "pgcrypto";')
|
| 21 |
+
|
| 22 |
+
# 2) Lookup tables
|
| 23 |
+
op.create_table(
|
| 24 |
+
'sources',
|
| 25 |
+
sa.Column('s_code', sa.Text(), primary_key=True),
|
| 26 |
+
sa.Column('label', sa.Text(), nullable=False),
|
| 27 |
+
)
|
| 28 |
+
op.create_table(
|
| 29 |
+
'region',
|
| 30 |
+
sa.Column('r_code', sa.Text(), primary_key=True),
|
| 31 |
+
sa.Column('label', sa.Text(), nullable=False),
|
| 32 |
+
)
|
| 33 |
+
op.create_table(
|
| 34 |
+
'category',
|
| 35 |
+
sa.Column('cat_code', sa.Text(), primary_key=True),
|
| 36 |
+
sa.Column('label', sa.Text(), nullable=False),
|
| 37 |
+
)
|
| 38 |
+
op.create_table(
|
| 39 |
+
'country',
|
| 40 |
+
sa.Column('c_code', sa.CHAR(length=2), primary_key=True),
|
| 41 |
+
sa.Column('label', sa.Text(), nullable=False),
|
| 42 |
+
)
|
| 43 |
+
op.create_table(
|
| 44 |
+
'model',
|
| 45 |
+
sa.Column('m_code', sa.Text(), primary_key=True),
|
| 46 |
+
sa.Column('label', sa.Text(), nullable=False),
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
# 3) maps table
|
| 50 |
+
op.create_table(
|
| 51 |
+
'maps',
|
| 52 |
+
sa.Column('map_id', sa.UUID(),
|
| 53 |
+
server_default=sa.text('gen_random_uuid()'),
|
| 54 |
+
primary_key=True),
|
| 55 |
+
sa.Column('file_key', sa.Text(), nullable=False),
|
| 56 |
+
sa.Column('sha256', sa.Text(), nullable=False),
|
| 57 |
+
sa.Column('source', sa.Text(), nullable=False),
|
| 58 |
+
sa.Column('region', sa.Text(), nullable=False),
|
| 59 |
+
sa.Column('category', sa.Text(), nullable=False),
|
| 60 |
+
sa.Column('created_at',sa.TIMESTAMP(timezone=True),
|
| 61 |
+
server_default=sa.text('NOW()'),
|
| 62 |
+
nullable=False),
|
| 63 |
+
sa.ForeignKeyConstraint(['source'], ['sources.s_code']),
|
| 64 |
+
sa.ForeignKeyConstraint(['region'], ['region.r_code']),
|
| 65 |
+
sa.ForeignKeyConstraint(['category'], ['category.cat_code']),
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
# 4) map_countries join table
|
| 69 |
+
op.create_table(
|
| 70 |
+
'map_countries',
|
| 71 |
+
sa.Column('map_id', sa.UUID(), nullable=False),
|
| 72 |
+
sa.Column('c_code', sa.CHAR(length=2), nullable=False),
|
| 73 |
+
sa.PrimaryKeyConstraint('map_id', 'c_code'),
|
| 74 |
+
sa.ForeignKeyConstraint(['map_id'], ['maps.map_id'], ondelete='CASCADE'),
|
| 75 |
+
sa.ForeignKeyConstraint(['c_code'], ['country.c_code']),
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
# 5) captions table
|
| 79 |
+
op.create_table(
|
| 80 |
+
'captions',
|
| 81 |
+
sa.Column('cap_id', sa.UUID(),
|
| 82 |
+
server_default=sa.text('gen_random_uuid()'),
|
| 83 |
+
primary_key=True),
|
| 84 |
+
sa.Column('map_id', sa.UUID(), nullable=False, unique=True),
|
| 85 |
+
sa.Column('model', sa.Text(), nullable=False),
|
| 86 |
+
sa.Column('raw_json', sa.JSON(), nullable=False),
|
| 87 |
+
sa.Column('generated', sa.Text(), nullable=False),
|
| 88 |
+
sa.Column('edited', sa.Text(), nullable=True),
|
| 89 |
+
sa.Column('accuracy', sa.SmallInteger(),
|
| 90 |
+
sa.CheckConstraint('accuracy BETWEEN 0 AND 100')),
|
| 91 |
+
sa.Column('context', sa.SmallInteger(),
|
| 92 |
+
sa.CheckConstraint('context BETWEEN 0 AND 100')),
|
| 93 |
+
sa.Column('usability', sa.SmallInteger(),
|
| 94 |
+
sa.CheckConstraint('usability BETWEEN 0 AND 100')),
|
| 95 |
+
sa.Column('created_at',sa.TIMESTAMP(timezone=True),
|
| 96 |
+
server_default=sa.text('NOW()'),
|
| 97 |
+
nullable=False),
|
| 98 |
+
sa.Column('updated_at',sa.TIMESTAMP(timezone=True), nullable=True),
|
| 99 |
+
sa.ForeignKeyConstraint(['map_id'], ['maps.map_id'], ondelete='CASCADE'),
|
| 100 |
+
sa.ForeignKeyConstraint(['model'], ['model.m_code']),
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def downgrade():
|
| 105 |
+
# drop in reverse order to respect FKs
|
| 106 |
+
op.drop_table('captions')
|
| 107 |
+
op.drop_table('map_countries')
|
| 108 |
+
op.drop_table('maps')
|
| 109 |
+
op.drop_table('model')
|
| 110 |
+
op.drop_table('country')
|
| 111 |
+
op.drop_table('category')
|
| 112 |
+
op.drop_table('region')
|
| 113 |
+
op.drop_table('sources')
|
py_backend/app/config.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseSettings
|
| 2 |
+
|
| 3 |
+
class Settings(BaseSettings):
|
| 4 |
+
DATABASE_URL: str
|
| 5 |
+
S3_ENDPOINT: str
|
| 6 |
+
S3_ACCESS_KEY: str
|
| 7 |
+
S3_SECRET_KEY: str
|
| 8 |
+
S3_BUCKET: str
|
| 9 |
+
|
| 10 |
+
class Config:
|
| 11 |
+
env_file = ".env"
|
| 12 |
+
env_file_encoding = "utf-8-sig"
|
| 13 |
+
|
| 14 |
+
# instantiate a single global settings object
|
| 15 |
+
settings = Settings()
|
py_backend/app/crud.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import io, hashlib
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from . import models
|
| 4 |
+
|
| 5 |
+
def hash_bytes(data: bytes) -> str:
|
| 6 |
+
"""Compute SHAβ256 hex digest of the data."""
|
| 7 |
+
return hashlib.sha256(data).hexdigest()
|
| 8 |
+
|
| 9 |
+
def create_map(db: Session, src, reg, cat, key, sha, countries: list[str]):
|
| 10 |
+
"""Insert into maps and map_countries."""
|
| 11 |
+
m = models.Map(
|
| 12 |
+
source=src, region=reg, category=cat,
|
| 13 |
+
file_key=key, sha256=sha
|
| 14 |
+
)
|
| 15 |
+
db.add(m)
|
| 16 |
+
db.flush() # assign m.map_id
|
| 17 |
+
|
| 18 |
+
# link countries
|
| 19 |
+
for c in countries:
|
| 20 |
+
country = db.get(models.Country, c)
|
| 21 |
+
if country:
|
| 22 |
+
m.countries.append(country)
|
| 23 |
+
|
| 24 |
+
db.commit()
|
| 25 |
+
db.refresh(m)
|
| 26 |
+
return m
|
| 27 |
+
|
| 28 |
+
def get_map(db: Session, map_id):
|
| 29 |
+
return db.get(models.Map, map_id)
|
| 30 |
+
|
| 31 |
+
def create_caption(db: Session, map_id, model_code, raw_json, text):
|
| 32 |
+
c = models.Caption(
|
| 33 |
+
map_id=map_id,
|
| 34 |
+
model=model_code,
|
| 35 |
+
raw_json=raw_json,
|
| 36 |
+
generated=text
|
| 37 |
+
)
|
| 38 |
+
db.add(c)
|
| 39 |
+
db.commit()
|
| 40 |
+
db.refresh(c)
|
| 41 |
+
return c
|
| 42 |
+
|
| 43 |
+
def get_caption(db: Session, cap_id):
|
| 44 |
+
return db.get(models.Caption, cap_id)
|
| 45 |
+
|
| 46 |
+
def update_caption(db: Session, cap_id, edited, accuracy, context, usability):
|
| 47 |
+
c = get_caption(db, cap_id)
|
| 48 |
+
if not c:
|
| 49 |
+
return None
|
| 50 |
+
c.edited = edited
|
| 51 |
+
c.accuracy = accuracy
|
| 52 |
+
c.context = context
|
| 53 |
+
c.usability = usability
|
| 54 |
+
db.commit()
|
| 55 |
+
db.refresh(c)
|
| 56 |
+
return c
|
py_backend/app/database.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 3 |
+
from sqlalchemy.orm import sessionmaker
|
| 4 |
+
from .config import settings
|
| 5 |
+
|
| 6 |
+
# Create the SQLAlchemy engine
|
| 7 |
+
engine = create_engine(settings.DATABASE_URL, echo=True)
|
| 8 |
+
|
| 9 |
+
# Each instance of SessionLocal is a database session
|
| 10 |
+
SessionLocal = sessionmaker(
|
| 11 |
+
autocommit=False,
|
| 12 |
+
autoflush=False,
|
| 13 |
+
bind=engine
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
# Base class for ORM models
|
| 17 |
+
Base = declarative_base()
|
py_backend/app/main.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# py_backend/app/main.py
|
| 2 |
+
|
| 3 |
+
from fastapi import FastAPI
|
| 4 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 5 |
+
from app.routers import upload, caption, metadata
|
| 6 |
+
from app.config import settings
|
| 7 |
+
import boto3
|
| 8 |
+
|
| 9 |
+
app = FastAPI(title="PromptAid Vision")
|
| 10 |
+
|
| 11 |
+
# CORS: allow your React dev server(s)
|
| 12 |
+
app.add_middleware(
|
| 13 |
+
CORSMiddleware,
|
| 14 |
+
allow_origins=[
|
| 15 |
+
"http://localhost:3000",
|
| 16 |
+
"http://localhost:5173"
|
| 17 |
+
],
|
| 18 |
+
allow_credentials=True,
|
| 19 |
+
allow_methods=["*"],
|
| 20 |
+
allow_headers=["*"],
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# Mount routers
|
| 24 |
+
app.include_router(upload.router, prefix="/api/maps", tags=["maps"])
|
| 25 |
+
app.include_router(caption.router, prefix="/api", tags=["captions"])
|
| 26 |
+
app.include_router(metadata.router, prefix="/api", tags=["metadata"])
|
| 27 |
+
|
py_backend/app/models.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import (
|
| 2 |
+
Column, String, DateTime, JSON, SmallInteger, Table, ForeignKey
|
| 3 |
+
)
|
| 4 |
+
from sqlalchemy.dialects.postgresql import UUID, TIMESTAMP, CHAR
|
| 5 |
+
from sqlalchemy.orm import relationship
|
| 6 |
+
import datetime, uuid
|
| 7 |
+
from .database import Base
|
| 8 |
+
|
| 9 |
+
# association table maps β countries
|
| 10 |
+
map_countries = Table(
|
| 11 |
+
"map_countries", Base.metadata,
|
| 12 |
+
Column("map_id", UUID(as_uuid=True), ForeignKey("maps.map_id", ondelete="CASCADE")),
|
| 13 |
+
Column("c_code", CHAR(2), ForeignKey("country.c_code")),
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
class Source(Base):
|
| 17 |
+
__tablename__ = "sources"
|
| 18 |
+
s_code = Column(String, primary_key=True)
|
| 19 |
+
label = Column(String, nullable=False)
|
| 20 |
+
|
| 21 |
+
class Region(Base):
|
| 22 |
+
__tablename__ = "region"
|
| 23 |
+
r_code = Column(String, primary_key=True)
|
| 24 |
+
label = Column(String, nullable=False)
|
| 25 |
+
|
| 26 |
+
class Category(Base):
|
| 27 |
+
__tablename__ = "category"
|
| 28 |
+
cat_code = Column(String, primary_key=True)
|
| 29 |
+
label = Column(String, nullable=False)
|
| 30 |
+
|
| 31 |
+
class Country(Base):
|
| 32 |
+
__tablename__ = "country"
|
| 33 |
+
c_code = Column(CHAR(2), primary_key=True)
|
| 34 |
+
label = Column(String, nullable=False)
|
| 35 |
+
|
| 36 |
+
class ModelLookup(Base):
|
| 37 |
+
__tablename__ = "model"
|
| 38 |
+
m_code = Column(String, primary_key=True)
|
| 39 |
+
label = Column(String, nullable=False)
|
| 40 |
+
|
| 41 |
+
class Map(Base):
|
| 42 |
+
__tablename__ = "maps"
|
| 43 |
+
map_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 44 |
+
file_key = Column(String, nullable=False)
|
| 45 |
+
sha256 = Column(String, nullable=False)
|
| 46 |
+
source = Column(String, ForeignKey("sources.s_code"), nullable=False)
|
| 47 |
+
region = Column(String, ForeignKey("region.r_code"), nullable=False)
|
| 48 |
+
category = Column(String, ForeignKey("category.cat_code"), nullable=False)
|
| 49 |
+
created_at = Column(TIMESTAMP(timezone=True), default=datetime.datetime.utcnow)
|
| 50 |
+
|
| 51 |
+
countries = relationship("Country", secondary=map_countries)
|
| 52 |
+
caption = relationship("Caption", uselist=False, back_populates="map")
|
| 53 |
+
|
| 54 |
+
class Caption(Base):
|
| 55 |
+
__tablename__ = "captions"
|
| 56 |
+
cap_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 57 |
+
map_id = Column(UUID(as_uuid=True), ForeignKey("maps.map_id", ondelete="CASCADE"), unique=True)
|
| 58 |
+
model = Column(String, ForeignKey("model.m_code"), nullable=False)
|
| 59 |
+
raw_json = Column(JSON, nullable=False)
|
| 60 |
+
generated = Column(String, nullable=False)
|
| 61 |
+
edited = Column(String)
|
| 62 |
+
accuracy = Column(SmallInteger)
|
| 63 |
+
context = Column(SmallInteger)
|
| 64 |
+
usability = Column(SmallInteger)
|
| 65 |
+
created_at = Column(TIMESTAMP(timezone=True), default=datetime.datetime.utcnow)
|
| 66 |
+
updated_at = Column(TIMESTAMP(timezone=True), onupdate=datetime.datetime.utcnow)
|
| 67 |
+
|
| 68 |
+
map = relationship("Map", back_populates="caption")
|
py_backend/app/routers/caption.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Depends
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from .. import crud, database, schemas, storage
|
| 4 |
+
import io
|
| 5 |
+
|
| 6 |
+
router = APIRouter()
|
| 7 |
+
|
| 8 |
+
def get_db():
|
| 9 |
+
db = database.SessionLocal()
|
| 10 |
+
try:
|
| 11 |
+
yield db
|
| 12 |
+
finally:
|
| 13 |
+
db.close()
|
| 14 |
+
|
| 15 |
+
# --- Stub captioner for now ---
|
| 16 |
+
class CaptionerStub:
|
| 17 |
+
def generate(self, image_bytes: bytes) -> tuple[str,str,dict]:
|
| 18 |
+
text = "This is a fake caption."
|
| 19 |
+
model = "STUB_MODEL"
|
| 20 |
+
raw = {"stub": True}
|
| 21 |
+
return text, model, raw
|
| 22 |
+
|
| 23 |
+
cap = CaptionerStub()
|
| 24 |
+
|
| 25 |
+
@router.post("/maps/{map_id}/caption", response_model=schemas.CaptionOut)
|
| 26 |
+
def create_caption(map_id: str, db: Session = Depends(get_db)):
|
| 27 |
+
m = crud.get_map(db, map_id)
|
| 28 |
+
if not m:
|
| 29 |
+
raise HTTPException(404, "map not found")
|
| 30 |
+
|
| 31 |
+
# fetch image bytes from S3
|
| 32 |
+
url = storage.generate_presigned_url(m.file_key)
|
| 33 |
+
# (in real code, you might stream from S3 directly)
|
| 34 |
+
import requests
|
| 35 |
+
resp = requests.get(url)
|
| 36 |
+
img_bytes = resp.content
|
| 37 |
+
|
| 38 |
+
# generate caption
|
| 39 |
+
text, model, raw = cap.generate(img_bytes)
|
| 40 |
+
|
| 41 |
+
# insert into DB
|
| 42 |
+
c = crud.create_caption(db, map_id, model, raw, text)
|
| 43 |
+
return c
|
| 44 |
+
|
| 45 |
+
@router.get("/captions/{cap_id}", response_model=schemas.CaptionOut)
|
| 46 |
+
def get_caption(cap_id: str, db: Session = Depends(get_db)):
|
| 47 |
+
c = crud.get_caption(db, cap_id)
|
| 48 |
+
if not c:
|
| 49 |
+
raise HTTPException(404, "caption not found")
|
| 50 |
+
return c
|
py_backend/app/routers/metadata.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException, Depends
|
| 2 |
+
from sqlalchemy.orm import Session
|
| 3 |
+
from .. import crud, database, schemas
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
|
| 7 |
+
def get_db():
|
| 8 |
+
db = database.SessionLocal()
|
| 9 |
+
try:
|
| 10 |
+
yield db
|
| 11 |
+
finally:
|
| 12 |
+
db.close()
|
| 13 |
+
|
| 14 |
+
@router.put("/maps/{map_id}/metadata", response_model=schemas.CaptionOut)
|
| 15 |
+
def update_metadata(
|
| 16 |
+
map_id: str,
|
| 17 |
+
update: schemas.CaptionUpdate,
|
| 18 |
+
db: Session = Depends(get_db)
|
| 19 |
+
):
|
| 20 |
+
# we stored cap_id == map_id in Go; here cap_id is uuid on captions table
|
| 21 |
+
c = crud.update_caption(db, map_id, **update.dict())
|
| 22 |
+
if not c:
|
| 23 |
+
raise HTTPException(404, "caption not found")
|
| 24 |
+
return c
|
py_backend/app/routers/upload.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, UploadFile, Form, Depends, HTTPException
|
| 2 |
+
import io
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from .. import crud, schemas, storage, database
|
| 5 |
+
|
| 6 |
+
router = APIRouter()
|
| 7 |
+
|
| 8 |
+
def get_db():
|
| 9 |
+
db = database.SessionLocal()
|
| 10 |
+
try:
|
| 11 |
+
yield db
|
| 12 |
+
finally:
|
| 13 |
+
db.close()
|
| 14 |
+
|
| 15 |
+
@router.post("/", response_model=schemas.MapOut)
|
| 16 |
+
async def upload_map(
|
| 17 |
+
source: str = Form(...),
|
| 18 |
+
region: str = Form(...),
|
| 19 |
+
category: str = Form(...),
|
| 20 |
+
countries: list[str] = Form([]),
|
| 21 |
+
file: UploadFile = Form(...),
|
| 22 |
+
db: Session = Depends(get_db)
|
| 23 |
+
):
|
| 24 |
+
# 1) read & hash
|
| 25 |
+
content = await file.read()
|
| 26 |
+
sha = crud.hash_bytes(content)
|
| 27 |
+
|
| 28 |
+
# 2) upload to S3
|
| 29 |
+
key = storage.upload_fileobj(io.BytesIO(content), file.filename)
|
| 30 |
+
|
| 31 |
+
# 3) insert into DB
|
| 32 |
+
m = crud.create_map(db, source, region, category, key, sha, countries)
|
| 33 |
+
return m
|
py_backend/app/schemas.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from uuid import UUID
|
| 4 |
+
|
| 5 |
+
#ββ For the UploadPage ββ
|
| 6 |
+
class MapCreate(BaseModel):
|
| 7 |
+
source: str
|
| 8 |
+
region: str
|
| 9 |
+
category: str
|
| 10 |
+
countries: List[str] = []
|
| 11 |
+
|
| 12 |
+
class MapOut(BaseModel):
|
| 13 |
+
map_id: UUID
|
| 14 |
+
file_key: str
|
| 15 |
+
sha256: str
|
| 16 |
+
source: str
|
| 17 |
+
region: str
|
| 18 |
+
category: str
|
| 19 |
+
|
| 20 |
+
class Config:
|
| 21 |
+
orm_mode = True
|
| 22 |
+
|
| 23 |
+
#ββ For the caption endpoints ββ
|
| 24 |
+
class CaptionOut(BaseModel):
|
| 25 |
+
cap_id: UUID
|
| 26 |
+
map_id: UUID
|
| 27 |
+
model: str
|
| 28 |
+
raw_json: dict
|
| 29 |
+
generated: str
|
| 30 |
+
edited: Optional[str]
|
| 31 |
+
accuracy: Optional[int]
|
| 32 |
+
context: Optional[int]
|
| 33 |
+
usability: Optional[int]
|
| 34 |
+
|
| 35 |
+
class Config:
|
| 36 |
+
orm_mode = True
|
| 37 |
+
|
| 38 |
+
class CaptionUpdate(BaseModel):
|
| 39 |
+
edited: str
|
| 40 |
+
accuracy: int = Field(..., ge=0, le=100)
|
| 41 |
+
context: int = Field(..., ge=0, le=100)
|
| 42 |
+
usability:int = Field(..., ge=0, le=100)
|
py_backend/app/storage.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# app/storage.py
|
| 2 |
+
|
| 3 |
+
import io
|
| 4 |
+
import boto3
|
| 5 |
+
import botocore
|
| 6 |
+
from uuid import uuid4
|
| 7 |
+
from typing import BinaryIO
|
| 8 |
+
from .config import settings
|
| 9 |
+
|
| 10 |
+
# Initialize the S3/MinIO client
|
| 11 |
+
s3 = boto3.client(
|
| 12 |
+
"s3",
|
| 13 |
+
endpoint_url=settings.S3_ENDPOINT,
|
| 14 |
+
aws_access_key_id=settings.S3_ACCESS_KEY,
|
| 15 |
+
aws_secret_access_key=settings.S3_SECRET_KEY,
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
def upload_fileobj(fileobj: BinaryIO, filename: str) -> str:
|
| 19 |
+
"""
|
| 20 |
+
Uploads a file-like object to the configured S3 bucket,
|
| 21 |
+
automatically creating the bucket if it doesn't exist.
|
| 22 |
+
Returns the object key.
|
| 23 |
+
"""
|
| 24 |
+
key = f"maps/{uuid4()}_{filename}"
|
| 25 |
+
|
| 26 |
+
# 1) Ensure the bucket exists
|
| 27 |
+
try:
|
| 28 |
+
s3.head_bucket(Bucket=settings.S3_BUCKET)
|
| 29 |
+
except botocore.exceptions.ClientError as e:
|
| 30 |
+
# A 404 or 403 here means the bucket doesn't exist or no access:
|
| 31 |
+
s3.create_bucket(Bucket=settings.S3_BUCKET)
|
| 32 |
+
|
| 33 |
+
# 2) Perform the upload
|
| 34 |
+
fileobj.seek(0) # rewind in case .read() was called
|
| 35 |
+
s3.upload_fileobj(fileobj, settings.S3_BUCKET, key)
|
| 36 |
+
|
| 37 |
+
return key
|
| 38 |
+
|
| 39 |
+
def generate_presigned_url(key: str, expires_in: int = 3600) -> str:
|
| 40 |
+
"""
|
| 41 |
+
Returns a presigned URL for GETting the object.
|
| 42 |
+
"""
|
| 43 |
+
return s3.generate_presigned_url(
|
| 44 |
+
ClientMethod="get_object",
|
| 45 |
+
Params={"Bucket": settings.S3_BUCKET, "Key": key},
|
| 46 |
+
ExpiresIn=expires_in,
|
| 47 |
+
)
|
py_backend/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi
|
| 2 |
+
uvicorn[standard]
|
| 3 |
+
sqlalchemy
|
| 4 |
+
alembic
|
| 5 |
+
psycopg2-binary
|
| 6 |
+
boto3
|
| 7 |
+
python-dotenv
|
| 8 |
+
pydantic<2.0.0
|
| 9 |
+
openai # or other VLM client
|