68fa86ea52
Markdown Canonical Phase 1B.5 — marker 가 추출하던 이미지를 NAS 에 영구 저장하고
DB 메타 + 인증 라우트 + 프론트 swap 까지 wiring.
핵심 변경:
- marker-service /convert 응답에 base64 image 리스트 포함 (stateless 유지, NAS write 권한 X)
- marker_worker 가 NAS `/documents/extracted_images/{doc_id}/` 에 persist + UPSERT +
고아 row DELETE + md_content ref 를 `docimg:img_NNN` stable scheme 으로 정규화
- /api/documents/{id}/images/{key}/raw 인증 라우트 (Cache-Control private + ETag = content_hash)
- frontend MarkdownDoc 가 placeholder card 안의 docimg ref 를 실제 <img> 로 swap
원칙:
- 이미지 binary = NAS, metadata = Postgres (학습 섹션 패턴 동일)
- image_key sequence 기반 결정적 → 재변환 idempotent
- MARKDOWN_IMAGE_PERSIST=false env 로 rollback 가능 (placeholder card 폴백 자연 유지)
기존 28건 marker success 문서는 본 PR 에서 건드리지 않음 — deploy + 신규 업로드 1건 +
sample 5건 검증 후 scripts/marker_reprocess_existing_success.py 로 targeted reprocess.
plan: ~/.claude/plans/piped-humming-crystal.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
43 lines
1.8 KiB
Python
43 lines
1.8 KiB
Python
"""document_images ORM (Phase 1B.5) — marker 추출 이미지 메타.
|
|
|
|
저장: NAS `/documents/extracted_images/{document_id}/{image_key}.{ext}`
|
|
표시: GET /api/documents/{doc_id}/images/{image_key}/raw (인증 필요)
|
|
|
|
md_content 의 ref 는 `` 형식 — image_key 가 sequence 기반 결정적이라
|
|
재변환 시 idempotent.
|
|
"""
|
|
|
|
from datetime import datetime
|
|
|
|
from sqlalchemy import BigInteger, DateTime, ForeignKey, Integer, String, Text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from core.database import Base
|
|
|
|
|
|
class DocumentImage(Base):
|
|
__tablename__ = "document_images"
|
|
|
|
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
|
document_id: Mapped[int] = mapped_column(
|
|
BigInteger, ForeignKey("documents.id", ondelete="CASCADE"), nullable=False
|
|
)
|
|
image_key: Mapped[str] = mapped_column(String(32), nullable=False)
|
|
relative_path: Mapped[str] = mapped_column(Text, nullable=False)
|
|
file_path: Mapped[str] = mapped_column(Text, nullable=False)
|
|
mime_type: Mapped[str] = mapped_column(Text, nullable=False)
|
|
file_size: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
|
content_hash: Mapped[str] = mapped_column(String(64), nullable=False)
|
|
width: Mapped[int | None] = mapped_column(Integer)
|
|
height: Mapped[int | None] = mapped_column(Integer)
|
|
page_index: Mapped[int | None] = mapped_column(Integer)
|
|
alt_text: Mapped[str | None] = mapped_column(Text)
|
|
source_slug: Mapped[str | None] = mapped_column(Text)
|
|
extraction_engine: Mapped[str] = mapped_column(
|
|
String(32), nullable=False, default="marker"
|
|
)
|
|
extraction_engine_version: Mapped[str | None] = mapped_column(String(32))
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=datetime.now, nullable=False
|
|
)
|