fc5: export script for FabledCurator migration — tags + image_artist/tag/series via sha256
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user