Persist OCR captions in gallery

This commit is contained in:
pewdiepie-archdaemon
2026-06-28 04:50:20 +00:00
parent a6903931f7
commit 3aa48e9025
5 changed files with 60 additions and 2 deletions
+25
View File
@@ -276,6 +276,7 @@ class GalleryImage(TimestampMixin, Base):
id = Column(String, primary_key=True, index=True)
filename = Column(String, nullable=False, unique=True)
prompt = Column(Text, nullable=False, default="")
caption = Column(Text, nullable=True, default="")
model = Column(String, nullable=True)
size = Column(String, nullable=True)
quality = Column(String, nullable=True)
@@ -1182,6 +1183,29 @@ def _migrate_add_multiuser_owner_columns():
_migrate_add_owner_to_table("documents", "ix_documents_owner")
def _migrate_add_gallery_caption_column():
"""Add OCR/vision caption storage for gallery images."""
import sqlite3
db_path = DATABASE_URL.replace("sqlite:///", "")
if not os.path.exists(db_path):
return
conn = None
try:
conn = sqlite3.connect(db_path)
columns = [row[1] for row in conn.execute("PRAGMA table_info(gallery_images)").fetchall()]
if columns and "caption" not in columns:
conn.execute("ALTER TABLE gallery_images ADD COLUMN caption TEXT DEFAULT ''")
conn.commit()
logging.getLogger(__name__).info("Migrated: added caption column to gallery_images")
except Exception as e:
logging.getLogger(__name__).warning(f"Migration gallery caption column failed: {e}")
finally:
try:
conn.close()
except Exception:
pass
def _migrate_add_api_token_scopes_column():
"""Add API token scopes for existing installs.
@@ -1812,6 +1836,7 @@ def init_db():
_migrate_add_token_columns()
_migrate_add_mode_column()
_migrate_add_multiuser_owner_columns()
_migrate_add_gallery_caption_column()
_migrate_add_api_token_scopes_column()
_migrate_backfill_document_owner_from_session()
_migrate_assign_legacy_owner()
+1
View File
@@ -99,6 +99,7 @@ def _image_to_dict(img: GalleryImage, session_name: str = None) -> Dict[str, Any
"filename": img.filename,
"url": f"/api/generated-image/{img.filename}",
"prompt": img.prompt,
"caption": img.caption or "",
"model": img.model,
"size": img.size,
"quality": img.quality,
+1
View File
@@ -508,6 +508,7 @@ def setup_gallery_routes() -> APIRouter:
from sqlalchemy import or_
q = q.filter(or_(
GalleryImage.prompt.ilike(term),
GalleryImage.caption.ilike(term),
GalleryImage.tags.ilike(term),
GalleryImage.ai_tags.ilike(term),
))
+31 -1
View File
@@ -267,6 +267,32 @@ def setup_upload_routes(upload_handler):
os.makedirs(cache_dir, exist_ok=True)
return os.path.join(cache_dir, file_id + ".txt")
def _sync_gallery_caption_for_upload(info: dict | None, owner: str | None, text: str) -> None:
"""Copy upload OCR/vision text onto the promoted gallery image row."""
if not info:
return
file_hash = info.get("hash")
if not file_hash:
return
db = SessionLocal()
try:
q = db.query(GalleryImage).filter(
GalleryImage.file_hash == file_hash,
GalleryImage.is_active == True, # noqa: E712
)
if owner:
q = q.filter(GalleryImage.owner == owner)
img = q.first()
if not img:
return
img.caption = (text or "").strip()
db.commit()
except Exception as e:
db.rollback()
logger.warning("Failed to sync OCR caption to gallery image: %s", e)
finally:
db.close()
@router.get("/{file_id}/vision")
async def get_vision_text(request: Request, file_id: str, force: int = 0):
"""Return the vision-model OCR/description for an uploaded image.
@@ -293,7 +319,9 @@ def setup_upload_routes(upload_handler):
if not force and os.path.exists(cache_path):
try:
with open(cache_path, encoding="utf-8") as f:
return {"text": f.read(), "cached": True}
cached_text = f.read()
_sync_gallery_caption_for_upload(info, file_owner or current_user, cached_text)
return {"text": cached_text, "cached": True}
except Exception as e:
logger.warning(f"Vision cache read failed for {file_id}: {e}")
from src.document_processor import analyze_image_with_vl
@@ -307,6 +335,7 @@ def setup_upload_routes(upload_handler):
f.write(text)
except Exception as e:
logger.warning(f"Vision cache write failed for {file_id}: {e}")
_sync_gallery_caption_for_upload(info, file_owner or current_user, text)
return {"text": text, "cached": False}
@router.put("/{file_id}/vision")
@@ -334,6 +363,7 @@ def setup_upload_routes(upload_handler):
raise HTTPException(400, "text must be a string")
with open(_vision_cache_path(file_id), "w", encoding="utf-8") as f:
f.write(text)
_sync_gallery_caption_for_upload(info, file_owner or current_user, text)
return {"ok": True}
async def periodic_rate_limit_cleanup():
+2 -1
View File
@@ -1201,7 +1201,7 @@ function _renderGrid() {
.replace(/\.[^.]+$/, '') // drop extension
.replace(/[_-]+/g, ' ')
.trim();
const labelText = (img.prompt || '').trim() || fallbackName || 'Photo';
const labelText = (img.caption || '').trim() || (img.prompt || '').trim() || fallbackName || 'Photo';
const promptPreview = labelText.length > 60 ? labelText.substring(0, 58) + '...' : labelText;
const favCls = img.favorite ? ' gallery-fav-active' : '';
html += `
@@ -1410,6 +1410,7 @@ function _openDetail(img) {
<svg class="gallery-name-enter" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 10 4 15 9 20"/><path d="M20 4v7a4 4 0 0 1-4 4H4"/></svg>
</div>
</div>
${img.caption ? `<div class="gallery-detail-section"><label>OCR Caption</label><div class="gallery-detail-prompt">${_esc(img.caption)}</div></div>` : ''}
${img.prompt && img.model !== 'imported' ? `<div class="gallery-detail-section"><label>Prompt</label><div class="gallery-detail-prompt">${_esc(img.prompt)}</div></div>` : ''}
<div class="gallery-detail-section gallery-detail-section-date">
<label>Date</label>