SCGR commited on
Commit
09ecaf7
Β·
1 Parent(s): 0ad5d66

transition to python backend

Browse files
.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
- - uses: actions/checkout@v4
 
11
 
12
  # --- Front-end: React ---
13
- - uses: actions/setup-node@v4
14
- with: { node-version: 20 }
 
 
15
 
16
- - run: |
 
17
  cd frontend
18
- npm ci # 3) install deps
19
- npm run build # 4) make sure it builds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
21
- # --- Back-end: Go ---
22
- - uses: actions/setup-go@v5
23
- with: { go-version: 1.22 }
 
 
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 => Go/Python => postgreSQL => fly.io
 
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(&params); 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
- db:
3
  image: postgres:16
4
- restart: unless-stopped
5
  environment:
6
  POSTGRES_USER: promptaid
7
  POSTGRES_PASSWORD: promptaid
8
  POSTGRES_DB: promptaid
9
  ports:
10
- - "5432:5432" # host β†’ container
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  volumes:
12
- - db_data:/var/lib/postgresql/data
 
 
13
 
14
  volumes:
15
- db_data:
 
 
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, useNavigate } 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
- 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 = 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
  /* 2) caption */
97
  const capRes = await fetch(
98
- `/api/maps/${mapJson.mapId}/caption`,
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
- console.log('βš™οΈΒ VITE CONFIG LOADED')
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