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