autokeeper 0.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- autokeeper/__init__.py +54 -0
- autokeeper/__main__.py +3 -0
- autokeeper/agents/__init__.py +0 -0
- autokeeper/agents/artist.py +88 -0
- autokeeper/agents/card.py +516 -0
- autokeeper/agents/cast.py +1446 -0
- autokeeper/agents/director.py +292 -0
- autokeeper/agents/guard.py +94 -0
- autokeeper/agents/writing.py +8 -0
- autokeeper/artkit.py +218 -0
- autokeeper/atmosphere.py +270 -0
- autokeeper/bundle/bridge/bcdice_bridge.mjs +51 -0
- autokeeper/bundle/bridge/package-lock.json +57 -0
- autokeeper/bundle/bridge/package.json +9 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/agnes.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/agnes.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/anna.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/anna.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/ante.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/ante.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/brucker_farm.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/brucker_farm.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/cardwell_farm.color +9 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/cardwell_farm.txt +9 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/caroline.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/caroline.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/church.color +11 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/church.txt +11 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/clinic.color +13 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/clinic.txt +13 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/darvill_house.color +14 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/darvill_house.txt +14 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/darvill_parents.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/darvill_parents.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/doreen.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/doreen.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/eastman_house.color +8 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/eastman_house.txt +8 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/erie.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/erie.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/francis.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/francis.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/main_street.color +10 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/main_street.txt +10 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/pastor.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/pastor.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/post_office.color +9 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/post_office.txt +9 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/ratger_house.color +10 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/ratger_house.txt +10 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/reggie.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/reggie.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/satin_hill.color +9 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/satin_hill.txt +9 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/thomas.color +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/ascii/thomas.txt +7 -0
- autokeeper/bundle/modules/dongxuezhixia/art/credits.yaml +182 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/agnes.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/anna.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/ante.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/caroline.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/darvill_parents.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/doreen.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/erie.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/francis.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/pastor.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/reggie.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/people/thomas.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/places/brucker_farm.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/places/cardwell_farm.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/places/church.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/places/clinic.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/places/darvill_house.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/places/eastman_house.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/places/main_street.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/places/ratger_house.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/art/places/satin_hill.jpg +0 -0
- autokeeper/bundle/modules/dongxuezhixia/clues.yaml +565 -0
- autokeeper/bundle/modules/dongxuezhixia/module.yaml +67 -0
- autokeeper/bundle/modules/dongxuezhixia/people.yaml +358 -0
- autokeeper/bundle/modules/dongxuezhixia/places.yaml +185 -0
- autokeeper/bundle/modules/dongxuezhixia/plugins/winter_disease/manifest.json +576 -0
- autokeeper/bundle/modules/dongxuezhixia/plugins/winter_disease/plugin.py +238 -0
- autokeeper/bundle/modules/dongxuezhixia/situation.yaml +371 -0
- autokeeper/bundle/modules/dongxuezhixia/story.yaml +199 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/annabelle.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/annabelle.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/arnold.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/arnold.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/bank.color +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/bank.txt +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/bert.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/bert.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/betty.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/betty.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/bingham.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/bingham.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/brad_todd.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/brad_todd.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/brady.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/brady.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/cavanaugh.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/cavanaugh.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/church.color +12 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/church.txt +12 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/donald.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/donald.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/elois.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/elois.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/felix.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/felix.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/ferguson_house.color +10 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/ferguson_house.txt +10 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/fitzgerald.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/fitzgerald.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/fred.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/fred.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/gladys.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/gladys.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/glenda.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/glenda.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/greenhouse.color +11 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/greenhouse.txt +11 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/gregson_store.color +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/gregson_store.txt +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/hart.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/hart.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/hart_house.color +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/hart_house.txt +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/herman.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/herman.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/hospital.color +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/hospital.txt +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/inspectors.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/inspectors.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/jensen.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/jensen.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/jensen_farm.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/jensen_farm.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/jensen_gang.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/jensen_gang.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/leonard.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/leonard.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/lowry_house.color +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/lowry_house.txt +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/lucile.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/lucile.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/main_street.color +10 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/main_street.txt +10 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/massey.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/massey.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/mccurdy.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/mccurdy.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/mcdermott_house.color +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/mcdermott_house.txt +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/meeting_house.color +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/meeting_house.txt +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/mother_bakers.color +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/mother_bakers.txt +8 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/mrs_baker.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/mrs_baker.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/park_mine.color +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/park_mine.txt +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/pearson.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/pearson.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/police_station.color +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/police_station.txt +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/raymond.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/raymond.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/stanley.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/stanley.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/steakhouse.color +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/steakhouse.txt +9 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/twins.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/twins.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/wesley.color +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/ascii/wesley.txt +7 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/credits.yaml +403 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/annabelle.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/arnold.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/bert.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/betty.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/bingham.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/brad_todd.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/brady.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/cavanaugh.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/donald.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/elois.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/felix.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/fitzgerald.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/fred.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/gladys.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/glenda.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/hart.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/herman.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/inspectors.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/jensen.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/jensen_gang.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/leonard.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/lucile.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/massey.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/mccurdy.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/mrs_baker.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/pearson.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/raymond.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/stanley.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/twins.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/people/wesley.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/church.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/ferguson_house.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/gregson_store.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/hart_house.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/hospital.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/jensen_farm.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/lowry_house.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/main_street.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/mcdermott_house.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/meeting_house.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/mother_bakers.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/park_mine.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/police_station.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/art/places/steakhouse.jpg +0 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/clues.yaml +346 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/module.yaml +72 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/people.yaml +802 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/places.yaml +273 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/plugins/stowell_survival/manifest.json +395 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/plugins/stowell_survival/plugin.py +78 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/situation.yaml +420 -0
- autokeeper/bundle/modules/fallout3-youxierenyouyanbujin/story.yaml +155 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/baoshe.color +10 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/baoshe.txt +10 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/bianji.color +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/bianji.txt +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/brennan.color +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/brennan.txt +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/douglas.color +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/douglas.txt +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/hale.color +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/hale.txt +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/huochezhan.color +12 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/huochezhan.txt +12 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jefferson.color +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jefferson.txt +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jiefang.color +12 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jiefang.txt +12 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jinboer_jia.color +11 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jinboer_jia.txt +11 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jingguan.color +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jingguan.txt +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jingju.color +12 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/jingju.txt +12 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/lingmu.color +11 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/lingmu.txt +11 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/mudi.color +11 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/mudi.txt +11 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/odell.color +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/odell.txt +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/shufang.color +10 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/shufang.txt +10 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/suidao.color +13 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/suidao.txt +13 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/thomas.color +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/thomas.txt +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/tushuguan.color +11 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/tushuguan.txt +11 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/zhanwuyuan.color +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/zhanwuyuan.txt +7 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/zhenzhongxin.color +12 -0
- autokeeper/bundle/modules/zhuishuren/art/ascii/zhenzhongxin.txt +12 -0
- autokeeper/bundle/modules/zhuishuren/art/credits.yaml +164 -0
- autokeeper/bundle/modules/zhuishuren/art/people/bianji.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/people/brennan.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/people/douglas.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/people/hale.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/people/jefferson.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/people/jingguan.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/people/odell.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/people/thomas.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/people/zhanwuyuan.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/places/huochezhan.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/places/jiefang.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/places/jinboer_jia.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/places/lingmu.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/places/mudi.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/places/shufang.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/places/suidao.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/places/tushuguan.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/art/places/zhenzhongxin.jpg +0 -0
- autokeeper/bundle/modules/zhuishuren/clues.yaml +250 -0
- autokeeper/bundle/modules/zhuishuren/module.yaml +86 -0
- autokeeper/bundle/modules/zhuishuren/people.yaml +185 -0
- autokeeper/bundle/modules/zhuishuren/places.yaml +155 -0
- autokeeper/bundle/modules/zhuishuren/plugins/shujia/manifest.json +487 -0
- autokeeper/bundle/modules/zhuishuren/plugins/shujia/plugin.py +83 -0
- autokeeper/bundle/modules/zhuishuren/situation.yaml +303 -0
- autokeeper/bundle/modules/zhuishuren/story.yaml +256 -0
- autokeeper/bundle/skills/ascii-art/SKILL.md +40 -0
- autokeeper/bundle/skills/autokeeper-play/SKILL.md +32 -0
- autokeeper/bundle/skills/coc7-combat/SKILL.md +37 -0
- autokeeper/bundle/skills/coc7-core/SKILL.md +69 -0
- autokeeper/bundle/skills/coc7-sanity/SKILL.md +40 -0
- autokeeper/bundle/skills/keeper-craft/SKILL.md +62 -0
- autokeeper/bundle/skills/pingshu-voice/SKILL.md +28 -0
- autokeeper/cli.py +1164 -0
- autokeeper/config.py +437 -0
- autokeeper/credits.py +70 -0
- autokeeper/engine/__init__.py +0 -0
- autokeeper/engine/autoplay.py +293 -0
- autokeeper/engine/check.py +107 -0
- autokeeper/engine/debrief.py +185 -0
- autokeeper/engine/developer_page/app.js +121 -0
- autokeeper/engine/developer_page/index.html +37 -0
- autokeeper/engine/developer_page/style.css +4 -0
- autokeeper/engine/developer_web.py +180 -0
- autokeeper/engine/diagnostics.py +275 -0
- autokeeper/engine/fog.py +374 -0
- autokeeper/engine/game.py +2731 -0
- autokeeper/engine/graph.py +226 -0
- autokeeper/engine/observability.py +160 -0
- autokeeper/engine/progress.py +126 -0
- autokeeper/engine/showcase.py +249 -0
- autokeeper/engine/state.py +298 -0
- autokeeper/engine/views.py +47 -0
- autokeeper/engine/visuals.py +429 -0
- autokeeper/engine/world.py +76 -0
- autokeeper/eval/__init__.py +0 -0
- autokeeper/eval/blind.py +146 -0
- autokeeper/eval/context.py +51 -0
- autokeeper/eval/experience.py +89 -0
- autokeeper/eval/lore.py +137 -0
- autokeeper/eval/rhetoric.py +237 -0
- autokeeper/eval/session.py +541 -0
- autokeeper/eval/style.py +537 -0
- autokeeper/harness/__init__.py +31 -0
- autokeeper/harness/context.py +214 -0
- autokeeper/harness/loop.py +158 -0
- autokeeper/harness/models.py +814 -0
- autokeeper/harness/replay.py +170 -0
- autokeeper/harness/search.py +108 -0
- autokeeper/harness/spend.py +98 -0
- autokeeper/harness/tools.py +199 -0
- autokeeper/harness/trace.py +201 -0
- autokeeper/imagegen.py +474 -0
- autokeeper/imagery.py +811 -0
- autokeeper/knowledge.py +239 -0
- autokeeper/mcp_server.py +447 -0
- autokeeper/module/__init__.py +0 -0
- autokeeper/module/adapt.py +934 -0
- autokeeper/module/compiler.py +379 -0
- autokeeper/module/conditions.py +195 -0
- autokeeper/module/example/art/ascii/preacher.color +7 -0
- autokeeper/module/example/art/ascii/preacher.txt +7 -0
- autokeeper/module/example/art/ascii/square.color +9 -0
- autokeeper/module/example/art/ascii/square.txt +9 -0
- autokeeper/module/example/art/ascii/warden.color +7 -0
- autokeeper/module/example/art/ascii/warden.txt +7 -0
- autokeeper/module/example/art/ascii/zhouheng.color +7 -0
- autokeeper/module/example/art/ascii/zhouheng.txt +7 -0
- autokeeper/module/example/clues.yaml +45 -0
- autokeeper/module/example/module.yaml +19 -0
- autokeeper/module/example/people.yaml +33 -0
- autokeeper/module/example/places.yaml +21 -0
- autokeeper/module/example/story.yaml +36 -0
- autokeeper/module/generator.py +106 -0
- autokeeper/module/graph.py +386 -0
- autokeeper/module/importer.py +659 -0
- autokeeper/module/improv.py +231 -0
- autokeeper/module/package.py +251 -0
- autokeeper/module/prelude.py +100 -0
- autokeeper/module/schema.py +416 -0
- autokeeper/module/simulate.py +282 -0
- autokeeper/module/sources.py +115 -0
- autokeeper/module/yamledit.py +209 -0
- autokeeper/mood.py +123 -0
- autokeeper/net/__init__.py +1 -0
- autokeeper/net/cards.py +305 -0
- autokeeper/net/hub.py +1179 -0
- autokeeper/net/link.py +59 -0
- autokeeper/net/media.py +61 -0
- autokeeper/net/page/app_index.html +264 -0
- autokeeper/net/room.py +335 -0
- autokeeper/net/web.py +74 -0
- autokeeper/paths.py +515 -0
- autokeeper/plugins/API.md +81 -0
- autokeeper/plugins/__init__.py +9 -0
- autokeeper/plugins/author.py +94 -0
- autokeeper/plugins/engine.py +631 -0
- autokeeper/plugins/runtime.py +152 -0
- autokeeper/plugins/schema.py +348 -0
- autokeeper/plugins/storage.py +30 -0
- autokeeper/plugins/worker.py +87 -0
- autokeeper/prefs.py +160 -0
- autokeeper/roster.py +51 -0
- autokeeper/rules/__init__.py +0 -0
- autokeeper/rules/bcdice.py +227 -0
- autokeeper/rules/catalog.py +135 -0
- autokeeper/rules/coc.py +418 -0
- autokeeper/rules/commands.py +214 -0
- autokeeper/rules/creation.py +134 -0
- autokeeper/rules/physdice.py +525 -0
- autokeeper/skills.py +51 -0
- autokeeper/tui/__init__.py +0 -0
- autokeeper/tui/app.py +126 -0
- autokeeper/tui/app.tcss +382 -0
- autokeeper/tui/drawing/__init__.py +0 -0
- autokeeper/tui/drawing/art.py +133 -0
- autokeeper/tui/drawing/cover.jpg +0 -0
- autokeeper/tui/drawing/cover.py +273 -0
- autokeeper/tui/drawing/dice.py +492 -0
- autokeeper/tui/drawing/diceart.py +634 -0
- autokeeper/tui/library.py +92 -0
- autokeeper/tui/modals/__init__.py +0 -0
- autokeeper/tui/modals/cardedit.py +101 -0
- autokeeper/tui/modals/debrief.py +147 -0
- autokeeper/tui/modals/dialogs.py +381 -0
- autokeeper/tui/modals/guide.py +364 -0
- autokeeper/tui/modals/guide.tcss +83 -0
- autokeeper/tui/modals/mapview.py +1042 -0
- autokeeper/tui/modals/menu.py +105 -0
- autokeeper/tui/modals/plugins.py +232 -0
- autokeeper/tui/modals/prefs.py +97 -0
- autokeeper/tui/modals/prefs.tcss +45 -0
- autokeeper/tui/modals/retell.py +55 -0
- autokeeper/tui/modals/settings.py +133 -0
- autokeeper/tui/modals/sheet.py +259 -0
- autokeeper/tui/modals/showcase.py +72 -0
- autokeeper/tui/modals/suggest.py +167 -0
- autokeeper/tui/modals/trouble.py +83 -0
- autokeeper/tui/modals/usage.py +84 -0
- autokeeper/tui/screens/__init__.py +0 -0
- autokeeper/tui/screens/card.py +424 -0
- autokeeper/tui/screens/cards.py +437 -0
- autokeeper/tui/screens/game.py +1430 -0
- autokeeper/tui/screens/host.py +371 -0
- autokeeper/tui/screens/importing.py +710 -0
- autokeeper/tui/screens/importing.tcss +156 -0
- autokeeper/tui/screens/lobby.py +578 -0
- autokeeper/tui/screens/net.py +478 -0
- autokeeper/tui/screens/prelude.py +119 -0
- autokeeper/tui/screens/prelude.tcss +36 -0
- autokeeper/tui/screens/room.tcss +153 -0
- autokeeper/tui/screens/saves.py +461 -0
- autokeeper/tui/screens/seatcard.py +119 -0
- autokeeper/tui/screens/setup.py +238 -0
- autokeeper/tui/screens/title.py +259 -0
- autokeeper/tui/widgets/__init__.py +0 -0
- autokeeper/tui/widgets/pages.py +201 -0
- autokeeper/tui/widgets/panels.py +1001 -0
- autokeeper/tui/widgets/plugin_ui.py +454 -0
- autokeeper/tui/widgets/speaker.py +285 -0
- autokeeper/tui/widgets/tips.py +243 -0
- autokeeper/weather.py +109 -0
- autokeeper/world/__init__.py +6 -0
- autokeeper/world/adapters.py +258 -0
- autokeeper/world/agents.py +518 -0
- autokeeper/world/context.py +485 -0
- autokeeper/world/desk.py +88 -0
- autokeeper/world/index.py +138 -0
- autokeeper/world/knowledge.py +380 -0
- autokeeper/world/maintenance.py +100 -0
- autokeeper/world/ops.py +1012 -0
- autokeeper/world/record.py +299 -0
- autokeeper/world/rewrite.py +179 -0
- autokeeper/world/scene.py +564 -0
- autokeeper/world/scheduler.py +523 -0
- autokeeper/world/stage.py +189 -0
- autokeeper/world/store.py +272 -0
- autokeeper/world/threads.py +42 -0
- autokeeper/world/tools.py +386 -0
- autokeeper-0.0.3.dist-info/METADATA +279 -0
- autokeeper-0.0.3.dist-info/RECORD +475 -0
- autokeeper-0.0.3.dist-info/WHEEL +4 -0
- autokeeper-0.0.3.dist-info/entry_points.txt +2 -0
- autokeeper-0.0.3.dist-info/licenses/LICENSE +21 -0
autokeeper/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""AutoKeeper: an automated Call of Cthulhu 7th edition keeper for Chinese tabletop play."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
__version__ = "0.0.3"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def utf8_stdio() -> None:
|
|
9
|
+
"""Pipes and redirects on Windows default to the ANSI code page (cp1252, cp936),
|
|
10
|
+
which cannot print every character the game uses. Switch them to UTF-8."""
|
|
11
|
+
for stream in (sys.stdout, sys.stderr):
|
|
12
|
+
encoding = (getattr(stream, "encoding", None) or "").lower().replace("-", "")
|
|
13
|
+
if encoding != "utf8" and hasattr(stream, "reconfigure"):
|
|
14
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Terminals that show 24-bit colour but do not always say so: inside WSL, Windows Terminal
|
|
18
|
+
# passes WT_SESSION and no COLORTERM, and Textual then squeezes photos into 256 colours,
|
|
19
|
+
# where blue-greys turn into the theme's navy and teal.
|
|
20
|
+
TRUECOLOR_PROGRAMS = {"vscode", "iterm.app", "wezterm", "hyper", "tabby", "ghostty", "warpterminal"}
|
|
21
|
+
TRUECOLOR_TERMS = ("kitty", "alacritty", "foot", "wezterm", "ghostty", "truecolor", "24bit", "direct")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def truecolor() -> bool:
|
|
25
|
+
"""Say COLORTERM=truecolor for a terminal known to show it; AUTOKEEPER_TRUECOLOR=1/0 decides
|
|
26
|
+
outright. True when the game may draw in 24-bit colour."""
|
|
27
|
+
import os
|
|
28
|
+
|
|
29
|
+
forced = os.environ.get("AUTOKEEPER_TRUECOLOR", "")
|
|
30
|
+
if forced in ("0", "1"):
|
|
31
|
+
if forced == "1":
|
|
32
|
+
os.environ["COLORTERM"] = "truecolor"
|
|
33
|
+
return forced == "1"
|
|
34
|
+
if os.environ.get("COLORTERM", "").lower() in ("truecolor", "24bit"):
|
|
35
|
+
return True
|
|
36
|
+
term, program = os.environ.get("TERM", "").lower(), os.environ.get("TERM_PROGRAM", "").lower()
|
|
37
|
+
vte = os.environ.get("VTE_VERSION", "0")
|
|
38
|
+
known = (
|
|
39
|
+
bool(os.environ.get("WT_SESSION") or os.environ.get("KONSOLE_VERSION"))
|
|
40
|
+
or program in TRUECOLOR_PROGRAMS
|
|
41
|
+
or any(t in term for t in TRUECOLOR_TERMS)
|
|
42
|
+
or (vte.isdigit() and int(vte) >= 3600)
|
|
43
|
+
)
|
|
44
|
+
if known:
|
|
45
|
+
os.environ["COLORTERM"] = "truecolor"
|
|
46
|
+
return known
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main() -> None:
|
|
50
|
+
utf8_stdio()
|
|
51
|
+
truecolor()
|
|
52
|
+
from .cli import app
|
|
53
|
+
|
|
54
|
+
app()
|
autokeeper/__main__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""The artist: draws ASCII art and paints colour masks for it (Claude by default)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from .. import artkit, skills
|
|
12
|
+
from ..config import Config
|
|
13
|
+
from ..harness import Reply, Session
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Mask(BaseModel):
|
|
17
|
+
mask: str = Field(description="与原图同形状的配色蒙版,每行长度与原图对应行相同")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Drawing(BaseModel):
|
|
21
|
+
art: str = Field(description="ASCII 图")
|
|
22
|
+
mask: str = Field(description="同形状的配色蒙版")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
SYSTEM = f"""你是 AutoKeeper 的美术,专画终端里的 ASCII 图,并给它上色。
|
|
26
|
+
|
|
27
|
+
{skills.wrap("ascii-art")}
|
|
28
|
+
|
|
29
|
+
配色蒙版:与图同形状的文本,每个字符是一个颜色键,“.”表示用默认色。可用颜色键:
|
|
30
|
+
{artkit.PALETTE_DOC}
|
|
31
|
+
上色要有氛围:灯火用 o/y,木头用 n/N,水和风用 c/b,夜色用 B,血用 R,诡异的记号和光用 p/P/m,人脸用 s,衣服挑一两种主色,背景结构用 k/K 压暗,让视线落在主体上。"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async def color(cfg: Config, name: str, text: str, hint: str = "") -> str:
|
|
35
|
+
session = Session("artist", cfg.role("artist"), SYSTEM, schema=Mask.model_json_schema())
|
|
36
|
+
try:
|
|
37
|
+
lines = text.split("\n")
|
|
38
|
+
prompt = (
|
|
39
|
+
f"给这张图上色:{name}。{hint}\n"
|
|
40
|
+
f"图共 {len(lines)} 行,各行长度依次为 {[len(x) for x in lines]}。\n"
|
|
41
|
+
"原图(放在代码块里,代码块本身不算):\n```\n" + text + "\n```\n"
|
|
42
|
+
"只输出蒙版,行数和每行长度必须和原图一致,空格处也要填“.”。"
|
|
43
|
+
)
|
|
44
|
+
reply = await session.send(prompt)
|
|
45
|
+
data = reply.data or {}
|
|
46
|
+
return artkit.fit_mask(text, str(data.get("mask", "")))
|
|
47
|
+
finally:
|
|
48
|
+
await session.close()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def draw(cfg: Config, subject: str, *, portrait: bool = False, on_reply: Callable[[Reply], None] | None = None) -> tuple[str, str]:
|
|
52
|
+
w, h = artkit.MAX_PORTRAIT if portrait else artkit.MAX_SCENE
|
|
53
|
+
session = Session("artist", cfg.role("artist"), SYSTEM, schema=Drawing.model_json_schema())
|
|
54
|
+
try:
|
|
55
|
+
prompt = f"画一张{'人物头像' if portrait else '场景或物件插图'}:{subject}\n宽不超过 {w} 列,高不超过 {h} 行,只用 ASCII 字符。同时给出配色蒙版。"
|
|
56
|
+
for _ in range(2):
|
|
57
|
+
reply = await session.send(prompt)
|
|
58
|
+
if on_reply:
|
|
59
|
+
on_reply(reply)
|
|
60
|
+
data = reply.data or {}
|
|
61
|
+
text = str(data.get("art", "")).rstrip("\n")
|
|
62
|
+
mask = artkit.fit_mask(text, str(data.get("mask", "")))
|
|
63
|
+
issues = artkit.check("illustration", text, mask, portrait=portrait)
|
|
64
|
+
if text.strip() and not issues:
|
|
65
|
+
return text, mask
|
|
66
|
+
prompt = "请修复并重新输出完整的 art 和 mask:" + (";".join(i.text for i in issues) or "图画不能为空")
|
|
67
|
+
raise ValueError("Artist returned an invalid illustration twice")
|
|
68
|
+
finally:
|
|
69
|
+
await session.close()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def color_folder(cfg: Config, art_dir: Path, hints: dict[str, str], *, force: bool = False, parallel: int = 4) -> list[str]:
|
|
73
|
+
"""Paint masks for every art/ascii/*.txt (or art/*.txt in a draft) that has none yet."""
|
|
74
|
+
sem = asyncio.Semaphore(parallel)
|
|
75
|
+
done: list[str] = []
|
|
76
|
+
|
|
77
|
+
async def one(path: Path) -> None:
|
|
78
|
+
mask_path = path.with_suffix(".color")
|
|
79
|
+
if mask_path.exists() and not force:
|
|
80
|
+
return
|
|
81
|
+
async with sem:
|
|
82
|
+
text = path.read_text(encoding="utf-8").rstrip("\n")
|
|
83
|
+
mask = await color(cfg, path.stem, text, hints.get(path.stem, ""))
|
|
84
|
+
mask_path.write_text(mask + "\n", encoding="utf-8")
|
|
85
|
+
done.append(path.stem)
|
|
86
|
+
|
|
87
|
+
await asyncio.gather(*(one(p) for p in sorted([*art_dir.glob("*.txt"), *art_dir.glob("ascii/*.txt")])))
|
|
88
|
+
return done
|
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
"""Character creation: from a public figure, or from a card the player already has.
|
|
2
|
+
|
|
3
|
+
a name -> what the web knows: Wikipedia (Chinese, Japanese, English) when there is a
|
|
4
|
+
page by that name, web search and the top pages otherwise; photos from
|
|
5
|
+
Wikipedia, image search and Wikimedia Commons, for the player to pick
|
|
6
|
+
-> three questions that settle what kind of investigator the player wants
|
|
7
|
+
-> an investigator for this module's era and place
|
|
8
|
+
-> changes asked for in the player's own words
|
|
9
|
+
|
|
10
|
+
The model decides who the person is in this story and what they are good at; the
|
|
11
|
+
creation rules (characteristics array, skill points, base chances) are applied in code
|
|
12
|
+
by rules/creation.py. Offline, the model works from what it already knows.
|
|
13
|
+
|
|
14
|
+
A card the player brings is read, not rebuilt: a dice bot's .st line is parsed by code,
|
|
15
|
+
a sheet in a document or a photo is copied by a model, and only impossible values are
|
|
16
|
+
corrected (rules/creation.accept). A character without numbers — a SillyTavern card, a
|
|
17
|
+
description — goes through the questions like a public figure, its picture as the photo.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import base64
|
|
24
|
+
import binascii
|
|
25
|
+
import importlib.util
|
|
26
|
+
import io
|
|
27
|
+
import json
|
|
28
|
+
import random
|
|
29
|
+
import re
|
|
30
|
+
import uuid
|
|
31
|
+
from dataclasses import asdict, dataclass, field
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Literal
|
|
34
|
+
|
|
35
|
+
import httpx
|
|
36
|
+
from PIL import Image
|
|
37
|
+
from pydantic import BaseModel, Field
|
|
38
|
+
|
|
39
|
+
from .. import credits, imagery
|
|
40
|
+
from ..config import Config
|
|
41
|
+
from ..harness import Session, search
|
|
42
|
+
from ..harness.search import resolve
|
|
43
|
+
from ..module import sources
|
|
44
|
+
from ..module.schema import Module
|
|
45
|
+
from ..rules.catalog import CHARACTERISTICS, SKILL_BASE, canonical
|
|
46
|
+
from ..rules.coc import Investigator
|
|
47
|
+
from ..rules.creation import FORMULAS, accept, make
|
|
48
|
+
from .writing import INTERFACE_COPY
|
|
49
|
+
|
|
50
|
+
RESPECT = "尊重本人:只用公开的身份和经历,写正面或中性的内容;不编造丑闻、罪行和私生活,不写住址和家人等隐私,不评价政治立场。"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CardError(Exception):
|
|
54
|
+
"""A step that did not work, already said in one plain sentence (a card made on the
|
|
55
|
+
host's models at a LAN table, net/cards.py)."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class Photo:
|
|
60
|
+
url: str
|
|
61
|
+
page: str = "" # where it was found
|
|
62
|
+
title: str = ""
|
|
63
|
+
license: str = ""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Figure:
|
|
68
|
+
query: str
|
|
69
|
+
title: str = "" # Wikipedia page, when there is one by that name
|
|
70
|
+
description: str = ""
|
|
71
|
+
extract: str = ""
|
|
72
|
+
page: str = ""
|
|
73
|
+
english: str = "" # English name, for photos on Commons
|
|
74
|
+
web: list[str] = field(default_factory=list) # search snippets and page excerpts, with their sources
|
|
75
|
+
photos: list[Photo] = field(default_factory=list)
|
|
76
|
+
source: str = "维基百科" # where `extract` comes from: Wikipedia, or the player's character card
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def name(self) -> str:
|
|
80
|
+
return self.title or self.query
|
|
81
|
+
|
|
82
|
+
def brief(self, limit: int = 4000) -> str:
|
|
83
|
+
if self.source == "描述": # the player's own idea of a character: nothing to look up
|
|
84
|
+
return f"玩家想要的调查员:{self.extract}"
|
|
85
|
+
about = f"{self.description}。" if self.description else ""
|
|
86
|
+
parts = [f"{self.source}《{self.name}》:{about}{self.extract}"] if self.extract else []
|
|
87
|
+
text = "\n".join(parts + self.web)[:limit]
|
|
88
|
+
return text or f"{self.query}(没有查到资料,按你知道的来)"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Question(BaseModel):
|
|
92
|
+
ask: str = Field(description="背景确认问题,直接询问一项内容,简洁正式,不超过 20 字")
|
|
93
|
+
options: list[str] = Field(description="2–4 个含义明确、结构平行的选项,每个不超过 24 字")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Proposal(BaseModel):
|
|
97
|
+
who: str = Field(description="玩家可见的人物简介,客观说明身份与经历,不评价、不使用宣传语,不超过 60 字")
|
|
98
|
+
questions: list[Question] = Field(description="三个问题")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Stats(BaseModel):
|
|
102
|
+
力量: int
|
|
103
|
+
体质: int
|
|
104
|
+
体型: int
|
|
105
|
+
敏捷: int
|
|
106
|
+
外貌: int
|
|
107
|
+
智力: int
|
|
108
|
+
意志: int
|
|
109
|
+
教育: int
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class Backstory(BaseModel):
|
|
113
|
+
个人描述: str
|
|
114
|
+
思想与信念: str
|
|
115
|
+
重要之人: str
|
|
116
|
+
意义非凡之地: str
|
|
117
|
+
宝贵之物: str
|
|
118
|
+
特质: str
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class Draft(BaseModel):
|
|
122
|
+
name: str = Field(description="名字:本人的名字,或改编后符合模组年代和地点的化名")
|
|
123
|
+
occupation: str = Field(description="职业,符合模组的年代和地点,可以加括号说明,如“作家(《晨报》副刊撰稿人)”")
|
|
124
|
+
age: int
|
|
125
|
+
sex: str = ""
|
|
126
|
+
residence: str = ""
|
|
127
|
+
birthplace: str = ""
|
|
128
|
+
characteristics: Stats = Field(description="把 80、70、60、60、50、50、50、40 分给八项属性,每个数用一次")
|
|
129
|
+
formula: Literal["教育×4", "教育×2+敏捷×2", "教育×2+力量×2", "教育×2+外貌×2", "教育×2+意志×2"] = Field(description="职业技能点的算法")
|
|
130
|
+
credit: int = Field(description="信用评级,从职业技能点里出")
|
|
131
|
+
occupation_skills: dict[str, int] = Field(description="职业技能在基础值之上加的点,八项左右,不含信用评级")
|
|
132
|
+
interest_skills: dict[str, int] = Field(description="兴趣技能在基础值之上加的点,合计不超过 智力×2")
|
|
133
|
+
backstory: Backstory
|
|
134
|
+
inventory: list[str] = Field(default_factory=list, description="随身物品三五件,符合年代")
|
|
135
|
+
note: str = Field("", description="玩家可见的配置说明,客观说明身份或技能侧重,不使用口号或人物评语,不超过 40 字")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class Sheet(BaseModel):
|
|
139
|
+
"""A card made elsewhere, copied as written."""
|
|
140
|
+
|
|
141
|
+
name: str = ""
|
|
142
|
+
occupation: str = ""
|
|
143
|
+
age: int = 0
|
|
144
|
+
sex: str = ""
|
|
145
|
+
residence: str = ""
|
|
146
|
+
birthplace: str = ""
|
|
147
|
+
characteristics: Stats = Field(description="八项属性,原卡写多少填多少,没写的填 0")
|
|
148
|
+
luck: int = Field(0, description="幸运,没写填 0")
|
|
149
|
+
san: int = Field(0, description="当前理智,没写填 0")
|
|
150
|
+
hp: int = Field(0, description="当前 HP,没写填 0")
|
|
151
|
+
skills: dict[str, int] = Field(default_factory=dict, description="技能的最终值,原卡写多少填多少")
|
|
152
|
+
backstory: dict[str, str] = Field(default_factory=dict, description="背景各项(个人描述、思想与信念、重要之人、意义非凡之地、宝贵之物、特质等),原卡有什么写什么")
|
|
153
|
+
inventory: list[str] = Field(default_factory=list)
|
|
154
|
+
note: str = Field("", description="玩家可见的导入说明,简洁正式地说明缺失信息及采用的假设")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
READ_SYSTEM = """你把玩家已经做好的《克苏鲁的呼唤》第 7 版人物卡录进游戏。原卡可能是骰娘的 .st 指令、Excel 或网页车卡器导出的表、PDF 人物卡、纸质卡的照片,或者一段文字。
|
|
158
|
+
|
|
159
|
+
- 原卡写了什么数值就填什么:不改、不补、不重新分配。属性填八项中文名,技能填最终值(不是加点),只填比基础值高的。
|
|
160
|
+
- 技能名换成这些标准名(括号里是专长,比如 艺术与手艺(摄影)、外语(英语)):{skills};这里没有的技能照原名保留。
|
|
161
|
+
- 原卡没写的文字项(姓名、职业、年龄、背景)留空,不要编;在 note 里说缺了什么。
|
|
162
|
+
- 原卡根本不是人物卡(没有属性数值,只是一段人设)时,八项属性都填 0,把人设里的姓名和背景照填。""" + "\n\n" + INTERFACE_COPY
|
|
163
|
+
|
|
164
|
+
REVISE_SYSTEM = """你在帮玩家改一张已经录进游戏的《克苏鲁的呼唤》第 7 版人物卡。按玩家的要求改,没提到的地方原样保留,输出改好的整张卡。
|
|
165
|
+
属性在 1–99,技能最终值不超过 99。note 里一句话说改了什么。""" + "\n\n" + INTERFACE_COPY
|
|
166
|
+
|
|
167
|
+
ASK_SYSTEM = f"""你在帮玩家把一位公众人物、一张角色卡里的人物,或者玩家自己的一句描述,做成《克苏鲁的呼唤》第 7 版的调查员。先根据资料出三个问题,弄清玩家想要什么样的角色。
|
|
168
|
+
|
|
169
|
+
- who:一句话介绍这个人,不超过 60 字。
|
|
170
|
+
- 第一个问题:在这个模组的年代和地点,他是什么身份。给 3 个选项:一个尽量贴近本人(同一个人,或者本人在那个年代最可能的样子),两个是有趣的改编,比如把歌手写成 1920 年代舞厅里的钢琴师。
|
|
171
|
+
- 第二个问题:侧重哪方面的本事。从行动(格斗、射击、闪避)、调查(侦查、聆听、图书馆使用)、学识(历史、科学、神秘学)、社交(说服、魅惑、话术)里挑贴合本人的 3 个,可以写得更具体。
|
|
172
|
+
- 第三个问题由你根据这个人来定,比如性格的哪一面、什么年纪的他、和这个案子有什么牵连。
|
|
173
|
+
- 每个问题 2–4 个选项,每个选项不超过 24 字,使用简洁、正式的问句与平行选项,不模仿闲聊口吻。
|
|
174
|
+
- {RESPECT}""" + "\n\n" + INTERFACE_COPY
|
|
175
|
+
|
|
176
|
+
BUILD_SYSTEM = f"""你按《克苏鲁的呼唤》第 7 版的规则,把一位公众人物、一张角色卡里的人物,或者玩家自己的一句描述,做成这个模组的调查员。
|
|
177
|
+
玩家给的是描述时,名字由你起,符合模组的年代和地点。
|
|
178
|
+
|
|
179
|
+
- 身份:按玩家选的来。职业、住处、随身物品都要符合模组的年代和地点,不出现那个年代没有的东西。可以用本人的名字;改编成别的身份时,可以用符合年代的化名。
|
|
180
|
+
- 属性:把 80、70、60、60、50、50、50、40 分给力量、体质、体型、敏捷、外貌、智力、意志、教育,每个数用一次,按本人的特点分,比如运动员力量和敏捷高,学者智力和教育高。
|
|
181
|
+
- 职业技能点:挑最贴合职业的算法({"、".join(FORMULAS)}),信用评级从这里出,其余分给八项左右的职业技能。
|
|
182
|
+
- 兴趣技能点:智力×2,分给任意技能。
|
|
183
|
+
- 填的是在基础值之上加的点,不是最终值。最拿手的技能最终在 60–80,最多 90。不要加克苏鲁神话。
|
|
184
|
+
- 技能名用这些,括号里的专长可以自己填,比如 艺术与手艺(钢琴)、外语(英语)、科学(物理):{{skills}}
|
|
185
|
+
- 背景:个人描述、思想与信念、重要之人、意义非凡之地、宝贵之物、特质,各一两句,从本人的经历里取材,改编到模组的年代。
|
|
186
|
+
- 玩家提了要求就照做;规则不允许的,在 note 中说明具体限制。
|
|
187
|
+
- {RESPECT}""" + "\n\n" + INTERFACE_COPY
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# -- looking them up ------------------------------------------------------------------
|
|
191
|
+
async def _json(client: httpx.AsyncClient, url: str, **params) -> dict:
|
|
192
|
+
for attempt in range(3):
|
|
193
|
+
r = await client.get(url, params=params or None)
|
|
194
|
+
if r.status_code != 429 or attempt == 2:
|
|
195
|
+
break
|
|
196
|
+
await asyncio.sleep(min(float(r.headers.get("retry-after") or 1.5), 5.0)) # Wikipedia throttles bursts
|
|
197
|
+
r.raise_for_status()
|
|
198
|
+
return r.json()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
async def _wikipedia(client: httpx.AsyncClient, fig: Figure) -> None:
|
|
202
|
+
"""The page by exactly that name (or a redirect to it), not one that merely mentions it."""
|
|
203
|
+
for lang in ("zh", "ja", "en"):
|
|
204
|
+
api = f"https://{lang}.wikipedia.org/w/api.php"
|
|
205
|
+
q = await _json(client, api, action="query", titles=fig.query, redirects=1, converttitles=1, prop="langlinks", lllang="en", format="json")
|
|
206
|
+
page = next(iter(q["query"]["pages"].values()))
|
|
207
|
+
if "missing" in page or "invalid" in page:
|
|
208
|
+
continue
|
|
209
|
+
s = await _json(client, f"https://{lang}.wikipedia.org/api/rest_v1/page/summary/{page['title']}")
|
|
210
|
+
if s.get("type") == "disambiguation":
|
|
211
|
+
continue
|
|
212
|
+
fig.title, fig.description, fig.extract = page["title"], s.get("description", ""), s.get("extract", "")[:1500]
|
|
213
|
+
fig.page = s.get("content_urls", {}).get("desktop", {}).get("page", "")
|
|
214
|
+
fig.english = page["title"] if lang == "en" else page.get("langlinks", [{}])[0].get("*", "")
|
|
215
|
+
if image := (s.get("originalimage") or s.get("thumbnail") or {}).get("source"):
|
|
216
|
+
fig.photos.insert(0, Photo(image, fig.page, f"维基百科《{fig.title}》"))
|
|
217
|
+
return
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _excerpt(text: str, name: str, size: int = 1000) -> str:
|
|
221
|
+
"""The part of a page about them: from a little before the first mention of the name."""
|
|
222
|
+
text = re.sub(r"!\[[^\]]*\]\([^)]*\)", "", text)
|
|
223
|
+
text = re.sub(r"\[([^\]]*)\]\([^)]*\)", r"\1", text)
|
|
224
|
+
text = re.sub(r"\s+", " ", text)
|
|
225
|
+
at = text.find(name)
|
|
226
|
+
return text[max(0, at - 100) : max(0, at - 100) + size] if at >= 0 else ""
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
async def _web(fig: Figure, provider: str) -> None:
|
|
230
|
+
hits = await search.search(fig.query, provider, n=6)
|
|
231
|
+
fig.web += [f"{h.title}({h.url}):{h.snippet}" for h in hits if h.snippet]
|
|
232
|
+
pages = [h.url for h in hits if "wikipedia.org" not in h.url][:2]
|
|
233
|
+
texts = await asyncio.gather(*(asyncio.wait_for(search.fetch(u, provider), 20) for u in pages), return_exceptions=True)
|
|
234
|
+
fig.web += [f"{u}:{ex}" for u, t in zip(pages, texts, strict=True) if isinstance(t, str) and (ex := _excerpt(t, fig.query))]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def _image_search(fig: Figure) -> None:
|
|
238
|
+
"""Photos from web image search (ddgs), larger ones first."""
|
|
239
|
+
if not importlib.util.find_spec("ddgs"):
|
|
240
|
+
return
|
|
241
|
+
from ddgs import DDGS
|
|
242
|
+
|
|
243
|
+
rows = await asyncio.to_thread(lambda: DDGS().images(fig.query, max_results=12))
|
|
244
|
+
rows = [r for r in rows or [] if min(int(r.get("width") or 0), int(r.get("height") or 0)) >= 300]
|
|
245
|
+
fig.photos += [Photo(r["image"], r.get("url", ""), r.get("title", "")[:40]) for r in rows[:6]]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def research(query: str, provider: str = "auto") -> Figure:
|
|
249
|
+
"""Who they are and what they look like, from whatever the web has."""
|
|
250
|
+
fig = Figure(query.strip())
|
|
251
|
+
if resolve(provider) == "none":
|
|
252
|
+
return fig
|
|
253
|
+
headers = {"User-Agent": imagery.USER_AGENT, "Accept-Language": "zh-cn"}
|
|
254
|
+
async with httpx.AsyncClient(timeout=20, headers=headers, follow_redirects=True) as client:
|
|
255
|
+
found = await asyncio.gather(_wikipedia(client, fig), _web(fig, provider), _image_search(fig), return_exceptions=True)
|
|
256
|
+
if len(fig.photos) < 3 and (fig.english or fig.title): # famous enough for Commons
|
|
257
|
+
try:
|
|
258
|
+
fig.photos += [Photo(c.thumb.replace("/360px-", "/800px-"), c.page, c.title, c.license) for c in await imagery.search(client, fig.english or fig.title, limit=8)]
|
|
259
|
+
except httpx.HTTPError:
|
|
260
|
+
pass
|
|
261
|
+
del found # a source that failed (offline, rate-limited, odd answer) just adds nothing
|
|
262
|
+
seen: set[str] = set()
|
|
263
|
+
fig.photos = [p for p in fig.photos if not (p.url in seen or seen.add(p.url))]
|
|
264
|
+
return fig
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
async def download(photo: Photo, dest: Path) -> bool:
|
|
268
|
+
"""One photo (on the web, or a file on this machine) saved at dest, at most 1024 px,
|
|
269
|
+
with where it came from next to it."""
|
|
270
|
+
try:
|
|
271
|
+
if Path(photo.url).is_file():
|
|
272
|
+
im = Image.open(photo.url).convert("RGB")
|
|
273
|
+
else:
|
|
274
|
+
async with httpx.AsyncClient(timeout=30, headers={"User-Agent": imagery.USER_AGENT}, follow_redirects=True) as client:
|
|
275
|
+
r = await client.get(photo.url)
|
|
276
|
+
r.raise_for_status()
|
|
277
|
+
im = Image.open(io.BytesIO(r.content)).convert("RGB")
|
|
278
|
+
except (httpx.HTTPError, OSError):
|
|
279
|
+
return False
|
|
280
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
im.thumbnail((1024, 1024))
|
|
282
|
+
im.save(dest, quality=92)
|
|
283
|
+
credits.put(dest, asdict(photo) | {"kind": "portrait"})
|
|
284
|
+
return True
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# -- cards the player brings -----------------------------------------------------------
|
|
288
|
+
ST_PAIR = re.compile(r"([^\d\s,,;;::|/、()()]+?)\s*[::=]?\s*(\d{1,3})(?!\d)")
|
|
289
|
+
POOLS = {"理智": "san", "SAN": "san", "幸运": "luck", "运气": "luck", "LUCK": "luck", "HP": "hp", "体力": "hp", "生命": "hp", "MP": "mp", "魔法": "mp"}
|
|
290
|
+
KNOWN = {k.split("(")[0] for k in SKILL_BASE} | {"计算机使用", "电子学"} # the modern-era ones too
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def parse_st(text: str) -> dict | None:
|
|
294
|
+
"""The numbers of a dice bot's .st line ("力量60str60敏捷50…侦查60"), read by code rather
|
|
295
|
+
than a model. None unless all eight characteristics are in it."""
|
|
296
|
+
stats: dict[str, int] = {}
|
|
297
|
+
skills: dict[str, int] = {}
|
|
298
|
+
pools: dict[str, int] = {}
|
|
299
|
+
for raw, num in ST_PAIR.findall(re.sub(r"[.。]st", " ", text, flags=re.I)):
|
|
300
|
+
word = raw.strip(".。")
|
|
301
|
+
if pool := POOLS.get(word) or POOLS.get(word.upper()):
|
|
302
|
+
pools[pool] = int(num)
|
|
303
|
+
continue
|
|
304
|
+
key = canonical(word)
|
|
305
|
+
if key in CHARACTERISTICS:
|
|
306
|
+
stats[key] = int(num)
|
|
307
|
+
elif key.split("(")[0] in KNOWN:
|
|
308
|
+
skills[key] = int(num)
|
|
309
|
+
return {"characteristics": stats, "skills": skills, **pools} if len(stats) == len(CHARACTERISTICS) else None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def is_description(text: str) -> bool:
|
|
313
|
+
"""A phrase about someone rather than a name: it says 的, lists traits, or runs long."""
|
|
314
|
+
s = text.strip()
|
|
315
|
+
return "的" in s or any(p in s for p in ",,、;;。") or len(s) > (24 if s.isascii() else 10)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def local_file(text: str) -> Path | None:
|
|
319
|
+
"""A file typed, pasted or dragged in (quoted, escaped, a file:// URI, a Windows path under WSL)."""
|
|
320
|
+
p = sources.dropped_path(text)
|
|
321
|
+
return p if p is not None and p.is_file() else None
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _tavern(data: object) -> dict | None:
|
|
325
|
+
"""The character in a SillyTavern card (spec v1, v2 or v3)."""
|
|
326
|
+
if not isinstance(data, dict):
|
|
327
|
+
return None
|
|
328
|
+
body = data["data"] if isinstance(data.get("data"), dict) else data
|
|
329
|
+
if not body.get("name") or not any(body.get(k) for k in ("description", "personality", "scenario")):
|
|
330
|
+
return None
|
|
331
|
+
name = str(body["name"]).strip()
|
|
332
|
+
parts = [("人物", body.get("description")), ("性格", body.get("personality")), ("背景", body.get("scenario")), ("开场白", body.get("first_mes"))]
|
|
333
|
+
text = "\n".join(f"{k}:{v.strip()}" for k, v in parts if isinstance(v, str) and v.strip())
|
|
334
|
+
return {"name": name, "text": text.replace("{{char}}", name).replace("{{user}}", "你").replace("<START>", "")[:2500]}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def read_file(path: Path) -> tuple[dict | None, str, list[bytes] | None]:
|
|
338
|
+
"""(a character card's persona, the text of a sheet, an image of a sheet): one of them."""
|
|
339
|
+
suffix = path.suffix.lower()
|
|
340
|
+
if suffix in (".png", ".webp", ".jpg", ".jpeg"):
|
|
341
|
+
with Image.open(path) as im:
|
|
342
|
+
raw = im.info.get("ccv3") or im.info.get("chara") # SillyTavern keeps the card in a text chunk
|
|
343
|
+
if raw:
|
|
344
|
+
try:
|
|
345
|
+
if persona := _tavern(json.loads(base64.b64decode(raw))):
|
|
346
|
+
return persona, "", None
|
|
347
|
+
except (ValueError, binascii.Error):
|
|
348
|
+
pass
|
|
349
|
+
buf = io.BytesIO()
|
|
350
|
+
im.convert("RGB").save(buf, format="PNG")
|
|
351
|
+
return None, "", [buf.getvalue()]
|
|
352
|
+
if suffix == ".json":
|
|
353
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
354
|
+
return (persona, "", None) if (persona := _tavern(data)) else (None, json.dumps(data, ensure_ascii=False)[:12000], None)
|
|
355
|
+
from markitdown import MarkItDown
|
|
356
|
+
|
|
357
|
+
return None, MarkItDown().convert(str(path)).text_content[:12000], None
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
# -- the agent ------------------------------------------------------------------------
|
|
361
|
+
def _setting(m: Module) -> str:
|
|
362
|
+
return f"【模组】《{m.title}》 {m.era} {m.setting}\n【开场】{m.hook.strip()[:400]}"
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def describe(inv: Investigator) -> str:
|
|
366
|
+
"""A sheet in one paragraph, for the model and for the record."""
|
|
367
|
+
stats = " ".join(f"{k}{v}" for k, v in inv.characteristics.items())
|
|
368
|
+
skills = " ".join(f"{k}{v}" for k, v in inv.skills.items())
|
|
369
|
+
story = ";".join(f"{k}:{v.rstrip('。')}" for k, v in inv.backstory.items())
|
|
370
|
+
return f"{inv.name},{inv.age} 岁,{inv.occupation}。属性:{stats},幸运{inv.luck}。技能:{skills}。{story}。随身:{'、'.join(inv.inventory)}"
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class Builder:
|
|
374
|
+
"""One card being made: the person, the answers, the requests, the latest sheet."""
|
|
375
|
+
|
|
376
|
+
def __init__(self, cfg: Config, m: Module, *, luck: int | None = None):
|
|
377
|
+
self.cfg, self.m = cfg, m
|
|
378
|
+
self.id = f"card-{uuid.uuid4().hex[:6]}"
|
|
379
|
+
self.luck = luck if luck is not None else sum(random.randint(1, 6) for _ in range(3)) * 5
|
|
380
|
+
self.figure: Figure | None = None
|
|
381
|
+
self.photo: Photo | None = None # the one the player picked
|
|
382
|
+
self.answers: list[tuple[str, str]] = []
|
|
383
|
+
self.requests: list[str] = []
|
|
384
|
+
self.card: Investigator | None = None
|
|
385
|
+
self.fixes: list[str] = []
|
|
386
|
+
self.note = ""
|
|
387
|
+
self.kind = "figure" # figure: built by the rules; sheet: a card brought as it was
|
|
388
|
+
|
|
389
|
+
async def _ask_model(self, system: str, prompt: str, schema: type[BaseModel], *, images: list[bytes] | None = None):
|
|
390
|
+
role = self.cfg.role("vision" if images else "card")
|
|
391
|
+
s = Session("card", role, system, schema=schema.model_json_schema())
|
|
392
|
+
try:
|
|
393
|
+
return schema.model_validate((await s.send(prompt, images=images)).data or {})
|
|
394
|
+
finally:
|
|
395
|
+
await s.close()
|
|
396
|
+
|
|
397
|
+
async def read(self, source: str, *, files: bool = True) -> str:
|
|
398
|
+
"""A card the player brings: pasted text or a file. "sheet" when it carries CoC numbers
|
|
399
|
+
(self.card is ready); "persona" for a character without them, set up as self.figure
|
|
400
|
+
to go through the questions like a public figure. `files`: False when the text came
|
|
401
|
+
from someone else's machine, so a path in it is never opened here."""
|
|
402
|
+
persona, text, images = None, source, None
|
|
403
|
+
if files and (path := local_file(source)):
|
|
404
|
+
persona, text, images = await asyncio.to_thread(read_file, path)
|
|
405
|
+
if persona and images is None and path.suffix.lower() in (".png", ".webp", ".jpg", ".jpeg"):
|
|
406
|
+
self.figure = Figure(persona["name"], title=persona["name"], source="角色卡", extract=persona["text"], photos=[Photo(str(path), title="角色卡的图")])
|
|
407
|
+
self.kind = "figure"
|
|
408
|
+
return "persona"
|
|
409
|
+
if persona:
|
|
410
|
+
self.figure = Figure(persona["name"], title=persona["name"], source="角色卡", extract=persona["text"])
|
|
411
|
+
self.kind = "figure"
|
|
412
|
+
return "persona"
|
|
413
|
+
numbers = parse_st(text) if text else None
|
|
414
|
+
rest = ST_PAIR.sub("", re.sub(r"[.。]st", "", text or "", flags=re.I)).strip(" \n,,;;.。")
|
|
415
|
+
sheet = Sheet(characteristics=Stats(**dict.fromkeys(CHARACTERISTICS, 0)))
|
|
416
|
+
if images or numbers is None or len(rest) > 10: # more than a bare .st line: names, a story, a scan
|
|
417
|
+
skills = "、".join(k for k in SKILL_BASE)
|
|
418
|
+
sheet = await self._ask_model(READ_SYSTEM.format(skills=skills), f"【原卡】\n{text or '(见图片)'}", Sheet, images=images)
|
|
419
|
+
stats = numbers["characteristics"] if numbers else sheet.characteristics.model_dump()
|
|
420
|
+
if not any(stats.values()): # not a sheet at all: a character to build one for
|
|
421
|
+
name = sheet.name or "这个角色"
|
|
422
|
+
self.figure = Figure(name, title=name, source="人设", extract=(text or "")[:2500])
|
|
423
|
+
self.kind = "figure"
|
|
424
|
+
return "persona"
|
|
425
|
+
skills_found = {**sheet.skills, **(numbers["skills"] if numbers else {})}
|
|
426
|
+
pools = {k: (numbers or {}).get(k) or getattr(sheet, k, 0) for k in ("luck", "san", "hp")} | {"mp": (numbers or {}).get("mp", 0)}
|
|
427
|
+
self.card, self.fixes = accept(
|
|
428
|
+
self.id, name=sheet.name or "无名调查员", occupation=sheet.occupation, age=sheet.age or 30, sex=sheet.sex,
|
|
429
|
+
residence=sheet.residence, birthplace=sheet.birthplace, characteristics=stats, skills=skills_found,
|
|
430
|
+
backstory={"说明": "玩家带来的人物卡"} | {k: v for k, v in sheet.backstory.items() if v}, inventory=sheet.inventory,
|
|
431
|
+
portrait=self.id, **pools,
|
|
432
|
+
) # fmt: skip
|
|
433
|
+
self.note = sheet.note or ("这张卡没有名字和职业,可以在“提要求修改”里补上" if not sheet.name else "")
|
|
434
|
+
self.kind = "sheet"
|
|
435
|
+
return "sheet"
|
|
436
|
+
|
|
437
|
+
async def look_up(self, query: str) -> Figure:
|
|
438
|
+
"""A name is looked up on the web; a description ("在码头长大、胆子很大的女记者") is taken
|
|
439
|
+
as the player's idea, with nothing to search for."""
|
|
440
|
+
if is_description(query):
|
|
441
|
+
self.figure = Figure(query.strip(), source="描述", extract=query.strip())
|
|
442
|
+
else:
|
|
443
|
+
self.figure = await research(query, self.cfg.search)
|
|
444
|
+
return self.figure
|
|
445
|
+
|
|
446
|
+
async def propose(self) -> Proposal:
|
|
447
|
+
assert self.figure
|
|
448
|
+
prompt = f"{_setting(self.m)}\n【这个人】{self.figure.brief()}"
|
|
449
|
+
p = await self._ask_model(ASK_SYSTEM, prompt, Proposal)
|
|
450
|
+
p.questions = [q for q in p.questions if q.options][:3]
|
|
451
|
+
return p
|
|
452
|
+
|
|
453
|
+
async def build(self, request: str = "") -> Investigator:
|
|
454
|
+
"""A new sheet from the answers and every request so far (the latest one included).
|
|
455
|
+
A card the player brought is changed as asked instead, its numbers kept otherwise."""
|
|
456
|
+
if self.kind == "sheet":
|
|
457
|
+
return await self._revise(request)
|
|
458
|
+
assert self.figure
|
|
459
|
+
if request:
|
|
460
|
+
self.requests.append(request)
|
|
461
|
+
lines = [_setting(self.m), f"【这个人】{self.figure.brief()}"]
|
|
462
|
+
lines += ["【玩家的选择】", *(f"- {q}:{a}" for q, a in self.answers)]
|
|
463
|
+
if self.requests:
|
|
464
|
+
lines += ["【玩家的要求】", *(f"- {r}" for r in self.requests)]
|
|
465
|
+
if self.card:
|
|
466
|
+
lines.append(f"【现在的卡】{describe(self.card)}")
|
|
467
|
+
skills = "、".join(k for k in SKILL_BASE if k != "克苏鲁神话")
|
|
468
|
+
d = await self._ask_model(BUILD_SYSTEM.format(skills=skills), "\n".join(lines), Draft)
|
|
469
|
+
story = {"说明": f"AutoKeeper 根据{self.figure.name}改编"} | d.backstory.model_dump()
|
|
470
|
+
card, self.fixes = make(
|
|
471
|
+
self.id, name=d.name, occupation=d.occupation, age=d.age, characteristics=d.characteristics.model_dump(),
|
|
472
|
+
formula=d.formula, credit=d.credit, occupation_skills=d.occupation_skills, interest_skills=d.interest_skills,
|
|
473
|
+
luck=self.luck, sex=d.sex, residence=d.residence, birthplace=d.birthplace, portrait=self.id,
|
|
474
|
+
backstory=story, inventory=d.inventory,
|
|
475
|
+
) # fmt: skip
|
|
476
|
+
self.card, self.note = card, d.note
|
|
477
|
+
return card
|
|
478
|
+
|
|
479
|
+
async def _revise(self, request: str) -> Investigator:
|
|
480
|
+
assert self.card
|
|
481
|
+
if request:
|
|
482
|
+
self.requests.append(request)
|
|
483
|
+
c = self.card
|
|
484
|
+
prompt = f"【现在的卡】{describe(c)}\n当前理智{c.san} HP{c.hp}\n【玩家的要求】{request}"
|
|
485
|
+
d = await self._ask_model(REVISE_SYSTEM, prompt, Sheet)
|
|
486
|
+
story = {"说明": c.backstory.get("说明", "玩家带来的人物卡")} | {k: v for k, v in d.backstory.items() if v and k != "说明"}
|
|
487
|
+
card, self.fixes = accept(
|
|
488
|
+
self.id, name=d.name or c.name, occupation=d.occupation or c.occupation, age=d.age or c.age, sex=d.sex or c.sex,
|
|
489
|
+
residence=d.residence or c.residence, birthplace=d.birthplace or c.birthplace,
|
|
490
|
+
characteristics=d.characteristics.model_dump(), skills=d.skills, luck=d.luck or c.luck, san=d.san or c.san,
|
|
491
|
+
hp=d.hp or c.hp, backstory=story or c.backstory, inventory=d.inventory or c.inventory, portrait=self.id,
|
|
492
|
+
) # fmt: skip
|
|
493
|
+
self.card, self.note = card, d.note
|
|
494
|
+
return card
|
|
495
|
+
|
|
496
|
+
async def find(self, query: str) -> Figure:
|
|
497
|
+
"""Photos for a card that came without one, by name (the card screen's 配一张照片)."""
|
|
498
|
+
self.figure = await research(query, self.cfg.search)
|
|
499
|
+
return self.figure
|
|
500
|
+
|
|
501
|
+
async def download(self, i: int, dest: Path) -> bool:
|
|
502
|
+
"""Photo candidate `i` of the figure, saved at dest."""
|
|
503
|
+
return bool(self.figure and 0 <= i < len(self.figure.photos)) and await download(self.figure.photos[i], dest)
|
|
504
|
+
|
|
505
|
+
def keep(self, photo: Path | None) -> None:
|
|
506
|
+
"""The finished card into 我的角色, with the photo the player picked."""
|
|
507
|
+
from .. import roster
|
|
508
|
+
|
|
509
|
+
assert self.card
|
|
510
|
+
roster.keep(self.card, self.record(), photo)
|
|
511
|
+
|
|
512
|
+
def record(self) -> dict:
|
|
513
|
+
"""How the card was made, kept next to it."""
|
|
514
|
+
fig = self.figure or Figure("")
|
|
515
|
+
source = {"query": fig.query, "title": fig.title, "page": fig.page, "photo": asdict(self.photo) if self.photo else None}
|
|
516
|
+
return {"figure": source, "answers": self.answers, "requests": self.requests, "module": self.m.id, "era": self.m.era}
|