Spaces:
Running
Running
database images table transformation
Browse files- frontend/src/pages/AnalyticsPage/AnalyticsPage.tsx +1 -1
- frontend/src/pages/ExplorePage/ExplorePage.tsx +1 -1
- py_backend/alembic/versions/0016_restructure_images_to_three_tables.py +206 -0
- py_backend/app/crud.py +49 -44
- py_backend/app/models.py +55 -22
- py_backend/app/routers/admin.py +5 -5
- py_backend/app/routers/caption.py +115 -86
- py_backend/app/routers/images.py +2 -12
- py_backend/app/routers/metadata.py +5 -5
- py_backend/app/routers/upload.py +97 -20
- py_backend/app/schemas.py +37 -2
- py_backend/tests/integration_tests/test_explore_page.py +2 -2
- py_backend/tests/integration_tests/test_upload_flow.py +1 -7
frontend/src/pages/AnalyticsPage/AnalyticsPage.tsx
CHANGED
|
@@ -210,7 +210,7 @@ export default function AnalyticsPage() {
|
|
| 210 |
if (map.context != null) ctr.avgContext += map.context;
|
| 211 |
if (map.usability != null) ctr.avgUsability += map.usability;
|
| 212 |
|
| 213 |
-
// Calculate edit time if both timestamps exist
|
| 214 |
if (map.created_at && map.updated_at) {
|
| 215 |
const created = new Date(map.created_at).getTime();
|
| 216 |
const updated = new Date(map.updated_at).getTime();
|
|
|
|
| 210 |
if (map.context != null) ctr.avgContext += map.context;
|
| 211 |
if (map.usability != null) ctr.avgUsability += map.usability;
|
| 212 |
|
| 213 |
+
// Calculate edit time if both timestamps exist (now from captions)
|
| 214 |
if (map.created_at && map.updated_at) {
|
| 215 |
const created = new Date(map.created_at).getTime();
|
| 216 |
const updated = new Date(map.updated_at).getTime();
|
frontend/src/pages/ExplorePage/ExplorePage.tsx
CHANGED
|
@@ -62,7 +62,7 @@ export default function ExplorePage() {
|
|
| 62 |
|
| 63 |
const fetchCaptions = () => {
|
| 64 |
setIsLoadingContent(true);
|
| 65 |
-
fetch('/api/captions')
|
| 66 |
.then(r => {
|
| 67 |
if (!r.ok) {
|
| 68 |
throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
|
|
|
| 62 |
|
| 63 |
const fetchCaptions = () => {
|
| 64 |
setIsLoadingContent(true);
|
| 65 |
+
fetch('/api/captions/legacy')
|
| 66 |
.then(r => {
|
| 67 |
if (!r.ok) {
|
| 68 |
throw new Error(`HTTP ${r.status}: ${r.statusText}`);
|
py_backend/alembic/versions/0016_restructure_images_to_three_tables.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Restructure images table into three tables: Images, Captions, and Images_captions
|
| 2 |
+
|
| 3 |
+
Revision ID: 0016
|
| 4 |
+
Revises: 0015
|
| 5 |
+
Create Date: 2024-01-01 00:00:00.000000
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from alembic import op
|
| 9 |
+
import sqlalchemy as sa
|
| 10 |
+
from sqlalchemy.dialects import postgresql
|
| 11 |
+
|
| 12 |
+
# revision identifiers, used by Alembic.
|
| 13 |
+
revision = '0016'
|
| 14 |
+
down_revision = '0015'
|
| 15 |
+
branch_labels = None
|
| 16 |
+
depends_on = None
|
| 17 |
+
|
| 18 |
+
def upgrade():
|
| 19 |
+
# Ensure pgcrypto extension is available
|
| 20 |
+
op.execute('CREATE EXTENSION IF NOT EXISTS pgcrypto;')
|
| 21 |
+
|
| 22 |
+
# First, ensure the old images table has all the caption columns
|
| 23 |
+
# (in case they were removed by a previous partial migration)
|
| 24 |
+
connection = op.get_bind()
|
| 25 |
+
|
| 26 |
+
# Check if caption columns exist and add them if they don't
|
| 27 |
+
caption_columns = [
|
| 28 |
+
('title', 'VARCHAR'),
|
| 29 |
+
('prompt', 'VARCHAR'),
|
| 30 |
+
('model', 'VARCHAR'),
|
| 31 |
+
('schema_id', 'VARCHAR'),
|
| 32 |
+
('raw_json', 'JSONB'),
|
| 33 |
+
('generated', 'TEXT'),
|
| 34 |
+
('edited', 'TEXT'),
|
| 35 |
+
('accuracy', 'SMALLINT'),
|
| 36 |
+
('context', 'SMALLINT'),
|
| 37 |
+
('usability', 'SMALLINT'),
|
| 38 |
+
('starred', 'BOOLEAN'),
|
| 39 |
+
('updated_at', 'TIMESTAMP WITH TIME ZONE'),
|
| 40 |
+
('created_at', 'TIMESTAMP WITH TIME ZONE')
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
for col_name, col_type in caption_columns:
|
| 44 |
+
result = connection.execute(sa.text(f"""
|
| 45 |
+
SELECT COUNT(*) FROM information_schema.columns
|
| 46 |
+
WHERE table_name = 'images' AND column_name = '{col_name}'
|
| 47 |
+
"""))
|
| 48 |
+
if result.scalar() == 0:
|
| 49 |
+
if col_type == 'BOOLEAN':
|
| 50 |
+
op.add_column('images', sa.Column(col_name, sa.Boolean(), nullable=True, server_default='false'))
|
| 51 |
+
elif col_type == 'TIMESTAMP WITH TIME ZONE':
|
| 52 |
+
op.add_column('images', sa.Column(col_name, sa.TIMESTAMP(timezone=True), nullable=True))
|
| 53 |
+
else:
|
| 54 |
+
op.add_column('images', sa.Column(col_name, sa.String() if col_type == 'VARCHAR' else sa.Text() if col_type == 'TEXT' else sa.SmallInteger(), nullable=True))
|
| 55 |
+
|
| 56 |
+
# Create new Images table with temporary caption_id column for mapping
|
| 57 |
+
op.create_table(
|
| 58 |
+
'images_new',
|
| 59 |
+
sa.Column('image_id', postgresql.UUID(as_uuid=True),
|
| 60 |
+
server_default=sa.text('gen_random_uuid()'),
|
| 61 |
+
primary_key=True),
|
| 62 |
+
sa.Column('file_key', sa.String(), nullable=False),
|
| 63 |
+
sa.Column('sha256', sa.String(), nullable=False),
|
| 64 |
+
sa.Column('source', sa.String(), sa.ForeignKey('sources.s_code'), nullable=True),
|
| 65 |
+
sa.Column('event_type', sa.String(), sa.ForeignKey('event_types.t_code'), nullable=False),
|
| 66 |
+
sa.Column('epsg', sa.String(), sa.ForeignKey('spatial_references.epsg'), nullable=False),
|
| 67 |
+
sa.Column('image_type', sa.String(), sa.ForeignKey('image_types.image_type'), nullable=False),
|
| 68 |
+
sa.Column('captured_at', sa.TIMESTAMP(timezone=True), nullable=True),
|
| 69 |
+
sa.Column('center_lon', sa.Float(precision=53), nullable=True),
|
| 70 |
+
sa.Column('center_lat', sa.Float(precision=53), nullable=True),
|
| 71 |
+
sa.Column('amsl_m', sa.Float(precision=53), nullable=True),
|
| 72 |
+
sa.Column('agl_m', sa.Float(precision=53), nullable=True),
|
| 73 |
+
sa.Column('heading_deg', sa.Float(precision=53), nullable=True),
|
| 74 |
+
sa.Column('yaw_deg', sa.Float(precision=53), nullable=True),
|
| 75 |
+
sa.Column('pitch_deg', sa.Float(precision=53), nullable=True),
|
| 76 |
+
sa.Column('roll_deg', sa.Float(precision=53), nullable=True),
|
| 77 |
+
sa.Column('rtk_fix', sa.Boolean(), nullable=True),
|
| 78 |
+
sa.Column('std_h_m', sa.Float(precision=53), nullable=True),
|
| 79 |
+
sa.Column('std_v_m', sa.Float(precision=53), nullable=True),
|
| 80 |
+
sa.Column('temp_caption_id', postgresql.UUID(as_uuid=True), nullable=True),
|
| 81 |
+
|
| 82 |
+
sa.CheckConstraint('center_lat IS NULL OR (center_lat BETWEEN -90 AND 90)', name='chk_images_new_center_lat'),
|
| 83 |
+
sa.CheckConstraint('center_lon IS NULL OR (center_lon BETWEEN -180 AND 180)', name='chk_images_new_center_lon'),
|
| 84 |
+
sa.CheckConstraint('heading_deg IS NULL OR (heading_deg >= 0 AND heading_deg <= 360)', name='chk_images_new_heading_deg'),
|
| 85 |
+
sa.CheckConstraint('pitch_deg IS NULL OR (pitch_deg BETWEEN -90 AND 90)', name='chk_images_new_pitch_deg'),
|
| 86 |
+
sa.CheckConstraint('yaw_deg IS NULL OR (yaw_deg BETWEEN -180 AND 180)', name='chk_images_new_yaw_deg'),
|
| 87 |
+
sa.CheckConstraint('roll_deg IS NULL OR (roll_deg BETWEEN -180 AND 180)', name='chk_images_new_roll_deg'),
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
# Create new Captions table
|
| 91 |
+
op.create_table(
|
| 92 |
+
'captions',
|
| 93 |
+
sa.Column('caption_id', postgresql.UUID(as_uuid=True),
|
| 94 |
+
server_default=sa.text('gen_random_uuid()'),
|
| 95 |
+
primary_key=True),
|
| 96 |
+
sa.Column('title', sa.String(), nullable=True),
|
| 97 |
+
sa.Column('prompt', sa.String(), sa.ForeignKey('prompts.p_code'), nullable=True),
|
| 98 |
+
sa.Column('model', sa.String(), sa.ForeignKey('models.m_code'), nullable=True),
|
| 99 |
+
sa.Column('schema_id', sa.String(), sa.ForeignKey('json_schemas.schema_id'), nullable=True),
|
| 100 |
+
sa.Column('raw_json', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
|
| 101 |
+
sa.Column('generated', sa.Text(), nullable=True),
|
| 102 |
+
sa.Column('edited', sa.Text(), nullable=True),
|
| 103 |
+
sa.Column('accuracy', sa.SmallInteger()),
|
| 104 |
+
sa.Column('context', sa.SmallInteger()),
|
| 105 |
+
sa.Column('usability', sa.SmallInteger()),
|
| 106 |
+
sa.Column('starred', sa.Boolean(), server_default=sa.text('false')),
|
| 107 |
+
sa.Column('created_at', sa.TIMESTAMP(timezone=True), server_default=sa.text('NOW()'), nullable=False),
|
| 108 |
+
sa.Column('updated_at', sa.TIMESTAMP(timezone=True), nullable=True),
|
| 109 |
+
|
| 110 |
+
sa.CheckConstraint('accuracy IS NULL OR (accuracy BETWEEN 0 AND 100)', name='chk_captions_accuracy'),
|
| 111 |
+
sa.CheckConstraint('context IS NULL OR (context BETWEEN 0 AND 100)', name='chk_captions_context'),
|
| 112 |
+
sa.CheckConstraint('usability IS NULL OR (usability BETWEEN 0 AND 100)', name='chk_captions_usability')
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
# Create Images_captions junction table
|
| 116 |
+
op.create_table(
|
| 117 |
+
'images_captions',
|
| 118 |
+
sa.Column('image_id', postgresql.UUID(as_uuid=True), nullable=False),
|
| 119 |
+
sa.Column('caption_id', postgresql.UUID(as_uuid=True), nullable=False),
|
| 120 |
+
sa.PrimaryKeyConstraint('image_id', 'caption_id', name='pk_images_captions'),
|
| 121 |
+
sa.ForeignKeyConstraint(['image_id'], ['images_new.image_id'], ondelete='CASCADE'),
|
| 122 |
+
sa.ForeignKeyConstraint(['caption_id'], ['captions.caption_id'], ondelete='CASCADE')
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Add indexes for performance
|
| 126 |
+
op.create_index('ix_images_captions_image_id', 'images_captions', ['image_id'])
|
| 127 |
+
op.create_index('ix_images_captions_caption_id', 'images_captions', ['caption_id'])
|
| 128 |
+
|
| 129 |
+
# Migrate data from old images table to new structure with temporary caption_id
|
| 130 |
+
op.execute("""
|
| 131 |
+
INSERT INTO images_new (
|
| 132 |
+
image_id, file_key, sha256, source, event_type, epsg, image_type, captured_at,
|
| 133 |
+
center_lon, center_lat, amsl_m, agl_m, heading_deg, yaw_deg, pitch_deg, roll_deg, rtk_fix, std_h_m, std_v_m,
|
| 134 |
+
temp_caption_id
|
| 135 |
+
)
|
| 136 |
+
SELECT
|
| 137 |
+
image_id, file_key, sha256, source, event_type, epsg, image_type, captured_at,
|
| 138 |
+
center_lon, center_lat, amsl_m, agl_m, heading_deg, yaw_deg, pitch_deg, roll_deg, rtk_fix, std_h_m, std_v_m,
|
| 139 |
+
CASE
|
| 140 |
+
WHEN title IS NOT NULL OR prompt IS NOT NULL OR model IS NOT NULL OR schema_id IS NOT NULL
|
| 141 |
+
OR raw_json IS NOT NULL OR generated IS NOT NULL OR edited IS NOT NULL
|
| 142 |
+
OR accuracy IS NOT NULL OR context IS NOT NULL OR usability IS NOT NULL
|
| 143 |
+
OR starred = true OR updated_at IS NOT NULL
|
| 144 |
+
THEN gen_random_uuid()
|
| 145 |
+
ELSE NULL
|
| 146 |
+
END as temp_caption_id
|
| 147 |
+
FROM images
|
| 148 |
+
""")
|
| 149 |
+
|
| 150 |
+
# Migrate caption data using the same temp_caption_id
|
| 151 |
+
op.execute("""
|
| 152 |
+
INSERT INTO captions (
|
| 153 |
+
caption_id, title, prompt, model, schema_id, raw_json, generated, edited,
|
| 154 |
+
accuracy, context, usability, starred, created_at, updated_at
|
| 155 |
+
)
|
| 156 |
+
SELECT
|
| 157 |
+
i_new.temp_caption_id, i.title, i.prompt, i.model, i.schema_id, i.raw_json, i.generated, i.edited,
|
| 158 |
+
i.accuracy, i.context, i.usability, i.starred, i.created_at, i.updated_at
|
| 159 |
+
FROM images i
|
| 160 |
+
JOIN images_new i_new ON i.image_id = i_new.image_id
|
| 161 |
+
WHERE i_new.temp_caption_id IS NOT NULL
|
| 162 |
+
""")
|
| 163 |
+
|
| 164 |
+
# Create relationships using the temp_caption_id
|
| 165 |
+
op.execute("""
|
| 166 |
+
INSERT INTO images_captions (image_id, caption_id)
|
| 167 |
+
SELECT image_id, temp_caption_id
|
| 168 |
+
FROM images_new
|
| 169 |
+
WHERE temp_caption_id IS NOT NULL
|
| 170 |
+
""")
|
| 171 |
+
|
| 172 |
+
# Drop the temporary column
|
| 173 |
+
op.drop_column('images_new', 'temp_caption_id')
|
| 174 |
+
|
| 175 |
+
# Save image_countries data to temp table before dropping
|
| 176 |
+
op.execute("CREATE TEMP TABLE tmp_image_countries AS TABLE image_countries")
|
| 177 |
+
|
| 178 |
+
# Drop old tables and constraints
|
| 179 |
+
op.drop_table('image_countries')
|
| 180 |
+
op.drop_index('ix_images_created_at', table_name='images', if_exists=True)
|
| 181 |
+
op.drop_table('images')
|
| 182 |
+
|
| 183 |
+
# Rename new table to 'images'
|
| 184 |
+
op.rename_table('images_new', 'images')
|
| 185 |
+
|
| 186 |
+
# Recreate image_countries table with new image_id reference
|
| 187 |
+
op.create_table(
|
| 188 |
+
'image_countries',
|
| 189 |
+
sa.Column('image_id', postgresql.UUID(as_uuid=True), nullable=False),
|
| 190 |
+
sa.Column('c_code', sa.CHAR(length=2), nullable=False),
|
| 191 |
+
sa.PrimaryKeyConstraint('image_id', 'c_code', name='pk_image_countries'),
|
| 192 |
+
sa.ForeignKeyConstraint(['image_id'], ['images.image_id'], ondelete='CASCADE'),
|
| 193 |
+
sa.ForeignKeyConstraint(['c_code'], ['countries.c_code'])
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
# Restore image_countries data
|
| 197 |
+
op.execute("""
|
| 198 |
+
INSERT INTO image_countries (image_id, c_code)
|
| 199 |
+
SELECT image_id, c_code FROM tmp_image_countries
|
| 200 |
+
""")
|
| 201 |
+
|
| 202 |
+
# Recreate index with proper name
|
| 203 |
+
op.create_index('ix_images_captured_at', 'images', ['captured_at'])
|
| 204 |
+
|
| 205 |
+
def downgrade():
|
| 206 |
+
raise NotImplementedError("Downgrade not supported for 0016 - this is a structural table split")
|
py_backend/app/crud.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import io, hashlib
|
| 2 |
-
from typing import Optional
|
| 3 |
from sqlalchemy.orm import Session, joinedload
|
| 4 |
from . import models, schemas
|
| 5 |
from fastapi import HTTPException
|
|
@@ -62,21 +62,23 @@ def create_image(db: Session, src, type_code, key, sha, countries: list[str], ep
|
|
| 62 |
return img
|
| 63 |
|
| 64 |
def get_images(db: Session):
|
| 65 |
-
"""Get all images with their countries"""
|
| 66 |
return (
|
| 67 |
db.query(models.Images)
|
| 68 |
.options(
|
| 69 |
joinedload(models.Images.countries),
|
|
|
|
| 70 |
)
|
| 71 |
.all()
|
| 72 |
)
|
| 73 |
|
| 74 |
def get_image(db: Session, image_id: str):
|
| 75 |
-
"""Get a single image by ID with its countries"""
|
| 76 |
return (
|
| 77 |
db.query(models.Images)
|
| 78 |
.options(
|
| 79 |
joinedload(models.Images.countries),
|
|
|
|
| 80 |
)
|
| 81 |
.filter(models.Images.image_id == image_id)
|
| 82 |
.first()
|
|
@@ -100,35 +102,49 @@ def create_caption(db: Session, image_id, title, prompt, model_code, raw_json, t
|
|
| 100 |
if img.image_type == "drone_image":
|
| 101 |
schema_id = "drone_caption@1.0.0"
|
| 102 |
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
print(f"About to commit caption to database...")
|
| 112 |
db.commit()
|
| 113 |
print(f"Caption commit successful!")
|
| 114 |
-
db.refresh(
|
| 115 |
print(f"Caption created successfully for image: {img.image_id}")
|
| 116 |
-
return
|
| 117 |
|
| 118 |
-
def get_caption(db: Session,
|
| 119 |
-
"""Get caption data for a specific
|
| 120 |
-
return db.get(models.
|
| 121 |
|
| 122 |
def get_captions_by_image(db: Session, image_id: str):
|
| 123 |
-
"""Get
|
| 124 |
img = db.get(models.Images, image_id)
|
| 125 |
-
if img
|
| 126 |
-
return
|
| 127 |
return []
|
| 128 |
|
| 129 |
def get_all_captions_with_images(db: Session):
|
| 130 |
-
"""Get all
|
| 131 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
|
| 133 |
def get_prompts(db: Session):
|
| 134 |
"""Get all available prompts"""
|
|
@@ -253,37 +269,26 @@ def update_prompt(db: Session, p_code: str, prompt_update: schemas.PromptUpdate)
|
|
| 253 |
db.refresh(prompt)
|
| 254 |
return prompt
|
| 255 |
|
| 256 |
-
def update_caption(db: Session,
|
| 257 |
-
"""Update caption data for
|
| 258 |
-
|
| 259 |
-
if not
|
| 260 |
return None
|
| 261 |
|
| 262 |
for field, value in update.dict(exclude_unset=True).items():
|
| 263 |
-
setattr(
|
| 264 |
|
| 265 |
db.commit()
|
| 266 |
-
db.refresh(
|
| 267 |
-
return
|
| 268 |
|
| 269 |
-
def delete_caption(db: Session,
|
| 270 |
-
"""Delete caption data for
|
| 271 |
-
|
| 272 |
-
if not
|
| 273 |
return False
|
| 274 |
|
| 275 |
-
|
| 276 |
-
img.prompt = None
|
| 277 |
-
img.model = None
|
| 278 |
-
img.schema_id = None
|
| 279 |
-
img.raw_json = None
|
| 280 |
-
img.generated = None
|
| 281 |
-
img.edited = None
|
| 282 |
-
img.accuracy = None
|
| 283 |
-
img.context = None
|
| 284 |
-
img.usability = None
|
| 285 |
-
img.starred = False
|
| 286 |
-
|
| 287 |
db.commit()
|
| 288 |
return True
|
| 289 |
|
|
@@ -373,4 +378,4 @@ def get_schema(db: Session, schema_id: str):
|
|
| 373 |
|
| 374 |
def get_recent_images_with_validation(db: Session, limit: int = 100):
|
| 375 |
"""Get recent images with validation info"""
|
| 376 |
-
return db.query(models.Images).order_by(models.Images.
|
|
|
|
| 1 |
import io, hashlib
|
| 2 |
+
from typing import Optional, List
|
| 3 |
from sqlalchemy.orm import Session, joinedload
|
| 4 |
from . import models, schemas
|
| 5 |
from fastapi import HTTPException
|
|
|
|
| 62 |
return img
|
| 63 |
|
| 64 |
def get_images(db: Session):
|
| 65 |
+
"""Get all images with their countries and captions"""
|
| 66 |
return (
|
| 67 |
db.query(models.Images)
|
| 68 |
.options(
|
| 69 |
joinedload(models.Images.countries),
|
| 70 |
+
joinedload(models.Images.captions),
|
| 71 |
)
|
| 72 |
.all()
|
| 73 |
)
|
| 74 |
|
| 75 |
def get_image(db: Session, image_id: str):
|
| 76 |
+
"""Get a single image by ID with its countries and captions"""
|
| 77 |
return (
|
| 78 |
db.query(models.Images)
|
| 79 |
.options(
|
| 80 |
joinedload(models.Images.countries),
|
| 81 |
+
joinedload(models.Images.captions),
|
| 82 |
)
|
| 83 |
.filter(models.Images.image_id == image_id)
|
| 84 |
.first()
|
|
|
|
| 102 |
if img.image_type == "drone_image":
|
| 103 |
schema_id = "drone_caption@1.0.0"
|
| 104 |
|
| 105 |
+
caption = models.Captions(
|
| 106 |
+
title=title,
|
| 107 |
+
prompt=prompt,
|
| 108 |
+
model=model_code,
|
| 109 |
+
schema_id=schema_id,
|
| 110 |
+
raw_json=raw_json,
|
| 111 |
+
generated=text,
|
| 112 |
+
edited=text
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
db.add(caption)
|
| 116 |
+
db.flush()
|
| 117 |
+
|
| 118 |
+
# Link caption to image
|
| 119 |
+
img.captions.append(caption)
|
| 120 |
|
| 121 |
print(f"About to commit caption to database...")
|
| 122 |
db.commit()
|
| 123 |
print(f"Caption commit successful!")
|
| 124 |
+
db.refresh(caption)
|
| 125 |
print(f"Caption created successfully for image: {img.image_id}")
|
| 126 |
+
return caption
|
| 127 |
|
| 128 |
+
def get_caption(db: Session, caption_id: str):
|
| 129 |
+
"""Get caption data for a specific caption ID"""
|
| 130 |
+
return db.get(models.Captions, caption_id)
|
| 131 |
|
| 132 |
def get_captions_by_image(db: Session, image_id: str):
|
| 133 |
+
"""Get all captions for a specific image"""
|
| 134 |
img = db.get(models.Images, image_id)
|
| 135 |
+
if img:
|
| 136 |
+
return img.captions
|
| 137 |
return []
|
| 138 |
|
| 139 |
def get_all_captions_with_images(db: Session):
|
| 140 |
+
"""Get all captions with their associated images"""
|
| 141 |
+
return (
|
| 142 |
+
db.query(models.Captions)
|
| 143 |
+
.options(
|
| 144 |
+
joinedload(models.Captions.images),
|
| 145 |
+
)
|
| 146 |
+
.all()
|
| 147 |
+
)
|
| 148 |
|
| 149 |
def get_prompts(db: Session):
|
| 150 |
"""Get all available prompts"""
|
|
|
|
| 269 |
db.refresh(prompt)
|
| 270 |
return prompt
|
| 271 |
|
| 272 |
+
def update_caption(db: Session, caption_id: str, update: schemas.CaptionUpdate):
|
| 273 |
+
"""Update caption data for a caption"""
|
| 274 |
+
caption = db.get(models.Captions, caption_id)
|
| 275 |
+
if not caption:
|
| 276 |
return None
|
| 277 |
|
| 278 |
for field, value in update.dict(exclude_unset=True).items():
|
| 279 |
+
setattr(caption, field, value)
|
| 280 |
|
| 281 |
db.commit()
|
| 282 |
+
db.refresh(caption)
|
| 283 |
+
return caption
|
| 284 |
|
| 285 |
+
def delete_caption(db: Session, caption_id: str):
|
| 286 |
+
"""Delete caption data for a caption"""
|
| 287 |
+
caption = db.get(models.Captions, caption_id)
|
| 288 |
+
if not caption:
|
| 289 |
return False
|
| 290 |
|
| 291 |
+
db.delete(caption)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
db.commit()
|
| 293 |
return True
|
| 294 |
|
|
|
|
| 378 |
|
| 379 |
def get_recent_images_with_validation(db: Session, limit: int = 100):
|
| 380 |
"""Get recent images with validation info"""
|
| 381 |
+
return db.query(models.Images).order_by(models.Images.captured_at.desc()).limit(limit).all()
|
py_backend/app/models.py
CHANGED
|
@@ -24,6 +24,22 @@ image_countries = Table(
|
|
| 24 |
),
|
| 25 |
)
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
class Source(Base):
|
| 28 |
__tablename__ = "sources"
|
| 29 |
s_code = Column(String, primary_key=True)
|
|
@@ -90,9 +106,12 @@ class JSONSchema(Base):
|
|
| 90 |
class Images(Base):
|
| 91 |
__tablename__ = "images"
|
| 92 |
__table_args__ = (
|
| 93 |
-
CheckConstraint('
|
| 94 |
-
CheckConstraint('
|
| 95 |
-
CheckConstraint('
|
|
|
|
|
|
|
|
|
|
| 96 |
)
|
| 97 |
|
| 98 |
image_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
@@ -102,27 +121,9 @@ class Images(Base):
|
|
| 102 |
event_type = Column(String, ForeignKey("event_types.t_code"), nullable=False)
|
| 103 |
epsg = Column(String, ForeignKey("spatial_references.epsg"), nullable=False)
|
| 104 |
image_type = Column(String, ForeignKey("image_types.image_type"), nullable=False)
|
| 105 |
-
created_at = Column(TIMESTAMP(timezone=True), default=datetime.datetime.utcnow)
|
| 106 |
captured_at = Column(TIMESTAMP(timezone=True))
|
| 107 |
|
| 108 |
-
|
| 109 |
-
prompt = Column(String, ForeignKey("prompts.p_code"), nullable=True)
|
| 110 |
-
model = Column(String, ForeignKey("models.m_code"), nullable=True)
|
| 111 |
-
schema_id = Column(String, ForeignKey("json_schemas.schema_id"), nullable=True)
|
| 112 |
-
raw_json = Column(JSONB, nullable=True)
|
| 113 |
-
generated = Column(Text, nullable=True)
|
| 114 |
-
edited = Column(Text)
|
| 115 |
-
accuracy = Column(SmallInteger)
|
| 116 |
-
context = Column(SmallInteger)
|
| 117 |
-
usability = Column(SmallInteger)
|
| 118 |
-
starred = Column(Boolean, default=False)
|
| 119 |
-
updated_at = Column(TIMESTAMP(timezone=True), onupdate=datetime.datetime.utcnow)
|
| 120 |
-
|
| 121 |
-
countries = relationship("Country", secondary=image_countries, backref="images")
|
| 122 |
-
schema = relationship("JSONSchema")
|
| 123 |
-
model_r = relationship("Models", foreign_keys=[model])
|
| 124 |
-
prompt_r = relationship("Prompts", foreign_keys=[prompt])
|
| 125 |
-
|
| 126 |
center_lon = Column(Float)
|
| 127 |
center_lat = Column(Float)
|
| 128 |
amsl_m = Column(Float)
|
|
@@ -134,3 +135,35 @@ class Images(Base):
|
|
| 134 |
rtk_fix = Column(Boolean)
|
| 135 |
std_h_m = Column(Float)
|
| 136 |
std_v_m = Column(Float)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
),
|
| 25 |
)
|
| 26 |
|
| 27 |
+
images_captions = Table(
|
| 28 |
+
"images_captions", Base.metadata,
|
| 29 |
+
Column(
|
| 30 |
+
"image_id",
|
| 31 |
+
UUID(as_uuid=True),
|
| 32 |
+
ForeignKey("images.image_id", ondelete="CASCADE"),
|
| 33 |
+
primary_key=True,
|
| 34 |
+
),
|
| 35 |
+
Column(
|
| 36 |
+
"caption_id",
|
| 37 |
+
UUID(as_uuid=True),
|
| 38 |
+
ForeignKey("captions.caption_id", ondelete="CASCADE"),
|
| 39 |
+
primary_key=True,
|
| 40 |
+
),
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
class Source(Base):
|
| 44 |
__tablename__ = "sources"
|
| 45 |
s_code = Column(String, primary_key=True)
|
|
|
|
| 106 |
class Images(Base):
|
| 107 |
__tablename__ = "images"
|
| 108 |
__table_args__ = (
|
| 109 |
+
CheckConstraint('center_lat IS NULL OR (center_lat BETWEEN -90 AND 90)', name='chk_images_center_lat'),
|
| 110 |
+
CheckConstraint('center_lon IS NULL OR (center_lon BETWEEN -180 AND 180)', name='chk_images_center_lon'),
|
| 111 |
+
CheckConstraint('heading_deg IS NULL OR (heading_deg >= 0 AND heading_deg <= 360)', name='chk_images_heading_deg'),
|
| 112 |
+
CheckConstraint('pitch_deg IS NULL OR (pitch_deg BETWEEN -90 AND 90)', name='chk_images_pitch_deg'),
|
| 113 |
+
CheckConstraint('yaw_deg IS NULL OR (yaw_deg BETWEEN -180 AND 180)', name='chk_images_yaw_deg'),
|
| 114 |
+
CheckConstraint('roll_deg IS NULL OR (roll_deg BETWEEN -180 AND 180)', name='chk_images_roll_deg'),
|
| 115 |
)
|
| 116 |
|
| 117 |
image_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
|
|
| 121 |
event_type = Column(String, ForeignKey("event_types.t_code"), nullable=False)
|
| 122 |
epsg = Column(String, ForeignKey("spatial_references.epsg"), nullable=False)
|
| 123 |
image_type = Column(String, ForeignKey("image_types.image_type"), nullable=False)
|
|
|
|
| 124 |
captured_at = Column(TIMESTAMP(timezone=True))
|
| 125 |
|
| 126 |
+
# Drone-specific fields
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
center_lon = Column(Float)
|
| 128 |
center_lat = Column(Float)
|
| 129 |
amsl_m = Column(Float)
|
|
|
|
| 135 |
rtk_fix = Column(Boolean)
|
| 136 |
std_h_m = Column(Float)
|
| 137 |
std_v_m = Column(Float)
|
| 138 |
+
|
| 139 |
+
# Relationships
|
| 140 |
+
countries = relationship("Country", secondary=image_countries, backref="images")
|
| 141 |
+
captions = relationship("Captions", secondary=images_captions, backref="images")
|
| 142 |
+
|
| 143 |
+
class Captions(Base):
|
| 144 |
+
__tablename__ = "captions"
|
| 145 |
+
__table_args__ = (
|
| 146 |
+
CheckConstraint('accuracy IS NULL OR (accuracy BETWEEN 0 AND 100)', name='chk_captions_accuracy'),
|
| 147 |
+
CheckConstraint('context IS NULL OR (context BETWEEN 0 AND 100)', name='chk_captions_context'),
|
| 148 |
+
CheckConstraint('usability IS NULL OR (usability BETWEEN 0 AND 100)', name='chk_captions_usability'),
|
| 149 |
+
)
|
| 150 |
+
|
| 151 |
+
caption_id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
| 152 |
+
title = Column(String, nullable=True)
|
| 153 |
+
prompt = Column(String, ForeignKey("prompts.p_code"), nullable=True)
|
| 154 |
+
model = Column(String, ForeignKey("models.m_code"), nullable=True)
|
| 155 |
+
schema_id = Column(String, ForeignKey("json_schemas.schema_id"), nullable=True)
|
| 156 |
+
raw_json = Column(JSONB, nullable=True)
|
| 157 |
+
generated = Column(Text, nullable=True)
|
| 158 |
+
edited = Column(Text)
|
| 159 |
+
accuracy = Column(SmallInteger)
|
| 160 |
+
context = Column(SmallInteger)
|
| 161 |
+
usability = Column(SmallInteger)
|
| 162 |
+
starred = Column(Boolean, default=False)
|
| 163 |
+
created_at = Column(TIMESTAMP(timezone=True), default=datetime.datetime.utcnow)
|
| 164 |
+
updated_at = Column(TIMESTAMP(timezone=True), onupdate=datetime.datetime.utcnow)
|
| 165 |
+
|
| 166 |
+
# Relationships
|
| 167 |
+
schema = relationship("JSONSchema")
|
| 168 |
+
model_r = relationship("Models", foreign_keys=[model])
|
| 169 |
+
prompt_r = relationship("Prompts", foreign_keys=[prompt])
|
py_backend/app/routers/admin.py
CHANGED
|
@@ -290,13 +290,13 @@ async def delete_model(
|
|
| 290 |
detail=f"Model '{model_code}' not found"
|
| 291 |
)
|
| 292 |
|
| 293 |
-
# Check if model is being used by any
|
| 294 |
-
from ..models import
|
| 295 |
-
|
| 296 |
-
if
|
| 297 |
raise HTTPException(
|
| 298 |
status_code=400,
|
| 299 |
-
detail=f"Cannot delete model '{model_code}' - it is used by {
|
| 300 |
)
|
| 301 |
|
| 302 |
# Hard delete model (remove from database)
|
|
|
|
| 290 |
detail=f"Model '{model_code}' not found"
|
| 291 |
)
|
| 292 |
|
| 293 |
+
# Check if model is being used by any captions
|
| 294 |
+
from ..models import Captions
|
| 295 |
+
caption_count = db.query(Captions).filter(Captions.model == model_code).count()
|
| 296 |
+
if caption_count > 0:
|
| 297 |
raise HTTPException(
|
| 298 |
status_code=400,
|
| 299 |
+
detail=f"Cannot delete model '{model_code}' - it is used by {caption_count} caption(s)"
|
| 300 |
)
|
| 301 |
|
| 302 |
# Hard delete model (remove from database)
|
py_backend/app/routers/caption.py
CHANGED
|
@@ -82,7 +82,7 @@ def get_db():
|
|
| 82 |
|
| 83 |
@router.post(
|
| 84 |
"/images/{image_id}/caption",
|
| 85 |
-
response_model=schemas.
|
| 86 |
)
|
| 87 |
async def create_caption(
|
| 88 |
image_id: str,
|
|
@@ -205,7 +205,7 @@ async def create_caption(
|
|
| 205 |
raw = {"error": str(e), "fallback": True}
|
| 206 |
metadata = {}
|
| 207 |
|
| 208 |
-
|
| 209 |
db,
|
| 210 |
image_id=image_id,
|
| 211 |
title=title,
|
|
@@ -216,141 +216,170 @@ async def create_caption(
|
|
| 216 |
metadata=metadata,
|
| 217 |
)
|
| 218 |
|
| 219 |
-
db.refresh(c)
|
| 220 |
-
|
| 221 |
-
print(f"DEBUG: Caption created, image object: {c}")
|
| 222 |
-
print(f"DEBUG: file_key: {c.file_key}")
|
| 223 |
-
print(f"DEBUG: image_id: {c.image_id}")
|
| 224 |
-
|
| 225 |
-
from .upload import convert_image_to_dict
|
| 226 |
-
try:
|
| 227 |
-
url = storage.get_object_url(c.file_key)
|
| 228 |
-
print(f"DEBUG: Generated URL: {url}")
|
| 229 |
-
if url.startswith('/') and settings.STORAGE_PROVIDER == "local":
|
| 230 |
-
url = f"http://localhost:8000{url}"
|
| 231 |
-
print(f"DEBUG: Local URL adjusted to: {url}")
|
| 232 |
-
except Exception as e:
|
| 233 |
-
print(f"DEBUG: URL generation failed: {e}")
|
| 234 |
-
url = f"/api/images/{c.image_id}/file"
|
| 235 |
-
print(f"DEBUG: Using fallback URL: {url}")
|
| 236 |
-
|
| 237 |
-
img_dict = convert_image_to_dict(c, url)
|
| 238 |
-
return schemas.ImageOut(**img_dict)
|
| 239 |
-
|
| 240 |
-
@router.get(
|
| 241 |
-
"/images/{image_id}/caption",
|
| 242 |
-
response_model=schemas.ImageOut,
|
| 243 |
-
)
|
| 244 |
-
def get_caption(
|
| 245 |
-
image_id: str,
|
| 246 |
-
db: Session = Depends(get_db),
|
| 247 |
-
):
|
| 248 |
-
caption = crud.get_caption(db, image_id)
|
| 249 |
-
if not caption or not caption.title:
|
| 250 |
-
raise HTTPException(404, "caption not found")
|
| 251 |
-
|
| 252 |
db.refresh(caption)
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
url = storage.get_object_url(caption.file_key)
|
| 257 |
-
if url.startswith('/') and settings.STORAGE_PROVIDER == "local":
|
| 258 |
-
url = f"http://localhost:8000{url}"
|
| 259 |
-
except Exception:
|
| 260 |
-
url = f"/api/images/{caption.image_id}/file"
|
| 261 |
|
| 262 |
-
|
| 263 |
-
return schemas.ImageOut(**img_dict)
|
| 264 |
|
| 265 |
@router.get(
|
| 266 |
-
"/
|
| 267 |
response_model=List[schemas.ImageOut],
|
| 268 |
)
|
| 269 |
-
def
|
| 270 |
-
image_id: str,
|
| 271 |
db: Session = Depends(get_db),
|
| 272 |
):
|
| 273 |
-
"""Get
|
| 274 |
-
captions
|
|
|
|
|
|
|
| 275 |
|
| 276 |
-
from .upload import convert_image_to_dict
|
| 277 |
result = []
|
| 278 |
for caption in captions:
|
| 279 |
db.refresh(caption)
|
| 280 |
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 288 |
|
|
|
|
| 289 |
return result
|
| 290 |
|
| 291 |
@router.get(
|
| 292 |
"/captions",
|
| 293 |
-
response_model=List[schemas.
|
| 294 |
)
|
| 295 |
def get_all_captions_with_images(
|
| 296 |
db: Session = Depends(get_db),
|
| 297 |
):
|
| 298 |
-
"""Get all
|
| 299 |
-
print(f"DEBUG: Fetching all captions
|
| 300 |
captions = crud.get_all_captions_with_images(db)
|
| 301 |
-
print(f"DEBUG: Found {len(captions)}
|
| 302 |
|
| 303 |
-
from .upload import convert_image_to_dict
|
| 304 |
result = []
|
| 305 |
for caption in captions:
|
| 306 |
-
print(f"DEBUG: Processing
|
| 307 |
|
| 308 |
db.refresh(caption)
|
| 309 |
-
|
| 310 |
-
try:
|
| 311 |
-
url = storage.get_object_url(caption.file_key)
|
| 312 |
-
except Exception:
|
| 313 |
-
url = f"/api/images/{caption.image_id}/file"
|
| 314 |
-
|
| 315 |
-
img_dict = convert_image_to_dict(caption, url)
|
| 316 |
-
result.append(schemas.ImageOut(**img_dict))
|
| 317 |
|
| 318 |
print(f"DEBUG: Returning {len(result)} formatted results")
|
| 319 |
return result
|
| 320 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
@router.put(
|
| 322 |
-
"/
|
| 323 |
-
response_model=schemas.
|
| 324 |
)
|
| 325 |
def update_caption(
|
| 326 |
-
|
| 327 |
update: schemas.CaptionUpdate,
|
| 328 |
db: Session = Depends(get_db),
|
| 329 |
):
|
| 330 |
-
caption = crud.update_caption(db,
|
| 331 |
if not caption:
|
| 332 |
raise HTTPException(404, "caption not found")
|
| 333 |
|
| 334 |
db.refresh(caption)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 335 |
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
url = storage.get_object_url(caption.file_key)
|
| 339 |
-
except Exception:
|
| 340 |
-
url = f"/api/images/{caption.image_id}/file"
|
| 341 |
|
| 342 |
-
|
| 343 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
|
| 345 |
@router.delete(
|
| 346 |
-
"/
|
| 347 |
)
|
| 348 |
def delete_caption(
|
| 349 |
-
|
| 350 |
db: Session = Depends(get_db),
|
| 351 |
):
|
| 352 |
-
"""Delete caption data for
|
| 353 |
-
success = crud.delete_caption(db,
|
| 354 |
if not success:
|
| 355 |
raise HTTPException(404, "caption not found")
|
| 356 |
return {"message": "Caption deleted successfully"}
|
|
|
|
| 82 |
|
| 83 |
@router.post(
|
| 84 |
"/images/{image_id}/caption",
|
| 85 |
+
response_model=schemas.CaptionOut,
|
| 86 |
)
|
| 87 |
async def create_caption(
|
| 88 |
image_id: str,
|
|
|
|
| 205 |
raw = {"error": str(e), "fallback": True}
|
| 206 |
metadata = {}
|
| 207 |
|
| 208 |
+
caption = crud.create_caption(
|
| 209 |
db,
|
| 210 |
image_id=image_id,
|
| 211 |
title=title,
|
|
|
|
| 216 |
metadata=metadata,
|
| 217 |
)
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
db.refresh(caption)
|
| 220 |
|
| 221 |
+
print(f"DEBUG: Caption created, caption object: {caption}")
|
| 222 |
+
print(f"DEBUG: caption_id: {caption.caption_id}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
|
| 224 |
+
return schemas.CaptionOut.from_orm(caption)
|
|
|
|
| 225 |
|
| 226 |
@router.get(
|
| 227 |
+
"/captions/legacy",
|
| 228 |
response_model=List[schemas.ImageOut],
|
| 229 |
)
|
| 230 |
+
def get_all_captions_legacy_format(
|
|
|
|
| 231 |
db: Session = Depends(get_db),
|
| 232 |
):
|
| 233 |
+
"""Get all images with captions in the old format for backward compatibility"""
|
| 234 |
+
print(f"DEBUG: Fetching all captions in legacy format...")
|
| 235 |
+
captions = crud.get_all_captions_with_images(db)
|
| 236 |
+
print(f"DEBUG: Found {len(captions)} captions")
|
| 237 |
|
|
|
|
| 238 |
result = []
|
| 239 |
for caption in captions:
|
| 240 |
db.refresh(caption)
|
| 241 |
|
| 242 |
+
# Get the associated image for this caption
|
| 243 |
+
if caption.images:
|
| 244 |
+
for image in caption.images:
|
| 245 |
+
# Create a response in the old format where caption data is embedded in image
|
| 246 |
+
from .upload import convert_image_to_dict
|
| 247 |
+
try:
|
| 248 |
+
url = storage.get_object_url(image.file_key)
|
| 249 |
+
if url.startswith('/') and settings.STORAGE_PROVIDER == "local":
|
| 250 |
+
url = f"http://localhost:8000{url}"
|
| 251 |
+
except Exception:
|
| 252 |
+
url = f"/api/images/{image.image_id}/file"
|
| 253 |
+
|
| 254 |
+
img_dict = convert_image_to_dict(image, url)
|
| 255 |
+
|
| 256 |
+
# Override with caption data
|
| 257 |
+
img_dict.update({
|
| 258 |
+
"title": caption.title,
|
| 259 |
+
"prompt": caption.prompt,
|
| 260 |
+
"model": caption.model,
|
| 261 |
+
"schema_id": caption.schema_id,
|
| 262 |
+
"raw_json": caption.raw_json,
|
| 263 |
+
"generated": caption.generated,
|
| 264 |
+
"edited": caption.edited,
|
| 265 |
+
"accuracy": caption.accuracy,
|
| 266 |
+
"context": caption.context,
|
| 267 |
+
"usability": caption.usability,
|
| 268 |
+
"starred": caption.starred,
|
| 269 |
+
"created_at": caption.created_at,
|
| 270 |
+
"updated_at": caption.updated_at,
|
| 271 |
+
})
|
| 272 |
+
|
| 273 |
+
result.append(schemas.ImageOut(**img_dict))
|
| 274 |
|
| 275 |
+
print(f"DEBUG: Returning {len(result)} legacy format results")
|
| 276 |
return result
|
| 277 |
|
| 278 |
@router.get(
|
| 279 |
"/captions",
|
| 280 |
+
response_model=List[schemas.CaptionOut],
|
| 281 |
)
|
| 282 |
def get_all_captions_with_images(
|
| 283 |
db: Session = Depends(get_db),
|
| 284 |
):
|
| 285 |
+
"""Get all captions"""
|
| 286 |
+
print(f"DEBUG: Fetching all captions...")
|
| 287 |
captions = crud.get_all_captions_with_images(db)
|
| 288 |
+
print(f"DEBUG: Found {len(captions)} captions")
|
| 289 |
|
|
|
|
| 290 |
result = []
|
| 291 |
for caption in captions:
|
| 292 |
+
print(f"DEBUG: Processing caption {caption.caption_id}, title: {caption.title}, generated: {caption.generated}, model: {caption.model}")
|
| 293 |
|
| 294 |
db.refresh(caption)
|
| 295 |
+
result.append(schemas.CaptionOut.from_orm(caption))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
print(f"DEBUG: Returning {len(result)} formatted results")
|
| 298 |
return result
|
| 299 |
|
| 300 |
+
@router.get(
|
| 301 |
+
"/images/{image_id}/captions",
|
| 302 |
+
response_model=List[schemas.CaptionOut],
|
| 303 |
+
)
|
| 304 |
+
def get_captions_by_image(
|
| 305 |
+
image_id: str,
|
| 306 |
+
db: Session = Depends(get_db),
|
| 307 |
+
):
|
| 308 |
+
"""Get all captions for a specific image"""
|
| 309 |
+
captions = crud.get_captions_by_image(db, image_id)
|
| 310 |
+
|
| 311 |
+
result = []
|
| 312 |
+
for caption in captions:
|
| 313 |
+
db.refresh(caption)
|
| 314 |
+
result.append(schemas.CaptionOut.from_orm(caption))
|
| 315 |
+
|
| 316 |
+
return result
|
| 317 |
+
|
| 318 |
+
@router.get(
|
| 319 |
+
"/captions/{caption_id}",
|
| 320 |
+
response_model=schemas.CaptionOut,
|
| 321 |
+
)
|
| 322 |
+
def get_caption(
|
| 323 |
+
caption_id: str,
|
| 324 |
+
db: Session = Depends(get_db),
|
| 325 |
+
):
|
| 326 |
+
caption = crud.get_caption(db, caption_id)
|
| 327 |
+
if not caption:
|
| 328 |
+
raise HTTPException(404, "caption not found")
|
| 329 |
+
|
| 330 |
+
db.refresh(caption)
|
| 331 |
+
return schemas.CaptionOut.from_orm(caption)
|
| 332 |
+
|
| 333 |
@router.put(
|
| 334 |
+
"/captions/{caption_id}",
|
| 335 |
+
response_model=schemas.CaptionOut,
|
| 336 |
)
|
| 337 |
def update_caption(
|
| 338 |
+
caption_id: str,
|
| 339 |
update: schemas.CaptionUpdate,
|
| 340 |
db: Session = Depends(get_db),
|
| 341 |
):
|
| 342 |
+
caption = crud.update_caption(db, caption_id, update)
|
| 343 |
if not caption:
|
| 344 |
raise HTTPException(404, "caption not found")
|
| 345 |
|
| 346 |
db.refresh(caption)
|
| 347 |
+
return schemas.CaptionOut.from_orm(caption)
|
| 348 |
+
|
| 349 |
+
@router.put(
|
| 350 |
+
"/images/{image_id}/caption",
|
| 351 |
+
response_model=schemas.CaptionOut,
|
| 352 |
+
)
|
| 353 |
+
def update_caption_by_image(
|
| 354 |
+
image_id: str,
|
| 355 |
+
update: schemas.CaptionUpdate,
|
| 356 |
+
db: Session = Depends(get_db),
|
| 357 |
+
):
|
| 358 |
+
"""Update the first caption for an image (for backward compatibility)"""
|
| 359 |
+
img = crud.get_image(db, image_id)
|
| 360 |
+
if not img:
|
| 361 |
+
raise HTTPException(404, "image not found")
|
| 362 |
|
| 363 |
+
if not img.captions:
|
| 364 |
+
raise HTTPException(404, "no captions found for this image")
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
+
# Update the first caption
|
| 367 |
+
caption = crud.update_caption(db, str(img.captions[0].caption_id), update)
|
| 368 |
+
if not caption:
|
| 369 |
+
raise HTTPException(404, "caption not found")
|
| 370 |
+
|
| 371 |
+
db.refresh(caption)
|
| 372 |
+
return schemas.CaptionOut.from_orm(caption)
|
| 373 |
|
| 374 |
@router.delete(
|
| 375 |
+
"/captions/{caption_id}",
|
| 376 |
)
|
| 377 |
def delete_caption(
|
| 378 |
+
caption_id: str,
|
| 379 |
db: Session = Depends(get_db),
|
| 380 |
):
|
| 381 |
+
"""Delete caption data for a caption"""
|
| 382 |
+
success = crud.delete_caption(db, caption_id)
|
| 383 |
if not success:
|
| 384 |
raise HTTPException(404, "caption not found")
|
| 385 |
return {"message": "Caption deleted successfully"}
|
py_backend/app/routers/images.py
CHANGED
|
@@ -153,17 +153,6 @@ async def create_image_from_url(payload: CreateImageFromUrlIn, db: Session = Dep
|
|
| 153 |
event_type=event_type,
|
| 154 |
epsg=epsg,
|
| 155 |
image_type=payload.image_type,
|
| 156 |
-
title="no title",
|
| 157 |
-
prompt=prompt_code,
|
| 158 |
-
model="STUB_MODEL",
|
| 159 |
-
schema_id=schema_id,
|
| 160 |
-
raw_json={},
|
| 161 |
-
generated="",
|
| 162 |
-
edited="",
|
| 163 |
-
accuracy=50,
|
| 164 |
-
context=50,
|
| 165 |
-
usability=50,
|
| 166 |
-
starred=False,
|
| 167 |
center_lon=payload.center_lon,
|
| 168 |
center_lat=payload.center_lat,
|
| 169 |
amsl_m=payload.amsl_m,
|
|
@@ -219,7 +208,8 @@ async def debug_database_status(db: Session = Depends(get_db)):
|
|
| 219 |
# Check required tables
|
| 220 |
tables_to_check = [
|
| 221 |
"sources", "event_types", "spatial_references", "image_types",
|
| 222 |
-
"prompts", "models", "json_schemas", "images", "image_countries"
|
|
|
|
| 223 |
]
|
| 224 |
|
| 225 |
for table in tables_to_check:
|
|
|
|
| 153 |
event_type=event_type,
|
| 154 |
epsg=epsg,
|
| 155 |
image_type=payload.image_type,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
center_lon=payload.center_lon,
|
| 157 |
center_lat=payload.center_lat,
|
| 158 |
amsl_m=payload.amsl_m,
|
|
|
|
| 208 |
# Check required tables
|
| 209 |
tables_to_check = [
|
| 210 |
"sources", "event_types", "spatial_references", "image_types",
|
| 211 |
+
"prompts", "models", "json_schemas", "images", "image_countries",
|
| 212 |
+
"captions", "images_captions"
|
| 213 |
]
|
| 214 |
|
| 215 |
for table in tables_to_check:
|
py_backend/app/routers/metadata.py
CHANGED
|
@@ -12,16 +12,16 @@ def get_db():
|
|
| 12 |
finally:
|
| 13 |
db.close()
|
| 14 |
|
| 15 |
-
@router.put("/
|
| 16 |
def update_metadata(
|
| 17 |
-
|
| 18 |
update: schemas.CaptionUpdate,
|
| 19 |
db: Session = Depends(get_db)
|
| 20 |
):
|
| 21 |
-
|
| 22 |
-
if not
|
| 23 |
raise HTTPException(404, "caption not found")
|
| 24 |
-
return
|
| 25 |
|
| 26 |
@router.get("/sources", response_model=List[schemas.SourceOut])
|
| 27 |
def get_sources(db: Session = Depends(get_db)):
|
|
|
|
| 12 |
finally:
|
| 13 |
db.close()
|
| 14 |
|
| 15 |
+
@router.put("/captions/{caption_id}/metadata", response_model=schemas.CaptionOut)
|
| 16 |
def update_metadata(
|
| 17 |
+
caption_id: str,
|
| 18 |
update: schemas.CaptionUpdate,
|
| 19 |
db: Session = Depends(get_db)
|
| 20 |
):
|
| 21 |
+
caption = crud.update_caption(db, caption_id, update)
|
| 22 |
+
if not caption:
|
| 23 |
raise HTTPException(404, "caption not found")
|
| 24 |
+
return schemas.CaptionOut.from_orm(caption)
|
| 25 |
|
| 26 |
@router.get("/sources", response_model=List[schemas.SourceOut])
|
| 27 |
def get_sources(db: Session = Depends(get_db)):
|
py_backend/app/routers/upload.py
CHANGED
|
@@ -9,6 +9,7 @@ from typing import List, Optional
|
|
| 9 |
import boto3
|
| 10 |
import time
|
| 11 |
import base64
|
|
|
|
| 12 |
|
| 13 |
router = APIRouter()
|
| 14 |
|
|
@@ -50,6 +51,62 @@ def convert_image_to_dict(img, image_url):
|
|
| 50 |
print(f"Warning: Error processing countries for image {img.image_id}: {e}")
|
| 51 |
countries_list = []
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
img_dict = {
|
| 54 |
"image_id": img.image_id,
|
| 55 |
"file_key": img.file_key,
|
|
@@ -60,19 +117,23 @@ def convert_image_to_dict(img, image_url):
|
|
| 60 |
"image_type": img.image_type,
|
| 61 |
"image_url": image_url,
|
| 62 |
"countries": countries_list,
|
| 63 |
-
"
|
| 64 |
-
"
|
| 65 |
-
"
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
"
|
| 69 |
-
"
|
| 70 |
-
"
|
| 71 |
-
"
|
| 72 |
-
"
|
| 73 |
-
"
|
| 74 |
-
"
|
| 75 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
|
| 77 |
# Drone-specific fields
|
| 78 |
"center_lon": getattr(img, 'center_lon', None),
|
|
@@ -367,8 +428,21 @@ def update_image_metadata(
|
|
| 367 |
img.epsg = metadata.epsg
|
| 368 |
if metadata.image_type is not None:
|
| 369 |
img.image_type = metadata.image_type
|
|
|
|
| 370 |
if metadata.starred is not None:
|
| 371 |
-
img.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
|
| 373 |
# Update drone-specific fields
|
| 374 |
if metadata.center_lon is not None:
|
|
@@ -435,12 +509,15 @@ def delete_image(image_id: str, db: Session = Depends(get_db), content_managemen
|
|
| 435 |
|
| 436 |
# Only increment delete count if this is NOT a content management delete
|
| 437 |
# Content management deletes (from map details) should not count against model performance
|
| 438 |
-
if not content_management and img.
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
if
|
| 442 |
-
|
| 443 |
-
|
|
|
|
|
|
|
|
|
|
| 444 |
|
| 445 |
db.delete(img)
|
| 446 |
db.commit()
|
|
|
|
| 9 |
import boto3
|
| 10 |
import time
|
| 11 |
import base64
|
| 12 |
+
import datetime
|
| 13 |
|
| 14 |
router = APIRouter()
|
| 15 |
|
|
|
|
| 51 |
print(f"Warning: Error processing countries for image {img.image_id}: {e}")
|
| 52 |
countries_list = []
|
| 53 |
|
| 54 |
+
captions_list = []
|
| 55 |
+
if hasattr(img, 'captions') and img.captions is not None:
|
| 56 |
+
try:
|
| 57 |
+
captions_list = [
|
| 58 |
+
{
|
| 59 |
+
"caption_id": c.caption_id,
|
| 60 |
+
"title": c.title,
|
| 61 |
+
"prompt": c.prompt,
|
| 62 |
+
"model": c.model,
|
| 63 |
+
"schema_id": c.schema_id,
|
| 64 |
+
"raw_json": c.raw_json,
|
| 65 |
+
"generated": c.generated,
|
| 66 |
+
"edited": c.edited,
|
| 67 |
+
"accuracy": c.accuracy,
|
| 68 |
+
"context": c.context,
|
| 69 |
+
"usability": c.usability,
|
| 70 |
+
"starred": c.starred if c.starred is not None else False,
|
| 71 |
+
"created_at": c.created_at,
|
| 72 |
+
"updated_at": c.updated_at
|
| 73 |
+
} for c in img.captions
|
| 74 |
+
]
|
| 75 |
+
except Exception as e:
|
| 76 |
+
print(f"Warning: Error processing captions for image {img.image_id}: {e}")
|
| 77 |
+
captions_list = []
|
| 78 |
+
|
| 79 |
+
# Get starred status and other caption fields from first caption for backward compatibility
|
| 80 |
+
starred = False
|
| 81 |
+
title = None
|
| 82 |
+
prompt = None
|
| 83 |
+
model = None
|
| 84 |
+
schema_id = None
|
| 85 |
+
raw_json = None
|
| 86 |
+
generated = None
|
| 87 |
+
edited = None
|
| 88 |
+
accuracy = None
|
| 89 |
+
context = None
|
| 90 |
+
usability = None
|
| 91 |
+
created_at = None
|
| 92 |
+
updated_at = None
|
| 93 |
+
|
| 94 |
+
if captions_list:
|
| 95 |
+
first_caption = captions_list[0]
|
| 96 |
+
starred = first_caption.get("starred", False)
|
| 97 |
+
title = first_caption.get("title")
|
| 98 |
+
prompt = first_caption.get("prompt")
|
| 99 |
+
model = first_caption.get("model")
|
| 100 |
+
schema_id = first_caption.get("schema_id")
|
| 101 |
+
raw_json = first_caption.get("raw_json")
|
| 102 |
+
generated = first_caption.get("generated")
|
| 103 |
+
edited = first_caption.get("edited")
|
| 104 |
+
accuracy = first_caption.get("accuracy")
|
| 105 |
+
context = first_caption.get("context")
|
| 106 |
+
usability = first_caption.get("usability")
|
| 107 |
+
created_at = first_caption.get("created_at")
|
| 108 |
+
updated_at = first_caption.get("updated_at")
|
| 109 |
+
|
| 110 |
img_dict = {
|
| 111 |
"image_id": img.image_id,
|
| 112 |
"file_key": img.file_key,
|
|
|
|
| 117 |
"image_type": img.image_type,
|
| 118 |
"image_url": image_url,
|
| 119 |
"countries": countries_list,
|
| 120 |
+
"captions": captions_list,
|
| 121 |
+
"starred": starred, # Backward compatibility
|
| 122 |
+
"captured_at": img.captured_at,
|
| 123 |
+
|
| 124 |
+
# Backward compatibility fields for legacy frontend
|
| 125 |
+
"title": title,
|
| 126 |
+
"prompt": prompt,
|
| 127 |
+
"model": model,
|
| 128 |
+
"schema_id": schema_id,
|
| 129 |
+
"raw_json": raw_json,
|
| 130 |
+
"generated": generated,
|
| 131 |
+
"edited": edited,
|
| 132 |
+
"accuracy": accuracy,
|
| 133 |
+
"context": context,
|
| 134 |
+
"usability": usability,
|
| 135 |
+
"created_at": created_at,
|
| 136 |
+
"updated_at": updated_at,
|
| 137 |
|
| 138 |
# Drone-specific fields
|
| 139 |
"center_lon": getattr(img, 'center_lon', None),
|
|
|
|
| 428 |
img.epsg = metadata.epsg
|
| 429 |
if metadata.image_type is not None:
|
| 430 |
img.image_type = metadata.image_type
|
| 431 |
+
# Handle starred field - update the first caption's starred status
|
| 432 |
if metadata.starred is not None:
|
| 433 |
+
if img.captions:
|
| 434 |
+
# Update the first caption's starred status
|
| 435 |
+
img.captions[0].starred = metadata.starred
|
| 436 |
+
else:
|
| 437 |
+
# If no captions exist, create a minimal caption with starred status
|
| 438 |
+
from app import models
|
| 439 |
+
caption = models.Captions(
|
| 440 |
+
title="",
|
| 441 |
+
starred=metadata.starred,
|
| 442 |
+
created_at=datetime.datetime.utcnow()
|
| 443 |
+
)
|
| 444 |
+
db.add(caption)
|
| 445 |
+
img.captions.append(caption)
|
| 446 |
|
| 447 |
# Update drone-specific fields
|
| 448 |
if metadata.center_lon is not None:
|
|
|
|
| 509 |
|
| 510 |
# Only increment delete count if this is NOT a content management delete
|
| 511 |
# Content management deletes (from map details) should not count against model performance
|
| 512 |
+
if not content_management and img.captions:
|
| 513 |
+
# Get model from the first caption
|
| 514 |
+
model_name = img.captions[0].model
|
| 515 |
+
if model_name:
|
| 516 |
+
from .. import crud as crud_module
|
| 517 |
+
model = crud_module.get_model(db, model_name)
|
| 518 |
+
if model:
|
| 519 |
+
model.delete_count += 1
|
| 520 |
+
db.commit()
|
| 521 |
|
| 522 |
db.delete(img)
|
| 523 |
db.commit()
|
py_backend/app/schemas.py
CHANGED
|
@@ -29,7 +29,7 @@ class ImageMetadataUpdate(BaseModel):
|
|
| 29 |
countries: Optional[List[str]] = None
|
| 30 |
epsg: Optional[str] = None
|
| 31 |
image_type: Optional[str] = None
|
| 32 |
-
starred: Optional[bool] = None
|
| 33 |
|
| 34 |
# Drone-specific fields (optional)
|
| 35 |
center_lon: Optional[float] = None
|
|
@@ -44,6 +44,25 @@ class ImageMetadataUpdate(BaseModel):
|
|
| 44 |
std_h_m: Optional[float] = None
|
| 45 |
std_v_m: Optional[float] = None
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
class ImageOut(BaseModel):
|
| 48 |
image_id: UUID
|
| 49 |
file_key: str
|
|
@@ -54,6 +73,11 @@ class ImageOut(BaseModel):
|
|
| 54 |
image_type: str
|
| 55 |
image_url: str
|
| 56 |
countries: List["CountryOut"] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
title: Optional[str] = None
|
| 58 |
prompt: Optional[str] = None
|
| 59 |
model: Optional[str] = None
|
|
@@ -64,7 +88,6 @@ class ImageOut(BaseModel):
|
|
| 64 |
accuracy: Optional[int] = None
|
| 65 |
context: Optional[int] = None
|
| 66 |
usability: Optional[int] = None
|
| 67 |
-
starred: bool = False
|
| 68 |
created_at: Optional[datetime] = None
|
| 69 |
updated_at: Optional[datetime] = None
|
| 70 |
|
|
@@ -94,6 +117,18 @@ class CaptionUpdate(BaseModel):
|
|
| 94 |
context: Optional[int] = None
|
| 95 |
usability: Optional[int] = None
|
| 96 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
class PromptOut(BaseModel):
|
| 98 |
p_code: str
|
| 99 |
label: str
|
|
|
|
| 29 |
countries: Optional[List[str]] = None
|
| 30 |
epsg: Optional[str] = None
|
| 31 |
image_type: Optional[str] = None
|
| 32 |
+
starred: Optional[bool] = None # Backward compatibility - updates first caption
|
| 33 |
|
| 34 |
# Drone-specific fields (optional)
|
| 35 |
center_lon: Optional[float] = None
|
|
|
|
| 44 |
std_h_m: Optional[float] = None
|
| 45 |
std_v_m: Optional[float] = None
|
| 46 |
|
| 47 |
+
class CaptionOut(BaseModel):
|
| 48 |
+
caption_id: UUID
|
| 49 |
+
title: Optional[str] = None
|
| 50 |
+
prompt: Optional[str] = None
|
| 51 |
+
model: Optional[str] = None
|
| 52 |
+
schema_id: Optional[str] = None
|
| 53 |
+
raw_json: Optional[dict] = None
|
| 54 |
+
generated: Optional[str] = None
|
| 55 |
+
edited: Optional[str] = None
|
| 56 |
+
accuracy: Optional[int] = None
|
| 57 |
+
context: Optional[int] = None
|
| 58 |
+
usability: Optional[int] = None
|
| 59 |
+
starred: bool = False
|
| 60 |
+
created_at: Optional[datetime] = None
|
| 61 |
+
updated_at: Optional[datetime] = None
|
| 62 |
+
|
| 63 |
+
class Config:
|
| 64 |
+
from_attributes = True
|
| 65 |
+
|
| 66 |
class ImageOut(BaseModel):
|
| 67 |
image_id: UUID
|
| 68 |
file_key: str
|
|
|
|
| 73 |
image_type: str
|
| 74 |
image_url: str
|
| 75 |
countries: List["CountryOut"] = []
|
| 76 |
+
captions: List[CaptionOut] = []
|
| 77 |
+
starred: bool = False # Backward compatibility - from first caption
|
| 78 |
+
captured_at: Optional[datetime] = None
|
| 79 |
+
|
| 80 |
+
# Backward compatibility fields for legacy frontend
|
| 81 |
title: Optional[str] = None
|
| 82 |
prompt: Optional[str] = None
|
| 83 |
model: Optional[str] = None
|
|
|
|
| 88 |
accuracy: Optional[int] = None
|
| 89 |
context: Optional[int] = None
|
| 90 |
usability: Optional[int] = None
|
|
|
|
| 91 |
created_at: Optional[datetime] = None
|
| 92 |
updated_at: Optional[datetime] = None
|
| 93 |
|
|
|
|
| 117 |
context: Optional[int] = None
|
| 118 |
usability: Optional[int] = None
|
| 119 |
|
| 120 |
+
class CaptionCreate(BaseModel):
|
| 121 |
+
title: str
|
| 122 |
+
prompt: str
|
| 123 |
+
model: str
|
| 124 |
+
raw_json: dict
|
| 125 |
+
generated: str
|
| 126 |
+
edited: str
|
| 127 |
+
accuracy: Optional[int] = None
|
| 128 |
+
context: Optional[int] = None
|
| 129 |
+
usability: Optional[int] = None
|
| 130 |
+
starred: bool = False
|
| 131 |
+
|
| 132 |
class PromptOut(BaseModel):
|
| 133 |
p_code: str
|
| 134 |
label: str
|
py_backend/tests/integration_tests/test_explore_page.py
CHANGED
|
@@ -163,8 +163,8 @@ def test_database_consistency():
|
|
| 163 |
images = db.query(models.Images).all()
|
| 164 |
print(f" + Total images in database: {len(images)}")
|
| 165 |
|
| 166 |
-
|
| 167 |
-
print(f" +
|
| 168 |
|
| 169 |
sources = db.query(models.Source).all()
|
| 170 |
print(f" + Total sources: {len(sources)}")
|
|
|
|
| 163 |
images = db.query(models.Images).all()
|
| 164 |
print(f" + Total images in database: {len(images)}")
|
| 165 |
|
| 166 |
+
captions = db.query(models.Captions).all()
|
| 167 |
+
print(f" + Total captions in database: {len(captions)}")
|
| 168 |
|
| 169 |
sources = db.query(models.Source).all()
|
| 170 |
print(f" + Total sources: {len(sources)}")
|
py_backend/tests/integration_tests/test_upload_flow.py
CHANGED
|
@@ -27,13 +27,7 @@ def test_database_connection():
|
|
| 27 |
source="OTHER",
|
| 28 |
event_type="OTHER",
|
| 29 |
epsg="4326",
|
| 30 |
-
image_type="crisis_map"
|
| 31 |
-
title="Test Title",
|
| 32 |
-
prompt="Test prompt",
|
| 33 |
-
model="STUB_MODEL",
|
| 34 |
-
schema_id="default_caption@1.0.0",
|
| 35 |
-
raw_json={"test": True},
|
| 36 |
-
generated="This is a test caption"
|
| 37 |
)
|
| 38 |
|
| 39 |
db.add(test_img)
|
|
|
|
| 27 |
source="OTHER",
|
| 28 |
event_type="OTHER",
|
| 29 |
epsg="4326",
|
| 30 |
+
image_type="crisis_map"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
)
|
| 32 |
|
| 33 |
db.add(test_img)
|