meme-generator-next 0.2.0__tar.gz
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.
- meme_generator_next-0.2.0/LICENSE +21 -0
- meme_generator_next-0.2.0/PKG-INFO +169 -0
- meme_generator_next-0.2.0/README.md +129 -0
- meme_generator_next-0.2.0/meme_generator/__init__.py +59 -0
- meme_generator_next-0.2.0/meme_generator/app.py +322 -0
- meme_generator_next-0.2.0/meme_generator/cli.py +238 -0
- meme_generator_next-0.2.0/meme_generator/compat.py +96 -0
- meme_generator_next-0.2.0/meme_generator/config.py +98 -0
- meme_generator_next-0.2.0/meme_generator/dirs.py +251 -0
- meme_generator_next-0.2.0/meme_generator/download.py +116 -0
- meme_generator_next-0.2.0/meme_generator/exception.py +120 -0
- meme_generator_next-0.2.0/meme_generator/log.py +87 -0
- meme_generator_next-0.2.0/meme_generator/manager.py +111 -0
- meme_generator_next-0.2.0/meme_generator/meme.py +167 -0
- meme_generator_next-0.2.0/meme_generator/memes/5000choyen/__init__.py +202 -0
- meme_generator_next-0.2.0/meme_generator/memes/abstinence/__init__.py +93 -0
- meme_generator_next-0.2.0/meme_generator/memes/ace_attorney_dialog/__init__.py +122 -0
- meme_generator_next-0.2.0/meme_generator/memes/acg_entrance/__init__.py +47 -0
- meme_generator_next-0.2.0/meme_generator/memes/add_chaos/__init__.py +29 -0
- meme_generator_next-0.2.0/meme_generator/memes/addiction/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/alike/__init__.py +32 -0
- meme_generator_next-0.2.0/meme_generator/memes/alipay/__init__.py +89 -0
- meme_generator_next-0.2.0/meme_generator/memes/always/__init__.py +138 -0
- meme_generator_next-0.2.0/meme_generator/memes/always_like/__init__.py +96 -0
- meme_generator_next-0.2.0/meme_generator/memes/anti_kidnap/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/anya_suki/__init__.py +50 -0
- meme_generator_next-0.2.0/meme_generator/memes/applaud/__init__.py +39 -0
- meme_generator_next-0.2.0/meme_generator/memes/arona_throw/__init__.py +64 -0
- meme_generator_next-0.2.0/meme_generator/memes/ascension/__init__.py +37 -0
- meme_generator_next-0.2.0/meme_generator/memes/ask/__init__.py +94 -0
- meme_generator_next-0.2.0/meme_generator/memes/atri_pillow/__init__.py +75 -0
- meme_generator_next-0.2.0/meme_generator/memes/ayachi_holdsign/__init__.py +49 -0
- meme_generator_next-0.2.0/meme_generator/memes/ba_say/__init__.py +144 -0
- meme_generator_next-0.2.0/meme_generator/memes/back_to_work/__init__.py +28 -0
- meme_generator_next-0.2.0/meme_generator/memes/backflip/__init__.py +81 -0
- meme_generator_next-0.2.0/meme_generator/memes/bad_news/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/beat_head/__init__.py +52 -0
- meme_generator_next-0.2.0/meme_generator/memes/beat_up/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/bite/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/blamed_mahiro/__init__.py +78 -0
- meme_generator_next-0.2.0/meme_generator/memes/blood_pressure/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/bluearchive/__init__.py +67 -0
- meme_generator_next-0.2.0/meme_generator/memes/bocchi_draft/__init__.py +52 -0
- meme_generator_next-0.2.0/meme_generator/memes/bronya_holdsign/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/bubble_tea/__init__.py +76 -0
- meme_generator_next-0.2.0/meme_generator/memes/call_110/__init__.py +28 -0
- meme_generator_next-0.2.0/meme_generator/memes/capoo_draw/__init__.py +47 -0
- meme_generator_next-0.2.0/meme_generator/memes/capoo_point/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/capoo_rip/__init__.py +63 -0
- meme_generator_next-0.2.0/meme_generator/memes/capoo_rub/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/capoo_say/__init__.py +71 -0
- meme_generator_next-0.2.0/meme_generator/memes/capoo_stew/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/capoo_strike/__init__.py +48 -0
- meme_generator_next-0.2.0/meme_generator/memes/captain/__init__.py +39 -0
- meme_generator_next-0.2.0/meme_generator/memes/caused_by_this/__init__.py +66 -0
- meme_generator_next-0.2.0/meme_generator/memes/certificate/__init__.py +125 -0
- meme_generator_next-0.2.0/meme_generator/memes/charpic/__init__.py +43 -0
- meme_generator_next-0.2.0/meme_generator/memes/chase_train/__init__.py +66 -0
- meme_generator_next-0.2.0/meme_generator/memes/china_flag/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/chino_throw/__init__.py +60 -0
- meme_generator_next-0.2.0/meme_generator/memes/clauvio_twist/__init__.py +58 -0
- meme_generator_next-0.2.0/meme_generator/memes/clown/__init__.py +91 -0
- meme_generator_next-0.2.0/meme_generator/memes/clown_mask/__init__.py +86 -0
- meme_generator_next-0.2.0/meme_generator/memes/confuse/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/coupon/__init__.py +45 -0
- meme_generator_next-0.2.0/meme_generator/memes/cover_face/__init__.py +27 -0
- meme_generator_next-0.2.0/meme_generator/memes/crawl/__init__.py +63 -0
- meme_generator_next-0.2.0/meme_generator/memes/cyan/__init__.py +39 -0
- meme_generator_next-0.2.0/meme_generator/memes/daynight/__init__.py +28 -0
- meme_generator_next-0.2.0/meme_generator/memes/decent_kiss/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/dianzhongdian/__init__.py +62 -0
- meme_generator_next-0.2.0/meme_generator/memes/dinosaur/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/distracted/__init__.py +33 -0
- meme_generator_next-0.2.0/meme_generator/memes/divorce/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/dog_dislike/__init__.py +94 -0
- meme_generator_next-0.2.0/meme_generator/memes/dog_of_vtb/__init__.py +31 -0
- meme_generator_next-0.2.0/meme_generator/memes/dont_go_near/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/dont_press/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/dont_touch/__init__.py +66 -0
- meme_generator_next-0.2.0/meme_generator/memes/douyin/__init__.py +80 -0
- meme_generator_next-0.2.0/meme_generator/memes/eat/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/emperor_dragon/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/erised_mirror/__init__.py +39 -0
- meme_generator_next-0.2.0/meme_generator/memes/fade_away/__init__.py +134 -0
- meme_generator_next-0.2.0/meme_generator/memes/fanatic/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/father_work/__init__.py +49 -0
- meme_generator_next-0.2.0/meme_generator/memes/fencing/__init__.py +45 -0
- meme_generator_next-0.2.0/meme_generator/memes/fight_with_sunuo/__init__.py +32 -0
- meme_generator_next-0.2.0/meme_generator/memes/fill_head/__init__.py +48 -0
- meme_generator_next-0.2.0/meme_generator/memes/find_chips/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/firefly_holdsign/__init__.py +105 -0
- meme_generator_next-0.2.0/meme_generator/memes/flash_blind/__init__.py +65 -0
- meme_generator_next-0.2.0/meme_generator/memes/flick/__init__.py +109 -0
- meme_generator_next-0.2.0/meme_generator/memes/flush/__init__.py +55 -0
- meme_generator_next-0.2.0/meme_generator/memes/fogging/__init__.py +51 -0
- meme_generator_next-0.2.0/meme_generator/memes/follow/__init__.py +46 -0
- meme_generator_next-0.2.0/meme_generator/memes/frieren_take/__init__.py +50 -0
- meme_generator_next-0.2.0/meme_generator/memes/funny_mirror/__init__.py +31 -0
- meme_generator_next-0.2.0/meme_generator/memes/garbage/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/genshin_eat/__init__.py +99 -0
- meme_generator_next-0.2.0/meme_generator/memes/genshin_start/__init__.py +61 -0
- meme_generator_next-0.2.0/meme_generator/memes/gif_subtitle/__init__.py +197 -0
- meme_generator_next-0.2.0/meme_generator/memes/good_news/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/google/__init__.py +29 -0
- meme_generator_next-0.2.0/meme_generator/memes/google_captcha/__init__.py +78 -0
- meme_generator_next-0.2.0/meme_generator/memes/gorilla_throw/__init__.py +58 -0
- meme_generator_next-0.2.0/meme_generator/memes/guichu/__init__.py +129 -0
- meme_generator_next-0.2.0/meme_generator/memes/gun/__init__.py +76 -0
- meme_generator_next-0.2.0/meme_generator/memes/hammer/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/haruhi_raise/__init__.py +36 -0
- meme_generator_next-0.2.0/meme_generator/memes/high_EQ/__init__.py +56 -0
- meme_generator_next-0.2.0/meme_generator/memes/hit_screen/__init__.py +56 -0
- meme_generator_next-0.2.0/meme_generator/memes/hold_grudge/__init__.py +37 -0
- meme_generator_next-0.2.0/meme_generator/memes/hold_tight/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/hug/__init__.py +45 -0
- meme_generator_next-0.2.0/meme_generator/memes/hug_leg/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/hutao_bite/__init__.py +35 -0
- meme_generator_next-0.2.0/meme_generator/memes/imprison/__init__.py +37 -0
- meme_generator_next-0.2.0/meme_generator/memes/incivilization/__init__.py +47 -0
- meme_generator_next-0.2.0/meme_generator/memes/intel_inside/__init__.py +47 -0
- meme_generator_next-0.2.0/meme_generator/memes/interview/__init__.py +49 -0
- meme_generator_next-0.2.0/meme_generator/memes/jerry_stare/__init__.py +32 -0
- meme_generator_next-0.2.0/meme_generator/memes/jiji_king/__init__.py +117 -0
- meme_generator_next-0.2.0/meme_generator/memes/jinhsi/__init__.py +81 -0
- meme_generator_next-0.2.0/meme_generator/memes/jiubingfufa/__init__.py +53 -0
- meme_generator_next-0.2.0/meme_generator/memes/jiujiu/__init__.py +31 -0
- meme_generator_next-0.2.0/meme_generator/memes/jump/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/kaleidoscope/__init__.py +73 -0
- meme_generator_next-0.2.0/meme_generator/memes/karyl_point/__init__.py +28 -0
- meme_generator_next-0.2.0/meme_generator/memes/keep_away/__init__.py +51 -0
- meme_generator_next-0.2.0/meme_generator/memes/keep_your_money/__init__.py +70 -0
- meme_generator_next-0.2.0/meme_generator/memes/kick_ball/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/kirby_hammer/__init__.py +80 -0
- meme_generator_next-0.2.0/meme_generator/memes/kiss/__init__.py +43 -0
- meme_generator_next-0.2.0/meme_generator/memes/klee_eat/__init__.py +43 -0
- meme_generator_next-0.2.0/meme_generator/memes/knock/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/kokona_seal/__init__.py +81 -0
- meme_generator_next-0.2.0/meme_generator/memes/konata_watch/__init__.py +36 -0
- meme_generator_next-0.2.0/meme_generator/memes/learn/__init__.py +47 -0
- meme_generator_next-0.2.0/meme_generator/memes/left_right_jump/__init__.py +105 -0
- meme_generator_next-0.2.0/meme_generator/memes/let_me_in/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/lick_candy/__init__.py +131 -0
- meme_generator_next-0.2.0/meme_generator/memes/lim_x_0/__init__.py +43 -0
- meme_generator_next-0.2.0/meme_generator/memes/listen_music/__init__.py +34 -0
- meme_generator_next-0.2.0/meme_generator/memes/little_angel/__init__.py +57 -0
- meme_generator_next-0.2.0/meme_generator/memes/loading/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/look_flat/__init__.py +79 -0
- meme_generator_next-0.2.0/meme_generator/memes/look_this_icon/__init__.py +48 -0
- meme_generator_next-0.2.0/meme_generator/memes/loop/__init__.py +108 -0
- meme_generator_next-0.2.0/meme_generator/memes/lost_dog/__init__.py +43 -0
- meme_generator_next-0.2.0/meme_generator/memes/love_you/__init__.py +34 -0
- meme_generator_next-0.2.0/meme_generator/memes/luotianyi_need/__init__.py +34 -0
- meme_generator_next-0.2.0/meme_generator/memes/luotianyi_say/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/luoyonghao_say/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/luxun_say/__init__.py +39 -0
- meme_generator_next-0.2.0/meme_generator/memes/mahiro_readbook/__init__.py +45 -0
- meme_generator_next-0.2.0/meme_generator/memes/maimai_awaken/__init__.py +34 -0
- meme_generator_next-0.2.0/meme_generator/memes/maimai_join/__init__.py +32 -0
- meme_generator_next-0.2.0/meme_generator/memes/make_friend/__init__.py +52 -0
- meme_generator_next-0.2.0/meme_generator/memes/marriage/__init__.py +35 -0
- meme_generator_next-0.2.0/meme_generator/memes/meteor/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/mihoyo/__init__.py +33 -0
- meme_generator_next-0.2.0/meme_generator/memes/mourning/__init__.py +55 -0
- meme_generator_next-0.2.0/meme_generator/memes/murmur/__init__.py +36 -0
- meme_generator_next-0.2.0/meme_generator/memes/my_friend/__init__.py +96 -0
- meme_generator_next-0.2.0/meme_generator/memes/my_opinion/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/my_wife/__init__.py +102 -0
- meme_generator_next-0.2.0/meme_generator/memes/nahida_bite/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/name_generator/__init__.py +48 -0
- meme_generator_next-0.2.0/meme_generator/memes/need/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/nekoha_holdsign/__init__.py +42 -0
- meme_generator_next-0.2.0/meme_generator/memes/nijika_holdsign/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/no_response/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/nokia/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/not_call_me/__init__.py +37 -0
- meme_generator_next-0.2.0/meme_generator/memes/note_for_leave/__init__.py +107 -0
- meme_generator_next-0.2.0/meme_generator/memes/oshi_no_ko/__init__.py +75 -0
- meme_generator_next-0.2.0/meme_generator/memes/osu/__init__.py +42 -0
- meme_generator_next-0.2.0/meme_generator/memes/out/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/overtime/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/p5letter/__init__.py +158 -0
- meme_generator_next-0.2.0/meme_generator/memes/paint/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/painter/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/panda_dragon_figure/__init__.py +88 -0
- meme_generator_next-0.2.0/meme_generator/memes/pass_the_buck/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/pat/__init__.py +37 -0
- meme_generator_next-0.2.0/meme_generator/memes/pepe_raise/__init__.py +51 -0
- meme_generator_next-0.2.0/meme_generator/memes/perfect/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/petpet/__init__.py +67 -0
- meme_generator_next-0.2.0/meme_generator/memes/pinch/__init__.py +33 -0
- meme_generator_next-0.2.0/meme_generator/memes/pixelate/__init__.py +59 -0
- meme_generator_next-0.2.0/meme_generator/memes/pjsk/__init__.py +154 -0
- meme_generator_next-0.2.0/meme_generator/memes/plana_eat/__init__.py +32 -0
- meme_generator_next-0.2.0/meme_generator/memes/play/__init__.py +54 -0
- meme_generator_next-0.2.0/meme_generator/memes/play_baseball/__init__.py +53 -0
- meme_generator_next-0.2.0/meme_generator/memes/play_basketball/__init__.py +84 -0
- meme_generator_next-0.2.0/meme_generator/memes/play_game/__init__.py +54 -0
- meme_generator_next-0.2.0/meme_generator/memes/play_together/__init__.py +61 -0
- meme_generator_next-0.2.0/meme_generator/memes/police/__init__.py +68 -0
- meme_generator_next-0.2.0/meme_generator/memes/pornhub/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/potato/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/pound/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/printing/__init__.py +39 -0
- meme_generator_next-0.2.0/meme_generator/memes/prpr/__init__.py +36 -0
- meme_generator_next-0.2.0/meme_generator/memes/psyduck/__init__.py +78 -0
- meme_generator_next-0.2.0/meme_generator/memes/punch/__init__.py +39 -0
- meme_generator_next-0.2.0/meme_generator/memes/pyramid/__init__.py +99 -0
- meme_generator_next-0.2.0/meme_generator/memes/raise_image/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/raise_sign/__init__.py +43 -0
- meme_generator_next-0.2.0/meme_generator/memes/read_book/__init__.py +70 -0
- meme_generator_next-0.2.0/meme_generator/memes/remote_control/__init__.py +52 -0
- meme_generator_next-0.2.0/meme_generator/memes/repeat/__init__.py +70 -0
- meme_generator_next-0.2.0/meme_generator/memes/rip/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/rip_angrily/__init__.py +27 -0
- meme_generator_next-0.2.0/meme_generator/memes/rip_clothes/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/rise_dead/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/roll/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/rotate_3d/__init__.py +79 -0
- meme_generator_next-0.2.0/meme_generator/memes/rub/__init__.py +47 -0
- meme_generator_next-0.2.0/meme_generator/memes/run/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/run_away/__init__.py +47 -0
- meme_generator_next-0.2.0/meme_generator/memes/safe_sense/__init__.py +48 -0
- meme_generator_next-0.2.0/meme_generator/memes/saimin_app/__init__.py +45 -0
- meme_generator_next-0.2.0/meme_generator/memes/scratch_head/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/scratchcard/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/scroll/__init__.py +61 -0
- meme_generator_next-0.2.0/meme_generator/memes/seal/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/sekaiichi_kawaii/__init__.py +45 -0
- meme_generator_next-0.2.0/meme_generator/memes/shake_head/__init__.py +56 -0
- meme_generator_next-0.2.0/meme_generator/memes/shiroko_pero/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/shock/__init__.py +31 -0
- meme_generator_next-0.2.0/meme_generator/memes/shutup/__init__.py +37 -0
- meme_generator_next-0.2.0/meme_generator/memes/sit_still/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/slap/__init__.py +36 -0
- meme_generator_next-0.2.0/meme_generator/memes/slogan/__init__.py +49 -0
- meme_generator_next-0.2.0/meme_generator/memes/smash/__init__.py +36 -0
- meme_generator_next-0.2.0/meme_generator/memes/sold_out/__init__.py +40 -0
- meme_generator_next-0.2.0/meme_generator/memes/speechless/__init__.py +52 -0
- meme_generator_next-0.2.0/meme_generator/memes/sphere_rotate/__init__.py +166 -0
- meme_generator_next-0.2.0/meme_generator/memes/spider/__init__.py +50 -0
- meme_generator_next-0.2.0/meme_generator/memes/stare_at_you/__init__.py +49 -0
- meme_generator_next-0.2.0/meme_generator/memes/steam_message/__init__.py +101 -0
- meme_generator_next-0.2.0/meme_generator/memes/step_on/__init__.py +39 -0
- meme_generator_next-0.2.0/meme_generator/memes/stew/__init__.py +46 -0
- meme_generator_next-0.2.0/meme_generator/memes/stickman_dancing/__init__.py +62 -0
- meme_generator_next-0.2.0/meme_generator/memes/subject3/__init__.py +53 -0
- meme_generator_next-0.2.0/meme_generator/memes/suck/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/support/__init__.py +26 -0
- meme_generator_next-0.2.0/meme_generator/memes/swirl_turn/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/symmetric/__init__.py +134 -0
- meme_generator_next-0.2.0/meme_generator/memes/tankuku_raisesign/__init__.py +51 -0
- meme_generator_next-0.2.0/meme_generator/memes/taunt/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/teach/__init__.py +50 -0
- meme_generator_next-0.2.0/meme_generator/memes/tease/__init__.py +61 -0
- meme_generator_next-0.2.0/meme_generator/memes/telescope/__init__.py +52 -0
- meme_generator_next-0.2.0/meme_generator/memes/thermometer_gun/__init__.py +61 -0
- meme_generator_next-0.2.0/meme_generator/memes/think_what/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/this_chicken/__init__.py +49 -0
- meme_generator_next-0.2.0/meme_generator/memes/throw/__init__.py +35 -0
- meme_generator_next-0.2.0/meme_generator/memes/throw_gif/__init__.py +42 -0
- meme_generator_next-0.2.0/meme_generator/memes/thump/__init__.py +35 -0
- meme_generator_next-0.2.0/meme_generator/memes/thump_wildly/__init__.py +34 -0
- meme_generator_next-0.2.0/meme_generator/memes/tightly/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/time_to_go/__init__.py +43 -0
- meme_generator_next-0.2.0/meme_generator/memes/together/__init__.py +49 -0
- meme_generator_next-0.2.0/meme_generator/memes/tom_tease/__init__.py +39 -0
- meme_generator_next-0.2.0/meme_generator/memes/tomb_yeah/__init__.py +33 -0
- meme_generator_next-0.2.0/meme_generator/memes/top_notch/__init__.py +37 -0
- meme_generator_next-0.2.0/meme_generator/memes/trance/__init__.py +31 -0
- meme_generator_next-0.2.0/meme_generator/memes/trolley/__init__.py +59 -0
- meme_generator_next-0.2.0/meme_generator/memes/turn/__init__.py +31 -0
- meme_generator_next-0.2.0/meme_generator/memes/twist/__init__.py +38 -0
- meme_generator_next-0.2.0/meme_generator/memes/universal/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/upside_down/__init__.py +48 -0
- meme_generator_next-0.2.0/meme_generator/memes/vibrate/__init__.py +44 -0
- meme_generator_next-0.2.0/meme_generator/memes/wakeup/__init__.py +41 -0
- meme_generator_next-0.2.0/meme_generator/memes/wallpaper/__init__.py +35 -0
- meme_generator_next-0.2.0/meme_generator/memes/walnut_pad/__init__.py +32 -0
- meme_generator_next-0.2.0/meme_generator/memes/walnut_zoom/__init__.py +46 -0
- meme_generator_next-0.2.0/meme_generator/memes/washer/__init__.py +34 -0
- meme_generator_next-0.2.0/meme_generator/memes/wave/__init__.py +54 -0
- meme_generator_next-0.2.0/meme_generator/memes/wechat_pay/__init__.py +92 -0
- meme_generator_next-0.2.0/meme_generator/memes/what_I_want_to_do/__init__.py +30 -0
- meme_generator_next-0.2.0/meme_generator/memes/what_he_wants/__init__.py +52 -0
- meme_generator_next-0.2.0/meme_generator/memes/why_at_me/__init__.py +28 -0
- meme_generator_next-0.2.0/meme_generator/memes/why_have_hands/__init__.py +79 -0
- meme_generator_next-0.2.0/meme_generator/memes/windmill_turn/__init__.py +35 -0
- meme_generator_next-0.2.0/meme_generator/memes/wish_fail/__init__.py +37 -0
- meme_generator_next-0.2.0/meme_generator/memes/wooden_fish/__init__.py +29 -0
- meme_generator_next-0.2.0/meme_generator/memes/worship/__init__.py +33 -0
- meme_generator_next-0.2.0/meme_generator/memes/wujing/__init__.py +65 -0
- meme_generator_next-0.2.0/meme_generator/memes/yeshu/__init__.py +227 -0
- meme_generator_next-0.2.0/meme_generator/memes/you_dont_get/__init__.py +28 -0
- meme_generator_next-0.2.0/meme_generator/memes/you_should_call/__init__.py +54 -0
- meme_generator_next-0.2.0/meme_generator/memes/your_new_years_eve/__init__.py +34 -0
- meme_generator_next-0.2.0/meme_generator/memes/youtube/__init__.py +65 -0
- meme_generator_next-0.2.0/meme_generator/redis_cache.py +37 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f602.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f605.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f60f.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f62d.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f642.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f913.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f917.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f924.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f970.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/emojis/emoji_u1f975.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/icons/hot.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/icons/image.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/icons/image_disabled.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/icons/new.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/icons/text.png +0 -0
- meme_generator_next-0.2.0/meme_generator/resources/images/icons/text_disabled.png +0 -0
- meme_generator_next-0.2.0/meme_generator/storage.py +110 -0
- meme_generator_next-0.2.0/meme_generator/tags.py +146 -0
- meme_generator_next-0.2.0/meme_generator/utils.py +517 -0
- meme_generator_next-0.2.0/meme_generator/version.py +1 -0
- meme_generator_next-0.2.0/meme_generator_cli.py +36 -0
- meme_generator_next-0.2.0/pyproject.toml +64 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 chiskat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: meme-generator-next
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: The next-generation Python version of meme-generator
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: chiskat
|
|
8
|
+
Author-email: 1@paperplane.cc
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: Pillow (>=12.3.0,<13.0.0)
|
|
18
|
+
Requires-Dist: arclet-alconna (>=1.8.23,<2.0.0,!=1.8.27)
|
|
19
|
+
Requires-Dist: arclet-alconna-tools (>=0.7.9,<0.8.0)
|
|
20
|
+
Requires-Dist: boto3 (>=1.35.0,<2.0.0)
|
|
21
|
+
Requires-Dist: dateparser (>=1.2.0,<2.0.0)
|
|
22
|
+
Requires-Dist: fastapi (>=0.93.0,<1.0.0)
|
|
23
|
+
Requires-Dist: filetype (>=1.2.0,<2.0.0)
|
|
24
|
+
Requires-Dist: httpx (>=0.20.0,<1.0.0)
|
|
25
|
+
Requires-Dist: loguru (>=0.6.0,<1.0.0)
|
|
26
|
+
Requires-Dist: pil-utils-fix-deps (>=0.2.0,<0.3.0)
|
|
27
|
+
Requires-Dist: pycairo (>=1.26.0)
|
|
28
|
+
Requires-Dist: python-multipart (>=0.0.9,<0.1.0,!=0.0.13,!=0.0.14)
|
|
29
|
+
Requires-Dist: qrcode (>=8.0,<9.0)
|
|
30
|
+
Requires-Dist: redis (>=5.0.0,<6.0.0)
|
|
31
|
+
Requires-Dist: rich (>=13.0.0,<14.0.0)
|
|
32
|
+
Requires-Dist: skia-python (>=138.0)
|
|
33
|
+
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
34
|
+
Requires-Dist: typing-extensions (>=4.4.0,<5.0.0)
|
|
35
|
+
Requires-Dist: uvicorn (>=0.20.0,<1.0.0)
|
|
36
|
+
Project-URL: Homepage, https://github.com/chiskat/meme-generator-next
|
|
37
|
+
Project-URL: Repository, https://github.com/chiskat/meme-generator-next
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# meme-generator-next
|
|
41
|
+
|
|
42
|
+
本项目 fork 自 [MemeCrafters/meme-generator](https://github.com/MemeCrafters/meme-generator)。
|
|
43
|
+
|
|
44
|
+
特性升级:
|
|
45
|
+
|
|
46
|
+
- 处理了 `Pillow` 和 `pil-utils` 的依赖版本,以避免和 Astrbot 新版本不兼容;
|
|
47
|
+
- 依赖项包含 `pycairo`,因此在加载 [meme_emoji](https://github.com/anyliew/meme_emoji) 时不再提示缺少依赖;
|
|
48
|
+
- 支持自定义 `config.toml` 文件路径,详见 [配置文件路径](#配置文件路径);
|
|
49
|
+
- 支持自动上传 S3 并返回 URL;
|
|
50
|
+
- 支持通过 Redis 连接,缓存预览图。
|
|
51
|
+
|
|
52
|
+
# 特性升级介绍
|
|
53
|
+
|
|
54
|
+
## 配置文件路径
|
|
55
|
+
|
|
56
|
+
作为包导入或 Docker 部署时,可通过 `MEME_GENERATOR_CONFIG_FILE` 指定配置文件路径,文件名不必为 `config.toml`:
|
|
57
|
+
|
|
58
|
+
```yaml
|
|
59
|
+
services:
|
|
60
|
+
meme-generator:
|
|
61
|
+
volumes:
|
|
62
|
+
- ./config:/config
|
|
63
|
+
environment:
|
|
64
|
+
MEME_GENERATOR_CONFIG_FILE: /config/config.toml
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
命令行启动时,可使用 `--config-file` 参数指定配置文件路径:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# CLI 用法
|
|
71
|
+
meme --config-file /path/to/config.toml run
|
|
72
|
+
|
|
73
|
+
# Web Server
|
|
74
|
+
python -m meme_generator.app --config-file=/path/to/config.toml
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
兼容说明:旧版本的 `MEME_GENERATOR_CONFIG_DIR` 环境变量(目录形式)仍然有效,但已不推荐使用;`--config-dir` 参数已被 `--config-file` 取代。
|
|
78
|
+
|
|
79
|
+
## S3 存储
|
|
80
|
+
|
|
81
|
+
在 `config.toml` 中启用 S3 后,HTTP Server 会为每个表情新增一个 `/memes/{key}/url/` 端点。该端点的请求参数与原有的 `/memes/{key}/` 端点完全相同,生成成功后会自动上传到 S3,并返回 JSON:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"url": "https://cdn.example.com/meme-generator/artifact/petpet/xxx.gif",
|
|
86
|
+
"meme_key": "petpet",
|
|
87
|
+
"object_key": "meme-generator/artifact/petpet/xxx.gif"
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
生成结果的对象名格式为 `{result_prefix}/{meme_key}/{random_key}.{extension}`,预览图为 `{preview_prefix}/{meme_key}/{random_key}.{extension}`。其中 `random_key` 为 32 位 UUID 随机十六进制字符串,不包含日期。
|
|
92
|
+
|
|
93
|
+
生成结果和预览图分别使用 `result_prefix` 与 `preview_prefix`,没有通用对象前缀配置。
|
|
94
|
+
|
|
95
|
+
配置示例:
|
|
96
|
+
|
|
97
|
+
```toml
|
|
98
|
+
[storage.s3]
|
|
99
|
+
enabled = true
|
|
100
|
+
# 自定义 S3 兼容服务地址;AWS 官方 S3 可以留空
|
|
101
|
+
endpoint_url = "https://s3.example.com"
|
|
102
|
+
region = "ap-northeast-1"
|
|
103
|
+
bucket = "meme-generator"
|
|
104
|
+
access_key_id = "your-access-key-id"
|
|
105
|
+
secret_access_key = "your-secret-access-key"
|
|
106
|
+
# 生成结果前缀
|
|
107
|
+
result_prefix = "meme-generator/artifact"
|
|
108
|
+
# 预览图前缀
|
|
109
|
+
preview_prefix = "meme-generator/preview"
|
|
110
|
+
# 推荐配置为 bucket 的公开访问地址;留空时会根据 endpoint/region 生成 S3 URL
|
|
111
|
+
public_base_url = "https://cdn.example.com"
|
|
112
|
+
# 大多数 S3 兼容服务(MinIO、Cloudflare R2 等)需要 path_style = true
|
|
113
|
+
path_style = true
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
返回的 `url` 需要能被访问;如果使用私有 bucket,请将 `public_base_url` 指向具有读取权限的 CDN/反向代理,或自行生成预签名 URL。
|
|
117
|
+
|
|
118
|
+
Docker 部署时,也可以使用环境变量来配置:
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
services:
|
|
122
|
+
meme-generator:
|
|
123
|
+
environment:
|
|
124
|
+
S3_ENABLED: 'true'
|
|
125
|
+
S3_ENDPOINT_URL: 'https://s3.example.com'
|
|
126
|
+
S3_REGION: 'ap-northeast-1'
|
|
127
|
+
S3_BUCKET: 'meme-generator'
|
|
128
|
+
S3_ACCESS_KEY_ID: 'your-access-key-id'
|
|
129
|
+
S3_SECRET_ACCESS_KEY: 'your-secret-access-key'
|
|
130
|
+
S3_SESSION_TOKEN: ''
|
|
131
|
+
S3_RESULT_PREFIX: 'meme-generator/artifact'
|
|
132
|
+
S3_PREVIEW_PREFIX: 'meme-generator/preview'
|
|
133
|
+
S3_PUBLIC_BASE_URL: 'https://cdn.example.com'
|
|
134
|
+
S3_PATH_STYLE: 'true'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Redis 预览缓存
|
|
138
|
+
|
|
139
|
+
启用 S3 和 Redis 后,每个表情都会新增 `GET /memes/{key}/preview/url/` 端点。第一次访问时会生成预览图并上传到 S3,然后将返回的公开 URL 写入 Redis;后续访问会直接返回缓存中的 URL:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"url": "https://cdn.example.com/meme-generator/preview/petpet/xxx.gif",
|
|
144
|
+
"meme_key": "petpet"
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Redis 配置示例:
|
|
149
|
+
|
|
150
|
+
```toml
|
|
151
|
+
[storage.redis]
|
|
152
|
+
enabled = true
|
|
153
|
+
url = "redis://localhost:6379/0"
|
|
154
|
+
key_prefix = "meme-generator:preview"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`key_prefix` 用于区分不同部署的缓存键。缓存写入时不设置过期时间;如果 Redis 配置了 `maxmemory-policy`,请避免选择会主动删除键的淘汰策略,或使用专门的 Redis 实例。
|
|
158
|
+
|
|
159
|
+
Docker 部署时,也可以使用环境变量来配置:
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
services:
|
|
163
|
+
meme-generator:
|
|
164
|
+
environment:
|
|
165
|
+
REDIS_ENABLED: 'true'
|
|
166
|
+
REDIS_URL: 'redis://localhost:6379/0'
|
|
167
|
+
REDIS_KEY_PREFIX: 'meme-generator:preview'
|
|
168
|
+
```
|
|
169
|
+
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# meme-generator-next
|
|
2
|
+
|
|
3
|
+
本项目 fork 自 [MemeCrafters/meme-generator](https://github.com/MemeCrafters/meme-generator)。
|
|
4
|
+
|
|
5
|
+
特性升级:
|
|
6
|
+
|
|
7
|
+
- 处理了 `Pillow` 和 `pil-utils` 的依赖版本,以避免和 Astrbot 新版本不兼容;
|
|
8
|
+
- 依赖项包含 `pycairo`,因此在加载 [meme_emoji](https://github.com/anyliew/meme_emoji) 时不再提示缺少依赖;
|
|
9
|
+
- 支持自定义 `config.toml` 文件路径,详见 [配置文件路径](#配置文件路径);
|
|
10
|
+
- 支持自动上传 S3 并返回 URL;
|
|
11
|
+
- 支持通过 Redis 连接,缓存预览图。
|
|
12
|
+
|
|
13
|
+
# 特性升级介绍
|
|
14
|
+
|
|
15
|
+
## 配置文件路径
|
|
16
|
+
|
|
17
|
+
作为包导入或 Docker 部署时,可通过 `MEME_GENERATOR_CONFIG_FILE` 指定配置文件路径,文件名不必为 `config.toml`:
|
|
18
|
+
|
|
19
|
+
```yaml
|
|
20
|
+
services:
|
|
21
|
+
meme-generator:
|
|
22
|
+
volumes:
|
|
23
|
+
- ./config:/config
|
|
24
|
+
environment:
|
|
25
|
+
MEME_GENERATOR_CONFIG_FILE: /config/config.toml
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
命令行启动时,可使用 `--config-file` 参数指定配置文件路径:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
# CLI 用法
|
|
32
|
+
meme --config-file /path/to/config.toml run
|
|
33
|
+
|
|
34
|
+
# Web Server
|
|
35
|
+
python -m meme_generator.app --config-file=/path/to/config.toml
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
兼容说明:旧版本的 `MEME_GENERATOR_CONFIG_DIR` 环境变量(目录形式)仍然有效,但已不推荐使用;`--config-dir` 参数已被 `--config-file` 取代。
|
|
39
|
+
|
|
40
|
+
## S3 存储
|
|
41
|
+
|
|
42
|
+
在 `config.toml` 中启用 S3 后,HTTP Server 会为每个表情新增一个 `/memes/{key}/url/` 端点。该端点的请求参数与原有的 `/memes/{key}/` 端点完全相同,生成成功后会自动上传到 S3,并返回 JSON:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"url": "https://cdn.example.com/meme-generator/artifact/petpet/xxx.gif",
|
|
47
|
+
"meme_key": "petpet",
|
|
48
|
+
"object_key": "meme-generator/artifact/petpet/xxx.gif"
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
生成结果的对象名格式为 `{result_prefix}/{meme_key}/{random_key}.{extension}`,预览图为 `{preview_prefix}/{meme_key}/{random_key}.{extension}`。其中 `random_key` 为 32 位 UUID 随机十六进制字符串,不包含日期。
|
|
53
|
+
|
|
54
|
+
生成结果和预览图分别使用 `result_prefix` 与 `preview_prefix`,没有通用对象前缀配置。
|
|
55
|
+
|
|
56
|
+
配置示例:
|
|
57
|
+
|
|
58
|
+
```toml
|
|
59
|
+
[storage.s3]
|
|
60
|
+
enabled = true
|
|
61
|
+
# 自定义 S3 兼容服务地址;AWS 官方 S3 可以留空
|
|
62
|
+
endpoint_url = "https://s3.example.com"
|
|
63
|
+
region = "ap-northeast-1"
|
|
64
|
+
bucket = "meme-generator"
|
|
65
|
+
access_key_id = "your-access-key-id"
|
|
66
|
+
secret_access_key = "your-secret-access-key"
|
|
67
|
+
# 生成结果前缀
|
|
68
|
+
result_prefix = "meme-generator/artifact"
|
|
69
|
+
# 预览图前缀
|
|
70
|
+
preview_prefix = "meme-generator/preview"
|
|
71
|
+
# 推荐配置为 bucket 的公开访问地址;留空时会根据 endpoint/region 生成 S3 URL
|
|
72
|
+
public_base_url = "https://cdn.example.com"
|
|
73
|
+
# 大多数 S3 兼容服务(MinIO、Cloudflare R2 等)需要 path_style = true
|
|
74
|
+
path_style = true
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
返回的 `url` 需要能被访问;如果使用私有 bucket,请将 `public_base_url` 指向具有读取权限的 CDN/反向代理,或自行生成预签名 URL。
|
|
78
|
+
|
|
79
|
+
Docker 部署时,也可以使用环境变量来配置:
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
services:
|
|
83
|
+
meme-generator:
|
|
84
|
+
environment:
|
|
85
|
+
S3_ENABLED: 'true'
|
|
86
|
+
S3_ENDPOINT_URL: 'https://s3.example.com'
|
|
87
|
+
S3_REGION: 'ap-northeast-1'
|
|
88
|
+
S3_BUCKET: 'meme-generator'
|
|
89
|
+
S3_ACCESS_KEY_ID: 'your-access-key-id'
|
|
90
|
+
S3_SECRET_ACCESS_KEY: 'your-secret-access-key'
|
|
91
|
+
S3_SESSION_TOKEN: ''
|
|
92
|
+
S3_RESULT_PREFIX: 'meme-generator/artifact'
|
|
93
|
+
S3_PREVIEW_PREFIX: 'meme-generator/preview'
|
|
94
|
+
S3_PUBLIC_BASE_URL: 'https://cdn.example.com'
|
|
95
|
+
S3_PATH_STYLE: 'true'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Redis 预览缓存
|
|
99
|
+
|
|
100
|
+
启用 S3 和 Redis 后,每个表情都会新增 `GET /memes/{key}/preview/url/` 端点。第一次访问时会生成预览图并上传到 S3,然后将返回的公开 URL 写入 Redis;后续访问会直接返回缓存中的 URL:
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"url": "https://cdn.example.com/meme-generator/preview/petpet/xxx.gif",
|
|
105
|
+
"meme_key": "petpet"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Redis 配置示例:
|
|
110
|
+
|
|
111
|
+
```toml
|
|
112
|
+
[storage.redis]
|
|
113
|
+
enabled = true
|
|
114
|
+
url = "redis://localhost:6379/0"
|
|
115
|
+
key_prefix = "meme-generator:preview"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`key_prefix` 用于区分不同部署的缓存键。缓存写入时不设置过期时间;如果 Redis 配置了 `maxmemory-policy`,请避免选择会主动删除键的淘汰策略,或使用专门的 Redis 实例。
|
|
119
|
+
|
|
120
|
+
Docker 部署时,也可以使用环境变量来配置:
|
|
121
|
+
|
|
122
|
+
```yaml
|
|
123
|
+
services:
|
|
124
|
+
meme-generator:
|
|
125
|
+
environment:
|
|
126
|
+
REDIS_ENABLED: 'true'
|
|
127
|
+
REDIS_URL: 'redis://localhost:6379/0'
|
|
128
|
+
REDIS_KEY_PREFIX: 'meme-generator:preview'
|
|
129
|
+
```
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
CONFIG_FILE_ENV = "MEME_GENERATOR_CONFIG_FILE"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _is_module_invocation(module_name: str) -> bool:
|
|
10
|
+
orig_argv = getattr(sys, "orig_argv", [])
|
|
11
|
+
try:
|
|
12
|
+
option_index = orig_argv.index("-m")
|
|
13
|
+
except ValueError:
|
|
14
|
+
return False
|
|
15
|
+
return (
|
|
16
|
+
option_index + 1 < len(orig_argv) and orig_argv[option_index + 1] == module_name
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _apply_config_file_option() -> None:
|
|
21
|
+
if not _is_module_invocation("meme_generator.app"):
|
|
22
|
+
return
|
|
23
|
+
|
|
24
|
+
parser = argparse.ArgumentParser(add_help=False, allow_abbrev=False)
|
|
25
|
+
parser.add_argument("--config-file", dest="config_file")
|
|
26
|
+
args, remaining = parser.parse_known_args(sys.argv[1:])
|
|
27
|
+
|
|
28
|
+
if args.config_file is None:
|
|
29
|
+
return
|
|
30
|
+
if not args.config_file:
|
|
31
|
+
parser.error("--config-file must not be empty")
|
|
32
|
+
|
|
33
|
+
os.environ[CONFIG_FILE_ENV] = str(Path(args.config_file).expanduser())
|
|
34
|
+
sys.argv[:] = [sys.argv[0], *remaining]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_apply_config_file_option()
|
|
38
|
+
|
|
39
|
+
from meme_generator.config import meme_config as config
|
|
40
|
+
from meme_generator.manager import add_meme as add_meme
|
|
41
|
+
from meme_generator.manager import get_meme as get_meme
|
|
42
|
+
from meme_generator.manager import get_meme_keys as get_meme_keys
|
|
43
|
+
from meme_generator.manager import get_memes as get_memes
|
|
44
|
+
from meme_generator.manager import load_meme as load_meme
|
|
45
|
+
from meme_generator.manager import load_memes as load_memes
|
|
46
|
+
from meme_generator.meme import CommandShortcut as CommandShortcut
|
|
47
|
+
from meme_generator.meme import Meme as Meme
|
|
48
|
+
from meme_generator.meme import MemeArgsModel as MemeArgsModel
|
|
49
|
+
from meme_generator.meme import MemeArgsType as MemeArgsType
|
|
50
|
+
from meme_generator.meme import MemeParamsType as MemeParamsType
|
|
51
|
+
from meme_generator.meme import ParserArg as ParserArg
|
|
52
|
+
from meme_generator.meme import ParserOption as ParserOption
|
|
53
|
+
from meme_generator.version import __version__ as __version__
|
|
54
|
+
|
|
55
|
+
if config.meme.load_builtin_memes:
|
|
56
|
+
for path in (Path(__file__).parent / "memes").iterdir():
|
|
57
|
+
load_meme(f"meme_generator.memes.{path.name}")
|
|
58
|
+
for meme_dir in config.meme.meme_dirs:
|
|
59
|
+
load_memes(meme_dir)
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from typing import Any, Literal, Optional
|
|
4
|
+
|
|
5
|
+
import filetype
|
|
6
|
+
from fastapi import Depends, FastAPI, Form, HTTPException, Response, UploadFile
|
|
7
|
+
from pydantic import BaseModel, ValidationError
|
|
8
|
+
from redis.exceptions import RedisError
|
|
9
|
+
|
|
10
|
+
from meme_generator.compat import model_dump, model_json_schema, type_validate_python
|
|
11
|
+
from meme_generator.config import meme_config
|
|
12
|
+
from meme_generator.exception import (
|
|
13
|
+
ArgModelMismatch,
|
|
14
|
+
MemeGeneratorException,
|
|
15
|
+
NoSuchMeme,
|
|
16
|
+
RedisCacheError,
|
|
17
|
+
S3StorageError,
|
|
18
|
+
)
|
|
19
|
+
from meme_generator.log import LOGGING_CONFIG, setup_logger
|
|
20
|
+
from meme_generator.manager import get_meme, get_meme_keys, get_memes
|
|
21
|
+
from meme_generator.meme import CommandShortcut, Meme, MemeArgsModel, ParserOption
|
|
22
|
+
from meme_generator.redis_cache import (
|
|
23
|
+
cache_preview_url,
|
|
24
|
+
create_redis_client,
|
|
25
|
+
get_cached_preview_url,
|
|
26
|
+
)
|
|
27
|
+
from meme_generator.storage import upload_to_s3
|
|
28
|
+
from meme_generator.utils import MemeProperties, render_meme_list, run_sync
|
|
29
|
+
from meme_generator.version import __version__
|
|
30
|
+
|
|
31
|
+
app = FastAPI()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MemeArgsResponse(BaseModel):
|
|
35
|
+
args_model: dict[str, Any]
|
|
36
|
+
args_examples: list[dict[str, Any]]
|
|
37
|
+
parser_options: list[ParserOption]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class MemeParamsResponse(BaseModel):
|
|
41
|
+
min_images: int
|
|
42
|
+
max_images: int
|
|
43
|
+
min_texts: int
|
|
44
|
+
max_texts: int
|
|
45
|
+
default_texts: list[str]
|
|
46
|
+
args_type: Optional[MemeArgsResponse] = None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MemeUrlResponse(BaseModel):
|
|
50
|
+
url: str
|
|
51
|
+
meme_key: str
|
|
52
|
+
object_key: str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MemePreviewUrlResponse(BaseModel):
|
|
56
|
+
url: str
|
|
57
|
+
meme_key: str
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class MemeInfoResponse(BaseModel):
|
|
61
|
+
key: str
|
|
62
|
+
params_type: MemeParamsResponse
|
|
63
|
+
keywords: list[str]
|
|
64
|
+
shortcuts: list[CommandShortcut]
|
|
65
|
+
tags: set[str]
|
|
66
|
+
date_created: datetime
|
|
67
|
+
date_modified: datetime
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
async def generate_meme(
|
|
71
|
+
meme: Meme, images: list[bytes], texts: list[str], args: Any
|
|
72
|
+
) -> tuple[bytes, str]:
|
|
73
|
+
try:
|
|
74
|
+
result = await run_sync(meme)(images=images, texts=texts, args=model_dump(args))
|
|
75
|
+
except MemeGeneratorException as e:
|
|
76
|
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
|
77
|
+
|
|
78
|
+
content = result.getvalue()
|
|
79
|
+
media_type = str(filetype.guess_mime(content)) or "text/plain"
|
|
80
|
+
return content, media_type
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def register_router(meme: Meme):
|
|
84
|
+
if args_type := meme.params_type.args_type:
|
|
85
|
+
args_model = args_type.args_model
|
|
86
|
+
else:
|
|
87
|
+
args_model = MemeArgsModel
|
|
88
|
+
|
|
89
|
+
def args_checker(
|
|
90
|
+
args: Optional[str] = Form(default=json.dumps(model_dump(args_model()))),
|
|
91
|
+
):
|
|
92
|
+
if not args:
|
|
93
|
+
return MemeArgsModel()
|
|
94
|
+
try:
|
|
95
|
+
model = type_validate_python(args_model, json.loads(args))
|
|
96
|
+
except ValidationError as e:
|
|
97
|
+
e = ArgModelMismatch(str(e))
|
|
98
|
+
raise HTTPException(status_code=552, detail=e.message)
|
|
99
|
+
return model
|
|
100
|
+
|
|
101
|
+
@app.post(f"/memes/{meme.key}/")
|
|
102
|
+
async def _(
|
|
103
|
+
images: list[UploadFile] = [],
|
|
104
|
+
texts: list[str] = meme.params_type.default_texts,
|
|
105
|
+
args: args_model = Depends(args_checker), # type: ignore
|
|
106
|
+
):
|
|
107
|
+
imgs: list[bytes] = []
|
|
108
|
+
for image in images:
|
|
109
|
+
imgs.append(await image.read())
|
|
110
|
+
|
|
111
|
+
texts = [text for text in texts if text]
|
|
112
|
+
|
|
113
|
+
assert isinstance(args, args_model)
|
|
114
|
+
|
|
115
|
+
content, media_type = await generate_meme(meme, imgs, texts, args)
|
|
116
|
+
|
|
117
|
+
return Response(content=content, media_type=media_type)
|
|
118
|
+
|
|
119
|
+
@app.post(f"/memes/{meme.key}/url/")
|
|
120
|
+
async def _(
|
|
121
|
+
images: list[UploadFile] = [],
|
|
122
|
+
texts: list[str] = meme.params_type.default_texts,
|
|
123
|
+
args: args_model = Depends(args_checker), # type: ignore
|
|
124
|
+
):
|
|
125
|
+
imgs: list[bytes] = []
|
|
126
|
+
for image in images:
|
|
127
|
+
imgs.append(await image.read())
|
|
128
|
+
|
|
129
|
+
texts = [text for text in texts if text]
|
|
130
|
+
|
|
131
|
+
assert isinstance(args, args_model)
|
|
132
|
+
|
|
133
|
+
content, media_type = await generate_meme(meme, imgs, texts, args)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
upload_result = await run_sync(upload_to_s3)(
|
|
137
|
+
content=content,
|
|
138
|
+
media_type=media_type,
|
|
139
|
+
meme_key=meme.key,
|
|
140
|
+
config=meme_config.storage.s3,
|
|
141
|
+
upload_kind="result",
|
|
142
|
+
)
|
|
143
|
+
except S3StorageError as e:
|
|
144
|
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
|
145
|
+
|
|
146
|
+
return MemeUrlResponse(
|
|
147
|
+
url=upload_result.url,
|
|
148
|
+
meme_key=meme.key,
|
|
149
|
+
object_key=upload_result.object_key,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@app.get(f"/memes/{meme.key}/preview/url/")
|
|
153
|
+
async def _():
|
|
154
|
+
redis_config = meme_config.storage.redis
|
|
155
|
+
if not redis_config.enabled:
|
|
156
|
+
error = RedisCacheError(
|
|
157
|
+
"Redis 缓存未启用,请在 config.toml 中配置 storage.redis"
|
|
158
|
+
)
|
|
159
|
+
raise HTTPException(status_code=error.status_code, detail=error.message)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
redis_client = create_redis_client(redis_config)
|
|
163
|
+
except RedisCacheError as e:
|
|
164
|
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
cached_url = await get_cached_preview_url(
|
|
168
|
+
redis_client, redis_config, meme.key
|
|
169
|
+
)
|
|
170
|
+
if cached_url:
|
|
171
|
+
return MemePreviewUrlResponse(url=cached_url, meme_key=meme.key)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
result = await run_sync(meme.generate_preview)()
|
|
175
|
+
except MemeGeneratorException as e:
|
|
176
|
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
|
177
|
+
|
|
178
|
+
content = result.getvalue()
|
|
179
|
+
media_type = str(filetype.guess_mime(content)) or "text/plain"
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
upload_result = await run_sync(upload_to_s3)(
|
|
183
|
+
content=content,
|
|
184
|
+
media_type=media_type,
|
|
185
|
+
meme_key=meme.key,
|
|
186
|
+
config=meme_config.storage.s3,
|
|
187
|
+
upload_kind="preview",
|
|
188
|
+
)
|
|
189
|
+
except S3StorageError as e:
|
|
190
|
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
await cache_preview_url(
|
|
194
|
+
redis_client, redis_config, meme.key, upload_result.url
|
|
195
|
+
)
|
|
196
|
+
except RedisError as e:
|
|
197
|
+
error = RedisCacheError(f"Redis 缓存写入失败:{e}")
|
|
198
|
+
raise HTTPException(status_code=error.status_code, detail=error.message)
|
|
199
|
+
|
|
200
|
+
return MemePreviewUrlResponse(url=upload_result.url, meme_key=meme.key)
|
|
201
|
+
except RedisError as e:
|
|
202
|
+
error = RedisCacheError(f"Redis 缓存读取失败:{e}")
|
|
203
|
+
raise HTTPException(status_code=error.status_code, detail=error.message)
|
|
204
|
+
finally:
|
|
205
|
+
await redis_client.aclose()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class MemeKeyWithProperties(BaseModel):
|
|
209
|
+
meme_key: str
|
|
210
|
+
disabled: bool = False
|
|
211
|
+
labels: list[Literal["new", "hot"]] = []
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
default_meme_list = [
|
|
215
|
+
MemeKeyWithProperties(meme_key=meme.key)
|
|
216
|
+
for meme in sorted(get_memes(), key=lambda meme: meme.key)
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class RenderMemeListRequest(BaseModel):
|
|
221
|
+
meme_list: list[MemeKeyWithProperties] = default_meme_list
|
|
222
|
+
text_template: str = "{keywords}"
|
|
223
|
+
add_category_icon: bool = True
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def register_routers():
|
|
227
|
+
@app.post("/memes/render_list")
|
|
228
|
+
def _(params: RenderMemeListRequest = RenderMemeListRequest()):
|
|
229
|
+
try:
|
|
230
|
+
meme_list = [
|
|
231
|
+
(
|
|
232
|
+
get_meme(p.meme_key),
|
|
233
|
+
MemeProperties(disabled=p.disabled, labels=p.labels),
|
|
234
|
+
)
|
|
235
|
+
for p in params.meme_list
|
|
236
|
+
]
|
|
237
|
+
except NoSuchMeme as e:
|
|
238
|
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
|
239
|
+
|
|
240
|
+
result = render_meme_list(
|
|
241
|
+
meme_list,
|
|
242
|
+
text_template=params.text_template,
|
|
243
|
+
add_category_icon=params.add_category_icon,
|
|
244
|
+
)
|
|
245
|
+
content = result.getvalue()
|
|
246
|
+
media_type = str(filetype.guess_mime(content)) or "text/plain"
|
|
247
|
+
return Response(content=content, media_type=media_type)
|
|
248
|
+
|
|
249
|
+
@app.get("/meme/version")
|
|
250
|
+
def _():
|
|
251
|
+
return __version__
|
|
252
|
+
|
|
253
|
+
@app.get("/memes/keys")
|
|
254
|
+
def _():
|
|
255
|
+
return get_meme_keys()
|
|
256
|
+
|
|
257
|
+
@app.get("/memes/{key}/info")
|
|
258
|
+
def _(key: str):
|
|
259
|
+
try:
|
|
260
|
+
meme = get_meme(key)
|
|
261
|
+
except NoSuchMeme as e:
|
|
262
|
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
|
263
|
+
|
|
264
|
+
args_type_response = None
|
|
265
|
+
if args_type := meme.params_type.args_type:
|
|
266
|
+
args_model = args_type.args_model
|
|
267
|
+
args_type_response = MemeArgsResponse(
|
|
268
|
+
args_model=model_json_schema(args_model),
|
|
269
|
+
args_examples=[
|
|
270
|
+
model_dump(example) for example in args_type.args_examples
|
|
271
|
+
],
|
|
272
|
+
parser_options=args_type.parser_options,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return MemeInfoResponse(
|
|
276
|
+
key=meme.key,
|
|
277
|
+
params_type=MemeParamsResponse(
|
|
278
|
+
min_images=meme.params_type.min_images,
|
|
279
|
+
max_images=meme.params_type.max_images,
|
|
280
|
+
min_texts=meme.params_type.min_texts,
|
|
281
|
+
max_texts=meme.params_type.max_texts,
|
|
282
|
+
default_texts=meme.params_type.default_texts,
|
|
283
|
+
args_type=args_type_response,
|
|
284
|
+
),
|
|
285
|
+
keywords=meme.keywords,
|
|
286
|
+
shortcuts=meme.shortcuts,
|
|
287
|
+
tags=meme.tags,
|
|
288
|
+
date_created=meme.date_created,
|
|
289
|
+
date_modified=meme.date_modified,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
@app.get("/memes/{key}/preview")
|
|
293
|
+
async def _(key: str):
|
|
294
|
+
try:
|
|
295
|
+
meme = get_meme(key)
|
|
296
|
+
result = await run_sync(meme.generate_preview)()
|
|
297
|
+
except MemeGeneratorException as e:
|
|
298
|
+
raise HTTPException(status_code=e.status_code, detail=e.message)
|
|
299
|
+
|
|
300
|
+
content = result.getvalue()
|
|
301
|
+
media_type = str(filetype.guess_mime(content)) or "text/plain"
|
|
302
|
+
return Response(content=content, media_type=media_type)
|
|
303
|
+
|
|
304
|
+
for meme in sorted(get_memes(), key=lambda meme: meme.key):
|
|
305
|
+
register_router(meme)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def run_server():
|
|
309
|
+
import uvicorn
|
|
310
|
+
|
|
311
|
+
register_routers()
|
|
312
|
+
uvicorn.run(
|
|
313
|
+
app,
|
|
314
|
+
host=meme_config.server.host,
|
|
315
|
+
port=meme_config.server.port,
|
|
316
|
+
log_config=LOGGING_CONFIG,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
if __name__ == "__main__":
|
|
321
|
+
setup_logger()
|
|
322
|
+
run_server()
|