← back to Animate Museum Posts
initial snapshot — gitify all builds (CLAUDE.md rule 2026-05-06)
e5e8b392d667a17855b7291f53967611fd3bb099 · 2026-05-06 10:20:31 -0700 · Steve
Files touched
A .env.exampleA .gitignoreA DRAMATIC_VIDEO_MODELS_RESEARCH.jsonA README.mdA REPLICATE_MODELS_QUICK_REFERENCE.mdA assets/intro.mp4A assets/outro.mp4A data/last-run.jsonA data/posted-artworks.jsonA ecosystem.config.cjsA museum-art-bot.serviceA output/artwork_1769835612798.jpgA output/artwork_1769839200769.jpgA output/artwork_1769930397505.jpgA output/artwork_1769955541913.jpgA output/artwork_1770011755402.jpgA output/artwork_1770020944859.jpgA output/artwork_1770035327832.jpgA output/artwork_1770044154986.jpgA output/artwork_1770089254297.jpgA output/artwork_1770098561130.jpgA output/artwork_1770117556959.jpgA output/artwork_1770120000835.jpgA output/artwork_1770125167080.jpgA output/artwork_1770195505488.jpgA output/artwork_1770210985020.jpgA output/artwork_1770231056684.jpgA output/artwork_1770277360291.jpgA output/artwork_1770288361064.jpgA output/artwork_1770295495989.jpgA output/artwork_1770302991852.jpgA output/artwork_1770323423731.jpgA output/artwork_1770326084840.jpgA output/artwork_1770336000687.jpgA output/artwork_1770339021052.jpgA output/artwork_1770362717875.jpgA output/artwork_1770362957605.jpgA output/artwork_1770363320841.jpgA output/artwork_1770363442632.jpgA output/artwork_1770363687872.jpgA output/artwork_1770365013472.jpgA output/artwork_1770365135248.jpgA output/artwork_1770365256192.jpgA output/artwork_1770379201423.jpgA output/artwork_1770400804167.jpgA output/artwork_1770403453727.jpgA output/artwork_1770403936465.jpgA output/artwork_1770404299077.jpgA output/artwork_1770404783572.jpgA output/artwork_1770405388375.jpgA output/artwork_1770405629028.jpgA output/artwork_1770405750351.jpgA output/met_1769774401699.jpgA output/met_1769796007708.jpgA output/met_1769817606610.jpgA output/met_1769824243953.jpgA output/met_1769828839876.jpgA output/met_1769828852441.jpgA output/met_1769832460238.jpgA output/met_1769835612556.jpgA output/met_1769836572474.jpgA output/met_1769840680766.jpgA output/met_1769841644674.jpgA output/met_1769843098236.jpgA output/met_1769843216906.jpgA output/met_1769843345030.jpgA output/met_1769843461010.jpgA output/met_1769843461280.jpgA output/met_1769843466834.jpgA output/met_1769843591986.jpgA output/met_1769843702177.jpgA output/met_1769843822499.jpgA output/met_1769843823100.jpgA output/met_1769843949676.jpgA output/met_1769844064441.jpgA output/met_1769844188929.jpgA output/met_1769844311012.jpgA output/met_1769844434712.jpgA output/met_1769844438877.jpgA output/met_1769844448749.jpgA output/met_1769844548184.jpgA output/met_1769844669775.jpgA output/met_1769844789562.jpgA output/met_1769844923285.jpgA output/met_1769845033423.jpgA output/met_1769845158195.jpgA output/met_1769860802556.jpgA output/met_1769860812709.jpgA output/met_1769882402007.jpgA output/met_1769904014088.jpgA output/met_1769911662299.jpgA output/met_1769911669065.jpgA output/met_1769914804593.jpgA output/met_1769916860555.jpgA output/met_1769920133642.jpgA output/met_1769920494801.jpgA output/met_1769921457271.jpgA output/met_1769923147919.jpgA output/met_1769925610592.jpgA output/met_1769926772338.jpgA output/met_1769929554107.jpgA output/met_1769929678565.jpgA output/met_1769929794229.jpgA output/met_1769929922950.jpgA output/met_1769930520505.jpgA output/met_1769933312063.jpgA output/met_1769937171852.jpgA output/met_1769937780871.jpgA output/met_1769941642381.jpgA output/met_1769941646983.jpgA output/met_1769942129365.jpgA output/met_1769943214162.jpgA output/met_1769947206695.jpgA output/met_1769962549972.jpgA output/met_1769962554385.jpgA output/met_1769962672220.jpgA output/met_1769962673381.jpgA output/met_1769962800813.jpgA output/met_1769963033383.jpgA output/met_1769968826586.jpgA output/met_1769990401393.jpgA output/met_1770001239283.jpgA output/met_1770004262088.jpgA output/met_1770006683704.jpgA output/met_1770012002839.jpgA output/met_1770012855219.jpgA output/met_1770012970126.jpgA output/met_1770021672748.jpgA output/met_1770021674762.jpgA output/met_1770021683281.jpgA output/met_1770025066268.jpgA output/met_1770028085718.jpgA output/met_1770028090601.jpgA output/met_1770029176693.jpgA output/met_1770029894518.jpgA output/met_1770033612582.jpgA output/met_1770036179891.jpgA output/met_1770036537777.jpgA output/met_1770042464781.jpgA output/met_1770044397517.jpgA output/met_1770045608182.jpgA output/met_1770045726564.jpgA output/met_1770045727605.jpgA output/met_1770047900924.jpgA output/met_1770048747161.jpgA output/met_1770049715124.jpgA output/met_1770049717751.jpgA output/met_1770055201231.jpgA output/met_1770056494533.jpgA output/met_1770056522260.jpgA output/met_1770060356331.jpgA output/met_1770076812528.jpgA output/met_1770089254010.jpgA output/met_1770089504384.jpgA output/met_1770089623207.jpgA output/met_1770089623458.jpgA output/met_1770089628495.jpgA output/met_1770094690889.jpgA output/met_1770095544765.jpgA output/met_1770097721435.jpgA output/met_1770098409457.jpgA output/met_1770101829396.jpgA output/met_1770109208616.jpgA output/met_1770109209173.jpgA output/met_1770113437738.jpgA output/met_1770113560269.jpgA output/met_1770116219173.jpgA output/met_1770116584262.jpgA output/met_1770117548291.jpgA output/met_1770117556813.jpgA output/met_1770118757428.jpgA output/met_1770118766479.jpgA output/met_1770119246203.jpgA output/met_1770119604354.jpgA output/met_1770123232703.jpgA output/met_1770128072042.jpgA output/met_1770130486349.jpgA output/met_1770131219546.jpgA output/met_1770132783774.jpgA output/met_1770136776211.jpgA output/met_1770137259082.jpgA output/met_1770141601019.jpgA output/met_1770163200840.jpgA output/met_1770184809670.jpgA output/met_1770187292538.jpgA output/met_1770193219082.jpgA output/met_1770194788037.jpgA output/met_1770205427261.jpgA output/met_1770205675085.jpgA output/met_1770206401310.jpgA output/met_1770209051083.jpgA output/met_1770210137607.jpgA output/met_1770210984839.jpgA output/met_1770213892314.jpgA output/met_1770216790772.jpgA output/met_1770219207330.jpgA output/met_1770219567965.jpgA output/met_1770228003631.jpgA output/met_1770229969857.jpgA output/met_1770229975286.jpgA output/met_1770230089672.jpgA output/met_1770230210902.jpgA output/met_1770230330662.jpgA output/met_1770230451234.jpgA output/met_1770230592106.jpgA output/met_1770230695020.jpgA output/met_1770230814342.jpgA output/met_1770230939263.jpgA output/met_1770238809207.jpgA output/met_1770249604983.jpgA output/met_1770254631549.jpgA output/met_1770254640655.jpgA output/met_1770271201413.jpgA output/met_1770276282753.jpgA output/met_1770277360141.jpgA output/met_1770278816483.jpgA output/met_1770285822826.jpgA output/met_1770288241371.jpgA output/met_1770288252741.jpgA output/met_1770289811569.jpgA output/met_1770290295801.jpgA output/met_1770290417447.jpgA output/met_1770292809038.jpgA output/met_1770295133865.jpgA output/met_1770300453946.jpgA output/met_1770300454667.jpgA output/met_1770304687726.jpgA output/met_1770313275637.jpgA output/met_1770314406918.jpgA output/met_1770314410080.jpgA output/met_1770314414339.jpgA output/met_1770317020417.jpgA output/met_1770317260998.jpgA output/met_1770323306047.jpgA output/met_1770323312868.jpgA output/met_1770323313872.jpgA output/met_1770332495769.jpgA output/met_1770335520515.jpgA output/met_1770336246161.jpgA output/met_1770339148949.jpgA output/met_1770357605609.jpgA output/met_1770362717630.jpgA output/met_1770362838604.jpgA output/met_1770363080904.jpgA output/met_1770363210261.jpgA output/met_1770363563838.jpgA output/met_1770363687688.jpgA output/met_1770363807152.jpgA output/met_1770363933897.jpgA output/met_1770364054606.jpgA output/met_1770364171679.jpgA output/met_1770364292208.jpgA output/met_1770364419933.jpgA output/met_1770364531892.jpgA output/met_1770364652950.jpgA output/met_1770364775830.jpgA output/met_1770364896203.jpgA output/met_1770365379809.jpgA output/met_1770365382758.jpgA output/met_1770365501933.jpgA output/met_1770365618694.jpgA output/met_1770379201260.jpgA output/met_1770400803944.jpgA output/met_1770403099419.jpgA output/met_1770403221810.jpgA output/met_1770403333759.jpgA output/met_1770403574988.jpgA output/met_1770403697204.jpgA output/met_1770403816330.jpgA output/met_1770404061322.jpgA output/met_1770404180659.jpgA output/met_1770404431808.jpgA output/met_1770404432466.jpgA output/met_1770404543372.jpgA output/met_1770404663460.jpgA output/met_1770404669818.jpgA output/met_1770404911996.jpgA output/met_1770405027734.jpgA output/met_1770405148169.jpgA output/met_1770405153396.jpgA output/met_1770405267893.jpgA output/met_1770405510402.jpgA output/met_1770405883635.jpgA output/met_1770405998755.jpgA output/met_1770405999008.jpgA output/met_1770406121993.jpgA output/metadata_1769835612798.jsonA output/metadata_1769839200769.jsonA output/metadata_1769930397505.jsonA output/metadata_1769955541913.jsonA output/metadata_1770011755402.jsonA output/metadata_1770020944859.jsonA output/metadata_1770035327832.jsonA output/metadata_1770044154986.jsonA output/metadata_1770089254297.jsonA output/metadata_1770098561130.jsonA output/metadata_1770117556959.jsonA output/metadata_1770120000835.jsonA output/metadata_1770125167080.jsonA output/metadata_1770195505488.jsonA output/metadata_1770210985020.jsonA output/metadata_1770231056684.jsonA output/metadata_1770277360291.jsonA output/metadata_1770288361064.jsonA output/metadata_1770295495989.jsonA output/metadata_1770302991852.jsonA output/metadata_1770323423731.jsonA output/metadata_1770326084840.jsonA output/metadata_1770336000687.jsonA output/metadata_1770339021052.jsonA output/metadata_1770362717875.jsonA output/metadata_1770362957605.jsonA output/metadata_1770363320841.jsonA output/metadata_1770363442632.jsonA output/metadata_1770363687872.jsonA output/metadata_1770365013472.jsonA output/metadata_1770365135248.jsonA output/metadata_1770365256192.jsonA output/metadata_1770379201423.jsonA output/metadata_1770400804167.jsonA output/metadata_1770403453727.jsonA output/metadata_1770403936465.jsonA output/metadata_1770404299077.jsonA output/metadata_1770404783572.jsonA output/metadata_1770405388375.jsonA output/metadata_1770405629028.jsonA output/metadata_1770405750351.jsonA output/thumbnails/thumb_1768128218436.jpgA output/thumbnails/thumb_1768128310140.jpgA output/thumbnails/thumb_1768144317138.jpgA output/thumbnails/thumb_1768144518253.jpgA output/thumbnails/thumb_1768144732341.jpgA output/thumbnails/thumb_1768144934368.jpgA output/thumbnails/thumb_1768145120280.jpgA output/thumbnails/thumb_1768145314449.jpgA output/thumbnails/thumb_1768152020572.jpgA output/thumbnails/thumb_1768152215875.jpgA output/thumbnails/thumb_1768152401849.jpgA output/thumbnails/thumb_1768152606874.jpgA output/thumbnails/thumb_1768152801379.jpgA output/thumbnails/thumb_1768153001502.jpgA output/thumbnails/thumb_1768153191039.jpgA output/thumbnails/thumb_1768153374417.jpgA output/thumbnails/thumb_1768153551861.jpgA package-lock.jsonA package.jsonA public/callback.htmlA public/favicon.svgA public/index.htmlA public/privacy.htmlA public/terms.htmlA ralphy.jsA scripts/post-3-wave-branded.jsA scripts/test-wave-metamorphosis.jsA scripts/test-wave-moderate.jsA scripts/wave-repeat-test.jsA setup.shA src/addBranding.jsA src/animateKenBurns.jsA src/animateWithAI.jsA src/animateWithGrok.jsA src/animateWithGrokFrames.jsA src/animateWithHuggingFace.jsA src/animateWithLuma.jsA src/animateWithMorphing.jsA src/animateWithReplicate.jsA src/animateWithReplicateAlt.jsA src/animateWithRunway.jsA src/artworks.jsA src/contentGenerator.jsA src/createAnimation.jsA src/database.jsA src/duplicateTracker.jsA src/fetchArtwork.jsA src/fetchFromAIC.jsA src/fetchFromMet.jsA src/fetchFromMuseums.jsA src/fetchFromRijksmuseum.jsA src/healthServer.jsA src/index.jsA src/postToX.jsA src/runOvernight.jsA src/scheduledPoster.jsA src/sendEmail.jsA src/slackNotify.jsA src/utils/retry.jsA src/validateCredentials.jsA src/verifyVideo.jsA src/webDashboard.jsA tasks/prd-museum-art-bot.mdA test-all-services.jsA test-sunflowers.js
Diff
commit e5e8b392d667a17855b7291f53967611fd3bb099
Author: Steve <steve@designerwallcoverings.com>
Date: Wed May 6 10:20:31 2026 -0700
initial snapshot — gitify all builds (CLAUDE.md rule 2026-05-06)
---
.env.example | 25 +
.gitignore | 7 +
DRAMATIC_VIDEO_MODELS_RESEARCH.json | 608 +++++
README.md | 153 ++
REPLICATE_MODELS_QUICK_REFERENCE.md | 303 +++
assets/intro.mp4 | Bin 0 -> 7951 bytes
assets/outro.mp4 | Bin 0 -> 10477 bytes
data/last-run.json | 5 +
data/posted-artworks.json | 3794 +++++++++++++++++++++++++++++
ecosystem.config.cjs | 39 +
museum-art-bot.service | 17 +
output/artwork_1769835612798.jpg | Bin 0 -> 507944 bytes
output/artwork_1769839200769.jpg | Bin 0 -> 170794 bytes
output/artwork_1769930397505.jpg | Bin 0 -> 216699 bytes
output/artwork_1769955541913.jpg | Bin 0 -> 507944 bytes
output/artwork_1770011755402.jpg | Bin 0 -> 507944 bytes
output/artwork_1770020944859.jpg | Bin 0 -> 955353 bytes
output/artwork_1770035327832.jpg | Bin 0 -> 881302 bytes
output/artwork_1770044154986.jpg | Bin 0 -> 170794 bytes
output/artwork_1770089254297.jpg | Bin 0 -> 507944 bytes
output/artwork_1770098561130.jpg | Bin 0 -> 216699 bytes
output/artwork_1770117556959.jpg | Bin 0 -> 216699 bytes
output/artwork_1770120000835.jpg | Bin 0 -> 881302 bytes
output/artwork_1770125167080.jpg | Bin 0 -> 1391462 bytes
output/artwork_1770195505488.jpg | Bin 0 -> 320721 bytes
output/artwork_1770210985020.jpg | Bin 0 -> 176929 bytes
output/artwork_1770231056684.jpg | Bin 0 -> 507944 bytes
output/artwork_1770277360291.jpg | Bin 0 -> 176929 bytes
output/artwork_1770288361064.jpg | Bin 0 -> 216699 bytes
output/artwork_1770295495989.jpg | Bin 0 -> 320721 bytes
output/artwork_1770302991852.jpg | Bin 0 -> 216699 bytes
output/artwork_1770323423731.jpg | Bin 0 -> 216699 bytes
output/artwork_1770326084840.jpg | Bin 0 -> 269652 bytes
output/artwork_1770336000687.jpg | Bin 0 -> 1391462 bytes
output/artwork_1770339021052.jpg | Bin 0 -> 216699 bytes
output/artwork_1770362717875.jpg | Bin 0 -> 176929 bytes
output/artwork_1770362957605.jpg | Bin 0 -> 647241 bytes
output/artwork_1770363320841.jpg | Bin 0 -> 881302 bytes
output/artwork_1770363442632.jpg | Bin 0 -> 176929 bytes
output/artwork_1770363687872.jpg | Bin 0 -> 881302 bytes
output/artwork_1770365013472.jpg | Bin 0 -> 955353 bytes
output/artwork_1770365135248.jpg | Bin 0 -> 216699 bytes
output/artwork_1770365256192.jpg | Bin 0 -> 955353 bytes
output/artwork_1770379201423.jpg | Bin 0 -> 647241 bytes
output/artwork_1770400804167.jpg | Bin 0 -> 1391462 bytes
output/artwork_1770403453727.jpg | Bin 0 -> 507944 bytes
output/artwork_1770403936465.jpg | Bin 0 -> 881302 bytes
output/artwork_1770404299077.jpg | Bin 0 -> 955353 bytes
output/artwork_1770404783572.jpg | Bin 0 -> 176929 bytes
output/artwork_1770405388375.jpg | Bin 0 -> 1391462 bytes
output/artwork_1770405629028.jpg | Bin 0 -> 647241 bytes
output/artwork_1770405750351.jpg | Bin 0 -> 1391462 bytes
output/met_1769774401699.jpg | Bin 0 -> 5627064 bytes
output/met_1769796007708.jpg | Bin 0 -> 3694106 bytes
output/met_1769817606610.jpg | Bin 0 -> 1960115 bytes
output/met_1769824243953.jpg | Bin 0 -> 974580 bytes
output/met_1769828839876.jpg | Bin 0 -> 4369131 bytes
output/met_1769828852441.jpg | Bin 0 -> 2997347 bytes
output/met_1769832460238.jpg | Bin 0 -> 1432331 bytes
output/met_1769835612556.jpg | Bin 0 -> 5224241 bytes
output/met_1769836572474.jpg | Bin 0 -> 2345438 bytes
output/met_1769840680766.jpg | Bin 0 -> 1061942 bytes
output/met_1769841644674.jpg | Bin 0 -> 1046161 bytes
output/met_1769843098236.jpg | Bin 0 -> 2843065 bytes
output/met_1769843216906.jpg | Bin 0 -> 542065 bytes
output/met_1769843345030.jpg | Bin 0 -> 1997303 bytes
output/met_1769843461010.jpg | Bin 0 -> 1538594 bytes
output/met_1769843461280.jpg | Bin 0 -> 2048879 bytes
output/met_1769843466834.jpg | Bin 0 -> 1900548 bytes
output/met_1769843591986.jpg | Bin 0 -> 3646460 bytes
output/met_1769843702177.jpg | Bin 0 -> 100864 bytes
output/met_1769843822499.jpg | Bin 0 -> 961548 bytes
output/met_1769843823100.jpg | Bin 0 -> 2138600 bytes
output/met_1769843949676.jpg | Bin 0 -> 1840600 bytes
output/met_1769844064441.jpg | Bin 0 -> 1730251 bytes
output/met_1769844188929.jpg | Bin 0 -> 1092718 bytes
output/met_1769844311012.jpg | Bin 0 -> 2478995 bytes
output/met_1769844434712.jpg | Bin 0 -> 4474074 bytes
output/met_1769844438877.jpg | Bin 0 -> 1101083 bytes
output/met_1769844448749.jpg | Bin 0 -> 3196124 bytes
output/met_1769844548184.jpg | Bin 0 -> 519889 bytes
output/met_1769844669775.jpg | Bin 0 -> 3548514 bytes
output/met_1769844789562.jpg | Bin 0 -> 2623926 bytes
output/met_1769844923285.jpg | Bin 0 -> 4523925 bytes
output/met_1769845033423.jpg | Bin 0 -> 1816098 bytes
output/met_1769845158195.jpg | Bin 0 -> 1394102 bytes
output/met_1769860802556.jpg | Bin 0 -> 1350422 bytes
output/met_1769860812709.jpg | Bin 0 -> 3169955 bytes
output/met_1769882402007.jpg | Bin 0 -> 626517 bytes
output/met_1769904014088.jpg | Bin 0 -> 3679202 bytes
output/met_1769911662299.jpg | Bin 0 -> 1226740 bytes
output/met_1769911669065.jpg | Bin 0 -> 3014056 bytes
output/met_1769914804593.jpg | Bin 0 -> 1289210 bytes
output/met_1769916860555.jpg | Bin 0 -> 3879918 bytes
output/met_1769920133642.jpg | Bin 0 -> 2426550 bytes
output/met_1769920494801.jpg | Bin 0 -> 3253830 bytes
output/met_1769921457271.jpg | Bin 0 -> 2630138 bytes
output/met_1769923147919.jpg | Bin 0 -> 2325608 bytes
output/met_1769925610592.jpg | Bin 0 -> 4973821 bytes
output/met_1769926772338.jpg | Bin 0 -> 59177 bytes
output/met_1769929554107.jpg | Bin 0 -> 1980073 bytes
output/met_1769929678565.jpg | Bin 0 -> 3092584 bytes
output/met_1769929794229.jpg | Bin 0 -> 1135263 bytes
output/met_1769929922950.jpg | Bin 0 -> 2334792 bytes
output/met_1769930520505.jpg | Bin 0 -> 1112937 bytes
output/met_1769933312063.jpg | Bin 0 -> 3786230 bytes
output/met_1769937171852.jpg | Bin 0 -> 1588127 bytes
output/met_1769937780871.jpg | Bin 0 -> 3669264 bytes
output/met_1769941642381.jpg | Bin 0 -> 2319400 bytes
output/met_1769941646983.jpg | Bin 0 -> 2729419 bytes
output/met_1769942129365.jpg | Bin 0 -> 1291905 bytes
output/met_1769943214162.jpg | Bin 0 -> 706968 bytes
output/met_1769947206695.jpg | Bin 0 -> 2439974 bytes
output/met_1769962549972.jpg | Bin 0 -> 3646460 bytes
output/met_1769962554385.jpg | Bin 0 -> 1156949 bytes
output/met_1769962672220.jpg | Bin 0 -> 2508895 bytes
output/met_1769962673381.jpg | Bin 0 -> 2271853 bytes
output/met_1769962800813.jpg | Bin 0 -> 1343061 bytes
output/met_1769963033383.jpg | Bin 0 -> 1510179 bytes
output/met_1769968826586.jpg | Bin 0 -> 12724306 bytes
output/met_1769990401393.jpg | Bin 0 -> 2427726 bytes
output/met_1770001239283.jpg | Bin 0 -> 788277 bytes
output/met_1770004262088.jpg | Bin 0 -> 2348925 bytes
output/met_1770006683704.jpg | Bin 0 -> 1472219 bytes
output/met_1770012002839.jpg | Bin 0 -> 3294235 bytes
output/met_1770012855219.jpg | Bin 0 -> 4296939 bytes
output/met_1770012970126.jpg | Bin 0 -> 1216612 bytes
output/met_1770021672748.jpg | Bin 0 -> 607156 bytes
output/met_1770021674762.jpg | Bin 0 -> 626517 bytes
output/met_1770021683281.jpg | Bin 0 -> 2115886 bytes
output/met_1770025066268.jpg | Bin 0 -> 3263209 bytes
output/met_1770028085718.jpg | Bin 0 -> 2891100 bytes
output/met_1770028090601.jpg | Bin 0 -> 712832 bytes
output/met_1770029176693.jpg | Bin 0 -> 2566981 bytes
output/met_1770029894518.jpg | Bin 0 -> 3661377 bytes
output/met_1770033612582.jpg | Bin 0 -> 5621860 bytes
output/met_1770036179891.jpg | Bin 0 -> 1228292 bytes
output/met_1770036537777.jpg | Bin 0 -> 1819019 bytes
output/met_1770042464781.jpg | Bin 0 -> 1230867 bytes
output/met_1770044397517.jpg | Bin 0 -> 467757 bytes
output/met_1770045608182.jpg | Bin 0 -> 1460287 bytes
output/met_1770045726564.jpg | Bin 0 -> 1168501 bytes
output/met_1770045727605.jpg | Bin 0 -> 927757 bytes
output/met_1770047900924.jpg | Bin 0 -> 2431690 bytes
output/met_1770048747161.jpg | Bin 0 -> 54444 bytes
output/met_1770049715124.jpg | Bin 0 -> 2138600 bytes
output/met_1770049717751.jpg | Bin 0 -> 730708 bytes
output/met_1770055201231.jpg | Bin 0 -> 1584717 bytes
output/met_1770056494533.jpg | Bin 0 -> 4610061 bytes
output/met_1770056522260.jpg | Bin 0 -> 14846906 bytes
output/met_1770060356331.jpg | Bin 0 -> 2384583 bytes
output/met_1770076812528.jpg | Bin 0 -> 4762877 bytes
output/met_1770089254010.jpg | Bin 0 -> 2508895 bytes
output/met_1770089504384.jpg | Bin 0 -> 1650864 bytes
output/met_1770089623207.jpg | Bin 0 -> 5352386 bytes
output/met_1770089623458.jpg | Bin 0 -> 1289210 bytes
output/met_1770089628495.jpg | Bin 0 -> 2263231 bytes
output/met_1770094690889.jpg | Bin 0 -> 986039 bytes
output/met_1770095544765.jpg | Bin 0 -> 2246859 bytes
output/met_1770097721435.jpg | Bin 0 -> 2823831 bytes
output/met_1770098409457.jpg | Bin 0 -> 4540273 bytes
output/met_1770101829396.jpg | Bin 0 -> 2463091 bytes
output/met_1770109208616.jpg | Bin 0 -> 3113210 bytes
output/met_1770109209173.jpg | Bin 0 -> 719704 bytes
output/met_1770113437738.jpg | Bin 0 -> 1299656 bytes
output/met_1770113560269.jpg | Bin 0 -> 2033760 bytes
output/met_1770116219173.jpg | Bin 0 -> 5739447 bytes
output/met_1770116584262.jpg | Bin 0 -> 1174224 bytes
output/met_1770117548291.jpg | Bin 0 -> 2104668 bytes
output/met_1770117556813.jpg | Bin 0 -> 3008706 bytes
output/met_1770118757428.jpg | Bin 0 -> 3053431 bytes
output/met_1770118766479.jpg | Bin 0 -> 2474458 bytes
output/met_1770119246203.jpg | Bin 0 -> 2289610 bytes
output/met_1770119604354.jpg | Bin 0 -> 1412805 bytes
output/met_1770123232703.jpg | Bin 0 -> 3122987 bytes
output/met_1770128072042.jpg | Bin 0 -> 980450 bytes
output/met_1770130486349.jpg | Bin 0 -> 2313602 bytes
output/met_1770131219546.jpg | Bin 0 -> 2530607 bytes
output/met_1770132783774.jpg | Bin 0 -> 2378344 bytes
output/met_1770136776211.jpg | Bin 0 -> 1206231 bytes
output/met_1770137259082.jpg | Bin 0 -> 5352573 bytes
output/met_1770141601019.jpg | Bin 0 -> 3605589 bytes
output/met_1770163200840.jpg | Bin 0 -> 3073778 bytes
output/met_1770184809670.jpg | Bin 0 -> 2237083 bytes
output/met_1770187292538.jpg | Bin 0 -> 1947988 bytes
output/met_1770193219082.jpg | Bin 0 -> 1700329 bytes
output/met_1770194788037.jpg | Bin 0 -> 2686097 bytes
output/met_1770205427261.jpg | Bin 0 -> 4137333 bytes
output/met_1770205675085.jpg | Bin 0 -> 2701001 bytes
output/met_1770206401310.jpg | Bin 0 -> 1512662 bytes
output/met_1770209051083.jpg | Bin 0 -> 3271947 bytes
output/met_1770210137607.jpg | Bin 0 -> 1092718 bytes
output/met_1770210984839.jpg | Bin 0 -> 1604887 bytes
output/met_1770213892314.jpg | Bin 0 -> 3460601 bytes
output/met_1770216790772.jpg | Bin 0 -> 1357735 bytes
output/met_1770219207330.jpg | Bin 0 -> 685082 bytes
output/met_1770219567965.jpg | Bin 0 -> 708221 bytes
output/met_1770228003631.jpg | Bin 0 -> 968795 bytes
output/met_1770229969857.jpg | Bin 0 -> 2259365 bytes
output/met_1770229975286.jpg | Bin 0 -> 2109398 bytes
output/met_1770230089672.jpg | Bin 0 -> 2431690 bytes
output/met_1770230210902.jpg | Bin 0 -> 2638182 bytes
output/met_1770230330662.jpg | Bin 0 -> 5982471 bytes
output/met_1770230451234.jpg | Bin 0 -> 1326704 bytes
output/met_1770230592106.jpg | Bin 0 -> 7126163 bytes
output/met_1770230695020.jpg | Bin 0 -> 1465285 bytes
output/met_1770230814342.jpg | Bin 0 -> 2257458 bytes
output/met_1770230939263.jpg | Bin 0 -> 1195950 bytes
output/met_1770238809207.jpg | Bin 0 -> 3629816 bytes
output/met_1770249604983.jpg | Bin 0 -> 208467 bytes
output/met_1770254631549.jpg | Bin 0 -> 2254326 bytes
output/met_1770254640655.jpg | Bin 0 -> 3519592 bytes
output/met_1770271201413.jpg | Bin 0 -> 1882182 bytes
output/met_1770276282753.jpg | Bin 0 -> 3378778 bytes
output/met_1770277360141.jpg | Bin 0 -> 964072 bytes
output/met_1770278816483.jpg | Bin 0 -> 2351381 bytes
output/met_1770285822826.jpg | Bin 0 -> 2519804 bytes
output/met_1770288241371.jpg | Bin 0 -> 2695357 bytes
output/met_1770288252741.jpg | Bin 0 -> 5234940 bytes
output/met_1770289811569.jpg | Bin 0 -> 1324417 bytes
output/met_1770290295801.jpg | Bin 0 -> 1644091 bytes
output/met_1770290417447.jpg | Bin 0 -> 654219 bytes
output/met_1770292809038.jpg | Bin 0 -> 3439540 bytes
output/met_1770295133865.jpg | Bin 0 -> 3001010 bytes
output/met_1770300453946.jpg | Bin 0 -> 3073778 bytes
output/met_1770300454667.jpg | Bin 0 -> 2651194 bytes
output/met_1770304687726.jpg | Bin 0 -> 1546115 bytes
output/met_1770313275637.jpg | Bin 0 -> 1377669 bytes
output/met_1770314406918.jpg | Bin 0 -> 1676540 bytes
output/met_1770314410080.jpg | Bin 0 -> 8640487 bytes
output/met_1770314414339.jpg | Bin 0 -> 1686399 bytes
output/met_1770317020417.jpg | Bin 0 -> 531691 bytes
output/met_1770317260998.jpg | Bin 0 -> 1771038 bytes
output/met_1770323306047.jpg | Bin 0 -> 4973821 bytes
output/met_1770323312868.jpg | Bin 0 -> 1730157 bytes
output/met_1770323313872.jpg | Bin 0 -> 4412274 bytes
output/met_1770332495769.jpg | Bin 0 -> 1292971 bytes
output/met_1770335520515.jpg | Bin 0 -> 1686399 bytes
output/met_1770336246161.jpg | Bin 0 -> 1133493 bytes
output/met_1770339148949.jpg | Bin 0 -> 1410919 bytes
output/met_1770357605609.jpg | Bin 0 -> 2476245 bytes
output/met_1770362717630.jpg | Bin 0 -> 2150565 bytes
output/met_1770362838604.jpg | Bin 0 -> 4188255 bytes
output/met_1770363080904.jpg | Bin 0 -> 597468 bytes
output/met_1770363210261.jpg | Bin 0 -> 4014177 bytes
output/met_1770363563838.jpg | Bin 0 -> 2487888 bytes
output/met_1770363687688.jpg | Bin 0 -> 1267758 bytes
output/met_1770363807152.jpg | Bin 0 -> 2706101 bytes
output/met_1770363933897.jpg | Bin 0 -> 4017428 bytes
output/met_1770364054606.jpg | Bin 0 -> 2410865 bytes
output/met_1770364171679.jpg | Bin 0 -> 3995154 bytes
output/met_1770364292208.jpg | Bin 0 -> 992247 bytes
output/met_1770364419933.jpg | Bin 0 -> 3048413 bytes
output/met_1770364531892.jpg | Bin 0 -> 1084027 bytes
output/met_1770364652950.jpg | Bin 0 -> 3088085 bytes
output/met_1770364775830.jpg | Bin 0 -> 3018065 bytes
output/met_1770364896203.jpg | Bin 0 -> 2242552 bytes
output/met_1770365379809.jpg | Bin 0 -> 2695261 bytes
output/met_1770365382758.jpg | Bin 0 -> 1234962 bytes
output/met_1770365501933.jpg | Bin 0 -> 2052531 bytes
output/met_1770365618694.jpg | Bin 0 -> 2899641 bytes
output/met_1770379201260.jpg | Bin 0 -> 1743565 bytes
output/met_1770400803944.jpg | Bin 0 -> 4610061 bytes
output/met_1770403099419.jpg | Bin 0 -> 3861485 bytes
output/met_1770403221810.jpg | Bin 0 -> 3198148 bytes
output/met_1770403333759.jpg | Bin 0 -> 3916648 bytes
output/met_1770403574988.jpg | Bin 0 -> 1346148 bytes
output/met_1770403697204.jpg | Bin 0 -> 520751 bytes
output/met_1770403816330.jpg | Bin 0 -> 2192792 bytes
output/met_1770404061322.jpg | Bin 0 -> 3087498 bytes
output/met_1770404180659.jpg | Bin 0 -> 322917 bytes
output/met_1770404431808.jpg | Bin 0 -> 2703604 bytes
output/met_1770404432466.jpg | Bin 0 -> 3194870 bytes
output/met_1770404543372.jpg | Bin 0 -> 2271853 bytes
output/met_1770404663460.jpg | Bin 0 -> 2325608 bytes
output/met_1770404669818.jpg | Bin 0 -> 1826503 bytes
output/met_1770404911996.jpg | Bin 0 -> 2444477 bytes
output/met_1770405027734.jpg | Bin 0 -> 1614940 bytes
output/met_1770405148169.jpg | Bin 0 -> 2175752 bytes
output/met_1770405153396.jpg | Bin 0 -> 1249876 bytes
output/met_1770405267893.jpg | Bin 0 -> 395925 bytes
output/met_1770405510402.jpg | Bin 0 -> 2772824 bytes
output/met_1770405883635.jpg | Bin 0 -> 3940699 bytes
output/met_1770405998755.jpg | Bin 0 -> 2818864 bytes
output/met_1770405999008.jpg | Bin 0 -> 2025164 bytes
output/met_1770406121993.jpg | Bin 0 -> 2296719 bytes
output/metadata_1769835612798.json | 13 +
output/metadata_1769839200769.json | 13 +
output/metadata_1769930397505.json | 13 +
output/metadata_1769955541913.json | 13 +
output/metadata_1770011755402.json | 13 +
output/metadata_1770020944859.json | 13 +
output/metadata_1770035327832.json | 13 +
output/metadata_1770044154986.json | 13 +
output/metadata_1770089254297.json | 13 +
output/metadata_1770098561130.json | 13 +
output/metadata_1770117556959.json | 13 +
output/metadata_1770120000835.json | 13 +
output/metadata_1770125167080.json | 13 +
output/metadata_1770195505488.json | 13 +
output/metadata_1770210985020.json | 13 +
output/metadata_1770231056684.json | 13 +
output/metadata_1770277360291.json | 13 +
output/metadata_1770288361064.json | 13 +
output/metadata_1770295495989.json | 13 +
output/metadata_1770302991852.json | 13 +
output/metadata_1770323423731.json | 13 +
output/metadata_1770326084840.json | 13 +
output/metadata_1770336000687.json | 13 +
output/metadata_1770339021052.json | 13 +
output/metadata_1770362717875.json | 13 +
output/metadata_1770362957605.json | 13 +
output/metadata_1770363320841.json | 13 +
output/metadata_1770363442632.json | 13 +
output/metadata_1770363687872.json | 13 +
output/metadata_1770365013472.json | 13 +
output/metadata_1770365135248.json | 13 +
output/metadata_1770365256192.json | 13 +
output/metadata_1770379201423.json | 13 +
output/metadata_1770400804167.json | 13 +
output/metadata_1770403453727.json | 13 +
output/metadata_1770403936465.json | 13 +
output/metadata_1770404299077.json | 13 +
output/metadata_1770404783572.json | 13 +
output/metadata_1770405388375.json | 13 +
output/metadata_1770405629028.json | 13 +
output/metadata_1770405750351.json | 13 +
output/thumbnails/thumb_1768128218436.jpg | Bin 0 -> 10828 bytes
output/thumbnails/thumb_1768128310140.jpg | Bin 0 -> 10828 bytes
output/thumbnails/thumb_1768144317138.jpg | Bin 0 -> 68407 bytes
output/thumbnails/thumb_1768144518253.jpg | Bin 0 -> 111964 bytes
output/thumbnails/thumb_1768144732341.jpg | Bin 0 -> 75441 bytes
output/thumbnails/thumb_1768144934368.jpg | Bin 0 -> 68581 bytes
output/thumbnails/thumb_1768145120280.jpg | Bin 0 -> 116668 bytes
output/thumbnails/thumb_1768145314449.jpg | Bin 0 -> 74693 bytes
output/thumbnails/thumb_1768152020572.jpg | Bin 0 -> 67190 bytes
output/thumbnails/thumb_1768152215875.jpg | Bin 0 -> 116770 bytes
output/thumbnails/thumb_1768152401849.jpg | Bin 0 -> 78855 bytes
output/thumbnails/thumb_1768152606874.jpg | Bin 0 -> 54779 bytes
output/thumbnails/thumb_1768152801379.jpg | Bin 0 -> 44556 bytes
output/thumbnails/thumb_1768153001502.jpg | Bin 0 -> 101531 bytes
output/thumbnails/thumb_1768153191039.jpg | Bin 0 -> 47552 bytes
output/thumbnails/thumb_1768153374417.jpg | Bin 0 -> 86388 bytes
output/thumbnails/thumb_1768153551861.jpg | Bin 0 -> 48823 bytes
package-lock.json | 2098 ++++++++++++++++
package.json | 28 +
public/callback.html | 54 +
public/favicon.svg | 4 +
public/index.html | 144 ++
public/privacy.html | 113 +
public/terms.html | 81 +
ralphy.js | 353 +++
scripts/post-3-wave-branded.js | 138 ++
scripts/test-wave-metamorphosis.js | 176 ++
scripts/test-wave-moderate.js | 126 +
scripts/wave-repeat-test.js | 197 ++
setup.sh | 67 +
src/addBranding.js | 241 ++
src/animateKenBurns.js | 130 +
src/animateWithAI.js | 154 ++
src/animateWithGrok.js | 179 ++
src/animateWithGrokFrames.js | 206 ++
src/animateWithHuggingFace.js | 172 ++
src/animateWithLuma.js | 260 ++
src/animateWithMorphing.js | 170 ++
src/animateWithReplicate.js | 133 +
src/animateWithReplicateAlt.js | 142 ++
src/animateWithRunway.js | 113 +
src/artworks.js | 55 +
src/contentGenerator.js | 125 +
src/createAnimation.js | 127 +
src/database.js | 209 ++
src/duplicateTracker.js | 81 +
src/fetchArtwork.js | 148 ++
src/fetchFromAIC.js | 104 +
src/fetchFromMet.js | 140 ++
src/fetchFromMuseums.js | 93 +
src/fetchFromRijksmuseum.js | 101 +
src/healthServer.js | 115 +
src/index.js | 231 ++
src/postToX.js | 222 ++
src/runOvernight.js | 54 +
src/scheduledPoster.js | 76 +
src/sendEmail.js | 55 +
src/slackNotify.js | 126 +
src/utils/retry.js | 72 +
src/validateCredentials.js | 82 +
src/verifyVideo.js | 152 ++
src/webDashboard.js | 303 +++
tasks/prd-museum-art-bot.md | 78 +
test-all-services.js | 146 ++
test-sunflowers.js | 86 +
392 files changed, 13944 insertions(+)
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..e5daec2
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,25 @@
+# X.com (Twitter) API Credentials
+TWITTER_API_KEY=your_api_key
+TWITTER_API_SECRET=your_api_secret
+TWITTER_ACCESS_TOKEN=your_access_token
+TWITTER_ACCESS_SECRET=your_access_secret
+
+# Grok API Credentials
+GROK_API_KEY=your_grok_api_key
+
+# Rijksmuseum API Key (optional - works without but with limits)
+RIJKSMUSEUM_API_KEY=0
+
+# Bot Account
+X_USERNAME=goodquestionai
+
+# Cron Schedule (default: every 6 hours)
+CRON_SCHEDULE=0 */6 * * *
+
+# Email Notification Settings
+EMAIL_HOST=smtp.gmail.com
+EMAIL_PORT=587
+EMAIL_SECURE=false
+EMAIL_USER=your-email@gmail.com
+EMAIL_PASS=your-app-password
+EMAIL_FROM=your-email@gmail.com
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..95ad207
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+node_modules/
+.next/
+dist/
+.cache/
+*.log
+.env
+.env.local
diff --git a/DRAMATIC_VIDEO_MODELS_RESEARCH.json b/DRAMATIC_VIDEO_MODELS_RESEARCH.json
new file mode 100644
index 0000000..c19f724
--- /dev/null
+++ b/DRAMATIC_VIDEO_MODELS_RESEARCH.json
@@ -0,0 +1,608 @@
+{
+ "search_summary": {
+ "platforms_searched": ["replicate.com", "github", "research papers", "model documentation"],
+ "repositories_analyzed": 15,
+ "docs_reviewed": 12,
+ "search_date": "2026-01-11",
+ "issue_identified": "SVD produces subtle motion; Luma single-frame outputs incorrect"
+ },
+
+ "repositories": [
+ {
+ "citation": "[1] Kling AI. 'Kling v2.5 Turbo Pro - Image-to-Video Generation.' Replicate, 2026. https://replicate.com/kwaivgi/kling-v2.5-turbo-pro",
+ "platform": "replicate",
+ "model_id": "kwaivgi/kling-v2.5-turbo-pro",
+ "stats": {
+ "runs": "1.6M",
+ "last_updated": "2026",
+ "cost": "$0.28 per second of video"
+ },
+ "key_features": [
+ "Pro-level text-to-video and image-to-video",
+ "Complex camera moves and high-speed action",
+ "Fluid motion with cinematic depth",
+ "Smooth motion with stable frames",
+ "Remarkable prompt adherence"
+ ],
+ "architecture": "Diffusion-based video generation with advanced physics simulation",
+ "code_quality": {
+ "testing": "production",
+ "documentation": "good",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "kwaivgi/kling-v2.5-turbo-pro",
+ "input": {
+ "prompt": "Vincent van Gogh's Starry Night painting coming alive with dramatic swirling stars and moving clouds",
+ "start_image": "https://url-to-starry-night.jpg",
+ "duration": 10,
+ "aspect_ratio": "16:9"
+ },
+ "output": "MP4 video URL with dramatic swirling motion"
+ },
+ "limitations": [
+ "Premium pricing ($0.28/second = $2.80 for 10s)",
+ "Turbo version may sacrifice some quality for speed"
+ ],
+ "alternatives": ["Kling v2.0 (slower but higher quality)", "MiniMax Hailuo 2.3"]
+ },
+ {
+ "citation": "[2] MiniMax. 'Hailuo 2.3 - High-fidelity Video Generation.' Replicate, 2026. https://replicate.com/minimax/hailuo-2.3",
+ "platform": "replicate",
+ "model_id": "minimax/hailuo-2.3",
+ "stats": {
+ "runs": "38K",
+ "last_updated": "2026"
+ },
+ "key_features": [
+ "Realistic human motion",
+ "Cinematic VFX capabilities",
+ "Strong prompt and style adherence",
+ "Image-to-video support via first_frame_image",
+ "Built-in cinematic camera movements",
+ "Handles complex physical movements (gymnastics, parkour)"
+ ],
+ "architecture": "High-fidelity model optimized for realistic human motion and cinematic effects",
+ "code_quality": {
+ "testing": "production",
+ "documentation": "excellent",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "minimax/hailuo-2.3",
+ "input": {
+ "prompt": "A dramatic camera pan across van Gogh's Starry Night as the stars swirl and dance in the night sky",
+ "first_frame_image": "https://url-to-image.jpg",
+ "duration": 10,
+ "resolution": "768p"
+ },
+ "output": "MP4 video with cinematic camera movements"
+ },
+ "limitations": [
+ "1080p limited to 6 seconds only",
+ "768p required for 10 second videos"
+ ],
+ "alternatives": ["Hailuo 2.3 Fast (lower latency version)", "video-01"]
+ },
+ {
+ "citation": "[3] Google. 'Veo 3.1 - Advanced Video Generation with Audio.' Replicate, 2026. https://replicate.com/google/veo-3.1",
+ "platform": "replicate",
+ "model_id": "google/veo-3.1",
+ "stats": {
+ "runs": "272K",
+ "last_updated": "2026"
+ },
+ "key_features": [
+ "Higher-fidelity video output",
+ "Context-aware audio generation",
+ "Reference image support (1-3 images)",
+ "Last frame support for interpolation",
+ "Realistic physics and motion",
+ "Natural camera movement",
+ "SynthID watermarking"
+ ],
+ "architecture": "Google's state-of-the-art diffusion model with audio synthesis",
+ "code_quality": {
+ "testing": "production",
+ "documentation": "excellent",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "google/veo-3.1",
+ "input": {
+ "image": "https://url-to-portrait.jpg",
+ "prompt": "A person singing opera with dramatic facial expressions",
+ "duration": 8,
+ "resolution": "1080p",
+ "generate_audio": true
+ },
+ "output": "High-fidelity video with synchronized audio"
+ },
+ "limitations": [
+ "More focused on realistic/natural motion than extreme dramatic effects",
+ "Smoother transitions rather than explosive movement",
+ "Best for coherent, realistic sequences"
+ ],
+ "alternatives": ["Veo 3.1 Fast", "Wan 2.5"]
+ },
+ {
+ "citation": "[4] Alibaba. 'Wan 2.5 Image-to-Video with Audio Sync.' Replicate, 2026. https://replicate.com/wan-video/wan-2.5-i2v",
+ "platform": "replicate",
+ "model_id": "wan-video/wan-2.5-i2v",
+ "stats": {
+ "runs": "153.5K",
+ "last_updated": "2026"
+ },
+ "key_features": [
+ "Image to video with background audio",
+ "One-pass A/V sync capability",
+ "Lip-sync features for voiced content",
+ "Audio/voiceover synchronization",
+ "Up to 1080p resolution",
+ "10 second duration support",
+ "Multilingual support (Chinese and others)"
+ ],
+ "architecture": "Alibaba Wan 2.5 with audio synchronization pipeline",
+ "code_quality": {
+ "testing": "production",
+ "documentation": "good",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "wan-video/wan-2.5-i2v",
+ "input": {
+ "image": "https://url-to-face.jpg",
+ "prompt": "Person singing a beautiful song with emotion",
+ "audio": "https://url-to-song.mp3",
+ "duration": 10,
+ "resolution": "1080p"
+ },
+ "output": "Video with synchronized lip movements and audio"
+ },
+ "limitations": [
+ "Audio files limited to 3-30 seconds and ≤15MB",
+ "Motion intensity not explicitly quantified"
+ ],
+ "alternatives": ["Wan 2.5 Fast (optimized for speed)"]
+ },
+ {
+ "citation": "[5] SadTalker Team. 'SadTalker - Audio-Driven Talking Face Animation.' Replicate, CVPR 2023. https://replicate.com/cjwbw/sadtalker",
+ "platform": "replicate",
+ "model_id": "cjwbw/sadtalker",
+ "stats": {
+ "github_stars": "High (OpenTalker/SadTalker)",
+ "last_updated": "2023-2026"
+ },
+ "key_features": [
+ "Single portrait image + audio = talking head video",
+ "Realistic 3D motion coefficients",
+ "Natural head movements and lip-sync",
+ "Eye blinking support",
+ "GFPGAN face enhancement",
+ "Pose style control (0-45)",
+ "Still mode to reduce head motion"
+ ],
+ "architecture": "Learning realistic 3D motion coefficients for stylized audio-driven single image talking face animation",
+ "code_quality": {
+ "testing": "comprehensive",
+ "documentation": "excellent",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "cjwbw/sadtalker",
+ "input": {
+ "source_image": "https://url-to-portrait.jpg",
+ "driven_audio": "https://url-to-song.wav",
+ "use_enhancer": true,
+ "use_eyeblink": true,
+ "pose_style": 20,
+ "expression_scale": 1.2
+ },
+ "output": "Animated talking/singing head video"
+ },
+ "limitations": [
+ "Best for frontal portraits",
+ "May struggle with extreme angles",
+ "Resolution limited to 256 or 512"
+ ],
+ "alternatives": ["AniPortrait", "Video-Retalking", "LivePortrait"]
+ },
+ {
+ "citation": "[6] Fofr. 'LivePortrait - Efficient Portrait Animation.' Replicate, 2026. https://replicate.com/fofr/live-portrait",
+ "platform": "replicate",
+ "model_id": "fofr/live-portrait",
+ "stats": {
+ "last_updated": "2026",
+ "github_source": "KlingTeam/LivePortrait"
+ },
+ "key_features": [
+ "Portrait animation using driving video source",
+ "Stitching and retargeting control",
+ "Eye and lip retargeting with multipliers",
+ "Face scaling and positioning controls",
+ "Frame sampling interval control",
+ "Output up to 2048px resolution"
+ ],
+ "architecture": "Efficient portrait animation system with motion transfer from driving video",
+ "code_quality": {
+ "testing": "good",
+ "documentation": "good",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "fofr/live-portrait",
+ "input": {
+ "face_image": "https://url-to-portrait.jpg",
+ "driving_video": "https://url-to-singing-video.mp4",
+ "live_portrait_eye_retargeting": true,
+ "live_portrait_lip_retargeting": true
+ },
+ "output": "Array of video URLs with transferred motion"
+ },
+ "limitations": [
+ "Cannot be used commercially (InsightFace buffalo_l models)",
+ "MIT license for code/weights but not for commercial use",
+ "Requires driving video source"
+ ],
+ "alternatives": ["SadTalker", "Sync Lipsync-2"]
+ },
+ {
+ "citation": "[7] Sync Labs. 'Lipsync-2 Pro - Studio-Grade Lipsync.' Replicate, 2026. https://replicate.com/sync/lipsync-2-pro",
+ "platform": "replicate",
+ "model_id": "sync/lipsync-2-pro",
+ "stats": {
+ "collection": "lipsync",
+ "quality": "studio-grade"
+ },
+ "key_features": [
+ "Studio-grade quality",
+ "Advanced emotional nuance",
+ "Facial alignment precision",
+ "High fidelity output",
+ "Professional video production ready",
+ "Expressive vocal content support"
+ ],
+ "architecture": "Advanced lipsync system optimized for professional production",
+ "code_quality": {
+ "testing": "production",
+ "documentation": "good",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "sync/lipsync-2-pro",
+ "input": {
+ "video_or_image": "https://url-to-portrait.jpg",
+ "audio": "https://url-to-singing.mp3"
+ },
+ "output": "Professional-grade lipsynced video"
+ },
+ "limitations": [
+ "Premium pricing for pro version",
+ "May be overkill for simple projects"
+ ],
+ "alternatives": ["sync/lipsync-2 (faster, cheaper)", "Kling Lip-sync"]
+ },
+ {
+ "citation": "[8] Luma AI. 'Ray - Fast Text-to-Video and Image-to-Video.' Replicate, 2026. https://replicate.com/luma/ray",
+ "platform": "replicate",
+ "model_id": "luma/ray",
+ "stats": {
+ "versions": "Multiple (Ray, Ray-2, Ray Flash variants)",
+ "resolutions": "540p, 720p"
+ },
+ "key_features": [
+ "Fast, high quality text-to-video",
+ "Image-to-video generation",
+ "5s and 9s video durations",
+ "Multiple resolution options",
+ "Ray-2-720p for highest quality",
+ "Ray-Flash variants for speed"
+ ],
+ "architecture": "Luma Dream Machine Ray 2 generation system",
+ "code_quality": {
+ "testing": "production",
+ "documentation": "good",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "luma/ray-2-720p",
+ "input": {
+ "prompt": "Dramatic swirling stars in night sky",
+ "image": "https://url-to-starry-night.jpg"
+ },
+ "output": "720p video URL (MP4)"
+ },
+ "limitations": [
+ "Short durations (5-9 seconds max)",
+ "Some confusion about output formats in documentation"
+ ],
+ "alternatives": ["luma/ray-flash-2-720p (faster)", "luma/ray-2-540p (cheaper)"]
+ },
+ {
+ "citation": "[9] ByteDance. 'Seedance 1 Pro - Fast Video Generation.' Replicate, 2026. https://replicate.com/bytedance/seedance-1-pro-fast",
+ "platform": "replicate",
+ "model_id": "bytedance/seedance-1-pro-fast",
+ "stats": {
+ "runs": "456.3K",
+ "optimization": "faster and cheaper"
+ },
+ "key_features": [
+ "Optimized for speed",
+ "Cost-effective option",
+ "High run volume indicates popularity"
+ ],
+ "architecture": "ByteDance Seedance generation system",
+ "code_quality": {
+ "testing": "production",
+ "documentation": "good",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "bytedance/seedance-1-pro-fast",
+ "input": {
+ "prompt": "Dynamic scene with movement"
+ },
+ "output": "Video URL"
+ },
+ "limitations": [
+ "Limited documentation available",
+ "Fast version may sacrifice quality"
+ ],
+ "alternatives": ["Seedance 1 Pro (higher quality)"]
+ },
+ {
+ "citation": "[10] Tencent. 'HunyuanVideo - Large-Scale Video Generation.' Replicate, 2026. https://replicate.com/tencent/hunyuan-video",
+ "platform": "replicate",
+ "model_id": "tencent/hunyuan-video",
+ "stats": {
+ "parameters": "13 billion",
+ "performance": "Beats Runway Gen-3 in benchmarks"
+ },
+ "key_features": [
+ "13-billion-parameter model",
+ "Cinematic quality output",
+ "Motion accuracy excellence",
+ "Strong ecosystem support",
+ "Various aspect ratios and resolutions",
+ "Temporal consistency",
+ "Text-to-video (no image-to-video)"
+ ],
+ "architecture": "Comprehensive framework integrating image-video joint model training with efficient infrastructure for large-scale training",
+ "code_quality": {
+ "testing": "comprehensive",
+ "documentation": "excellent",
+ "maintenance": "active"
+ },
+ "usage_example": {
+ "model": "tencent/hunyuan-video",
+ "input": {
+ "prompt": "A drone shot of mountains at sunset with dramatic clouds",
+ "width": 1280,
+ "height": 720,
+ "video_length": 129,
+ "fps": 24,
+ "inference_steps": 50
+ },
+ "output": "High-quality cinematic video"
+ },
+ "limitations": [
+ "Text-to-video only (no image-to-video support)",
+ "Complex motions require prompt engineering",
+ "Requires 60GB+ GPU memory (80GB recommended)",
+ "Longer generation times"
+ ],
+ "alternatives": ["HunyuanVideo-I2V (separate image-to-video version on fal.ai)"]
+ }
+ ],
+
+ "technical_insights": {
+ "common_patterns": [
+ "Most models use diffusion-based architectures",
+ "Audio sync models separate lip-sync from general motion",
+ "Higher resolutions generally require longer processing",
+ "Prompt engineering critical for dramatic effects",
+ "Duration vs quality tradeoffs common",
+ "Most models output MP4 via Replicate delivery URLs"
+ ],
+ "best_practices": [
+ "For face animation/singing: Use SadTalker or LivePortrait with audio",
+ "For dramatic scene motion: Use Kling v2.5 or Hailuo 2.3",
+ "For professional lipsync: Use Sync Lipsync-2 Pro",
+ "For cinematic quality: Use Veo 3.1 or HunyuanVideo",
+ "For fast prototyping: Use Wan 2.5 Fast or Ray Flash variants",
+ "Always specify motion in prompts: 'swirling', 'dramatic', 'dancing', 'moving'",
+ "Use image-to-video mode with start_image/first_frame_image parameters",
+ "Combine models: Generate base video, then add lipsync separately"
+ ],
+ "pitfalls": [
+ "SVD produces subtle motion - avoid for dramatic effects",
+ "Luma model confusion - use luma/ray not specific version IDs",
+ "Commercial use restrictions on LivePortrait",
+ "Resolution limits affect duration (1080p often limited to 6s)",
+ "Audio file size/length limits vary by model",
+ "Some models don't support image-to-video despite being in i2v collection"
+ ],
+ "emerging_trends": [
+ "Integrated audio generation becoming standard (Veo 3.1)",
+ "Multi-reference image support for consistency",
+ "Start/end frame interpolation capabilities",
+ "Higher parameter counts (13B+ for HunyuanVideo)",
+ "Specialized lipsync models outperform general video models",
+ "Turbo/Fast variants provide speed/cost tradeoffs"
+ ]
+ },
+
+ "implementation_recommendations": [
+ {
+ "scenario": "Animate museum portrait to sing/talk",
+ "recommended_solution": "SadTalker (cjwbw/sadtalker) + audio file",
+ "rationale": "Specifically designed for audio-driven talking faces with natural lip-sync, head movement, and eye blinking. Single image input makes it perfect for museum portraits.",
+ "code_example": {
+ "model": "cjwbw/sadtalker",
+ "input": {
+ "source_image": "https://museum-portrait.jpg",
+ "driven_audio": "https://opera-singing.wav",
+ "use_enhancer": true,
+ "use_eyeblink": true,
+ "pose_style": 15,
+ "expression_scale": 1.0
+ }
+ }
+ },
+ {
+ "scenario": "Make Starry Night stars swirl dramatically",
+ "recommended_solution": "Kling v2.5 Turbo Pro (kwaivgi/kling-v2.5-turbo-pro)",
+ "rationale": "Excellent for complex camera moves and dramatic motion. High prompt adherence means it will follow instructions for swirling, dancing stars. Image-to-video support via start_image.",
+ "code_example": {
+ "model": "kwaivgi/kling-v2.5-turbo-pro",
+ "input": {
+ "prompt": "The stars in the night sky swirl and dance dramatically in spiraling patterns, the cypress tree sways, and the village below remains still. Dramatic swirling motion throughout the sky.",
+ "start_image": "https://starry-night.jpg",
+ "duration": 10,
+ "aspect_ratio": "16:9"
+ }
+ }
+ },
+ {
+ "scenario": "Bring historical scene to life with motion",
+ "recommended_solution": "MiniMax Hailuo 2.3 (minimax/hailuo-2.3)",
+ "rationale": "Optimized for realistic human motion and cinematic camera movements. Handles complex physical movements well. Good prompt adherence for historical accuracy.",
+ "code_example": {
+ "model": "minimax/hailuo-2.3",
+ "input": {
+ "prompt": "Renaissance marketplace with people walking, merchants selling goods, camera slowly panning across the busy scene",
+ "first_frame_image": "https://renaissance-painting.jpg",
+ "duration": 10,
+ "resolution": "768p"
+ }
+ }
+ },
+ {
+ "scenario": "Professional production quality lipsync",
+ "recommended_solution": "Sync Lipsync-2 Pro (sync/lipsync-2-pro)",
+ "rationale": "Studio-grade quality with advanced emotional nuance. Best for professional museum installations or high-quality presentations.",
+ "code_example": {
+ "model": "sync/lipsync-2-pro",
+ "input": {
+ "video_or_image": "https://portrait.jpg",
+ "audio": "https://narration.mp3"
+ }
+ }
+ },
+ {
+ "scenario": "Generate video with synchronized audio",
+ "recommended_solution": "Google Veo 3.1 (google/veo-3.1) or Wan 2.5 (wan-video/wan-2.5-i2v)",
+ "rationale": "Both support native audio generation/sync. Veo 3.1 generates context-aware audio automatically. Wan 2.5 accepts custom audio files for precise control.",
+ "code_example": {
+ "model": "google/veo-3.1",
+ "input": {
+ "image": "https://scene.jpg",
+ "prompt": "Orchestra playing with dramatic music",
+ "duration": 8,
+ "resolution": "1080p",
+ "generate_audio": true
+ }
+ }
+ },
+ {
+ "scenario": "Cinematic quality for museum exhibition",
+ "recommended_solution": "Tencent HunyuanVideo (tencent/hunyuan-video) - text only, or Veo 3.1 for image-to-video",
+ "rationale": "HunyuanVideo beats Runway Gen-3 in benchmarks with 13B parameters. Exceptional cinematic quality and motion accuracy. Use Veo 3.1 if you need image-to-video.",
+ "code_example": {
+ "model": "google/veo-3.1",
+ "input": {
+ "image": "https://artwork.jpg",
+ "prompt": "Museum artwork coming to life with cinematic lighting and smooth camera movement",
+ "duration": 8,
+ "resolution": "1080p",
+ "reference_images": ["https://style-ref1.jpg", "https://style-ref2.jpg"]
+ }
+ }
+ }
+ ],
+
+ "community_insights": {
+ "popular_solutions": [
+ "Kling v2.5 Turbo Pro - 1.6M runs, most popular for dramatic effects",
+ "ByteDance Seedance - 456K runs, popular for cost/speed balance",
+ "Google Veo 3.1 - 272K runs, popular for quality and audio",
+ "Wan 2.5 - 153K runs, popular for audio sync"
+ ],
+ "controversial_topics": [
+ "LivePortrait commercial restrictions despite MIT license",
+ "Quality vs speed tradeoffs in Turbo/Fast variants",
+ "Resolution limitations at higher durations",
+ "Lack of true image-to-video on some advertised models"
+ ],
+ "expert_opinions": [
+ "For museum animations, prioritize motion quality over speed",
+ "Audio-driven face animation works better than trying to prompt facial expressions",
+ "Multiple shorter clips often work better than single long generations",
+ "Prompt engineering is critical - always specify motion verbs (swirl, dance, move)",
+ "Consider generating base video then adding audio/lipsync in second pass"
+ ]
+ },
+
+ "model_comparison_matrix": {
+ "face_animation_singing_talking": {
+ "best": "cjwbw/sadtalker",
+ "alternatives": ["fofr/live-portrait", "sync/lipsync-2-pro", "wan-video/wan-2.5-i2v"],
+ "why": "Purpose-built for audio-driven facial animation with natural lip-sync and expressions"
+ },
+ "dramatic_scene_motion": {
+ "best": "kwaivgi/kling-v2.5-turbo-pro",
+ "alternatives": ["minimax/hailuo-2.3", "google/veo-3.1"],
+ "why": "Excellent complex camera moves, high-speed action, fluid motion, strong prompt adherence"
+ },
+ "cinematic_quality": {
+ "best": "google/veo-3.1",
+ "alternatives": ["tencent/hunyuan-video", "minimax/hailuo-2.3"],
+ "why": "State-of-the-art quality, realistic physics, native audio, reference image support"
+ },
+ "cost_effective": {
+ "best": "wan-video/wan-2.5-i2v-fast",
+ "alternatives": ["bytedance/seedance-1-pro-fast", "luma/ray-flash-2-540p"],
+ "why": "Fast variants optimize for speed and cost while maintaining quality"
+ },
+ "professional_lipsync": {
+ "best": "sync/lipsync-2-pro",
+ "alternatives": ["sync/lipsync-2", "kwaivgi/kling-lip-sync"],
+ "why": "Studio-grade quality with emotional nuance and facial precision"
+ }
+ },
+
+ "migration_from_svd": {
+ "problem": "SVD (Stable Video Diffusion) produces very subtle, minimal motion",
+ "solution": "Replace with dramatic motion models",
+ "code_changes": [
+ {
+ "from": "stability-ai/stable-video-diffusion",
+ "to": "kwaivgi/kling-v2.5-turbo-pro",
+ "reason": "Kling produces dramatic, expressive motion vs SVD's subtle drift"
+ },
+ {
+ "parameter_changes": {
+ "add": "prompt parameter with motion descriptors",
+ "example": "prompt: 'dramatic swirling motion, stars dancing across the sky, energetic movement'"
+ }
+ }
+ ],
+ "expected_improvements": [
+ "Visible, dramatic motion instead of subtle drift",
+ "Better prompt control over motion style",
+ "More engaging, museum-worthy animations",
+ "Professional quality output"
+ ]
+ },
+
+ "quick_start_guide": {
+ "step_1": "Identify your use case",
+ "step_2": "Choose model from recommendations above",
+ "step_3": "Prepare inputs (image + prompt or audio)",
+ "step_4": "Call Replicate API with model ID",
+ "step_5": "Retrieve video URL from output",
+ "example_code": {
+ "javascript": "const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN });\n\nconst output = await replicate.run(\n 'kwaivgi/kling-v2.5-turbo-pro',\n {\n input: {\n prompt: 'Dramatic swirling stars in night sky',\n start_image: 'https://starry-night.jpg',\n duration: 10,\n aspect_ratio: '16:9'\n }\n }\n);\n\nconsole.log('Video URL:', output);"
+ }
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9d1f063
--- /dev/null
+++ b/README.md
@@ -0,0 +1,153 @@
+# Museum Art Animation Bot
+
+Automated bot that fetches random public domain artwork from Rijksmuseum, animates it using Grok AI, and posts to X.com.
+
+## Features
+
+- Fetches high-quality public domain artwork from Rijksmuseum
+- Works without API key (limited) or with free Rijksmuseum API key
+- Attempts animation using Grok API (optional)
+- Posts to X.com with artwork details and hashtags
+- Automatic cleanup of old files
+- Configurable posting schedule
+
+## Setup
+
+1. **Clone and install dependencies:**
+```bash
+npm install
+```
+
+2. **Configure API credentials:**
+```bash
+cp .env.example .env
+```
+
+Edit `.env` file with your credentials:
+- **X.com (Twitter) API**: Required for posting
+- **Grok API**: Optional for animation (falls back to static images)
+- **Rijksmuseum API**: Optional (works without but with rate limits)
+
+3. **Run setup script:**
+```bash
+chmod +x setup.sh
+./setup.sh
+```
+
+## Usage
+
+### Run once:
+```bash
+npm run fetch # Just fetch artwork
+npm run post # Post existing artwork
+npm start # Full process
+node src/index.js --once # Single run
+```
+
+### Schedule automatic posting:
+
+**Option 1: Built-in scheduler (recommended for testing)**
+```bash
+node src/index.js
+```
+
+**Option 2: System cron (recommended for production)**
+```bash
+# Add to crontab (runs every 6 hours)
+crontab -e
+0 */6 * * * cd /root/animatemuseumposts && node src/index.js --once >> logs/cron.log 2>&1
+```
+
+**Option 3: Systemd service (most robust)**
+```bash
+sudo cp museum-art-bot.service /etc/systemd/system/
+sudo systemctl daemon-reload
+sudo systemctl enable museum-art-bot
+sudo systemctl start museum-art-bot
+```
+
+## Configuration
+
+### Environment Variables (.env):
+- `TWITTER_API_KEY`: X.com API key
+- `TWITTER_API_SECRET`: X.com API secret
+- `TWITTER_ACCESS_TOKEN`: X.com access token
+- `TWITTER_ACCESS_SECRET`: X.com access secret
+- `GROK_API_KEY`: Grok API key (optional)
+- `RIJKSMUSEUM_API_KEY`: Rijksmuseum API key (optional)
+- `CRON_SCHEDULE`: Custom cron schedule (default: "0 */6 * * *")
+
+### Posting Schedule:
+Default is every 6 hours. Modify in:
+- `.env` file: `CRON_SCHEDULE="0 */6 * * *"`
+- Or in crontab directly
+
+## API Information
+
+### Rijksmuseum API
+- No API key required for basic usage
+- Register for free key at: https://data.rijksmuseum.nl/object-metadata/api/
+- Focuses on public domain artwork from famous artists
+
+### X.com (Twitter) API
+- Required for posting
+- Apply at: https://developer.twitter.com/
+- Needs Elevated access for media upload
+
+### Grok API
+- Optional for AI animation
+- Get access at: https://x.ai/api
+- Falls back to static images if unavailable
+
+## File Structure
+```
+animatemuseumposts/
+├── src/
+│ ├── index.js # Main orchestrator
+│ ├── fetchArtwork.js # Rijksmuseum fetcher
+│ ├── animateWithGrok.js # Animation processor
+│ └── postToX.js # X.com poster
+├── output/ # Temporary artwork files
+├── logs/ # Application logs
+├── .env # API credentials
+└── package.json # Dependencies
+```
+
+## Monitoring
+
+Check logs:
+```bash
+tail -f logs/posts.log # Successful posts
+tail -f logs/errors.log # Errors
+tail -f logs/cron.log # Cron output
+```
+
+Check systemd service:
+```bash
+sudo systemctl status museum-art-bot
+sudo journalctl -u museum-art-bot -f
+```
+
+## Troubleshooting
+
+1. **X.com authentication fails:**
+ - Verify API credentials in .env
+ - Ensure app has write permissions
+ - Check rate limits
+
+2. **No artwork found:**
+ - Rijksmuseum API may be rate limited
+ - Try adding API key for higher limits
+
+3. **Animation fails:**
+ - Grok API key may be invalid
+ - Bot falls back to static images automatically
+
+4. **Cron not running:**
+ - Check cron service: `sudo service cron status`
+ - Verify paths are absolute in crontab
+ - Check logs directory permissions
+
+## License
+
+MIT
\ No newline at end of file
diff --git a/REPLICATE_MODELS_QUICK_REFERENCE.md b/REPLICATE_MODELS_QUICK_REFERENCE.md
new file mode 100644
index 0000000..3287229
--- /dev/null
+++ b/REPLICATE_MODELS_QUICK_REFERENCE.md
@@ -0,0 +1,303 @@
+# Replicate Models for Dramatic Video Animation - Quick Reference
+
+## Problem Identified
+- **SVD (Stable Video Diffusion)**: Produces very subtle, minimal motion - NOT suitable for dramatic effects
+- **Luma models**: Confusion about video output - some versions output single frames instead of video
+
+## Solutions: Dramatic Motion Models
+
+### 1. BEST FOR DRAMATIC SCENE ANIMATION (Starry Night swirling stars, etc.)
+
+**Model ID**: `kwaivgi/kling-v2.5-turbo-pro`
+
+**Why**: Complex camera moves, high-speed action, fluid motion, excellent prompt adherence
+
+**Usage**:
+```javascript
+{
+ model: "kwaivgi/kling-v2.5-turbo-pro",
+ input: {
+ prompt: "The stars in van Gogh's Starry Night swirl dramatically in spiraling patterns, the cypress tree sways, clouds move energetically across the sky",
+ start_image: "https://url-to-image.jpg",
+ duration: 10, // seconds
+ aspect_ratio: "16:9"
+ }
+}
+```
+
+**Output**: MP4 video URL with dramatic motion
+**Cost**: $0.28 per second ($2.80 for 10s video)
+**Runs**: 1.6M (most popular)
+
+---
+
+### 2. BEST FOR FACE ANIMATION - SINGING/TALKING
+
+**Model ID**: `cjwbw/sadtalker`
+
+**Why**: Purpose-built for audio-driven facial animation with realistic lip-sync, head movement, eye blinking
+
+**Usage**:
+```javascript
+{
+ model: "cjwbw/sadtalker",
+ input: {
+ source_image: "https://portrait.jpg",
+ driven_audio: "https://song-or-speech.wav",
+ use_enhancer: true, // GFPGAN face enhancement
+ use_eyeblink: true, // Add eye blinking
+ pose_style: 15, // 0-45, controls head movement
+ expression_scale: 1.0 // Expression intensity
+ }
+}
+```
+
+**Output**: Animated talking/singing face video
+**Best For**: Museum portraits coming to life, historical figures speaking
+
+---
+
+### 3. BEST FOR CINEMATIC QUALITY + AUDIO
+
+**Model ID**: `google/veo-3.1`
+
+**Why**: State-of-the-art quality, realistic physics, generates audio automatically, supports reference images
+
+**Usage**:
+```javascript
+{
+ model: "google/veo-3.1",
+ input: {
+ image: "https://scene.jpg",
+ prompt: "Orchestra playing with dramatic music and movement",
+ duration: 8, // 4, 6, or 8 seconds
+ resolution: "1080p", // 720p or 1080p
+ generate_audio: true, // Auto-generate synchronized audio
+ aspect_ratio: "16:9"
+ }
+}
+```
+
+**Output**: High-fidelity video with synchronized audio
+**Runs**: 272K
+
+---
+
+### 4. BEST FOR DRAMATIC HUMAN MOTION + CINEMATIC CAMERA
+
+**Model ID**: `minimax/hailuo-2.3`
+
+**Why**: Optimized for realistic human motion, cinematic VFX, handles complex physical movements (gymnastics, parkour)
+
+**Usage**:
+```javascript
+{
+ model: "minimax/hailuo-2.3",
+ input: {
+ prompt: "Renaissance marketplace with people walking, merchants selling, camera panning across busy scene",
+ first_frame_image: "https://painting.jpg",
+ duration: 10, // 6 or 10 seconds
+ resolution: "768p" // 768p or 1080p (1080p limited to 6s)
+ }
+}
+```
+
+**Output**: Cinematic video with dramatic camera movements
+**Runs**: 38K
+
+---
+
+### 5. BEST FOR PROFESSIONAL LIPSYNC
+
+**Model ID**: `sync/lipsync-2-pro`
+
+**Why**: Studio-grade quality with advanced emotional nuance, best for professional installations
+
+**Usage**:
+```javascript
+{
+ model: "sync/lipsync-2-pro",
+ input: {
+ video_or_image: "https://portrait.jpg",
+ audio: "https://narration.mp3"
+ }
+}
+```
+
+**Output**: Professional-grade lipsynced video
+**Alternative**: `sync/lipsync-2` (faster, cheaper version)
+
+---
+
+### 6. BEST FOR VIDEO-DRIVEN FACE ANIMATION
+
+**Model ID**: `fofr/live-portrait`
+
+**Why**: Transfer facial expressions/movements from any video to your portrait
+
+**Usage**:
+```javascript
+{
+ model: "fofr/live-portrait",
+ input: {
+ face_image: "https://portrait.jpg",
+ driving_video: "https://singing-performance.mp4", // Source of motion
+ live_portrait_eye_retargeting: true,
+ live_portrait_lip_retargeting: true
+ }
+}
+```
+
+**Output**: Portrait animated with motion from driving video
+**Limitation**: Cannot be used commercially (licensing restriction)
+
+---
+
+### 7. BEST FOR AUDIO SYNC + VIDEO
+
+**Model ID**: `wan-video/wan-2.5-i2v`
+
+**Why**: One-pass audio/video sync, lip-sync features, custom audio integration
+
+**Usage**:
+```javascript
+{
+ model: "wan-video/wan-2.5-i2v",
+ input: {
+ image: "https://face.jpg",
+ prompt: "Person singing with emotion",
+ audio: "https://song.mp3", // 3-30 seconds, ≤15MB
+ duration: 10,
+ resolution: "1080p"
+ }
+}
+```
+
+**Output**: Video with synchronized lip movements and audio
+**Runs**: 153.5K
+
+---
+
+### 8. LUMA MODELS (Fixed)
+
+**Correct Model ID for Video Output**: `luma/ray`
+
+**Other Luma Models**:
+- `luma/ray-2-720p` - 720p video output (5s and 9s)
+- `luma/ray-2-540p` - 540p video output
+- `luma/ray-flash-2-720p` - Faster 720p generation
+- `luma/ray-flash-2-540p` - Faster 540p generation
+
+**Usage**:
+```javascript
+{
+ model: "luma/ray-2-720p",
+ input: {
+ prompt: "Dramatic motion in scene",
+ image: "https://image.jpg" // Optional for image-to-video
+ }
+}
+```
+
+**Output**: MP4 video URL (NOT single frames)
+**Duration**: 5 or 9 seconds
+
+---
+
+## Model Comparison Matrix
+
+| Use Case | Best Model | Runner-up | Why |
+|----------|-----------|-----------|-----|
+| **Dramatic scene motion** | `kwaivgi/kling-v2.5-turbo-pro` | `minimax/hailuo-2.3` | Complex camera moves, fluid motion, strong prompt adherence |
+| **Face singing/talking** | `cjwbw/sadtalker` | `wan-video/wan-2.5-i2v` | Purpose-built for audio-driven faces, natural lip-sync |
+| **Cinematic quality** | `google/veo-3.1` | `tencent/hunyuan-video` | State-of-the-art, realistic physics, native audio |
+| **Professional lipsync** | `sync/lipsync-2-pro` | `sync/lipsync-2` | Studio-grade quality, emotional nuance |
+| **Cost-effective** | `wan-video/wan-2.5-i2v-fast` | `bytedance/seedance-1-pro-fast` | Fast variants, good quality/cost balance |
+
+---
+
+## Key Insights
+
+### What Makes Motion "Dramatic"?
+
+1. **Prompt Engineering**: Use action verbs
+ - Good: "swirling, dancing, spiraling, energetic motion"
+ - Bad: "moving slightly"
+
+2. **Model Selection**: Choose motion-focused models
+ - SVD = subtle drift (avoid)
+ - Kling/Hailuo = dramatic action (use)
+
+3. **Duration**: Longer allows more dramatic development
+ - 5s = limited motion
+ - 10s = full dramatic arc
+
+### Best Practices
+
+1. **For museum portraits**: Use `sadtalker` with audio file
+2. **For paintings coming alive**: Use `kling-v2.5-turbo-pro` with motion prompt
+3. **For professional quality**: Use `veo-3.1` or `lipsync-2-pro`
+4. **For quick tests**: Use `-fast` or `-turbo` variants
+5. **Always specify motion**: Don't rely on model defaults
+
+### Common Pitfalls
+
+- Using SVD for dramatic effects (too subtle)
+- Not specifying motion in prompts
+- Trying to use text-only models for image-to-video
+- Ignoring duration vs resolution tradeoffs
+- Forgetting audio file size/length limits
+
+---
+
+## Migration from SVD
+
+### Old Code (Subtle Motion):
+```javascript
+{
+ model: "stability-ai/stable-video-diffusion",
+ input: {
+ image: "https://starry-night.jpg"
+ }
+}
+// Result: Barely visible motion
+```
+
+### New Code (Dramatic Motion):
+```javascript
+{
+ model: "kwaivgi/kling-v2.5-turbo-pro",
+ input: {
+ prompt: "Stars swirling dramatically in spiraling patterns, cypress tree swaying, clouds moving energetically",
+ start_image: "https://starry-night.jpg",
+ duration: 10,
+ aspect_ratio: "16:9"
+ }
+}
+// Result: Dramatic, visible, engaging motion
+```
+
+---
+
+## Additional Resources
+
+- Full research report: `DRAMATIC_VIDEO_MODELS_RESEARCH.json`
+- Replicate Image-to-Video Collection: https://replicate.com/collections/image-to-video
+- Replicate Lipsync Collection: https://replicate.com/collections/lipsync
+
+---
+
+## Quick Decision Tree
+
+```
+Need to animate a face?
+├─ Yes → Using audio file?
+│ ├─ Yes → sadtalker or wan-2.5-i2v
+│ └─ No → live-portrait (with driving video)
+│
+└─ No → Animating a scene?
+ ├─ Need dramatic motion? → kling-v2.5-turbo-pro
+ ├─ Need cinematic quality? → veo-3.1
+ ├─ Need human motion? → hailuo-2.3
+ └─ Need it fast/cheap? → wan-2.5-i2v-fast
+```
diff --git a/assets/intro.mp4 b/assets/intro.mp4
new file mode 100644
index 0000000..ac8e9c4
Binary files /dev/null and b/assets/intro.mp4 differ
diff --git a/assets/outro.mp4 b/assets/outro.mp4
new file mode 100644
index 0000000..b202408
Binary files /dev/null and b/assets/outro.mp4 differ
diff --git a/data/last-run.json b/data/last-run.json
new file mode 100644
index 0000000..287cde2
--- /dev/null
+++ b/data/last-run.json
@@ -0,0 +1,5 @@
+{
+ "lastPost": "2026-02-06T07:47:36.708Z",
+ "nextPost": "2026-02-06T13:47:36.708Z",
+ "updatedAt": "2026-02-06T07:47:36.708Z"
+}
\ No newline at end of file
diff --git a/data/posted-artworks.json b/data/posted-artworks.json
new file mode 100644
index 0000000..44d57fd
--- /dev/null
+++ b/data/posted-artworks.json
@@ -0,0 +1,3794 @@
+[
+ {
+ "objectNumber": "MET-454011",
+ "title": "Black Stork in a Landscape",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010286418845991253",
+ "postedAt": "2026-01-11T09:43:26.496Z"
+ },
+ {
+ "objectNumber": "MET-437869",
+ "title": "Juan de Pareja (ca. 1608–1670)",
+ "artist": "Velázquez (Diego Rodríguez de Silva y Velázquez)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010286441734295999",
+ "postedAt": "2026-01-11T09:43:31.953Z"
+ },
+ {
+ "objectNumber": "MET-682098",
+ "title": "L'Amour dans le globe; ou, L'Almanach volant, composé de petites pièces fugitives, légères ou galantes, en prose & en vers: avac un précis historique de l'origine du globe aérostatique, des expériences du Champ-de-Mars, de Versailles, de la Muette & des Tuileries, ainsi que vers & chansons y relatifs. Le tout enrichi de figures",
+ "artist": "Chez Desnos",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010286933193441603",
+ "postedAt": "2026-01-11T09:45:29.163Z"
+ },
+ {
+ "objectNumber": "MET-853646",
+ "title": "Garden View, Brooklyn",
+ "artist": "Fidelia Bridges",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010287348689367412",
+ "postedAt": "2026-01-11T09:47:08.162Z"
+ },
+ {
+ "objectNumber": "MET-3152",
+ "title": "Desk and Bookcase",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010288011976622505",
+ "postedAt": "2026-01-11T09:49:46.216Z"
+ },
+ {
+ "objectNumber": "MET-777786",
+ "title": "Teapot",
+ "artist": "John Bartlam",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010288437451198604",
+ "postedAt": "2026-01-11T09:51:27.727Z"
+ },
+ {
+ "objectNumber": "MET-311024",
+ "title": "Nsodie (memorial head)",
+ "artist": "Akan artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010288879186972677",
+ "postedAt": "2026-01-11T09:53:12.965Z"
+ },
+ {
+ "objectNumber": "MET-243823",
+ "title": "Silver-gilt bowl",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010289973618962559",
+ "postedAt": "2026-01-11T09:57:33.881Z"
+ },
+ {
+ "objectNumber": "MET-39496",
+ "title": "Bird-shaped vessel",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010290199608377850",
+ "postedAt": "2026-01-11T09:58:27.737Z"
+ },
+ {
+ "objectNumber": "MET-10522",
+ "title": "Daniel Crommelin Verplanck",
+ "artist": "John Singleton Copley",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010292889985933656",
+ "postedAt": "2026-01-11T10:09:09.224Z"
+ },
+ {
+ "objectNumber": "PD-1768126164948",
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "museum": "Public Domain Collection",
+ "tweetId": "2010293346963701798",
+ "postedAt": "2026-01-11T10:10:58.178Z"
+ },
+ {
+ "objectNumber": "MET-205116",
+ "title": "Virgin and Child",
+ "artist": "Andrea della Robbia",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010293791685554548",
+ "postedAt": "2026-01-11T10:12:44.274Z"
+ },
+ {
+ "objectNumber": "MET-325688",
+ "title": "Statuette of a female",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010296793762816319",
+ "postedAt": "2026-01-11T10:24:39.967Z"
+ },
+ {
+ "objectNumber": "MET-654264",
+ "title": "Presentation Drawing of \"The Statue of Liberty Illuminating the World\"",
+ "artist": "Frédéric-Auguste Bartholdi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010371426654859380",
+ "postedAt": "2026-01-11T15:21:13.901Z"
+ },
+ {
+ "objectNumber": "MET-38006",
+ "title": "Portrait of the Indian Monk Atisha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010423398309286006",
+ "postedAt": "2026-01-11T18:47:44.835Z"
+ },
+ {
+ "objectNumber": "PD-1768176000529",
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2010502561191678382",
+ "postedAt": "2026-01-12T00:02:18.972Z"
+ },
+ {
+ "objectNumber": "MET-700240",
+ "title": "The cabinet-maker and upholsterer's guide, or, Repository of designs for every article of household furniture, in the newest and most approved taste : displaying a great variety of patterns for chairs, stools ... in the plainest and most enriched styles : with a scale to each, and an explanation in letter press : also the plan of a room, shewing the proper distribution of the furniture ... from drawings",
+ "artist": "A. Hepplewhite & Co.",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010593333475934264",
+ "postedAt": "2026-01-12T06:03:00.548Z"
+ },
+ {
+ "objectNumber": "MET-437654",
+ "title": "Circus Sideshow (Parade de cirque)",
+ "artist": "Georges Seurat",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010683760912576567",
+ "postedAt": "2026-01-12T12:02:20.205Z"
+ },
+ {
+ "objectNumber": "PD-1768240800919",
+ "title": "The Garden of Earthly Delights",
+ "artist": "Hieronymus Bosch",
+ "museum": "Public Domain Collection",
+ "tweetId": "2010774344528458239",
+ "postedAt": "2026-01-12T18:02:17.007Z"
+ },
+ {
+ "objectNumber": "MET-500709",
+ "title": "Taūs (mayuri)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010865084415795413",
+ "postedAt": "2026-01-13T00:02:51.009Z"
+ },
+ {
+ "objectNumber": "MET-501534",
+ "title": "Transverse Flute in D-flat",
+ "artist": "Claude Laurent",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010950575219744826",
+ "postedAt": "2026-01-13T05:42:33.688Z"
+ },
+ {
+ "objectNumber": "MET-591826",
+ "title": "Vita del Tintoretto",
+ "artist": "Carlo Ridolfi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010955625115897978",
+ "postedAt": "2026-01-13T06:02:37.677Z"
+ },
+ {
+ "objectNumber": "MET-547951",
+ "title": "Portrait of the Boy Eutyches",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2010984095992447319",
+ "postedAt": "2026-01-13T07:55:46.101Z"
+ },
+ {
+ "objectNumber": "MET-249186",
+ "title": "Ivory decorative plaque",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011597472477692050",
+ "postedAt": "2026-01-15T00:33:06.428Z"
+ },
+ {
+ "objectNumber": "MET-334816",
+ "title": "Music-Making Angels",
+ "artist": "Albrecht Dürer",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011675151998279727",
+ "postedAt": "2026-01-15T05:41:46.708Z"
+ },
+ {
+ "objectNumber": "MET-323163",
+ "title": "Nude female figure",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011679770854244540",
+ "postedAt": "2026-01-15T06:00:08.001Z"
+ },
+ {
+ "objectNumber": "MET-435621",
+ "title": "Joan of Arc",
+ "artist": "Jules Bastien-Lepage",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011680702635401687",
+ "postedAt": "2026-01-15T06:03:49.496Z"
+ },
+ {
+ "objectNumber": "PD-1768462587144",
+ "title": "The Birth of Venus",
+ "artist": "Sandro Botticelli",
+ "museum": "Public Domain Collection",
+ "tweetId": "2011704017362305341",
+ "postedAt": "2026-01-15T07:36:28.358Z"
+ },
+ {
+ "objectNumber": "PD-1768465247282",
+ "title": "The Arnolfini Portrait",
+ "artist": "Jan van Eyck",
+ "museum": "Public Domain Collection",
+ "tweetId": "2011715173472506304",
+ "postedAt": "2026-01-15T08:20:48.150Z"
+ },
+ {
+ "objectNumber": "MET-543903",
+ "title": "Striding Figure",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011724821475049628",
+ "postedAt": "2026-01-15T08:59:08.701Z"
+ },
+ {
+ "objectNumber": "PD-1768471538265",
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "museum": "Public Domain Collection",
+ "tweetId": "2011741562045448428",
+ "postedAt": "2026-01-15T10:05:39.843Z"
+ },
+ {
+ "objectNumber": "MET-909780",
+ "title": "Souvenir",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011746631948152858",
+ "postedAt": "2026-01-15T10:25:48.334Z"
+ },
+ {
+ "objectNumber": "MET-50342",
+ "title": "Dish with Three Jars",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011754247163937160",
+ "postedAt": "2026-01-15T10:56:04.277Z"
+ },
+ {
+ "objectNumber": "MET-194622",
+ "title": "Drop-front secretary (Secrétaire en armoire)",
+ "artist": "Jean Henri Riesener",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011758804963377492",
+ "postedAt": "2026-01-15T11:14:11.002Z"
+ },
+ {
+ "objectNumber": "MET-459246",
+ "title": "Dish: The Destruction of the Hosts of Pharaoh",
+ "artist": "Master IC (probably Jean Court)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011765929068413147",
+ "postedAt": "2026-01-15T11:42:29.304Z"
+ },
+ {
+ "objectNumber": "MET-192729",
+ "title": "Triton",
+ "artist": "Giambologna",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011768956013646198",
+ "postedAt": "2026-01-15T11:54:31.285Z"
+ },
+ {
+ "objectNumber": "MET-550755",
+ "title": "Naophorous Block Statue of a Governor of Sais, Psamtikseneb",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011770352133947764",
+ "postedAt": "2026-01-15T12:00:04.031Z"
+ },
+ {
+ "objectNumber": "MET-458956",
+ "title": "The Adoration of the Magi",
+ "artist": "Bartolo di Fredi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011805986718437579",
+ "postedAt": "2026-01-15T14:21:40.198Z"
+ },
+ {
+ "objectNumber": "MET-744940",
+ "title": "Royal processions, ceremonies and entertainments",
+ "artist": "Court of King Mindon or Thibaw, or associated workshops",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011813595772752358",
+ "postedAt": "2026-01-15T14:51:54.319Z"
+ },
+ {
+ "objectNumber": "PD-1768491972903",
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "museum": "Public Domain Collection",
+ "tweetId": "2011827269875253304",
+ "postedAt": "2026-01-15T15:46:14.054Z"
+ },
+ {
+ "objectNumber": "MET-824357",
+ "title": "L'œuvre complet de Rembrandt : catalogue raisonné de toutes les eaux-fortes du maître et de ses peintures",
+ "artist": "Charles Blanc",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011849593613664498",
+ "postedAt": "2026-01-15T17:14:56.573Z"
+ },
+ {
+ "objectNumber": "MET-459038",
+ "title": "Portrait of a Woman, possibly Ginevra d'Antonio Lupari Gozzadini",
+ "artist": "Maestro delle Storie del Pane",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011851115881762841",
+ "postedAt": "2026-01-15T17:20:59.731Z"
+ },
+ {
+ "objectNumber": "MET-921065",
+ "title": "Portrait of Nicolás Matías Fernández Méndez",
+ "artist": "José Campeche",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011858250212524164",
+ "postedAt": "2026-01-15T17:49:20.751Z"
+ },
+ {
+ "objectNumber": "MET-464125",
+ "title": "Box with Romance Scenes",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011860969903780284",
+ "postedAt": "2026-01-15T18:00:09.043Z"
+ },
+ {
+ "objectNumber": "MET-38267",
+ "title": "Head of a Buddha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011861315740995590",
+ "postedAt": "2026-01-15T18:01:31.971Z"
+ },
+ {
+ "objectNumber": "PD-1768506483544",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2011888132585619773",
+ "postedAt": "2026-01-15T19:48:04.884Z"
+ },
+ {
+ "objectNumber": "MET-322608",
+ "title": "Human-headed winged bull (lamassu)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011914016546046113",
+ "postedAt": "2026-01-15T21:30:56.385Z"
+ },
+ {
+ "objectNumber": "MET-22876",
+ "title": "Shield for the Field or Tournament (Targe)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011951548948262943",
+ "postedAt": "2026-01-16T00:00:04.731Z"
+ },
+ {
+ "objectNumber": "MET-27936",
+ "title": "Helmet",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011953572628295819",
+ "postedAt": "2026-01-16T00:08:07.009Z"
+ },
+ {
+ "objectNumber": "MET-79048",
+ "title": "Suit",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011955638864105495",
+ "postedAt": "2026-01-16T00:16:19.795Z"
+ },
+ {
+ "objectNumber": "MET-449330",
+ "title": "Bowl with Inscription, \"Sovereignty is God's\"",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2011991630937481584",
+ "postedAt": "2026-01-16T02:39:21.111Z"
+ },
+ {
+ "objectNumber": "MET-312677",
+ "title": "Feathered panel",
+ "artist": "Wari artist(s)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012008867908247796",
+ "postedAt": "2026-01-16T03:47:50.636Z"
+ },
+ {
+ "objectNumber": "PD-1768536352019",
+ "title": "The Birth of Venus",
+ "artist": "Sandro Botticelli",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012013414583832837",
+ "postedAt": "2026-01-16T04:05:54.424Z"
+ },
+ {
+ "objectNumber": "MET-256975",
+ "title": "Ganymede jewelry",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012024581939437632",
+ "postedAt": "2026-01-16T04:50:17.057Z"
+ },
+ {
+ "objectNumber": "MET-544692",
+ "title": "Haremhab as a Scribe of the King",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012028636631875860",
+ "postedAt": "2026-01-16T05:06:23.818Z"
+ },
+ {
+ "objectNumber": "MET-544498",
+ "title": "Sphinx of Amenhotep III, possibly from a Model of a Temple",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012042156471665001",
+ "postedAt": "2026-01-16T06:00:07.179Z"
+ },
+ {
+ "objectNumber": "MET-909994",
+ "title": "Locking box in book form",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012049420087685623",
+ "postedAt": "2026-01-16T06:28:58.683Z"
+ },
+ {
+ "objectNumber": "MET-24671",
+ "title": "Armor of Henry II, King of France (reigned 1547–59)",
+ "artist": "Jean Cousin the Elder",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012083909937295638",
+ "postedAt": "2026-01-16T08:46:02.010Z"
+ },
+ {
+ "objectNumber": "PD-1768559807037",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012111786619891955",
+ "postedAt": "2026-01-16T10:36:48.127Z"
+ },
+ {
+ "objectNumber": "MET-22739",
+ "title": "Mask",
+ "artist": "Myōchin Muneakira",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012121462552117412",
+ "postedAt": "2026-01-16T11:15:15.465Z"
+ },
+ {
+ "objectNumber": "MET-209028",
+ "title": "Monteith with the Gibbs and Nelthorpe coats of arms",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012127021032513749",
+ "postedAt": "2026-01-16T11:37:20.995Z"
+ },
+ {
+ "objectNumber": "MET-325562",
+ "title": "Furniture element carved in the round with the head of a roaring lion",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012132737818468626",
+ "postedAt": "2026-01-16T12:00:03.573Z"
+ },
+ {
+ "objectNumber": "MET-22387",
+ "title": "Double-Barreled Wheellock Pistol Made for Emperor Charles V (reigned 1519–56)",
+ "artist": "Peter Peck",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012140691850482142",
+ "postedAt": "2026-01-16T12:31:40.020Z"
+ },
+ {
+ "objectNumber": "MET-545870",
+ "title": "Statuette of Kary carrying a standard of Horus",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012146269842706943",
+ "postedAt": "2026-01-16T12:53:49.881Z"
+ },
+ {
+ "objectNumber": "MET-322485",
+ "title": "Belt adornment with an eagle and its prey",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012147281253061055",
+ "postedAt": "2026-01-16T12:57:50.996Z"
+ },
+ {
+ "objectNumber": "MET-38468",
+ "title": "Patravahaka yaksha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012156412663681147",
+ "postedAt": "2026-01-16T13:34:08.390Z"
+ },
+ {
+ "objectNumber": "MET-199674",
+ "title": "The Holy Family with Saints Anne and Joachim",
+ "artist": "Diego de Pesquera",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012158476831817829",
+ "postedAt": "2026-01-16T13:42:20.271Z"
+ },
+ {
+ "objectNumber": "MET-359320",
+ "title": "Nocturne: Palaces",
+ "artist": "James McNeill Whistler",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012196475271594108",
+ "postedAt": "2026-01-16T16:13:19.822Z"
+ },
+ {
+ "objectNumber": "MET-311294",
+ "title": "Ancestor figure",
+ "artist": "Kambot artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012197491454656610",
+ "postedAt": "2026-01-16T16:17:22.098Z"
+ },
+ {
+ "objectNumber": "PD-1768586400456",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012223330225655848",
+ "postedAt": "2026-01-16T18:00:02.102Z"
+ },
+ {
+ "objectNumber": "MET-65397",
+ "title": "Pensive bodhisattva",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012229451254526251",
+ "postedAt": "2026-01-16T18:24:21.695Z"
+ },
+ {
+ "objectNumber": "MET-312231",
+ "title": "Aduno Koro (Ark of the World) vessel with ancestral figures",
+ "artist": "Dogon blacksmith",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012242129851199502",
+ "postedAt": "2026-01-16T19:14:44.592Z"
+ },
+ {
+ "objectNumber": "MET-544184",
+ "title": "Face of Senwosret III",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012262434396872772",
+ "postedAt": "2026-01-16T20:35:25.719Z"
+ },
+ {
+ "objectNumber": "MET-39021",
+ "title": "The Transcendent Buddha Vairochana (?) Seated in Western Fashion",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012266050713194645",
+ "postedAt": "2026-01-16T20:49:47.619Z"
+ },
+ {
+ "objectNumber": "MET-471884",
+ "title": "Saint James the Greater",
+ "artist": "Gil de Siloe",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012266998089007245",
+ "postedAt": "2026-01-16T20:53:33.681Z"
+ },
+ {
+ "objectNumber": "PD-1768599229155",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012277134895526126",
+ "postedAt": "2026-01-16T21:33:50.281Z"
+ },
+ {
+ "objectNumber": "MET-545281",
+ "title": "Model of a Granary with Scribes",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012310648525316296",
+ "postedAt": "2026-01-16T23:47:00.906Z"
+ },
+ {
+ "objectNumber": "MET-501520",
+ "title": "Alto Recorder in F",
+ "artist": "Johann Benedikt Gahn",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012313683993489803",
+ "postedAt": "2026-01-16T23:59:04.358Z"
+ },
+ {
+ "objectNumber": "MET-37397",
+ "title": "Ganesha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012313983617769640",
+ "postedAt": "2026-01-17T00:00:16.020Z"
+ },
+ {
+ "objectNumber": "MET-755777",
+ "title": "Explication des tableaux de la galerie de Versailles, et de ses deux sallons",
+ "artist": "Pierre Rainssant",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012351764251419034",
+ "postedAt": "2026-01-17T02:30:23.753Z"
+ },
+ {
+ "objectNumber": "MET-282163",
+ "title": "Saint-Maclou, Rouen",
+ "artist": "Edmond Bacot",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012404533402415415",
+ "postedAt": "2026-01-17T06:00:04.876Z"
+ },
+ {
+ "objectNumber": "MET-446563",
+ "title": "\"A Muslim Pilgrim Learns a Lesson in Piety from a Brahman\", Folio from a Khamsa (Quintet) of Amir Khusrau Dihlavi",
+ "artist": "Amir Khusrau Dihlavi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012419647241695247",
+ "postedAt": "2026-01-17T07:00:08.053Z"
+ },
+ {
+ "objectNumber": "MET-37788",
+ "title": "Devananda's Fourteen Auspicious Dreams Foretelling the Birth of Mahavira: Folio from a Kalpasutra Manuscript",
+ "artist": "Master of the Jaunpur Kalpasutra",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012421171468435939",
+ "postedAt": "2026-01-17T07:06:11.500Z"
+ },
+ {
+ "objectNumber": "MET-591863",
+ "title": "Catalogue of the Collection of Jewels and Precious Works of Art, the Property of J. Pierpont Morgan",
+ "artist": "George Charles Williamson",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012440453938135221",
+ "postedAt": "2026-01-17T08:22:48.690Z"
+ },
+ {
+ "objectNumber": "MET-626692",
+ "title": "The Jabach Family",
+ "artist": "Charles Le Brun",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012446528686973437",
+ "postedAt": "2026-01-17T08:46:57.135Z"
+ },
+ {
+ "objectNumber": "PD-1768639974250",
+ "title": "Sunflowers",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012448032105906475",
+ "postedAt": "2026-01-17T08:52:55.209Z"
+ },
+ {
+ "objectNumber": "MET-337487",
+ "title": "Otto, Count of Nassau and his Wife Adelheid van Vianen",
+ "artist": "Bernard van Orley",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012451633146511616",
+ "postedAt": "2026-01-17T09:07:14.190Z"
+ },
+ {
+ "objectNumber": "PD-1768644931788",
+ "title": "The Birth of Venus",
+ "artist": "Sandro Botticelli",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012468827129594298",
+ "postedAt": "2026-01-17T10:15:33.322Z"
+ },
+ {
+ "objectNumber": "MET-467733",
+ "title": "Mirror Case",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012489116038963526",
+ "postedAt": "2026-01-17T11:36:10.727Z"
+ },
+ {
+ "objectNumber": "PD-1768651200449",
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012495122487767451",
+ "postedAt": "2026-01-17T12:00:02.598Z"
+ },
+ {
+ "objectNumber": "PD-1768653517972",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012504839159488917",
+ "postedAt": "2026-01-17T12:38:39.216Z"
+ },
+ {
+ "objectNumber": "MET-247009",
+ "title": "Wall painting from Room H of the Villa of P. Fannius Synistor at Boscoreale",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012510437586416086",
+ "postedAt": "2026-01-17T13:00:54.841Z"
+ },
+ {
+ "objectNumber": "MET-316442",
+ "title": "Lidded saltcellar",
+ "artist": "Temne or Bullom artist(s)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012517026393162089",
+ "postedAt": "2026-01-17T13:27:05.295Z"
+ },
+ {
+ "objectNumber": "MET-547556",
+ "title": "Inlay of the Horus falcon on the hieroglyph for \"gold\"",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012522099362128226",
+ "postedAt": "2026-01-17T13:47:14.353Z"
+ },
+ {
+ "objectNumber": "MET-451995",
+ "title": "Brasero",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012528173507133451",
+ "postedAt": "2026-01-17T14:11:22.521Z"
+ },
+ {
+ "objectNumber": "MET-447000",
+ "title": "Mosque Lamp for the Mausoleum of Amir Aydakin al-'Ala'i al-Bunduqdar",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012538843199852557",
+ "postedAt": "2026-01-17T14:53:46.489Z"
+ },
+ {
+ "objectNumber": "PD-1768662103859",
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012540854666465372",
+ "postedAt": "2026-01-17T15:01:46.082Z"
+ },
+ {
+ "objectNumber": "MET-36457",
+ "title": "Grapevine in the Wind",
+ "artist": "Unidentified artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012585727259791530",
+ "postedAt": "2026-01-17T18:00:04.855Z"
+ },
+ {
+ "objectNumber": "MET-504210",
+ "title": "Cittern",
+ "artist": "Joachim Tielke",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012612861974167741",
+ "postedAt": "2026-01-17T19:47:54.215Z"
+ },
+ {
+ "objectNumber": "MET-437430",
+ "title": "By the Seashore",
+ "artist": "Auguste Renoir",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012615904937292118",
+ "postedAt": "2026-01-17T19:59:59.113Z"
+ },
+ {
+ "objectNumber": "MET-918235",
+ "title": "Carrelages & pavages céramiques : manufacture DeFrance et Comp.ie à Pont Ste. Maxence (Oise)",
+ "artist": "Defrance et Cie.",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012619977631948915",
+ "postedAt": "2026-01-17T20:16:10.822Z"
+ },
+ {
+ "objectNumber": "MET-50897",
+ "title": "Evening bell from mist-shrouded temple (left); Autumn moon over Lake Dongting (right)",
+ "artist": "An Gyeon",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012662083738071241",
+ "postedAt": "2026-01-17T23:03:29.946Z"
+ },
+ {
+ "objectNumber": "MET-271885",
+ "title": "Two Pupils in Greek Dress",
+ "artist": "Thomas Eakins",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012672740621238762",
+ "postedAt": "2026-01-17T23:45:50.181Z"
+ },
+ {
+ "objectNumber": "MET-438815",
+ "title": "Madame Georges Charpentier (Marguerite-Louise Lemonnier, 1848–1904) and Her Children, Georgette-Berthe (1872–1945) and Paul-Emile-Charles (1875–1895)",
+ "artist": "Auguste Renoir",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012676338671558962",
+ "postedAt": "2026-01-18T00:00:08.685Z"
+ },
+ {
+ "objectNumber": "MET-591852",
+ "title": "Incidents of Travel in Central America, Chiapas, and Yucatan",
+ "artist": "John Lloyd Stephens",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012677308184707515",
+ "postedAt": "2026-01-18T00:03:58.943Z"
+ },
+ {
+ "objectNumber": "MET-459062",
+ "title": "Archangel Gabriel; The Virgin Annunciate",
+ "artist": "Gerard David",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012696073916006414",
+ "postedAt": "2026-01-18T01:18:33.434Z"
+ },
+ {
+ "objectNumber": "MET-195473",
+ "title": "Coin cabinet",
+ "artist": "Charles Percier",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012709808273494512",
+ "postedAt": "2026-01-18T02:13:08.146Z"
+ },
+ {
+ "objectNumber": "MET-543901",
+ "title": "Nikare with his Wife and Daughter",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012731100057964690",
+ "postedAt": "2026-01-18T03:37:44.117Z"
+ },
+ {
+ "objectNumber": "MET-459189",
+ "title": "Landscape with Stars",
+ "artist": "Henri-Edmond Cross (Henri-Edmond Delacroix)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012748345127633230",
+ "postedAt": "2026-01-18T04:46:15.838Z"
+ },
+ {
+ "objectNumber": "MET-459245",
+ "title": "Armorial dish: Supper at the House of Simon the Pharisee",
+ "artist": "Maestro Giorgio Andreoli",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012751363466215440",
+ "postedAt": "2026-01-18T04:58:15.542Z"
+ },
+ {
+ "objectNumber": "MET-459052",
+ "title": "A Goldsmith in his Shop",
+ "artist": "Petrus Christus",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012754999814537373",
+ "postedAt": "2026-01-18T05:12:43.061Z"
+ },
+ {
+ "objectNumber": "MET-700091",
+ "title": "The grammar of ornament",
+ "artist": "Owen Jones",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012766958475714943",
+ "postedAt": "2026-01-18T06:00:13.667Z"
+ },
+ {
+ "objectNumber": "MET-910059",
+ "title": "Box in the form of a book",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012778800237457545",
+ "postedAt": "2026-01-18T06:47:17.263Z"
+ },
+ {
+ "objectNumber": "MET-256846",
+ "title": "Terracotta dinos (mixing bowl)",
+ "artist": "Polyteleia Painter",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012784832066256982",
+ "postedAt": "2026-01-18T07:11:15.131Z"
+ },
+ {
+ "objectNumber": "MET-38945",
+ "title": "Hanuman Conversing",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012798492750815452",
+ "postedAt": "2026-01-18T08:05:31.989Z"
+ },
+ {
+ "objectNumber": "MET-255973",
+ "title": "Statue of Dionysos leaning on a female figure (\"Hope Dionysos\")",
+ "artist": "Pacetti, Vincenzo",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012809632146936150",
+ "postedAt": "2026-01-18T08:49:47.861Z"
+ },
+ {
+ "objectNumber": "PD-1768734038220",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012842565431693692",
+ "postedAt": "2026-01-18T11:00:39.334Z"
+ },
+ {
+ "objectNumber": "MET-503678",
+ "title": "Square Piano",
+ "artist": "Robert Nunns",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012848652335661320",
+ "postedAt": "2026-01-18T11:24:50.909Z"
+ },
+ {
+ "objectNumber": "MET-889442",
+ "title": "Jewel casket",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012853247715991736",
+ "postedAt": "2026-01-18T11:43:06.851Z"
+ },
+ {
+ "objectNumber": "MET-323178",
+ "title": "Relief: two servants bearing food and drink",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012856265106747458",
+ "postedAt": "2026-01-18T11:55:06.098Z"
+ },
+ {
+ "objectNumber": "MET-544864",
+ "title": "Statuette of a hippo goddess, probably Taweret",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012857515168665715",
+ "postedAt": "2026-01-18T12:00:03.801Z"
+ },
+ {
+ "objectNumber": "MET-202192",
+ "title": "Potpourri vase (pot-pourri gondole)",
+ "artist": "Sèvres Manufactory",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012861334996795640",
+ "postedAt": "2026-01-18T12:15:14.850Z"
+ },
+ {
+ "objectNumber": "MET-504256",
+ "title": "Transverse Flute",
+ "artist": "Garion",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012878574756491690",
+ "postedAt": "2026-01-18T13:23:45.011Z"
+ },
+ {
+ "objectNumber": "MET-444408",
+ "title": "Astrolabe of ‘Umar ibn Yusuf ibn ‘Umar ibn ‘Ali ibn Rasul al-Muzaffari",
+ "artist": "‘Umar ibn Yusuf ibn ‘Umar ibn ‘Ali ibn Rasul al-Muzaffari",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012892283214147593",
+ "postedAt": "2026-01-18T14:18:13.405Z"
+ },
+ {
+ "objectNumber": "MET-459007",
+ "title": "The Nativity",
+ "artist": "Lorenzo Monaco (Piero di Giovanni)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012915117269090400",
+ "postedAt": "2026-01-18T15:48:57.561Z"
+ },
+ {
+ "objectNumber": "MET-22860",
+ "title": "Sallet in the Shape of a Lion's Head",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012919196443889853",
+ "postedAt": "2026-01-18T16:05:09.801Z"
+ },
+ {
+ "objectNumber": "MET-313654",
+ "title": "Male figure (tiki) representing the deity Rongo",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012920699661480135",
+ "postedAt": "2026-01-18T16:11:08.171Z"
+ },
+ {
+ "objectNumber": "MET-680832",
+ "title": "Anthologie françoise, ou, Chansons choisies, depuis le 13a siècle jusqu'à présent",
+ "artist": "Jean Monnet",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012927798910820551",
+ "postedAt": "2026-01-18T16:39:21.046Z"
+ },
+ {
+ "objectNumber": "PD-1768755808618",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2012933880928760023",
+ "postedAt": "2026-01-18T17:03:30.687Z"
+ },
+ {
+ "objectNumber": "MET-912357",
+ "title": "The art of the Saracens in Egypt",
+ "artist": "Stanley Lane-Poole",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012944555361075538",
+ "postedAt": "2026-01-18T17:45:56.054Z"
+ },
+ {
+ "objectNumber": "MET-591832",
+ "title": "Academie de l'espee; ou se domonstrent par reigles mathématiques sur le fondement d'un cercle mystérieux, la théorie et pratique des vrais . . .",
+ "artist": "Girard Thibault",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012948122356380123",
+ "postedAt": "2026-01-18T18:00:06.165Z"
+ },
+ {
+ "objectNumber": "MET-485518",
+ "title": "Antigraceful",
+ "artist": "Umberto Boccioni",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012983586916577575",
+ "postedAt": "2026-01-18T20:21:01.992Z"
+ },
+ {
+ "objectNumber": "MET-705290",
+ "title": "Bo gu tu",
+ "artist": "Fu Wang",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2012987141253472563",
+ "postedAt": "2026-01-18T20:35:09.172Z"
+ },
+ {
+ "objectNumber": "MET-437487",
+ "title": "Francesco d'Este (born ca. 1429, died after 1486)",
+ "artist": "Rogier van der Weyden",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013006908026720438",
+ "postedAt": "2026-01-18T21:53:41.521Z"
+ },
+ {
+ "objectNumber": "MET-699505",
+ "title": "Dialogos de la pintvra : sv defensa, origen, essecia, definicion, modos y diferencias",
+ "artist": "Vicente Carducho",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013018076615414227",
+ "postedAt": "2026-01-18T22:38:04.897Z"
+ },
+ {
+ "objectNumber": "MET-37557",
+ "title": "Bottle with Birds and Peony Scroll",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013032307280535726",
+ "postedAt": "2026-01-18T23:34:37.587Z"
+ },
+ {
+ "objectNumber": "MET-466296",
+ "title": "Tip of a Pointer",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013035827606225377",
+ "postedAt": "2026-01-18T23:48:37.007Z"
+ },
+ {
+ "objectNumber": "MET-436121",
+ "title": "A Woman Seated beside a Vase of Flowers (Madame Paul Valpinçon?)",
+ "artist": "Edgar Degas",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013038725673144401",
+ "postedAt": "2026-01-19T00:00:08.553Z"
+ },
+ {
+ "objectNumber": "MET-896001",
+ "title": "Willow trees at a brook",
+ "artist": "Emilie Mediz-Pelikan",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013052153653256310",
+ "postedAt": "2026-01-19T00:53:29.825Z"
+ },
+ {
+ "objectNumber": "PD-1768785434034",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2013058164602667019",
+ "postedAt": "2026-01-19T01:17:22.182Z"
+ },
+ {
+ "objectNumber": "MET-459201",
+ "title": "Watch",
+ "artist": "Jacques Goullons",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013072376481603888",
+ "postedAt": "2026-01-19T02:13:50.532Z"
+ },
+ {
+ "objectNumber": "MET-39887",
+ "title": "Brahma with Attendants and Musicians",
+ "artist": "Unidentified artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013074383204389142",
+ "postedAt": "2026-01-19T02:21:49.618Z"
+ },
+ {
+ "objectNumber": "MET-38926",
+ "title": "Reliquary in the Shape of a Stupa",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013121594088992823",
+ "postedAt": "2026-01-19T05:29:25.257Z"
+ },
+ {
+ "objectNumber": "MET-201895",
+ "title": "Madame de Wailly, née Adélaïde-Flore Belleville (1765–1838)",
+ "artist": "Augustin Pajou",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013129402117161077",
+ "postedAt": "2026-01-19T06:00:26.845Z"
+ },
+ {
+ "objectNumber": "MET-466096",
+ "title": "Opus Anglicanum (Chasuble)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013143860919009756",
+ "postedAt": "2026-01-19T06:57:54.202Z"
+ },
+ {
+ "objectNumber": "MET-395485",
+ "title": "Jupiter and Juno: Study for the \"Furti di Giove\" Tapestries",
+ "artist": "Perino del Vaga (Pietro Buonaccorsi)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013151962267619637",
+ "postedAt": "2026-01-19T07:30:05.887Z"
+ },
+ {
+ "objectNumber": "MET-458961",
+ "title": "The Coronation of the Virgin",
+ "artist": "Niccolò di Buonaccorso",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013168170614522042",
+ "postedAt": "2026-01-19T08:34:29.504Z"
+ },
+ {
+ "objectNumber": "MET-310870",
+ "title": "Eyema byeri (reliquary guardian figure)",
+ "artist": "Okak-Fang artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013182374272790603",
+ "postedAt": "2026-01-19T09:30:56.493Z"
+ },
+ {
+ "objectNumber": "MET-448183",
+ "title": "\"Krishna Holds Up Mount Govardhan to Shelter the Villagers of Braj\", Folio from a Harivamsa (The Legend of Hari (Krishna))",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013196060248911941",
+ "postedAt": "2026-01-19T10:25:19.650Z"
+ },
+ {
+ "objectNumber": "MET-469856",
+ "title": "Saint Nicholas Accuses the Consul from Scenes from the Life of Saint Nicholas",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013219899242668455",
+ "postedAt": "2026-01-19T12:00:02.962Z"
+ },
+ {
+ "objectNumber": "MET-254649",
+ "title": "Terracotta column-krater (bowl for mixing wine and water)",
+ "artist": "Group of Boston 00.348",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013219902442840551",
+ "postedAt": "2026-01-19T12:00:04.559Z"
+ },
+ {
+ "objectNumber": "MET-20415",
+ "title": "Architectural woodwork and paneling",
+ "artist": "George A. Schastey & Co.",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013220918865330616",
+ "postedAt": "2026-01-19T12:04:06.626Z"
+ },
+ {
+ "objectNumber": "MET-892561",
+ "title": "The Harper establishment; or, How the story books are made",
+ "artist": "Jacob Abbott",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013227519835856991",
+ "postedAt": "2026-01-19T12:30:19.864Z"
+ },
+ {
+ "objectNumber": "MET-504019",
+ "title": "Transverse Flute in D-flat",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013232073549689051",
+ "postedAt": "2026-01-19T12:48:25.724Z"
+ },
+ {
+ "objectNumber": "MET-207667",
+ "title": "Small desk with folding top (bureau brisé)",
+ "artist": "Alexandre-Jean Oppenordt",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013272626526060898",
+ "postedAt": "2026-01-19T15:29:34.590Z"
+ },
+ {
+ "objectNumber": "MET-201862",
+ "title": "Paris",
+ "artist": "Antico (Pier Jacopo Alari Bonacolsi)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013283777615892856",
+ "postedAt": "2026-01-19T16:13:52.864Z"
+ },
+ {
+ "objectNumber": "MET-470309",
+ "title": "The Hours of Jeanne d'Evreux, Queen of France",
+ "artist": "Jean Pucelle",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013310557701710022",
+ "postedAt": "2026-01-19T18:00:18.209Z"
+ },
+ {
+ "objectNumber": "PD-1768846361853",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2013313687004553320",
+ "postedAt": "2026-01-19T18:12:43.553Z"
+ },
+ {
+ "objectNumber": "MET-506724",
+ "title": "Clarinet in D",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013322817195122708",
+ "postedAt": "2026-01-19T18:49:00.625Z"
+ },
+ {
+ "objectNumber": "MET-9724",
+ "title": "Wardrobe",
+ "artist": "Herter Brothers",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013330942891340065",
+ "postedAt": "2026-01-19T19:21:17.898Z"
+ },
+ {
+ "objectNumber": "PD-1768854703132",
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "museum": "Public Domain Collection",
+ "tweetId": "2013348672537534930",
+ "postedAt": "2026-01-19T20:31:44.721Z"
+ },
+ {
+ "objectNumber": "MET-4285",
+ "title": "High chest of drawers",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013354781721469322",
+ "postedAt": "2026-01-19T20:56:01.513Z"
+ },
+ {
+ "objectNumber": "MET-446297",
+ "title": "Kitab suwar al-kawakib al-thabita (Book of the Images of the Fixed Stars) of al-Sufi",
+ "artist": "`Abd al-Rahman al-Sufi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013363924574298373",
+ "postedAt": "2026-01-19T21:32:21.577Z"
+ },
+ {
+ "objectNumber": "MET-20615",
+ "title": "Sampler",
+ "artist": "Julia Ann Fitch",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013375064587043195",
+ "postedAt": "2026-01-19T22:16:37.853Z"
+ },
+ {
+ "objectNumber": "MET-37800",
+ "title": "Sadaksari-Lokeshvara Surrounded by Manifestations and Monks",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013393840170799353",
+ "postedAt": "2026-01-19T23:31:14.246Z"
+ },
+ {
+ "objectNumber": "MET-910730",
+ "title": "Ice skaters on the Amstel in Amsterdam",
+ "artist": "Hans Bol",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013401104227127680",
+ "postedAt": "2026-01-20T00:00:06.138Z"
+ },
+ {
+ "objectNumber": "MET-24960",
+ "title": "Colt Third Model Dragoon Percussion Revolver, serial no. 12406",
+ "artist": "Samuel Colt",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013413104735195501",
+ "postedAt": "2026-01-20T00:47:46.806Z"
+ },
+ {
+ "objectNumber": "MET-915638",
+ "title": "Catalogue d'une riche collection de tableaux des maîtres les plus célebres des trois écoles : qui composent le cabinet de feu son Altesse Sérenissime Monseigneur le Prince de Conti, prince du sang, & grand prieur de France",
+ "artist": "Pierre Rémy",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013422234879078711",
+ "postedAt": "2026-01-20T01:24:03.357Z"
+ },
+ {
+ "objectNumber": "MET-324291",
+ "title": "Vessel terminating in the forepart of a fantastic leonine creature",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013438456685568086",
+ "postedAt": "2026-01-20T02:28:31.566Z"
+ },
+ {
+ "objectNumber": "PD-1768877676439",
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2013445032288887225",
+ "postedAt": "2026-01-20T02:54:38.816Z"
+ },
+ {
+ "objectNumber": "MET-856282",
+ "title": "Table screen with landscape",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013452677221630375",
+ "postedAt": "2026-01-20T03:25:02.149Z"
+ },
+ {
+ "objectNumber": "MET-22405",
+ "title": "Rowel Spur",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013488673350725729",
+ "postedAt": "2026-01-20T05:48:03.529Z"
+ },
+ {
+ "objectNumber": "MET-253056",
+ "title": "Sheet-gold decoration for a sword scabbard",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013491718549582214",
+ "postedAt": "2026-01-20T06:00:09.927Z"
+ },
+ {
+ "objectNumber": "MET-755774",
+ "title": "Nouvelle description des chateaux et parcs de Versailles et de Marly : contenant une explication historique de toutes les peintures, tableaux, statues, vases & ornemens qui s'y voient : leurs dimensions : & les noms des peintres, des sculpteurs & des graveurs qui les ont faits",
+ "artist": "Jean-Aimar Piganiol de La Force",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013512068591141194",
+ "postedAt": "2026-01-20T07:21:01.625Z"
+ },
+ {
+ "objectNumber": "MET-820668",
+ "title": "Metropolitan Museum of Art Library accession books, 1881-1969",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013518669569995176",
+ "postedAt": "2026-01-20T07:47:15.766Z"
+ },
+ {
+ "objectNumber": "MET-79091",
+ "title": "Cape",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013530270046048643",
+ "postedAt": "2026-01-20T08:33:21.015Z"
+ },
+ {
+ "objectNumber": "PD-1768900769625",
+ "title": "Sunflowers",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2013541890852098096",
+ "postedAt": "2026-01-20T09:19:31.532Z"
+ },
+ {
+ "objectNumber": "MET-337057",
+ "title": "Bacchanal with a Wine Vat",
+ "artist": "Andrea Mantegna",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013543964603793791",
+ "postedAt": "2026-01-20T09:27:46.906Z"
+ },
+ {
+ "objectNumber": "MET-347780",
+ "title": "Scrapbook of Drawings and Prints",
+ "artist": "Gideon Saint",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013562690808225893",
+ "postedAt": "2026-01-20T10:42:11.195Z"
+ },
+ {
+ "objectNumber": "PD-1768906210335",
+ "title": "The Birth of Venus",
+ "artist": "Sandro Botticelli",
+ "museum": "Public Domain Collection",
+ "tweetId": "2013564710692762058",
+ "postedAt": "2026-01-20T10:50:12.275Z"
+ },
+ {
+ "objectNumber": "MET-456074",
+ "title": "Bifolium from the \"Nurse's Qur'an\" (Mushaf al-Hadina)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013579536093585594",
+ "postedAt": "2026-01-20T11:49:07.936Z"
+ },
+ {
+ "objectNumber": "MET-472181",
+ "title": "Saint-Guilhem Cloister",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013582296117584093",
+ "postedAt": "2026-01-20T12:00:05.579Z"
+ },
+ {
+ "objectNumber": "MET-855856",
+ "title": "Moyō hinagata miyako no nishiki / [henshū Yamanaka Kichirobē]",
+ "artist": "Kichirobe Yamanaka",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013583481629208829",
+ "postedAt": "2026-01-20T12:04:48.303Z"
+ },
+ {
+ "objectNumber": "MET-72714",
+ "title": "Standing Four-Armed Shiva",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013594129977250086",
+ "postedAt": "2026-01-20T12:47:06.669Z"
+ },
+ {
+ "objectNumber": "MET-317793",
+ "title": "Bottle in the shape of a feline",
+ "artist": "Wari artist(s)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013604280779882772",
+ "postedAt": "2026-01-20T13:27:27.228Z"
+ },
+ {
+ "objectNumber": "MET-45428",
+ "title": "Illustrated Legends of the Kitano Tenjin Shrine (Kitano Tenjin engi emaki)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013615460034793544",
+ "postedAt": "2026-01-20T14:11:52.285Z"
+ },
+ {
+ "objectNumber": "MET-438417",
+ "title": "Two Men Contemplating the Moon",
+ "artist": "Caspar David Friedrich",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013620523809313035",
+ "postedAt": "2026-01-20T14:32:00.183Z"
+ },
+ {
+ "objectNumber": "MET-459202",
+ "title": "Aquamanile in the Form of Aristotle and Phyllis",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013625085949804831",
+ "postedAt": "2026-01-20T14:50:07.451Z"
+ },
+ {
+ "objectNumber": "MET-679629",
+ "title": "A. Persii Satyrarum liber I.D. Iunii Iuuenalis Satyrarum lib. V. Supliciae Satyra I : cum veteribus commentarijs nunc primum editis ex bibliotheca P. Pithoei I C. cuius etiam notae quaedam adiectae sunt",
+ "artist": "Persius",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013639293952983330",
+ "postedAt": "2026-01-20T15:46:34.625Z"
+ },
+ {
+ "objectNumber": "MET-503546",
+ "title": "Cornet à Pistons in B-flat",
+ "artist": "Courtois frères",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013648927967584768",
+ "postedAt": "2026-01-20T16:24:51.720Z"
+ },
+ {
+ "objectNumber": "MET-717562",
+ "title": "Women's Acoma Cape or Dress",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013673003238990330",
+ "postedAt": "2026-01-20T18:00:31.000Z"
+ },
+ {
+ "objectNumber": "MET-502064",
+ "title": "Sesando",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013679370599350760",
+ "postedAt": "2026-01-20T18:25:49.602Z"
+ },
+ {
+ "objectNumber": "MET-451095",
+ "title": "Fragment of a Kaftan Back",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013689516729786693",
+ "postedAt": "2026-01-20T19:06:08.910Z"
+ },
+ {
+ "objectNumber": "MET-436964",
+ "title": "Young Lady in 1866",
+ "artist": "Edouard Manet",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013695102435336501",
+ "postedAt": "2026-01-20T19:28:20.702Z"
+ },
+ {
+ "objectNumber": "MET-81136",
+ "title": "Waistcoat",
+ "artist": "Anna Maria Garthwaite",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013705311413002383",
+ "postedAt": "2026-01-20T20:08:54.945Z"
+ },
+ {
+ "objectNumber": "MET-707455",
+ "title": "Recueil de planches, sur les sciences, les arts libéraux, et les arts méchaniques : avec leur explication",
+ "artist": "Denis Diderot",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013712373526470964",
+ "postedAt": "2026-01-20T20:36:58.256Z"
+ },
+ {
+ "objectNumber": "MET-38341",
+ "title": "Durga as Slayer of the Buffalo Demon Mahishasura",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013721995285012843",
+ "postedAt": "2026-01-20T21:15:12.030Z"
+ },
+ {
+ "objectNumber": "MET-845712",
+ "title": "Publius Terentius Afer",
+ "artist": "Terence (Publius Terentius Afer)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013722484592284106",
+ "postedAt": "2026-01-20T21:17:08.470Z"
+ },
+ {
+ "objectNumber": "MET-337062",
+ "title": "Self-Portrait, from \"The Iconography\"",
+ "artist": "Anthony van Dyck",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013763478788677829",
+ "postedAt": "2026-01-21T00:00:02.497Z"
+ },
+ {
+ "objectNumber": "MET-248268",
+ "title": "Marble head of a goddess",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013769124636217404",
+ "postedAt": "2026-01-21T00:22:28.629Z"
+ },
+ {
+ "objectNumber": "PD-1768957124285",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2013778256688787910",
+ "postedAt": "2026-01-21T00:58:45.653Z"
+ },
+ {
+ "objectNumber": "MET-38952",
+ "title": "Bodhisattva Avalokiteshvara",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013795515930972594",
+ "postedAt": "2026-01-21T02:07:20.975Z"
+ },
+ {
+ "objectNumber": "MET-701639",
+ "title": "Figure of a Lion",
+ "artist": "John Bell",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013905575722287500",
+ "postedAt": "2026-01-21T09:24:41.096Z"
+ },
+ {
+ "objectNumber": "MET-466070",
+ "title": "Manuscript Leaf with the Crucifixion, from a Missal",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013912674959266151",
+ "postedAt": "2026-01-21T09:52:53.788Z"
+ },
+ {
+ "objectNumber": "MET-336228",
+ "title": "Melencolia I",
+ "artist": "Albrecht Dürer",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013920799426769113",
+ "postedAt": "2026-01-21T10:25:10.712Z"
+ },
+ {
+ "objectNumber": "MET-459208",
+ "title": "Adoration of the Shepherds",
+ "artist": "Francesco di Marco Marmitta da Parma",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013921820840513629",
+ "postedAt": "2026-01-21T10:29:14.115Z"
+ },
+ {
+ "objectNumber": "MET-503951",
+ "title": "Cornetto in A",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013930430186320177",
+ "postedAt": "2026-01-21T11:03:26.593Z"
+ },
+ {
+ "objectNumber": "PD-1768994372305",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2013934487554703412",
+ "postedAt": "2026-01-21T11:19:33.812Z"
+ },
+ {
+ "objectNumber": "MET-687670",
+ "title": "Hiawatha",
+ "artist": "Edmonia Lewis",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2013944679616926070",
+ "postedAt": "2026-01-21T12:00:03.979Z"
+ },
+ {
+ "objectNumber": "PD-1769005979928",
+ "title": "The Garden of Earthly Delights",
+ "artist": "Hieronymus Bosch",
+ "museum": "Public Domain Collection",
+ "tweetId": "2013983173399765365",
+ "postedAt": "2026-01-21T14:33:01.505Z"
+ },
+ {
+ "objectNumber": "MET-548584",
+ "title": "Cosmetic Spoon in the Shape of Swimming Woman Holding a Dish",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014014128139219437",
+ "postedAt": "2026-01-21T16:36:01.798Z"
+ },
+ {
+ "objectNumber": "MET-468346",
+ "title": "The Antioch \"Chalice\"",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014015153776554172",
+ "postedAt": "2026-01-21T16:40:06.746Z"
+ },
+ {
+ "objectNumber": "MET-39186",
+ "title": "Standing Buddha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014035297336697203",
+ "postedAt": "2026-01-21T18:00:09.291Z"
+ },
+ {
+ "objectNumber": "MET-316393",
+ "title": "Plaque with Warrior and Attendants",
+ "artist": "Ìgùn Ẹ́rọ̀nwwọ̀n (brass-casting guild) artists",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014049628195529129",
+ "postedAt": "2026-01-21T18:57:05.781Z"
+ },
+ {
+ "objectNumber": "MET-453183",
+ "title": "The House of Bijapur",
+ "artist": "Kamal Muhammad",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014051137218027936",
+ "postedAt": "2026-01-21T19:03:05.993Z"
+ },
+ {
+ "objectNumber": "MET-254825",
+ "title": "Bronze statuette of a rider wearing an elephant skin",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014052669187559437",
+ "postedAt": "2026-01-21T19:09:11.269Z"
+ },
+ {
+ "objectNumber": "MET-446990",
+ "title": "Morgan Casket",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014108952351613123",
+ "postedAt": "2026-01-21T22:52:49.847Z"
+ },
+ {
+ "objectNumber": "PD-1769037055956",
+ "title": "The Birth of Venus",
+ "artist": "Sandro Botticelli",
+ "museum": "Public Domain Collection",
+ "tweetId": "2014113515636928930",
+ "postedAt": "2026-01-21T23:10:57.486Z"
+ },
+ {
+ "objectNumber": "MET-468716",
+ "title": "Marble Portrait Bust of a Woman with a Scroll",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014124173841543604",
+ "postedAt": "2026-01-21T23:53:18.858Z"
+ },
+ {
+ "objectNumber": "MET-37966",
+ "title": "Buddha Amoghasiddhi with Eight Bodhisattvas",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014280396163473837",
+ "postedAt": "2026-01-22T10:14:05.552Z"
+ },
+ {
+ "objectNumber": "MET-591824",
+ "title": "\"Alberti Dvreri pictoris et architecti praestantissimi De vrbibvs...\"",
+ "artist": "Albrecht Dürer",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014293584598696128",
+ "postedAt": "2026-01-22T11:06:29.444Z"
+ },
+ {
+ "objectNumber": "MET-40118",
+ "title": "Weapon Handle with Ganesha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014305251365671378",
+ "postedAt": "2026-01-22T11:52:50.959Z"
+ },
+ {
+ "objectNumber": "MET-53176",
+ "title": "Fudō Myōō, the Immovable Wisdom King (Achala Vidyaraja)",
+ "artist": "Kaikei",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014307067696488454",
+ "postedAt": "2026-01-22T12:00:04.555Z"
+ },
+ {
+ "objectNumber": "MET-37799",
+ "title": "Thousand-Armed Chenresi, a Cosmic Form of the Bodhisattva Avalokiteshvara",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014317915462660361",
+ "postedAt": "2026-01-22T12:43:10.913Z"
+ },
+ {
+ "objectNumber": "MET-459206",
+ "title": "Tondo mirror frame",
+ "artist": "Italian, Siena",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014318928949092404",
+ "postedAt": "2026-01-22T12:47:12.134Z"
+ },
+ {
+ "objectNumber": "MET-440393",
+ "title": "The Baptism of Christ",
+ "artist": "Jacopo Bassano (Jacopo da Ponte)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014319938589344139",
+ "postedAt": "2026-01-22T12:51:12.869Z"
+ },
+ {
+ "objectNumber": "MET-42641",
+ "title": "Standing Four-Armed Avalokiteshvara, the Bodhisattva of Infinite Compassion",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014330099420799311",
+ "postedAt": "2026-01-22T13:31:35.254Z"
+ },
+ {
+ "objectNumber": "MET-453351",
+ "title": "\"The Angel Gabriel meets 'Amr ibn Zaid (the Shepherd)\", Folio from a Siyer-i Nebi (the Life of the Prophet)",
+ "artist": "Mustafa ibn Vali",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014397667296870770",
+ "postedAt": "2026-01-22T18:00:04.967Z"
+ },
+ {
+ "objectNumber": "MET-325511",
+ "title": "Cup with a frieze of gazelles",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014401582981644802",
+ "postedAt": "2026-01-22T18:15:38.842Z"
+ },
+ {
+ "objectNumber": "MET-854907",
+ "title": "Portrait of Shakyashribhadra with His Life Episodes and Lineage",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014442143641952564",
+ "postedAt": "2026-01-22T20:56:48.344Z"
+ },
+ {
+ "objectNumber": "MET-337844",
+ "title": "Portrait of Nicolas Trigault in Chinese Costume",
+ "artist": "Peter Paul Rubens",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014442653950345473",
+ "postedAt": "2026-01-22T20:58:50.189Z"
+ },
+ {
+ "objectNumber": "MET-310604",
+ "title": "Pair of Figures",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014443153307345220",
+ "postedAt": "2026-01-22T21:00:49.220Z"
+ },
+ {
+ "objectNumber": "MET-437532",
+ "title": "Rubens, Helena Fourment (1614–1673), and Their Son Frans (1633–1678)",
+ "artist": "Peter Paul Rubens",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014467518002545020",
+ "postedAt": "2026-01-22T22:37:38.839Z"
+ },
+ {
+ "objectNumber": "MET-191803",
+ "title": "Adam",
+ "artist": "Auguste Rodin",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014483233027850662",
+ "postedAt": "2026-01-22T23:40:05.114Z"
+ },
+ {
+ "objectNumber": "MET-38583",
+ "title": "The Goddess Durga Slaying the Demon Buffalo Mahisha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014488256939991358",
+ "postedAt": "2026-01-23T00:00:02.903Z"
+ },
+ {
+ "objectNumber": "MET-464070",
+ "title": "Pectoral with Coins and Pseudo-Medallion",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014499952316346465",
+ "postedAt": "2026-01-23T00:46:31.899Z"
+ },
+ {
+ "objectNumber": "MET-452037",
+ "title": "Khusrau and Shirin",
+ "artist": "Hatifi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014647045106413954",
+ "postedAt": "2026-01-23T10:31:01.005Z"
+ },
+ {
+ "objectNumber": "MET-681534",
+ "title": "Contes et nouvelles de Bocace florentin : traduction libre, accommodée au gout de ce temps, & enrichie de figures en taille-douce gravées par Mr. Romain de Hooge",
+ "artist": "Giovanni Boccaccio",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014651585130070206",
+ "postedAt": "2026-01-23T10:49:03.652Z"
+ },
+ {
+ "objectNumber": "MET-203008",
+ "title": "Medal cabinet",
+ "artist": "William Vile",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014662733179060300",
+ "postedAt": "2026-01-23T11:33:21.258Z"
+ },
+ {
+ "objectNumber": "MET-11417",
+ "title": "Washington Crossing the Delaware",
+ "artist": "Emanuel Leutze",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014669454169538969",
+ "postedAt": "2026-01-23T12:00:03.597Z"
+ },
+ {
+ "objectNumber": "PD-1769173683356",
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "museum": "Public Domain Collection",
+ "tweetId": "2014686574689341693",
+ "postedAt": "2026-01-23T13:08:05.448Z"
+ },
+ {
+ "objectNumber": "MET-464023",
+ "title": "Processional Cross",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014699773698441632",
+ "postedAt": "2026-01-23T14:00:32.746Z"
+ },
+ {
+ "objectNumber": "MET-459204",
+ "title": "Tabernacle frame",
+ "artist": "del Tasso",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014733260702822883",
+ "postedAt": "2026-01-23T16:13:36.385Z"
+ },
+ {
+ "objectNumber": "MET-10159",
+ "title": "Fur Traders Descending the Missouri",
+ "artist": "George Caleb Bingham",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014746446575370652",
+ "postedAt": "2026-01-23T17:06:00.407Z"
+ },
+ {
+ "objectNumber": "MET-738765",
+ "title": "The song of Hiawatha",
+ "artist": "Henry Wadsworth Longfellow",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014760075496460456",
+ "postedAt": "2026-01-23T18:00:09.633Z"
+ },
+ {
+ "objectNumber": "PD-1769191826233",
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "museum": "Public Domain Collection",
+ "tweetId": "2014762666791993487",
+ "postedAt": "2026-01-23T18:10:27.113Z"
+ },
+ {
+ "objectNumber": "MET-470311",
+ "title": "Enthroned Virgin and Child",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014806812806271041",
+ "postedAt": "2026-01-23T21:05:52.637Z"
+ },
+ {
+ "objectNumber": "MET-459123",
+ "title": "Madame Roulin and Her Baby",
+ "artist": "Vincent van Gogh",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014813901972767180",
+ "postedAt": "2026-01-23T21:34:02.980Z"
+ },
+ {
+ "objectNumber": "MET-39664",
+ "title": "Irises at Yatsuhashi (Eight Bridges)",
+ "artist": "Ogata Kōrin",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014823549786325143",
+ "postedAt": "2026-01-23T22:12:22.924Z"
+ },
+ {
+ "objectNumber": "MET-250951",
+ "title": "Marble akroterion",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014825564335698206",
+ "postedAt": "2026-01-23T22:20:23.151Z"
+ },
+ {
+ "objectNumber": "MET-254595",
+ "title": "Gold earrings with disk and boat-shaped pendant",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014850644038058207",
+ "postedAt": "2026-01-24T00:00:02.820Z"
+ },
+ {
+ "objectNumber": "MET-325089",
+ "title": "Figure of a man with an oryx, a monkey, and a leopard skin",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014861586218025180",
+ "postedAt": "2026-01-24T00:43:31.849Z"
+ },
+ {
+ "objectNumber": "MET-466287",
+ "title": "Standing Virgin and Child",
+ "artist": "Niclaus Gerhaert von Leyden",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2014868681562734643",
+ "postedAt": "2026-01-24T01:11:43.205Z"
+ },
+ {
+ "objectNumber": "MET-282190",
+ "title": "Pierrot Laughing",
+ "artist": "Nadar",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015027421955957047",
+ "postedAt": "2026-01-24T11:42:30.197Z"
+ },
+ {
+ "objectNumber": "MET-544070",
+ "title": "Antelope Head",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015031842886594912",
+ "postedAt": "2026-01-24T12:00:04.381Z"
+ },
+ {
+ "objectNumber": "MET-450513",
+ "title": "Ewer",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015054795804164569",
+ "postedAt": "2026-01-24T13:31:16.464Z"
+ },
+ {
+ "objectNumber": "MET-591856",
+ "title": "The Whole Art of Marbling as Applied to Paper, Book Edges, etc.",
+ "artist": "Charles W. Woolnough",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015055803993161949",
+ "postedAt": "2026-01-24T13:35:16.525Z"
+ },
+ {
+ "objectNumber": "PD-1769265827213",
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015073052313428102",
+ "postedAt": "2026-01-24T14:43:49.081Z"
+ },
+ {
+ "objectNumber": "MET-204758",
+ "title": "Perseus with the Head of Medusa",
+ "artist": "Antonio Canova",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015091321674039725",
+ "postedAt": "2026-01-24T15:56:25.121Z"
+ },
+ {
+ "objectNumber": "MET-436896",
+ "title": "Portrait of a Woman with a Man at a Casement",
+ "artist": "Fra Filippo Lippi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015099948820488661",
+ "postedAt": "2026-01-24T16:30:42.053Z"
+ },
+ {
+ "objectNumber": "MET-254779",
+ "title": "Terracotta stirrup jar with octopus",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015109602162229613",
+ "postedAt": "2026-01-24T17:09:03.604Z"
+ },
+ {
+ "objectNumber": "MET-40001",
+ "title": "Dish with Recumbent Elephant Surrounded by Clouds",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015113155354202196",
+ "postedAt": "2026-01-24T17:23:10.280Z"
+ },
+ {
+ "objectNumber": "MET-464490",
+ "title": "Reliquary Casket with Scenes from the Martyrdom of Saint Thomas Becket",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015122443267174581",
+ "postedAt": "2026-01-24T18:00:04.861Z"
+ },
+ {
+ "objectNumber": "MET-475513",
+ "title": "The Cloisters Playing Cards",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015142571274805276",
+ "postedAt": "2026-01-24T19:20:03.759Z"
+ },
+ {
+ "objectNumber": "PD-1769285300783",
+ "title": "The Garden of Earthly Delights",
+ "artist": "Hieronymus Bosch",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015154728918032687",
+ "postedAt": "2026-01-24T20:08:22.071Z"
+ },
+ {
+ "objectNumber": "PD-1769286385977",
+ "title": "The Garden of Earthly Delights",
+ "artist": "Hieronymus Bosch",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015159280148869322",
+ "postedAt": "2026-01-24T20:26:27.186Z"
+ },
+ {
+ "objectNumber": "MET-472562",
+ "title": "The Fieschi Morgan Staurotheke",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015167399583228035",
+ "postedAt": "2026-01-24T20:58:43.274Z"
+ },
+ {
+ "objectNumber": "PD-1769292311022",
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015184134533255337",
+ "postedAt": "2026-01-24T22:05:12.966Z"
+ },
+ {
+ "objectNumber": "MET-257603",
+ "title": "Marble head of a Ptolemaic queen",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015200373200531495",
+ "postedAt": "2026-01-24T23:09:44.789Z"
+ },
+ {
+ "objectNumber": "PD-1769299085458",
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015212545943150626",
+ "postedAt": "2026-01-24T23:58:06.755Z"
+ },
+ {
+ "objectNumber": "MET-337497",
+ "title": "Studies for the Libyan Sibyl (recto); Studies for the Libyan Sibyl and a small Sketch for a Seated Figure (verso)",
+ "artist": "Michelangelo Buonarroti",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015394230701814147",
+ "postedAt": "2026-01-25T12:00:04.230Z"
+ },
+ {
+ "objectNumber": "MET-38314",
+ "title": "Standing Shiva or Temple Guardian (Dvarapala)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015413884644159655",
+ "postedAt": "2026-01-25T13:18:09.806Z"
+ },
+ {
+ "objectNumber": "MET-24927",
+ "title": "Pair of Flintlock Pistols of Empress Catherine the Great (1729–1796)",
+ "artist": "Johan Adolph Grecke",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015417423512674578",
+ "postedAt": "2026-01-25T13:32:14.153Z"
+ },
+ {
+ "objectNumber": "MET-435888",
+ "title": "Soap Bubbles",
+ "artist": "Jean Siméon Chardin",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015442780584390816",
+ "postedAt": "2026-01-25T15:12:59.566Z"
+ },
+ {
+ "objectNumber": "PD-1769358693667",
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015462562679460025",
+ "postedAt": "2026-01-25T16:31:35.434Z"
+ },
+ {
+ "objectNumber": "MET-917487",
+ "title": "Preisliste Nr. 20B",
+ "artist": "Günther Wagner",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015464158691770509",
+ "postedAt": "2026-01-25T16:37:56.278Z"
+ },
+ {
+ "objectNumber": "MET-467705",
+ "title": "Tomb Effigy Bust of Marie de France (1327-41), daughter of Charles IV of France and Jeanne d'Evreux",
+ "artist": "Jean de Liège",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015476757626032348",
+ "postedAt": "2026-01-25T17:27:59.888Z"
+ },
+ {
+ "objectNumber": "MET-681384",
+ "title": "Rapport au roi sur la situation, au 31 mars, 1829, des canaux et autres ouvrages entrepris en vertu des lois des 20 juin et 5 aout 1821",
+ "artist": "L'Imprimerie Royale",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015484873973416099",
+ "postedAt": "2026-01-25T18:00:15.291Z"
+ },
+ {
+ "objectNumber": "PD-1769380696651",
+ "title": "Sunflowers",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015554846724887009",
+ "postedAt": "2026-01-25T22:38:17.515Z"
+ },
+ {
+ "objectNumber": "MET-38133",
+ "title": "Enthroned Vishnu",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015568556302414260",
+ "postedAt": "2026-01-25T23:32:46.449Z"
+ },
+ {
+ "objectNumber": "MET-436101",
+ "title": "The Rest on the Flight into Egypt",
+ "artist": "Gerard David",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015571099799654471",
+ "postedAt": "2026-01-25T23:42:53.165Z"
+ },
+ {
+ "objectNumber": "MET-912075",
+ "title": "An illustrated handbook of Indian arms : being a classified and descriptive catalogue of the arms exhibited at the India Museum : with an introductory sketch of the military history of India",
+ "artist": "Lord Wilbraham Egerton",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015575446910566444",
+ "postedAt": "2026-01-26T00:00:09.608Z"
+ },
+ {
+ "objectNumber": "MET-469844",
+ "title": "Enthroned Virgin and Child",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015589350923530641",
+ "postedAt": "2026-01-26T00:55:24.250Z"
+ },
+ {
+ "objectNumber": "MET-503528",
+ "title": "Bassoon",
+ "artist": "Dominique Antony Porthaux",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015591397156651338",
+ "postedAt": "2026-01-26T01:03:32.134Z"
+ },
+ {
+ "objectNumber": "PD-1769389644042",
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015592377013174624",
+ "postedAt": "2026-01-26T01:07:25.559Z"
+ },
+ {
+ "objectNumber": "MET-910052",
+ "title": "Box in book form with drawer, Album",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015609390175551832",
+ "postedAt": "2026-01-26T02:15:02.757Z"
+ },
+ {
+ "objectNumber": "MET-469836",
+ "title": "Saint Margaret of Antioch",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015657816674197575",
+ "postedAt": "2026-01-26T05:27:27.703Z"
+ },
+ {
+ "objectNumber": "MET-466178",
+ "title": "Fragment of a Tapestry or Wall Hanging",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015756621406998544",
+ "postedAt": "2026-01-26T12:00:04.281Z"
+ },
+ {
+ "objectNumber": "MET-459241",
+ "title": "At the Circus: The Spanish Walk (Au Cirque: Le Pas espagnol)",
+ "artist": "Henri de Toulouse-Lautrec",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015759728417345706",
+ "postedAt": "2026-01-26T12:12:26.017Z"
+ },
+ {
+ "objectNumber": "MET-39918",
+ "title": "Biographies of Lian Po and Lin Xiangru",
+ "artist": "Huang Tingjian",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015765311451189331",
+ "postedAt": "2026-01-26T12:34:36.310Z"
+ },
+ {
+ "objectNumber": "MET-544118",
+ "title": "Cat Statuette intended to contain a mummified cat",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015768854648295755",
+ "postedAt": "2026-01-26T12:48:41.428Z"
+ },
+ {
+ "objectNumber": "MET-42733",
+ "title": "Buddha Maitreya (Mile)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015787621050687958",
+ "postedAt": "2026-01-26T14:03:15.793Z"
+ },
+ {
+ "objectNumber": "MET-503443",
+ "title": "Violin",
+ "artist": "Joachim Tielke",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015847217496818058",
+ "postedAt": "2026-01-26T18:00:04.363Z"
+ },
+ {
+ "objectNumber": "MET-75414",
+ "title": "Buddha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015918513509302363",
+ "postedAt": "2026-01-26T22:43:22.923Z"
+ },
+ {
+ "objectNumber": "MET-459227",
+ "title": "Emperor Vespasian Cured by Veronica's Veil",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015928740703089114",
+ "postedAt": "2026-01-26T23:24:00.721Z"
+ },
+ {
+ "objectNumber": "MET-453336",
+ "title": "A Stallion",
+ "artist": "Habiballah of Sava",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015937841327751486",
+ "postedAt": "2026-01-27T00:00:10.888Z"
+ },
+ {
+ "objectNumber": "MET-910031",
+ "title": "Spruce gum box, ER. PQ",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015941895290290364",
+ "postedAt": "2026-01-27T00:16:18.186Z"
+ },
+ {
+ "objectNumber": "PD-1769473690522",
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015944897065574470",
+ "postedAt": "2026-01-27T00:28:12.979Z"
+ },
+ {
+ "objectNumber": "MET-38326",
+ "title": "Vishnu",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015954543562182857",
+ "postedAt": "2026-01-27T01:06:32.875Z"
+ },
+ {
+ "objectNumber": "MET-909681",
+ "title": "Vinaigrette box, M. or A. Ebsen, 1845",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015957584415768693",
+ "postedAt": "2026-01-27T01:18:37.646Z"
+ },
+ {
+ "objectNumber": "MET-437423",
+ "title": "The Immaculate Conception",
+ "artist": "Guido Reni",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015961658922041548",
+ "postedAt": "2026-01-27T01:34:49.621Z"
+ },
+ {
+ "objectNumber": "MET-468181",
+ "title": "Bow Brooch",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015975339898634439",
+ "postedAt": "2026-01-27T02:29:10.945Z"
+ },
+ {
+ "objectNumber": "MET-447509",
+ "title": "Carpet with Triple-Arch Design",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2015980462905426080",
+ "postedAt": "2026-01-27T02:49:32.236Z"
+ },
+ {
+ "objectNumber": "PD-1769482282240",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2015980928045314531",
+ "postedAt": "2026-01-27T02:51:23.258Z"
+ },
+ {
+ "objectNumber": "PD-1769515200472",
+ "title": "The Birth of Venus",
+ "artist": "Sandro Botticelli",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016119022937583773",
+ "postedAt": "2026-01-27T12:00:07.702Z"
+ },
+ {
+ "objectNumber": "MET-38515",
+ "title": "Vaikuntha Vishnu",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016122923376115862",
+ "postedAt": "2026-01-27T12:15:38.064Z"
+ },
+ {
+ "objectNumber": "MET-38237",
+ "title": "Buddha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016123938204746227",
+ "postedAt": "2026-01-27T12:19:39.747Z"
+ },
+ {
+ "objectNumber": "MET-227284",
+ "title": "Cravat end",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016159013126414509",
+ "postedAt": "2026-01-27T14:39:02.621Z"
+ },
+ {
+ "objectNumber": "MET-483438",
+ "title": "Self-Portrait",
+ "artist": "Egon Schiele",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016209619803381782",
+ "postedAt": "2026-01-27T18:00:08.182Z"
+ },
+ {
+ "objectNumber": "MET-469858",
+ "title": "Diptych with Scenes of the Annunciation, Nativity, Crucifixion, and Resurrection",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016300204128141574",
+ "postedAt": "2026-01-28T00:00:04.869Z"
+ },
+ {
+ "objectNumber": "MET-435876",
+ "title": "Madame Cézanne (Hortense Fiquet, 1850–1922) in a Red Dress",
+ "artist": "Paul Cézanne",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016313657240735989",
+ "postedAt": "2026-01-28T00:53:32.350Z"
+ },
+ {
+ "objectNumber": "PD-1769565100321",
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016328291947581774",
+ "postedAt": "2026-01-28T01:51:41.241Z"
+ },
+ {
+ "objectNumber": "MET-10497",
+ "title": "View from Mount Holyoke, Northampton, Massachusetts, after a Thunderstorm—The Oxbow",
+ "artist": "Thomas Cole",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016332346270924821",
+ "postedAt": "2026-01-28T02:07:48.350Z"
+ },
+ {
+ "objectNumber": "MET-322375",
+ "title": "Funerary relief",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016343958599925807",
+ "postedAt": "2026-01-28T02:53:57.162Z"
+ },
+ {
+ "objectNumber": "PD-1769569433283",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016346468840595766",
+ "postedAt": "2026-01-28T03:03:55.140Z"
+ },
+ {
+ "objectNumber": "MET-38159",
+ "title": "Standing Ganesha",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016359619472589132",
+ "postedAt": "2026-01-28T03:56:10.488Z"
+ },
+ {
+ "objectNumber": "MET-13606",
+ "title": "Phebe Warner Coverlet",
+ "artist": "Sarah Furman Warner Williams",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016378862704484683",
+ "postedAt": "2026-01-28T05:12:38.724Z"
+ },
+ {
+ "objectNumber": "MET-917450",
+ "title": "Belcher Mosaic Glass Co. : [catalog]",
+ "artist": "Belcher Mosaic Glass Co.",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016381369342136473",
+ "postedAt": "2026-01-28T05:22:36.366Z"
+ },
+ {
+ "objectNumber": "MET-634108",
+ "title": "The Dream of the Shepherd (Der Traum des Hirten)",
+ "artist": "Ferdinand Hodler",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016390815506169988",
+ "postedAt": "2026-01-28T06:00:08.238Z"
+ },
+ {
+ "objectNumber": "PD-1769580517292",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016392957012930928",
+ "postedAt": "2026-01-28T06:08:38.763Z"
+ },
+ {
+ "objectNumber": "PD-1769580757277",
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016393964119888116",
+ "postedAt": "2026-01-28T06:12:38.860Z"
+ },
+ {
+ "objectNumber": "MET-547772",
+ "title": "Ritual Statuette of Thutmose III",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016481433880322098",
+ "postedAt": "2026-01-28T12:00:13.390Z"
+ },
+ {
+ "objectNumber": "MET-459082",
+ "title": "Portrait of Gerard de Lairesse",
+ "artist": "Rembrandt (Rembrandt van Rijn)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016572018125393976",
+ "postedAt": "2026-01-28T18:00:10.750Z"
+ },
+ {
+ "objectNumber": "MET-910056",
+ "title": "Box in book form, carved with roses",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016662660151234576",
+ "postedAt": "2026-01-29T00:00:21.435Z"
+ },
+ {
+ "objectNumber": "PD-1769649549176",
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016682496335970683",
+ "postedAt": "2026-01-29T01:19:10.117Z"
+ },
+ {
+ "objectNumber": "MET-24948",
+ "title": "Blade for a Dagger (Tantō)",
+ "artist": "Rai Kunitoshi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016684003387199810",
+ "postedAt": "2026-01-29T01:25:09.673Z"
+ },
+ {
+ "objectNumber": "MET-544450",
+ "title": "Seated Statue of Hatshepsut",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016689565369803086",
+ "postedAt": "2026-01-29T01:47:15.892Z"
+ },
+ {
+ "objectNumber": "MET-245787",
+ "title": "Glass garland bowl",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016708254219620698",
+ "postedAt": "2026-01-29T03:01:31.538Z"
+ },
+ {
+ "objectNumber": "MET-1662",
+ "title": "Side Chair",
+ "artist": "Benjamin Randolph",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016735545838481569",
+ "postedAt": "2026-01-29T04:49:58.696Z"
+ },
+ {
+ "objectNumber": "MET-22506",
+ "title": "Armor (<i>Yoroi</i>) of Ashikaga Takauji (1305–1358)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016737067733307635",
+ "postedAt": "2026-01-29T04:56:01.460Z"
+ },
+ {
+ "objectNumber": "PD-1769662670568",
+ "title": "The Arnolfini Portrait",
+ "artist": "Jan van Eyck",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016737531430367616",
+ "postedAt": "2026-01-29T04:57:51.523Z"
+ },
+ {
+ "objectNumber": "MET-327434",
+ "title": "Cylinder seal with cultic scene",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016753232673198530",
+ "postedAt": "2026-01-29T06:00:15.427Z"
+ },
+ {
+ "objectNumber": "PD-1769667725270",
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016758731946807816",
+ "postedAt": "2026-01-29T06:22:06.109Z"
+ },
+ {
+ "objectNumber": "MET-38327",
+ "title": "Bodhisattva, probably Padmapani Lokeshvara",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016812827655491892",
+ "postedAt": "2026-01-29T09:57:03.602Z"
+ },
+ {
+ "objectNumber": "MET-81134",
+ "title": "Suit",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016815364513104206",
+ "postedAt": "2026-01-29T10:07:08.764Z"
+ },
+ {
+ "objectNumber": "PD-1769682542584",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016820881075687891",
+ "postedAt": "2026-01-29T10:29:03.694Z"
+ },
+ {
+ "objectNumber": "MET-459016",
+ "title": "The Annunciation",
+ "artist": "Botticelli (Alessandro di Mariano Filipepi)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016821393040822566",
+ "postedAt": "2026-01-29T10:31:06.124Z"
+ },
+ {
+ "objectNumber": "MET-311336",
+ "title": "Handle for a Fly Whisk (Tahiri)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016822904521171298",
+ "postedAt": "2026-01-29T10:37:06.232Z"
+ },
+ {
+ "objectNumber": "MET-359453",
+ "title": "Portrait of Emma Hamilton",
+ "artist": "Angelica Kauffmann",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016843857649160384",
+ "postedAt": "2026-01-29T12:00:22.233Z"
+ },
+ {
+ "objectNumber": "MET-72307",
+ "title": "Bowl with inscription and chrysanthemums and tortoiseshell decoration",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016847703989465575",
+ "postedAt": "2026-01-29T12:15:39.040Z"
+ },
+ {
+ "objectNumber": "MET-44696",
+ "title": "Gibbons in a Landscape",
+ "artist": "Sesson Shūkei 雪村周継",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016849801795837953",
+ "postedAt": "2026-01-29T12:23:59.277Z"
+ },
+ {
+ "objectNumber": "PD-1769690861028",
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016855771653558422",
+ "postedAt": "2026-01-29T12:47:42.292Z"
+ },
+ {
+ "objectNumber": "MET-27792",
+ "title": "Harquebusier's Armor of Pedro II, King of Portugal (reigned 1683–1706) with Buff Coat",
+ "artist": "Richard Holden",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016882083810202009",
+ "postedAt": "2026-01-29T14:32:16.002Z"
+ },
+ {
+ "objectNumber": "MET-38127",
+ "title": "Shiva as Mrityunjaya, the Conquerer of Death",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016891216017928425",
+ "postedAt": "2026-01-29T15:08:33.098Z"
+ },
+ {
+ "objectNumber": "PD-1769699663634",
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016892692836544767",
+ "postedAt": "2026-01-29T15:14:24.987Z"
+ },
+ {
+ "objectNumber": "MET-354631",
+ "title": "Christ Crucified between the Two Thieves: The Three Crosses",
+ "artist": "Rembrandt (Rembrandt van Rijn)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016899283807850546",
+ "postedAt": "2026-01-29T15:40:36.744Z"
+ },
+ {
+ "objectNumber": "MET-503578",
+ "title": "Musette Baroque",
+ "artist": "Unknown",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016909401739927906",
+ "postedAt": "2026-01-29T16:20:48.868Z"
+ },
+ {
+ "objectNumber": "MET-910083",
+ "title": "Folding portable lantern in book form",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016914448389406753",
+ "postedAt": "2026-01-29T16:40:51.963Z"
+ },
+ {
+ "objectNumber": "PD-1769705572785",
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "museum": "Public Domain Collection",
+ "tweetId": "2016917477167681897",
+ "postedAt": "2026-01-29T16:52:54.027Z"
+ },
+ {
+ "objectNumber": "MET-591830",
+ "title": "Architectura Curiosa Nova: Das ist, neue ergotzliche Sinn und Kunstreiche auch nutzliche Bau- und Wasser-Kunst . . .",
+ "artist": "Georg Andreas Böckler",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016920516888764894",
+ "postedAt": "2026-01-29T17:04:58.746Z"
+ },
+ {
+ "objectNumber": "MET-438818",
+ "title": "Joseph-Antoine Moltedo (born 1775)",
+ "artist": "Jean Auguste Dominique Ingres",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016927621599408171",
+ "postedAt": "2026-01-29T17:33:13.054Z"
+ },
+ {
+ "objectNumber": "MET-336046",
+ "title": "Saada, the Wife of Abraham Ben-Chimol, and Préciada, One of Their Daughters",
+ "artist": "Eugène Delacroix",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2016934433073541135",
+ "postedAt": "2026-01-29T18:00:16.827Z"
+ },
+ {
+ "objectNumber": "PD-1769731210191",
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "museum": "Public Domain Collection",
+ "tweetId": "2017025008921641440",
+ "postedAt": "2026-01-30T00:00:11.570Z"
+ },
+ {
+ "objectNumber": "PD-1769736404754",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2017046795516596623",
+ "postedAt": "2026-01-30T01:26:45.826Z"
+ },
+ {
+ "objectNumber": "MET-437609",
+ "title": "The Holy Family with the Young Saint John the Baptist",
+ "artist": "Andrea del Sarto (Andrea d'Agnolo)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017048322591379783",
+ "postedAt": "2026-01-30T01:32:50.254Z"
+ },
+ {
+ "objectNumber": "MET-336256",
+ "title": "The Entrance Hall of the Regensburg Synagogue",
+ "artist": "Albrecht Altdorfer",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017296793143546202",
+ "postedAt": "2026-01-30T18:00:10.359Z"
+ },
+ {
+ "objectNumber": "MET-282756",
+ "title": "Wrack",
+ "artist": "William Henry Fox Talbot",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017387383554248978",
+ "postedAt": "2026-01-31T00:00:08.711Z"
+ },
+ {
+ "objectNumber": "MET-543867",
+ "title": "Game of Hounds and Jackals",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017415219967082781",
+ "postedAt": "2026-01-31T01:50:45.115Z"
+ },
+ {
+ "objectNumber": "MET-680818",
+ "title": "Almanach royal, année bissextile M.DCC.LXXXVIII, présenté a sa Majeste pour la premiere fois en 1699",
+ "artist": "Laurent Charles d' Houry",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017434553338761684",
+ "postedAt": "2026-01-31T03:07:34.902Z"
+ },
+ {
+ "objectNumber": "MET-255417",
+ "title": "Marble female figure",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017449681929015312",
+ "postedAt": "2026-01-31T04:07:41.565Z"
+ },
+ {
+ "objectNumber": "PD-1769835612798",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2017462904329826600",
+ "postedAt": "2026-01-31T05:00:13.978Z"
+ },
+ {
+ "objectNumber": "MET-11985",
+ "title": "Osage Warrior",
+ "artist": "Charles Balthazar Julien Févret de Saint-Mémin",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017466933122372094",
+ "postedAt": "2026-01-31T05:16:14.961Z"
+ },
+ {
+ "objectNumber": "PD-1769839200769",
+ "title": "The Arnolfini Portrait",
+ "artist": "Jan van Eyck",
+ "museum": "Public Domain Collection",
+ "tweetId": "2017477952355860730",
+ "postedAt": "2026-01-31T06:00:01.630Z"
+ },
+ {
+ "objectNumber": "MET-194432",
+ "title": "Ewer (Brocca)",
+ "artist": "Medici Porcelain Manufactory",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017484162077495757",
+ "postedAt": "2026-01-31T06:24:42.350Z"
+ },
+ {
+ "objectNumber": "MET-503517",
+ "title": "ex \"Kurtz\" Violin",
+ "artist": "Andrea Amati",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017488205407523051",
+ "postedAt": "2026-01-31T06:40:46.375Z"
+ },
+ {
+ "objectNumber": "MET-329077",
+ "title": "Head of a ruler",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017494304969863505",
+ "postedAt": "2026-01-31T07:05:00.897Z"
+ },
+ {
+ "objectNumber": "MET-453576",
+ "title": "Pyxis (Cylindrical Container)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017494798450757985",
+ "postedAt": "2026-01-31T07:06:58.109Z"
+ },
+ {
+ "objectNumber": "MET-334710",
+ "title": "The Kaunitz Sisters (Leopoldine, Caroline, and Ferdinandine)",
+ "artist": "Jean Auguste Dominique Ingres",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017495338006024264",
+ "postedAt": "2026-01-31T07:09:06.964Z"
+ },
+ {
+ "objectNumber": "MET-544683",
+ "title": "Statue of two men and a boy that served as a domestic icon",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017495848880640063",
+ "postedAt": "2026-01-31T07:11:08.765Z"
+ },
+ {
+ "objectNumber": "MET-821934",
+ "title": "Les modes",
+ "artist": "Galerie Manzi, Joyant & Cie.",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017496376813490377",
+ "postedAt": "2026-01-31T07:13:14.894Z"
+ },
+ {
+ "objectNumber": "MET-501876",
+ "title": "Tombak",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017496832323289128",
+ "postedAt": "2026-01-31T07:15:02.935Z"
+ },
+ {
+ "objectNumber": "MET-501691",
+ "title": "Ceramic Horn",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017497345454444822",
+ "postedAt": "2026-01-31T07:17:05.893Z"
+ },
+ {
+ "objectNumber": "MET-325710",
+ "title": "Sword and scabbard",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017659178366734809",
+ "postedAt": "2026-01-31T18:00:09.229Z"
+ },
+ {
+ "objectNumber": "MET-325693",
+ "title": "Vessel with six animal friezes",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017749804940693828",
+ "postedAt": "2026-02-01T00:00:16.944Z"
+ },
+ {
+ "objectNumber": "MET-726678",
+ "title": "Honoré Daumier, l'homme et l'œuvre : ouvrage orné d'un portrait à l'ea-forte, de deux héliogravures et de 47 illustrations",
+ "artist": "Arsène Alexandre",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017781910395883908",
+ "postedAt": "2026-02-01T02:07:51.301Z"
+ },
+ {
+ "objectNumber": "MET-39325",
+ "title": "Standing Parvati",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017795060981764341",
+ "postedAt": "2026-02-01T03:00:06.568Z"
+ },
+ {
+ "objectNumber": "MET-11122",
+ "title": "The Gulf Stream",
+ "artist": "Winslow Homer",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017803686710546501",
+ "postedAt": "2026-02-01T03:34:23.243Z"
+ },
+ {
+ "objectNumber": "MET-451367",
+ "title": "The 'Nigde' Carpet",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017817412515860684",
+ "postedAt": "2026-02-01T04:28:55.535Z"
+ },
+ {
+ "objectNumber": "MET-335663",
+ "title": "Youth Playing a Pipe for a Satyr",
+ "artist": "Giovanni Benedetto Castiglione (Il Grechetto)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017818929029386300",
+ "postedAt": "2026-02-01T04:34:57.169Z"
+ },
+ {
+ "objectNumber": "MET-311159",
+ "title": "Ceremonial Board (Malu)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017822965199671420",
+ "postedAt": "2026-02-01T04:50:59.515Z"
+ },
+ {
+ "objectNumber": "MET-42178",
+ "title": "Female Dancer",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017830054794068165",
+ "postedAt": "2026-02-01T05:19:09.641Z"
+ },
+ {
+ "objectNumber": "MET-627326",
+ "title": "Portrait of Cosimo de' Medici in full armor, his left hand resting on a staff",
+ "artist": "Niccolò della Casa",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017840388728623254",
+ "postedAt": "2026-02-01T06:00:13.696Z"
+ },
+ {
+ "objectNumber": "MET-503440",
+ "title": "Guitar",
+ "artist": "Joseph de Frías",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017845254209294674",
+ "postedAt": "2026-02-01T06:19:33.194Z"
+ },
+ {
+ "objectNumber": "MET-437455",
+ "title": "The Holy Family with Saints Anne and Catherine of Alexandria",
+ "artist": "Jusepe de Ribera (called Lo Spagnoletto)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017856924801478834",
+ "postedAt": "2026-02-01T07:05:55.969Z"
+ },
+ {
+ "objectNumber": "MET-36029",
+ "title": "Water-moon Avalokiteshvara",
+ "artist": "Unidentified artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017857448997261524",
+ "postedAt": "2026-02-01T07:08:01.053Z"
+ },
+ {
+ "objectNumber": "MET-547771",
+ "title": "Ram's-head Amulet",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017857931925196935",
+ "postedAt": "2026-02-01T07:09:56.134Z"
+ },
+ {
+ "objectNumber": "MET-466084",
+ "title": "Saint Stephen",
+ "artist": "Hans Leinberger",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017858472445165812",
+ "postedAt": "2026-02-01T07:12:05.168Z"
+ },
+ {
+ "objectNumber": "PD-1769930397505",
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "museum": "Public Domain Collection",
+ "tweetId": "2017860459484090548",
+ "postedAt": "2026-02-01T07:19:58.434Z"
+ },
+ {
+ "objectNumber": "MET-256974",
+ "title": "Marble stele (grave marker) with a youth and little girl, and a capital and finial in the form of a sphinx",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2017860979112219108",
+ "postedAt": "2026-02-01T07:22:02.742Z"
+ },
+ {
+ "objectNumber": "MET-78197",
+ "title": "Mandala of the Sun God Surya",
+ "artist": "Kitaharasa",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018021640752537970",
+ "postedAt": "2026-02-01T18:00:26.957Z"
+ },
+ {
+ "objectNumber": "MET-266133",
+ "title": "The Town on the Hill, New Almaden",
+ "artist": "Carleton E. Watkins",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018112139144429599",
+ "postedAt": "2026-02-02T00:00:03.940Z"
+ },
+ {
+ "objectNumber": "MET-544451",
+ "title": "Statuette of the Child Amenemhab",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018157594221871595",
+ "postedAt": "2026-02-02T03:00:41.116Z"
+ },
+ {
+ "objectNumber": "MET-459027",
+ "title": "Portrait of a Woman, Possibly a Nun of San Secondo; (verso) Scene in Grisaille",
+ "artist": "Jacometto (Jacometto Veneziano)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018170273175101556",
+ "postedAt": "2026-02-02T03:51:04.234Z"
+ },
+ {
+ "objectNumber": "MET-910015",
+ "title": "Locking biscuit box in book form : Biscuits Olibet",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018180428696576134",
+ "postedAt": "2026-02-02T04:31:25.322Z"
+ },
+ {
+ "objectNumber": "PD-1770011755402",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2018201699614445838",
+ "postedAt": "2026-02-02T05:55:56.491Z"
+ },
+ {
+ "objectNumber": "MET-447071",
+ "title": "Folio from a Mu'nis al-ahrar fi daqa'iq al-ash'ar (The Free Man's Companion to the Subtleties of Poems) of Jajarmi",
+ "artist": "Muhammad ibn Badr al-Din Jajarmi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018202741605294517",
+ "postedAt": "2026-02-02T06:00:05.194Z"
+ },
+ {
+ "objectNumber": "MET-459210",
+ "title": "Satire on Art Criticism",
+ "artist": "Rembrandt (Rembrandt van Rijn)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018206317496930688",
+ "postedAt": "2026-02-02T06:14:17.958Z"
+ },
+ {
+ "objectNumber": "MET-316682",
+ "title": "Poporo (lime container) with female figures",
+ "artist": "Quimbaya artist(s)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018206797228900821",
+ "postedAt": "2026-02-02T06:16:12.000Z"
+ },
+ {
+ "objectNumber": "PD-1770020944859",
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "museum": "Public Domain Collection",
+ "tweetId": "2018240244030886082",
+ "postedAt": "2026-02-02T08:29:06.211Z"
+ },
+ {
+ "objectNumber": "MET-11380",
+ "title": "Nocturne",
+ "artist": "John La Farge",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018243342866567223",
+ "postedAt": "2026-02-02T08:41:25.242Z"
+ },
+ {
+ "objectNumber": "MET-24937",
+ "title": "Armor of Infante Luis, Prince of Asturias (1707–1724)",
+ "artist": "Jean Drouart",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018257534113734728",
+ "postedAt": "2026-02-02T09:37:48.930Z"
+ },
+ {
+ "objectNumber": "MET-466045",
+ "title": "Crucified Christ",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018270215755513865",
+ "postedAt": "2026-02-02T10:28:12.208Z"
+ },
+ {
+ "objectNumber": "MET-448281",
+ "title": "Folio from a Qur'an Manuscript",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018274772640231822",
+ "postedAt": "2026-02-02T10:46:18.662Z"
+ },
+ {
+ "objectNumber": "MET-39883",
+ "title": "Kshitigarbha",
+ "artist": "Unidentified artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018277786356728161",
+ "postedAt": "2026-02-02T10:58:17.511Z"
+ },
+ {
+ "objectNumber": "MET-337725",
+ "title": "The Round Tower, from \"Carceri d'invenzione\" (Imaginary Prisons)",
+ "artist": "Giovanni Battista Piranesi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018293372902101454",
+ "postedAt": "2026-02-02T12:00:12.958Z"
+ },
+ {
+ "objectNumber": "PD-1770035327832",
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "museum": "Public Domain Collection",
+ "tweetId": "2018300569870602386",
+ "postedAt": "2026-02-02T12:28:49.074Z"
+ },
+ {
+ "objectNumber": "MET-78190",
+ "title": "Chakrasamvara and Vajravarahi",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018389462926098941",
+ "postedAt": "2026-02-02T18:22:02.602Z"
+ },
+ {
+ "objectNumber": "MET-459028",
+ "title": "Portrait of Alvise Contarini(?); (verso) A Tethered Roebuck",
+ "artist": "Jacometto (Jacometto Veneziano)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018405549474529596",
+ "postedAt": "2026-02-02T19:25:58.341Z"
+ },
+ {
+ "objectNumber": "MET-739504",
+ "title": "Examples of Chinese ornament selected from objects in the South Kensington Museum and other collections",
+ "artist": "Owen Jones",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018474576112762960",
+ "postedAt": "2026-02-03T00:00:15.844Z"
+ },
+ {
+ "objectNumber": "PD-1770089254297",
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "museum": "Public Domain Collection",
+ "tweetId": "2018526754160480381",
+ "postedAt": "2026-02-03T03:27:35.552Z"
+ },
+ {
+ "objectNumber": "MET-554769",
+ "title": "Stelophorous Statue of Bay",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018527804896534861",
+ "postedAt": "2026-02-03T03:31:46.281Z"
+ },
+ {
+ "objectNumber": "MET-336489",
+ "title": "Queen Esther Approaching the Palace of Ahasuerus",
+ "artist": "Claude Lorrain (Claude Gellée)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018528325145395519",
+ "postedAt": "2026-02-03T03:33:50.162Z"
+ },
+ {
+ "objectNumber": "MET-818177",
+ "title": "William Wyon, R.A., chief engraver of Her Majesty's Mint",
+ "artist": "Leonard Charles Wyon",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018549556838027559",
+ "postedAt": "2026-02-03T04:58:12.234Z"
+ },
+ {
+ "objectNumber": "MET-459213",
+ "title": "Madame Félix Gallois",
+ "artist": "Jean Auguste Dominique Ingres",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018553141688733860",
+ "postedAt": "2026-02-03T05:12:27.052Z"
+ },
+ {
+ "objectNumber": "MET-459138",
+ "title": "Ecstatic Christ",
+ "artist": "Hans Baldung (called Hans Baldung Grien)",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018562270897504678",
+ "postedAt": "2026-02-03T05:48:43.666Z"
+ },
+ {
+ "objectNumber": "MET-324008",
+ "title": "Orthostat relief: lion-hunt scene",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018565159653163121",
+ "postedAt": "2026-02-03T06:00:12.577Z"
+ },
+ {
+ "objectNumber": "PD-1770098561130",
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "museum": "Public Domain Collection",
+ "tweetId": "2018565789746712699",
+ "postedAt": "2026-02-03T06:02:42.285Z"
+ },
+ {
+ "objectNumber": "MET-910543",
+ "title": "Woman at her dressing table",
+ "artist": "Leng Mei",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018579501056467269",
+ "postedAt": "2026-02-03T06:57:11.837Z"
+ },
+ {
+ "objectNumber": "MET-548310",
+ "title": "Statuette of Isis with the infant Horus",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018610451777802386",
+ "postedAt": "2026-02-03T09:00:10.714Z"
+ },
+ {
+ "objectNumber": "MET-476711",
+ "title": "Bust of the Virgin",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018628188046594264",
+ "postedAt": "2026-02-03T10:10:39.410Z"
+ },
+ {
+ "objectNumber": "MET-256978",
+ "title": "Two bronze helmets",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018628704180793712",
+ "postedAt": "2026-02-03T10:12:42.617Z"
+ },
+ {
+ "objectNumber": "MET-437344",
+ "title": "The Shepherd's Song",
+ "artist": "Pierre Puvis de Chavannes",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018639849721651301",
+ "postedAt": "2026-02-03T10:56:59.461Z"
+ },
+ {
+ "objectNumber": "MET-242008",
+ "title": "Limestone Herakles",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018641385046598026",
+ "postedAt": "2026-02-03T11:03:05.823Z"
+ },
+ {
+ "objectNumber": "MET-267019",
+ "title": "American Barque \"Jane Tudor,\" Conway Bay",
+ "artist": "David Johnson",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018836912707137983",
+ "postedAt": "2026-02-04T00:00:03.724Z"
+ },
+ {
+ "objectNumber": "MET-451804",
+ "title": "Dish with Two Intertwined Dragons",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018927545706770542",
+ "postedAt": "2026-02-04T06:00:11.884Z"
+ },
+ {
+ "objectNumber": "MET-42162",
+ "title": "Buddha Maitreya (Mile) Altarpiece",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018937958729179438",
+ "postedAt": "2026-02-04T06:41:34.540Z"
+ },
+ {
+ "objectNumber": "MET-76445",
+ "title": "Enthroned Buddha Granting Boons",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018962816355766665",
+ "postedAt": "2026-02-04T08:20:21.013Z"
+ },
+ {
+ "objectNumber": "MET-459244",
+ "title": "Dish (piatto): Man Washing the Mouth of an Ass",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2018969398288290060",
+ "postedAt": "2026-02-04T08:46:30.389Z"
+ },
+ {
+ "objectNumber": "PD-1770195505488",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2018972403041435813",
+ "postedAt": "2026-02-04T08:58:26.445Z"
+ },
+ {
+ "objectNumber": "MET-824771",
+ "title": "Marie Antoinette in a Park",
+ "artist": "Elisabeth Louise Vigée Le Brun",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019014025003286673",
+ "postedAt": "2026-02-04T11:43:50.473Z"
+ },
+ {
+ "objectNumber": "MET-451402",
+ "title": "\"The Elephant Clock\", Folio from a Book of the Knowledge of Ingenious Mechanical Devices by al-Jazari",
+ "artist": "Farrukh ibn `Abd al-Latif",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019015062107926921",
+ "postedAt": "2026-02-04T11:47:57.596Z"
+ },
+ {
+ "objectNumber": "MET-544067",
+ "title": "Hair Comb Decorated with Rows of Wild Animals",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019018105318699345",
+ "postedAt": "2026-02-04T12:00:02.828Z"
+ },
+ {
+ "objectNumber": "MET-78187",
+ "title": "Bhairava",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019029222753448213",
+ "postedAt": "2026-02-04T12:44:13.953Z"
+ },
+ {
+ "objectNumber": "MET-204804",
+ "title": "La Crainte des Traits de l'Amour",
+ "artist": "Jean-Louis Lemoyne",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019033776555532554",
+ "postedAt": "2026-02-04T13:02:19.282Z"
+ },
+ {
+ "objectNumber": "PD-1770210985020",
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019037329013485857",
+ "postedAt": "2026-02-04T13:16:25.991Z"
+ },
+ {
+ "objectNumber": "MET-680708",
+ "title": "Almanach royal : année bissextile M.DCC.LXXXIV. présenté a Sa Majesté pour la premiere fois en 1699 par Laurent d'Houry ...",
+ "artist": "Laurent Charles d' Houry",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019049528679399840",
+ "postedAt": "2026-02-04T14:04:55.057Z"
+ },
+ {
+ "objectNumber": "MET-42298",
+ "title": "Covered box decorated with chrysanthemums and waterfowl among lotus",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019061682463744468",
+ "postedAt": "2026-02-04T14:53:12.680Z"
+ },
+ {
+ "objectNumber": "MET-446996",
+ "title": "Swan-Neck Glass Bottle",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019071818452382016",
+ "postedAt": "2026-02-04T15:33:29.181Z"
+ },
+ {
+ "objectNumber": "MET-503939",
+ "title": "Sgra-Snyan",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019073330008260700",
+ "postedAt": "2026-02-04T15:39:29.655Z"
+ },
+ {
+ "objectNumber": "MET-21940",
+ "title": "Crossbow of Count Ulrich V of Württemberg (1413–1480)",
+ "artist": "Heinrich Heid von Winterthur",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019108713135792614",
+ "postedAt": "2026-02-04T18:00:05.592Z"
+ },
+ {
+ "objectNumber": "MET-591858",
+ "title": "Diaries of Samuel P. Avery",
+ "artist": "Samuel Putnam Avery Sr.",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019199313281446206",
+ "postedAt": "2026-02-05T00:00:06.080Z"
+ },
+ {
+ "objectNumber": "MET-681128",
+ "title": "Les aventures de Télémaque, fils d'Ulysse par M. de Fénélon ; avec figures en taille-douce, dessinées par MM. Cochin et Moreau le jeune",
+ "artist": "François de Salignac de La Mothe-Fénelon",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019220439403462892",
+ "postedAt": "2026-02-05T01:24:03.260Z"
+ },
+ {
+ "objectNumber": "MET-248644",
+ "title": "Marble funerary lekythos",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019289899409818084",
+ "postedAt": "2026-02-05T06:00:04.132Z"
+ },
+ {
+ "objectNumber": "MET-933846",
+ "title": "Architecture et décoration turques au XVe siècle",
+ "artist": "Léon Parvillée",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019311212794904818",
+ "postedAt": "2026-02-05T07:24:45.398Z"
+ },
+ {
+ "objectNumber": "PD-1770277360291",
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019315726583230787",
+ "postedAt": "2026-02-05T07:42:41.149Z"
+ },
+ {
+ "objectNumber": "MET-256548",
+ "title": "Terracotta kernos (vase for multiple offerings)",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019321838799843517",
+ "postedAt": "2026-02-05T08:06:58.765Z"
+ },
+ {
+ "objectNumber": "MET-677426",
+ "title": "Vishnu",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019351224945111395",
+ "postedAt": "2026-02-05T10:03:44.980Z"
+ },
+ {
+ "objectNumber": "MET-912077",
+ "title": "L'art japonais",
+ "artist": "Louis Gonse",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019361420429914206",
+ "postedAt": "2026-02-05T10:44:16.161Z"
+ },
+ {
+ "objectNumber": "PD-1770288361064",
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019361867257565291",
+ "postedAt": "2026-02-05T10:46:02.009Z"
+ },
+ {
+ "objectNumber": "MET-253505",
+ "title": "Marble grave stele of a young woman and servant",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019367953423298572",
+ "postedAt": "2026-02-05T11:10:13.424Z"
+ },
+ {
+ "objectNumber": "MET-79101",
+ "title": "Shoes",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019369986108903515",
+ "postedAt": "2026-02-05T11:18:17.906Z"
+ },
+ {
+ "objectNumber": "MET-687677",
+ "title": "Minnehaha",
+ "artist": "Edmonia Lewis",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019370495251222683",
+ "postedAt": "2026-02-05T11:20:19.188Z"
+ },
+ {
+ "objectNumber": "MET-918158",
+ "title": "Maison R. Garnier, Brun-Cottan frères successeurs ... Paris : cuivrerie et serrurerie artistiques ..",
+ "artist": "Maison R. Garnier",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019380529486000201",
+ "postedAt": "2026-02-05T12:00:11.898Z"
+ },
+ {
+ "objectNumber": "MET-10821",
+ "title": "The Dancing Lesson",
+ "artist": "Thomas Eakins",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019390279774867493",
+ "postedAt": "2026-02-05T12:38:56.400Z"
+ },
+ {
+ "objectNumber": "PD-1770295495989",
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019391794300219627",
+ "postedAt": "2026-02-05T12:44:57.142Z"
+ },
+ {
+ "objectNumber": "MET-3141",
+ "title": "Desk and bookcase",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019412595967742001",
+ "postedAt": "2026-02-05T14:07:37.014Z"
+ },
+ {
+ "objectNumber": "PD-1770302991852",
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019423233255330077",
+ "postedAt": "2026-02-05T14:49:52.735Z"
+ },
+ {
+ "objectNumber": "PD-1770336000687",
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019561710269653262",
+ "postedAt": "2026-02-06T00:00:08.388Z"
+ },
+ {
+ "objectNumber": "MET-502040",
+ "title": "Saùng-Gauk",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019562715795321250",
+ "postedAt": "2026-02-06T00:04:08.186Z"
+ },
+ {
+ "objectNumber": "PD-1770339021052",
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019574351205683270",
+ "postedAt": "2026-02-06T00:50:22.083Z"
+ },
+ {
+ "objectNumber": "MET-253348",
+ "title": "Terracotta lekythos (oil flask)",
+ "artist": "Amasis Painter",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019574890161181004",
+ "postedAt": "2026-02-06T00:52:31.066Z"
+ },
+ {
+ "objectNumber": "MET-546037",
+ "title": "Magical Stela with Horus the Child",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019652304237130107",
+ "postedAt": "2026-02-06T06:00:08.009Z"
+ },
+ {
+ "objectNumber": "PD-1770362717875",
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019673742251815260",
+ "postedAt": "2026-02-06T07:25:18.732Z"
+ },
+ {
+ "objectNumber": "MET-452112",
+ "title": "\"Tahmuras Defeats the Divs\", Folio 23v from the Shahnama (Book of Kings) of Shah Tahmasp",
+ "artist": "Abu'l Qasim Firdausi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019674255223583164",
+ "postedAt": "2026-02-06T07:27:21.472Z"
+ },
+ {
+ "objectNumber": "PD-1770362957605",
+ "title": "The Garden of Earthly Delights",
+ "artist": "Hieronymus Bosch",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019674749232902645",
+ "postedAt": "2026-02-06T07:29:18.865Z"
+ },
+ {
+ "objectNumber": "MET-503971",
+ "title": "Natural Trumpet",
+ "artist": "Johann Wilhelm Haas",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019675266596082066",
+ "postedAt": "2026-02-06T07:31:22.331Z"
+ },
+ {
+ "objectNumber": "MET-910038",
+ "title": "Box in the form of a book",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019675814493814886",
+ "postedAt": "2026-02-06T07:33:33.343Z"
+ },
+ {
+ "objectNumber": "PD-1770363320841",
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019676272771858863",
+ "postedAt": "2026-02-06T07:35:22.188Z"
+ },
+ {
+ "objectNumber": "PD-1770363442632",
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019676781947810250",
+ "postedAt": "2026-02-06T07:37:23.448Z"
+ },
+ {
+ "objectNumber": "MET-451400",
+ "title": "\"Preparing Medicine from Honey\", from a Dispersed Manuscript of an Arabic Translation of De Materia Medica of Dioscorides",
+ "artist": "'Abdullah ibn al-Fadl",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019677295934570805",
+ "postedAt": "2026-02-06T07:39:26.268Z"
+ },
+ {
+ "objectNumber": "PD-1770363687872",
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "museum": "Public Domain Collection",
+ "tweetId": "2019677811880124584",
+ "postedAt": "2026-02-06T07:41:29.145Z"
+ },
+ {
+ "objectNumber": "MET-10481",
+ "title": "Heart of the Andes",
+ "artist": "Frederic Edwin Church",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019678316236796368",
+ "postedAt": "2026-02-06T07:43:29.646Z"
+ },
+ {
+ "objectNumber": "MET-322499",
+ "title": "Belt clasp",
+ "artist": "Unknown Artist",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019678848267481300",
+ "postedAt": "2026-02-06T07:45:36.594Z"
+ },
+ {
+ "objectNumber": "MET-446567",
+ "title": "\"The Story of the Princess of the Blue Pavillion: The Youth of Rum Is Entertained in a Garden by a Fairy and her Maidens\", Folio from a Khamsa (Quintet) of Amir Khusrau Dihlavi",
+ "artist": "Amir Khusrau Dihlavi",
+ "museum": "The Metropolitan Museum of Art",
+ "tweetId": "2019679353014219000",
+ "postedAt": "2026-02-06T07:47:36.705Z"
+ }
+]
\ No newline at end of file
diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs
new file mode 100644
index 0000000..07b22af
--- /dev/null
+++ b/ecosystem.config.cjs
@@ -0,0 +1,39 @@
+/**
+ * PM2 Ecosystem Configuration
+ * Museum Art Animation Bot
+ */
+
+module.exports = {
+ apps: [
+ {
+ name: 'museum-art-bot',
+ script: 'src/index.js',
+ cwd: '/root/Projects/animate-museum-posts',
+
+ // Environment
+ env: {
+ NODE_ENV: 'production',
+ PORT: 7890
+ },
+
+ // Restart behavior
+ autorestart: true,
+ watch: false,
+ max_restarts: 10,
+ min_uptime: '10s',
+ restart_delay: 5000,
+
+ // Logging
+ log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
+ error_file: '/root/Projects/animate-museum-posts/logs/pm2-error.log',
+ out_file: '/root/Projects/animate-museum-posts/logs/pm2-out.log',
+ merge_logs: true,
+
+ // Memory management
+ max_memory_restart: '500M',
+
+ // Graceful shutdown
+ kill_timeout: 5000
+ }
+ ]
+};
diff --git a/museum-art-bot.service b/museum-art-bot.service
new file mode 100644
index 0000000..f8429a3
--- /dev/null
+++ b/museum-art-bot.service
@@ -0,0 +1,17 @@
+[Unit]
+Description=Museum Art Animation Bot
+After=network.target
+
+[Service]
+Type=simple
+User=root
+WorkingDirectory=/root/animatemuseumposts
+ExecStart=/usr/bin/node /root/animatemuseumposts/src/index.js
+Restart=always
+RestartSec=10
+StandardOutput=append:/root/animatemuseumposts/logs/service.log
+StandardError=append:/root/animatemuseumposts/logs/service-error.log
+Environment=NODE_ENV=production
+
+[Install]
+WantedBy=multi-user.target
\ No newline at end of file
diff --git a/output/artwork_1769835612798.jpg b/output/artwork_1769835612798.jpg
new file mode 100644
index 0000000..c1a91fa
Binary files /dev/null and b/output/artwork_1769835612798.jpg differ
diff --git a/output/artwork_1769839200769.jpg b/output/artwork_1769839200769.jpg
new file mode 100644
index 0000000..0087e65
Binary files /dev/null and b/output/artwork_1769839200769.jpg differ
diff --git a/output/artwork_1769930397505.jpg b/output/artwork_1769930397505.jpg
new file mode 100644
index 0000000..55dac41
Binary files /dev/null and b/output/artwork_1769930397505.jpg differ
diff --git a/output/artwork_1769955541913.jpg b/output/artwork_1769955541913.jpg
new file mode 100644
index 0000000..c1a91fa
Binary files /dev/null and b/output/artwork_1769955541913.jpg differ
diff --git a/output/artwork_1770011755402.jpg b/output/artwork_1770011755402.jpg
new file mode 100644
index 0000000..c1a91fa
Binary files /dev/null and b/output/artwork_1770011755402.jpg differ
diff --git a/output/artwork_1770020944859.jpg b/output/artwork_1770020944859.jpg
new file mode 100644
index 0000000..044d43a
Binary files /dev/null and b/output/artwork_1770020944859.jpg differ
diff --git a/output/artwork_1770035327832.jpg b/output/artwork_1770035327832.jpg
new file mode 100644
index 0000000..cc90f5d
Binary files /dev/null and b/output/artwork_1770035327832.jpg differ
diff --git a/output/artwork_1770044154986.jpg b/output/artwork_1770044154986.jpg
new file mode 100644
index 0000000..0087e65
Binary files /dev/null and b/output/artwork_1770044154986.jpg differ
diff --git a/output/artwork_1770089254297.jpg b/output/artwork_1770089254297.jpg
new file mode 100644
index 0000000..c1a91fa
Binary files /dev/null and b/output/artwork_1770089254297.jpg differ
diff --git a/output/artwork_1770098561130.jpg b/output/artwork_1770098561130.jpg
new file mode 100644
index 0000000..55dac41
Binary files /dev/null and b/output/artwork_1770098561130.jpg differ
diff --git a/output/artwork_1770117556959.jpg b/output/artwork_1770117556959.jpg
new file mode 100644
index 0000000..55dac41
Binary files /dev/null and b/output/artwork_1770117556959.jpg differ
diff --git a/output/artwork_1770120000835.jpg b/output/artwork_1770120000835.jpg
new file mode 100644
index 0000000..cc90f5d
Binary files /dev/null and b/output/artwork_1770120000835.jpg differ
diff --git a/output/artwork_1770125167080.jpg b/output/artwork_1770125167080.jpg
new file mode 100644
index 0000000..9a24b89
Binary files /dev/null and b/output/artwork_1770125167080.jpg differ
diff --git a/output/artwork_1770195505488.jpg b/output/artwork_1770195505488.jpg
new file mode 100644
index 0000000..e63451c
Binary files /dev/null and b/output/artwork_1770195505488.jpg differ
diff --git a/output/artwork_1770210985020.jpg b/output/artwork_1770210985020.jpg
new file mode 100644
index 0000000..14bb8d9
Binary files /dev/null and b/output/artwork_1770210985020.jpg differ
diff --git a/output/artwork_1770231056684.jpg b/output/artwork_1770231056684.jpg
new file mode 100644
index 0000000..c1a91fa
Binary files /dev/null and b/output/artwork_1770231056684.jpg differ
diff --git a/output/artwork_1770277360291.jpg b/output/artwork_1770277360291.jpg
new file mode 100644
index 0000000..14bb8d9
Binary files /dev/null and b/output/artwork_1770277360291.jpg differ
diff --git a/output/artwork_1770288361064.jpg b/output/artwork_1770288361064.jpg
new file mode 100644
index 0000000..55dac41
Binary files /dev/null and b/output/artwork_1770288361064.jpg differ
diff --git a/output/artwork_1770295495989.jpg b/output/artwork_1770295495989.jpg
new file mode 100644
index 0000000..e63451c
Binary files /dev/null and b/output/artwork_1770295495989.jpg differ
diff --git a/output/artwork_1770302991852.jpg b/output/artwork_1770302991852.jpg
new file mode 100644
index 0000000..55dac41
Binary files /dev/null and b/output/artwork_1770302991852.jpg differ
diff --git a/output/artwork_1770323423731.jpg b/output/artwork_1770323423731.jpg
new file mode 100644
index 0000000..55dac41
Binary files /dev/null and b/output/artwork_1770323423731.jpg differ
diff --git a/output/artwork_1770326084840.jpg b/output/artwork_1770326084840.jpg
new file mode 100644
index 0000000..78b6502
Binary files /dev/null and b/output/artwork_1770326084840.jpg differ
diff --git a/output/artwork_1770336000687.jpg b/output/artwork_1770336000687.jpg
new file mode 100644
index 0000000..9a24b89
Binary files /dev/null and b/output/artwork_1770336000687.jpg differ
diff --git a/output/artwork_1770339021052.jpg b/output/artwork_1770339021052.jpg
new file mode 100644
index 0000000..55dac41
Binary files /dev/null and b/output/artwork_1770339021052.jpg differ
diff --git a/output/artwork_1770362717875.jpg b/output/artwork_1770362717875.jpg
new file mode 100644
index 0000000..14bb8d9
Binary files /dev/null and b/output/artwork_1770362717875.jpg differ
diff --git a/output/artwork_1770362957605.jpg b/output/artwork_1770362957605.jpg
new file mode 100644
index 0000000..293b3c2
Binary files /dev/null and b/output/artwork_1770362957605.jpg differ
diff --git a/output/artwork_1770363320841.jpg b/output/artwork_1770363320841.jpg
new file mode 100644
index 0000000..cc90f5d
Binary files /dev/null and b/output/artwork_1770363320841.jpg differ
diff --git a/output/artwork_1770363442632.jpg b/output/artwork_1770363442632.jpg
new file mode 100644
index 0000000..14bb8d9
Binary files /dev/null and b/output/artwork_1770363442632.jpg differ
diff --git a/output/artwork_1770363687872.jpg b/output/artwork_1770363687872.jpg
new file mode 100644
index 0000000..cc90f5d
Binary files /dev/null and b/output/artwork_1770363687872.jpg differ
diff --git a/output/artwork_1770365013472.jpg b/output/artwork_1770365013472.jpg
new file mode 100644
index 0000000..044d43a
Binary files /dev/null and b/output/artwork_1770365013472.jpg differ
diff --git a/output/artwork_1770365135248.jpg b/output/artwork_1770365135248.jpg
new file mode 100644
index 0000000..55dac41
Binary files /dev/null and b/output/artwork_1770365135248.jpg differ
diff --git a/output/artwork_1770365256192.jpg b/output/artwork_1770365256192.jpg
new file mode 100644
index 0000000..044d43a
Binary files /dev/null and b/output/artwork_1770365256192.jpg differ
diff --git a/output/artwork_1770379201423.jpg b/output/artwork_1770379201423.jpg
new file mode 100644
index 0000000..293b3c2
Binary files /dev/null and b/output/artwork_1770379201423.jpg differ
diff --git a/output/artwork_1770400804167.jpg b/output/artwork_1770400804167.jpg
new file mode 100644
index 0000000..9a24b89
Binary files /dev/null and b/output/artwork_1770400804167.jpg differ
diff --git a/output/artwork_1770403453727.jpg b/output/artwork_1770403453727.jpg
new file mode 100644
index 0000000..c1a91fa
Binary files /dev/null and b/output/artwork_1770403453727.jpg differ
diff --git a/output/artwork_1770403936465.jpg b/output/artwork_1770403936465.jpg
new file mode 100644
index 0000000..cc90f5d
Binary files /dev/null and b/output/artwork_1770403936465.jpg differ
diff --git a/output/artwork_1770404299077.jpg b/output/artwork_1770404299077.jpg
new file mode 100644
index 0000000..044d43a
Binary files /dev/null and b/output/artwork_1770404299077.jpg differ
diff --git a/output/artwork_1770404783572.jpg b/output/artwork_1770404783572.jpg
new file mode 100644
index 0000000..14bb8d9
Binary files /dev/null and b/output/artwork_1770404783572.jpg differ
diff --git a/output/artwork_1770405388375.jpg b/output/artwork_1770405388375.jpg
new file mode 100644
index 0000000..9a24b89
Binary files /dev/null and b/output/artwork_1770405388375.jpg differ
diff --git a/output/artwork_1770405629028.jpg b/output/artwork_1770405629028.jpg
new file mode 100644
index 0000000..293b3c2
Binary files /dev/null and b/output/artwork_1770405629028.jpg differ
diff --git a/output/artwork_1770405750351.jpg b/output/artwork_1770405750351.jpg
new file mode 100644
index 0000000..9a24b89
Binary files /dev/null and b/output/artwork_1770405750351.jpg differ
diff --git a/output/met_1769774401699.jpg b/output/met_1769774401699.jpg
new file mode 100644
index 0000000..3b143d5
Binary files /dev/null and b/output/met_1769774401699.jpg differ
diff --git a/output/met_1769796007708.jpg b/output/met_1769796007708.jpg
new file mode 100644
index 0000000..613687e
Binary files /dev/null and b/output/met_1769796007708.jpg differ
diff --git a/output/met_1769817606610.jpg b/output/met_1769817606610.jpg
new file mode 100644
index 0000000..8c7445f
Binary files /dev/null and b/output/met_1769817606610.jpg differ
diff --git a/output/met_1769824243953.jpg b/output/met_1769824243953.jpg
new file mode 100644
index 0000000..2ca419a
Binary files /dev/null and b/output/met_1769824243953.jpg differ
diff --git a/output/met_1769828839876.jpg b/output/met_1769828839876.jpg
new file mode 100644
index 0000000..dcff0c9
Binary files /dev/null and b/output/met_1769828839876.jpg differ
diff --git a/output/met_1769828852441.jpg b/output/met_1769828852441.jpg
new file mode 100644
index 0000000..32bea17
Binary files /dev/null and b/output/met_1769828852441.jpg differ
diff --git a/output/met_1769832460238.jpg b/output/met_1769832460238.jpg
new file mode 100644
index 0000000..9b98a9a
Binary files /dev/null and b/output/met_1769832460238.jpg differ
diff --git a/output/met_1769835612556.jpg b/output/met_1769835612556.jpg
new file mode 100644
index 0000000..81fc42a
Binary files /dev/null and b/output/met_1769835612556.jpg differ
diff --git a/output/met_1769836572474.jpg b/output/met_1769836572474.jpg
new file mode 100644
index 0000000..5df19a7
Binary files /dev/null and b/output/met_1769836572474.jpg differ
diff --git a/output/met_1769840680766.jpg b/output/met_1769840680766.jpg
new file mode 100644
index 0000000..8353574
Binary files /dev/null and b/output/met_1769840680766.jpg differ
diff --git a/output/met_1769841644674.jpg b/output/met_1769841644674.jpg
new file mode 100644
index 0000000..19a1f2c
Binary files /dev/null and b/output/met_1769841644674.jpg differ
diff --git a/output/met_1769843098236.jpg b/output/met_1769843098236.jpg
new file mode 100644
index 0000000..ea5a023
Binary files /dev/null and b/output/met_1769843098236.jpg differ
diff --git a/output/met_1769843216906.jpg b/output/met_1769843216906.jpg
new file mode 100644
index 0000000..432fdaf
Binary files /dev/null and b/output/met_1769843216906.jpg differ
diff --git a/output/met_1769843345030.jpg b/output/met_1769843345030.jpg
new file mode 100644
index 0000000..70d215f
Binary files /dev/null and b/output/met_1769843345030.jpg differ
diff --git a/output/met_1769843461010.jpg b/output/met_1769843461010.jpg
new file mode 100644
index 0000000..de76ac2
Binary files /dev/null and b/output/met_1769843461010.jpg differ
diff --git a/output/met_1769843461280.jpg b/output/met_1769843461280.jpg
new file mode 100644
index 0000000..cac44de
Binary files /dev/null and b/output/met_1769843461280.jpg differ
diff --git a/output/met_1769843466834.jpg b/output/met_1769843466834.jpg
new file mode 100644
index 0000000..5bbfe33
Binary files /dev/null and b/output/met_1769843466834.jpg differ
diff --git a/output/met_1769843591986.jpg b/output/met_1769843591986.jpg
new file mode 100644
index 0000000..8a53a83
Binary files /dev/null and b/output/met_1769843591986.jpg differ
diff --git a/output/met_1769843702177.jpg b/output/met_1769843702177.jpg
new file mode 100644
index 0000000..5703b79
Binary files /dev/null and b/output/met_1769843702177.jpg differ
diff --git a/output/met_1769843822499.jpg b/output/met_1769843822499.jpg
new file mode 100644
index 0000000..004f761
Binary files /dev/null and b/output/met_1769843822499.jpg differ
diff --git a/output/met_1769843823100.jpg b/output/met_1769843823100.jpg
new file mode 100644
index 0000000..3b2ab30
Binary files /dev/null and b/output/met_1769843823100.jpg differ
diff --git a/output/met_1769843949676.jpg b/output/met_1769843949676.jpg
new file mode 100644
index 0000000..5f7dd52
Binary files /dev/null and b/output/met_1769843949676.jpg differ
diff --git a/output/met_1769844064441.jpg b/output/met_1769844064441.jpg
new file mode 100644
index 0000000..a64bb9f
Binary files /dev/null and b/output/met_1769844064441.jpg differ
diff --git a/output/met_1769844188929.jpg b/output/met_1769844188929.jpg
new file mode 100644
index 0000000..05c23bc
Binary files /dev/null and b/output/met_1769844188929.jpg differ
diff --git a/output/met_1769844311012.jpg b/output/met_1769844311012.jpg
new file mode 100644
index 0000000..0615424
Binary files /dev/null and b/output/met_1769844311012.jpg differ
diff --git a/output/met_1769844434712.jpg b/output/met_1769844434712.jpg
new file mode 100644
index 0000000..ee9ac95
Binary files /dev/null and b/output/met_1769844434712.jpg differ
diff --git a/output/met_1769844438877.jpg b/output/met_1769844438877.jpg
new file mode 100644
index 0000000..f6f13f9
Binary files /dev/null and b/output/met_1769844438877.jpg differ
diff --git a/output/met_1769844448749.jpg b/output/met_1769844448749.jpg
new file mode 100644
index 0000000..df6716c
Binary files /dev/null and b/output/met_1769844448749.jpg differ
diff --git a/output/met_1769844548184.jpg b/output/met_1769844548184.jpg
new file mode 100644
index 0000000..b071867
Binary files /dev/null and b/output/met_1769844548184.jpg differ
diff --git a/output/met_1769844669775.jpg b/output/met_1769844669775.jpg
new file mode 100644
index 0000000..627cb00
Binary files /dev/null and b/output/met_1769844669775.jpg differ
diff --git a/output/met_1769844789562.jpg b/output/met_1769844789562.jpg
new file mode 100644
index 0000000..10a2a25
Binary files /dev/null and b/output/met_1769844789562.jpg differ
diff --git a/output/met_1769844923285.jpg b/output/met_1769844923285.jpg
new file mode 100644
index 0000000..486c373
Binary files /dev/null and b/output/met_1769844923285.jpg differ
diff --git a/output/met_1769845033423.jpg b/output/met_1769845033423.jpg
new file mode 100644
index 0000000..ffc316f
Binary files /dev/null and b/output/met_1769845033423.jpg differ
diff --git a/output/met_1769845158195.jpg b/output/met_1769845158195.jpg
new file mode 100644
index 0000000..62672bc
Binary files /dev/null and b/output/met_1769845158195.jpg differ
diff --git a/output/met_1769860802556.jpg b/output/met_1769860802556.jpg
new file mode 100644
index 0000000..5e2ad24
Binary files /dev/null and b/output/met_1769860802556.jpg differ
diff --git a/output/met_1769860812709.jpg b/output/met_1769860812709.jpg
new file mode 100644
index 0000000..5af4628
Binary files /dev/null and b/output/met_1769860812709.jpg differ
diff --git a/output/met_1769882402007.jpg b/output/met_1769882402007.jpg
new file mode 100644
index 0000000..a5772fd
Binary files /dev/null and b/output/met_1769882402007.jpg differ
diff --git a/output/met_1769904014088.jpg b/output/met_1769904014088.jpg
new file mode 100644
index 0000000..b283a1e
Binary files /dev/null and b/output/met_1769904014088.jpg differ
diff --git a/output/met_1769911662299.jpg b/output/met_1769911662299.jpg
new file mode 100644
index 0000000..95bc75a
Binary files /dev/null and b/output/met_1769911662299.jpg differ
diff --git a/output/met_1769911669065.jpg b/output/met_1769911669065.jpg
new file mode 100644
index 0000000..11e7b43
Binary files /dev/null and b/output/met_1769911669065.jpg differ
diff --git a/output/met_1769914804593.jpg b/output/met_1769914804593.jpg
new file mode 100644
index 0000000..e48b943
Binary files /dev/null and b/output/met_1769914804593.jpg differ
diff --git a/output/met_1769916860555.jpg b/output/met_1769916860555.jpg
new file mode 100644
index 0000000..b8a578d
Binary files /dev/null and b/output/met_1769916860555.jpg differ
diff --git a/output/met_1769920133642.jpg b/output/met_1769920133642.jpg
new file mode 100644
index 0000000..b627120
Binary files /dev/null and b/output/met_1769920133642.jpg differ
diff --git a/output/met_1769920494801.jpg b/output/met_1769920494801.jpg
new file mode 100644
index 0000000..6c59e51
Binary files /dev/null and b/output/met_1769920494801.jpg differ
diff --git a/output/met_1769921457271.jpg b/output/met_1769921457271.jpg
new file mode 100644
index 0000000..c1e044c
Binary files /dev/null and b/output/met_1769921457271.jpg differ
diff --git a/output/met_1769923147919.jpg b/output/met_1769923147919.jpg
new file mode 100644
index 0000000..0ed02a3
Binary files /dev/null and b/output/met_1769923147919.jpg differ
diff --git a/output/met_1769925610592.jpg b/output/met_1769925610592.jpg
new file mode 100644
index 0000000..38f7cff
Binary files /dev/null and b/output/met_1769925610592.jpg differ
diff --git a/output/met_1769926772338.jpg b/output/met_1769926772338.jpg
new file mode 100644
index 0000000..9cbe0d2
Binary files /dev/null and b/output/met_1769926772338.jpg differ
diff --git a/output/met_1769929554107.jpg b/output/met_1769929554107.jpg
new file mode 100644
index 0000000..4ab7fc9
Binary files /dev/null and b/output/met_1769929554107.jpg differ
diff --git a/output/met_1769929678565.jpg b/output/met_1769929678565.jpg
new file mode 100644
index 0000000..f582230
Binary files /dev/null and b/output/met_1769929678565.jpg differ
diff --git a/output/met_1769929794229.jpg b/output/met_1769929794229.jpg
new file mode 100644
index 0000000..112efca
Binary files /dev/null and b/output/met_1769929794229.jpg differ
diff --git a/output/met_1769929922950.jpg b/output/met_1769929922950.jpg
new file mode 100644
index 0000000..1153f07
Binary files /dev/null and b/output/met_1769929922950.jpg differ
diff --git a/output/met_1769930520505.jpg b/output/met_1769930520505.jpg
new file mode 100644
index 0000000..6dfb146
Binary files /dev/null and b/output/met_1769930520505.jpg differ
diff --git a/output/met_1769933312063.jpg b/output/met_1769933312063.jpg
new file mode 100644
index 0000000..2ef3348
Binary files /dev/null and b/output/met_1769933312063.jpg differ
diff --git a/output/met_1769937171852.jpg b/output/met_1769937171852.jpg
new file mode 100644
index 0000000..c4725e3
Binary files /dev/null and b/output/met_1769937171852.jpg differ
diff --git a/output/met_1769937780871.jpg b/output/met_1769937780871.jpg
new file mode 100644
index 0000000..c267854
Binary files /dev/null and b/output/met_1769937780871.jpg differ
diff --git a/output/met_1769941642381.jpg b/output/met_1769941642381.jpg
new file mode 100644
index 0000000..c578aeb
Binary files /dev/null and b/output/met_1769941642381.jpg differ
diff --git a/output/met_1769941646983.jpg b/output/met_1769941646983.jpg
new file mode 100644
index 0000000..796172f
Binary files /dev/null and b/output/met_1769941646983.jpg differ
diff --git a/output/met_1769942129365.jpg b/output/met_1769942129365.jpg
new file mode 100644
index 0000000..0753431
Binary files /dev/null and b/output/met_1769942129365.jpg differ
diff --git a/output/met_1769943214162.jpg b/output/met_1769943214162.jpg
new file mode 100644
index 0000000..279c41e
Binary files /dev/null and b/output/met_1769943214162.jpg differ
diff --git a/output/met_1769947206695.jpg b/output/met_1769947206695.jpg
new file mode 100644
index 0000000..9099064
Binary files /dev/null and b/output/met_1769947206695.jpg differ
diff --git a/output/met_1769962549972.jpg b/output/met_1769962549972.jpg
new file mode 100644
index 0000000..8a53a83
Binary files /dev/null and b/output/met_1769962549972.jpg differ
diff --git a/output/met_1769962554385.jpg b/output/met_1769962554385.jpg
new file mode 100644
index 0000000..e8570a3
Binary files /dev/null and b/output/met_1769962554385.jpg differ
diff --git a/output/met_1769962672220.jpg b/output/met_1769962672220.jpg
new file mode 100644
index 0000000..93ec1a8
Binary files /dev/null and b/output/met_1769962672220.jpg differ
diff --git a/output/met_1769962673381.jpg b/output/met_1769962673381.jpg
new file mode 100644
index 0000000..86ae801
Binary files /dev/null and b/output/met_1769962673381.jpg differ
diff --git a/output/met_1769962800813.jpg b/output/met_1769962800813.jpg
new file mode 100644
index 0000000..15b49de
Binary files /dev/null and b/output/met_1769962800813.jpg differ
diff --git a/output/met_1769963033383.jpg b/output/met_1769963033383.jpg
new file mode 100644
index 0000000..44587cf
Binary files /dev/null and b/output/met_1769963033383.jpg differ
diff --git a/output/met_1769968826586.jpg b/output/met_1769968826586.jpg
new file mode 100644
index 0000000..e4b9de2
Binary files /dev/null and b/output/met_1769968826586.jpg differ
diff --git a/output/met_1769990401393.jpg b/output/met_1769990401393.jpg
new file mode 100644
index 0000000..8a9d1e6
Binary files /dev/null and b/output/met_1769990401393.jpg differ
diff --git a/output/met_1770001239283.jpg b/output/met_1770001239283.jpg
new file mode 100644
index 0000000..5a68e78
Binary files /dev/null and b/output/met_1770001239283.jpg differ
diff --git a/output/met_1770004262088.jpg b/output/met_1770004262088.jpg
new file mode 100644
index 0000000..7ee5fc7
Binary files /dev/null and b/output/met_1770004262088.jpg differ
diff --git a/output/met_1770006683704.jpg b/output/met_1770006683704.jpg
new file mode 100644
index 0000000..753bd2d
Binary files /dev/null and b/output/met_1770006683704.jpg differ
diff --git a/output/met_1770012002839.jpg b/output/met_1770012002839.jpg
new file mode 100644
index 0000000..c7ac580
Binary files /dev/null and b/output/met_1770012002839.jpg differ
diff --git a/output/met_1770012855219.jpg b/output/met_1770012855219.jpg
new file mode 100644
index 0000000..8d51b7b
Binary files /dev/null and b/output/met_1770012855219.jpg differ
diff --git a/output/met_1770012970126.jpg b/output/met_1770012970126.jpg
new file mode 100644
index 0000000..8c5e54d
Binary files /dev/null and b/output/met_1770012970126.jpg differ
diff --git a/output/met_1770021672748.jpg b/output/met_1770021672748.jpg
new file mode 100644
index 0000000..eced2ed
Binary files /dev/null and b/output/met_1770021672748.jpg differ
diff --git a/output/met_1770021674762.jpg b/output/met_1770021674762.jpg
new file mode 100644
index 0000000..a5772fd
Binary files /dev/null and b/output/met_1770021674762.jpg differ
diff --git a/output/met_1770021683281.jpg b/output/met_1770021683281.jpg
new file mode 100644
index 0000000..d6d09fd
Binary files /dev/null and b/output/met_1770021683281.jpg differ
diff --git a/output/met_1770025066268.jpg b/output/met_1770025066268.jpg
new file mode 100644
index 0000000..34e4190
Binary files /dev/null and b/output/met_1770025066268.jpg differ
diff --git a/output/met_1770028085718.jpg b/output/met_1770028085718.jpg
new file mode 100644
index 0000000..d0bfd0a
Binary files /dev/null and b/output/met_1770028085718.jpg differ
diff --git a/output/met_1770028090601.jpg b/output/met_1770028090601.jpg
new file mode 100644
index 0000000..356018e
Binary files /dev/null and b/output/met_1770028090601.jpg differ
diff --git a/output/met_1770029176693.jpg b/output/met_1770029176693.jpg
new file mode 100644
index 0000000..c0bc663
Binary files /dev/null and b/output/met_1770029176693.jpg differ
diff --git a/output/met_1770029894518.jpg b/output/met_1770029894518.jpg
new file mode 100644
index 0000000..9992223
Binary files /dev/null and b/output/met_1770029894518.jpg differ
diff --git a/output/met_1770033612582.jpg b/output/met_1770033612582.jpg
new file mode 100644
index 0000000..df40d7a
Binary files /dev/null and b/output/met_1770033612582.jpg differ
diff --git a/output/met_1770036179891.jpg b/output/met_1770036179891.jpg
new file mode 100644
index 0000000..3fbbfb1
Binary files /dev/null and b/output/met_1770036179891.jpg differ
diff --git a/output/met_1770036537777.jpg b/output/met_1770036537777.jpg
new file mode 100644
index 0000000..0dc2ae1
Binary files /dev/null and b/output/met_1770036537777.jpg differ
diff --git a/output/met_1770042464781.jpg b/output/met_1770042464781.jpg
new file mode 100644
index 0000000..1c1af8b
Binary files /dev/null and b/output/met_1770042464781.jpg differ
diff --git a/output/met_1770044397517.jpg b/output/met_1770044397517.jpg
new file mode 100644
index 0000000..e104983
Binary files /dev/null and b/output/met_1770044397517.jpg differ
diff --git a/output/met_1770045608182.jpg b/output/met_1770045608182.jpg
new file mode 100644
index 0000000..2d585c3
Binary files /dev/null and b/output/met_1770045608182.jpg differ
diff --git a/output/met_1770045726564.jpg b/output/met_1770045726564.jpg
new file mode 100644
index 0000000..0e87254
Binary files /dev/null and b/output/met_1770045726564.jpg differ
diff --git a/output/met_1770045727605.jpg b/output/met_1770045727605.jpg
new file mode 100644
index 0000000..3bef930
Binary files /dev/null and b/output/met_1770045727605.jpg differ
diff --git a/output/met_1770047900924.jpg b/output/met_1770047900924.jpg
new file mode 100644
index 0000000..3915981
Binary files /dev/null and b/output/met_1770047900924.jpg differ
diff --git a/output/met_1770048747161.jpg b/output/met_1770048747161.jpg
new file mode 100644
index 0000000..21fb9b9
Binary files /dev/null and b/output/met_1770048747161.jpg differ
diff --git a/output/met_1770049715124.jpg b/output/met_1770049715124.jpg
new file mode 100644
index 0000000..3b2ab30
Binary files /dev/null and b/output/met_1770049715124.jpg differ
diff --git a/output/met_1770049717751.jpg b/output/met_1770049717751.jpg
new file mode 100644
index 0000000..6a5df42
Binary files /dev/null and b/output/met_1770049717751.jpg differ
diff --git a/output/met_1770055201231.jpg b/output/met_1770055201231.jpg
new file mode 100644
index 0000000..3f15986
Binary files /dev/null and b/output/met_1770055201231.jpg differ
diff --git a/output/met_1770056494533.jpg b/output/met_1770056494533.jpg
new file mode 100644
index 0000000..6af5861
Binary files /dev/null and b/output/met_1770056494533.jpg differ
diff --git a/output/met_1770056522260.jpg b/output/met_1770056522260.jpg
new file mode 100644
index 0000000..25bbf7d
Binary files /dev/null and b/output/met_1770056522260.jpg differ
diff --git a/output/met_1770060356331.jpg b/output/met_1770060356331.jpg
new file mode 100644
index 0000000..942a1e1
Binary files /dev/null and b/output/met_1770060356331.jpg differ
diff --git a/output/met_1770076812528.jpg b/output/met_1770076812528.jpg
new file mode 100644
index 0000000..7fb8c8c
Binary files /dev/null and b/output/met_1770076812528.jpg differ
diff --git a/output/met_1770089254010.jpg b/output/met_1770089254010.jpg
new file mode 100644
index 0000000..93ec1a8
Binary files /dev/null and b/output/met_1770089254010.jpg differ
diff --git a/output/met_1770089504384.jpg b/output/met_1770089504384.jpg
new file mode 100644
index 0000000..b73bd47
Binary files /dev/null and b/output/met_1770089504384.jpg differ
diff --git a/output/met_1770089623207.jpg b/output/met_1770089623207.jpg
new file mode 100644
index 0000000..5dce7a2
Binary files /dev/null and b/output/met_1770089623207.jpg differ
diff --git a/output/met_1770089623458.jpg b/output/met_1770089623458.jpg
new file mode 100644
index 0000000..e48b943
Binary files /dev/null and b/output/met_1770089623458.jpg differ
diff --git a/output/met_1770089628495.jpg b/output/met_1770089628495.jpg
new file mode 100644
index 0000000..ce3b281
Binary files /dev/null and b/output/met_1770089628495.jpg differ
diff --git a/output/met_1770094690889.jpg b/output/met_1770094690889.jpg
new file mode 100644
index 0000000..b8190c4
Binary files /dev/null and b/output/met_1770094690889.jpg differ
diff --git a/output/met_1770095544765.jpg b/output/met_1770095544765.jpg
new file mode 100644
index 0000000..f9e245f
Binary files /dev/null and b/output/met_1770095544765.jpg differ
diff --git a/output/met_1770097721435.jpg b/output/met_1770097721435.jpg
new file mode 100644
index 0000000..59d7e15
Binary files /dev/null and b/output/met_1770097721435.jpg differ
diff --git a/output/met_1770098409457.jpg b/output/met_1770098409457.jpg
new file mode 100644
index 0000000..3e3e925
Binary files /dev/null and b/output/met_1770098409457.jpg differ
diff --git a/output/met_1770101829396.jpg b/output/met_1770101829396.jpg
new file mode 100644
index 0000000..0d5757e
Binary files /dev/null and b/output/met_1770101829396.jpg differ
diff --git a/output/met_1770109208616.jpg b/output/met_1770109208616.jpg
new file mode 100644
index 0000000..67f6283
Binary files /dev/null and b/output/met_1770109208616.jpg differ
diff --git a/output/met_1770109209173.jpg b/output/met_1770109209173.jpg
new file mode 100644
index 0000000..340e73e
Binary files /dev/null and b/output/met_1770109209173.jpg differ
diff --git a/output/met_1770113437738.jpg b/output/met_1770113437738.jpg
new file mode 100644
index 0000000..22b5e03
Binary files /dev/null and b/output/met_1770113437738.jpg differ
diff --git a/output/met_1770113560269.jpg b/output/met_1770113560269.jpg
new file mode 100644
index 0000000..a679a8c
Binary files /dev/null and b/output/met_1770113560269.jpg differ
diff --git a/output/met_1770116219173.jpg b/output/met_1770116219173.jpg
new file mode 100644
index 0000000..396dfb7
Binary files /dev/null and b/output/met_1770116219173.jpg differ
diff --git a/output/met_1770116584262.jpg b/output/met_1770116584262.jpg
new file mode 100644
index 0000000..41d58cf
Binary files /dev/null and b/output/met_1770116584262.jpg differ
diff --git a/output/met_1770117548291.jpg b/output/met_1770117548291.jpg
new file mode 100644
index 0000000..26af633
Binary files /dev/null and b/output/met_1770117548291.jpg differ
diff --git a/output/met_1770117556813.jpg b/output/met_1770117556813.jpg
new file mode 100644
index 0000000..a43ffb3
Binary files /dev/null and b/output/met_1770117556813.jpg differ
diff --git a/output/met_1770118757428.jpg b/output/met_1770118757428.jpg
new file mode 100644
index 0000000..616dbf9
Binary files /dev/null and b/output/met_1770118757428.jpg differ
diff --git a/output/met_1770118766479.jpg b/output/met_1770118766479.jpg
new file mode 100644
index 0000000..380da9f
Binary files /dev/null and b/output/met_1770118766479.jpg differ
diff --git a/output/met_1770119246203.jpg b/output/met_1770119246203.jpg
new file mode 100644
index 0000000..39b291e
Binary files /dev/null and b/output/met_1770119246203.jpg differ
diff --git a/output/met_1770119604354.jpg b/output/met_1770119604354.jpg
new file mode 100644
index 0000000..01a8c4b
Binary files /dev/null and b/output/met_1770119604354.jpg differ
diff --git a/output/met_1770123232703.jpg b/output/met_1770123232703.jpg
new file mode 100644
index 0000000..ea03de7
Binary files /dev/null and b/output/met_1770123232703.jpg differ
diff --git a/output/met_1770128072042.jpg b/output/met_1770128072042.jpg
new file mode 100644
index 0000000..081dc05
Binary files /dev/null and b/output/met_1770128072042.jpg differ
diff --git a/output/met_1770130486349.jpg b/output/met_1770130486349.jpg
new file mode 100644
index 0000000..8e09337
Binary files /dev/null and b/output/met_1770130486349.jpg differ
diff --git a/output/met_1770131219546.jpg b/output/met_1770131219546.jpg
new file mode 100644
index 0000000..c53cdaa
Binary files /dev/null and b/output/met_1770131219546.jpg differ
diff --git a/output/met_1770132783774.jpg b/output/met_1770132783774.jpg
new file mode 100644
index 0000000..43c74bf
Binary files /dev/null and b/output/met_1770132783774.jpg differ
diff --git a/output/met_1770136776211.jpg b/output/met_1770136776211.jpg
new file mode 100644
index 0000000..641ec0f
Binary files /dev/null and b/output/met_1770136776211.jpg differ
diff --git a/output/met_1770137259082.jpg b/output/met_1770137259082.jpg
new file mode 100644
index 0000000..14b6099
Binary files /dev/null and b/output/met_1770137259082.jpg differ
diff --git a/output/met_1770141601019.jpg b/output/met_1770141601019.jpg
new file mode 100644
index 0000000..87ceaa7
Binary files /dev/null and b/output/met_1770141601019.jpg differ
diff --git a/output/met_1770163200840.jpg b/output/met_1770163200840.jpg
new file mode 100644
index 0000000..a3003e6
Binary files /dev/null and b/output/met_1770163200840.jpg differ
diff --git a/output/met_1770184809670.jpg b/output/met_1770184809670.jpg
new file mode 100644
index 0000000..7527b39
Binary files /dev/null and b/output/met_1770184809670.jpg differ
diff --git a/output/met_1770187292538.jpg b/output/met_1770187292538.jpg
new file mode 100644
index 0000000..4f71958
Binary files /dev/null and b/output/met_1770187292538.jpg differ
diff --git a/output/met_1770193219082.jpg b/output/met_1770193219082.jpg
new file mode 100644
index 0000000..6b74a79
Binary files /dev/null and b/output/met_1770193219082.jpg differ
diff --git a/output/met_1770194788037.jpg b/output/met_1770194788037.jpg
new file mode 100644
index 0000000..5bebf5c
Binary files /dev/null and b/output/met_1770194788037.jpg differ
diff --git a/output/met_1770205427261.jpg b/output/met_1770205427261.jpg
new file mode 100644
index 0000000..a06be13
Binary files /dev/null and b/output/met_1770205427261.jpg differ
diff --git a/output/met_1770205675085.jpg b/output/met_1770205675085.jpg
new file mode 100644
index 0000000..75ba74e
Binary files /dev/null and b/output/met_1770205675085.jpg differ
diff --git a/output/met_1770206401310.jpg b/output/met_1770206401310.jpg
new file mode 100644
index 0000000..4cf6239
Binary files /dev/null and b/output/met_1770206401310.jpg differ
diff --git a/output/met_1770209051083.jpg b/output/met_1770209051083.jpg
new file mode 100644
index 0000000..cbca608
Binary files /dev/null and b/output/met_1770209051083.jpg differ
diff --git a/output/met_1770210137607.jpg b/output/met_1770210137607.jpg
new file mode 100644
index 0000000..05c23bc
Binary files /dev/null and b/output/met_1770210137607.jpg differ
diff --git a/output/met_1770210984839.jpg b/output/met_1770210984839.jpg
new file mode 100644
index 0000000..dc2106a
Binary files /dev/null and b/output/met_1770210984839.jpg differ
diff --git a/output/met_1770213892314.jpg b/output/met_1770213892314.jpg
new file mode 100644
index 0000000..4c14254
Binary files /dev/null and b/output/met_1770213892314.jpg differ
diff --git a/output/met_1770216790772.jpg b/output/met_1770216790772.jpg
new file mode 100644
index 0000000..6fd808e
Binary files /dev/null and b/output/met_1770216790772.jpg differ
diff --git a/output/met_1770219207330.jpg b/output/met_1770219207330.jpg
new file mode 100644
index 0000000..275b2e0
Binary files /dev/null and b/output/met_1770219207330.jpg differ
diff --git a/output/met_1770219567965.jpg b/output/met_1770219567965.jpg
new file mode 100644
index 0000000..90aaf94
Binary files /dev/null and b/output/met_1770219567965.jpg differ
diff --git a/output/met_1770228003631.jpg b/output/met_1770228003631.jpg
new file mode 100644
index 0000000..584f2c3
Binary files /dev/null and b/output/met_1770228003631.jpg differ
diff --git a/output/met_1770229969857.jpg b/output/met_1770229969857.jpg
new file mode 100644
index 0000000..953ee13
Binary files /dev/null and b/output/met_1770229969857.jpg differ
diff --git a/output/met_1770229975286.jpg b/output/met_1770229975286.jpg
new file mode 100644
index 0000000..e823c6c
Binary files /dev/null and b/output/met_1770229975286.jpg differ
diff --git a/output/met_1770230089672.jpg b/output/met_1770230089672.jpg
new file mode 100644
index 0000000..3915981
Binary files /dev/null and b/output/met_1770230089672.jpg differ
diff --git a/output/met_1770230210902.jpg b/output/met_1770230210902.jpg
new file mode 100644
index 0000000..08b2fa0
Binary files /dev/null and b/output/met_1770230210902.jpg differ
diff --git a/output/met_1770230330662.jpg b/output/met_1770230330662.jpg
new file mode 100644
index 0000000..b79c3cf
Binary files /dev/null and b/output/met_1770230330662.jpg differ
diff --git a/output/met_1770230451234.jpg b/output/met_1770230451234.jpg
new file mode 100644
index 0000000..7bc4cc8
Binary files /dev/null and b/output/met_1770230451234.jpg differ
diff --git a/output/met_1770230592106.jpg b/output/met_1770230592106.jpg
new file mode 100644
index 0000000..490747d
Binary files /dev/null and b/output/met_1770230592106.jpg differ
diff --git a/output/met_1770230695020.jpg b/output/met_1770230695020.jpg
new file mode 100644
index 0000000..40fed1a
Binary files /dev/null and b/output/met_1770230695020.jpg differ
diff --git a/output/met_1770230814342.jpg b/output/met_1770230814342.jpg
new file mode 100644
index 0000000..2effcd2
Binary files /dev/null and b/output/met_1770230814342.jpg differ
diff --git a/output/met_1770230939263.jpg b/output/met_1770230939263.jpg
new file mode 100644
index 0000000..8b660dc
Binary files /dev/null and b/output/met_1770230939263.jpg differ
diff --git a/output/met_1770238809207.jpg b/output/met_1770238809207.jpg
new file mode 100644
index 0000000..41c89c2
Binary files /dev/null and b/output/met_1770238809207.jpg differ
diff --git a/output/met_1770249604983.jpg b/output/met_1770249604983.jpg
new file mode 100644
index 0000000..974db71
Binary files /dev/null and b/output/met_1770249604983.jpg differ
diff --git a/output/met_1770254631549.jpg b/output/met_1770254631549.jpg
new file mode 100644
index 0000000..b547947
Binary files /dev/null and b/output/met_1770254631549.jpg differ
diff --git a/output/met_1770254640655.jpg b/output/met_1770254640655.jpg
new file mode 100644
index 0000000..dc25348
Binary files /dev/null and b/output/met_1770254640655.jpg differ
diff --git a/output/met_1770271201413.jpg b/output/met_1770271201413.jpg
new file mode 100644
index 0000000..5581329
Binary files /dev/null and b/output/met_1770271201413.jpg differ
diff --git a/output/met_1770276282753.jpg b/output/met_1770276282753.jpg
new file mode 100644
index 0000000..0293135
Binary files /dev/null and b/output/met_1770276282753.jpg differ
diff --git a/output/met_1770277360141.jpg b/output/met_1770277360141.jpg
new file mode 100644
index 0000000..7324ef0
Binary files /dev/null and b/output/met_1770277360141.jpg differ
diff --git a/output/met_1770278816483.jpg b/output/met_1770278816483.jpg
new file mode 100644
index 0000000..1a8e191
Binary files /dev/null and b/output/met_1770278816483.jpg differ
diff --git a/output/met_1770285822826.jpg b/output/met_1770285822826.jpg
new file mode 100644
index 0000000..b9e594e
Binary files /dev/null and b/output/met_1770285822826.jpg differ
diff --git a/output/met_1770288241371.jpg b/output/met_1770288241371.jpg
new file mode 100644
index 0000000..9c28314
Binary files /dev/null and b/output/met_1770288241371.jpg differ
diff --git a/output/met_1770288252741.jpg b/output/met_1770288252741.jpg
new file mode 100644
index 0000000..fede6f9
Binary files /dev/null and b/output/met_1770288252741.jpg differ
diff --git a/output/met_1770289811569.jpg b/output/met_1770289811569.jpg
new file mode 100644
index 0000000..d77bb12
Binary files /dev/null and b/output/met_1770289811569.jpg differ
diff --git a/output/met_1770290295801.jpg b/output/met_1770290295801.jpg
new file mode 100644
index 0000000..8324b2a
Binary files /dev/null and b/output/met_1770290295801.jpg differ
diff --git a/output/met_1770290417447.jpg b/output/met_1770290417447.jpg
new file mode 100644
index 0000000..59ff8af
Binary files /dev/null and b/output/met_1770290417447.jpg differ
diff --git a/output/met_1770292809038.jpg b/output/met_1770292809038.jpg
new file mode 100644
index 0000000..8d875e4
Binary files /dev/null and b/output/met_1770292809038.jpg differ
diff --git a/output/met_1770295133865.jpg b/output/met_1770295133865.jpg
new file mode 100644
index 0000000..919fb31
Binary files /dev/null and b/output/met_1770295133865.jpg differ
diff --git a/output/met_1770300453946.jpg b/output/met_1770300453946.jpg
new file mode 100644
index 0000000..a3003e6
Binary files /dev/null and b/output/met_1770300453946.jpg differ
diff --git a/output/met_1770300454667.jpg b/output/met_1770300454667.jpg
new file mode 100644
index 0000000..60bd32a
Binary files /dev/null and b/output/met_1770300454667.jpg differ
diff --git a/output/met_1770304687726.jpg b/output/met_1770304687726.jpg
new file mode 100644
index 0000000..2c60a7c
Binary files /dev/null and b/output/met_1770304687726.jpg differ
diff --git a/output/met_1770313275637.jpg b/output/met_1770313275637.jpg
new file mode 100644
index 0000000..47898e2
Binary files /dev/null and b/output/met_1770313275637.jpg differ
diff --git a/output/met_1770314406918.jpg b/output/met_1770314406918.jpg
new file mode 100644
index 0000000..e9d1303
Binary files /dev/null and b/output/met_1770314406918.jpg differ
diff --git a/output/met_1770314410080.jpg b/output/met_1770314410080.jpg
new file mode 100644
index 0000000..ccf065f
Binary files /dev/null and b/output/met_1770314410080.jpg differ
diff --git a/output/met_1770314414339.jpg b/output/met_1770314414339.jpg
new file mode 100644
index 0000000..c941cd2
Binary files /dev/null and b/output/met_1770314414339.jpg differ
diff --git a/output/met_1770317020417.jpg b/output/met_1770317020417.jpg
new file mode 100644
index 0000000..a374d29
Binary files /dev/null and b/output/met_1770317020417.jpg differ
diff --git a/output/met_1770317260998.jpg b/output/met_1770317260998.jpg
new file mode 100644
index 0000000..907ae44
Binary files /dev/null and b/output/met_1770317260998.jpg differ
diff --git a/output/met_1770323306047.jpg b/output/met_1770323306047.jpg
new file mode 100644
index 0000000..38f7cff
Binary files /dev/null and b/output/met_1770323306047.jpg differ
diff --git a/output/met_1770323312868.jpg b/output/met_1770323312868.jpg
new file mode 100644
index 0000000..bdee0d2
Binary files /dev/null and b/output/met_1770323312868.jpg differ
diff --git a/output/met_1770323313872.jpg b/output/met_1770323313872.jpg
new file mode 100644
index 0000000..a49430f
Binary files /dev/null and b/output/met_1770323313872.jpg differ
diff --git a/output/met_1770332495769.jpg b/output/met_1770332495769.jpg
new file mode 100644
index 0000000..80b6284
Binary files /dev/null and b/output/met_1770332495769.jpg differ
diff --git a/output/met_1770335520515.jpg b/output/met_1770335520515.jpg
new file mode 100644
index 0000000..c941cd2
Binary files /dev/null and b/output/met_1770335520515.jpg differ
diff --git a/output/met_1770336246161.jpg b/output/met_1770336246161.jpg
new file mode 100644
index 0000000..2756d3c
Binary files /dev/null and b/output/met_1770336246161.jpg differ
diff --git a/output/met_1770339148949.jpg b/output/met_1770339148949.jpg
new file mode 100644
index 0000000..69e0570
Binary files /dev/null and b/output/met_1770339148949.jpg differ
diff --git a/output/met_1770357605609.jpg b/output/met_1770357605609.jpg
new file mode 100644
index 0000000..b5dfcd1
Binary files /dev/null and b/output/met_1770357605609.jpg differ
diff --git a/output/met_1770362717630.jpg b/output/met_1770362717630.jpg
new file mode 100644
index 0000000..5bb0d24
Binary files /dev/null and b/output/met_1770362717630.jpg differ
diff --git a/output/met_1770362838604.jpg b/output/met_1770362838604.jpg
new file mode 100644
index 0000000..90d3fb1
Binary files /dev/null and b/output/met_1770362838604.jpg differ
diff --git a/output/met_1770363080904.jpg b/output/met_1770363080904.jpg
new file mode 100644
index 0000000..e897320
Binary files /dev/null and b/output/met_1770363080904.jpg differ
diff --git a/output/met_1770363210261.jpg b/output/met_1770363210261.jpg
new file mode 100644
index 0000000..6266056
Binary files /dev/null and b/output/met_1770363210261.jpg differ
diff --git a/output/met_1770363563838.jpg b/output/met_1770363563838.jpg
new file mode 100644
index 0000000..3b20553
Binary files /dev/null and b/output/met_1770363563838.jpg differ
diff --git a/output/met_1770363687688.jpg b/output/met_1770363687688.jpg
new file mode 100644
index 0000000..9bceb60
Binary files /dev/null and b/output/met_1770363687688.jpg differ
diff --git a/output/met_1770363807152.jpg b/output/met_1770363807152.jpg
new file mode 100644
index 0000000..184d288
Binary files /dev/null and b/output/met_1770363807152.jpg differ
diff --git a/output/met_1770363933897.jpg b/output/met_1770363933897.jpg
new file mode 100644
index 0000000..928be0d
Binary files /dev/null and b/output/met_1770363933897.jpg differ
diff --git a/output/met_1770364054606.jpg b/output/met_1770364054606.jpg
new file mode 100644
index 0000000..6537267
Binary files /dev/null and b/output/met_1770364054606.jpg differ
diff --git a/output/met_1770364171679.jpg b/output/met_1770364171679.jpg
new file mode 100644
index 0000000..f4367ec
Binary files /dev/null and b/output/met_1770364171679.jpg differ
diff --git a/output/met_1770364292208.jpg b/output/met_1770364292208.jpg
new file mode 100644
index 0000000..15e5036
Binary files /dev/null and b/output/met_1770364292208.jpg differ
diff --git a/output/met_1770364419933.jpg b/output/met_1770364419933.jpg
new file mode 100644
index 0000000..1787c61
Binary files /dev/null and b/output/met_1770364419933.jpg differ
diff --git a/output/met_1770364531892.jpg b/output/met_1770364531892.jpg
new file mode 100644
index 0000000..6ebcb2b
Binary files /dev/null and b/output/met_1770364531892.jpg differ
diff --git a/output/met_1770364652950.jpg b/output/met_1770364652950.jpg
new file mode 100644
index 0000000..84eadb4
Binary files /dev/null and b/output/met_1770364652950.jpg differ
diff --git a/output/met_1770364775830.jpg b/output/met_1770364775830.jpg
new file mode 100644
index 0000000..b0353b4
Binary files /dev/null and b/output/met_1770364775830.jpg differ
diff --git a/output/met_1770364896203.jpg b/output/met_1770364896203.jpg
new file mode 100644
index 0000000..f62103a
Binary files /dev/null and b/output/met_1770364896203.jpg differ
diff --git a/output/met_1770365379809.jpg b/output/met_1770365379809.jpg
new file mode 100644
index 0000000..a3a6a2e
Binary files /dev/null and b/output/met_1770365379809.jpg differ
diff --git a/output/met_1770365382758.jpg b/output/met_1770365382758.jpg
new file mode 100644
index 0000000..171bebd
Binary files /dev/null and b/output/met_1770365382758.jpg differ
diff --git a/output/met_1770365501933.jpg b/output/met_1770365501933.jpg
new file mode 100644
index 0000000..bed79f1
Binary files /dev/null and b/output/met_1770365501933.jpg differ
diff --git a/output/met_1770365618694.jpg b/output/met_1770365618694.jpg
new file mode 100644
index 0000000..5267959
Binary files /dev/null and b/output/met_1770365618694.jpg differ
diff --git a/output/met_1770379201260.jpg b/output/met_1770379201260.jpg
new file mode 100644
index 0000000..05af857
Binary files /dev/null and b/output/met_1770379201260.jpg differ
diff --git a/output/met_1770400803944.jpg b/output/met_1770400803944.jpg
new file mode 100644
index 0000000..6af5861
Binary files /dev/null and b/output/met_1770400803944.jpg differ
diff --git a/output/met_1770403099419.jpg b/output/met_1770403099419.jpg
new file mode 100644
index 0000000..e4726b1
Binary files /dev/null and b/output/met_1770403099419.jpg differ
diff --git a/output/met_1770403221810.jpg b/output/met_1770403221810.jpg
new file mode 100644
index 0000000..49e6362
Binary files /dev/null and b/output/met_1770403221810.jpg differ
diff --git a/output/met_1770403333759.jpg b/output/met_1770403333759.jpg
new file mode 100644
index 0000000..f1b8bd1
Binary files /dev/null and b/output/met_1770403333759.jpg differ
diff --git a/output/met_1770403574988.jpg b/output/met_1770403574988.jpg
new file mode 100644
index 0000000..42f80f4
Binary files /dev/null and b/output/met_1770403574988.jpg differ
diff --git a/output/met_1770403697204.jpg b/output/met_1770403697204.jpg
new file mode 100644
index 0000000..2e588ec
Binary files /dev/null and b/output/met_1770403697204.jpg differ
diff --git a/output/met_1770403816330.jpg b/output/met_1770403816330.jpg
new file mode 100644
index 0000000..22b9595
Binary files /dev/null and b/output/met_1770403816330.jpg differ
diff --git a/output/met_1770404061322.jpg b/output/met_1770404061322.jpg
new file mode 100644
index 0000000..150b52a
Binary files /dev/null and b/output/met_1770404061322.jpg differ
diff --git a/output/met_1770404180659.jpg b/output/met_1770404180659.jpg
new file mode 100644
index 0000000..546b935
Binary files /dev/null and b/output/met_1770404180659.jpg differ
diff --git a/output/met_1770404431808.jpg b/output/met_1770404431808.jpg
new file mode 100644
index 0000000..f8e800d
Binary files /dev/null and b/output/met_1770404431808.jpg differ
diff --git a/output/met_1770404432466.jpg b/output/met_1770404432466.jpg
new file mode 100644
index 0000000..7b10f0c
Binary files /dev/null and b/output/met_1770404432466.jpg differ
diff --git a/output/met_1770404543372.jpg b/output/met_1770404543372.jpg
new file mode 100644
index 0000000..86ae801
Binary files /dev/null and b/output/met_1770404543372.jpg differ
diff --git a/output/met_1770404663460.jpg b/output/met_1770404663460.jpg
new file mode 100644
index 0000000..0ed02a3
Binary files /dev/null and b/output/met_1770404663460.jpg differ
diff --git a/output/met_1770404669818.jpg b/output/met_1770404669818.jpg
new file mode 100644
index 0000000..75a7059
Binary files /dev/null and b/output/met_1770404669818.jpg differ
diff --git a/output/met_1770404911996.jpg b/output/met_1770404911996.jpg
new file mode 100644
index 0000000..77a6235
Binary files /dev/null and b/output/met_1770404911996.jpg differ
diff --git a/output/met_1770405027734.jpg b/output/met_1770405027734.jpg
new file mode 100644
index 0000000..f0a9236
Binary files /dev/null and b/output/met_1770405027734.jpg differ
diff --git a/output/met_1770405148169.jpg b/output/met_1770405148169.jpg
new file mode 100644
index 0000000..33a835f
Binary files /dev/null and b/output/met_1770405148169.jpg differ
diff --git a/output/met_1770405153396.jpg b/output/met_1770405153396.jpg
new file mode 100644
index 0000000..0f64f18
Binary files /dev/null and b/output/met_1770405153396.jpg differ
diff --git a/output/met_1770405267893.jpg b/output/met_1770405267893.jpg
new file mode 100644
index 0000000..d4bf150
Binary files /dev/null and b/output/met_1770405267893.jpg differ
diff --git a/output/met_1770405510402.jpg b/output/met_1770405510402.jpg
new file mode 100644
index 0000000..6fd1ab5
Binary files /dev/null and b/output/met_1770405510402.jpg differ
diff --git a/output/met_1770405883635.jpg b/output/met_1770405883635.jpg
new file mode 100644
index 0000000..e480a38
Binary files /dev/null and b/output/met_1770405883635.jpg differ
diff --git a/output/met_1770405998755.jpg b/output/met_1770405998755.jpg
new file mode 100644
index 0000000..51d63d4
Binary files /dev/null and b/output/met_1770405998755.jpg differ
diff --git a/output/met_1770405999008.jpg b/output/met_1770405999008.jpg
new file mode 100644
index 0000000..3c8f57d
Binary files /dev/null and b/output/met_1770405999008.jpg differ
diff --git a/output/met_1770406121993.jpg b/output/met_1770406121993.jpg
new file mode 100644
index 0000000..2876c72
Binary files /dev/null and b/output/met_1770406121993.jpg differ
diff --git a/output/metadata_1769835612798.json b/output/metadata_1769835612798.json
new file mode 100644
index 0000000..6ef7573
--- /dev/null
+++ b/output/metadata_1769835612798.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "year": "1642",
+ "description": "The Night Watch is a masterpiece created by Rembrandt van Rijn in 1642. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1769835612798",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/The_Night_Watch_-_HD.jpg/1920px-The_Night_Watch_-_HD.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1769835612798.jpg",
+ "fileName": "artwork_1769835612798.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Rembrandt van Rijn (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1769839200769.json b/output/metadata_1769839200769.json
new file mode 100644
index 0000000..04ad063
--- /dev/null
+++ b/output/metadata_1769839200769.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Arnolfini Portrait",
+ "artist": "Jan van Eyck",
+ "year": "1434",
+ "description": "The Arnolfini Portrait is a masterpiece created by Jan van Eyck in 1434. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1769839200769",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Van_Eyck_-_Arnolfini_Portrait.jpg/800px-Van_Eyck_-_Arnolfini_Portrait.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1769839200769.jpg",
+ "fileName": "artwork_1769839200769.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Jan van Eyck (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1769930397505.json b/output/metadata_1769930397505.json
new file mode 100644
index 0000000..1e13c5e
--- /dev/null
+++ b/output/metadata_1769930397505.json
@@ -0,0 +1,13 @@
+{
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "year": "1665",
+ "description": "Girl with a Pearl Earring is a masterpiece created by Johannes Vermeer in 1665. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1769930397505",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/1665_Girl_with_a_Pearl_Earring.jpg/800px-1665_Girl_with_a_Pearl_Earring.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1769930397505.jpg",
+ "fileName": "artwork_1769930397505.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Johannes Vermeer (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1769955541913.json b/output/metadata_1769955541913.json
new file mode 100644
index 0000000..bf0efdc
--- /dev/null
+++ b/output/metadata_1769955541913.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "year": "1642",
+ "description": "The Night Watch is a masterpiece created by Rembrandt van Rijn in 1642. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1769955541913",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/The_Night_Watch_-_HD.jpg/1920px-The_Night_Watch_-_HD.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1769955541913.jpg",
+ "fileName": "artwork_1769955541913.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Rembrandt van Rijn (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770011755402.json b/output/metadata_1770011755402.json
new file mode 100644
index 0000000..dfc21ca
--- /dev/null
+++ b/output/metadata_1770011755402.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "year": "1642",
+ "description": "The Night Watch is a masterpiece created by Rembrandt van Rijn in 1642. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770011755402",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/The_Night_Watch_-_HD.jpg/1920px-The_Night_Watch_-_HD.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770011755402.jpg",
+ "fileName": "artwork_1770011755402.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Rembrandt van Rijn (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770020944859.json b/output/metadata_1770020944859.json
new file mode 100644
index 0000000..5a389b8
--- /dev/null
+++ b/output/metadata_1770020944859.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "year": "1831",
+ "description": "The Great Wave off Kanagawa is a masterpiece created by Katsushika Hokusai in 1831. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770020944859",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/The_Great_Wave_off_Kanagawa.jpg/1920px-The_Great_Wave_off_Kanagawa.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770020944859.jpg",
+ "fileName": "artwork_1770020944859.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Katsushika Hokusai (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770035327832.json b/output/metadata_1770035327832.json
new file mode 100644
index 0000000..d43caf1
--- /dev/null
+++ b/output/metadata_1770035327832.json
@@ -0,0 +1,13 @@
+{
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "year": "1830",
+ "description": "Liberty Leading the People is a masterpiece created by Eugène Delacroix in 1830. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770035327832",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg/1920px-Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770035327832.jpg",
+ "fileName": "artwork_1770035327832.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Eugène Delacroix (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770044154986.json b/output/metadata_1770044154986.json
new file mode 100644
index 0000000..4ca4e04
--- /dev/null
+++ b/output/metadata_1770044154986.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Arnolfini Portrait",
+ "artist": "Jan van Eyck",
+ "year": "1434",
+ "description": "The Arnolfini Portrait is a masterpiece created by Jan van Eyck in 1434. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770044154986",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Van_Eyck_-_Arnolfini_Portrait.jpg/800px-Van_Eyck_-_Arnolfini_Portrait.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770044154986.jpg",
+ "fileName": "artwork_1770044154986.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Jan van Eyck (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770089254297.json b/output/metadata_1770089254297.json
new file mode 100644
index 0000000..a1802db
--- /dev/null
+++ b/output/metadata_1770089254297.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "year": "1642",
+ "description": "The Night Watch is a masterpiece created by Rembrandt van Rijn in 1642. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770089254297",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/The_Night_Watch_-_HD.jpg/1920px-The_Night_Watch_-_HD.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770089254297.jpg",
+ "fileName": "artwork_1770089254297.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Rembrandt van Rijn (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770098561130.json b/output/metadata_1770098561130.json
new file mode 100644
index 0000000..0163d03
--- /dev/null
+++ b/output/metadata_1770098561130.json
@@ -0,0 +1,13 @@
+{
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "year": "1665",
+ "description": "Girl with a Pearl Earring is a masterpiece created by Johannes Vermeer in 1665. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770098561130",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/1665_Girl_with_a_Pearl_Earring.jpg/800px-1665_Girl_with_a_Pearl_Earring.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770098561130.jpg",
+ "fileName": "artwork_1770098561130.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Johannes Vermeer (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770117556959.json b/output/metadata_1770117556959.json
new file mode 100644
index 0000000..5a7240b
--- /dev/null
+++ b/output/metadata_1770117556959.json
@@ -0,0 +1,13 @@
+{
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "year": "1665",
+ "description": "Girl with a Pearl Earring is a masterpiece created by Johannes Vermeer in 1665. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770117556959",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/1665_Girl_with_a_Pearl_Earring.jpg/800px-1665_Girl_with_a_Pearl_Earring.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770117556959.jpg",
+ "fileName": "artwork_1770117556959.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Johannes Vermeer (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770120000835.json b/output/metadata_1770120000835.json
new file mode 100644
index 0000000..5e4afc7
--- /dev/null
+++ b/output/metadata_1770120000835.json
@@ -0,0 +1,13 @@
+{
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "year": "1830",
+ "description": "Liberty Leading the People is a masterpiece created by Eugène Delacroix in 1830. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770120000835",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg/1920px-Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770120000835.jpg",
+ "fileName": "artwork_1770120000835.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Eugène Delacroix (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770125167080.json b/output/metadata_1770125167080.json
new file mode 100644
index 0000000..4b20fca
--- /dev/null
+++ b/output/metadata_1770125167080.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "year": "1889",
+ "description": "The Starry Night is a masterpiece created by Vincent van Gogh in 1889. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770125167080",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1920px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770125167080.jpg",
+ "fileName": "artwork_1770125167080.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Vincent van Gogh (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770195505488.json b/output/metadata_1770195505488.json
new file mode 100644
index 0000000..93aa920
--- /dev/null
+++ b/output/metadata_1770195505488.json
@@ -0,0 +1,13 @@
+{
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "year": "1656",
+ "description": "Las Meninas is a masterpiece created by Diego Velázquez in 1656. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770195505488",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Las_Meninas%2C_by_Diego_Vel%C3%A1zquez%2C_from_Prado_in_Google_Earth.jpg/1200px-Las_Meninas%2C_by_Diego_Vel%C3%A1zquez%2C_from_Prado_in_Google_Earth.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770195505488.jpg",
+ "fileName": "artwork_1770195505488.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Diego Velázquez (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770210985020.json b/output/metadata_1770210985020.json
new file mode 100644
index 0000000..22d1aad
--- /dev/null
+++ b/output/metadata_1770210985020.json
@@ -0,0 +1,13 @@
+{
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "year": "1818",
+ "description": "Wanderer above the Sea of Fog is a masterpiece created by Caspar David Friedrich in 1818. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770210985020",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg/800px-Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770210985020.jpg",
+ "fileName": "artwork_1770210985020.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Caspar David Friedrich (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770231056684.json b/output/metadata_1770231056684.json
new file mode 100644
index 0000000..5cd01b7
--- /dev/null
+++ b/output/metadata_1770231056684.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "year": "1642",
+ "description": "The Night Watch is a masterpiece created by Rembrandt van Rijn in 1642. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770231056684",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/The_Night_Watch_-_HD.jpg/1920px-The_Night_Watch_-_HD.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770231056684.jpg",
+ "fileName": "artwork_1770231056684.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Rembrandt van Rijn (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770277360291.json b/output/metadata_1770277360291.json
new file mode 100644
index 0000000..8f5fbb6
--- /dev/null
+++ b/output/metadata_1770277360291.json
@@ -0,0 +1,13 @@
+{
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "year": "1818",
+ "description": "Wanderer above the Sea of Fog is a masterpiece created by Caspar David Friedrich in 1818. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770277360291",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg/800px-Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770277360291.jpg",
+ "fileName": "artwork_1770277360291.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Caspar David Friedrich (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770288361064.json b/output/metadata_1770288361064.json
new file mode 100644
index 0000000..93e78ad
--- /dev/null
+++ b/output/metadata_1770288361064.json
@@ -0,0 +1,13 @@
+{
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "year": "1665",
+ "description": "Girl with a Pearl Earring is a masterpiece created by Johannes Vermeer in 1665. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770288361064",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/1665_Girl_with_a_Pearl_Earring.jpg/800px-1665_Girl_with_a_Pearl_Earring.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770288361064.jpg",
+ "fileName": "artwork_1770288361064.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Johannes Vermeer (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770295495989.json b/output/metadata_1770295495989.json
new file mode 100644
index 0000000..6f76a7a
--- /dev/null
+++ b/output/metadata_1770295495989.json
@@ -0,0 +1,13 @@
+{
+ "title": "Las Meninas",
+ "artist": "Diego Velázquez",
+ "year": "1656",
+ "description": "Las Meninas is a masterpiece created by Diego Velázquez in 1656. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770295495989",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Las_Meninas%2C_by_Diego_Vel%C3%A1zquez%2C_from_Prado_in_Google_Earth.jpg/1200px-Las_Meninas%2C_by_Diego_Vel%C3%A1zquez%2C_from_Prado_in_Google_Earth.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770295495989.jpg",
+ "fileName": "artwork_1770295495989.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Diego Velázquez (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770302991852.json b/output/metadata_1770302991852.json
new file mode 100644
index 0000000..1e6de7a
--- /dev/null
+++ b/output/metadata_1770302991852.json
@@ -0,0 +1,13 @@
+{
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "year": "1665",
+ "description": "Girl with a Pearl Earring is a masterpiece created by Johannes Vermeer in 1665. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770302991852",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/1665_Girl_with_a_Pearl_Earring.jpg/800px-1665_Girl_with_a_Pearl_Earring.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770302991852.jpg",
+ "fileName": "artwork_1770302991852.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Johannes Vermeer (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770323423731.json b/output/metadata_1770323423731.json
new file mode 100644
index 0000000..0eb00d8
--- /dev/null
+++ b/output/metadata_1770323423731.json
@@ -0,0 +1,13 @@
+{
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "year": "1665",
+ "description": "Girl with a Pearl Earring is a masterpiece created by Johannes Vermeer in 1665. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770323423731",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/1665_Girl_with_a_Pearl_Earring.jpg/800px-1665_Girl_with_a_Pearl_Earring.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770323423731.jpg",
+ "fileName": "artwork_1770323423731.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Johannes Vermeer (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770326084840.json b/output/metadata_1770326084840.json
new file mode 100644
index 0000000..968f478
--- /dev/null
+++ b/output/metadata_1770326084840.json
@@ -0,0 +1,13 @@
+{
+ "title": "Sunflowers",
+ "artist": "Vincent van Gogh",
+ "year": "1888",
+ "description": "Sunflowers is a masterpiece created by Vincent van Gogh in 1888. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770326084840",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Vincent_Willem_van_Gogh_127.jpg/800px-Vincent_Willem_van_Gogh_127.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770326084840.jpg",
+ "fileName": "artwork_1770326084840.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Vincent van Gogh (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770336000687.json b/output/metadata_1770336000687.json
new file mode 100644
index 0000000..d227141
--- /dev/null
+++ b/output/metadata_1770336000687.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "year": "1889",
+ "description": "The Starry Night is a masterpiece created by Vincent van Gogh in 1889. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770336000687",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1920px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770336000687.jpg",
+ "fileName": "artwork_1770336000687.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Vincent van Gogh (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770339021052.json b/output/metadata_1770339021052.json
new file mode 100644
index 0000000..64184df
--- /dev/null
+++ b/output/metadata_1770339021052.json
@@ -0,0 +1,13 @@
+{
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "year": "1665",
+ "description": "Girl with a Pearl Earring is a masterpiece created by Johannes Vermeer in 1665. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770339021052",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/1665_Girl_with_a_Pearl_Earring.jpg/800px-1665_Girl_with_a_Pearl_Earring.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770339021052.jpg",
+ "fileName": "artwork_1770339021052.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Johannes Vermeer (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770362717875.json b/output/metadata_1770362717875.json
new file mode 100644
index 0000000..0efdb16
--- /dev/null
+++ b/output/metadata_1770362717875.json
@@ -0,0 +1,13 @@
+{
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "year": "1818",
+ "description": "Wanderer above the Sea of Fog is a masterpiece created by Caspar David Friedrich in 1818. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770362717875",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg/800px-Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770362717875.jpg",
+ "fileName": "artwork_1770362717875.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Caspar David Friedrich (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770362957605.json b/output/metadata_1770362957605.json
new file mode 100644
index 0000000..551b6a4
--- /dev/null
+++ b/output/metadata_1770362957605.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Garden of Earthly Delights",
+ "artist": "Hieronymus Bosch",
+ "year": "1515",
+ "description": "The Garden of Earthly Delights is a masterpiece created by Hieronymus Bosch in 1515. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770362957605",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/El_jard%C3%ADn_de_las_Delicias%2C_de_El_Bosco.jpg/1920px-El_jard%C3%ADn_de_las_Delicias%2C_de_El_Bosco.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770362957605.jpg",
+ "fileName": "artwork_1770362957605.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Hieronymus Bosch (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770363320841.json b/output/metadata_1770363320841.json
new file mode 100644
index 0000000..c26221e
--- /dev/null
+++ b/output/metadata_1770363320841.json
@@ -0,0 +1,13 @@
+{
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "year": "1830",
+ "description": "Liberty Leading the People is a masterpiece created by Eugène Delacroix in 1830. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770363320841",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg/1920px-Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770363320841.jpg",
+ "fileName": "artwork_1770363320841.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Eugène Delacroix (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770363442632.json b/output/metadata_1770363442632.json
new file mode 100644
index 0000000..a8897ed
--- /dev/null
+++ b/output/metadata_1770363442632.json
@@ -0,0 +1,13 @@
+{
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "year": "1818",
+ "description": "Wanderer above the Sea of Fog is a masterpiece created by Caspar David Friedrich in 1818. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770363442632",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg/800px-Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770363442632.jpg",
+ "fileName": "artwork_1770363442632.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Caspar David Friedrich (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770363687872.json b/output/metadata_1770363687872.json
new file mode 100644
index 0000000..363397a
--- /dev/null
+++ b/output/metadata_1770363687872.json
@@ -0,0 +1,13 @@
+{
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "year": "1830",
+ "description": "Liberty Leading the People is a masterpiece created by Eugène Delacroix in 1830. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770363687872",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg/1920px-Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770363687872.jpg",
+ "fileName": "artwork_1770363687872.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Eugène Delacroix (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770365013472.json b/output/metadata_1770365013472.json
new file mode 100644
index 0000000..f41b846
--- /dev/null
+++ b/output/metadata_1770365013472.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "year": "1831",
+ "description": "The Great Wave off Kanagawa is a masterpiece created by Katsushika Hokusai in 1831. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770365013472",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/The_Great_Wave_off_Kanagawa.jpg/1920px-The_Great_Wave_off_Kanagawa.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770365013472.jpg",
+ "fileName": "artwork_1770365013472.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Katsushika Hokusai (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770365135248.json b/output/metadata_1770365135248.json
new file mode 100644
index 0000000..c3365d9
--- /dev/null
+++ b/output/metadata_1770365135248.json
@@ -0,0 +1,13 @@
+{
+ "title": "Girl with a Pearl Earring",
+ "artist": "Johannes Vermeer",
+ "year": "1665",
+ "description": "Girl with a Pearl Earring is a masterpiece created by Johannes Vermeer in 1665. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770365135248",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/1665_Girl_with_a_Pearl_Earring.jpg/800px-1665_Girl_with_a_Pearl_Earring.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770365135248.jpg",
+ "fileName": "artwork_1770365135248.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Johannes Vermeer (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770365256192.json b/output/metadata_1770365256192.json
new file mode 100644
index 0000000..d3476d9
--- /dev/null
+++ b/output/metadata_1770365256192.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "year": "1831",
+ "description": "The Great Wave off Kanagawa is a masterpiece created by Katsushika Hokusai in 1831. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770365256192",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/The_Great_Wave_off_Kanagawa.jpg/1920px-The_Great_Wave_off_Kanagawa.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770365256192.jpg",
+ "fileName": "artwork_1770365256192.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Katsushika Hokusai (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770379201423.json b/output/metadata_1770379201423.json
new file mode 100644
index 0000000..36c737b
--- /dev/null
+++ b/output/metadata_1770379201423.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Garden of Earthly Delights",
+ "artist": "Hieronymus Bosch",
+ "year": "1515",
+ "description": "The Garden of Earthly Delights is a masterpiece created by Hieronymus Bosch in 1515. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770379201423",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/El_jard%C3%ADn_de_las_Delicias%2C_de_El_Bosco.jpg/1920px-El_jard%C3%ADn_de_las_Delicias%2C_de_El_Bosco.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770379201423.jpg",
+ "fileName": "artwork_1770379201423.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Hieronymus Bosch (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770400804167.json b/output/metadata_1770400804167.json
new file mode 100644
index 0000000..f9040e1
--- /dev/null
+++ b/output/metadata_1770400804167.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "year": "1889",
+ "description": "The Starry Night is a masterpiece created by Vincent van Gogh in 1889. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770400804167",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1920px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770400804167.jpg",
+ "fileName": "artwork_1770400804167.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Vincent van Gogh (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770403453727.json b/output/metadata_1770403453727.json
new file mode 100644
index 0000000..9c72330
--- /dev/null
+++ b/output/metadata_1770403453727.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Night Watch",
+ "artist": "Rembrandt van Rijn",
+ "year": "1642",
+ "description": "The Night Watch is a masterpiece created by Rembrandt van Rijn in 1642. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770403453727",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/The_Night_Watch_-_HD.jpg/1920px-The_Night_Watch_-_HD.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770403453727.jpg",
+ "fileName": "artwork_1770403453727.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Rembrandt van Rijn (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770403936465.json b/output/metadata_1770403936465.json
new file mode 100644
index 0000000..e9d59ed
--- /dev/null
+++ b/output/metadata_1770403936465.json
@@ -0,0 +1,13 @@
+{
+ "title": "Liberty Leading the People",
+ "artist": "Eugène Delacroix",
+ "year": "1830",
+ "description": "Liberty Leading the People is a masterpiece created by Eugène Delacroix in 1830. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770403936465",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg/1920px-Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770403936465.jpg",
+ "fileName": "artwork_1770403936465.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Eugène Delacroix (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770404299077.json b/output/metadata_1770404299077.json
new file mode 100644
index 0000000..cd1854f
--- /dev/null
+++ b/output/metadata_1770404299077.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Great Wave off Kanagawa",
+ "artist": "Katsushika Hokusai",
+ "year": "1831",
+ "description": "The Great Wave off Kanagawa is a masterpiece created by Katsushika Hokusai in 1831. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770404299077",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/The_Great_Wave_off_Kanagawa.jpg/1920px-The_Great_Wave_off_Kanagawa.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770404299077.jpg",
+ "fileName": "artwork_1770404299077.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Katsushika Hokusai (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770404783572.json b/output/metadata_1770404783572.json
new file mode 100644
index 0000000..c3ccb6a
--- /dev/null
+++ b/output/metadata_1770404783572.json
@@ -0,0 +1,13 @@
+{
+ "title": "Wanderer above the Sea of Fog",
+ "artist": "Caspar David Friedrich",
+ "year": "1818",
+ "description": "Wanderer above the Sea of Fog is a masterpiece created by Caspar David Friedrich in 1818. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770404783572",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg/800px-Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770404783572.jpg",
+ "fileName": "artwork_1770404783572.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Caspar David Friedrich (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770405388375.json b/output/metadata_1770405388375.json
new file mode 100644
index 0000000..c0eb05f
--- /dev/null
+++ b/output/metadata_1770405388375.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "year": "1889",
+ "description": "The Starry Night is a masterpiece created by Vincent van Gogh in 1889. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770405388375",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1920px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770405388375.jpg",
+ "fileName": "artwork_1770405388375.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Vincent van Gogh (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770405629028.json b/output/metadata_1770405629028.json
new file mode 100644
index 0000000..487c5a5
--- /dev/null
+++ b/output/metadata_1770405629028.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Garden of Earthly Delights",
+ "artist": "Hieronymus Bosch",
+ "year": "1515",
+ "description": "The Garden of Earthly Delights is a masterpiece created by Hieronymus Bosch in 1515. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770405629028",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/El_jard%C3%ADn_de_las_Delicias%2C_de_El_Bosco.jpg/1920px-El_jard%C3%ADn_de_las_Delicias%2C_de_El_Bosco.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770405629028.jpg",
+ "fileName": "artwork_1770405629028.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Hieronymus Bosch (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/metadata_1770405750351.json b/output/metadata_1770405750351.json
new file mode 100644
index 0000000..d19faf1
--- /dev/null
+++ b/output/metadata_1770405750351.json
@@ -0,0 +1,13 @@
+{
+ "title": "The Starry Night",
+ "artist": "Vincent van Gogh",
+ "year": "1889",
+ "description": "The Starry Night is a masterpiece created by Vincent van Gogh in 1889. This artwork is now in the public domain and represents a significant contribution to art history.",
+ "objectNumber": "PD-1770405750351",
+ "imageUrl": "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1920px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg",
+ "localPath": "/root/Projects/animate-museum-posts/output/artwork_1770405750351.jpg",
+ "fileName": "artwork_1770405750351.jpg",
+ "museum": "Public Domain Collection",
+ "creditLine": "Vincent van Gogh (Public Domain)",
+ "dimensions": []
+}
\ No newline at end of file
diff --git a/output/thumbnails/thumb_1768128218436.jpg b/output/thumbnails/thumb_1768128218436.jpg
new file mode 100644
index 0000000..f159767
Binary files /dev/null and b/output/thumbnails/thumb_1768128218436.jpg differ
diff --git a/output/thumbnails/thumb_1768128310140.jpg b/output/thumbnails/thumb_1768128310140.jpg
new file mode 100644
index 0000000..f159767
Binary files /dev/null and b/output/thumbnails/thumb_1768128310140.jpg differ
diff --git a/output/thumbnails/thumb_1768144317138.jpg b/output/thumbnails/thumb_1768144317138.jpg
new file mode 100644
index 0000000..ee02e0e
Binary files /dev/null and b/output/thumbnails/thumb_1768144317138.jpg differ
diff --git a/output/thumbnails/thumb_1768144518253.jpg b/output/thumbnails/thumb_1768144518253.jpg
new file mode 100644
index 0000000..72ab085
Binary files /dev/null and b/output/thumbnails/thumb_1768144518253.jpg differ
diff --git a/output/thumbnails/thumb_1768144732341.jpg b/output/thumbnails/thumb_1768144732341.jpg
new file mode 100644
index 0000000..f777530
Binary files /dev/null and b/output/thumbnails/thumb_1768144732341.jpg differ
diff --git a/output/thumbnails/thumb_1768144934368.jpg b/output/thumbnails/thumb_1768144934368.jpg
new file mode 100644
index 0000000..ced1910
Binary files /dev/null and b/output/thumbnails/thumb_1768144934368.jpg differ
diff --git a/output/thumbnails/thumb_1768145120280.jpg b/output/thumbnails/thumb_1768145120280.jpg
new file mode 100644
index 0000000..474a6b7
Binary files /dev/null and b/output/thumbnails/thumb_1768145120280.jpg differ
diff --git a/output/thumbnails/thumb_1768145314449.jpg b/output/thumbnails/thumb_1768145314449.jpg
new file mode 100644
index 0000000..566d2d4
Binary files /dev/null and b/output/thumbnails/thumb_1768145314449.jpg differ
diff --git a/output/thumbnails/thumb_1768152020572.jpg b/output/thumbnails/thumb_1768152020572.jpg
new file mode 100644
index 0000000..bf60ac6
Binary files /dev/null and b/output/thumbnails/thumb_1768152020572.jpg differ
diff --git a/output/thumbnails/thumb_1768152215875.jpg b/output/thumbnails/thumb_1768152215875.jpg
new file mode 100644
index 0000000..9b31a9e
Binary files /dev/null and b/output/thumbnails/thumb_1768152215875.jpg differ
diff --git a/output/thumbnails/thumb_1768152401849.jpg b/output/thumbnails/thumb_1768152401849.jpg
new file mode 100644
index 0000000..17648b3
Binary files /dev/null and b/output/thumbnails/thumb_1768152401849.jpg differ
diff --git a/output/thumbnails/thumb_1768152606874.jpg b/output/thumbnails/thumb_1768152606874.jpg
new file mode 100644
index 0000000..1a8f52c
Binary files /dev/null and b/output/thumbnails/thumb_1768152606874.jpg differ
diff --git a/output/thumbnails/thumb_1768152801379.jpg b/output/thumbnails/thumb_1768152801379.jpg
new file mode 100644
index 0000000..7ab32f9
Binary files /dev/null and b/output/thumbnails/thumb_1768152801379.jpg differ
diff --git a/output/thumbnails/thumb_1768153001502.jpg b/output/thumbnails/thumb_1768153001502.jpg
new file mode 100644
index 0000000..178cbf2
Binary files /dev/null and b/output/thumbnails/thumb_1768153001502.jpg differ
diff --git a/output/thumbnails/thumb_1768153191039.jpg b/output/thumbnails/thumb_1768153191039.jpg
new file mode 100644
index 0000000..09f2a74
Binary files /dev/null and b/output/thumbnails/thumb_1768153191039.jpg differ
diff --git a/output/thumbnails/thumb_1768153374417.jpg b/output/thumbnails/thumb_1768153374417.jpg
new file mode 100644
index 0000000..70042f8
Binary files /dev/null and b/output/thumbnails/thumb_1768153374417.jpg differ
diff --git a/output/thumbnails/thumb_1768153551861.jpg b/output/thumbnails/thumb_1768153551861.jpg
new file mode 100644
index 0000000..eb08344
Binary files /dev/null and b/output/thumbnails/thumb_1768153551861.jpg differ
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..d45ab76
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2098 @@
+{
+ "name": "animate-museum-posts",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "animate-museum-posts",
+ "version": "1.0.0",
+ "dependencies": {
+ "@huggingface/inference": "^4.8.0",
+ "@runwayml/sdk": "^2.11.0",
+ "axios": "^1.6.0",
+ "dotenv": "^16.3.1",
+ "express": "^5.2.1",
+ "form-data": "^4.0.0",
+ "gif-encoder-2": "^1.0.5",
+ "helmet": "^8.1.0",
+ "node-cron": "^3.0.3",
+ "nodemailer": "^7.0.6",
+ "pg": "^8.16.3",
+ "replicate": "^1.2.0",
+ "sharp": "^0.33.0",
+ "twitter-api-v2": "^1.15.2"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
+ "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@huggingface/inference": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-4.8.0.tgz",
+ "integrity": "sha512-Eq98EAXqYn4rKMfrbEXuhc3IjKfaeIO6eXNOZk9xk6v5akrIWRtd6d1h0fjAWyX4zRbdUpXRh6MvsqXnzGvXCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@huggingface/jinja": "^0.5.1",
+ "@huggingface/tasks": "^0.19.45"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@huggingface/jinja": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.5.1.tgz",
+ "integrity": "sha512-yUZLld4lrM9iFxHCwFQ7D1HW2MWMwSbeB7WzWqFYDWK+rEb+WldkLdAJxUPOmgICMHZLzZGVcVjFh3w/YGubng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@huggingface/tasks": {
+ "version": "0.19.48",
+ "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.19.48.tgz",
+ "integrity": "sha512-9OIMhl6lSvuim0EisMPbwO4fUmMlbAXh5/GIMVAQALoByouockaaDxMuzJ9IffHRsl3qjS+QSD/mPe65kuPeyQ==",
+ "license": "MIT"
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
+ "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
+ "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
+ "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.5"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
+ "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
+ "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
+ "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
+ "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.2.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
+ "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@runwayml/sdk": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@runwayml/sdk/-/sdk-2.11.0.tgz",
+ "integrity": "sha512-Pn69srRbWUNQCpgCglVuWckl2+ZiIxfXwUbdms1AhYY/lJDL5yZozB4GrJEL30Gmbk7gm0gzFJ/Fa0TWRQMr+g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/node": "^18.11.18",
+ "@types/node-fetch": "^2.6.4",
+ "abort-controller": "^3.0.0",
+ "agentkeepalive": "^4.2.1",
+ "form-data-encoder": "1.7.2",
+ "formdata-node": "^4.3.2",
+ "node-fetch": "^2.6.7"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "18.19.127",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.127.tgz",
+ "integrity": "sha512-gSjxjrnKXML/yo0BO099uPixMqfpJU0TKYjpfLU7TrtA2WWDki412Np/RSTPRil1saKBhvVVKzVx/p/6p94nVA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
+ },
+ "node_modules/@types/node-fetch": {
+ "version": "2.6.13",
+ "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+ "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "form-data": "^4.0.4"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "^3.0.0",
+ "negotiator": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/accepts/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/agentkeepalive": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+ "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "humanize-ms": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 8.0.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz",
+ "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/body-parser": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "^3.1.2",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.3",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
+ "on-finished": "^2.4.1",
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/content-disposition": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.6.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz",
+ "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dotenv": {
+ "version": "16.6.1",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
+ "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
+ "license": "MIT"
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/express": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "^2.0.0",
+ "body-parser": "^2.2.1",
+ "content-disposition": "^1.0.0",
+ "content-type": "^1.0.5",
+ "cookie": "^0.7.1",
+ "cookie-signature": "^1.2.1",
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "finalhandler": "^2.1.0",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.0",
+ "merge-descriptors": "^2.0.0",
+ "mime-types": "^3.0.0",
+ "on-finished": "^2.4.1",
+ "once": "^1.4.0",
+ "parseurl": "^1.3.3",
+ "proxy-addr": "^2.0.7",
+ "qs": "^6.14.0",
+ "range-parser": "^1.2.1",
+ "router": "^2.2.0",
+ "send": "^1.1.0",
+ "serve-static": "^2.2.0",
+ "statuses": "^2.0.1",
+ "type-is": "^2.0.1",
+ "vary": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/express/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "on-finished": "^2.4.1",
+ "parseurl": "^1.3.3",
+ "statuses": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/form-data-encoder": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+ "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+ "license": "MIT"
+ },
+ "node_modules/formdata-node": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+ "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+ "license": "MIT",
+ "dependencies": {
+ "node-domexception": "1.0.0",
+ "web-streams-polyfill": "4.0.0-beta.3"
+ },
+ "engines": {
+ "node": ">= 12.20"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gif-encoder-2": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/gif-encoder-2/-/gif-encoder-2-1.0.5.tgz",
+ "integrity": "sha512-fsRAKbZuUoZ7FYGjpFElmflTkKwsn/CzAmL/xDl4558aTAgysIDCUF6AXWO8dmai/ApfZACbPVAM+vPezJXlFg==",
+ "license": "UNLICENSE"
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/helmet": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
+ "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
+ "license": "MIT",
+ "dependencies": {
+ "depd": "~2.0.0",
+ "inherits": "~2.0.4",
+ "setprototypeof": "~1.2.0",
+ "statuses": "~2.0.2",
+ "toidentifier": "~1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/humanize-ms": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.0.0"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause",
+ "optional": true
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
+ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
+ "license": "MIT"
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "license": "MIT"
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-cron": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
+ "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
+ "license": "ISC",
+ "dependencies": {
+ "uuid": "8.3.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/node-domexception": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+ "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+ "deprecated": "Use your platform's native DOMException instead",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/jimmywarting"
+ },
+ {
+ "type": "github",
+ "url": "https://paypal.me/jimmywarting"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.5.0"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/nodemailer": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.6.tgz",
+ "integrity": "sha512-F44uVzgwo49xboqbFgBGkRaiMgtoBrBEWCVincJPK9+S9Adkzt/wXCLKbf7dxucmxfTI5gHGB+bEmdyzN6QKjw==",
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "license": "MIT",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "license": "ISC",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/pg": {
+ "version": "8.16.3",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
+ "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.9.1",
+ "pg-pool": "^3.10.1",
+ "pg-protocol": "^1.10.3",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.2.7"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
+ "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.9.1",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
+ "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
+ "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
+ "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "license": "MIT",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.14.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "~3.1.2",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.7.0",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/replicate": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/replicate/-/replicate-1.2.0.tgz",
+ "integrity": "sha512-11A1LEP0jib+H2ISWYvW12Tg43Fs7HHZPvJvA9Naqr7/REwCWlExP6MWJHZBKe5WVs9iiFV/VYhCEcFUEIFgeg==",
+ "license": "Apache-2.0",
+ "engines": {
+ "git": ">=2.11.0",
+ "node": ">=18.0.0",
+ "npm": ">=7.19.0",
+ "yarn": ">=1.7.0"
+ },
+ "optionalDependencies": {
+ "readable-stream": ">=4.0.0"
+ }
+ },
+ "node_modules/router": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "depd": "^2.0.0",
+ "is-promise": "^4.0.0",
+ "parseurl": "^1.3.3",
+ "path-to-regexp": "^8.0.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "etag": "^1.8.1",
+ "fresh": "^2.0.0",
+ "http-errors": "^2.0.1",
+ "mime-types": "^3.0.2",
+ "ms": "^2.1.3",
+ "on-finished": "^2.4.1",
+ "range-parser": "^1.2.1",
+ "statuses": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/send/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/send/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/serve-static": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "license": "MIT",
+ "dependencies": {
+ "encodeurl": "^2.0.0",
+ "escape-html": "^1.0.3",
+ "parseurl": "^1.3.3",
+ "send": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
+ "license": "ISC"
+ },
+ "node_modules/sharp": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
+ "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.3",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.33.5",
+ "@img/sharp-darwin-x64": "0.33.5",
+ "@img/sharp-libvips-darwin-arm64": "1.0.4",
+ "@img/sharp-libvips-darwin-x64": "1.0.4",
+ "@img/sharp-libvips-linux-arm": "1.0.5",
+ "@img/sharp-libvips-linux-arm64": "1.0.4",
+ "@img/sharp-libvips-linux-s390x": "1.0.4",
+ "@img/sharp-libvips-linux-x64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
+ "@img/sharp-linux-arm": "0.33.5",
+ "@img/sharp-linux-arm64": "0.33.5",
+ "@img/sharp-linux-s390x": "0.33.5",
+ "@img/sharp-linux-x64": "0.33.5",
+ "@img/sharp-linuxmusl-arm64": "0.33.5",
+ "@img/sharp-linuxmusl-x64": "0.33.5",
+ "@img/sharp-wasm32": "0.33.5",
+ "@img/sharp-win32-ia32": "0.33.5",
+ "@img/sharp-win32-x64": "0.33.5"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
+ "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/statuses": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "license": "MIT"
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/twitter-api-v2": {
+ "version": "1.27.0",
+ "resolved": "https://registry.npmjs.org/twitter-api-v2/-/twitter-api-v2-1.27.0.tgz",
+ "integrity": "sha512-hbIFwzg0NeOcFOdmJqtKMCXjLjc0INff/7NwhnZ2zpnw65oku8i+0eMxo5M0iTc1hs+inD/IpDw3S0Xh2c45QQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/type-is": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "media-typer": "^1.1.0",
+ "mime-types": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-db": {
+ "version": "1.54.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/type-is/node_modules/mime-types": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "^1.54.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "license": "MIT"
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/web-streams-polyfill": {
+ "version": "4.0.0-beta.3",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+ "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "license": "ISC"
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..2a53098
--- /dev/null
+++ b/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "animate-museum-posts",
+ "version": "1.0.0",
+ "description": "Automated museum art animation and posting bot",
+ "main": "index.js",
+ "type": "module",
+ "scripts": {
+ "start": "node src/index.js",
+ "fetch": "node src/fetchArtwork.js",
+ "post": "node src/postToX.js"
+ },
+ "dependencies": {
+ "@huggingface/inference": "^4.8.0",
+ "@runwayml/sdk": "^2.11.0",
+ "axios": "^1.6.0",
+ "dotenv": "^16.3.1",
+ "express": "^5.2.1",
+ "form-data": "^4.0.0",
+ "gif-encoder-2": "^1.0.5",
+ "helmet": "^8.1.0",
+ "node-cron": "^3.0.3",
+ "nodemailer": "^7.0.6",
+ "pg": "^8.16.3",
+ "replicate": "^1.2.0",
+ "sharp": "^0.33.0",
+ "twitter-api-v2": "^1.15.2"
+ }
+}
diff --git a/public/callback.html b/public/callback.html
new file mode 100644
index 0000000..d1d3e6e
--- /dev/null
+++ b/public/callback.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Museum Art Bot - Authentication Callback</title>
+ <style>
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 100vh;
+ margin: 0;
+ }
+ .container {
+ text-align: center;
+ padding: 2rem;
+ background: rgba(255,255,255,0.1);
+ border-radius: 10px;
+ backdrop-filter: blur(10px);
+ }
+ h1 { margin-bottom: 1rem; }
+ .success { color: #4ade80; }
+ .error { color: #f87171; }
+ </style>
+</head>
+<body>
+ <div class="container">
+ <h1>🎨 Museum Art Bot</h1>
+ <div id="status">
+ <h2 class="success">✓ Authentication Successful!</h2>
+ <p>You have successfully authorized the Museum Art Bot.</p>
+ <p>You can close this window and return to your application.</p>
+ </div>
+ <script>
+ // Handle OAuth callback
+ const params = new URLSearchParams(window.location.search);
+ if (params.get('error')) {
+ document.getElementById('status').innerHTML = `
+ <h2 class="error">✗ Authentication Failed</h2>
+ <p>Error: ${params.get('error_description') || params.get('error')}</p>
+ `;
+ }
+ // Log tokens for manual configuration if needed
+ if (params.get('oauth_token')) {
+ console.log('OAuth Token received:', params.get('oauth_token'));
+ }
+ </script>
+ </div>
+</body>
+</html>
\ No newline at end of file
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..0bf53be
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
+<rect width="32" height="32" rx="6" fill="#10b981"/>
+<text x="50%" y="55%" text-anchor="middle" dominant-baseline="middle" font-size="20" font-family="Apple Color Emoji, Segoe UI Emoji, sans-serif" fill="white">A</text>
+</svg>
\ No newline at end of file
diff --git a/public/index.html b/public/index.html
new file mode 100644
index 0000000..f6fdb22
--- /dev/null
+++ b/public/index.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Museum Art Bot - Bringing Classical Art to X</title>
+ <style>
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #333;
+ margin: 0;
+ padding: 0;
+ }
+ header {
+ background: rgba(255,255,255,0.95);
+ padding: 2rem;
+ text-align: center;
+ box-shadow: 0 2px 20px rgba(0,0,0,0.1);
+ }
+ h1 {
+ margin: 0;
+ color: #667eea;
+ font-size: 2.5rem;
+ }
+ .tagline {
+ color: #666;
+ margin-top: 0.5rem;
+ }
+ .container {
+ max-width: 800px;
+ margin: 2rem auto;
+ padding: 2rem;
+ background: white;
+ border-radius: 10px;
+ box-shadow: 0 10px 40px rgba(0,0,0,0.1);
+ }
+ .features {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 2rem;
+ margin: 2rem 0;
+ }
+ .feature {
+ text-align: center;
+ }
+ .feature-icon {
+ font-size: 3rem;
+ margin-bottom: 1rem;
+ }
+ .cta {
+ background: #667eea;
+ color: white;
+ padding: 1rem 2rem;
+ border-radius: 50px;
+ text-decoration: none;
+ display: inline-block;
+ margin: 1rem;
+ transition: transform 0.2s;
+ }
+ .cta:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 5px 20px rgba(102,126,234,0.4);
+ }
+ .gallery {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 1rem;
+ margin: 2rem 0;
+ }
+ .artwork {
+ aspect-ratio: 1;
+ background: #f3f4f6;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 2rem;
+ }
+ footer {
+ text-align: center;
+ padding: 2rem;
+ color: white;
+ }
+ a { color: #667eea; }
+ footer a { color: white; }
+ </style>
+</head>
+<body>
+ <header>
+ <h1>🎨 Museum Art Bot</h1>
+ <p class="tagline">Bringing timeless masterpieces to your X timeline</p>
+ </header>
+
+ <div class="container">
+ <h2>About</h2>
+ <p>Museum Art Bot is an automated service that shares beautiful public domain artwork from world-renowned museums and artists on X (formerly Twitter). Every few hours, we post a carefully selected masterpiece with historical context and artist information.</p>
+
+ <div class="features">
+ <div class="feature">
+ <div class="feature-icon">🖼️</div>
+ <h3>Classic Art</h3>
+ <p>Masterpieces from Rembrandt, Van Gogh, Vermeer, and more</p>
+ </div>
+ <div class="feature">
+ <div class="feature-icon">🏛️</div>
+ <h3>Public Domain</h3>
+ <p>All artwork is in the public domain and free to share</p>
+ </div>
+ <div class="feature">
+ <div class="feature-icon">🤖</div>
+ <h3>Automated</h3>
+ <p>Posts every 6 hours with artwork details and context</p>
+ </div>
+ </div>
+
+ <h2>Featured Artists</h2>
+ <div class="gallery">
+ <div class="artwork">🌌</div>
+ <div class="artwork">👤</div>
+ <div class="artwork">🌻</div>
+ <div class="artwork">🌊</div>
+ <div class="artwork">🎭</div>
+ <div class="artwork">🏛️</div>
+ </div>
+
+ <h2>Follow Us</h2>
+ <p>Follow <strong>@goodquestionai</strong> on X to get daily doses of classical art in your timeline!</p>
+
+ <div style="text-align: center;">
+ <a href="https://x.com/goodquestionai" class="cta">Follow on X</a>
+ </div>
+
+ <h2>Open Source</h2>
+ <p>This bot is open source! Check out the code, contribute, or create your own art bot.</p>
+ <p><a href="https://github.com/yourusername/museum-art-bot">View on GitHub</a></p>
+ </div>
+
+ <footer>
+ <p>© 2025 Museum Art Bot | <a href="/terms.html">Terms</a> | <a href="/privacy.html">Privacy</a></p>
+ <p>Not affiliated with any museum. All artwork is public domain.</p>
+ </footer>
+</body>
+</html>
\ No newline at end of file
diff --git a/public/privacy.html b/public/privacy.html
new file mode 100644
index 0000000..53944a5
--- /dev/null
+++ b/public/privacy.html
@@ -0,0 +1,113 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Privacy Policy - Museum Art Bot</title>
+ <style>
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ line-height: 1.6;
+ color: #333;
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 2rem;
+ background: #f9fafb;
+ }
+ h1 { color: #667eea; }
+ h2 { color: #764ba2; margin-top: 2rem; }
+ .date { color: #666; font-style: italic; }
+ .highlight {
+ background: #fef3c7;
+ padding: 1rem;
+ border-radius: 8px;
+ margin: 1rem 0;
+ }
+ a { color: #667eea; }
+ </style>
+</head>
+<body>
+ <h1>Privacy Policy</h1>
+ <p class="date">Last updated: September 2025</p>
+
+ <div class="highlight">
+ <strong>TL;DR:</strong> Museum Art Bot doesn't collect, store, or process any personal data. We only post public domain artwork to X.
+ </div>
+
+ <h2>1. Information We Don't Collect</h2>
+ <p>Museum Art Bot is designed with privacy in mind. We do NOT collect:</p>
+ <ul>
+ <li>Personal information (names, emails, addresses)</li>
+ <li>Browsing data or cookies</li>
+ <li>Location information</li>
+ <li>Device information</li>
+ <li>User interactions or analytics</li>
+ </ul>
+
+ <h2>2. OAuth and X Authentication</h2>
+ <p>When you authorize Museum Art Bot:</p>
+ <ul>
+ <li>Authentication is handled entirely by X (Twitter)</li>
+ <li>We receive only the permissions needed to post content</li>
+ <li>Your credentials are never stored by our service</li>
+ <li>You can revoke access anytime via your X account settings</li>
+ </ul>
+
+ <h2>3. What the Bot Does</h2>
+ <p>Museum Art Bot only:</p>
+ <ul>
+ <li>Posts public domain artwork to the @goodquestionai account</li>
+ <li>Includes educational information about the artwork</li>
+ <li>Operates on a scheduled basis (every 6 hours)</li>
+ </ul>
+
+ <h2>4. Data Storage</h2>
+ <p>The only data we store:</p>
+ <ul>
+ <li>Temporary image files of public domain artwork (deleted after 7 days)</li>
+ <li>Basic logs of posted content for debugging (no personal data)</li>
+ <li>Configuration files for bot operation</li>
+ </ul>
+
+ <h2>5. Third-Party Services</h2>
+ <p>Museum Art Bot interacts with:</p>
+ <ul>
+ <li><strong>X (Twitter):</strong> For posting content (see X's privacy policy)</li>
+ <li><strong>Wikimedia Commons:</strong> For sourcing public domain images</li>
+ <li><strong>Public museum APIs:</strong> For artwork information (when available)</li>
+ </ul>
+
+ <h2>6. Children's Privacy</h2>
+ <p>Museum Art Bot does not knowingly collect information from children under 13. The service is intended for general audiences interested in art.</p>
+
+ <h2>7. Data Security</h2>
+ <p>Since we don't collect personal data, there's no personal information to secure. The bot operates with minimal permissions and only posts public content.</p>
+
+ <h2>8. Your Rights</h2>
+ <p>You have the right to:</p>
+ <ul>
+ <li>Revoke bot access via X account settings</li>
+ <li>Request information about our practices</li>
+ <li>Report concerns about the service</li>
+ </ul>
+
+ <h2>9. International Users</h2>
+ <p>Museum Art Bot is accessible globally. We don't collect data that would be subject to GDPR, CCPA, or other privacy regulations.</p>
+
+ <h2>10. Changes to Privacy Policy</h2>
+ <p>If we update this policy, we'll post changes here with a new "Last updated" date.</p>
+
+ <h2>11. Open Source</h2>
+ <p>Museum Art Bot is open source. You can review our code to verify our privacy practices.</p>
+
+ <h2>12. Contact</h2>
+ <p>For privacy questions or concerns:</p>
+ <ul>
+ <li>X: @goodquestionai</li>
+ <li>GitHub: Issues section of our repository</li>
+ </ul>
+
+ <hr>
+ <p><a href="/index.html">← Back to Home</a> | <a href="/terms.html">Terms of Service</a></p>
+</body>
+</html>
\ No newline at end of file
diff --git a/public/terms.html b/public/terms.html
new file mode 100644
index 0000000..7029633
--- /dev/null
+++ b/public/terms.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Terms of Service - Museum Art Bot</title>
+ <style>
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ line-height: 1.6;
+ color: #333;
+ max-width: 800px;
+ margin: 0 auto;
+ padding: 2rem;
+ background: #f9fafb;
+ }
+ h1 { color: #667eea; }
+ h2 { color: #764ba2; margin-top: 2rem; }
+ .date { color: #666; font-style: italic; }
+ a { color: #667eea; }
+ </style>
+</head>
+<body>
+ <h1>Terms of Service</h1>
+ <p class="date">Last updated: September 2025</p>
+
+ <h2>1. Acceptance of Terms</h2>
+ <p>By using Museum Art Bot ("the Service"), you agree to these Terms of Service. If you do not agree, please do not use the Service.</p>
+
+ <h2>2. Description of Service</h2>
+ <p>Museum Art Bot is an automated service that posts public domain artwork to X (formerly Twitter). The Service:</p>
+ <ul>
+ <li>Shares artwork that is in the public domain</li>
+ <li>Provides educational content about art history</li>
+ <li>Posts on a regular schedule to @goodquestionai</li>
+ </ul>
+
+ <h2>3. Use of Content</h2>
+ <p>All artwork shared by the Service is in the public domain and free to use. The Service adds no additional copyright restrictions. Attribution information is provided for educational purposes.</p>
+
+ <h2>4. No Warranties</h2>
+ <p>The Service is provided "as is" without warranties of any kind. We do not guarantee:</p>
+ <ul>
+ <li>Continuous, uninterrupted operation</li>
+ <li>Accuracy of artwork attribution or historical information</li>
+ <li>Availability of specific artworks</li>
+ </ul>
+
+ <h2>5. User Conduct</h2>
+ <p>When interacting with the Service, you agree not to:</p>
+ <ul>
+ <li>Use the Service for any illegal purpose</li>
+ <li>Attempt to disrupt or overload the Service</li>
+ <li>Impersonate the Service or create unauthorized derivative accounts</li>
+ </ul>
+
+ <h2>6. Privacy</h2>
+ <p>The Service does not collect personal information. When you authorize the app, X handles authentication according to their privacy policy.</p>
+
+ <h2>7. Intellectual Property</h2>
+ <p>The Service respects intellectual property rights. All artwork shared is verified to be in the public domain. The bot software itself is open source.</p>
+
+ <h2>8. Limitation of Liability</h2>
+ <p>The Service and its operators shall not be liable for any damages arising from use or inability to use the Service.</p>
+
+ <h2>9. Changes to Terms</h2>
+ <p>We may update these terms at any time. Continued use of the Service constitutes acceptance of updated terms.</p>
+
+ <h2>10. Termination</h2>
+ <p>We reserve the right to terminate or suspend the Service at any time without notice.</p>
+
+ <h2>11. Contact</h2>
+ <p>For questions about these Terms, contact us via X at @goodquestionai</p>
+
+ <h2>12. Disclaimer</h2>
+ <p>Museum Art Bot is not affiliated with any museum, gallery, or artistic institution. All opinions and descriptions are for educational purposes only.</p>
+
+ <hr>
+ <p><a href="/index.html">← Back to Home</a> | <a href="/privacy.html">Privacy Policy</a></p>
+</body>
+</html>
\ No newline at end of file
diff --git a/ralphy.js b/ralphy.js
new file mode 100755
index 0000000..4490e83
--- /dev/null
+++ b/ralphy.js
@@ -0,0 +1,353 @@
+#!/usr/bin/env node
+/**
+ * Ralphy Boy - Free Local Task Runner
+ * A simple task management system for autonomous development
+ * No paid services, no amp CLI - just pure local goodness!
+ *
+ * Usage:
+ * node ralph.js next - Show next available task
+ * node ralph.js start T001 - Mark task as in_progress
+ * node ralph.js done T001 - Mark task as completed
+ * node ralph.js status - Show overall progress
+ * node ralph.js list - List all tasks
+ * node ralph.js phase 1 - Show tasks for phase 1
+ * node ralph.js reset - Reset all tasks to pending
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const TASKS_FILE = path.join(__dirname, 'scripts/ralph/tasks.json');
+const PROGRESS_FILE = path.join(__dirname, 'scripts/ralph/progress.txt');
+
+// ANSI colors
+const colors = {
+ reset: '\x1b[0m',
+ bright: '\x1b[1m',
+ red: '\x1b[31m',
+ green: '\x1b[32m',
+ yellow: '\x1b[33m',
+ blue: '\x1b[34m',
+ magenta: '\x1b[35m',
+ cyan: '\x1b[36m',
+ gray: '\x1b[90m'
+};
+
+function loadTasks() {
+ const data = fs.readFileSync(TASKS_FILE, 'utf-8');
+ return JSON.parse(data);
+}
+
+function saveTasks(data) {
+ fs.writeFileSync(TASKS_FILE, JSON.stringify(data, null, 2));
+}
+
+function logProgress(message) {
+ const timestamp = new Date().toISOString();
+ const entry = `[${timestamp}] ${message}\n`;
+ fs.appendFileSync(PROGRESS_FILE, entry);
+}
+
+function getTaskById(tasks, id) {
+ return tasks.find(t => t.id === id);
+}
+
+function isTaskReady(task, tasks) {
+ if (task.status !== 'pending') return false;
+
+ for (const depId of task.dependsOn) {
+ const dep = getTaskById(tasks, depId);
+ if (!dep || dep.status !== 'completed') {
+ return false;
+ }
+ }
+ return true;
+}
+
+function getNextTask(tasks) {
+ // Find first ready task (pending with all deps completed)
+ return tasks.find(t => isTaskReady(t, tasks));
+}
+
+function getReadyTasks(tasks) {
+ return tasks.filter(t => isTaskReady(t, tasks));
+}
+
+function printTask(task, verbose = true) {
+ const statusColors = {
+ pending: colors.gray,
+ in_progress: colors.yellow,
+ completed: colors.green
+ };
+
+ const statusIcon = {
+ pending: '○',
+ in_progress: '◐',
+ completed: '●'
+ };
+
+ console.log(`\n${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
+ console.log(`${colors.bright}Task: ${task.id}${colors.reset} - Phase ${task.phase}`);
+ console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
+ console.log(`${colors.bright}Title:${colors.reset} ${task.title}`);
+ console.log(`${colors.bright}Status:${colors.reset} ${statusColors[task.status]}${statusIcon[task.status]} ${task.status}${colors.reset}`);
+
+ if (task.dependsOn.length > 0) {
+ console.log(`${colors.bright}Depends on:${colors.reset} ${task.dependsOn.join(', ')}`);
+ }
+
+ if (verbose) {
+ console.log(`\n${colors.bright}Description:${colors.reset}`);
+ console.log(` ${task.description}`);
+
+ console.log(`\n${colors.bright}Files to modify:${colors.reset}`);
+ task.files.forEach(f => console.log(` - ${f}`));
+
+ console.log(`\n${colors.bright}Acceptance Criteria:${colors.reset}`);
+ task.acceptance.forEach(a => console.log(` [ ] ${a}`));
+
+ if (task.commands) {
+ console.log(`\n${colors.bright}Commands:${colors.reset}`);
+ if (task.commands.run) console.log(` ${colors.green}Run:${colors.reset} ${task.commands.run}`);
+ if (task.commands.verify) console.log(` ${colors.blue}Verify:${colors.reset} ${task.commands.verify}`);
+ }
+ }
+
+ console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
+}
+
+function printStatus(data) {
+ const tasks = data.tasks;
+ const completed = tasks.filter(t => t.status === 'completed').length;
+ const inProgress = tasks.filter(t => t.status === 'in_progress').length;
+ const pending = tasks.filter(t => t.status === 'pending').length;
+ const ready = getReadyTasks(tasks).length;
+
+ const pct = Math.round((completed / tasks.length) * 100);
+ const bar = '█'.repeat(Math.floor(pct / 5)) + '░'.repeat(20 - Math.floor(pct / 5));
+
+ console.log(`\n${colors.bright}${colors.cyan}╔═══════════════════════════════════════════════════════════╗${colors.reset}`);
+ console.log(`${colors.cyan}║${colors.reset} ${colors.bright}RALPHY BOY - Museum Art Bot Tasks${colors.reset} ${colors.cyan}║${colors.reset}`);
+ console.log(`${colors.cyan}╠═══════════════════════════════════════════════════════════╣${colors.reset}`);
+ console.log(`${colors.cyan}║${colors.reset} Progress: [${colors.green}${bar}${colors.reset}] ${pct}% ${colors.cyan}║${colors.reset}`);
+ console.log(`${colors.cyan}╠═══════════════════════════════════════════════════════════╣${colors.reset}`);
+ console.log(`${colors.cyan}║${colors.reset} ${colors.green}● Completed:${colors.reset} ${String(completed).padStart(2)} / ${tasks.length} ${colors.cyan}║${colors.reset}`);
+ console.log(`${colors.cyan}║${colors.reset} ${colors.yellow}◐ In Progress:${colors.reset} ${String(inProgress).padStart(2)} ${colors.cyan}║${colors.reset}`);
+ console.log(`${colors.cyan}║${colors.reset} ${colors.blue}○ Ready:${colors.reset} ${String(ready).padStart(2)} (dependencies met) ${colors.cyan}║${colors.reset}`);
+ console.log(`${colors.cyan}║${colors.reset} ${colors.gray}○ Blocked:${colors.reset} ${String(pending - ready).padStart(2)} (waiting on dependencies) ${colors.cyan}║${colors.reset}`);
+ console.log(`${colors.cyan}╚═══════════════════════════════════════════════════════════╝${colors.reset}`);
+
+ // Show by phase
+ console.log(`\n${colors.bright}By Phase:${colors.reset}`);
+ for (let phase = 1; phase <= 5; phase++) {
+ const phaseTasks = tasks.filter(t => t.phase === phase);
+ const phaseCompleted = phaseTasks.filter(t => t.status === 'completed').length;
+ const phaseNames = ['', 'Stability', 'Museums', 'Animation', 'Content', 'Monitoring'];
+ console.log(` Phase ${phase} (${phaseNames[phase]}): ${phaseCompleted}/${phaseTasks.length}`);
+ }
+
+ // Show next task
+ const next = getNextTask(tasks);
+ if (next) {
+ console.log(`\n${colors.bright}Next task:${colors.reset} ${next.id} - ${next.title}`);
+ console.log(`${colors.gray}Run: node ralph.js next${colors.reset}`);
+ } else if (completed === tasks.length) {
+ console.log(`\n${colors.green}${colors.bright}All tasks completed!${colors.reset}`);
+ } else {
+ console.log(`\n${colors.yellow}No tasks ready - complete in-progress tasks first${colors.reset}`);
+ }
+
+ console.log('');
+}
+
+function printList(data, phaseFilter = null) {
+ const tasks = phaseFilter
+ ? data.tasks.filter(t => t.phase === phaseFilter)
+ : data.tasks;
+
+ const statusIcon = {
+ pending: `${colors.gray}○${colors.reset}`,
+ in_progress: `${colors.yellow}◐${colors.reset}`,
+ completed: `${colors.green}●${colors.reset}`
+ };
+
+ console.log(`\n${colors.bright}Task List${phaseFilter ? ` (Phase ${phaseFilter})` : ''}:${colors.reset}\n`);
+
+ let currentPhase = 0;
+ for (const task of tasks) {
+ if (task.phase !== currentPhase) {
+ currentPhase = task.phase;
+ const phaseNames = ['', 'Stability', 'Museums', 'Animation', 'Content', 'Monitoring'];
+ console.log(`\n${colors.bright}Phase ${currentPhase}: ${phaseNames[currentPhase]}${colors.reset}`);
+ }
+
+ const ready = isTaskReady(task, data.tasks);
+ const readyMarker = ready && task.status === 'pending' ? `${colors.blue}[READY]${colors.reset} ` : '';
+
+ console.log(` ${statusIcon[task.status]} ${task.id}: ${readyMarker}${task.title}`);
+ }
+ console.log('');
+}
+
+// Main CLI
+const args = process.argv.slice(2);
+const command = args[0] || 'status';
+
+try {
+ const data = loadTasks();
+
+ switch (command) {
+ case 'next': {
+ const next = getNextTask(data.tasks);
+ if (next) {
+ printTask(next);
+ console.log(`${colors.bright}To start this task:${colors.reset} node ralph.js start ${next.id}`);
+ } else {
+ const inProgress = data.tasks.filter(t => t.status === 'in_progress');
+ if (inProgress.length > 0) {
+ console.log(`\n${colors.yellow}Tasks in progress:${colors.reset}`);
+ inProgress.forEach(t => console.log(` ${t.id}: ${t.title}`));
+ console.log(`\n${colors.gray}Complete these first: node ralph.js done <ID>${colors.reset}`);
+ } else {
+ console.log(`\n${colors.green}All tasks completed!${colors.reset}`);
+ }
+ }
+ break;
+ }
+
+ case 'start': {
+ const taskId = args[1];
+ if (!taskId) {
+ console.log(`${colors.red}Error: Specify task ID${colors.reset}`);
+ console.log('Usage: node ralph.js start T001');
+ process.exit(1);
+ }
+
+ const task = getTaskById(data.tasks, taskId);
+ if (!task) {
+ console.log(`${colors.red}Error: Task ${taskId} not found${colors.reset}`);
+ process.exit(1);
+ }
+
+ if (!isTaskReady(task, data.tasks) && task.status === 'pending') {
+ console.log(`${colors.red}Error: Task ${taskId} has unmet dependencies${colors.reset}`);
+ console.log(`Depends on: ${task.dependsOn.join(', ')}`);
+ process.exit(1);
+ }
+
+ task.status = 'in_progress';
+ task.startedAt = new Date().toISOString();
+ saveTasks(data);
+ logProgress(`STARTED: ${taskId} - ${task.title}`);
+
+ console.log(`${colors.green}Started task ${taskId}${colors.reset}`);
+ printTask(task);
+ break;
+ }
+
+ case 'done': {
+ const taskId = args[1];
+ if (!taskId) {
+ console.log(`${colors.red}Error: Specify task ID${colors.reset}`);
+ console.log('Usage: node ralph.js done T001');
+ process.exit(1);
+ }
+
+ const task = getTaskById(data.tasks, taskId);
+ if (!task) {
+ console.log(`${colors.red}Error: Task ${taskId} not found${colors.reset}`);
+ process.exit(1);
+ }
+
+ task.status = 'completed';
+ task.completedAt = new Date().toISOString();
+ saveTasks(data);
+ logProgress(`COMPLETED: ${taskId} - ${task.title}`);
+
+ console.log(`${colors.green}Completed task ${taskId}!${colors.reset}`);
+
+ // Show next task
+ const next = getNextTask(data.tasks);
+ if (next) {
+ console.log(`\n${colors.bright}Next available task:${colors.reset} ${next.id} - ${next.title}`);
+ }
+ break;
+ }
+
+ case 'status':
+ printStatus(data);
+ break;
+
+ case 'list':
+ printList(data);
+ break;
+
+ case 'phase': {
+ const phase = parseInt(args[1]);
+ if (isNaN(phase) || phase < 1 || phase > 5) {
+ console.log(`${colors.red}Error: Specify phase 1-5${colors.reset}`);
+ process.exit(1);
+ }
+ printList(data, phase);
+ break;
+ }
+
+ case 'show': {
+ const taskId = args[1];
+ if (!taskId) {
+ console.log(`${colors.red}Error: Specify task ID${colors.reset}`);
+ process.exit(1);
+ }
+ const task = getTaskById(data.tasks, taskId);
+ if (!task) {
+ console.log(`${colors.red}Error: Task ${taskId} not found${colors.reset}`);
+ process.exit(1);
+ }
+ printTask(task);
+ break;
+ }
+
+ case 'reset': {
+ data.tasks.forEach(t => {
+ t.status = 'pending';
+ delete t.startedAt;
+ delete t.completedAt;
+ });
+ saveTasks(data);
+ logProgress('RESET: All tasks reset to pending');
+ console.log(`${colors.yellow}All tasks reset to pending${colors.reset}`);
+ break;
+ }
+
+ case 'help':
+ default:
+ console.log(`
+${colors.bright}Ralphy Boy - Free Local Task Runner${colors.reset}
+
+${colors.cyan}Commands:${colors.reset}
+ node ralph.js next Show next available task
+ node ralph.js start T001 Mark task as in_progress
+ node ralph.js done T001 Mark task as completed
+ node ralph.js show T001 Show task details
+ node ralph.js status Show overall progress
+ node ralph.js list List all tasks
+ node ralph.js phase 1 Show tasks for phase 1-5
+ node ralph.js reset Reset all tasks to pending
+ node ralph.js help Show this help
+
+${colors.cyan}Workflow:${colors.reset}
+ 1. Run 'node ralph.js next' to see next task
+ 2. Run 'node ralph.js start T001' to begin
+ 3. Implement the task
+ 4. Run 'node ralph.js done T001' when complete
+ 5. Repeat!
+`);
+ }
+} catch (error) {
+ console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
+ process.exit(1);
+}
diff --git a/scripts/post-3-wave-branded.js b/scripts/post-3-wave-branded.js
new file mode 100644
index 0000000..5c051c6
--- /dev/null
+++ b/scripts/post-3-wave-branded.js
@@ -0,0 +1,138 @@
+#!/usr/bin/env node
+/**
+ * Post 3 Great Wave videos with:
+ * - Moderate motion (127)
+ * - 2s intro + 2s outro branding
+ */
+
+import Replicate from 'replicate';
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import sharp from 'sharp';
+import dotenv from 'dotenv';
+import { execSync } from 'child_process';
+import { postToX } from '../src/postToX.js';
+import { addBrandingToVideo } from '../src/addBranding.js';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const projectRoot = path.join(__dirname, '..');
+const OUTPUT_DIR = path.join(projectRoot, 'output');
+
+async function createAndPost(num) {
+ console.log(`\n${'='.repeat(50)}`);
+ console.log(`POST ${num}/3 - ${new Date().toISOString()}`);
+ console.log(`${'='.repeat(50)}`);
+
+ const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN });
+
+ // Get source image
+ let imagePath = path.join(OUTPUT_DIR, 'great_wave_source.jpg');
+ if (!fs.existsSync(imagePath)) {
+ console.log('Downloading The Great Wave...');
+ const response = await axios.get('https://images.metmuseum.org/CRDImages/as/original/DP141063.jpg', {
+ responseType: 'arraybuffer',
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ArtBot/1.0)' }
+ });
+ fs.writeFileSync(imagePath, response.data);
+ }
+
+ // Prepare image
+ const optimizedPath = path.join(OUTPUT_DIR, `wave_opt_${num}.jpg`);
+ await sharp(imagePath)
+ .resize(1024, 576, { fit: 'cover', position: 'center' })
+ .jpeg({ quality: 95 })
+ .toFile(optimizedPath);
+
+ const imageBuffer = fs.readFileSync(optimizedPath);
+ const dataUri = `data:image/jpeg;base64,${imageBuffer.toString('base64')}`;
+
+ // Moderate motion settings
+ const seed = Math.floor(Math.random() * 1000000);
+ console.log(`Generating with moderate motion (127), seed: ${seed}...`);
+
+ const startTime = Date.now();
+ const output = await replicate.run(
+ "stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438",
+ {
+ input: {
+ input_image: dataUri,
+ video_length: "25_frames_with_svd_xt",
+ sizing_strategy: "maintain_aspect_ratio",
+ frames_per_second: 8,
+ motion_bucket_id: 127, // MODERATE motion
+ cond_aug: 0.02,
+ decoding_t: 14,
+ seed: seed
+ }
+ }
+ );
+ console.log(`Generated in ${((Date.now() - startTime) / 1000).toFixed(1)}s`);
+
+ // Download video
+ const videoResponse = await axios.get(output, { responseType: 'arraybuffer' });
+ const timestamp = Date.now();
+ const videoPath = path.join(OUTPUT_DIR, `wave_branded_${num}_${timestamp}.mp4`);
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ // Convert to H.264
+ const h264Path = videoPath.replace('.mp4', '_h264.mp4');
+ execSync(`ffmpeg -y -i "${videoPath}" -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -movflags +faststart -an "${h264Path}"`, { stdio: 'pipe' });
+ fs.unlinkSync(videoPath);
+ fs.renameSync(h264Path, videoPath);
+
+ // Add 2s intro + 2s outro branding
+ console.log('Adding GoodQuestion.AI branding (2s intro + 2s outro)...');
+ const metadata = { title: "The Great Wave off Kanagawa", artist: "Katsushika Hokusai" };
+ await addBrandingToVideo(videoPath, metadata);
+
+ // Cleanup
+ fs.unlinkSync(optimizedPath);
+
+ // Post to Twitter
+ console.log('Posting to Twitter...');
+ const animationResult = {
+ animatedVideo: videoPath,
+ artworkMetadata: {
+ title: "The Great Wave off Kanagawa",
+ artist: "Katsushika Hokusai",
+ year: "1831",
+ museum: "Public Domain Collection"
+ },
+ isAnimated: true
+ };
+
+ const result = await postToX(animationResult);
+ console.log(`Posted! https://twitter.com/goodquestionai/status/${result.data.id}`);
+ return result;
+}
+
+async function main() {
+ console.log(`
+╔═══════════════════════════════════════════════════════════╗
+║ POSTING 3 GREAT WAVE VIDEOS WITH BRANDING ║
+║ - Moderate motion (127) ║
+║ - 2s intro + 2s outro ║
+╚═══════════════════════════════════════════════════════════╝
+`);
+
+ for (let i = 1; i <= 3; i++) {
+ try {
+ await createAndPost(i);
+ if (i < 3) {
+ console.log('\nWaiting 15s before next post...');
+ await new Promise(r => setTimeout(r, 15000));
+ }
+ } catch (err) {
+ console.error(`Post ${i} failed:`, err.message);
+ }
+ }
+
+ console.log('\n=== ALL 3 POSTS COMPLETE ===\n');
+}
+
+main().catch(console.error);
diff --git a/scripts/test-wave-metamorphosis.js b/scripts/test-wave-metamorphosis.js
new file mode 100644
index 0000000..ee88b3b
--- /dev/null
+++ b/scripts/test-wave-metamorphosis.js
@@ -0,0 +1,176 @@
+#!/usr/bin/env node
+/**
+ * Test metamorphosis animation with The Great Wave off Kanagawa
+ * This should create DRAMATIC wave motion, not just panning
+ */
+
+import Replicate from 'replicate';
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import sharp from 'sharp';
+import dotenv from 'dotenv';
+import { execSync } from 'child_process';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const projectRoot = path.join(__dirname, '..');
+
+const OUTPUT_DIR = path.join(projectRoot, 'output');
+if (!fs.existsSync(OUTPUT_DIR)) {
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
+}
+
+async function downloadGreatWave() {
+ console.log('Downloading The Great Wave off Kanagawa...');
+
+ // Met Museum version (public domain, no restrictions)
+ const imageUrl = 'https://images.metmuseum.org/CRDImages/as/original/DP141063.jpg';
+
+ const response = await axios.get(imageUrl, {
+ responseType: 'arraybuffer',
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (compatible; ArtBot/1.0)'
+ }
+ });
+ const imagePath = path.join(OUTPUT_DIR, 'great_wave_original.jpg');
+ fs.writeFileSync(imagePath, response.data);
+
+ console.log('Downloaded Great Wave image from Met Museum');
+ return imagePath;
+}
+
+async function createMetamorphosisVideo(imagePath) {
+ console.log('\n=== CREATING METAMORPHOSIS VIDEO ===');
+ console.log('Goal: Waves should CRASH, MOVE, TRANSFORM - not just pan!');
+
+ const replicate = new Replicate({
+ auth: process.env.REPLICATE_API_TOKEN,
+ });
+
+ // Prepare image for SVD (1024x576)
+ const optimizedPath = path.join(OUTPUT_DIR, 'wave_optimized.jpg');
+ await sharp(imagePath)
+ .resize(1024, 576, { fit: 'cover', position: 'center' })
+ .jpeg({ quality: 95 })
+ .toFile(optimizedPath);
+
+ const imageBuffer = fs.readFileSync(optimizedPath);
+ const base64Image = imageBuffer.toString('base64');
+ const dataUri = `data:image/jpeg;base64,${base64Image}`;
+
+ console.log('\nSending to Stable Video Diffusion with MAXIMUM motion...');
+ console.log('Settings:');
+ console.log(' - motion_bucket_id: 255 (MAXIMUM)');
+ console.log(' - cond_aug: 0.1 (high variation)');
+ console.log(' - fps: 6 (slower = more dramatic)');
+ console.log('\nPlease wait 60-180 seconds...');
+
+ const startTime = Date.now();
+
+ const output = await replicate.run(
+ "stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438",
+ {
+ input: {
+ input_image: dataUri,
+ video_length: "25_frames_with_svd_xt",
+ sizing_strategy: "maintain_aspect_ratio",
+ frames_per_second: 6, // Slower for dramatic effect
+ motion_bucket_id: 255, // MAXIMUM motion
+ cond_aug: 0.1, // Higher variation
+ decoding_t: 14,
+ seed: Math.floor(Math.random() * 1000000)
+ }
+ }
+ );
+
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+ console.log(`\nVideo generated in ${elapsed}s`);
+
+ // Download the video
+ const videoResponse = await axios.get(output, { responseType: 'arraybuffer' });
+ const timestamp = Date.now();
+ const videoPath = path.join(OUTPUT_DIR, `wave_metamorphosis_${timestamp}.mp4`);
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ // Check video properties
+ const probeCmd = `ffprobe -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`;
+ const duration = parseFloat(execSync(probeCmd, { stdio: 'pipe' }).toString().trim());
+ const stats = fs.statSync(videoPath);
+
+ console.log(`\nVideo created:`);
+ console.log(` - Path: ${videoPath}`);
+ console.log(` - Duration: ${duration.toFixed(2)}s`);
+ console.log(` - Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
+
+ // Convert to H.264 for Twitter
+ const h264Path = videoPath.replace('.mp4', '_h264.mp4');
+ console.log('\nConverting to Twitter-compatible H.264...');
+ execSync(`ffmpeg -y -i "${videoPath}" -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -movflags +faststart -an "${h264Path}"`, { stdio: 'pipe' });
+ fs.unlinkSync(videoPath);
+ fs.renameSync(h264Path, videoPath);
+
+ // Cleanup
+ fs.unlinkSync(optimizedPath);
+
+ return videoPath;
+}
+
+async function postToTwitter(videoPath) {
+ console.log('\n=== POSTING TO TWITTER ===');
+
+ const { postToX } = await import('../src/postToX.js');
+
+ // Create animationResult object matching postToX signature
+ const animationResult = {
+ animatedVideo: videoPath,
+ artworkMetadata: {
+ title: "The Great Wave off Kanagawa",
+ artist: "Katsushika Hokusai",
+ year: "1831",
+ museum: "Public Domain Collection"
+ },
+ isAnimated: true
+ };
+
+ const result = await postToX(animationResult);
+
+ console.log(`\nTweet posted successfully!`);
+ console.log(`Tweet ID: ${result.data.id}`);
+ console.log(`URL: https://twitter.com/goodquestionai/status/${result.data.id}`);
+
+ return result;
+}
+
+async function main() {
+ console.log(`
+╔═══════════════════════════════════════════════════════════╗
+║ THE GREAT WAVE - METAMORPHOSIS TEST ║
+║ Testing maximum motion SVD animation ║
+╚═══════════════════════════════════════════════════════════╝
+`);
+
+ try {
+ // 1. Download the Great Wave image
+ const imagePath = await downloadGreatWave();
+
+ // 2. Create metamorphosis video
+ const videoPath = await createMetamorphosisVideo(imagePath);
+
+ // 3. Post to Twitter
+ await postToTwitter(videoPath);
+
+ console.log('\n=== TEST COMPLETE ===');
+ console.log('Check the tweet to see if waves are actually moving/transforming');
+ console.log('Not just panning across the image\n');
+
+ } catch (error) {
+ console.error('Error:', error.message);
+ console.error(error.stack);
+ }
+}
+
+main();
diff --git a/scripts/test-wave-moderate.js b/scripts/test-wave-moderate.js
new file mode 100644
index 0000000..8d04d39
--- /dev/null
+++ b/scripts/test-wave-moderate.js
@@ -0,0 +1,126 @@
+#!/usr/bin/env node
+/**
+ * Test Great Wave with MODERATE motion
+ * motion_bucket_id: 127 (middle) instead of 255 (max)
+ * Waves should MOVE but not wash away the image
+ */
+
+import Replicate from 'replicate';
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import sharp from 'sharp';
+import dotenv from 'dotenv';
+import { execSync } from 'child_process';
+import { postToX } from '../src/postToX.js';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const projectRoot = path.join(__dirname, '..');
+const OUTPUT_DIR = path.join(projectRoot, 'output');
+
+async function main() {
+ console.log(`
+╔═══════════════════════════════════════════════════════════╗
+║ THE GREAT WAVE - MODERATE MOTION TEST ║
+║ motion_bucket_id: 127 (half of max) ║
+║ cond_aug: 0.02 (default, less variation) ║
+╚═══════════════════════════════════════════════════════════╝
+`);
+
+ const replicate = new Replicate({
+ auth: process.env.REPLICATE_API_TOKEN,
+ });
+
+ // Use existing source image
+ let imagePath = path.join(OUTPUT_DIR, 'great_wave_source.jpg');
+ if (!fs.existsSync(imagePath)) {
+ console.log('Downloading The Great Wave...');
+ const imageUrl = 'https://images.metmuseum.org/CRDImages/as/original/DP141063.jpg';
+ const response = await axios.get(imageUrl, {
+ responseType: 'arraybuffer',
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ArtBot/1.0)' }
+ });
+ fs.writeFileSync(imagePath, response.data);
+ }
+
+ // Prepare image
+ const optimizedPath = path.join(OUTPUT_DIR, 'wave_moderate_opt.jpg');
+ await sharp(imagePath)
+ .resize(1024, 576, { fit: 'cover', position: 'center' })
+ .jpeg({ quality: 95 })
+ .toFile(optimizedPath);
+
+ const imageBuffer = fs.readFileSync(optimizedPath);
+ const base64Image = imageBuffer.toString('base64');
+ const dataUri = `data:image/jpeg;base64,${base64Image}`;
+
+ console.log('Settings (MODERATE):');
+ console.log(' - motion_bucket_id: 127 (half of max 255)');
+ console.log(' - cond_aug: 0.02 (low variation)');
+ console.log(' - fps: 8 (smooth playback)');
+ console.log('\nGenerating... (60-120s)\n');
+
+ const startTime = Date.now();
+
+ const output = await replicate.run(
+ "stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438",
+ {
+ input: {
+ input_image: dataUri,
+ video_length: "25_frames_with_svd_xt",
+ sizing_strategy: "maintain_aspect_ratio",
+ frames_per_second: 8,
+ motion_bucket_id: 127, // MODERATE motion (half of max)
+ cond_aug: 0.02, // Low variation - preserve image
+ decoding_t: 14,
+ seed: Math.floor(Math.random() * 1000000)
+ }
+ }
+ );
+
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+ console.log(`Generated in ${elapsed}s`);
+
+ // Download video
+ const videoResponse = await axios.get(output, { responseType: 'arraybuffer' });
+ const timestamp = Date.now();
+ const videoPath = path.join(OUTPUT_DIR, `wave_moderate_${timestamp}.mp4`);
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ // Convert to H.264
+ const h264Path = videoPath.replace('.mp4', '_h264.mp4');
+ execSync(`ffmpeg -y -i "${videoPath}" -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -movflags +faststart -an "${h264Path}"`, { stdio: 'pipe' });
+ fs.unlinkSync(videoPath);
+ fs.renameSync(h264Path, videoPath);
+
+ const stats = fs.statSync(videoPath);
+ console.log(`\nVideo created: ${videoPath}`);
+ console.log(`Size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
+
+ // Cleanup
+ fs.unlinkSync(optimizedPath);
+
+ // Post to Twitter
+ console.log('\nPosting to Twitter...');
+ const animationResult = {
+ animatedVideo: videoPath,
+ artworkMetadata: {
+ title: "The Great Wave off Kanagawa",
+ artist: "Katsushika Hokusai",
+ year: "1831",
+ museum: "Moderate Motion Test - Waves should move naturally"
+ },
+ isAnimated: true
+ };
+
+ const result = await postToX(animationResult);
+ console.log(`\nPosted! Tweet ID: ${result.data.id}`);
+ console.log(`URL: https://twitter.com/goodquestionai/status/${result.data.id}`);
+ console.log('\nCheck if waves are moving naturally without distorting the image!');
+}
+
+main().catch(console.error);
diff --git a/scripts/wave-repeat-test.js b/scripts/wave-repeat-test.js
new file mode 100644
index 0000000..99282ce
--- /dev/null
+++ b/scripts/wave-repeat-test.js
@@ -0,0 +1,197 @@
+#!/usr/bin/env node
+/**
+ * Post The Great Wave with different metamorphosis variations
+ * every 5 minutes for 3 hours (36 posts)
+ */
+
+import Replicate from 'replicate';
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import sharp from 'sharp';
+import dotenv from 'dotenv';
+import { execSync } from 'child_process';
+import { postToX } from '../src/postToX.js';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const projectRoot = path.join(__dirname, '..');
+const OUTPUT_DIR = path.join(projectRoot, 'output');
+
+const TOTAL_POSTS = 36; // 3 hours at 5 min intervals
+const INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
+
+let postCount = 0;
+let successCount = 0;
+
+async function downloadGreatWave() {
+ const imagePath = path.join(OUTPUT_DIR, 'great_wave_source.jpg');
+
+ if (fs.existsSync(imagePath)) {
+ return imagePath;
+ }
+
+ console.log('Downloading The Great Wave...');
+ const imageUrl = 'https://images.metmuseum.org/CRDImages/as/original/DP141063.jpg';
+ const response = await axios.get(imageUrl, {
+ responseType: 'arraybuffer',
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ArtBot/1.0)' }
+ });
+ fs.writeFileSync(imagePath, response.data);
+ return imagePath;
+}
+
+async function createMetamorphosisVariation(imagePath, variationNum) {
+ console.log(`\n=== Creating Variation ${variationNum} ===`);
+
+ const replicate = new Replicate({
+ auth: process.env.REPLICATE_API_TOKEN,
+ });
+
+ // Prepare image
+ const optimizedPath = path.join(OUTPUT_DIR, `wave_opt_${variationNum}.jpg`);
+ await sharp(imagePath)
+ .resize(1024, 576, { fit: 'cover', position: 'center' })
+ .jpeg({ quality: 95 })
+ .toFile(optimizedPath);
+
+ const imageBuffer = fs.readFileSync(optimizedPath);
+ const base64Image = imageBuffer.toString('base64');
+ const dataUri = `data:image/jpeg;base64,${base64Image}`;
+
+ // Vary the motion parameters for different effects
+ const seed = Math.floor(Math.random() * 1000000);
+ const motionBucket = 200 + Math.floor(Math.random() * 55); // 200-255 range
+ const condAug = 0.05 + (Math.random() * 0.1); // 0.05-0.15 range
+ const fps = 5 + Math.floor(Math.random() * 4); // 5-8 fps
+
+ console.log(`Settings: motion=${motionBucket}, cond_aug=${condAug.toFixed(2)}, fps=${fps}, seed=${seed}`);
+ console.log('Generating... (60-120s)');
+
+ const startTime = Date.now();
+
+ const output = await replicate.run(
+ "stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438",
+ {
+ input: {
+ input_image: dataUri,
+ video_length: "25_frames_with_svd_xt",
+ sizing_strategy: "maintain_aspect_ratio",
+ frames_per_second: fps,
+ motion_bucket_id: motionBucket,
+ cond_aug: condAug,
+ decoding_t: 14,
+ seed: seed
+ }
+ }
+ );
+
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
+ console.log(`Generated in ${elapsed}s`);
+
+ // Download video
+ const videoResponse = await axios.get(output, { responseType: 'arraybuffer' });
+ const timestamp = Date.now();
+ const videoPath = path.join(OUTPUT_DIR, `wave_v${variationNum}_${timestamp}.mp4`);
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ // Convert to H.264
+ const h264Path = videoPath.replace('.mp4', '_h264.mp4');
+ execSync(`ffmpeg -y -i "${videoPath}" -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -movflags +faststart -an "${h264Path}"`, { stdio: 'pipe' });
+ fs.unlinkSync(videoPath);
+ fs.renameSync(h264Path, videoPath);
+
+ // Cleanup
+ fs.unlinkSync(optimizedPath);
+
+ return videoPath;
+}
+
+async function postVariation(videoPath, variationNum) {
+ const captions = [
+ "Watch the waves crash and surge!",
+ "Hokusai's masterpiece comes alive",
+ "AI brings motion to ancient art",
+ "The ocean awakens",
+ "Waves in perpetual motion",
+ "Art transformed by AI",
+ "Feel the power of the sea",
+ "Dynamic reinterpretation",
+ "The wave never stops",
+ "Ancient art, modern motion"
+ ];
+
+ const animationResult = {
+ animatedVideo: videoPath,
+ artworkMetadata: {
+ title: "The Great Wave off Kanagawa",
+ artist: "Katsushika Hokusai",
+ year: "1831",
+ museum: `Variation ${variationNum}/36 - ${captions[variationNum % captions.length]}`
+ },
+ isAnimated: true
+ };
+
+ const result = await postToX(animationResult);
+ console.log(`Posted! Tweet ID: ${result.data.id}`);
+ return result;
+}
+
+async function runOneIteration() {
+ postCount++;
+ console.log(`\n${'='.repeat(60)}`);
+ console.log(`POST ${postCount}/${TOTAL_POSTS} - ${new Date().toISOString()}`);
+ console.log(`${'='.repeat(60)}`);
+
+ try {
+ const imagePath = await downloadGreatWave();
+ const videoPath = await createMetamorphosisVariation(imagePath, postCount);
+ await postVariation(videoPath, postCount);
+ successCount++;
+ console.log(`Success! (${successCount}/${postCount} successful)`);
+ } catch (error) {
+ console.error(`Failed: ${error.message}`);
+ }
+
+ if (postCount < TOTAL_POSTS) {
+ console.log(`\nNext post in 5 minutes...`);
+ console.log(`Remaining: ${TOTAL_POSTS - postCount} posts, ~${((TOTAL_POSTS - postCount) * 5)} minutes`);
+ }
+}
+
+async function main() {
+ console.log(`
+╔═══════════════════════════════════════════════════════════╗
+║ THE GREAT WAVE - 3 HOUR METAMORPHOSIS TEST ║
+║ 36 variations posted every 5 minutes ║
+╚═══════════════════════════════════════════════════════════╝
+
+Duration: 3 hours
+Interval: 5 minutes
+Total Posts: 36
+Start: ${new Date().toISOString()}
+End (estimated): ${new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString()}
+`);
+
+ // First post immediately
+ await runOneIteration();
+
+ // Then schedule remaining posts
+ const interval = setInterval(async () => {
+ await runOneIteration();
+
+ if (postCount >= TOTAL_POSTS) {
+ clearInterval(interval);
+ console.log(`\n${'='.repeat(60)}`);
+ console.log('ALL POSTS COMPLETE!');
+ console.log(`Success rate: ${successCount}/${TOTAL_POSTS} (${((successCount/TOTAL_POSTS)*100).toFixed(1)}%)`);
+ console.log(`${'='.repeat(60)}\n`);
+ process.exit(0);
+ }
+ }, INTERVAL_MS);
+}
+
+main().catch(console.error);
diff --git a/setup.sh b/setup.sh
new file mode 100755
index 0000000..86419e8
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,67 @@
+#!/bin/bash
+
+echo "Museum Art Bot Setup Script"
+echo "==========================="
+
+if [ ! -f .env ]; then
+ echo "Creating .env file from template..."
+ cp .env.example .env
+ echo "Please edit .env file with your API credentials"
+fi
+
+echo "Installing dependencies..."
+npm install
+
+echo "Creating required directories..."
+mkdir -p output
+mkdir -p logs
+
+echo ""
+echo "Testing configuration..."
+node src/index.js --test
+
+if [ $? -eq 0 ]; then
+ echo "✓ Configuration test passed!"
+else
+ echo "✗ Configuration test failed. Please check your .env file"
+ exit 1
+fi
+
+echo ""
+echo "Do you want to:"
+echo "1) Run once now"
+echo "2) Set up system cron job (runs every 6 hours)"
+echo "3) Run continuously with built-in scheduler"
+echo "4) Exit"
+
+read -p "Choose option (1-4): " choice
+
+case $choice in
+ 1)
+ echo "Running once..."
+ node src/index.js --once
+ ;;
+ 2)
+ echo "Setting up system cron job..."
+ SCRIPT_PATH="$(cd "$(dirname "$0")" && pwd)/src/index.js"
+ CRON_CMD="0 */6 * * * cd $(pwd) && /usr/bin/node $SCRIPT_PATH --once >> $(pwd)/logs/cron.log 2>&1"
+
+ (crontab -l 2>/dev/null | grep -v "animatemuseumposts"; echo "$CRON_CMD") | crontab -
+
+ echo "Cron job installed! It will run every 6 hours."
+ echo "To view cron jobs: crontab -l"
+ echo "To remove: crontab -l | grep -v animatemuseumposts | crontab -"
+ ;;
+ 3)
+ echo "Starting continuous scheduler..."
+ node src/index.js
+ ;;
+ 4)
+ echo "Exiting..."
+ exit 0
+ ;;
+ *)
+ echo "Invalid option"
+ exit 1
+ ;;
+esac
\ No newline at end of file
diff --git a/src/addBranding.js b/src/addBranding.js
new file mode 100644
index 0000000..4250519
--- /dev/null
+++ b/src/addBranding.js
@@ -0,0 +1,241 @@
+import { execSync, spawn } from 'child_process';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import https from 'https';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const ASSETS_DIR = path.join(__dirname, '..', 'assets');
+const OUTPUT_DIR = path.join(__dirname, '..', 'output');
+
+// Ensure assets directory exists
+if (!fs.existsSync(ASSETS_DIR)) {
+ fs.mkdirSync(ASSETS_DIR, { recursive: true });
+}
+
+/**
+ * Download royalty-free jazz music if not exists
+ */
+async function ensureJazzMusic() {
+ const musicPath = path.join(ASSETS_DIR, 'jazz_loop.mp3');
+
+ if (fs.existsSync(musicPath)) {
+ return musicPath;
+ }
+
+ console.log('Downloading royalty-free jazz music...');
+
+ // Use a simple tone generation as fallback (no copyright issues)
+ // Create a simple ambient sound using ffmpeg
+ try {
+ execSync(`ffmpeg -f lavfi -i "sine=frequency=440:duration=5" -af "volume=0.3,lowpass=f=800" -y "${musicPath}"`, {
+ stdio: 'pipe'
+ });
+ console.log('Created ambient intro sound');
+ } catch (e) {
+ console.log('Could not create audio, continuing without music');
+ return null;
+ }
+
+ return musicPath;
+}
+
+/**
+ * Create intro video with GoodQuestion.AI branding
+ */
+async function createIntroVideo() {
+ const introPath = path.join(ASSETS_DIR, 'intro.mp4');
+
+ // Create intro video with text overlay
+ // 1-second intro with quick fade
+ const ffmpegCmd = `ffmpeg -y -f lavfi -i "color=c=black:s=1280x720:d=1" \
+ -vf "drawtext=text='GoodQuestion.AI':fontcolor=white:fontsize=72:x=(w-text_w)/2:y=(h-text_h)/2:alpha='if(lt(t,0.2),t*5,1)'" \
+ -c:v libx264 -pix_fmt yuv420p -t 1 "${introPath}"`;
+
+ try {
+ execSync(ffmpegCmd, { stdio: 'pipe', shell: true });
+ console.log('Created intro video');
+ return introPath;
+ } catch (e) {
+ console.error('Failed to create intro:', e.message);
+ return null;
+ }
+}
+
+/**
+ * Create outro video with GoodQuestion.AI branding
+ */
+async function createOutroVideo() {
+ const outroPath = path.join(ASSETS_DIR, 'outro.mp4');
+
+ // Create outro video with call to action
+ // 1-second outro
+ const ffmpegCmd = `ffmpeg -y -f lavfi -i "color=c=black:s=1280x720:d=1" \
+ -vf "drawtext=text='@goodquestionai':fontcolor=cyan:fontsize=48:x=(w-text_w)/2:y=(h-text_h)/2:alpha='if(lt(t,0.2),t*5,1)'" \
+ -c:v libx264 -pix_fmt yuv420p -t 1 "${outroPath}"`;
+
+ try {
+ execSync(ffmpegCmd, { stdio: 'pipe', shell: true });
+ console.log('Created outro video');
+ return outroPath;
+ } catch (e) {
+ console.error('Failed to create outro:', e.message);
+ return null;
+ }
+}
+
+/**
+ * Add branding intro/outro to video
+ * Extends main video to 10 seconds, adds 3s intro + 3s outro = 16s total
+ */
+export async function addBrandingToVideo(videoPath, metadata) {
+ try {
+ console.log('Adding GoodQuestion.AI branding...');
+
+ // Check if video exists and has valid duration
+ const probeCmd = `ffprobe -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`;
+ let duration;
+ try {
+ duration = parseFloat(execSync(probeCmd, { stdio: 'pipe' }).toString().trim());
+ } catch (e) {
+ console.log('Could not probe video, skipping branding');
+ return videoPath;
+ }
+
+ if (duration < 0.5) {
+ console.log('Video too short for branding, skipping');
+ return videoPath;
+ }
+
+ // Extend video to 10 seconds if shorter
+ const TARGET_DURATION = 10;
+ if (duration < TARGET_DURATION) {
+ console.log(`Extending video from ${duration.toFixed(1)}s to ${TARGET_DURATION}s...`);
+ const extendedPath = videoPath.replace('.mp4', '_extended.mp4');
+
+ // Calculate slowdown factor or loop count
+ const slowFactor = Math.min(duration / TARGET_DURATION, 0.5); // Max 2x slowdown
+ const loopCount = Math.ceil(TARGET_DURATION / duration);
+
+ // Use a combination: slow down slightly + loop to reach 10s
+ const slowSpeed = Math.max(0.5, duration / TARGET_DURATION);
+
+ try {
+ // Method: Slow down + loop + trim to exactly 10 seconds
+ execSync(`ffmpeg -y -stream_loop ${loopCount} -i "${videoPath}" -vf "setpts=${1/slowSpeed}*PTS" -t ${TARGET_DURATION} -c:v libx264 -pix_fmt yuv420p "${extendedPath}"`, {
+ stdio: 'pipe',
+ timeout: 60000
+ });
+ fs.unlinkSync(videoPath);
+ fs.renameSync(extendedPath, videoPath);
+ console.log(`Video extended to ${TARGET_DURATION}s`);
+ } catch (e) {
+ console.log('Could not extend video, using original');
+ }
+ }
+
+ // Create intro and outro if they don't exist
+ const introPath = await createIntroVideo();
+ const outroPath = await createOutroVideo();
+
+ if (!introPath || !outroPath) {
+ console.log('Could not create branding videos, using original');
+ return videoPath;
+ }
+
+ // Get video dimensions
+ const dimensionsCmd = `ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 "${videoPath}"`;
+ let width = 1280, height = 720;
+ try {
+ const dims = execSync(dimensionsCmd, { stdio: 'pipe' }).toString().trim().split(',');
+ width = parseInt(dims[0]) || 1280;
+ height = parseInt(dims[1]) || 720;
+ } catch (e) {}
+
+ // Create concat file
+ const timestamp = Date.now();
+ const concatFile = path.join(OUTPUT_DIR, `concat_${timestamp}.txt`);
+ const brandedPath = path.join(OUTPUT_DIR, `branded_${timestamp}.mp4`);
+
+ // Scale all videos to same size and concatenate
+ const scaledIntro = path.join(OUTPUT_DIR, `scaled_intro_${timestamp}.mp4`);
+ const scaledMain = path.join(OUTPUT_DIR, `scaled_main_${timestamp}.mp4`);
+ const scaledOutro = path.join(OUTPUT_DIR, `scaled_outro_${timestamp}.mp4`);
+
+ // Scale intro
+ execSync(`ffmpeg -y -i "${introPath}" -vf "scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2" -c:v libx264 -pix_fmt yuv420p "${scaledIntro}"`, { stdio: 'pipe' });
+
+ // Scale main video
+ execSync(`ffmpeg -y -i "${videoPath}" -vf "scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2" -c:v libx264 -pix_fmt yuv420p "${scaledMain}"`, { stdio: 'pipe' });
+
+ // Scale outro
+ execSync(`ffmpeg -y -i "${outroPath}" -vf "scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2" -c:v libx264 -pix_fmt yuv420p "${scaledOutro}"`, { stdio: 'pipe' });
+
+ // Create concat file
+ fs.writeFileSync(concatFile, `file '${scaledIntro}'\nfile '${scaledMain}'\nfile '${scaledOutro}'`);
+
+ // Concatenate videos
+ execSync(`ffmpeg -y -f concat -safe 0 -i "${concatFile}" -c:v libx264 -pix_fmt yuv420p "${brandedPath}"`, { stdio: 'pipe' });
+
+ // Cleanup temp files
+ [concatFile, scaledIntro, scaledMain, scaledOutro].forEach(f => {
+ try { fs.unlinkSync(f); } catch (e) {}
+ });
+
+ // Replace original with branded version
+ fs.unlinkSync(videoPath);
+ fs.renameSync(brandedPath, videoPath);
+
+ console.log('Added GoodQuestion.AI branding successfully!');
+ return videoPath;
+
+ } catch (error) {
+ console.error('Branding failed:', error.message);
+ return videoPath; // Return original on failure
+ }
+}
+
+/**
+ * Add text overlay with artwork info
+ */
+export async function addArtworkOverlay(videoPath, metadata) {
+ try {
+ const title = (metadata.title || 'Artwork').replace(/'/g, "\\'").substring(0, 40);
+ const artist = (metadata.artist || 'Unknown').replace(/'/g, "\\'").substring(0, 30);
+
+ const timestamp = Date.now();
+ const overlayPath = path.join(OUTPUT_DIR, `overlay_${timestamp}.mp4`);
+
+ // Add subtle text overlay at bottom
+ const ffmpegCmd = `ffmpeg -y -i "${videoPath}" \
+ -vf "drawtext=text='${title}':fontcolor=white:fontsize=24:x=20:y=h-60:alpha=0.8,\
+ drawtext=text='${artist}':fontcolor=gray:fontsize=18:x=20:y=h-30:alpha=0.7" \
+ -c:v libx264 -pix_fmt yuv420p "${overlayPath}"`;
+
+ execSync(ffmpegCmd, { stdio: 'pipe', shell: true });
+
+ fs.unlinkSync(videoPath);
+ fs.renameSync(overlayPath, videoPath);
+
+ console.log('Added artwork overlay');
+ return videoPath;
+
+ } catch (error) {
+ console.error('Overlay failed:', error.message);
+ return videoPath;
+ }
+}
+
+// Test if run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ const testVideo = process.argv[2];
+ if (testVideo) {
+ addBrandingToVideo(testVideo, { title: 'Test', artist: 'Test' })
+ .then(r => console.log('Branded video:', r))
+ .catch(e => console.error('Error:', e));
+ } else {
+ console.log('Usage: node addBranding.js <video-path>');
+ }
+}
diff --git a/src/animateKenBurns.js b/src/animateKenBurns.js
new file mode 100644
index 0000000..1e0566e
--- /dev/null
+++ b/src/animateKenBurns.js
@@ -0,0 +1,130 @@
+import sharp from 'sharp';
+import GIFEncoder from 'gif-encoder-2';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+/**
+ * Create Ken Burns effect (pan/zoom) animation locally
+ * No external API required - uses sharp for image manipulation
+ */
+export async function createKenBurnsAnimation(imagePath, metadata) {
+ try {
+ console.log('Creating Ken Burns animation locally...');
+
+ const FRAME_COUNT = 30;
+ const FPS = 6;
+ const DURATION = 5; // 5 seconds
+
+ // Get image dimensions
+ const imageInfo = await sharp(imagePath).metadata();
+ const { width, height } = imageInfo;
+
+ // Target output size
+ const outputWidth = 800;
+ const outputHeight = 600;
+
+ // Create GIF encoder
+ const encoder = new GIFEncoder(outputWidth, outputHeight, 'neuquant', true);
+ const outputDir = path.join(__dirname, '..', 'output');
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ const timestamp = Date.now();
+ const gifPath = path.join(outputDir, `kenburns_${timestamp}.gif`);
+ const writeStream = fs.createWriteStream(gifPath);
+
+ encoder.createReadStream().pipe(writeStream);
+ encoder.start();
+ encoder.setDelay(1000 / FPS);
+ encoder.setQuality(10);
+ encoder.setRepeat(0); // Loop forever
+
+ // Ken Burns parameters - slow zoom in with slight pan
+ const startScale = 1.0;
+ const endScale = 1.15; // 15% zoom
+ const panX = width * 0.05; // 5% horizontal pan
+ const panY = height * 0.03; // 3% vertical pan
+
+ console.log(`Generating ${FRAME_COUNT} frames...`);
+
+ for (let i = 0; i < FRAME_COUNT; i++) {
+ const progress = i / (FRAME_COUNT - 1);
+
+ // Calculate current scale and position
+ const scale = startScale + (endScale - startScale) * progress;
+ const cropWidth = Math.floor(width / scale);
+ const cropHeight = Math.floor(height / scale);
+
+ // Center with slight pan
+ const left = Math.floor((width - cropWidth) / 2 + panX * progress);
+ const top = Math.floor((height - cropHeight) / 2 + panY * progress);
+
+ // Ensure we don't go out of bounds
+ const safeLeft = Math.max(0, Math.min(left, width - cropWidth));
+ const safeTop = Math.max(0, Math.min(top, height - cropHeight));
+
+ // Extract and resize frame
+ const frameBuffer = await sharp(imagePath)
+ .extract({
+ left: safeLeft,
+ top: safeTop,
+ width: cropWidth,
+ height: cropHeight
+ })
+ .resize(outputWidth, outputHeight)
+ .raw()
+ .toBuffer();
+
+ // Add frame to GIF
+ encoder.addFrame(frameBuffer);
+ }
+
+ encoder.finish();
+
+ // Wait for write to complete
+ await new Promise((resolve, reject) => {
+ writeStream.on('finish', resolve);
+ writeStream.on('error', reject);
+ });
+
+ const stats = fs.statSync(gifPath);
+ console.log(`Ken Burns GIF created: ${(stats.size / 1024).toFixed(1)} KB`);
+
+ return {
+ originalImage: imagePath,
+ animatedImage: gifPath,
+ artworkMetadata: metadata,
+ isAnimated: true,
+ animationType: 'ken-burns',
+ format: 'gif',
+ frames: FRAME_COUNT,
+ duration: DURATION
+ };
+
+ } catch (error) {
+ console.error('Ken Burns animation failed:', error.message);
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ error: error.message
+ };
+ }
+}
+
+// Test if run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ const testImage = process.argv[2] || path.join(__dirname, '..', 'output', 'artwork_test.jpg');
+ if (fs.existsSync(testImage)) {
+ createKenBurnsAnimation(testImage, { title: 'Test' })
+ .then(r => console.log('Result:', r))
+ .catch(e => console.error('Failed:', e));
+ } else {
+ console.log('No test image found. Usage: node animateKenBurns.js <image-path>');
+ }
+}
diff --git a/src/animateWithAI.js b/src/animateWithAI.js
new file mode 100644
index 0000000..f558426
--- /dev/null
+++ b/src/animateWithAI.js
@@ -0,0 +1,154 @@
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import sharp from 'sharp';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Free AI animation options
+const ANIMATION_SERVICES = {
+ // LeiaPix - Free depth-based animation (no API key needed)
+ LEIAPIX: {
+ url: 'https://www.immersity.ai/api/v1/animation',
+ free: true
+ },
+ // MyHeritage AI - Free for limited use
+ MYHERITAGE: {
+ url: 'https://www.myheritage.com/deep-nostalgia/api',
+ free: true
+ },
+ // Replicate - Pay per use but very cheap ($0.02 per generation)
+ REPLICATE: {
+ url: 'https://api.replicate.com/v1/predictions',
+ model: 'stability-ai/stable-video-diffusion',
+ free: false
+ }
+};
+
+export async function animateWithFreeAI(imagePath, metadata) {
+ try {
+ console.log('Attempting free AI animation...');
+
+ // Option 1: Try Immersity AI (LeiaPix) - Creates 3D depth animation
+ try {
+ console.log('Trying Immersity AI for 3D depth animation...');
+
+ // This service creates a 3D depth map and animates it
+ // Note: This is a simplified example - the actual API might require registration
+ const imageBuffer = fs.readFileSync(imagePath);
+ const base64Image = imageBuffer.toString('base64');
+
+ // For Immersity AI, we'd need to:
+ // 1. Upload the image
+ // 2. Wait for processing
+ // 3. Download the animated result
+
+ // Since this requires web scraping or unofficial API, let's try another approach
+
+ } catch (error) {
+ console.log('Immersity AI not available');
+ }
+
+ // Option 2: Use Stable Video Diffusion via Replicate (very cheap but not free)
+ if (process.env.REPLICATE_API_TOKEN) {
+ console.log('Using Replicate Stable Video Diffusion...');
+
+ const response = await axios.post(
+ 'https://api.replicate.com/v1/predictions',
+ {
+ version: 'stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438',
+ input: {
+ input_image: `data:image/jpeg;base64,${fs.readFileSync(imagePath).toString('base64')}`,
+ frames_per_second: 7,
+ motion_bucket_id: 127,
+ cond_aug: 0.02,
+ decoding_t: 7,
+ seed: Math.floor(Math.random() * 1000000)
+ }
+ },
+ {
+ headers: {
+ 'Authorization': `Token ${process.env.REPLICATE_API_TOKEN}`,
+ 'Content-Type': 'application/json'
+ }
+ }
+ );
+
+ // Poll for completion
+ let prediction = response.data;
+ while (prediction.status !== 'succeeded' && prediction.status !== 'failed') {
+ await new Promise(resolve => setTimeout(resolve, 1000));
+ const checkResponse = await axios.get(
+ `https://api.replicate.com/v1/predictions/${prediction.id}`,
+ {
+ headers: {
+ 'Authorization': `Token ${process.env.REPLICATE_API_TOKEN}`
+ }
+ }
+ );
+ prediction = checkResponse.data;
+ }
+
+ if (prediction.status === 'succeeded' && prediction.output) {
+ // Download the video
+ const videoResponse = await axios.get(prediction.output, {
+ responseType: 'arraybuffer'
+ });
+
+ const outputDir = path.join(__dirname, '..', 'output');
+ const timestamp = new Date().getTime();
+ const videoPath = path.join(outputDir, `animated_${timestamp}.mp4`);
+
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ console.log('AI-animated video created successfully!');
+ return {
+ originalImage: imagePath,
+ animatedVideo: videoPath,
+ artworkMetadata: metadata,
+ isAnimated: true,
+ animationType: 'ai-video'
+ };
+ }
+ }
+
+ // Option 3: Create a cinemagraph-style animation with depth estimation
+ console.log('Creating depth-based animation effect...');
+
+ // This would use depth estimation to create parallax effect
+ // For now, returning original as we need proper AI service
+
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ needsAIService: true
+ };
+
+ } catch (error) {
+ console.error('Error in AI animation:', error.message);
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ error: error.message
+ };
+ }
+}
+
+// Helper function to create depth-based animation locally
+export async function createDepthAnimation(imagePath) {
+ // This would:
+ // 1. Use MiDaS or DPT for depth estimation
+ // 2. Create layers based on depth
+ // 3. Animate layers with parallax
+ // But requires Python integration or pre-trained models
+
+ console.log('Depth animation requires additional setup');
+ return null;
+}
\ No newline at end of file
diff --git a/src/animateWithGrok.js b/src/animateWithGrok.js
new file mode 100644
index 0000000..b8a7fbf
--- /dev/null
+++ b/src/animateWithGrok.js
@@ -0,0 +1,179 @@
+import { createLumaAnimation } from './animateWithLuma.js';
+import { createHuggingFaceAnimation } from './animateWithHuggingFace.js';
+import { createRunwayAnimation } from './animateWithRunway.js';
+import { createRealAIVideo } from './animateWithReplicate.js';
+import { createKenBurnsAnimation } from './animateKenBurns.js';
+import { addBrandingToVideo, addArtworkOverlay } from './addBranding.js';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Track animation service success rates
+const STATS_FILE = path.join(__dirname, '..', 'logs', 'animation-stats.json');
+
+function loadStats() {
+ try {
+ if (fs.existsSync(STATS_FILE)) {
+ return JSON.parse(fs.readFileSync(STATS_FILE, 'utf-8'));
+ }
+ } catch (e) {}
+ return {
+ luma: { attempts: 0, successes: 0 },
+ huggingface: { attempts: 0, successes: 0 },
+ runway: { attempts: 0, successes: 0 },
+ replicate: { attempts: 0, successes: 0 },
+ kenburns: { attempts: 0, successes: 0 },
+ static: { attempts: 0, successes: 0 }
+ };
+}
+
+function saveStats(stats) {
+ const logsDir = path.join(__dirname, '..', 'logs');
+ if (!fs.existsSync(logsDir)) {
+ fs.mkdirSync(logsDir, { recursive: true });
+ }
+ fs.writeFileSync(STATS_FILE, JSON.stringify(stats, null, 2));
+}
+
+function recordAttempt(service, success) {
+ const stats = loadStats();
+ // Ensure the service exists in stats
+ if (!stats[service]) {
+ stats[service] = { attempts: 0, successes: 0 };
+ }
+ stats[service].attempts++;
+ if (success) stats[service].successes++;
+ saveStats(stats);
+}
+
+/**
+ * Animation cascade - tries services in order of cost/quality
+ * Order: HuggingFace (free) → Runway (quality) → Replicate (reliable) → KenBurns (local) → Static
+ */
+export async function animateArtwork(imagePath, metadata) {
+ console.log('Starting animation cascade...');
+ console.log('Goal: Make artwork COME ALIVE with real animation!');
+
+ // 0. Skip Luma for now (single frame output issue)
+ // TODO: Fix Luma model to generate actual video
+ console.log('[0/5] Skipping Luma (output issues, using SVD instead)');
+
+ // 1. Try HuggingFace (free tier)
+ if (process.env.HUGGINGFACE_TOKEN) {
+ console.log('\n[1/5] Trying HuggingFace (free)...');
+ try {
+ const result = await withTimeout(
+ createHuggingFaceAnimation(imagePath, metadata),
+ 60000,
+ 'HuggingFace timeout'
+ );
+ if (result.isAnimated) {
+ recordAttempt('huggingface', true);
+ console.log('HuggingFace animation succeeded!');
+ return result;
+ }
+ recordAttempt('huggingface', false);
+ } catch (error) {
+ console.log('HuggingFace failed:', error.message);
+ recordAttempt('huggingface', false);
+ }
+ } else {
+ console.log('[1/5] Skipping HuggingFace (no token)');
+ }
+
+ // 2. Try RunwayML (premium quality)
+ if (process.env.RUNWAY_API_TOKEN) {
+ console.log('\n[2/5] Trying RunwayML (premium)...');
+ try {
+ const result = await withTimeout(
+ createRunwayAnimation(imagePath, metadata),
+ 180000, // 3 min for Runway
+ 'RunwayML timeout'
+ );
+ if (result.isAnimated) {
+ recordAttempt('runway', true);
+ console.log('RunwayML animation succeeded!');
+ return result;
+ }
+ recordAttempt('runway', false);
+ } catch (error) {
+ console.log('RunwayML failed:', error.message);
+ recordAttempt('runway', false);
+ }
+ } else {
+ console.log('[2/5] Skipping RunwayML (no token)');
+ }
+
+ // 3. Try Replicate (reliable, $0.02/video) - Real AI animation!
+ if (process.env.REPLICATE_API_TOKEN) {
+ console.log('\n[3/5] Trying Replicate SVD ($0.02) - Real AI animation...');
+ try {
+ const result = await withTimeout(
+ createRealAIVideo(imagePath, metadata),
+ 200000, // 3+ minutes for AI video generation
+ 'Replicate timeout'
+ );
+ if (result.isAnimated && result.animatedVideo) {
+ recordAttempt('replicate', true);
+ console.log('Replicate animation succeeded!');
+ // Add GoodQuestion.AI branding
+ await addBrandingToVideo(result.animatedVideo, metadata);
+ return result;
+ }
+ recordAttempt('replicate', false);
+ } catch (error) {
+ console.log('Replicate failed:', error.message);
+ recordAttempt('replicate', false);
+ }
+ } else {
+ console.log('[3/5] Skipping Replicate (no token)');
+ }
+
+ // 4. Try Ken Burns (local, free)
+ console.log('\n[4/5] Trying Ken Burns (local)...');
+ try {
+ const result = await createKenBurnsAnimation(imagePath, metadata);
+ if (result.isAnimated) {
+ recordAttempt('kenburns', true);
+ console.log('Ken Burns animation succeeded!');
+ return result;
+ }
+ recordAttempt('kenburns', false);
+ } catch (error) {
+ console.log('Ken Burns failed:', error.message);
+ recordAttempt('kenburns', false);
+ }
+
+ // 5. Final fallback - static image
+ console.log('\n[5/5] Using static image (all animations failed)');
+ recordAttempt('static', true);
+
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ animationType: 'static'
+ };
+}
+
+// Helper function with timeout
+async function withTimeout(promise, ms, message) {
+ return Promise.race([
+ promise,
+ new Promise((_, reject) => setTimeout(() => reject(new Error(message)), ms))
+ ]);
+}
+
+export function createSimpleAnimation(imagePath, metadata) {
+ return animateArtwork(imagePath, metadata);
+}
+
+export function getAnimationStats() {
+ return loadStats();
+}
diff --git a/src/animateWithGrokFrames.js b/src/animateWithGrokFrames.js
new file mode 100644
index 0000000..e83ca69
--- /dev/null
+++ b/src/animateWithGrokFrames.js
@@ -0,0 +1,206 @@
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import GIFEncoder from 'gif-encoder-2';
+import sharp from 'sharp';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const GROK_API_KEY = process.env.GROK_API_KEY;
+const GROK_API_URL = 'https://api.x.ai/v1';
+
+export async function createAIAnimation(imagePath, metadata) {
+ try {
+ if (!GROK_API_KEY) {
+ throw new Error('GROK_API_KEY not configured');
+ }
+
+ console.log('Creating AI animation using Grok vision capabilities...');
+
+ const imageBuffer = fs.readFileSync(imagePath);
+ const base64Image = imageBuffer.toString('base64');
+
+ // Ask Grok to describe how the painting should be animated
+ const animationResponse = await axios.post(
+ `${GROK_API_URL}/chat/completions`,
+ {
+ model: 'grok-2-vision-1212',
+ messages: [
+ {
+ role: 'system',
+ content: 'You are an expert at bringing classical paintings to life through animation.'
+ },
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ text: `Analyze this painting "${metadata.title}" and describe exactly how to animate it. Focus on one subtle movement that would bring it to life. For example: water rippling, flames flickering, fabric swaying, eyes blinking, or light changing. Be specific about what moves and how. Keep it under 50 words.`
+ },
+ {
+ type: 'image_url',
+ image_url: {
+ url: `data:image/jpeg;base64,${base64Image}`
+ }
+ }
+ ]
+ }
+ ],
+ max_tokens: 100,
+ temperature: 0.7
+ },
+ {
+ headers: {
+ 'Authorization': `Bearer ${GROK_API_KEY}`,
+ 'Content-Type': 'application/json'
+ }
+ }
+ );
+
+ const animationConcept = animationResponse.data.choices[0].message.content;
+ console.log('Animation concept:', animationConcept);
+
+ // Since Grok can't generate actual video, we'll create a sophisticated animated GIF
+ // with effects that simulate the suggested animation
+ const outputDir = path.join(__dirname, '..', 'output');
+ const timestamp = new Date().getTime();
+ const outputPath = path.join(outputDir, `ai_animated_${timestamp}.gif`);
+
+ const width = 500;
+ const height = 500;
+ const frameCount = 20;
+
+ // Load and prepare the base image
+ const baseImage = await sharp(imagePath)
+ .resize(width, height, { fit: 'cover' })
+ .jpeg()
+ .toBuffer();
+
+ // Create GIF encoder
+ const encoder = new GIFEncoder(width, height, 'neuquant');
+ encoder.setDelay(100); // 100ms between frames
+ encoder.setRepeat(0); // Loop forever
+ encoder.setQuality(10);
+
+ const stream = fs.createWriteStream(outputPath);
+ encoder.createReadStream().pipe(stream);
+ encoder.start();
+
+ // Generate frames with subtle animations based on the concept
+ for (let i = 0; i < frameCount; i++) {
+ const progress = i / frameCount;
+
+ // Create frame with effects based on animation concept
+ let frameBuffer;
+
+ if (animationConcept.toLowerCase().includes('water') || animationConcept.toLowerCase().includes('rippl')) {
+ // Water ripple effect
+ const wave = Math.sin(progress * Math.PI * 4) * 5;
+ frameBuffer = await sharp(baseImage)
+ .modulate({
+ brightness: 1 + (Math.sin(progress * Math.PI * 2) * 0.05),
+ saturation: 1 + (Math.cos(progress * Math.PI * 2) * 0.1)
+ })
+ .blur(Math.max(0.3, Math.abs(wave) * 0.3))
+ .raw()
+ .toBuffer();
+
+ } else if (animationConcept.toLowerCase().includes('flame') || animationConcept.toLowerCase().includes('fire')) {
+ // Fire flicker effect
+ frameBuffer = await sharp(baseImage)
+ .modulate({
+ brightness: 0.9 + (Math.random() * 0.2),
+ hue: Math.floor(Math.random() * 10 - 5)
+ })
+ .raw()
+ .toBuffer();
+
+ } else if (animationConcept.toLowerCase().includes('light') || animationConcept.toLowerCase().includes('glow')) {
+ // Lighting change effect
+ const lightIntensity = 0.8 + (Math.sin(progress * Math.PI * 2) * 0.3);
+ frameBuffer = await sharp(baseImage)
+ .modulate({
+ brightness: lightIntensity
+ })
+ .gamma(Math.max(1.0, Math.min(3.0, 1.0 + (Math.sin(progress * Math.PI * 2) * 0.2))))
+ .raw()
+ .toBuffer();
+
+ } else if (animationConcept.toLowerCase().includes('wind') || animationConcept.toLowerCase().includes('sway')) {
+ // Swaying effect with slight distortion
+ const skewAmount = Math.sin(progress * Math.PI * 2) * 2;
+ frameBuffer = await sharp(baseImage)
+ .rotate(skewAmount, { background: { r: 0, g: 0, b: 0, alpha: 0 } })
+ .resize(width, height, { fit: 'cover' })
+ .raw()
+ .toBuffer();
+
+ } else {
+ // Default: subtle zoom and brightness pulse
+ const zoomFactor = 1 + (Math.sin(progress * Math.PI * 2) * 0.02);
+ const size = Math.round(width * zoomFactor);
+
+ frameBuffer = await sharp(baseImage)
+ .resize(size, size, { fit: 'cover' })
+ .extract({
+ left: Math.round((size - width) / 2),
+ top: Math.round((size - height) / 2),
+ width: width,
+ height: height
+ })
+ .modulate({
+ brightness: 0.95 + (Math.sin(progress * Math.PI * 2) * 0.05)
+ })
+ .raw()
+ .toBuffer();
+ }
+
+ // Convert to RGBA for GIF encoder
+ const rgbaBuffer = Buffer.alloc(width * height * 4);
+ for (let j = 0; j < frameBuffer.length; j += 3) {
+ const pixelIndex = Math.floor(j / 3) * 4;
+ rgbaBuffer[pixelIndex] = frameBuffer[j]; // R
+ rgbaBuffer[pixelIndex + 1] = frameBuffer[j + 1]; // G
+ rgbaBuffer[pixelIndex + 2] = frameBuffer[j + 2]; // B
+ rgbaBuffer[pixelIndex + 3] = 255; // A
+ }
+
+ encoder.addFrame(rgbaBuffer);
+ }
+
+ encoder.finish();
+
+ return new Promise((resolve) => {
+ stream.on('finish', () => {
+ console.log(`AI-animated GIF created: ${outputPath}`);
+
+ // Check file size
+ const stats = fs.statSync(outputPath);
+ console.log(`File size: ${(stats.size / 1024).toFixed(2)} KB`);
+
+ resolve({
+ originalImage: imagePath,
+ animatedImage: outputPath,
+ artworkMetadata: metadata,
+ isAnimated: true,
+ animationType: 'ai-enhanced',
+ animationConcept: animationConcept
+ });
+ });
+ });
+
+ } catch (error) {
+ console.error('Error creating AI animation:', error.message);
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ error: error.message
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/animateWithHuggingFace.js b/src/animateWithHuggingFace.js
new file mode 100644
index 0000000..6f46900
--- /dev/null
+++ b/src/animateWithHuggingFace.js
@@ -0,0 +1,172 @@
+import { HfInference } from '@huggingface/inference';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import axios from 'axios';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export async function createHuggingFaceAnimation(imagePath, metadata) {
+ try {
+ const HF_TOKEN = process.env.HUGGINGFACE_TOKEN;
+
+ if (!HF_TOKEN) {
+ throw new Error('HUGGINGFACE_TOKEN not configured - get it from https://huggingface.co/settings/tokens');
+ }
+
+ console.log('Creating animation with Hugging Face models...');
+
+ const hf = new HfInference(HF_TOKEN);
+ const TIMEOUT = 60000; // 60 second timeout
+
+ // Read the image
+ const imageBuffer = fs.readFileSync(imagePath);
+
+ // Try different video generation models on Hugging Face
+ console.log('Attempting video generation...');
+
+ try {
+ // Option 1: Try Stable Video Diffusion on HuggingFace (PRO required)
+ console.log('Trying Stable Video Diffusion on HuggingFace PRO...');
+ const response = await hf.imageToVideo({
+ model: 'stabilityai/stable-video-diffusion-img2vid-xt',
+ inputs: imageBuffer,
+ parameters: {
+ num_frames: 25,
+ motion_bucket_id: 180, // Balanced motion (not too chaotic)
+ fps: 7,
+ decode_chunk_size: 8
+ },
+ options: {
+ use_gpu: true,
+ wait_for_model: true
+ }
+ });
+
+ if (response) {
+ const outputDir = path.join(__dirname, '..', 'output');
+ const timestamp = new Date().getTime();
+ const videoPath = path.join(outputDir, `hf_animated_${timestamp}.mp4`);
+
+ // Save the video
+ const videoBuffer = await response.arrayBuffer();
+ fs.writeFileSync(videoPath, Buffer.from(videoBuffer));
+
+ console.log('Hugging Face video created!');
+ return {
+ originalImage: imagePath,
+ animatedVideo: videoPath,
+ artworkMetadata: metadata,
+ isAnimated: true,
+ animationType: 'huggingface-svd'
+ };
+ }
+ } catch (e) {
+ console.log('SVD error:', e.message);
+ if (e.message.includes('quota')) {
+ console.log('GPU quota exceeded - try again later');
+ } else if (e.message.includes('Pro subscription')) {
+ console.log('Model requires PRO subscription');
+ }
+ }
+
+ // Option 2: Try AnimateDiff
+ try {
+ const response = await hf.textToVideo({
+ model: 'ByteDance/AnimateDiff-Lightning',
+ inputs: `animate the painting ${metadata.title}, make elements move naturally, flowers swaying, no camera movement`,
+ parameters: {
+ num_frames: 16,
+ guidance_scale: 7.5
+ }
+ });
+
+ if (response) {
+ const outputDir = path.join(__dirname, '..', 'output');
+ const timestamp = new Date().getTime();
+ const videoPath = path.join(outputDir, `hf_animated_${timestamp}.mp4`);
+
+ const videoBuffer = await response.arrayBuffer();
+ fs.writeFileSync(videoPath, Buffer.from(videoBuffer));
+
+ console.log('AnimateDiff video created!');
+ return {
+ originalImage: imagePath,
+ animatedVideo: videoPath,
+ artworkMetadata: metadata,
+ isAnimated: true,
+ animationType: 'animatediff'
+ };
+ }
+ } catch (e) {
+ console.log('AnimateDiff not available');
+ }
+
+ // Option 3: Use image-to-image to create animation frames
+ console.log('Creating animation frames with img2img...');
+
+ const frames = [];
+ const frameCount = 10;
+
+ for (let i = 0; i < frameCount; i++) {
+ const prompt = `${metadata.title} painting with subtle movement, frame ${i + 1} of animation sequence, slight variation`;
+
+ try {
+ const response = await hf.imageToImage({
+ model: 'stabilityai/stable-diffusion-xl-refiner-1.0',
+ inputs: imageBuffer,
+ parameters: {
+ prompt: prompt,
+ strength: 0.3, // Low strength to keep it similar
+ guidance_scale: 7
+ }
+ });
+
+ if (response) {
+ frames.push(await response.arrayBuffer());
+ }
+ } catch (e) {
+ console.log(`Frame ${i} failed`);
+ }
+ }
+
+ if (frames.length > 5) {
+ // Convert frames to video (would need ffmpeg or similar)
+ console.log(`Generated ${frames.length} frames for animation`);
+
+ // For now, return the original
+ return {
+ originalImage: imagePath,
+ artworkMetadata: metadata,
+ isAnimated: false,
+ frames: frames.length,
+ needsVideoCompilation: true
+ };
+ }
+
+ throw new Error('No animation method succeeded');
+
+ } catch (error) {
+ console.error('Error with Hugging Face animation:', error.message);
+
+ if (error.message.includes('HUGGINGFACE_TOKEN')) {
+ console.error('\n⚠️ To use Hugging Face:');
+ console.error('1. Go to https://huggingface.co/join');
+ console.error('2. Sign up (free)');
+ console.error('3. Go to https://huggingface.co/settings/tokens');
+ console.error('4. Create new token');
+ console.error('5. Add to .env: HUGGINGFACE_TOKEN=hf_xxxxx\n');
+ }
+
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ error: error.message
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/animateWithLuma.js b/src/animateWithLuma.js
new file mode 100644
index 0000000..d9cdd62
--- /dev/null
+++ b/src/animateWithLuma.js
@@ -0,0 +1,260 @@
+import Replicate from 'replicate';
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { execSync } from 'child_process';
+import sharp from 'sharp';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+/**
+ * Generate a creative animation prompt based on artwork content
+ */
+function generateAnimationPrompt(metadata) {
+ const title = (metadata.title || '').toLowerCase();
+ const description = (metadata.description || '').toLowerCase();
+ const department = (metadata.department || '').toLowerCase();
+
+ // Analyze content and generate appropriate animation prompts
+ const prompts = [];
+
+ // Portrait/figure detection
+ if (title.includes('portrait') || title.includes('head') || title.includes('bust') ||
+ title.includes('figure') || title.includes('man') || title.includes('woman') ||
+ title.includes('child') || department.includes('portrait')) {
+ prompts.push(
+ "the subject's eyes slowly blink and look around, subtle breathing movement, slight head turn",
+ "gentle eye movement, subtle smile forming, natural breathing animation",
+ "the figure comes to life with blinking eyes and subtle facial expressions"
+ );
+ }
+
+ // Landscape/nature detection
+ if (title.includes('landscape') || title.includes('garden') || title.includes('forest') ||
+ title.includes('river') || title.includes('lake') || title.includes('mountain') ||
+ title.includes('tree') || title.includes('flower')) {
+ prompts.push(
+ "wind gently moves through the trees, clouds drift slowly, water ripples",
+ "leaves rustle in the breeze, sunlight shifts through clouds, nature comes alive",
+ "gentle wind animation, flowing water, moving clouds, swaying plants"
+ );
+ }
+
+ // Animal detection
+ if (title.includes('horse') || title.includes('dog') || title.includes('cat') ||
+ title.includes('bird') || title.includes('lion') || title.includes('stork') ||
+ title.includes('animal') || department.includes('animal')) {
+ prompts.push(
+ "the animal breathes, blinks, and makes subtle natural movements",
+ "gentle breathing, tail or ear movement, the creature comes to life",
+ "realistic animal animation with breathing and natural movement"
+ );
+ }
+
+ // Water/maritime detection
+ if (title.includes('sea') || title.includes('ocean') || title.includes('ship') ||
+ title.includes('boat') || title.includes('water') || title.includes('wave')) {
+ prompts.push(
+ "waves gently roll, water sparkles, boats rock gently on the water",
+ "ocean waves in motion, reflections shimmer, maritime scene comes alive"
+ );
+ }
+
+ // Still life detection
+ if (title.includes('still life') || title.includes('vase') || title.includes('flower') ||
+ title.includes('fruit') || title.includes('table')) {
+ prompts.push(
+ "subtle lighting shifts, flower petals gently sway, dust motes float in light",
+ "candlelight flickers, shadows shift, delicate movement in the scene"
+ );
+ }
+
+ // Mask/sculpture detection
+ if (title.includes('mask') || title.includes('sculpture') || title.includes('statue') ||
+ title.includes('memorial') || title.includes('head')) {
+ prompts.push(
+ "the mask suddenly comes alive with glowing eyes and speaking mouth",
+ "mystical animation where the sculpture begins to breathe and look around",
+ "dramatic awakening - the figure opens its eyes and starts to speak"
+ );
+ }
+
+ // Default fallback
+ if (prompts.length === 0) {
+ prompts.push(
+ "subtle movement brings the artwork to life, gentle animation",
+ "cinematic camera movement, lighting shifts, scene comes alive",
+ "dramatic animation with natural movement and atmospheric effects"
+ );
+ }
+
+ // Return random prompt from generated options
+ return prompts[Math.floor(Math.random() * prompts.length)];
+}
+
+export async function createLumaAnimation(imagePath, metadata) {
+ try {
+ const REPLICATE_API_TOKEN = process.env.REPLICATE_API_TOKEN;
+
+ if (!REPLICATE_API_TOKEN) {
+ throw new Error('REPLICATE_API_TOKEN not configured');
+ }
+
+ console.log('Creating animation with Luma Dream Machine...');
+ console.log('This will make the artwork come ALIVE!');
+
+ const replicate = new Replicate({
+ auth: REPLICATE_API_TOKEN,
+ });
+
+ // Prepare image - Luma works well with various resolutions
+ const optimizedImagePath = path.join(path.dirname(imagePath), `luma_${path.basename(imagePath)}`);
+ await sharp(imagePath)
+ .resize(1280, 720, {
+ fit: 'inside',
+ withoutEnlargement: false
+ })
+ .jpeg({ quality: 95 })
+ .toFile(optimizedImagePath);
+
+ // Convert to base64 data URI
+ const imageBuffer = fs.readFileSync(optimizedImagePath);
+ const base64Image = imageBuffer.toString('base64');
+ const dataUri = `data:image/jpeg;base64,${base64Image}`;
+
+ // Generate creative animation prompt
+ const animationPrompt = generateAnimationPrompt(metadata);
+ console.log(`Animation prompt: "${animationPrompt}"`);
+
+ console.log('Sending to Luma Dream Machine...');
+ console.log('Please wait 60-120 seconds for dramatic animation...');
+
+ // Try Luma models on Replicate
+ let output;
+ try {
+ // Try luma-photon first (faster)
+ output = await Promise.race([
+ replicate.run(
+ "luma/photon",
+ {
+ input: {
+ prompt: animationPrompt,
+ image: dataUri,
+ aspect_ratio: "16:9"
+ }
+ }
+ ),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Luma timeout')), 180000))
+ ]);
+ } catch (e1) {
+ console.log('Luma Photon failed, trying Ray2...');
+ try {
+ output = await Promise.race([
+ replicate.run(
+ "luma/ray-2",
+ {
+ input: {
+ prompt: animationPrompt,
+ image: dataUri
+ }
+ }
+ ),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Ray2 timeout')), 180000))
+ ]);
+ } catch (e2) {
+ console.log('Ray2 failed, trying Dream Machine...');
+ output = await Promise.race([
+ replicate.run(
+ "luma/dream-machine",
+ {
+ input: {
+ prompt: animationPrompt,
+ image_url: dataUri
+ }
+ }
+ ),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Dream Machine timeout')), 180000))
+ ]);
+ }
+ }
+
+ console.log('Luma animation complete!');
+
+ if (!output) {
+ throw new Error('No video generated from Luma');
+ }
+
+ // Download the generated video
+ console.log('Downloading animated video...');
+ const videoUrl = typeof output === 'string' ? output : output.video || output;
+ const videoResponse = await axios.get(videoUrl, {
+ responseType: 'arraybuffer'
+ });
+
+ const outputDir = path.join(__dirname, '..', 'output');
+ const timestamp = Date.now();
+ const videoPath = path.join(outputDir, `luma_video_${timestamp}.mp4`);
+
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ // Convert to Twitter-compatible H.264 format
+ console.log('Converting to Twitter-compatible H.264 format...');
+ const h264VideoPath = videoPath.replace('.mp4', '_h264.mp4');
+ try {
+ execSync(`ffmpeg -y -i "${videoPath}" -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -movflags +faststart -an "${h264VideoPath}"`, {
+ stdio: 'pipe',
+ timeout: 60000
+ });
+ // Replace original with converted
+ fs.unlinkSync(videoPath);
+ fs.renameSync(h264VideoPath, videoPath);
+ console.log('Video converted to H.264 successfully');
+ } catch (ffmpegError) {
+ console.log('FFmpeg conversion failed, using original:', ffmpegError.message);
+ }
+
+ // Clean up temp file
+ if (fs.existsSync(optimizedImagePath)) {
+ fs.unlinkSync(optimizedImagePath);
+ }
+
+ const stats = fs.statSync(videoPath);
+ console.log(`Luma video created: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
+
+ return {
+ originalImage: imagePath,
+ animatedVideo: videoPath,
+ artworkMetadata: metadata,
+ isAnimated: true,
+ animationType: 'luma-dream-machine',
+ animationPrompt: animationPrompt,
+ videoFormat: 'mp4'
+ };
+
+ } catch (error) {
+ console.error('Luma animation failed:', error.message);
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ error: error.message
+ };
+ }
+}
+
+// Test if run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ const testImage = process.argv[2];
+ if (testImage) {
+ createLumaAnimation(testImage, { title: 'Test', artist: 'Test' })
+ .then(r => console.log('Result:', r))
+ .catch(e => console.error('Error:', e));
+ } else {
+ console.log('Usage: node animateWithLuma.js <image-path>');
+ }
+}
diff --git a/src/animateWithMorphing.js b/src/animateWithMorphing.js
new file mode 100644
index 0000000..437608f
--- /dev/null
+++ b/src/animateWithMorphing.js
@@ -0,0 +1,170 @@
+import Replicate from 'replicate';
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import sharp from 'sharp';
+import { execSync } from 'child_process';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const OUTPUT_DIR = path.join(__dirname, '..', 'output');
+
+/**
+ * Generate creative morphing prompts based on artwork
+ */
+function getMorphingPrompt(metadata) {
+ const title = (metadata.title || '').toLowerCase();
+
+ const morphEffects = [
+ "A playful pitbull dog runs into frame, sniffs the artwork, then magically morphs into a fluffy cat that walks away",
+ "Butterflies emerge from the painting and transform into colorful birds flying away",
+ "The scene comes alive as a dog appears, chases its tail, then transforms into a sleeping cat",
+ "Flowers in the image bloom and morph into flying hummingbirds",
+ "A curious puppy enters, looks at viewer, then shapeshifts into a majestic lion",
+ "Water ripples form into fish that leap out and transform into dolphins",
+ "Clouds drift and morph into galloping horses running across the sky",
+ "A pitbull trots through the scene, stops, and metamorphoses into a cat stretching lazily",
+ "Elements swirl and transform - dogs become cats, birds become butterflies",
+ "Surreal transformation: everyday objects morph into animals that come alive"
+ ];
+
+ // Pick random morphing effect
+ const effect = morphEffects[Math.floor(Math.random() * morphEffects.length)];
+
+ return `Cinematic video of famous artwork "${metadata.title}". ${effect}. Dramatic lighting, smooth animation, 4K quality, dreamlike atmosphere.`;
+}
+
+/**
+ * Create morphing video using prompt-based models
+ */
+export async function createMorphingVideo(imagePath, metadata) {
+ console.log('Creating MORPHING video with cats & dogs...');
+
+ const replicate = new Replicate({ auth: process.env.REPLICATE_API_TOKEN });
+
+ // Prepare image
+ const optimizedPath = path.join(OUTPUT_DIR, `morph_opt_${Date.now()}.jpg`);
+ await sharp(imagePath)
+ .resize(1280, 720, { fit: 'cover', position: 'center' })
+ .jpeg({ quality: 95 })
+ .toFile(optimizedPath);
+
+ const imageBuffer = fs.readFileSync(optimizedPath);
+ const base64Image = imageBuffer.toString('base64');
+ const dataUri = `data:image/jpeg;base64,${base64Image}`;
+
+ const prompt = getMorphingPrompt(metadata);
+ console.log(`Prompt: "${prompt.substring(0, 100)}..."`);
+
+ let output;
+ let modelUsed;
+
+ // Try Minimax first (reliable for image-to-video with prompts)
+ try {
+ console.log('Trying Minimax video-01...');
+ output = await Promise.race([
+ replicate.run("minimax/video-01", {
+ input: {
+ prompt: prompt,
+ first_frame_image: dataUri
+ }
+ }),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Minimax timeout')), 300000))
+ ]);
+ modelUsed = 'minimax';
+ } catch (e) {
+ console.log('Minimax failed:', e.message);
+
+ // Try Luma Photon
+ try {
+ console.log('Trying Luma Photon...');
+ output = await Promise.race([
+ replicate.run("luma/photon", {
+ input: {
+ prompt: prompt,
+ image: dataUri,
+ aspect_ratio: "16:9"
+ }
+ }),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Luma timeout')), 300000))
+ ]);
+ modelUsed = 'luma-photon';
+ } catch (e2) {
+ console.log('Luma failed:', e2.message);
+
+ // Final fallback to SVD with max motion
+ try {
+ console.log('Fallback to SVD max motion...');
+ output = await Promise.race([
+ replicate.run("stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438", {
+ input: {
+ input_image: dataUri,
+ video_length: "25_frames_with_svd_xt",
+ frames_per_second: 8,
+ motion_bucket_id: 255,
+ cond_aug: 0.1,
+ seed: Math.floor(Math.random() * 1000000)
+ }
+ }),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('SVD timeout')), 200000))
+ ]);
+ modelUsed = 'svd-max';
+ } catch (e3) {
+ console.log('All models failed');
+ fs.unlinkSync(optimizedPath);
+ throw new Error('No animation model available');
+ }
+ }
+ }
+
+ console.log(`Video generated with ${modelUsed}!`);
+
+ // Get video URL from output
+ let videoUrl;
+ if (typeof output === 'string') {
+ videoUrl = output;
+ } else if (output?.video) {
+ videoUrl = output.video;
+ } else if (Array.isArray(output)) {
+ videoUrl = output[0];
+ } else {
+ videoUrl = output;
+ }
+
+ // Download video
+ const videoResponse = await axios.get(videoUrl, { responseType: 'arraybuffer' });
+ const timestamp = Date.now();
+ const videoPath = path.join(OUTPUT_DIR, `morphing_${timestamp}.mp4`);
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ // Convert to H.264 for Twitter
+ const h264Path = videoPath.replace('.mp4', '_h264.mp4');
+ execSync(`ffmpeg -y -i "${videoPath}" -c:v libx264 -preset fast -crf 23 -pix_fmt yuv420p -movflags +faststart -an "${h264Path}"`, { stdio: 'pipe' });
+ fs.unlinkSync(videoPath);
+ fs.renameSync(h264Path, videoPath);
+
+ // Cleanup
+ fs.unlinkSync(optimizedPath);
+
+ const stats = fs.statSync(videoPath);
+ console.log(`Morphing video created: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
+
+ return {
+ videoPath,
+ modelUsed,
+ prompt,
+ motionSettings: { model: modelUsed, prompt }
+ };
+}
+
+// Test if run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ const testImage = process.argv[2] || './output/great_wave_source.jpg';
+ createMorphingVideo(testImage, { title: 'The Great Wave', artist: 'Hokusai' })
+ .then(r => console.log('Result:', r))
+ .catch(e => console.error('Error:', e));
+}
diff --git a/src/animateWithReplicate.js b/src/animateWithReplicate.js
new file mode 100644
index 0000000..2dfc2a1
--- /dev/null
+++ b/src/animateWithReplicate.js
@@ -0,0 +1,133 @@
+import Replicate from 'replicate';
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import sharp from 'sharp';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export async function createRealAIVideo(imagePath, metadata) {
+ try {
+ const REPLICATE_API_TOKEN = process.env.REPLICATE_API_TOKEN;
+
+ if (!REPLICATE_API_TOKEN) {
+ throw new Error('REPLICATE_API_TOKEN not configured - get it from https://replicate.com/account/api-tokens');
+ }
+
+ console.log('Creating REAL AI video with Stable Video Diffusion...');
+
+ const replicate = new Replicate({
+ auth: REPLICATE_API_TOKEN,
+ });
+
+ // Prepare the image - SVD works best with 1024x576 resolution
+ const optimizedImagePath = path.join(path.dirname(imagePath), `optimized_${path.basename(imagePath)}`);
+ await sharp(imagePath)
+ .resize(1024, 576, {
+ fit: 'cover',
+ position: 'center'
+ })
+ .jpeg({ quality: 95 })
+ .toFile(optimizedImagePath);
+
+ // Convert image to base64 data URI
+ const imageBuffer = fs.readFileSync(optimizedImagePath);
+ const base64Image = imageBuffer.toString('base64');
+ const dataUri = `data:image/jpeg;base64,${base64Image}`;
+
+ console.log('Sending to Stable Video Diffusion...');
+ console.log('Cost estimate: ~$0.02 per video');
+
+ // Run Stable Video Diffusion - allow up to 3 minutes
+ console.log('Please wait 60-180 seconds for AI video generation...');
+ console.log('Creating SURREAL METAMORPHOSIS - objects transform, morph, come alive!');
+
+ const output = await Promise.race([
+ replicate.run(
+ "stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438",
+ {
+ input: {
+ input_image: dataUri,
+ video_length: "25_frames_with_svd_xt",
+ sizing_strategy: "maintain_aspect_ratio",
+ frames_per_second: 8, // Slightly slower for dramatic effect
+ motion_bucket_id: 255, // MAXIMUM motion for dramatic metamorphosis
+ cond_aug: 0.1, // Higher variation for surreal transformations
+ decoding_t: 14,
+ seed: Math.floor(Math.random() * 1000000)
+ }
+ }
+ ),
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Replicate timeout after 180s')), 180000))
+ ]);
+
+ console.log('Video generation complete!');
+
+ if (!output) {
+ throw new Error('No video generated');
+ }
+
+ // Download the generated video
+ console.log('Downloading animated video...');
+ const videoUrl = output;
+ const videoResponse = await axios.get(videoUrl, {
+ responseType: 'arraybuffer'
+ });
+
+ const outputDir = path.join(__dirname, '..', 'output');
+ const timestamp = new Date().getTime();
+ const videoPath = path.join(outputDir, `ai_video_${timestamp}.mp4`);
+
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ // Clean up optimized image
+ if (fs.existsSync(optimizedImagePath)) {
+ fs.unlinkSync(optimizedImagePath);
+ }
+
+ const stats = fs.statSync(videoPath);
+ console.log(`AI video created: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
+
+ // Check if we need to convert to GIF for Twitter
+ // Twitter supports MP4 but has size limits
+ if (stats.size > 15 * 1024 * 1024) {
+ console.log('Video too large for Twitter, converting to optimized version...');
+
+ // For now, return the video as is
+ // In production, you'd use ffmpeg to compress it
+ }
+
+ return {
+ originalImage: imagePath,
+ animatedVideo: videoPath,
+ artworkMetadata: metadata,
+ isAnimated: true,
+ animationType: 'stable-video-diffusion',
+ videoFormat: 'mp4'
+ };
+
+ } catch (error) {
+ console.error('Error creating AI video:', error.message);
+
+ if (error.message.includes('REPLICATE_API_TOKEN')) {
+ console.error('\n⚠️ To use real AI video animation:');
+ console.error('1. Go to https://replicate.com');
+ console.error('2. Sign up for free account');
+ console.error('3. Get API token from https://replicate.com/account/api-tokens');
+ console.error('4. Add to .env: REPLICATE_API_TOKEN=your_token_here');
+ console.error('5. Cost: ~$0.02 per video\n');
+ }
+
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ error: error.message
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/animateWithReplicateAlt.js b/src/animateWithReplicateAlt.js
new file mode 100644
index 0000000..a2e4772
--- /dev/null
+++ b/src/animateWithReplicateAlt.js
@@ -0,0 +1,142 @@
+import Replicate from 'replicate';
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import sharp from 'sharp';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export async function createRealAnimation(imagePath, metadata) {
+ try {
+ const REPLICATE_API_TOKEN = process.env.REPLICATE_API_TOKEN;
+
+ if (!REPLICATE_API_TOKEN) {
+ throw new Error('REPLICATE_API_TOKEN not configured');
+ }
+
+ console.log('Creating REAL animation with AI...');
+
+ const replicate = new Replicate({
+ auth: REPLICATE_API_TOKEN,
+ });
+
+ // Convert image to base64
+ const imageBuffer = fs.readFileSync(imagePath);
+ const base64Image = imageBuffer.toString('base64');
+ const dataUri = `data:image/jpeg;base64,${base64Image}`;
+
+ // Try different animation models
+ console.log('Attempting AnimateDiff for artistic animation...');
+
+ try {
+ // AnimateDiff - better for artistic animation
+ const output = await replicate.run(
+ "lucataco/animate-diff:1531004ee4c98894ab11f8a4ce6206099e732c1da15121987a8eef54828f0663",
+ {
+ input: {
+ path: dataUri,
+ prompt: `animate ${metadata.title}, make the elements move naturally, flowers swaying, leaves rustling, light changing, everything should move and come alive, not camera movement but actual animation of objects`,
+ negative_prompt: "panning, zooming, camera movement, static",
+ num_frames: 16,
+ num_inference_steps: 25,
+ guidance_scale: 7.5,
+ seed: Math.floor(Math.random() * 1000000)
+ }
+ }
+ );
+
+ if (output) {
+ console.log('AnimateDiff animation created!');
+ return await downloadVideo(output, metadata);
+ }
+ } catch (e) {
+ console.log('AnimateDiff not available, trying alternative...');
+ }
+
+ // Try I2VGen-XL for image to video
+ try {
+ const output = await replicate.run(
+ "ali-vilab/i2vgen-xl:5821a338d00033abaaba89080a17eb8783d9a17ed710a6b4246a18e0900ccad4",
+ {
+ input: {
+ image: dataUri,
+ prompt: `Make the ${metadata.title} painting come alive with natural movement. Animate the flowers, make petals move, leaves sway, create life and motion in the scene. No camera movement.`,
+ max_frames: 16,
+ guidance_scale: 9,
+ seed: Math.floor(Math.random() * 1000000)
+ }
+ }
+ );
+
+ if (output) {
+ console.log('I2VGen-XL animation created!');
+ return await downloadVideo(output, metadata);
+ }
+ } catch (e) {
+ console.log('I2VGen-XL not available, trying next...');
+ }
+
+ // Last resort - try SVD with different prompt
+ console.log('Using modified Stable Video Diffusion...');
+
+ const output = await replicate.run(
+ "stability-ai/stable-video-diffusion:3f0457e4619daac51203dedb472816fd4af51f3149fa7a9e0b5ffcf1b8172438",
+ {
+ input: {
+ input_image: dataUri,
+ video_length: "25_frames_with_svd_xt",
+ sizing_strategy: "crop_to_16_9", // Try different strategy
+ frames_per_second: 6,
+ motion_bucket_id: 200, // High but not max
+ cond_aug: 0.3,
+ decoding_t: 14,
+ seed: Math.floor(Math.random() * 1000000)
+ }
+ }
+ );
+
+ if (output) {
+ return await downloadVideo(output, metadata);
+ }
+
+ throw new Error('No animation model succeeded');
+
+ } catch (error) {
+ console.error('Error creating animation:', error.message);
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ error: error.message
+ };
+ }
+}
+
+async function downloadVideo(videoUrl, metadata) {
+ console.log('Downloading animated video...');
+ const videoResponse = await axios.get(videoUrl, {
+ responseType: 'arraybuffer'
+ });
+
+ const outputDir = path.join(__dirname, '..', 'output');
+ const timestamp = new Date().getTime();
+ const videoPath = path.join(outputDir, `ai_animated_${timestamp}.mp4`);
+
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ const stats = fs.statSync(videoPath);
+ console.log(`AI video created: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
+
+ return {
+ animatedVideo: videoPath,
+ artworkMetadata: metadata,
+ isAnimated: true,
+ animationType: 'ai-animation',
+ videoFormat: 'mp4'
+ };
+}
\ No newline at end of file
diff --git a/src/animateWithRunway.js b/src/animateWithRunway.js
new file mode 100644
index 0000000..615856f
--- /dev/null
+++ b/src/animateWithRunway.js
@@ -0,0 +1,113 @@
+import RunwayML from '@runwayml/sdk';
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export async function createRunwayAnimation(imagePath, metadata) {
+ try {
+ const RUNWAY_API_TOKEN = process.env.RUNWAY_API_TOKEN;
+
+ if (!RUNWAY_API_TOKEN) {
+ throw new Error('RUNWAY_API_TOKEN not configured - get it from https://app.runwayml.com/settings/apikeys');
+ }
+
+ console.log('Creating REAL object animation with RunwayML Gen-2...');
+
+ const client = new RunwayML({
+ apiKey: RUNWAY_API_TOKEN,
+ });
+
+ // Read and prepare the image
+ const imageBuffer = fs.readFileSync(imagePath);
+ const base64Image = imageBuffer.toString('base64');
+
+ // Create animation prompt based on the painting
+ let animationPrompt = '';
+
+ if (metadata.title.toLowerCase().includes('sunflower')) {
+ animationPrompt = 'Make the sunflowers gently sway in a breeze, petals moving individually, stems bending slightly, natural flower movement';
+ } else if (metadata.title.toLowerCase().includes('water') || metadata.title.toLowerCase().includes('wave')) {
+ animationPrompt = 'Animate the water with realistic waves, ripples, and flow. Make the water move naturally';
+ } else if (metadata.title.toLowerCase().includes('night')) {
+ animationPrompt = 'Add twinkling stars, moving clouds, flickering lights, create a living night scene';
+ } else {
+ animationPrompt = `Bring the painting "${metadata.title}" to life. Make elements move naturally - fabric swaying, leaves rustling, light shifting, everything should have subtle natural movement. No camera panning or zooming.`;
+ }
+
+ console.log('Animation prompt:', animationPrompt);
+
+ // Create the generation task
+ const task = await client.imageToVideo.create({
+ model: 'gen-3-turbo', // Or 'gen-2' for older model
+ image: `data:image/jpeg;base64,${base64Image}`,
+ prompt: animationPrompt,
+ duration: 4, // 4 seconds
+ aspectRatio: '16:9'
+ });
+
+ console.log('Generation started, task ID:', task.id);
+ console.log('Waiting for video generation (this takes 1-2 minutes)...');
+
+ // Poll for completion
+ let result = await task.poll({
+ interval: 5000, // Check every 5 seconds
+ timeout: 180000 // 3 minute timeout
+ });
+
+ if (result.status === 'succeeded' && result.output) {
+ console.log('Video generation complete!');
+
+ // Download the video
+ const videoResponse = await axios.get(result.output, {
+ responseType: 'arraybuffer'
+ });
+
+ const outputDir = path.join(__dirname, '..', 'output');
+ const timestamp = new Date().getTime();
+ const videoPath = path.join(outputDir, `runway_animated_${timestamp}.mp4`);
+
+ fs.writeFileSync(videoPath, videoResponse.data);
+
+ const stats = fs.statSync(videoPath);
+ console.log(`RunwayML video created: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
+
+ return {
+ originalImage: imagePath,
+ animatedVideo: videoPath,
+ artworkMetadata: metadata,
+ isAnimated: true,
+ animationType: 'runway-gen2',
+ videoFormat: 'mp4'
+ };
+ } else {
+ throw new Error('Video generation failed: ' + result.status);
+ }
+
+ } catch (error) {
+ console.error('Error creating RunwayML animation:', error.message);
+
+ if (error.message.includes('RUNWAY_API_TOKEN')) {
+ console.error('\n⚠️ To use RunwayML Gen-2:');
+ console.error('1. Go to https://app.runwayml.com');
+ console.error('2. Sign up (they have free credits to start)');
+ console.error('3. Go to Settings > API Keys');
+ console.error('4. Create an API key');
+ console.error('5. Add to .env: RUNWAY_API_TOKEN=your_token_here');
+ console.error('6. Cost: ~$0.05 per 4-second video\n');
+ }
+
+ return {
+ originalImage: imagePath,
+ isStatic: true,
+ artworkMetadata: metadata,
+ error: error.message
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/artworks.js b/src/artworks.js
new file mode 100644
index 0000000..d4bc137
--- /dev/null
+++ b/src/artworks.js
@@ -0,0 +1,55 @@
+// 50 Famous Public Domain Paintings
+export const FAMOUS_PAINTINGS = [
+ { title: "The Great Wave off Kanagawa", artist: "Katsushika Hokusai", year: "1831", museum: "Public Domain", imageUrl: "https://images.metmuseum.org/CRDImages/as/original/DP141063.jpg" },
+ { title: "Starry Night", artist: "Vincent van Gogh", year: "1889", museum: "MoMA", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1280px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg" },
+ { title: "Mona Lisa", artist: "Leonardo da Vinci", year: "1503", museum: "Louvre", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/thumb/e/ec/Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg/800px-Mona_Lisa%2C_by_Leonardo_da_Vinci%2C_from_C2RMF_retouched.jpg" },
+ { title: "The Scream", artist: "Edvard Munch", year: "1893", museum: "National Gallery Norway", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/c/c5/Edvard_Munch%2C_1893%2C_The_Scream%2C_oil%2C_tempera_and_pastel_on_cardboard%2C_91_x_73_cm%2C_National_Gallery_of_Norway.jpg" },
+ { title: "Girl with a Pearl Earring", artist: "Johannes Vermeer", year: "1665", museum: "Mauritshuis", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/0/0f/1665_Girl_with_a_Pearl_Earring.jpg" },
+ { title: "The Birth of Venus", artist: "Sandro Botticelli", year: "1485", museum: "Uffizi Gallery", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/0/0b/Sandro_Botticelli_-_La_nascita_di_Venere_-_Google_Art_Project_-_edited.jpg" },
+ { title: "The Night Watch", artist: "Rembrandt", year: "1642", museum: "Rijksmuseum", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/5/5a/The_Night_Watch_-_HD.jpg" },
+ { title: "The Kiss", artist: "Gustav Klimt", year: "1908", museum: "Belvedere", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/4/4d/Klimt_-_Der_Kuss.jpeg" },
+ { title: "American Gothic", artist: "Grant Wood", year: "1930", museum: "Art Institute Chicago", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/c/cc/Grant_Wood_-_American_Gothic_-_Google_Art_Project.jpg" },
+ { title: "Water Lilies", artist: "Claude Monet", year: "1906", museum: "Art Institute Chicago", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/a/aa/Claude_Monet_-_Water_Lilies_-_1906%2C_Ryerson.jpg" },
+ { title: "The Persistence of Memory", artist: "Salvador Dalí", year: "1931", museum: "MoMA", imageUrl: "https://upload.wikimedia.org/wikipedia/en/d/dd/The_Persistence_of_Memory.jpg" },
+ { title: "A Sunday on La Grande Jatte", artist: "Georges Seurat", year: "1886", museum: "Art Institute Chicago", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/7/7d/A_Sunday_on_La_Grande_Jatte%2C_Georges_Seurat%2C_1884.jpg" },
+ { title: "The Last Supper", artist: "Leonardo da Vinci", year: "1498", museum: "Santa Maria delle Grazie", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/4/4b/%C3%9Altima_Cena_-_Da_Vinci_5.jpg" },
+ { title: "Creation of Adam", artist: "Michelangelo", year: "1512", museum: "Sistine Chapel", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/5/5b/Michelangelo_-_Creation_of_Adam_%28cropped%29.jpg" },
+ { title: "The Garden of Earthly Delights", artist: "Hieronymus Bosch", year: "1510", museum: "Museo del Prado", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/9/96/The_Garden_of_earthly_delights.jpg" },
+ { title: "Whistler's Mother", artist: "James Whistler", year: "1871", museum: "Musée d'Orsay", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/1/1b/Whistlers_Mother_high_res.jpg" },
+ { title: "The Arnolfini Portrait", artist: "Jan van Eyck", year: "1434", museum: "National Gallery London", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/3/33/Van_Eyck_-_Arnolfini_Portrait.jpg" },
+ { title: "Liberty Leading the People", artist: "Eugène Delacroix", year: "1830", museum: "Louvre", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/5/5d/Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg" },
+ { title: "The Wanderer Above the Sea of Fog", artist: "Caspar David Friedrich", year: "1818", museum: "Hamburger Kunsthalle", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/b/b9/Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg" },
+ { title: "Napoleon Crossing the Alps", artist: "Jacques-Louis David", year: "1801", museum: "Château de Malmaison", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/f/fd/David_-_Napoleon_crossing_the_Alps_-_Malmaison2.jpg" },
+ { title: "Nighthawks", artist: "Edward Hopper", year: "1942", museum: "Art Institute Chicago", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/a/a8/Nighthawks_by_Edward_Hopper_1942.jpg" },
+ { title: "The Starry Night Over the Rhône", artist: "Vincent van Gogh", year: "1888", museum: "Musée d'Orsay", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/9/94/Starry_Night_Over_the_Rhone.jpg" },
+ { title: "Café Terrace at Night", artist: "Vincent van Gogh", year: "1888", museum: "Kröller-Müller Museum", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/0/09/Van_Gogh_-_Terrasse_des_Caf%C3%A9s_an_der_Place_du_Forum_in_Arles_am_Abend1.jpeg" },
+ { title: "Sunflowers", artist: "Vincent van Gogh", year: "1888", museum: "National Gallery London", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/4/46/Vincent_Willem_van_Gogh_127.jpg" },
+ { title: "The Son of Man", artist: "René Magritte", year: "1964", museum: "Private Collection", imageUrl: "https://upload.wikimedia.org/wikipedia/en/e/ec/Magritte_TheSonOfMan.jpg" },
+ { title: "Girl Before a Mirror", artist: "Pablo Picasso", year: "1932", museum: "MoMA", imageUrl: "https://upload.wikimedia.org/wikipedia/en/0/04/Girl_before_a_Mirror.jpg" },
+ { title: "Guernica", artist: "Pablo Picasso", year: "1937", museum: "Museo Reina Sofía", imageUrl: "https://upload.wikimedia.org/wikipedia/en/7/74/Guernica.jpg" },
+ { title: "The Sleeping Gypsy", artist: "Henri Rousseau", year: "1897", museum: "MoMA", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/7/7e/Henri_Rousseau_-_La_Boh%C3%A9mienne_endormie.jpg" },
+ { title: "The Swing", artist: "Jean-Honoré Fragonard", year: "1767", museum: "Wallace Collection", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/e/eb/Fragonard%2C_The_Swing.jpg" },
+ { title: "Ophelia", artist: "John Everett Millais", year: "1852", museum: "Tate Britain", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/9/94/John_Everett_Millais_-_Ophelia_-_Google_Art_Project.jpg" },
+ { title: "The Raft of the Medusa", artist: "Théodore Géricault", year: "1819", museum: "Louvre", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/1/15/JEAN_LOUIS_TH%C3%89ODORE_G%C3%89RICAULT_-_La_Balsa_de_la_Medusa_%28Museo_del_Louvre%2C_1818-19%29.jpg" },
+ { title: "The School of Athens", artist: "Raphael", year: "1511", museum: "Vatican Museums", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/4/49/%22The_School_of_Athens%22_by_Raffaello_Sanzio_da_Urbino.jpg" },
+ { title: "Saturn Devouring His Son", artist: "Francisco Goya", year: "1823", museum: "Museo del Prado", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/8/82/Francisco_de_Goya%2C_Saturno_devorando_a_su_hijo_%281819-1823%29.jpg" },
+ { title: "The Third of May 1808", artist: "Francisco Goya", year: "1814", museum: "Museo del Prado", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/f/fd/El_Tres_de_Mayo%2C_by_Francisco_de_Goya%2C_from_Prado_thin_black_margin.jpg" },
+ { title: "Impression, Sunrise", artist: "Claude Monet", year: "1872", museum: "Musée Marmottan Monet", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/5/59/Monet_-_Impression%2C_Sunrise.jpg" },
+ { title: "The Ballet Class", artist: "Edgar Degas", year: "1874", museum: "Musée d'Orsay", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/6/6e/Edgar_Degas_-_The_Ballet_Class_-_Google_Art_Project.jpg" },
+ { title: "A Bar at the Folies-Bergère", artist: "Édouard Manet", year: "1882", museum: "Courtauld Gallery", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/0/0d/Edouard_Manet%2C_A_Bar_at_the_Folies-Berg%C3%A8re.jpg" },
+ { title: "Luncheon on the Grass", artist: "Édouard Manet", year: "1863", museum: "Musée d'Orsay", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/9/90/Edouard_Manet_-_Luncheon_on_the_Grass_-_Google_Art_Project.jpg" },
+ { title: "The Birth of Venus", artist: "William-Adolphe Bouguereau", year: "1879", museum: "Musée d'Orsay", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/2/2a/William-Adolphe_Bouguereau_%281825-1905%29_-_The_Birth_of_Venus_%281879%29.jpg" },
+ { title: "The Hay Wain", artist: "John Constable", year: "1821", museum: "National Gallery London", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/d/d9/John_Constable_The_Hay_Wain.jpg" },
+ { title: "The Fighting Temeraire", artist: "J.M.W. Turner", year: "1839", museum: "National Gallery London", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/3/30/The_Fighting_Temeraire%2C_JMW_Turner%2C_National_Gallery.jpg" },
+ { title: "Rain, Steam and Speed", artist: "J.M.W. Turner", year: "1844", museum: "National Gallery London", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/a/a8/Turner_-_Rain%2C_Steam_and_Speed_-_National_Gallery_file.jpg" },
+ { title: "Las Meninas", artist: "Diego Velázquez", year: "1656", museum: "Museo del Prado", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/3/31/Las_Meninas%2C_by_Diego_Vel%C3%A1zquez%2C_from_Prado_in_Google_Earth.jpg" },
+ { title: "The Milkmaid", artist: "Johannes Vermeer", year: "1658", museum: "Rijksmuseum", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/2/20/Johannes_Vermeer_-_Het_melkmeisje_-_Google_Art_Project.jpg" },
+ { title: "The Art of Painting", artist: "Johannes Vermeer", year: "1668", museum: "Kunsthistorisches Museum", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/a/a1/Jan_Vermeer_-_The_Art_of_Painting_-_Google_Art_Project.jpg" },
+ { title: "Self-Portrait with Bandaged Ear", artist: "Vincent van Gogh", year: "1889", museum: "Courtauld Gallery", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/a/a4/Vincent_van_Gogh_-_Self-Portrait_with_Bandaged_Ear.jpg" },
+ { title: "Judith Slaying Holofernes", artist: "Artemisia Gentileschi", year: "1620", museum: "Uffizi Gallery", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/4/4e/Artemisia_Gentileschi_-_Judith_Beheading_Holofernes_-_WGA8563.jpg" },
+ { title: "The Tower of Babel", artist: "Pieter Bruegel", year: "1563", museum: "Kunsthistorisches Museum", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/f/fc/Pieter_Bruegel_the_Elder_-_The_Tower_of_Babel_%28Vienna%29_-_Google_Art_Project_-_edited.jpg" },
+ { title: "The Blue Boy", artist: "Thomas Gainsborough", year: "1770", museum: "Huntington Library", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/e/e9/The_Blue_Boy_%28Gainsborough%29.jpg" },
+ { title: "Washington Crossing the Delaware", artist: "Emanuel Leutze", year: "1851", museum: "Met Museum", imageUrl: "https://upload.wikimedia.org/wikipedia/commons/9/95/Washington_Crossing_the_Delaware_by_Emanuel_Leutze%2C_MMA-NYC%2C_1851.jpg" }
+];
+
+export default FAMOUS_PAINTINGS;
diff --git a/src/contentGenerator.js b/src/contentGenerator.js
new file mode 100644
index 0000000..035acc0
--- /dev/null
+++ b/src/contentGenerator.js
@@ -0,0 +1,125 @@
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import { execSync } from 'child_process';
+import { addBrandingToVideo } from './addBranding.js';
+import { addScheduledPost, initDatabase } from './database.js';
+import { createMorphingVideo } from './animateWithMorphing.js';
+import { FAMOUS_PAINTINGS } from './artworks.js';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const OUTPUT_DIR = path.join(__dirname, '..', 'output');
+const THUMBNAILS_DIR = path.join(__dirname, '..', 'output', 'thumbnails');
+
+// Ensure directories exist
+if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
+if (!fs.existsSync(THUMBNAILS_DIR)) fs.mkdirSync(THUMBNAILS_DIR, { recursive: true });
+
+// Use 50 famous paintings from artworks.js
+const ARTWORKS = FAMOUS_PAINTINGS;
+
+/**
+ * Download image from URL
+ */
+async function downloadImage(url, filename) {
+ const imagePath = path.join(OUTPUT_DIR, filename);
+
+ const response = await axios.get(url, {
+ responseType: 'arraybuffer',
+ headers: { 'User-Agent': 'Mozilla/5.0 (compatible; ArtBot/1.0)' }
+ });
+ fs.writeFileSync(imagePath, response.data);
+ return imagePath;
+}
+
+/**
+ * Create morphing video using prompt-based models (cats/dogs transform)
+ */
+async function createAnimatedVideo(imagePath, artwork) {
+ console.log(`Creating MORPHING video for: ${artwork.title}`);
+
+ const startTime = Date.now();
+ const result = await createMorphingVideo(imagePath, artwork);
+ console.log(` Generated in ${((Date.now() - startTime) / 1000).toFixed(1)}s with ${result.modelUsed}`);
+
+ const videoPath = result.videoPath;
+ const timestamp = Date.now();
+
+ // Add branding (1s intro + 1s outro)
+ console.log(' Adding GoodQuestion.AI branding...');
+ await addBrandingToVideo(videoPath, { title: artwork.title, artist: artwork.artist });
+
+ // Create thumbnail
+ const thumbnailPath = path.join(THUMBNAILS_DIR, `thumb_${timestamp}.jpg`);
+ execSync(`ffmpeg -y -i "${videoPath}" -ss 00:00:01 -vframes 1 "${thumbnailPath}"`, { stdio: 'pipe' });
+
+ return { videoPath, thumbnailPath, motionSettings: result.motionSettings };
+}
+
+/**
+ * Generate content for the next N hours
+ * Posts every 20 minutes
+ */
+export async function generateScheduledContent(hoursAhead = 12) {
+ await initDatabase();
+
+ const postsPerHour = 3; // Every 20 minutes
+ const totalPosts = hoursAhead * postsPerHour;
+
+ console.log(`\nGenerating ${totalPosts} posts for the next ${hoursAhead} hours...\n`);
+
+ let artworkIndex = 0;
+ const now = new Date();
+
+ for (let i = 0; i < totalPosts; i++) {
+ const artwork = ARTWORKS[artworkIndex % ARTWORKS.length];
+ artworkIndex++;
+
+ // Calculate scheduled time (every 20 minutes)
+ const scheduledTime = new Date(now.getTime() + (i * 20 * 60 * 1000));
+
+ console.log(`\n[${i + 1}/${totalPosts}] ${artwork.title}`);
+ console.log(` Scheduled for: ${scheduledTime.toISOString()}`);
+
+ try {
+ // Download image
+ const imagePath = await downloadImage(
+ artwork.imageUrl,
+ `source_${artwork.title.replace(/\s+/g, '_').toLowerCase()}.jpg`
+ );
+
+ // Create video
+ const { videoPath, thumbnailPath, motionSettings } = await createAnimatedVideo(imagePath, artwork);
+
+ // Store in database
+ const postId = await addScheduledPost({
+ title: artwork.title,
+ artist: artwork.artist,
+ year: artwork.year,
+ museum: artwork.museum,
+ videoPath: videoPath,
+ thumbnailPath: thumbnailPath,
+ scheduledTime: scheduledTime,
+ motionSettings: motionSettings
+ });
+
+ console.log(` Saved to database with ID: ${postId}`);
+
+ } catch (err) {
+ console.error(` Failed: ${err.message}`);
+ }
+ }
+
+ console.log(`\n=== Content generation complete ===\n`);
+}
+
+// Run if called directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ const hours = parseInt(process.argv[2]) || 3;
+ generateScheduledContent(hours).catch(console.error);
+}
diff --git a/src/createAnimation.js b/src/createAnimation.js
new file mode 100644
index 0000000..0949d92
--- /dev/null
+++ b/src/createAnimation.js
@@ -0,0 +1,127 @@
+import sharp from 'sharp';
+import GIFEncoder from 'gif-encoder-2';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+export async function createAnimatedGIF(imagePath, effect = 'zoom') {
+ try {
+ console.log(`Creating animated GIF with ${effect} effect...`);
+
+ const outputDir = path.join(__dirname, '..', 'output');
+ const timestamp = new Date().getTime();
+ const outputPath = path.join(outputDir, `animated_${timestamp}.gif`);
+
+ // Load the image
+ const image = sharp(imagePath);
+ const metadata = await image.metadata();
+
+ const width = 500; // Smaller size for Twitter
+ const height = 500;
+ const frameCount = 15; // Fewer frames for smaller file
+
+ // Create GIF encoder
+ const encoder = new GIFEncoder(width, height, 'neuquant');
+ encoder.setDelay(150); // 150ms between frames for smoother playback
+ encoder.setRepeat(0); // Loop forever
+ encoder.setQuality(20); // Lower quality for smaller file size
+
+ const stream = fs.createWriteStream(outputPath);
+ encoder.createReadStream().pipe(stream);
+ encoder.start();
+
+ // Generate frames based on effect
+ for (let i = 0; i < frameCount; i++) {
+ const progress = i / frameCount;
+ let frameBuffer;
+
+ switch (effect) {
+ case 'zoom':
+ // Ken Burns zoom effect
+ const zoomFactor = 1 + (progress * 0.3); // Zoom from 100% to 130%
+ const cropSize = Math.round(Math.min(metadata.width, metadata.height) / zoomFactor);
+ const xOffset = Math.round((metadata.width - cropSize) * progress / 2);
+ const yOffset = Math.round((metadata.height - cropSize) * progress / 2);
+
+ frameBuffer = await sharp(imagePath)
+ .extract({
+ left: xOffset,
+ top: yOffset,
+ width: Math.min(cropSize, metadata.width - xOffset),
+ height: Math.min(cropSize, metadata.height - yOffset)
+ })
+ .resize(width, height, { fit: 'cover' })
+ .raw()
+ .toBuffer();
+ break;
+
+ case 'pan':
+ // Horizontal pan effect
+ const panOffset = Math.round((metadata.width - width) * progress);
+
+ frameBuffer = await sharp(imagePath)
+ .extract({
+ left: Math.min(panOffset, metadata.width - width),
+ top: 0,
+ width: width,
+ height: Math.min(height, metadata.height)
+ })
+ .resize(width, height, { fit: 'cover' })
+ .raw()
+ .toBuffer();
+ break;
+
+ case 'fade':
+ // Fade in/out effect
+ const opacity = progress < 0.5
+ ? progress * 2
+ : 2 - (progress * 2);
+
+ frameBuffer = await sharp(imagePath)
+ .resize(width, height, { fit: 'cover' })
+ .modulate({ brightness: 0.3 + (opacity * 0.7) })
+ .raw()
+ .toBuffer();
+ break;
+
+ default:
+ // Static frame
+ frameBuffer = await sharp(imagePath)
+ .resize(width, height, { fit: 'cover' })
+ .raw()
+ .toBuffer();
+ }
+
+ // Convert raw buffer to RGBA for GIF encoder
+ const rgbaBuffer = Buffer.alloc(width * height * 4);
+ for (let j = 0; j < frameBuffer.length; j += 3) {
+ const pixelIndex = Math.floor(j / 3) * 4;
+ rgbaBuffer[pixelIndex] = frameBuffer[j]; // R
+ rgbaBuffer[pixelIndex + 1] = frameBuffer[j + 1]; // G
+ rgbaBuffer[pixelIndex + 2] = frameBuffer[j + 2]; // B
+ rgbaBuffer[pixelIndex + 3] = 255; // A
+ }
+
+ encoder.addFrame(rgbaBuffer);
+ }
+
+ // Skip reverse frames to keep file size small for Twitter
+
+ encoder.finish();
+
+ return new Promise((resolve, reject) => {
+ stream.on('finish', () => {
+ console.log(`Animated GIF created: ${outputPath}`);
+ resolve(outputPath);
+ });
+ stream.on('error', reject);
+ });
+
+ } catch (error) {
+ console.error('Error creating animated GIF:', error);
+ throw error;
+ }
+}
\ No newline at end of file
diff --git a/src/database.js b/src/database.js
new file mode 100644
index 0000000..7ffff84
--- /dev/null
+++ b/src/database.js
@@ -0,0 +1,209 @@
+import pg from 'pg';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const { Pool } = pg;
+
+// PostgreSQL connection
+const pool = new Pool({
+ host: process.env.POSTGRES_HOST || 'localhost',
+ port: process.env.POSTGRES_PORT || 5432,
+ database: process.env.POSTGRES_DB || 'museum_posts',
+ user: process.env.POSTGRES_USER || 'postgres',
+ password: process.env.POSTGRES_PASSWORD || 'postgres'
+});
+
+/**
+ * Initialize database schema
+ */
+export async function initDatabase() {
+ const client = await pool.connect();
+ try {
+ await client.query(`
+ CREATE TABLE IF NOT EXISTS scheduled_posts (
+ id SERIAL PRIMARY KEY,
+ title VARCHAR(255) NOT NULL,
+ artist VARCHAR(255),
+ year VARCHAR(50),
+ museum VARCHAR(255),
+ video_path TEXT NOT NULL,
+ thumbnail_path TEXT,
+ scheduled_time TIMESTAMP NOT NULL,
+ status VARCHAR(50) DEFAULT 'pending',
+ tweet_id VARCHAR(100),
+ tweet_url TEXT,
+ created_at TIMESTAMP DEFAULT NOW(),
+ posted_at TIMESTAMP,
+ error_message TEXT,
+ motion_settings JSONB
+ )
+ `);
+
+ await client.query(`
+ CREATE INDEX IF NOT EXISTS idx_scheduled_time ON scheduled_posts(scheduled_time);
+ CREATE INDEX IF NOT EXISTS idx_status ON scheduled_posts(status);
+ `);
+
+ console.log('Database initialized');
+ } finally {
+ client.release();
+ }
+}
+
+/**
+ * Add a scheduled post
+ */
+export async function addScheduledPost(post) {
+ const { title, artist, year, museum, videoPath, thumbnailPath, scheduledTime, motionSettings } = post;
+
+ const result = await pool.query(`
+ INSERT INTO scheduled_posts
+ (title, artist, year, museum, video_path, thumbnail_path, scheduled_time, motion_settings)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
+ RETURNING id
+ `, [title, artist, year, museum, videoPath, thumbnailPath, scheduledTime, JSON.stringify(motionSettings)]);
+
+ return result.rows[0].id;
+}
+
+/**
+ * Get all scheduled posts
+ */
+export async function getAllPosts() {
+ const result = await pool.query(`
+ SELECT * FROM scheduled_posts
+ ORDER BY scheduled_time ASC
+ `);
+ return result.rows;
+}
+
+/**
+ * Get pending posts ready to be posted
+ */
+export async function getPendingPosts() {
+ const result = await pool.query(`
+ SELECT * FROM scheduled_posts
+ WHERE status = 'pending'
+ AND scheduled_time <= NOW()
+ ORDER BY scheduled_time ASC
+ `);
+ return result.rows;
+}
+
+/**
+ * Get upcoming posts
+ */
+export async function getUpcomingPosts() {
+ const result = await pool.query(`
+ SELECT * FROM scheduled_posts
+ WHERE status = 'pending'
+ ORDER BY scheduled_time ASC
+ `);
+ return result.rows;
+}
+
+/**
+ * Update post status after posting
+ */
+export async function markAsPosted(id, tweetId, tweetUrl) {
+ await pool.query(`
+ UPDATE scheduled_posts
+ SET status = 'posted',
+ tweet_id = $2,
+ tweet_url = $3,
+ posted_at = NOW()
+ WHERE id = $1
+ `, [id, tweetId, tweetUrl]);
+}
+
+/**
+ * Mark post as failed
+ */
+export async function markAsFailed(id, errorMessage) {
+ await pool.query(`
+ UPDATE scheduled_posts
+ SET status = 'failed',
+ error_message = $2
+ WHERE id = $1
+ `, [id, errorMessage]);
+}
+
+/**
+ * Delete a scheduled post
+ */
+export async function deletePost(id) {
+ // Get the video path first to delete the file
+ const result = await pool.query(`
+ SELECT video_path, thumbnail_path FROM scheduled_posts WHERE id = $1
+ `, [id]);
+
+ if (result.rows.length > 0) {
+ await pool.query(`DELETE FROM scheduled_posts WHERE id = $1`, [id]);
+ return result.rows[0];
+ }
+ return null;
+}
+
+/**
+ * Get post by ID
+ */
+export async function getPostById(id) {
+ const result = await pool.query(`
+ SELECT * FROM scheduled_posts WHERE id = $1
+ `, [id]);
+ return result.rows[0];
+}
+
+/**
+ * Update a scheduled post
+ */
+export async function updatePost(id, updates) {
+ const { title, artist, scheduledTime, status } = updates;
+ const setClauses = [];
+ const values = [id];
+ let paramIndex = 2;
+
+ if (title !== undefined) {
+ setClauses.push(`title = $${paramIndex++}`);
+ values.push(title);
+ }
+ if (artist !== undefined) {
+ setClauses.push(`artist = $${paramIndex++}`);
+ values.push(artist);
+ }
+ if (scheduledTime !== undefined) {
+ setClauses.push(`scheduled_time = $${paramIndex++}`);
+ values.push(scheduledTime);
+ }
+ if (status !== undefined) {
+ setClauses.push(`status = $${paramIndex++}`);
+ values.push(status);
+ }
+
+ if (setClauses.length === 0) return null;
+
+ const result = await pool.query(`
+ UPDATE scheduled_posts
+ SET ${setClauses.join(', ')}
+ WHERE id = $1
+ RETURNING *
+ `, values);
+
+ return result.rows[0];
+}
+
+/**
+ * Get posting status (paused or active)
+ */
+let postingPaused = false;
+
+export function isPaused() {
+ return postingPaused;
+}
+
+export function setPaused(paused) {
+ postingPaused = paused;
+}
+
+export { pool };
diff --git a/src/duplicateTracker.js b/src/duplicateTracker.js
new file mode 100644
index 0000000..93233ef
--- /dev/null
+++ b/src/duplicateTracker.js
@@ -0,0 +1,81 @@
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const DATA_DIR = path.join(__dirname, '..', 'data');
+const POSTED_FILE = path.join(DATA_DIR, 'posted-artworks.json');
+const MAX_ENTRIES = 1000;
+const DUPLICATE_WINDOW_DAYS = 30;
+
+function ensureDataDir() {
+ if (!fs.existsSync(DATA_DIR)) {
+ fs.mkdirSync(DATA_DIR, { recursive: true });
+ }
+}
+
+function loadPostedArtworks() {
+ ensureDataDir();
+ try {
+ if (fs.existsSync(POSTED_FILE)) {
+ return JSON.parse(fs.readFileSync(POSTED_FILE, 'utf-8'));
+ }
+ } catch (error) {
+ console.error('Error loading posted artworks:', error.message);
+ }
+ return [];
+}
+
+function savePostedArtworks(artworks) {
+ ensureDataDir();
+ // Keep only last MAX_ENTRIES
+ const trimmed = artworks.slice(-MAX_ENTRIES);
+ fs.writeFileSync(POSTED_FILE, JSON.stringify(trimmed, null, 2));
+}
+
+export function isRecentlyPosted(objectNumber) {
+ const posted = loadPostedArtworks();
+ const cutoff = Date.now() - (DUPLICATE_WINDOW_DAYS * 24 * 60 * 60 * 1000);
+
+ return posted.some(entry => {
+ if (entry.objectNumber !== objectNumber) return false;
+ const postedTime = new Date(entry.postedAt).getTime();
+ return postedTime > cutoff;
+ });
+}
+
+export function markAsPosted(artwork, tweetId) {
+ const posted = loadPostedArtworks();
+
+ posted.push({
+ objectNumber: artwork.objectNumber,
+ title: artwork.title,
+ artist: artwork.artist,
+ museum: artwork.museum || artwork.sourceMuseum,
+ tweetId: tweetId,
+ postedAt: new Date().toISOString()
+ });
+
+ savePostedArtworks(posted);
+ console.log(`Marked as posted: ${artwork.objectNumber}`);
+}
+
+export function getRecentPosts(limit = 10) {
+ const posted = loadPostedArtworks();
+ return posted.slice(-limit).reverse();
+}
+
+export function getStats() {
+ const posted = loadPostedArtworks();
+ const cutoff = Date.now() - (DUPLICATE_WINDOW_DAYS * 24 * 60 * 60 * 1000);
+ const recent = posted.filter(p => new Date(p.postedAt).getTime() > cutoff);
+
+ return {
+ totalPosted: posted.length,
+ recentCount: recent.length,
+ oldestEntry: posted[0]?.postedAt || null,
+ newestEntry: posted[posted.length - 1]?.postedAt || null
+ };
+}
diff --git a/src/fetchArtwork.js b/src/fetchArtwork.js
new file mode 100644
index 0000000..076cf15
--- /dev/null
+++ b/src/fetchArtwork.js
@@ -0,0 +1,148 @@
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const FAMOUS_ARTWORKS = [
+ {
+ title: 'The Night Watch',
+ artist: 'Rembrandt van Rijn',
+ year: '1642',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/The_Night_Watch_-_HD.jpg/1920px-The_Night_Watch_-_HD.jpg'
+ },
+ {
+ title: 'Girl with a Pearl Earring',
+ artist: 'Johannes Vermeer',
+ year: '1665',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0f/1665_Girl_with_a_Pearl_Earring.jpg/800px-1665_Girl_with_a_Pearl_Earring.jpg'
+ },
+ {
+ title: 'The Starry Night',
+ artist: 'Vincent van Gogh',
+ year: '1889',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/e/ea/Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg/1920px-Van_Gogh_-_Starry_Night_-_Google_Art_Project.jpg'
+ },
+ {
+ title: 'The Great Wave off Kanagawa',
+ artist: 'Katsushika Hokusai',
+ year: '1831',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/The_Great_Wave_off_Kanagawa.jpg/1920px-The_Great_Wave_off_Kanagawa.jpg'
+ },
+ {
+ title: 'The Birth of Venus',
+ artist: 'Sandro Botticelli',
+ year: '1485',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/Sandro_Botticelli_-_La_nascita_di_Venere_-_Google_Art_Project_-_edited.jpg/1920px-Sandro_Botticelli_-_La_nascita_di_Venere_-_Google_Art_Project_-_edited.jpg'
+ },
+ {
+ title: 'The Garden of Earthly Delights',
+ artist: 'Hieronymus Bosch',
+ year: '1515',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/a/ae/El_jard%C3%ADn_de_las_Delicias%2C_de_El_Bosco.jpg/1920px-El_jard%C3%ADn_de_las_Delicias%2C_de_El_Bosco.jpg'
+ },
+ {
+ title: 'Las Meninas',
+ artist: 'Diego Velázquez',
+ year: '1656',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/31/Las_Meninas%2C_by_Diego_Vel%C3%A1zquez%2C_from_Prado_in_Google_Earth.jpg/1200px-Las_Meninas%2C_by_Diego_Vel%C3%A1zquez%2C_from_Prado_in_Google_Earth.jpg'
+ },
+ {
+ title: 'The Arnolfini Portrait',
+ artist: 'Jan van Eyck',
+ year: '1434',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/3/33/Van_Eyck_-_Arnolfini_Portrait.jpg/800px-Van_Eyck_-_Arnolfini_Portrait.jpg'
+ },
+ {
+ title: 'Sunflowers',
+ artist: 'Vincent van Gogh',
+ year: '1888',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Vincent_Willem_van_Gogh_127.jpg/800px-Vincent_Willem_van_Gogh_127.jpg'
+ },
+ {
+ title: 'The Milkmaid',
+ artist: 'Johannes Vermeer',
+ year: '1658',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/2/20/Johannes_Vermeer_-_Het_melkmeisje_-_Google_Art_Project.jpg/800px-Johannes_Vermeer_-_Het_melkmeisje_-_Google_Art_Project.jpg'
+ },
+ {
+ title: 'Liberty Leading the People',
+ artist: 'Eugène Delacroix',
+ year: '1830',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/5/5d/Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg/1920px-Eug%C3%A8ne_Delacroix_-_Le_28_Juillet._La_Libert%C3%A9_guidant_le_peuple.jpg'
+ },
+ {
+ title: 'Wanderer above the Sea of Fog',
+ artist: 'Caspar David Friedrich',
+ year: '1818',
+ imageUrl: 'https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg/800px-Caspar_David_Friedrich_-_Wanderer_above_the_sea_of_fog.jpg'
+ }
+];
+
+export async function fetchRandomArtwork() {
+ try {
+ const randomArtwork = FAMOUS_ARTWORKS[Math.floor(Math.random() * FAMOUS_ARTWORKS.length)];
+
+ console.log(`Fetching: ${randomArtwork.title} by ${randomArtwork.artist}...`);
+
+ const response = await axios.get(randomArtwork.imageUrl, {
+ responseType: 'arraybuffer',
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (compatible; MuseumBot/1.0; +https://github.com/museum-bot)',
+ 'Accept': 'image/webp,image/apng,image/*,*/*;q=0.8'
+ },
+ maxRedirects: 5,
+ timeout: 30000
+ });
+
+ if (!response.data || response.data.length < 1000) {
+ throw new Error('Failed to download image');
+ }
+
+ const outputDir = path.join(__dirname, '..', 'output');
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ const timestamp = new Date().getTime();
+ const fileName = `artwork_${timestamp}.jpg`;
+ const filePath = path.join(outputDir, fileName);
+
+ fs.writeFileSync(filePath, response.data);
+
+ const metadata = {
+ title: randomArtwork.title,
+ artist: randomArtwork.artist,
+ year: randomArtwork.year,
+ description: `${randomArtwork.title} is a masterpiece created by ${randomArtwork.artist} in ${randomArtwork.year}. This artwork is now in the public domain and represents a significant contribution to art history.`,
+ objectNumber: `PD-${timestamp}`,
+ imageUrl: randomArtwork.imageUrl,
+ localPath: filePath,
+ fileName: fileName,
+ museum: 'Public Domain Collection',
+ creditLine: `${randomArtwork.artist} (Public Domain)`,
+ dimensions: []
+ };
+
+ const metadataPath = path.join(outputDir, `metadata_${timestamp}.json`);
+ fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
+
+ console.log(`Artwork fetched: ${metadata.title} by ${metadata.artist}`);
+ return metadata;
+
+ } catch (error) {
+ console.error('Error fetching artwork:', error.message);
+ throw error;
+ }
+}
+
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ fetchRandomArtwork()
+ .then(metadata => console.log('Success:', metadata))
+ .catch(error => console.error('Failed:', error));
+}
\ No newline at end of file
diff --git a/src/fetchFromAIC.js b/src/fetchFromAIC.js
new file mode 100644
index 0000000..1eb4a16
--- /dev/null
+++ b/src/fetchFromAIC.js
@@ -0,0 +1,104 @@
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const AIC_API_BASE = 'https://api.artic.edu/api/v1';
+const IIIF_BASE = 'https://www.artic.edu/iiif/2';
+
+export async function fetchFromAIC() {
+ try {
+ console.log('Fetching from Art Institute of Chicago...');
+
+ // Search for public domain artworks with images
+ const response = await axios.get(`${AIC_API_BASE}/artworks/search`, {
+ params: {
+ q: '*',
+ query: {
+ bool: {
+ must: [
+ { term: { is_public_domain: true } },
+ { exists: { field: 'image_id' } }
+ ]
+ }
+ },
+ fields: 'id,title,artist_title,date_display,image_id,thumbnail,medium_display,dimensions,credit_line,place_of_origin,style_title',
+ limit: 100
+ },
+ timeout: 30000
+ });
+
+ if (!response.data || !response.data.data || response.data.data.length === 0) {
+ throw new Error('No artworks found from Art Institute of Chicago');
+ }
+
+ const artworks = response.data.data.filter(a => a.image_id);
+ if (artworks.length === 0) {
+ throw new Error('No artworks with images found');
+ }
+
+ // Pick random artwork
+ const artwork = artworks[Math.floor(Math.random() * artworks.length)];
+
+ // Construct IIIF image URL (843px wide is good quality)
+ const imageUrl = `${IIIF_BASE}/${artwork.image_id}/full/843,/0/default.jpg`;
+
+ console.log(`Downloading: ${artwork.title}`);
+ const imageResponse = await axios.get(imageUrl, {
+ responseType: 'arraybuffer',
+ timeout: 30000,
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (compatible; MuseumBot/1.0)'
+ }
+ });
+
+ if (!imageResponse.data || imageResponse.data.length < 1000) {
+ throw new Error('Failed to download image');
+ }
+
+ // Save to output directory
+ const outputDir = path.join(__dirname, '..', 'output');
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ const timestamp = Date.now();
+ const fileName = `aic_${timestamp}.jpg`;
+ const filePath = path.join(outputDir, fileName);
+ fs.writeFileSync(filePath, imageResponse.data);
+
+ // Return normalized metadata
+ const metadata = {
+ title: artwork.title || 'Untitled',
+ artist: artwork.artist_title || 'Unknown Artist',
+ year: artwork.date_display || 'Date Unknown',
+ description: artwork.medium_display || '',
+ objectNumber: `AIC-${artwork.id}`,
+ imageUrl: imageUrl,
+ localPath: filePath,
+ fileName: fileName,
+ museum: 'Art Institute of Chicago',
+ creditLine: artwork.credit_line || 'Art Institute of Chicago (Public Domain)',
+ dimensions: artwork.dimensions ? [artwork.dimensions] : [],
+ style: artwork.style_title,
+ origin: artwork.place_of_origin
+ };
+
+ console.log(`AIC: Fetched "${metadata.title}" by ${metadata.artist}`);
+ return metadata;
+
+ } catch (error) {
+ console.error('AIC fetch failed:', error.message);
+ throw error;
+ }
+}
+
+// Test if run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ fetchFromAIC()
+ .then(m => console.log('Success:', JSON.stringify(m, null, 2)))
+ .catch(e => console.error('Failed:', e.message));
+}
diff --git a/src/fetchFromMet.js b/src/fetchFromMet.js
new file mode 100644
index 0000000..32873f9
--- /dev/null
+++ b/src/fetchFromMet.js
@@ -0,0 +1,140 @@
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const MET_API_BASE = 'https://collectionapi.metmuseum.org/public/collection/v1';
+
+const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
+
+// Cache of highlight object IDs (public domain with images)
+let cachedObjectIds = null;
+
+async function getHighlightObjectIds() {
+ if (cachedObjectIds && cachedObjectIds.length > 0) {
+ return cachedObjectIds;
+ }
+
+ try {
+ // Search for highlighted public domain artworks with images
+ const response = await axios.get(`${MET_API_BASE}/search`, {
+ params: {
+ isHighlight: true,
+ hasImages: true,
+ q: '*' // All artworks
+ },
+ timeout: 30000
+ });
+
+ if (response.data && response.data.objectIDs) {
+ cachedObjectIds = response.data.objectIDs;
+ console.log(`Met Museum: Found ${cachedObjectIds.length} highlighted artworks`);
+ return cachedObjectIds;
+ }
+ } catch (error) {
+ console.error('Met Museum search failed:', error.message);
+ }
+
+ return [];
+}
+
+async function getObjectDetails(objectId) {
+ try {
+ const response = await axios.get(`${MET_API_BASE}/objects/${objectId}`, {
+ timeout: 15000
+ });
+ return response.data;
+ } catch (error) {
+ console.error(`Failed to fetch object ${objectId}:`, error.message);
+ return null;
+ }
+}
+
+export async function fetchFromMet() {
+ try {
+ console.log('Fetching from Metropolitan Museum of Art...');
+
+ const objectIds = await getHighlightObjectIds();
+ if (objectIds.length === 0) {
+ throw new Error('No artworks found from Met Museum');
+ }
+
+ // Try up to 10 random artworks to find one with valid public domain image
+ for (let attempt = 0; attempt < 10; attempt++) {
+ const randomId = objectIds[Math.floor(Math.random() * objectIds.length)];
+ const artwork = await getObjectDetails(randomId);
+
+ // Accept public domain OR CC0 licensed images
+ const hasValidLicense = artwork?.isPublicDomain ||
+ artwork?.rightsAndReproduction?.toLowerCase().includes('cc0') ||
+ artwork?.rightsAndReproduction?.toLowerCase().includes('public domain');
+
+ if (!artwork || !hasValidLicense || !artwork.primaryImage) {
+ if (attempt < 9) await sleep(500); // Small delay between attempts
+ continue;
+ }
+
+ // Download the image
+ console.log(`Downloading: ${artwork.title}`);
+ const imageResponse = await axios.get(artwork.primaryImage, {
+ responseType: 'arraybuffer',
+ timeout: 30000,
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (compatible; MuseumBot/1.0)'
+ }
+ });
+
+ if (!imageResponse.data || imageResponse.data.length < 1000) {
+ continue;
+ }
+
+ // Save to output directory
+ const outputDir = path.join(__dirname, '..', 'output');
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ const timestamp = Date.now();
+ const fileName = `met_${timestamp}.jpg`;
+ const filePath = path.join(outputDir, fileName);
+ fs.writeFileSync(filePath, imageResponse.data);
+
+ // Return normalized metadata
+ const metadata = {
+ title: artwork.title || 'Untitled',
+ artist: artwork.artistDisplayName || 'Unknown Artist',
+ year: artwork.objectDate || 'Date Unknown',
+ description: artwork.medium || '',
+ objectNumber: `MET-${artwork.objectID}`,
+ imageUrl: artwork.primaryImage,
+ localPath: filePath,
+ fileName: fileName,
+ museum: 'The Metropolitan Museum of Art',
+ creditLine: artwork.creditLine || 'The Metropolitan Museum of Art (Public Domain)',
+ dimensions: artwork.dimensions ? [artwork.dimensions] : [],
+ department: artwork.department,
+ culture: artwork.culture,
+ period: artwork.period
+ };
+
+ console.log(`Met Museum: Fetched "${metadata.title}" by ${metadata.artist}`);
+ return metadata;
+ }
+
+ throw new Error('Could not find valid public domain artwork after 5 attempts');
+
+ } catch (error) {
+ console.error('Met Museum fetch failed:', error.message);
+ throw error;
+ }
+}
+
+// Test if run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ fetchFromMet()
+ .then(m => console.log('Success:', JSON.stringify(m, null, 2)))
+ .catch(e => console.error('Failed:', e.message));
+}
diff --git a/src/fetchFromMuseums.js b/src/fetchFromMuseums.js
new file mode 100644
index 0000000..249a6fb
--- /dev/null
+++ b/src/fetchFromMuseums.js
@@ -0,0 +1,93 @@
+import { fetchFromRijksmuseum } from './fetchFromRijksmuseum.js';
+import { fetchFromMet } from './fetchFromMet.js';
+import { fetchFromAIC } from './fetchFromAIC.js';
+import { fetchRandomArtwork } from './fetchArtwork.js';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Museum sources with weights
+const MUSEUMS = [
+ { name: 'Rijksmuseum', fetch: fetchFromRijksmuseum, weight: 3, requiresKey: true },
+ { name: 'Metropolitan Museum', fetch: fetchFromMet, weight: 3, requiresKey: false },
+ { name: 'Art Institute Chicago', fetch: fetchFromAIC, weight: 3, requiresKey: false },
+ { name: 'Hardcoded Collection', fetch: fetchRandomArtwork, weight: 1, requiresKey: false }
+];
+
+function selectRandomMuseum() {
+ // Filter out museums that require keys we don't have
+ const available = MUSEUMS.filter(m => {
+ if (m.requiresKey && m.name === 'Rijksmuseum') {
+ const key = process.env.RIJKSMUSEUM_API_KEY;
+ return key && key !== '0';
+ }
+ return true;
+ });
+
+ // Weighted random selection
+ const totalWeight = available.reduce((sum, m) => sum + m.weight, 0);
+ let random = Math.random() * totalWeight;
+
+ for (const museum of available) {
+ random -= museum.weight;
+ if (random <= 0) {
+ return museum;
+ }
+ }
+
+ return available[available.length - 1];
+}
+
+export async function fetchFromMuseums() {
+ const errors = [];
+
+ // Try random museum first
+ const primaryMuseum = selectRandomMuseum();
+ console.log(`Trying primary source: ${primaryMuseum.name}`);
+
+ try {
+ const artwork = await primaryMuseum.fetch();
+ artwork.sourceMuseum = primaryMuseum.name;
+ console.log(`Success from ${primaryMuseum.name}`);
+ return artwork;
+ } catch (error) {
+ console.error(`${primaryMuseum.name} failed:`, error.message);
+ errors.push({ museum: primaryMuseum.name, error: error.message });
+ }
+
+ // Fallback cascade
+ const fallbackOrder = ['Metropolitan Museum', 'Art Institute Chicago', 'Hardcoded Collection'];
+
+ for (const museumName of fallbackOrder) {
+ if (museumName === primaryMuseum.name) continue;
+
+ const museum = MUSEUMS.find(m => m.name === museumName);
+ if (!museum) continue;
+
+ console.log(`Fallback: Trying ${museum.name}...`);
+
+ try {
+ const artwork = await museum.fetch();
+ artwork.sourceMuseum = museum.name;
+ console.log(`Success from ${museum.name} (fallback)`);
+ return artwork;
+ } catch (error) {
+ console.error(`${museum.name} failed:`, error.message);
+ errors.push({ museum: museum.name, error: error.message });
+ }
+ }
+
+ // Log all errors
+ console.error('All museum sources failed:', errors);
+ throw new Error('All museum sources failed');
+}
+
+// Test if run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ fetchFromMuseums()
+ .then(m => console.log('Success:', JSON.stringify(m, null, 2)))
+ .catch(e => console.error('Failed:', e.message));
+}
diff --git a/src/fetchFromRijksmuseum.js b/src/fetchFromRijksmuseum.js
new file mode 100644
index 0000000..b5d4fa6
--- /dev/null
+++ b/src/fetchFromRijksmuseum.js
@@ -0,0 +1,101 @@
+import axios from 'axios';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const RIJKS_API_BASE = 'https://www.rijksmuseum.nl/api/nl/collection';
+
+export async function fetchFromRijksmuseum() {
+ const apiKey = process.env.RIJKSMUSEUM_API_KEY;
+
+ if (!apiKey || apiKey === '0') {
+ throw new Error('Rijksmuseum API key not configured');
+ }
+
+ try {
+ console.log('Fetching from Rijksmuseum...');
+
+ // Get collection with images only, top pieces
+ const response = await axios.get(RIJKS_API_BASE, {
+ params: {
+ key: apiKey,
+ imgonly: true,
+ toppieces: true,
+ ps: 50, // 50 results per page
+ p: Math.floor(Math.random() * 10) + 1 // Random page 1-10
+ },
+ timeout: 30000
+ });
+
+ if (!response.data || !response.data.artObjects || response.data.artObjects.length === 0) {
+ throw new Error('No artworks found from Rijksmuseum');
+ }
+
+ // Pick random artwork with valid image
+ const artworks = response.data.artObjects.filter(a => a.webImage && a.webImage.url);
+ if (artworks.length === 0) {
+ throw new Error('No artworks with images found');
+ }
+
+ const artwork = artworks[Math.floor(Math.random() * artworks.length)];
+
+ console.log(`Downloading: ${artwork.title}`);
+ const imageResponse = await axios.get(artwork.webImage.url, {
+ responseType: 'arraybuffer',
+ timeout: 30000,
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (compatible; MuseumBot/1.0)'
+ }
+ });
+
+ if (!imageResponse.data || imageResponse.data.length < 1000) {
+ throw new Error('Failed to download image');
+ }
+
+ // Save to output directory
+ const outputDir = path.join(__dirname, '..', 'output');
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ const timestamp = Date.now();
+ const fileName = `rijks_${timestamp}.jpg`;
+ const filePath = path.join(outputDir, fileName);
+ fs.writeFileSync(filePath, imageResponse.data);
+
+ // Return normalized metadata
+ const metadata = {
+ title: artwork.title || 'Untitled',
+ artist: artwork.principalOrFirstMaker || 'Unknown Artist',
+ year: artwork.dating?.presentingDate || 'Date Unknown',
+ description: artwork.longTitle || '',
+ objectNumber: artwork.objectNumber,
+ imageUrl: artwork.webImage.url,
+ localPath: filePath,
+ fileName: fileName,
+ museum: 'Rijksmuseum, Amsterdam',
+ creditLine: 'Rijksmuseum (Public Domain)',
+ dimensions: []
+ };
+
+ console.log(`Rijksmuseum: Fetched "${metadata.title}" by ${metadata.artist}`);
+ return metadata;
+
+ } catch (error) {
+ console.error('Rijksmuseum fetch failed:', error.message);
+ throw error;
+ }
+}
+
+// Test if run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ fetchFromRijksmuseum()
+ .then(m => console.log('Success:', JSON.stringify(m, null, 2)))
+ .catch(e => console.error('Failed:', e.message));
+}
diff --git a/src/healthServer.js b/src/healthServer.js
new file mode 100644
index 0000000..dd36ae1
--- /dev/null
+++ b/src/healthServer.js
@@ -0,0 +1,115 @@
+import http from 'http';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const PORT = process.env.PORT || 7890;
+
+// Track bot state
+let lastPostTime = null;
+let nextPostTime = null;
+let isRunning = false;
+
+export function updateLastPost(time) {
+ lastPostTime = time || new Date().toISOString();
+ // Next post is 6 hours from now
+ nextPostTime = new Date(Date.now() + 6 * 60 * 60 * 1000).toISOString();
+
+ // Save to file for persistence
+ const dataDir = path.join(__dirname, '..', 'data');
+ if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
+ fs.writeFileSync(path.join(dataDir, 'last-run.json'), JSON.stringify({
+ lastPost: lastPostTime,
+ nextPost: nextPostTime,
+ updatedAt: new Date().toISOString()
+ }, null, 2));
+}
+
+export function setRunning(running) {
+ isRunning = running;
+}
+
+function getLast5Posts() {
+ try {
+ const logsDir = path.join(__dirname, '..', 'logs');
+ const logFile = path.join(logsDir, 'posts.log');
+ if (!fs.existsSync(logFile)) return [];
+
+ const content = fs.readFileSync(logFile, 'utf-8');
+ const lines = content.trim().split('\n').filter(Boolean);
+ return lines.slice(-5).map(line => {
+ try { return JSON.parse(line); } catch { return null; }
+ }).filter(Boolean);
+ } catch {
+ return [];
+ }
+}
+
+function loadLastRun() {
+ try {
+ const dataDir = path.join(__dirname, '..', 'data');
+ const lastRunFile = path.join(dataDir, 'last-run.json');
+ if (fs.existsSync(lastRunFile)) {
+ const data = JSON.parse(fs.readFileSync(lastRunFile, 'utf-8'));
+ lastPostTime = data.lastPost;
+ nextPostTime = data.nextPost;
+ }
+ } catch {}
+}
+
+const server = http.createServer((req, res) => {
+ res.setHeader('Content-Type', 'application/json');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+
+ if (req.url === '/health' || req.url === '/') {
+ const response = {
+ status: 'ok',
+ service: 'museum-art-bot',
+ isRunning,
+ lastPost: lastPostTime,
+ nextPost: nextPostTime,
+ uptime: process.uptime(),
+ timestamp: new Date().toISOString()
+ };
+ res.writeHead(200);
+ res.end(JSON.stringify(response, null, 2));
+ }
+ else if (req.url === '/status') {
+ const posts = getLast5Posts();
+ const response = {
+ status: 'ok',
+ recentPosts: posts.map(p => ({
+ timestamp: p.timestamp,
+ tweetId: p.tweetId,
+ artwork: p.artwork?.title,
+ artist: p.artwork?.artist
+ })),
+ totalPosts: posts.length,
+ lastPost: lastPostTime,
+ nextPost: nextPostTime
+ };
+ res.writeHead(200);
+ res.end(JSON.stringify(response, null, 2));
+ }
+ else {
+ res.writeHead(404);
+ res.end(JSON.stringify({ error: 'Not found' }));
+ }
+});
+
+export function startHealthServer() {
+ loadLastRun();
+ server.listen(PORT, () => {
+ console.log(`Health server running on port ${PORT}`);
+ isRunning = true;
+ });
+ return server;
+}
+
+// If run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ startHealthServer();
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..59bec7d
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,231 @@
+import cron from 'node-cron';
+import { fetchFromMuseums } from './fetchFromMuseums.js';
+import { fetchRandomArtwork } from './fetchArtwork.js';
+import { postToX, testCredentials } from './postToX.js';
+// Animation disabled - posting photos only
+// import { animateArtwork, getAnimationStats } from './animateWithGrok.js';
+import { startHealthServer, updateLastPost, setRunning } from './healthServer.js';
+import { validateCredentials } from './validateCredentials.js';
+import { isRecentlyPosted, markAsPosted } from './duplicateTracker.js';
+import { notifySuccess, notifyError } from './slackNotify.js';
+import { withRetry } from './utils/retry.js';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+async function processAndPost() {
+ console.log('\n========================================');
+ console.log('Starting museum art post process...');
+ console.log('Time:', new Date().toISOString());
+ console.log('========================================\n');
+
+ try {
+ // Step 1: Fetch artwork from museums with retry
+ console.log('Step 1: Fetching artwork from museums...');
+ let artwork;
+ let attempts = 0;
+ const maxAttempts = 5;
+
+ while (attempts < maxAttempts) {
+ attempts++;
+ try {
+ artwork = await withRetry(
+ () => fetchFromMuseums(),
+ { maxRetries: 2, initialDelay: 2000 }
+ );
+
+ // Check for duplicates
+ if (isRecentlyPosted(artwork.objectNumber)) {
+ console.log(`Skipping duplicate: ${artwork.title} (posted recently)`);
+ continue;
+ }
+
+ break;
+ } catch (error) {
+ console.log(`Fetch attempt ${attempts} failed:`, error.message);
+ if (attempts === maxAttempts) {
+ // Final fallback to hardcoded
+ console.log('Using hardcoded fallback...');
+ artwork = await fetchRandomArtwork();
+ }
+ }
+ }
+
+ console.log(`Fetched: "${artwork.title}" by ${artwork.artist}`);
+ console.log(`Source: ${artwork.museum || artwork.sourceMuseum || 'Hardcoded'}`);
+
+ // Step 2: Prepare photo for posting (no animation)
+ console.log('\nStep 2: Preparing photo for posting...');
+ const animationResult = {
+ originalImage: artwork.localPath,
+ isAnimated: false,
+ animationType: 'static_photo',
+ artworkMetadata: artwork
+ };
+ console.log(`Using static photo: ${artwork.localPath}`);
+
+ // Step 3: Post to X.com with retry
+ console.log('\nStep 3: Posting to X.com...');
+ const tweet = await withRetry(
+ () => postToX(animationResult),
+ { maxRetries: 3, initialDelay: 5000 }
+ );
+
+ console.log('\nSuccessfully posted!');
+ console.log(`Tweet ID: ${tweet.data.id}`);
+
+ // Mark as posted to prevent duplicates
+ markAsPosted(artwork, tweet.data.id);
+
+ // Send Slack notification
+ await notifySuccess(artwork, tweet.data.id);
+
+ // Cleanup old files
+ cleanupOldFiles();
+
+ // Photo posted successfully
+ console.log('\nPhoto post completed!');
+
+ return { success: true, artwork, tweet };
+
+ } catch (error) {
+ console.error('\nError in process:', error);
+
+ // Log error
+ const errorLog = {
+ timestamp: new Date().toISOString(),
+ error: error.message,
+ stack: error.stack
+ };
+
+ const logsDir = path.join(__dirname, '..', 'logs');
+ if (!fs.existsSync(logsDir)) {
+ fs.mkdirSync(logsDir, { recursive: true });
+ }
+
+ const errorFile = path.join(logsDir, 'errors.log');
+ fs.appendFileSync(errorFile, JSON.stringify(errorLog) + '\n');
+
+ // Send Slack error notification
+ await notifyError(error.message, 'processAndPost');
+
+ return { success: false, error: error.message };
+ }
+}
+
+function cleanupOldFiles() {
+ try {
+ const outputDir = path.join(__dirname, '..', 'output');
+ if (!fs.existsSync(outputDir)) return;
+
+ const files = fs.readdirSync(outputDir);
+ const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
+
+ files.forEach(file => {
+ const filePath = path.join(outputDir, file);
+ try {
+ const stats = fs.statSync(filePath);
+ if (stats.mtimeMs < oneWeekAgo) {
+ fs.unlinkSync(filePath);
+ console.log(`Cleaned up old file: ${file}`);
+ }
+ } catch (e) {}
+ });
+ } catch (error) {
+ console.log('Cleanup error:', error.message);
+ }
+}
+
+async function runOnce() {
+ console.log('Running single post...');
+
+ // Validate credentials first
+ const credsValid = validateCredentials();
+ if (!credsValid) {
+ console.error('Missing required credentials. Exiting.');
+ process.exit(1);
+ }
+
+ const result = await processAndPost();
+ if (result.success) {
+ console.log('\nPost completed successfully!');
+ process.exit(0);
+ } else {
+ console.error('\nPost failed:', result.error);
+ process.exit(1);
+ }
+}
+
+async function startCronJob() {
+ console.log('========================================');
+ console.log('Museum Art Bot Starting...');
+ console.log('========================================\n');
+
+ // Validate all credentials
+ const credsValid = validateCredentials();
+ if (!credsValid) {
+ console.error('Missing required credentials. Exiting.');
+ process.exit(1);
+ }
+
+ // Start health server
+ startHealthServer();
+
+ console.log('Testing X.com credentials...');
+ const credentialsValid = await testCredentials();
+ if (!credentialsValid) {
+ console.error('Failed to authenticate with X.com. Please check your credentials.');
+ // Don't exit - continue running for health checks
+ console.log('Bot will retry posting when credentials are fixed.');
+ }
+
+ const schedule = process.env.CRON_SCHEDULE || '0 */6 * * *';
+ console.log(`\nSetting up cron job with schedule: ${schedule}`);
+
+ // Log heartbeat every hour
+ cron.schedule('0 * * * *', () => {
+ console.log(`[Heartbeat] ${new Date().toISOString()} - Bot is alive`);
+ });
+
+ // Main posting schedule
+ cron.schedule(schedule, async () => {
+ console.log('\nCron job triggered');
+ const result = await processAndPost();
+ if (result.success) {
+ updateLastPost();
+ }
+ });
+
+ console.log('Cron job scheduled successfully');
+ console.log('Bot is running. Press Ctrl+C to stop.\n');
+ setRunning(true);
+
+ // Run initial post if credentials are valid
+ if (credentialsValid) {
+ const result = await processAndPost();
+ if (result.success) {
+ updateLastPost();
+ }
+ }
+}
+
+const args = process.argv.slice(2);
+if (args.includes('--once')) {
+ runOnce();
+} else if (args.includes('--test')) {
+ validateCredentials();
+ testCredentials().then(valid => {
+ console.log('Credentials test:', valid ? 'PASSED' : 'FAILED');
+ process.exit(valid ? 0 : 1);
+ });
+} else if (args.includes('--stats')) {
+ console.log('Photo-only mode - no animation stats available');
+} else {
+ startCronJob();
+}
diff --git a/src/postToX.js b/src/postToX.js
new file mode 100644
index 0000000..23bdfbe
--- /dev/null
+++ b/src/postToX.js
@@ -0,0 +1,222 @@
+import { TwitterApi } from 'twitter-api-v2';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import { sendPostNotification } from './sendEmail.js';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+// Rotating intro phrases
+const INTRO_PHRASES = [
+ 'Public Domain Image of the Day',
+ 'Art Spotlight',
+ 'Masterpiece of the Day',
+ 'From the Archives',
+ 'Classical Art Feature',
+ 'Timeless Beauty',
+ 'Art History Moment',
+ 'Museum Treasure'
+];
+
+// Art period hashtags
+function getArtHashtags(metadata) {
+ const hashtags = ['#PublicDomain', '#Art'];
+ const title = (metadata.title || '').toLowerCase();
+ const artist = (metadata.artist || '').toLowerCase();
+ const year = parseInt(metadata.year) || 0;
+
+ // Period-based hashtags
+ if (year < 1500) hashtags.push('#Medieval', '#Renaissance');
+ else if (year < 1700) hashtags.push('#Baroque');
+ else if (year < 1800) hashtags.push('#Neoclassical');
+ else if (year < 1870) hashtags.push('#Romanticism');
+ else if (year < 1910) hashtags.push('#Impressionism');
+ else if (year < 1950) hashtags.push('#ModernArt');
+
+ // Artist-based hashtags
+ if (artist.includes('van gogh')) hashtags.push('#VanGogh');
+ if (artist.includes('rembrandt')) hashtags.push('#Rembrandt');
+ if (artist.includes('vermeer')) hashtags.push('#Vermeer');
+ if (artist.includes('monet')) hashtags.push('#Monet');
+
+ // Subject-based hashtags
+ if (title.includes('portrait')) hashtags.push('#Portrait');
+ if (title.includes('landscape')) hashtags.push('#Landscape');
+ if (title.includes('still life') || title.includes('flower') || title.includes('sunflower')) hashtags.push('#StillLife');
+
+ // Museum hashtag
+ if (metadata.museum?.includes('Met')) hashtags.push('#MetMuseum');
+ if (metadata.museum?.includes('Rijks')) hashtags.push('#Rijksmuseum');
+ if (metadata.museum?.includes('Chicago')) hashtags.push('#ArtInstituteChicago');
+
+ return hashtags.slice(0, 5).join(' '); // Max 5 hashtags
+}
+
+function generateTweetText(metadata) {
+ const intro = INTRO_PHRASES[Math.floor(Math.random() * INTRO_PHRASES.length)];
+ const hashtags = getArtHashtags(metadata);
+ const museum = metadata.museum || metadata.sourceMuseum || 'Public Domain';
+
+ const tweetText = `${intro}
+
+"${metadata.title}"
+${metadata.artist} (${metadata.year})
+
+${museum}
+
+${hashtags}`;
+
+ // Ensure under 280 chars
+ if (tweetText.length > 280) {
+ // Truncate title if needed
+ const shortTitle = metadata.title.length > 30
+ ? metadata.title.substring(0, 27) + '...'
+ : metadata.title;
+ return `${intro}\n\n"${shortTitle}"\n${metadata.artist}\n\n${hashtags}`;
+ }
+
+ return tweetText;
+}
+
+function generateAltText(metadata) {
+ const description = metadata.description || '';
+ const period = metadata.period || metadata.style || '';
+
+ let altText = `${metadata.title} by ${metadata.artist}, ${metadata.year}.`;
+
+ if (period) altText += ` ${period}.`;
+ if (description && description.length < 500) altText += ` ${description}`;
+
+ // Twitter alt text max is 1000 chars
+ return altText.substring(0, 1000);
+}
+
+export async function postToX(animationResult) {
+ try {
+ if (!process.env.TWITTER_API_KEY || !process.env.TWITTER_ACCESS_TOKEN) {
+ throw new Error('Twitter API credentials not configured');
+ }
+
+ const client = new TwitterApi({
+ appKey: process.env.TWITTER_API_KEY,
+ appSecret: process.env.TWITTER_API_SECRET,
+ accessToken: process.env.TWITTER_ACCESS_TOKEN,
+ accessSecret: process.env.TWITTER_ACCESS_SECRET,
+ });
+
+ const metadata = animationResult.artworkMetadata;
+ // Use animated video/image if available, otherwise use original
+ const mediaPath = animationResult.animatedVideo || animationResult.animatedImage || animationResult.originalImage;
+
+ if (!fs.existsSync(mediaPath)) {
+ throw new Error(`Media file not found: ${mediaPath}`);
+ }
+
+ const tweetText = generateTweetText(metadata);
+
+ try {
+ console.log(`Uploading media: ${path.basename(mediaPath)}`);
+ const mediaId = await client.v1.uploadMedia(mediaPath);
+
+ // Add alt text for accessibility
+ const altText = generateAltText(metadata);
+ try {
+ await client.v1.createMediaMetadata(mediaId, { alt_text: { text: altText } });
+ console.log('Alt text added for accessibility');
+ } catch (altError) {
+ console.log('Could not add alt text:', altError.message);
+ }
+
+ const tweet = await client.v2.tweet({
+ text: tweetText,
+ media: {
+ media_ids: [mediaId]
+ }
+ });
+
+ console.log('Posted to X.com successfully!');
+ console.log(`Tweet ID: ${tweet.data.id}`);
+ console.log(`Tweet text: ${tweetText}`);
+
+ await sendPostNotification(tweetText, metadata, mediaPath);
+
+ const logEntry = {
+ timestamp: new Date().toISOString(),
+ tweetId: tweet.data.id,
+ tweetText: tweetText,
+ artwork: metadata,
+ mediaPath: mediaPath
+ };
+
+ const logsDir = path.join(__dirname, '..', 'logs');
+ if (!fs.existsSync(logsDir)) {
+ fs.mkdirSync(logsDir, { recursive: true });
+ }
+
+ const logFile = path.join(logsDir, 'posts.log');
+ fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n');
+
+ return tweet;
+
+ } catch (uploadError) {
+ console.error('Media upload failed:', uploadError.message);
+ if (uploadError.data) {
+ console.error('Error details:', uploadError.data);
+ }
+ console.log('Falling back to text-only tweet...');
+
+ const textOnlyTweet = await client.v2.tweet({
+ text: tweetText
+ });
+
+ console.log('Posted text-only tweet to X.com');
+ console.log(`Tweet ID: ${textOnlyTweet.data.id}`);
+
+ await sendPostNotification(tweetText, metadata, null);
+
+ return textOnlyTweet;
+ }
+
+ } catch (error) {
+ console.error('Error posting to X.com:', error.message);
+
+ if (error.code === 401) {
+ console.error('Authentication failed. Please check your Twitter API credentials.');
+ } else if (error.code === 403) {
+ console.error('Permission denied. Your app may need Elevated access for media upload.');
+ console.error('Apply for Elevated access at: https://developer.twitter.com/en/portal/products/elevated');
+ }
+
+ throw error;
+ }
+}
+
+export async function testCredentials() {
+ try {
+ const client = new TwitterApi({
+ appKey: process.env.TWITTER_API_KEY,
+ appSecret: process.env.TWITTER_API_SECRET,
+ accessToken: process.env.TWITTER_ACCESS_TOKEN,
+ accessSecret: process.env.TWITTER_ACCESS_SECRET,
+ });
+
+ const user = await client.v2.me();
+ console.log('Connected to X.com as:', user.data.username);
+
+ try {
+ const limits = await client.v2.rateLimitStatus();
+ console.log('API access level verified');
+ } catch (e) {
+ console.log('Note: App may need Elevated access for media uploads');
+ }
+
+ return true;
+ } catch (error) {
+ console.error('Failed to connect to X.com:', error.message);
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/runOvernight.js b/src/runOvernight.js
new file mode 100644
index 0000000..b2ccaf7
--- /dev/null
+++ b/src/runOvernight.js
@@ -0,0 +1,54 @@
+import { generateScheduledContent } from './contentGenerator.js';
+import { startPostingLoop } from './scheduledPoster.js';
+import { startDashboard } from './webDashboard.js';
+import { initDatabase } from './database.js';
+import { verifyVideo } from './verifyVideo.js';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+
+/**
+ * Run the overnight museum art bot
+ * 1. Generate content for next 12 hours
+ * 2. Start poster loop
+ * 3. Dashboard runs separately
+ */
+async function runOvernight() {
+ console.log('='.repeat(60));
+ console.log('MUSEUM ART BOT - OVERNIGHT MODE');
+ console.log('='.repeat(60));
+ console.log(`Started: ${new Date().toISOString()}`);
+ console.log('');
+
+ await initDatabase();
+
+ // Generate content for 12 hours (36 posts at 20 min intervals)
+ const hoursAhead = parseInt(process.argv[2]) || 12;
+ console.log(`Generating ${hoursAhead} hours of content...`);
+ console.log('');
+
+ try {
+ await generateScheduledContent(hoursAhead);
+ } catch (err) {
+ console.error('Content generation error:', err.message);
+ }
+
+ console.log('');
+ console.log('='.repeat(60));
+ console.log('Content generation complete. Starting poster loop...');
+ console.log('='.repeat(60));
+ console.log('');
+
+ // Start posting loop (checks every 60 seconds)
+ startPostingLoop(60000);
+
+ console.log('');
+ console.log('Bot is running. Posts every 20 minutes.');
+ console.log('Dashboard: http://45.61.58.125:7890');
+ console.log('');
+ console.log('Press Ctrl+C to stop.');
+}
+
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ runOvernight().catch(console.error);
+}
diff --git a/src/scheduledPoster.js b/src/scheduledPoster.js
new file mode 100644
index 0000000..de781a7
--- /dev/null
+++ b/src/scheduledPoster.js
@@ -0,0 +1,76 @@
+import { getPendingPosts, markAsPosted, markAsFailed, isPaused } from './database.js';
+import { postToX } from './postToX.js';
+import fs from 'fs';
+import { fileURLToPath } from 'url';
+
+/**
+ * Check for and post any pending scheduled posts
+ */
+export async function checkAndPost() {
+ if (isPaused()) {
+ console.log('Posting is paused');
+ return;
+ }
+
+ const pendingPosts = await getPendingPosts();
+
+ if (pendingPosts.length === 0) {
+ console.log('No posts ready to be posted');
+ return;
+ }
+
+ console.log(`Found ${pendingPosts.length} post(s) ready to post`);
+
+ for (const post of pendingPosts) {
+ console.log(`\nPosting: ${post.title} by ${post.artist}`);
+
+ try {
+ if (!fs.existsSync(post.video_path)) {
+ throw new Error(`Video file not found: ${post.video_path}`);
+ }
+
+ const animationResult = {
+ animatedVideo: post.video_path,
+ artworkMetadata: {
+ title: post.title,
+ artist: post.artist,
+ year: post.year,
+ museum: post.museum
+ },
+ isAnimated: true
+ };
+
+ const result = await postToX(animationResult);
+ const tweetId = result.data.id;
+ const tweetUrl = `https://twitter.com/goodquestionai/status/${tweetId}`;
+
+ await markAsPosted(post.id, tweetId, tweetUrl);
+ console.log(` Posted successfully! ${tweetUrl}`);
+
+ } catch (err) {
+ console.error(` Failed: ${err.message}`);
+ await markAsFailed(post.id, err.message);
+ }
+ }
+}
+
+/**
+ * Start the posting loop (checks every minute)
+ */
+export function startPostingLoop(intervalMs = 60000) {
+ console.log('Starting scheduled poster loop...');
+ console.log(`Checking every ${intervalMs / 1000} seconds`);
+
+ // Check immediately
+ checkAndPost().catch(console.error);
+
+ // Then check on interval
+ setInterval(() => {
+ checkAndPost().catch(console.error);
+ }, intervalMs);
+}
+
+// Run if called directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ startPostingLoop();
+}
diff --git a/src/sendEmail.js b/src/sendEmail.js
new file mode 100644
index 0000000..057772c
--- /dev/null
+++ b/src/sendEmail.js
@@ -0,0 +1,55 @@
+import nodemailer from 'nodemailer';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+export async function sendPostNotification(tweetText, metadata, mediaPath) {
+ try {
+ if (!process.env.EMAIL_HOST || !process.env.EMAIL_USER || !process.env.EMAIL_PASS) {
+ console.log('Email credentials not configured, skipping email notification');
+ return;
+ }
+
+ const transporter = nodemailer.createTransport({
+ host: process.env.EMAIL_HOST,
+ port: process.env.EMAIL_PORT || 587,
+ secure: process.env.EMAIL_SECURE === 'true',
+ auth: {
+ user: process.env.EMAIL_USER,
+ pass: process.env.EMAIL_PASS,
+ },
+ });
+
+ const mailOptions = {
+ from: process.env.EMAIL_FROM || process.env.EMAIL_USER,
+ to: 'steve@designerwallcoverings.com',
+ subject: `Posted: ${metadata.title}`,
+ text: tweetText,
+ html: `
+ <div style="font-family: Arial, sans-serif; max-width: 600px;">
+ <h2>New Post Published</h2>
+ <p><strong>Tweet:</strong></p>
+ <pre style="background: #f5f5f5; padding: 15px; border-radius: 5px;">${tweetText}</pre>
+ <hr>
+ <p><strong>Artwork Details:</strong></p>
+ <ul>
+ <li><strong>Title:</strong> ${metadata.title}</li>
+ <li><strong>Artist:</strong> ${metadata.artist}</li>
+ <li><strong>Year:</strong> ${metadata.year}</li>
+ <li><strong>Credit:</strong> ${metadata.creditLine}</li>
+ </ul>
+ </div>
+ `,
+ attachments: mediaPath ? [{
+ filename: mediaPath.split('/').pop(),
+ path: mediaPath
+ }] : []
+ };
+
+ await transporter.sendMail(mailOptions);
+ console.log('Email notification sent to steve@designerwallcoverings.com');
+
+ } catch (error) {
+ console.error('Error sending email:', error.message);
+ }
+}
\ No newline at end of file
diff --git a/src/slackNotify.js b/src/slackNotify.js
new file mode 100644
index 0000000..6d3112b
--- /dev/null
+++ b/src/slackNotify.js
@@ -0,0 +1,126 @@
+import https from 'https';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
+
+async function sendSlackMessage(message) {
+ if (!WEBHOOK_URL) {
+ console.log('[Slack] No webhook configured, skipping notification');
+ return false;
+ }
+
+ return new Promise((resolve, reject) => {
+ try {
+ const url = new URL(WEBHOOK_URL);
+ const payload = JSON.stringify(message);
+
+ const options = {
+ hostname: url.hostname,
+ path: url.pathname,
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Content-Length': Buffer.byteLength(payload)
+ }
+ };
+
+ const req = https.request(options, (res) => {
+ if (res.statusCode === 200) {
+ resolve(true);
+ } else {
+ console.log('[Slack] Webhook returned:', res.statusCode);
+ resolve(false);
+ }
+ });
+
+ req.on('error', (error) => {
+ console.log('[Slack] Error:', error.message);
+ resolve(false);
+ });
+
+ req.write(payload);
+ req.end();
+ } catch (error) {
+ console.log('[Slack] Error:', error.message);
+ resolve(false);
+ }
+ });
+}
+
+export async function notifySuccess(artwork, tweetId) {
+ const message = {
+ blocks: [
+ {
+ type: 'header',
+ text: {
+ type: 'plain_text',
+ text: 'Museum Art Bot Posted!'
+ }
+ },
+ {
+ type: 'section',
+ text: {
+ type: 'mrkdwn',
+ text: `*${artwork.title}*\nby ${artwork.artist} (${artwork.year})\n\nSource: ${artwork.museum || artwork.sourceMuseum || 'Public Domain'}`
+ }
+ },
+ {
+ type: 'section',
+ text: {
+ type: 'mrkdwn',
+ text: `<https://twitter.com/goodquestionai/status/${tweetId}|View Tweet>`
+ }
+ }
+ ]
+ };
+
+ return sendSlackMessage(message);
+}
+
+export async function notifyError(error, context = '') {
+ const message = {
+ blocks: [
+ {
+ type: 'header',
+ text: {
+ type: 'plain_text',
+ text: 'Museum Art Bot Error'
+ }
+ },
+ {
+ type: 'section',
+ text: {
+ type: 'mrkdwn',
+ text: `*Error:* ${error}\n*Context:* ${context || 'Unknown'}\n*Time:* ${new Date().toISOString()}`
+ }
+ }
+ ]
+ };
+
+ return sendSlackMessage(message);
+}
+
+export async function notifyDailySummary(stats) {
+ const message = {
+ blocks: [
+ {
+ type: 'header',
+ text: {
+ type: 'plain_text',
+ text: 'Museum Art Bot - Daily Summary'
+ }
+ },
+ {
+ type: 'section',
+ text: {
+ type: 'mrkdwn',
+ text: `*Posts Today:* ${stats.postsToday}\n*Errors:* ${stats.errorsToday}\n*Uptime:* ${stats.uptime}`
+ }
+ }
+ ]
+ };
+
+ return sendSlackMessage(message);
+}
diff --git a/src/utils/retry.js b/src/utils/retry.js
new file mode 100644
index 0000000..5dd4d5c
--- /dev/null
+++ b/src/utils/retry.js
@@ -0,0 +1,72 @@
+/**
+ * Retry utility with exponential backoff
+ */
+
+export async function withRetry(fn, options = {}) {
+ const {
+ maxRetries = 3,
+ initialDelay = 1000,
+ maxDelay = 30000,
+ backoffFactor = 2,
+ onRetry = null
+ } = options;
+
+ let lastError;
+ let delay = initialDelay;
+
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
+ try {
+ return await fn();
+ } catch (error) {
+ lastError = error;
+
+ if (attempt === maxRetries) {
+ break;
+ }
+
+ console.log(`Attempt ${attempt}/${maxRetries} failed: ${error.message}`);
+ console.log(`Retrying in ${delay}ms...`);
+
+ if (onRetry) {
+ onRetry(error, attempt, delay);
+ }
+
+ await sleep(delay);
+
+ // Exponential backoff with max cap
+ delay = Math.min(delay * backoffFactor, maxDelay);
+ }
+ }
+
+ throw lastError;
+}
+
+function sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Wrap an async function to never throw - returns null on error
+ */
+export function safeCall(fn, fallback = null) {
+ return async (...args) => {
+ try {
+ return await fn(...args);
+ } catch (error) {
+ console.error(`Safe call failed: ${error.message}`);
+ return fallback;
+ }
+ };
+}
+
+/**
+ * Wrap with timeout
+ */
+export function withTimeout(promise, ms, message = 'Timeout') {
+ return Promise.race([
+ promise,
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error(message)), ms)
+ )
+ ]);
+}
diff --git a/src/validateCredentials.js b/src/validateCredentials.js
new file mode 100644
index 0000000..0d67540
--- /dev/null
+++ b/src/validateCredentials.js
@@ -0,0 +1,82 @@
+import dotenv from 'dotenv';
+dotenv.config();
+
+const colors = {
+ green: '\x1b[32m',
+ red: '\x1b[31m',
+ yellow: '\x1b[33m',
+ reset: '\x1b[0m'
+};
+
+export function validateCredentials() {
+ console.log('\n=== Credential Validation ===\n');
+
+ let hasErrors = false;
+ let warnings = [];
+
+ // Required: Twitter credentials
+ const twitterKeys = [
+ 'TWITTER_API_KEY',
+ 'TWITTER_API_SECRET',
+ 'TWITTER_ACCESS_TOKEN',
+ 'TWITTER_ACCESS_SECRET'
+ ];
+
+ for (const key of twitterKeys) {
+ if (!process.env[key]) {
+ console.log(`${colors.red}[MISSING]${colors.reset} ${key} - REQUIRED`);
+ hasErrors = true;
+ } else {
+ console.log(`${colors.green}[OK]${colors.reset} ${key}`);
+ }
+ }
+
+ // Optional: Animation services
+ const optionalKeys = [
+ { key: 'REPLICATE_API_TOKEN', name: 'Replicate (AI Video)' },
+ { key: 'HUGGINGFACE_TOKEN', name: 'HuggingFace (Free AI)' },
+ { key: 'RUNWAY_API_TOKEN', name: 'Runway ML (Premium AI)' },
+ { key: 'GROK_API_KEY', name: 'Grok API' }
+ ];
+
+ console.log('\n--- Optional Animation Services ---');
+ for (const { key, name } of optionalKeys) {
+ if (!process.env[key]) {
+ console.log(`${colors.yellow}[SKIP]${colors.reset} ${key} - ${name} not configured`);
+ warnings.push(`${name} not available`);
+ } else {
+ console.log(`${colors.green}[OK]${colors.reset} ${key} - ${name}`);
+ }
+ }
+
+ // Optional: Museum APIs
+ console.log('\n--- Optional Museum APIs ---');
+ if (!process.env.RIJKSMUSEUM_API_KEY || process.env.RIJKSMUSEUM_API_KEY === '0') {
+ console.log(`${colors.yellow}[SKIP]${colors.reset} RIJKSMUSEUM_API_KEY - Using hardcoded artworks`);
+ warnings.push('Rijksmuseum API not configured - using fallback');
+ } else {
+ console.log(`${colors.green}[OK]${colors.reset} RIJKSMUSEUM_API_KEY`);
+ }
+
+ // Summary
+ console.log('\n=== Summary ===');
+ if (hasErrors) {
+ console.log(`${colors.red}FAILED: Missing required Twitter credentials${colors.reset}`);
+ console.log('Get credentials at: https://developer.twitter.com/');
+ return false;
+ }
+
+ if (warnings.length > 0) {
+ console.log(`${colors.yellow}WARNINGS:${colors.reset}`);
+ warnings.forEach(w => console.log(` - ${w}`));
+ }
+
+ console.log(`${colors.green}PASSED: All required credentials present${colors.reset}\n`);
+ return true;
+}
+
+// Run if called directly
+if (process.argv[1].includes('validateCredentials')) {
+ const valid = validateCredentials();
+ process.exit(valid ? 0 : 1);
+}
diff --git a/src/verifyVideo.js b/src/verifyVideo.js
new file mode 100644
index 0000000..f88caf3
--- /dev/null
+++ b/src/verifyVideo.js
@@ -0,0 +1,152 @@
+import { execSync } from 'child_process';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const VERIFICATION_DIR = path.join(__dirname, '..', 'verification');
+
+// Ensure verification directory exists
+if (!fs.existsSync(VERIFICATION_DIR)) fs.mkdirSync(VERIFICATION_DIR, { recursive: true });
+
+/**
+ * Verify video has actual motion/animation
+ * Returns motion score and verification result
+ */
+export async function verifyVideoMotion(videoPath) {
+ console.log(`Verifying video motion: ${path.basename(videoPath)}`);
+
+ if (!fs.existsSync(videoPath)) {
+ return { verified: false, error: 'Video file not found', motionScore: 0 };
+ }
+
+ const timestamp = Date.now();
+ const framesDir = path.join(VERIFICATION_DIR, `frames_${timestamp}`);
+ fs.mkdirSync(framesDir, { recursive: true });
+
+ try {
+ // Extract frames at different timestamps
+ const frameCount = 5;
+ for (let i = 0; i < frameCount; i++) {
+ const time = (i + 1) * 0.5; // Extract at 0.5s, 1s, 1.5s, 2s, 2.5s
+ execSync(`ffmpeg -y -ss ${time} -i "${videoPath}" -vframes 1 "${framesDir}/frame_${i}.jpg" 2>/dev/null`, { stdio: 'pipe' });
+ }
+
+ // Compare consecutive frames using ImageMagick
+ let totalDiff = 0;
+ let comparisons = 0;
+
+ for (let i = 0; i < frameCount - 1; i++) {
+ const frame1 = `${framesDir}/frame_${i}.jpg`;
+ const frame2 = `${framesDir}/frame_${i + 1}.jpg`;
+
+ if (fs.existsSync(frame1) && fs.existsSync(frame2)) {
+ try {
+ // Get difference percentage using ImageMagick compare
+ const result = execSync(`compare -metric RMSE "${frame1}" "${frame2}" null: 2>&1 || true`, { encoding: 'utf8' });
+ const match = result.match(/\(([\d.]+)\)/);
+ if (match) {
+ totalDiff += parseFloat(match[1]) * 100;
+ comparisons++;
+ }
+ } catch (e) {
+ // ImageMagick returns non-zero exit on diff, parse output anyway
+ const match = e.stdout?.toString().match(/\(([\d.]+)\)/) || e.message.match(/\(([\d.]+)\)/);
+ if (match) {
+ totalDiff += parseFloat(match[1]) * 100;
+ comparisons++;
+ }
+ }
+ }
+ }
+
+ // Cleanup frames
+ fs.rmSync(framesDir, { recursive: true, force: true });
+
+ const avgDiff = comparisons > 0 ? totalDiff / comparisons : 0;
+
+ // Motion thresholds:
+ // < 1%: No motion (static image)
+ // 1-5%: Subtle motion (panning/zoom only)
+ // 5-15%: Moderate motion (good animation)
+ // > 15%: High motion (morphing/dramatic changes)
+
+ const verified = avgDiff > 3; // At least 3% difference = animation present
+ const motionLevel = avgDiff < 1 ? 'none' : avgDiff < 5 ? 'subtle' : avgDiff < 15 ? 'moderate' : 'high';
+
+ console.log(` Motion score: ${avgDiff.toFixed(2)}% (${motionLevel})`);
+
+ return {
+ verified,
+ motionScore: avgDiff,
+ motionLevel,
+ message: verified
+ ? `Video has ${motionLevel} motion (${avgDiff.toFixed(1)}% frame difference)`
+ : `Video appears static or has minimal motion (${avgDiff.toFixed(1)}% frame difference)`
+ };
+
+ } catch (error) {
+ // Cleanup on error
+ try { fs.rmSync(framesDir, { recursive: true, force: true }); } catch (e) {}
+
+ console.error(` Verification error: ${error.message}`);
+ return { verified: false, error: error.message, motionScore: 0 };
+ }
+}
+
+/**
+ * Verify video duration is within expected range
+ */
+export function verifyVideoDuration(videoPath, minSeconds = 8, maxSeconds = 20) {
+ try {
+ const result = execSync(`ffprobe -v error -show_entries format=duration -of csv=p=0 "${videoPath}"`, { encoding: 'utf8' });
+ const duration = parseFloat(result.trim());
+
+ const verified = duration >= minSeconds && duration <= maxSeconds;
+
+ return {
+ verified,
+ duration,
+ message: verified
+ ? `Duration OK: ${duration.toFixed(1)}s`
+ : `Duration out of range: ${duration.toFixed(1)}s (expected ${minSeconds}-${maxSeconds}s)`
+ };
+ } catch (error) {
+ return { verified: false, duration: 0, error: error.message };
+ }
+}
+
+/**
+ * Full video verification
+ */
+export async function verifyVideo(videoPath) {
+ console.log(`\n=== Verifying: ${path.basename(videoPath)} ===`);
+
+ const motionResult = await verifyVideoMotion(videoPath);
+ const durationResult = verifyVideoDuration(videoPath);
+
+ const allVerified = motionResult.verified && durationResult.verified;
+
+ console.log(` Motion: ${motionResult.message}`);
+ console.log(` Duration: ${durationResult.message}`);
+ console.log(` Overall: ${allVerified ? 'VERIFIED' : 'FAILED'}`);
+
+ return {
+ verified: allVerified,
+ motion: motionResult,
+ duration: durationResult
+ };
+}
+
+// Test if run directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ const testVideo = process.argv[2];
+ if (testVideo) {
+ verifyVideo(testVideo)
+ .then(r => console.log('\nResult:', JSON.stringify(r, null, 2)))
+ .catch(e => console.error('Error:', e));
+ } else {
+ console.log('Usage: node verifyVideo.js <video-path>');
+ }
+}
diff --git a/src/webDashboard.js b/src/webDashboard.js
new file mode 100644
index 0000000..ecdb5ef
--- /dev/null
+++ b/src/webDashboard.js
@@ -0,0 +1,303 @@
+import express from 'express';
+import helmet from 'helmet';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import dotenv from 'dotenv';
+import { getAllPosts, getUpcomingPosts, deletePost, initDatabase, getPostById, updatePost, addScheduledPost } from './database.js';
+import { FAMOUS_PAINTINGS } from './artworks.js';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+const app = express();
+// Security headers via helmet (added 2026-05-04 overnight YOLO loop)
+app.use(helmet({ contentSecurityPolicy: false }));
+const PORT = process.env.DASHBOARD_PORT || 7890;
+
+app.use(express.json());
+app.use('/output', express.static(path.join(__dirname, '..', 'output')));
+
+// Dashboard HTML
+const dashboardHTML = `
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Museum Art Bot - Scheduled Posts</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <style>
+ * { box-sizing: border-box; margin: 0; padding: 0; }
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #eee; padding: 20px; }
+ h1 { text-align: center; margin-bottom: 30px; color: #00d4ff; }
+ .stats { display: flex; justify-content: center; gap: 30px; margin-bottom: 30px; }
+ .stat { background: #16213e; padding: 20px 40px; border-radius: 10px; text-align: center; }
+ .stat-number { font-size: 36px; font-weight: bold; color: #00d4ff; }
+ .stat-label { color: #888; margin-top: 5px; }
+ .posts { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
+ .post { background: #16213e; border-radius: 10px; overflow: hidden; position: relative; }
+ .post.posted { opacity: 0.6; }
+ .post.failed { border: 2px solid #ff4444; }
+ .post video, .post img { width: 100%; height: 180px; object-fit: cover; }
+ .post-info { padding: 15px; }
+ .post-title { font-size: 16px; font-weight: bold; margin-bottom: 5px; }
+ .post-artist { color: #888; font-size: 14px; margin-bottom: 10px; }
+ .post-time { color: #00d4ff; font-size: 13px; }
+ .post-status { position: absolute; top: 10px; right: 10px; padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: bold; }
+ .status-pending { background: #ffd700; color: #000; }
+ .status-posted { background: #00cc66; color: #fff; }
+ .status-failed { background: #ff4444; color: #fff; }
+ .delete-btn { background: #ff4444; color: #fff; border: none; padding: 8px 16px; border-radius: 5px; cursor: pointer; margin-top: 10px; width: 100%; }
+ .delete-btn:hover { background: #cc0000; }
+ .refresh-btn { position: fixed; bottom: 20px; right: 20px; background: #00d4ff; color: #000; border: none; padding: 15px 30px; border-radius: 30px; cursor: pointer; font-size: 16px; font-weight: bold; }
+ .tweet-link { color: #00d4ff; text-decoration: none; display: block; margin-top: 10px; }
+ .countdown { color: #ffd700; font-size: 12px; margin-top: 5px; }
+ .controls { display: flex; justify-content: center; gap: 15px; margin-bottom: 30px; }
+ .control-btn { padding: 12px 24px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: bold; }
+ .pause-btn { background: #ffd700; color: #000; }
+ .resume-btn { background: #00cc66; color: #fff; }
+ .edit-btn { background: #00d4ff; color: #000; border: none; padding: 6px 12px; border-radius: 5px; cursor: pointer; margin-top: 5px; width: 100%; font-size: 12px; }
+ .status-badge { padding: 4px 12px; border-radius: 15px; font-size: 12px; margin-left: 10px; }
+ .status-badge.active { background: #00cc66; color: #fff; }
+ .status-badge.paused { background: #ffd700; color: #000; }
+ </style>
+</head>
+<body>
+ <h1>🎨 Museum Art Bot - Scheduled Posts <span id="status-badge" class="status-badge active">ACTIVE</span></h1>
+
+ <div class="controls">
+ <button id="pause-btn" class="control-btn pause-btn" onclick="togglePause()">Pause Posting</button>
+ </div>
+
+ <div class="stats">
+ <div class="stat">
+ <div class="stat-number" id="pending-count">-</div>
+ <div class="stat-label">Pending</div>
+ </div>
+ <div class="stat">
+ <div class="stat-number" id="posted-count">-</div>
+ <div class="stat-label">Posted</div>
+ </div>
+ <div class="stat">
+ <div class="stat-number" id="failed-count">-</div>
+ <div class="stat-label">Failed</div>
+ </div>
+ </div>
+
+ <div class="posts" id="posts-container"></div>
+
+ <button class="refresh-btn" onclick="loadPosts()">🔄 Refresh</button>
+
+ <script>
+ async function loadPosts() {
+ const res = await fetch('/api/posts');
+ const posts = await res.json();
+
+ const pending = posts.filter(p => p.status === 'pending').length;
+ const posted = posts.filter(p => p.status === 'posted').length;
+ const failed = posts.filter(p => p.status === 'failed').length;
+
+ document.getElementById('pending-count').textContent = pending;
+ document.getElementById('posted-count').textContent = posted;
+ document.getElementById('failed-count').textContent = failed;
+
+ const container = document.getElementById('posts-container');
+ container.innerHTML = posts.map(post => {
+ const scheduledTime = new Date(post.scheduled_time);
+ const now = new Date();
+ const diff = scheduledTime - now;
+ const minutes = Math.floor(diff / 60000);
+ const countdown = diff > 0 ? (minutes > 60 ? Math.floor(minutes/60) + 'h ' + (minutes%60) + 'm' : minutes + ' min') : 'Now';
+
+ const videoUrl = post.video_path.replace('/root/Projects/animate-museum-posts/', '/');
+ const thumbUrl = post.thumbnail_path ? post.thumbnail_path.replace('/root/Projects/animate-museum-posts/', '/') : videoUrl;
+
+ return \`
+ <div class="post \${post.status}">
+ <video src="\${videoUrl}" muted loop onmouseenter="this.play()" onmouseleave="this.pause()"></video>
+ <span class="post-status status-\${post.status}">\${post.status.toUpperCase()}</span>
+ <div class="post-info">
+ <div class="post-title">\${post.title}</div>
+ <div class="post-artist">\${post.artist} (\${post.year})</div>
+ <div class="post-time">📅 \${scheduledTime.toLocaleString()}</div>
+ \${post.status === 'pending' ? \`<div class="countdown">⏱️ In \${countdown}</div>\` : ''}
+ \${post.tweet_url ? \`<a href="\${post.tweet_url}" target="_blank" class="tweet-link">View Tweet →</a>\` : ''}
+ \${post.status === 'pending' ? \`<button class="edit-btn" onclick="reschedulePost(\${post.id})">📅 Reschedule</button>\` : ''}
+ \${post.status === 'pending' ? \`<button class="delete-btn" onclick="deletePost(\${post.id})">🗑️ Delete</button>\` : ''}
+ </div>
+ </div>
+ \`;
+ }).join('');
+ }
+
+ async function deletePost(id) {
+ if (!confirm('Delete this scheduled post?')) return;
+ await fetch('/api/posts/' + id, { method: 'DELETE' });
+ loadPosts();
+ }
+
+ let isPaused = false;
+
+ async function loadStatus() {
+ const res = await fetch('/api/status');
+ const status = await res.json();
+ isPaused = status.paused;
+ updatePauseButton();
+ }
+
+ function updatePauseButton() {
+ const btn = document.getElementById('pause-btn');
+ const badge = document.getElementById('status-badge');
+ if (isPaused) {
+ btn.textContent = 'Resume Posting';
+ btn.className = 'control-btn resume-btn';
+ badge.textContent = 'PAUSED';
+ badge.className = 'status-badge paused';
+ } else {
+ btn.textContent = 'Pause Posting';
+ btn.className = 'control-btn pause-btn';
+ badge.textContent = 'ACTIVE';
+ badge.className = 'status-badge active';
+ }
+ }
+
+ async function togglePause() {
+ const endpoint = isPaused ? '/api/resume' : '/api/pause';
+ await fetch(endpoint, { method: 'POST' });
+ isPaused = !isPaused;
+ updatePauseButton();
+ }
+
+ async function reschedulePost(id) {
+ const newTime = prompt('Enter new time (ISO format or minutes from now):', '30');
+ if (!newTime) return;
+
+ let scheduledTime;
+ if (newTime.includes('T') || newTime.includes('-')) {
+ scheduledTime = new Date(newTime).toISOString();
+ } else {
+ const mins = parseInt(newTime);
+ scheduledTime = new Date(Date.now() + mins * 60000).toISOString();
+ }
+
+ await fetch('/api/posts/' + id, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ scheduledTime })
+ });
+ loadPosts();
+ }
+
+ loadPosts();
+ loadStatus();
+ setInterval(loadPosts, 30000);
+ setInterval(loadStatus, 30000);
+ </script>
+</body>
+</html>
+`;
+
+// Routes
+app.get('/', (req, res) => {
+ res.send(dashboardHTML);
+});
+
+app.get('/api/posts', async (req, res) => {
+ try {
+ const posts = await getAllPosts();
+ res.json(posts);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.delete('/api/posts/:id', async (req, res) => {
+ try {
+ const post = await deletePost(req.params.id);
+ if (post) {
+ // Delete the video file
+ if (fs.existsSync(post.video_path)) {
+ fs.unlinkSync(post.video_path);
+ }
+ if (post.thumbnail_path && fs.existsSync(post.thumbnail_path)) {
+ fs.unlinkSync(post.thumbnail_path);
+ }
+ res.json({ success: true });
+ } else {
+ res.status(404).json({ error: 'Post not found' });
+ }
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+app.get('/api/posts/:id', async (req, res) => {
+ try {
+ const post = await getPostById(req.params.id);
+ if (post) {
+ res.json(post);
+ } else {
+ res.status(404).json({ error: 'Post not found' });
+ }
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Update post (reschedule, edit)
+app.put('/api/posts/:id', async (req, res) => {
+ try {
+ const { title, artist, scheduledTime, status } = req.body;
+ const post = await updatePost(req.params.id, { title, artist, scheduledTime, status });
+ if (post) {
+ res.json(post);
+ } else {
+ res.status(404).json({ error: 'Post not found' });
+ }
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+// Get artworks list
+app.get('/api/artworks', (req, res) => {
+ res.json(FAMOUS_PAINTINGS.map((art, i) => ({
+ id: i,
+ title: art.title,
+ artist: art.artist,
+ year: art.year,
+ museum: art.museum,
+ imageUrl: art.imageUrl
+ })));
+});
+
+// Pause/resume state
+let isPaused = false;
+
+app.get('/api/status', (req, res) => {
+ res.json({ paused: isPaused });
+});
+
+app.post('/api/pause', (req, res) => {
+ isPaused = true;
+ res.json({ paused: true });
+});
+
+app.post('/api/resume', (req, res) => {
+ isPaused = false;
+ res.json({ paused: false });
+});
+
+export function startDashboard() {
+ initDatabase().then(() => {
+ app.listen(PORT, '0.0.0.0', () => {
+ console.log(`Dashboard running at http://45.61.58.125:${PORT}`);
+ });
+ });
+}
+
+// Run if called directly
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ startDashboard();
+}
diff --git a/tasks/prd-museum-art-bot.md b/tasks/prd-museum-art-bot.md
new file mode 100644
index 0000000..51feb44
--- /dev/null
+++ b/tasks/prd-museum-art-bot.md
@@ -0,0 +1,78 @@
+# PRD: Museum Art Animation Bot
+
+## Introduction
+Automated bot that animates 50 famous public domain artworks with AI morphing effects (cats/dogs transform, butterflies emerge) and motion effects (waves crash, stars swirl), posts to X.com @goodquestionai every 20 minutes continuously with 1s branded intro/outro.
+
+## Goals
+- 50 famous paintings rotating indefinitely every 20 mins
+- AI morphing (Kling/Minimax) with SVD motion fallback
+- 1s intro + 1s outro GoodQuestion.AI branding
+- Full admin dashboard (view, delete, reschedule, add, edit, pause/resume)
+- PostgreSQL scheduling with video verification
+- Verify animations are ACTUALLY morphing via video recording
+
+## User Stories
+
+### US-001: 50 Famous Paintings Library
+Add array of 50 public domain paintings with title, artist, year, museum, imageUrl.
+
+### US-002: AI Morphing Animation
+Use Kling/Minimax with prompts: "pitbull morphs into cat", "butterflies emerge". Verify video shows actual transformation.
+
+### US-003: Motion Animation Fallback
+SVD with motion_bucket_id=255 when morphing fails. Verify visible motion.
+
+### US-004: 1s Branded Intro/Outro
+FFmpeg 1s intro "GoodQuestion.AI" + 1s outro "@goodquestionai"
+
+### US-005: PostgreSQL Schema
+scheduled_posts table with status, tweet_id, video_path, etc.
+
+### US-006: Content Generator
+Pre-generate all 50 videos, schedule every 20 mins rotating.
+
+### US-007: Scheduled Poster
+Check every minute, post when scheduled_time <= NOW()
+
+### US-008: Dashboard View
+Grid with thumbnails, status badges, hover preview at port 7890
+
+### US-009: Dashboard Delete
+Delete button with confirmation, removes DB + video file
+
+### US-010: Dashboard Reschedule
+Time picker to change scheduled_time
+
+### US-011: Dashboard Add Posts
+Form to add custom artwork URL
+
+### US-012: Dashboard Edit
+Edit title/artist/year/museum
+
+### US-013: Dashboard Pause/Resume
+Global + individual post pause
+
+### US-014: Twitter Posting
+twitter-api-v2 with media upload, hashtags, alt text
+
+### US-015: Video Verification
+Record/screenshot each video to verify morphing is working
+
+## Functional Requirements
+- FR-1: 50 paintings array in src/artworks.js
+- FR-2: Morphing via Kling prompt-based animation
+- FR-3: Fallback to SVD motion_bucket_id=255
+- FR-4: 1s FFmpeg intro/outro
+- FR-5: PostgreSQL scheduled_posts table
+- FR-6: Content generator service
+- FR-7: Poster service checks every minute
+- FR-8: Express dashboard on port 7890
+- FR-9: CRUD operations on dashboard
+- FR-10: Continuous rotation, never stops
+- FR-11: Video verification system
+
+## Non-Goals
+- No auth on dashboard
+- No multi-platform
+- No audio
+- No analytics
diff --git a/test-all-services.js b/test-all-services.js
new file mode 100644
index 0000000..0a9fa1f
--- /dev/null
+++ b/test-all-services.js
@@ -0,0 +1,146 @@
+import fs from 'fs';
+import path from 'path';
+import axios from 'axios';
+import { fileURLToPath } from 'url';
+import { postToX } from './src/postToX.js';
+import { createRealAIVideo } from './src/animateWithReplicate.js';
+import { createHuggingFaceAnimation } from './src/animateWithHuggingFace.js';
+import dotenv from 'dotenv';
+
+dotenv.config();
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+async function downloadSunflowers() {
+ const imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Vincent_Willem_van_Gogh_127.jpg/800px-Vincent_Willem_van_Gogh_127.jpg';
+ const response = await axios.get(imageUrl, { responseType: 'arraybuffer' });
+
+ const outputDir = path.join(__dirname, 'output');
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ const timestamp = new Date().getTime();
+ const fileName = `sunflowers_${timestamp}.jpg`;
+ const filePath = path.join(outputDir, fileName);
+
+ fs.writeFileSync(filePath, response.data);
+
+ const metadata = {
+ title: 'Sunflowers',
+ artist: 'Vincent van Gogh',
+ year: '1888',
+ description: 'Testing different animation services',
+ localPath: filePath,
+ fileName: fileName,
+ creditLine: 'Vincent van Gogh (Public Domain)',
+ };
+
+ return { filePath, metadata };
+}
+
+async function postWithServiceName(result, serviceName, metadata) {
+ try {
+ // Create custom tweet text that mentions the service
+ const customTweetText = `🧪 Animation Test: ${serviceName}\n\n"${metadata.title}" by ${metadata.artist} (${metadata.year})\n\nTesting AI animation - does it move or just pan?\n\n#AIAnimation #TestPost`;
+
+ // Post to X with custom text
+ const { TwitterApi } = await import('twitter-api-v2');
+
+ const client = new TwitterApi({
+ appKey: process.env.TWITTER_API_KEY,
+ appSecret: process.env.TWITTER_API_SECRET,
+ accessToken: process.env.TWITTER_ACCESS_TOKEN,
+ accessSecret: process.env.TWITTER_ACCESS_SECRET,
+ });
+
+ const mediaPath = result.animatedVideo || result.animatedImage || result.originalImage;
+
+ console.log(`Uploading ${serviceName} video...`);
+ const mediaId = await client.v1.uploadMedia(mediaPath);
+
+ const tweet = await client.v2.tweet({
+ text: customTweetText,
+ media: {
+ media_ids: [mediaId]
+ }
+ });
+
+ console.log(`✅ Posted ${serviceName} test! Tweet ID: ${tweet.data.id}`);
+ return tweet;
+
+ } catch (error) {
+ console.error(`Failed to post ${serviceName}:`, error.message);
+ return null;
+ }
+}
+
+async function testService(serviceName, animationFunc, imagePath, metadata) {
+ console.log(`\n${'='.repeat(50)}`);
+ console.log(`Testing: ${serviceName}`);
+ console.log('='.repeat(50));
+
+ try {
+ const result = await animationFunc(imagePath, metadata);
+
+ if (result.isAnimated || result.animatedVideo) {
+ console.log(`✅ ${serviceName} created animation!`);
+
+ // Post to X with service name in tweet
+ await postWithServiceName(result, serviceName, metadata);
+
+ return true;
+ } else {
+ console.log(`❌ ${serviceName} failed to create animation`);
+ return false;
+ }
+ } catch (error) {
+ console.log(`❌ ${serviceName} error:`, error.message);
+ return false;
+ }
+}
+
+async function runAllTests() {
+ console.log('TESTING ALL ANIMATION SERVICES WITH VAN GOGH SUNFLOWERS');
+ console.log('Each animation will be posted with the service name\n');
+
+ const { filePath, metadata } = await downloadSunflowers();
+ console.log('Sunflowers downloaded:', filePath);
+
+ // Wait between posts to avoid rate limits
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
+
+ // Test 1: Replicate with Stable Video Diffusion
+ console.log('\nStarting Replicate/SVD test...');
+ await testService(
+ 'Replicate Stable Video Diffusion',
+ createRealAIVideo,
+ filePath,
+ metadata
+ );
+
+ console.log('\nWaiting 10 seconds before next test...');
+ await delay(10000);
+
+ // Test 2: Hugging Face (if configured)
+ if (process.env.HUGGINGFACE_TOKEN) {
+ console.log('\nStarting HuggingFace test...');
+ await testService(
+ 'HuggingFace AI Models',
+ createHuggingFaceAnimation,
+ filePath,
+ metadata
+ );
+ } else {
+ console.log('Skipping HuggingFace (no token configured)');
+ }
+
+ console.log('\n' + '='.repeat(50));
+ console.log('ALL TESTS COMPLETE!');
+ console.log('Check @goodquestionai to compare the animations');
+ console.log('Reply which one has real movement vs just camera panning!');
+ console.log('='.repeat(50));
+}
+
+runAllTests().catch(console.error);
\ No newline at end of file
diff --git a/test-sunflowers.js b/test-sunflowers.js
new file mode 100644
index 0000000..09278a1
--- /dev/null
+++ b/test-sunflowers.js
@@ -0,0 +1,86 @@
+import { fetchRandomArtwork } from './src/fetchArtwork.js';
+import { animateArtwork } from './src/animateWithGrok.js';
+import { postToX } from './src/postToX.js';
+import fs from 'fs';
+import path from 'path';
+import axios from 'axios';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+async function testSunflowers() {
+ try {
+ console.log('Testing with Van Gogh Sunflowers...\n');
+
+ // Download Sunflowers image directly
+ const imageUrl = 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/46/Vincent_Willem_van_Gogh_127.jpg/800px-Vincent_Willem_van_Gogh_127.jpg';
+
+ console.log('Downloading Sunflowers painting...');
+ const response = await axios.get(imageUrl, { responseType: 'arraybuffer' });
+
+ const outputDir = path.join(__dirname, 'output');
+ if (!fs.existsSync(outputDir)) {
+ fs.mkdirSync(outputDir, { recursive: true });
+ }
+
+ const timestamp = new Date().getTime();
+ const fileName = `sunflowers_test_${timestamp}.jpg`;
+ const filePath = path.join(outputDir, fileName);
+
+ fs.writeFileSync(filePath, response.data);
+ console.log('Image saved:', fileName);
+
+ const metadata = {
+ title: 'Sunflowers',
+ artist: 'Vincent van Gogh',
+ year: '1888',
+ description: 'One of Van Gogh\'s most famous paintings featuring vibrant sunflowers',
+ objectNumber: 'VG-1888',
+ imageUrl: imageUrl,
+ localPath: filePath,
+ fileName: fileName,
+ museum: 'Public Domain Collection',
+ creditLine: 'Vincent van Gogh (Public Domain)',
+ dimensions: []
+ };
+
+ console.log('\n=== STARTING AI VIDEO GENERATION ===');
+ console.log('This will use Stable Video Diffusion to create REAL animation');
+ console.log('Cost: approximately $0.02\n');
+
+ // Animate with Replicate
+ const animationResult = await animateArtwork(filePath, metadata);
+
+ if (animationResult.animatedVideo) {
+ console.log('\n✅ SUCCESS! AI video created:');
+ console.log('Video path:', animationResult.animatedVideo);
+
+ const stats = fs.statSync(animationResult.animatedVideo);
+ console.log('Video size:', (stats.size / 1024 / 1024).toFixed(2), 'MB');
+ console.log('Format:', animationResult.videoFormat || 'mp4');
+
+ console.log('\nPosting to X.com...');
+ const tweet = await postToX(animationResult);
+
+ console.log('\n🎉 COMPLETE!');
+ console.log('Tweet posted with AI-animated video');
+ console.log('Check @goodquestionai to see the animation!');
+
+ } else {
+ console.log('\n❌ Animation failed - check error messages above');
+ }
+
+ } catch (error) {
+ console.error('\n❌ Test failed:', error.message);
+ if (error.response) {
+ console.error('API Response:', error.response.data);
+ }
+ }
+}
+
+console.log('='.repeat(50));
+console.log('VAN GOGH SUNFLOWERS AI ANIMATION TEST');
+console.log('='.repeat(50));
+
+testSunflowers();
\ No newline at end of file
(oldest)
·
back to Animate Museum Posts
·
tighten .gitignore: add missing standing-rule patterns (tmp/ eb881b8 →