mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-07-08 11:56:59 +00:00
Persist OCR captions in gallery
This commit is contained in:
@@ -276,6 +276,7 @@ class GalleryImage(TimestampMixin, Base):
|
|||||||
id = Column(String, primary_key=True, index=True)
|
id = Column(String, primary_key=True, index=True)
|
||||||
filename = Column(String, nullable=False, unique=True)
|
filename = Column(String, nullable=False, unique=True)
|
||||||
prompt = Column(Text, nullable=False, default="")
|
prompt = Column(Text, nullable=False, default="")
|
||||||
|
caption = Column(Text, nullable=True, default="")
|
||||||
model = Column(String, nullable=True)
|
model = Column(String, nullable=True)
|
||||||
size = Column(String, nullable=True)
|
size = Column(String, nullable=True)
|
||||||
quality = 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")
|
_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():
|
def _migrate_add_api_token_scopes_column():
|
||||||
"""Add API token scopes for existing installs.
|
"""Add API token scopes for existing installs.
|
||||||
|
|
||||||
@@ -1812,6 +1836,7 @@ def init_db():
|
|||||||
_migrate_add_token_columns()
|
_migrate_add_token_columns()
|
||||||
_migrate_add_mode_column()
|
_migrate_add_mode_column()
|
||||||
_migrate_add_multiuser_owner_columns()
|
_migrate_add_multiuser_owner_columns()
|
||||||
|
_migrate_add_gallery_caption_column()
|
||||||
_migrate_add_api_token_scopes_column()
|
_migrate_add_api_token_scopes_column()
|
||||||
_migrate_backfill_document_owner_from_session()
|
_migrate_backfill_document_owner_from_session()
|
||||||
_migrate_assign_legacy_owner()
|
_migrate_assign_legacy_owner()
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ def _image_to_dict(img: GalleryImage, session_name: str = None) -> Dict[str, Any
|
|||||||
"filename": img.filename,
|
"filename": img.filename,
|
||||||
"url": f"/api/generated-image/{img.filename}",
|
"url": f"/api/generated-image/{img.filename}",
|
||||||
"prompt": img.prompt,
|
"prompt": img.prompt,
|
||||||
|
"caption": img.caption or "",
|
||||||
"model": img.model,
|
"model": img.model,
|
||||||
"size": img.size,
|
"size": img.size,
|
||||||
"quality": img.quality,
|
"quality": img.quality,
|
||||||
|
|||||||
@@ -508,6 +508,7 @@ def setup_gallery_routes() -> APIRouter:
|
|||||||
from sqlalchemy import or_
|
from sqlalchemy import or_
|
||||||
q = q.filter(or_(
|
q = q.filter(or_(
|
||||||
GalleryImage.prompt.ilike(term),
|
GalleryImage.prompt.ilike(term),
|
||||||
|
GalleryImage.caption.ilike(term),
|
||||||
GalleryImage.tags.ilike(term),
|
GalleryImage.tags.ilike(term),
|
||||||
GalleryImage.ai_tags.ilike(term),
|
GalleryImage.ai_tags.ilike(term),
|
||||||
))
|
))
|
||||||
|
|||||||
+31
-1
@@ -267,6 +267,32 @@ def setup_upload_routes(upload_handler):
|
|||||||
os.makedirs(cache_dir, exist_ok=True)
|
os.makedirs(cache_dir, exist_ok=True)
|
||||||
return os.path.join(cache_dir, file_id + ".txt")
|
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")
|
@router.get("/{file_id}/vision")
|
||||||
async def get_vision_text(request: Request, file_id: str, force: int = 0):
|
async def get_vision_text(request: Request, file_id: str, force: int = 0):
|
||||||
"""Return the vision-model OCR/description for an uploaded image.
|
"""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):
|
if not force and os.path.exists(cache_path):
|
||||||
try:
|
try:
|
||||||
with open(cache_path, encoding="utf-8") as f:
|
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:
|
except Exception as e:
|
||||||
logger.warning(f"Vision cache read failed for {file_id}: {e}")
|
logger.warning(f"Vision cache read failed for {file_id}: {e}")
|
||||||
from src.document_processor import analyze_image_with_vl
|
from src.document_processor import analyze_image_with_vl
|
||||||
@@ -307,6 +335,7 @@ def setup_upload_routes(upload_handler):
|
|||||||
f.write(text)
|
f.write(text)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Vision cache write failed for {file_id}: {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}
|
return {"text": text, "cached": False}
|
||||||
|
|
||||||
@router.put("/{file_id}/vision")
|
@router.put("/{file_id}/vision")
|
||||||
@@ -334,6 +363,7 @@ def setup_upload_routes(upload_handler):
|
|||||||
raise HTTPException(400, "text must be a string")
|
raise HTTPException(400, "text must be a string")
|
||||||
with open(_vision_cache_path(file_id), "w", encoding="utf-8") as f:
|
with open(_vision_cache_path(file_id), "w", encoding="utf-8") as f:
|
||||||
f.write(text)
|
f.write(text)
|
||||||
|
_sync_gallery_caption_for_upload(info, file_owner or current_user, text)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
async def periodic_rate_limit_cleanup():
|
async def periodic_rate_limit_cleanup():
|
||||||
|
|||||||
@@ -1201,7 +1201,7 @@ function _renderGrid() {
|
|||||||
.replace(/\.[^.]+$/, '') // drop extension
|
.replace(/\.[^.]+$/, '') // drop extension
|
||||||
.replace(/[_-]+/g, ' ')
|
.replace(/[_-]+/g, ' ')
|
||||||
.trim();
|
.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 promptPreview = labelText.length > 60 ? labelText.substring(0, 58) + '...' : labelText;
|
||||||
const favCls = img.favorite ? ' gallery-fav-active' : '';
|
const favCls = img.favorite ? ' gallery-fav-active' : '';
|
||||||
html += `
|
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>
|
<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>
|
||||||
</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>` : ''}
|
${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">
|
<div class="gallery-detail-section gallery-detail-section-date">
|
||||||
<label>Date</label>
|
<label>Date</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user