This repository has been archived on 2026-05-31. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
imagerepo/scripts/export_for_fabledcurator.py
T

134 lines
4.4 KiB
Python

#!/usr/bin/env python
"""Export IR's config (tags, artist assignments, tag associations, series pages)
as a single JSON file that FabledCurator's FC-5 ingest can read.
One-shot tool. Excludes:
- kind='artist' tags (FC stores artist as a row, not a tag — these become
image_artist_assignments keyed by sha256).
- kind='post' tags (FC has Post rows; scheduler refills going forward).
- archive_record, post_metadata, ML predictions/embeddings, suggestion data
(FC handles all of these via its own mechanisms — see FC-5 design doc).
All foreign keys collapsed to natural keys so the output is portable:
image_record.id -> image_record.hash (sha256); tag.id -> (tag.name, tag.kind).
Usage:
python scripts/export_for_fabledcurator.py > ir-export.json
"""
from __future__ import annotations
import json
import sys
from datetime import UTC, datetime
# Bootstrap IR's app context so we can use its SQLAlchemy session.
sys.path.insert(0, ".")
from app import create_app, db # noqa: E402
from app.models import ImageRecord, SeriesPage, Tag, image_tags # noqa: E402
_SKIP_KINDS = frozenset({"artist", "post"})
def _export_tags():
"""All tags except artist + post kinds, with fandom_name resolved."""
out = []
for tag in Tag.query.all():
if tag.kind in _SKIP_KINDS:
continue
fandom_name = None
if tag.fandom_id:
f = Tag.query.get(tag.fandom_id)
if f is not None:
fandom_name = f.name
out.append({
"name": tag.name,
"kind": tag.kind or "general",
"fandom_name": fandom_name,
})
return out
def _export_image_artist_assignments():
"""For each image, find its artist:Name tag (if any) and emit
{sha256, artist_name}."""
out = []
artist_tag_ids = {
t.id: t.name for t in Tag.query.filter_by(kind="artist").all()
}
if not artist_tag_ids:
return out
# Join image_record + image_tags to find which images have artist tags.
q = (
db.session.query(ImageRecord.hash, Tag.name)
.join(image_tags, image_tags.c.image_id == ImageRecord.id)
.join(Tag, Tag.id == image_tags.c.tag_id)
.filter(Tag.kind == "artist")
)
for sha, artist_name in q.all():
out.append({"sha256": sha, "artist_name": artist_name})
return out
def _export_image_tag_associations():
"""For each (image, tag) where tag.kind not in _SKIP_KINDS, emit
{sha256, tag_name, tag_kind, fandom_name}."""
out = []
q = (
db.session.query(ImageRecord.hash, Tag.name, Tag.kind, Tag.fandom_id)
.join(image_tags, image_tags.c.image_id == ImageRecord.id)
.join(Tag, Tag.id == image_tags.c.tag_id)
.filter(~Tag.kind.in_(_SKIP_KINDS))
)
fandom_name_cache: dict[int, str] = {}
for sha, tag_name, tag_kind, fandom_id in q.all():
fandom_name = None
if fandom_id:
if fandom_id not in fandom_name_cache:
f = Tag.query.get(fandom_id)
fandom_name_cache[fandom_id] = f.name if f else None
fandom_name = fandom_name_cache[fandom_id]
out.append({
"sha256": sha,
"tag_name": tag_name,
"tag_kind": tag_kind or "general",
"fandom_name": fandom_name,
})
return out
def _export_series_pages():
"""For each SeriesPage, emit {sha256, series_tag_name, page_number}."""
out = []
q = (
db.session.query(ImageRecord.hash, Tag.name, SeriesPage.page_number)
.join(SeriesPage, SeriesPage.image_id == ImageRecord.id)
.join(Tag, Tag.id == SeriesPage.series_tag_id)
)
for sha, series_tag_name, page_number in q.all():
out.append({
"sha256": sha,
"series_tag_name": series_tag_name,
"page_number": page_number,
})
return out
def main() -> None:
app = create_app()
with app.app_context():
export = {
"source_app": "imagerepo",
"schema_version": 1,
"exported_at": datetime.now(UTC).isoformat(),
"tags": _export_tags(),
"image_artist_assignments": _export_image_artist_assignments(),
"image_tag_associations": _export_image_tag_associations(),
"series_pages": _export_series_pages(),
}
json.dump(export, sys.stdout, indent=2)
sys.stdout.write("\n")
if __name__ == "__main__":
main()