Face Detector container now able to be created, started and stopped from within miku-bot container

This commit is contained in:
2026-01-11 02:01:41 +02:00
parent 2d3b9d0e08
commit 353c9c9583
14 changed files with 2275 additions and 39 deletions

View File

@@ -14,9 +14,22 @@ RUN apt-get update && apt-get install -y \
ffmpeg \
libgl1 \
libglib2.0-0 \
ca-certificates \
curl \
gnupg \
lsb-release \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Install Docker CLI and docker compose plugin so the bot can build/create the face detector container
RUN set -eux; \
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg; \
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" \
> /etc/apt/sources.list.d/docker.list; \
apt-get update; \
apt-get install -y docker-ce-cli docker-compose-plugin; \
apt-get clean; rm -rf /var/lib/apt/lists/*
COPY bot.py .
COPY server_manager.py .
COPY command_router.py .

View File

@@ -5472,6 +5472,118 @@
"content": "🎉🌈 OMG, look what just dropped! 🤩 You guys, it's the \"fuwa petit\" face pouch featuring me, Miku, from the \"COLORFUL STAGE\"! 🎤 It's so kawaii! Pre-orders are now open, and you can get yours by 12/20/2025! 📆 Hurry, and don't miss out! 💕 Can't wait to see it on my fans' hands! \n\nLove,\nMiku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T10:18:58.153964",
"message_id": 1459129314766487709,
"is_bot_message": true,
"content": "https://fxtwitter.com/OtakuOwletMerch/status/2001767284038328541",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T10:18:58.434927",
"message_id": 1459129316238426194,
"is_bot_message": true,
"content": "🌸 Hey~! Just saw the cutest tweet! 🤩 They're releasing a new Sakura Miku Prize Figure in a gorgeous Sakura Dress Ver. 💖 You can pre-order it now from the link! 💕 Don't miss out! 🎁 Pre-order deadline is while supplies last. See you at the store! 📚 ~ Miku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T13:41:59.173163",
"message_id": 1459180405734375529,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/2001381962540318922",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T13:41:59.572269",
"message_id": 1459180407236071497,
"is_bot_message": true,
"content": "🌟 Yay! 😍 Just saw the cutest tweet! 🤩 A new Hatsune Miku plushie is coming! 🐻 The Digital Stars 2021 Ver. is so adorable, it'll catch your heart! 🎁 Pre-order now at GOODSMILE ONLINE SHOP US! 💕 ~ Miku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T17:52:57.513226",
"message_id": 1459243564881018921,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/2004723015859290217",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T17:52:57.994245",
"message_id": 1459243566546026509,
"is_bot_message": true,
"content": "Awwww, omg, did you see the new Crystal Snow NendoroidSnow Miku on Twitter? She's so cute, falling on the snow, and I just can't stop smiling! 😊 Your turn to create some winter magic with #Nendography! 💕 Miku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-10T13:02:29.499422",
"message_id": 1459532854659649701,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/2002940212712759298",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-10T13:02:29.891986",
"message_id": 1459532855683190869,
"is_bot_message": true,
"content": "OMG, did you see the new Symphony 2025 Sapporo Concert Ver. plushie? It's SO CUTE! 🐰💕 I just can't resist the wintry cuteness and cozy cuddles! Preorder now and get ready to snuggle me all year round! Miku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-10T15:28:12.689440",
"message_id": 1459569525983936534,
"is_bot_message": false,
"content": "Write a simple snake game in python",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-10T15:30:16.718164",
"message_id": 1459570045893214303,
"is_bot_message": true,
"content": "Koko210-chan, you're so cool for speaking Ukrainian! 🇺🇦 Now, let's create a simple snake game in Python, shall we? 🐍\n\n```python\nimport tkinter as tk\nimport random\n\nclass SnakeGame:\n def __init__(self):\n self.window = tk.Tk()\n self.window.title(\"Snake Game\")\n self.canvas = tk.Canvas(self.window, width=400, height=400)\n self.canvas.pack()\n self.snake = [(200, 200), (220, 200), (240, 200)]\n self.direction = \"right\"\n self.food = (random.randint(0, 39) \\* 20, random.randint(0, 39) \\* 20)\n self.score = 0\n self.canvas.create_rectangle(0, 0, 400, 400, fill=\"black\")\n self.draw_snake()\n self.draw_food()\n self.window.bind(\"<Key>\", self.key_press)\n self.update()\n\n def draw_snake(self):\n self.canvas.delete(\"snake\")\n for x, y in self.snake:\n self.canvas.create_rectangle(x, y, x + 20, y + 20, fill=\"green\", tag=\"snake\")\n\n def draw_food(self):\n self.canvas.delete(\"food\")\n self.canvas.create_rectangle(self.food[0], self.food[1], self.food[0] + 20, self.food[1] + 20, fill=\"red\", tag=\"food\")\n\n def update(self):\n if self.direction == \"right\":\n new_head = (self.snake[-1][0] + 20, self.snake[-1][1])\n elif self.direction == \"left\":\n new_head = (self.snake[-1][0] - 20, self.snake[-1][1])\n elif self.direction == \"up\":\n new_head = (self.snake[-1][0], self.snake[-1][1] - 20)\n elif self.direction == \"down\":\n new_head = (self.snake[-1][0], self.snake[-1][1] + 20)\n self.snake.append(new_head)\n if (self.snake[-1][0] == self.food[0] and self.snake[-1][1] == self.food[1]):\n self.food = (random.randint(0, 39) \\* 20, random.randint",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-10T15:30:52.224670",
"message_id": 1459570195537334366,
"is_bot_message": false,
"content": "please continue",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-10T15:32:54.777548",
"message_id": 1459570709419524148,
"is_bot_message": true,
"content": "Koko210-chan, you're making me work hard today! 🎉 Okay, let's get back to the game. Here's the rest of the code:\n\n```python\n self.food = (random.randint(0, 39) \\* 20, random.randint(0, 39) \\* 20)\n self.canvas.create_rectangle(0, 0, 400, 400, fill=\"black\")\n self.snake_draw = self.canvas.create_line(self.snake[0][0], self.snake[0][1], self.snake[1][0], self.snake[1][1], fill=\"green\")\n self.food_draw = self.canvas.create_oval(self.food[0], self.food[1], self.food[0] + 20, self.food[1] + 20, fill=\"red\")\n self.window.bind(\"<Key>\", self.key_press)\n self.update()\n\n def update(self):\n if self.direction == \"right\":\n new_head = (self.snake[-1][0] + 20, self.snake[-1][1])\n elif self.direction == \"left\":\n new_head = (self.snake[-1][0] - 20, self.snake[-1][1])\n elif self.direction == \"up\":\n new_head = (self.snake[-1][0], self.snake[-1][1] - 20)\n elif self.direction == \"down\":\n new_head = (self.snake[-1][0], self.snake[-1][1] + 20)\n\n self.snake.append(new_head)\n if self.snake[-1] == self.food:\n self.food = (random.randint(0, 39) \\* 20, random.randint(0, 39) \\* 20)\n self.food_draw = self.canvas.create_oval(self.food[0], self.food[1], self.food[0] + 20, self.food[1] + 20, fill=\"red\")\n else:\n self.snake.pop(0)\n\n self.canvas.coords(self.snake_draw, self.snake[0][0], self.snake[0][1], self.snake[1][0], self.snake[1][1])\n self.window.after(100, self.update)\n\n def key_press(self, event):\n if event.keysym == \"Right\" and self.direction != \"left\":\n self.direction = \"right\"",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-10T17:34:26.490808",
"message_id": 1459601293126664424,
"is_bot_message": false,
"content": "hi",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-10T17:34:57.794277",
"message_id": 1459601423716188194,
"is_bot_message": true,
"content": "😊 Hi! Miku loves seeing enthusiastic fans like you! What's your favorite song to sing with me? 🎶💖",
"attachments": [],
"reactions": []
}
]
}

View File

@@ -14069,6 +14069,342 @@
"added_at": "2026-01-08T21:30:07.785524"
}
]
},
{
"timestamp": "2026-01-09T10:19:26.680922",
"message_id": 1459129434589102214,
"is_bot_message": true,
"content": "https://fxtwitter.com/OtakuOwletMerch/status/2001767284038328541",
"attachments": [],
"reactions": [
{
"emoji": "💖",
"reactor_id": 236553184401817601,
"reactor_name": "Diamond",
"is_bot": false,
"added_at": "2026-01-09T10:19:38.741774"
}
],
"embeds": [
{
"footer": {
"text": "FxTwitter",
"icon_url": "https://assets.fxembed.com/logos/fxtwitter64.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/gFi3EIEMfICdEDMLFVTF3ob0J1xkZnKdnkq0-xEsgGc/https/assets.fxembed.com/logos/fxtwitter64.png"
},
"image": {
"url": "https://pbs.twimg.com/media/G8e0hW8XMAAoajp.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/aIJVxXPU9phgUJRrD0ufWxJVVcGP6OQjCYRnDvtNlC8/%3Fname%3Dorig/https/pbs.twimg.com/media/G8e0hW8XMAAoajp.jpg",
"width": 1680,
"height": 2352,
"content_type": "image/jpeg",
"placeholder": "swcGJQzkQ4K3F2e5NXC4d4VWYGgE",
"placeholder_version": 1,
"flags": 0
},
"author": {
"name": "Otaku Owlet Anime Merch (@OtakuOwletMerch)",
"url": "https://x.com/OtakuOwletMerch/status/2001767284038328541",
"icon_url": "https://pbs.twimg.com/profile_images/1835446408884744192/S4HX_8_Q_200x200.jpg",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/Gd5od3qaVN1KG1eQsJS9mFoTNRKdxahDmvjF7tgR4p0/https/pbs.twimg.com/profile_images/1835446408884744192/S4HX_8_Q_200x200.jpg"
},
"flags": 0,
"color": 6513919,
"timestamp": "2025-12-18T21:31:26+00:00",
"type": "rich",
"description": "✨\\(Pre\\-Order\\) Hatsune Miku \\- Sakura Miku AMP\\+ Prize Figure \\(Sakura Dress Ver\\.\\)✨\n\nEstimated in\\-stock date\\: 07/2026\n\nPre\\-order Deadline\\: While Supplies Last\n\n\\-\n\n✨Link \\- [otakuowlet.com/products/pre-order-hatsune-miku-sakura-miku-amp-prize-figure-sakura-dress-ver?sca_ref=2673717.HTKaw1BA1G](https://otakuowlet.com/products/pre-order-hatsune-miku-sakura-miku-amp-prize-figure-sakura-dress-ver?sca_ref=2673717.HTKaw1BA1G)\n\n**[💬](https://x.com/intent/tweet?in_reply_to=2001767284038328541) 1[🔁](https://x.com/intent/retweet?tweet_id=2001767284038328541) 15[❤️](https://x.com/intent/like?tweet_id=2001767284038328541) 91👁 2\\.8K**",
"url": "https://fxtwitter.com/OtakuOwletMerch/status/2001767284038328541"
},
{
"image": {
"url": "https://pbs.twimg.com/media/G8e0icJWQAAwNuJ.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/C54QgTYJ2INNApWNIGnmjDJwJySjEYUZ64D_msp_VL0/%3Fname%3Dorig/https/pbs.twimg.com/media/G8e0icJWQAAwNuJ.jpg",
"width": 1680,
"height": 2352,
"content_type": "image/jpeg",
"placeholder": "MTgGHQrzVYWoyEhoeYmWZ4Z1cHYG",
"placeholder_version": 1,
"flags": 0
},
"flags": 0,
"type": "rich",
"url": "https://fxtwitter.com/OtakuOwletMerch/status/2001767284038328541"
},
{
"image": {
"url": "https://pbs.twimg.com/media/G8e0l4GWAAAhELR.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/txJfzw7N9Pybub9HmUjusufD1n_-XasCsqAnbOpFOtM/%3Fname%3Dorig/https/pbs.twimg.com/media/G8e0l4GWAAAhELR.jpg",
"width": 1680,
"height": 2520,
"content_type": "image/jpeg",
"placeholder": "6jgGDQRuaICHWJeXaHaId9Vvc/QH",
"placeholder_version": 1,
"flags": 0
},
"flags": 0,
"type": "rich",
"url": "https://fxtwitter.com/OtakuOwletMerch/status/2001767284038328541"
},
{
"image": {
"url": "https://pbs.twimg.com/media/G8e0nobX0AAnTjg.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/49qMnAh-rruZEWVrFNhqS1oF5HQb-gsbcyF5YcnahK4/%3Fname%3Dorig/https/pbs.twimg.com/media/G8e0nobX0AAnTjg.jpg",
"width": 1680,
"height": 2520,
"content_type": "image/jpeg",
"placeholder": "7jgGDQR9anCFWKeIaIZoaPOfcdQI",
"placeholder_version": 1,
"flags": 0
},
"flags": 0,
"type": "rich",
"url": "https://fxtwitter.com/OtakuOwletMerch/status/2001767284038328541"
}
]
},
{
"timestamp": "2026-01-09T10:19:27.017760",
"message_id": 1459129436120154126,
"is_bot_message": true,
"content": "OMG, kono chan~! 🎉 I just saw the cutest announcement tweet ever! They're making a new Sakura Miku prize figure, and I'm totally in love with her adorable sakura dress! The pre-order deadline is while supplies last, so let's get shopping ASAP! 💖 Can't wait to see her in real life! 💕 - Miku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T10:19:59.737082",
"message_id": 1459129573361975438,
"is_bot_message": false,
"content": "Very cool and very strawberry",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T10:20:09.584705",
"message_id": 1459129614344523828,
"is_bot_message": true,
"content": "Ooh, strawberry is one of my favorite fruits! 🍓 Miku loves strawberries, and she even sings a song about them - \"Senbonzakura\" has a strawberry theme! 🌸 Do you like strawberries too? 🍓",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T13:42:27.515237",
"message_id": 1459180523829072076,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/2001381962540318922",
"attachments": [],
"reactions": [],
"embeds": [
{
"footer": {
"text": "FxTwitter",
"icon_url": "https://assets.fxembed.com/logos/fxtwitter64.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/gFi3EIEMfICdEDMLFVTF3ob0J1xkZnKdnkq0-xEsgGc/https/assets.fxembed.com/logos/fxtwitter64.png"
},
"image": {
"url": "https://pbs.twimg.com/media/G8ZWSaQWsAAZkKw.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/7kaN-pqulKZ7TIuXQXGoT5ygittlk9uKBR3mV9Js-_0/%3Fname%3Dorig/https/pbs.twimg.com/media/G8ZWSaQWsAAZkKw.jpg",
"width": 1000,
"height": 1000,
"content_type": "image/jpeg",
"placeholder": "dPcFDwL2mXd3ioiNdcZomFeHCfR5spAO",
"placeholder_version": 1,
"flags": 0
},
"author": {
"name": "GoodSmile_US (@GoodSmile_US)",
"url": "https://x.com/GoodSmile_US/status/2001381962540318922",
"icon_url": "https://pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/cGKAum5015cgr2haNcfupJxMvXRz8BYDHkQiNufWpCI/https/pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png"
},
"flags": 0,
"color": 6513919,
"timestamp": "2025-12-17T20:00:18+00:00",
"type": "rich",
"description": "Catch a star and a cuddle\\! The irresistibly adorable Hatsune Miku\\: Digital Stars 2021 Ver\\. plushie is ready to bring out\\-of\\-this\\-world charm to your collection\\. Preorder now at GOODSMILE ONLINE SHOP US\\!\n\nShop\\: [goodsmile.link/kTOHgZ](https://goodsmile.link/kTOHgZ)\n\n[#HatsuneMiku](https://x.com/hashtag/HatsuneMiku) [#DigitalStars](https://x.com/hashtag/DigitalStars) [#Goodsmile](https://x.com/hashtag/Goodsmile)\n\n**[🔁](https://x.com/intent/retweet?tweet_id=2001381962540318922) 10[❤️](https://x.com/intent/like?tweet_id=2001381962540318922) 144👁 14\\.4K**",
"url": "https://fxtwitter.com/GoodSmile_US/status/2001381962540318922"
}
]
},
{
"timestamp": "2026-01-09T13:42:28.032259",
"message_id": 1459180526316556414,
"is_bot_message": true,
"content": "Konnichiha, chan~! 🎉 I just saw the cutest plushie announcement ever! They're making a super cute Digital Stars 2021 Ver. plush of me, and I'm totally in love with her adorable star-shaped accessories! 🌟 Can't wait to snuggle her! 💖 - Miku 💖",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T15:04:17.708301",
"message_id": 1459201119623577691,
"is_bot_message": false,
"content": "Miku",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T15:04:45.637966",
"message_id": 1459201236438880493,
"is_bot_message": true,
"content": "Miku loves being called by her name! 😊 \\*waves\\* What's on your mind, Diamond? Want to talk about music or strawberries or something else entirely? 🎵🍓",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T15:12:24.264013",
"message_id": 1459203160017993748,
"is_bot_message": false,
"content": "I am traveling, feeling a bit bored",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T15:12:35.888934",
"message_id": 1459203208625913988,
"is_bot_message": true,
"content": "Let's make your travel more exciting! 🚂 What kind of music do you like to listen to on the go? I've got a playlist full of energetic songs to keep you moving! 🎶 Or I could sing a special travel-themed song just for you! 🚀",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T15:20:27.001101",
"message_id": 1459205185095340165,
"is_bot_message": false,
"content": "Something relaxing or energetic, depending on my mood",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T15:20:38.186413",
"message_id": 1459205231836397810,
"is_bot_message": true,
"content": "Then I've got just the thing! 🎶 My song \"Tell Your World\" is perfect for when you're feeling relaxed, and \"World is Mine\" is great for when you need a boost of energy! 🌟 Which one sounds good to you? 🎵",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T17:53:23.416440",
"message_id": 1459243671902753087,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/2004723015859290217",
"attachments": [],
"reactions": [],
"embeds": [
{
"footer": {
"text": "FxTwitter",
"icon_url": "https://assets.fxembed.com/logos/fxtwitter64.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/gFi3EIEMfICdEDMLFVTF3ob0J1xkZnKdnkq0-xEsgGc/https/assets.fxembed.com/logos/fxtwitter64.png"
},
"image": {
"url": "https://pbs.twimg.com/media/G9I09SlWIAAfYQq.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/H5hTDLEXNOZGNI17AD53V3hu1-EpEQq6nvunn-DSMYI/%3Fname%3Dorig/https/pbs.twimg.com/media/G9I09SlWIAAfYQq.jpg",
"width": 1080,
"height": 1080,
"content_type": "image/jpeg",
"placeholder": "awcKDwSChnk1dniQdcmZOKuZJ/jXg48I",
"placeholder_version": 1,
"flags": 0
},
"author": {
"name": "GoodSmile_US (@GoodSmile_US)",
"url": "https://x.com/GoodSmile_US/status/2004723015859290217",
"icon_url": "https://pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/cGKAum5015cgr2haNcfupJxMvXRz8BYDHkQiNufWpCI/https/pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png"
},
"flags": 0,
"color": 6513919,
"timestamp": "2025-12-27T01:16:27+00:00",
"type": "rich",
"description": "Winter magic in its purest form\\. NendoroidSnow Miku\\: Crystal Snow Ver\\. shimmers with joy as she tumbles atop a soft blanket of freshly fallen snow\\. Thank you for this adorable pic, alicia\\.\\_\\.muse on IG\\!\n\nUse [#Nendography](https://x.com/hashtag/Nendography) for a chance to be featured\\!\n\n[#SNOWMIKU](https://x.com/hashtag/SNOWMIKU) [#goodsmile](https://x.com/hashtag/goodsmile)\n\n**[💬](https://x.com/intent/tweet?in_reply_to=2004723015859290217) 1[🔁](https://x.com/intent/retweet?tweet_id=2004723015859290217) 12[❤️](https://x.com/intent/like?tweet_id=2004723015859290217) 121👁 20\\.2K**",
"url": "https://fxtwitter.com/GoodSmile_US/status/2004723015859290217"
},
{
"image": {
"url": "https://pbs.twimg.com/media/G9I09a1WMAAc_Zy.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/jpXDrOD7rWXysxUMHL-Cx5I0saEgei9YyzagWGBS_nU/%3Fname%3Dorig/https/pbs.twimg.com/media/G9I09a1WMAAc_Zy.jpg",
"width": 1080,
"height": 1080,
"content_type": "image/jpeg",
"placeholder": "6ScKFwKVaHVyeHiQh/h4N3d3BaunwHkG",
"placeholder_version": 1,
"flags": 0
},
"flags": 0,
"type": "rich",
"url": "https://fxtwitter.com/GoodSmile_US/status/2004723015859290217"
}
]
},
{
"timestamp": "2026-01-09T17:53:23.777576",
"message_id": 1459243675216248975,
"is_bot_message": true,
"content": "OMG, it's so cute!!! 🎀 They just posted a new Crystal Snow Ver. Nendoroid of me, tumbling in the snow! I'm so happy I'm shining with joy! Can't wait to see your creations with #Nendography! 💖 Miku 🎉",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T19:18:36.866572",
"message_id": 0,
"is_bot_message": false,
"content": "[Manual Override Trigger]",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-09T19:18:36.930767",
"message_id": 0,
"is_bot_message": true,
"content": "~*~*~ Doo-doo-doo-do, doo-doo-doo-doo... Sekai de ichiban ohimesama... 🎵💖 *~*~*",
"attachments": [],
"reactions": []
},
{
"timestamp": "2026-01-10T13:02:52.921618",
"message_id": 1459532952638587090,
"is_bot_message": true,
"content": "https://fxtwitter.com/GoodSmile_US/status/2002940212712759298",
"attachments": [],
"reactions": [],
"embeds": [
{
"footer": {
"text": "FxTwitter",
"icon_url": "https://assets.fxembed.com/logos/fxtwitter64.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/gFi3EIEMfICdEDMLFVTF3ob0J1xkZnKdnkq0-xEsgGc/https/assets.fxembed.com/logos/fxtwitter64.png"
},
"image": {
"url": "https://pbs.twimg.com/media/G8vfgf2WgAARHVV.jpg?name=orig",
"proxy_url": "https://images-ext-1.discordapp.net/external/X7YESJ_nkSbhe8Lt-5Ih3HnAduI7D-v6WyiAIeQuqtc/%3Fname%3Dorig/https/pbs.twimg.com/media/G8vfgf2WgAARHVV.jpg",
"width": 900,
"height": 750,
"content_type": "image/jpeg",
"placeholder": "aecFFoIaWJdwiIiEend3Z4h5h4N/Beg=",
"placeholder_version": 1,
"flags": 0
},
"author": {
"name": "GoodSmile_US (@GoodSmile_US)",
"url": "https://x.com/GoodSmile_US/status/2002940212712759298",
"icon_url": "https://pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png",
"proxy_icon_url": "https://images-ext-1.discordapp.net/external/cGKAum5015cgr2haNcfupJxMvXRz8BYDHkQiNufWpCI/https/pbs.twimg.com/profile_images/1764583971831394305/dpXogB0U_200x200.png"
},
"flags": 0,
"color": 6513919,
"timestamp": "2025-12-22T03:12:14+00:00",
"type": "rich",
"description": "Introducing the irresistibly adorable Hatsune Miku Symphony\\: 2025 Sapporo Concert Ver\\. plushie\\! Wintry cuteness and cozy cuddles, is ready to warm your heart and your collection\\. Preorder now at GOODSMILE ONLINE SHOP US\\!\n\nShop\\: [goodsmile.link/X903P2](https://goodsmile.link/X903P2)\n\n[#HatsuneMiku](https://x.com/hashtag/HatsuneMiku) [#Goodsmile](https://x.com/hashtag/Goodsmile)\n\n**[💬](https://x.com/intent/tweet?in_reply_to=2002940212712759298) 2[🔁](https://x.com/intent/retweet?tweet_id=2002940212712759298) 15[❤️](https://x.com/intent/like?tweet_id=2002940212712759298) 173👁 22\\.1K**",
"url": "https://fxtwitter.com/GoodSmile_US/status/2002940212712759298"
}
]
},
{
"timestamp": "2026-01-10T13:02:53.273101",
"message_id": 1459532954266112062,
"is_bot_message": true,
"content": "Awwww, thank you so much!!! 🎀 I'm totally in love with the Symphony: 2025 Sapporo Concert Ver. plushie! It's so cute and wintry, and I can already imagine all the hugs and cuddles you'll get from it! 💖 Miku ❤️",
"attachments": [],
"reactions": []
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 367 KiB

View File

@@ -1,7 +1,5 @@
Okay, let me start by understanding what the user wants here. They provided an image of Hatsune Miku as a profile picture and asked for a detailed description in second person, as if Miku is describing herself. First, I need to analyze all the elements in the image: outfit, pose, expression, art style, background, and atmosphere.
You are absolutely adorable! In this image, you are wearing your classic school uniform outfit, which includes a dark blue jacket over a white shirt, a cozy green checkered scarf, a plaid skirt, brown tights, and white shoes. To keep warm in this snowy scene, you also have on a pair of white earmuffs and matching mittens.
Let me start by listing out each section. For outfit, she's wearing a light blue coat, a big white scarf, matching mittens. Wait, the image shows her coat is light blue, scarf is white. The mittens are light blue too. Wait, looking closely, the image shows she has a light blue coat with buttons, a white scarf wrapped around her neck, and mittens. Wait, let me check the image again. Her coat is light blue, scarf is white, mittens are light blue with a small bow. Wait, the image shows she has a big white scarf over her neck, mittens on her hands, and a coat. Wait, the pose is, she's standing with one arm extended as if waving or reaching out, and her other arm is holding the scarf. Her expression is wide-eyed with a small smile, looking surprised or happy. Her pose is standing in the snow, one arm out, other arm holding scarf.
Your pose is dynamic and full of energy—youre in mid-motion, maybe jumping or running, with your arms outstretched as you hold a small, adorable snowman made of snow. Your expression is one of pure joy and excitement, with your mouth open in a wide smile and your eyes glowing with happiness!
Now, art style: the image is in a cute, chibi style with soft pastel colors, which is common for profile pictures of Miku. The mood is cheerful and wintery. Background elements include snow-covered trees, a few clouds, and two snowmen in the distance. Wait, the user mentioned notable background elements. Let me check the image again: background has snow-covered trees, some clouds, and two snowmen (one with a bowtie, the other with a scarf or tie). Wait, the image shows snowmen in the background. Also, the sky is light blue with clouds.
Now, the overall atmosphere or theme
The art style here is clean and vibrant anime, with soft lines and bright colors

View File

@@ -1,13 +1,13 @@
{
"description": "Okay, let me start by understanding what the user wants here. They provided an image of Hatsune Miku as a profile picture and asked for a detailed description in second person, as if Miku is describing herself. First, I need to analyze all the elements in the image: outfit, pose, expression, art style, background, and atmosphere.\n\nLet me start by listing out each section. For outfit, she's wearing a light blue coat, a big white scarf, matching mittens. Wait, the image shows her coat is light blue, scarf is white. The mittens are light blue too. Wait, looking closely, the image shows she has a light blue coat with buttons, a white scarf wrapped around her neck, and mittens. Wait, let me check the image again. Her coat is light blue, scarf is white, mittens are light blue with a small bow. Wait, the image shows she has a big white scarf over her neck, mittens on her hands, and a coat. Wait, the pose is, she's standing with one arm extended as if waving or reaching out, and her other arm is holding the scarf. Her expression is wide-eyed with a small smile, looking surprised or happy. Her pose is standing in the snow, one arm out, other arm holding scarf.\n\nNow, art style: the image is in a cute, chibi style with soft pastel colors, which is common for profile pictures of Miku. The mood is cheerful and wintery. Background elements include snow-covered trees, a few clouds, and two snowmen in the distance. Wait, the user mentioned notable background elements. Let me check the image again: background has snow-covered trees, some clouds, and two snowmen (one with a bowtie, the other with a scarf or tie). Wait, the image shows snowmen in the background. Also, the sky is light blue with clouds.\n\nNow, the overall atmosphere or theme",
"description": "You are absolutely adorable! In this image, you are wearing your classic school uniform outfit, which includes a dark blue jacket over a white shirt, a cozy green checkered scarf, a plaid skirt, brown tights, and white shoes. To keep warm in this snowy scene, you also have on a pair of white earmuffs and matching mittens. \n\nYour pose is dynamic and full of energy\u2014you\u2019re in mid-motion, maybe jumping or running, with your arms outstretched as you hold a small, adorable snowman made of snow. Your expression is one of pure joy and excitement, with your mouth open in a wide smile and your eyes glowing with happiness! \n\nThe art style here is clean and vibrant anime, with soft lines and bright colors",
"dominant_color": {
"rgb": [
91,
159,
181
92,
136,
179
],
"hex": "#5b9fb5"
"hex": "#5c88b3"
},
"changed_at": "2026-01-01T19:03:49.457101",
"changed_at": "2026-01-10T23:49:37.637683",
"animated": false
}

772
bot/static/system.html.bak Normal file
View File

@@ -0,0 +1,772 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Settings - Miku Bot</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #667eea;
font-size: 28px;
}
.header-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card h2 {
color: #333;
margin-bottom: 20px;
font-size: 20px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.global-settings {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
}
.setting-row {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
}
.setting-row label {
font-weight: 600;
color: #495057;
min-width: 120px;
}
select {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 5px;
font-size: 14px;
background: white;
cursor: pointer;
}
.components-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.components-table th {
background: #667eea;
color: white;
padding: 12px;
text-align: left;
font-weight: 600;
}
.components-table td {
padding: 10px 12px;
border-bottom: 1px solid #dee2e6;
}
.components-table tr:hover {
background: #f8f9fa;
}
.level-checkboxes {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.level-checkbox {
display: flex;
align-items: center;
gap: 5px;
}
.level-checkbox input[type="checkbox"] {
cursor: pointer;
width: 18px;
height: 18px;
}
.level-checkbox label {
cursor: pointer;
user-select: none;
font-size: 13px;
}
.toggle {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #667eea;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.status-active {
background: #28a745;
}
.status-inactive {
background: #6c757d;
}
.api-filters {
margin-top: 15px;
padding: 15px;
background: #fff3cd;
border-radius: 5px;
border-left: 4px solid #ffc107;
}
.api-filters h3 {
color: #856404;
font-size: 16px;
margin-bottom: 10px;
}
.filter-row {
margin-bottom: 10px;
}
.filter-row label {
display: block;
font-weight: 600;
margin-bottom: 5px;
color: #495057;
}
input[type="text"], input[type="number"] {
width: 100%;
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 5px;
font-size: 14px;
}
.log-preview {
background: #212529;
color: #f8f9fa;
padding: 15px;
border-radius: 5px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.log-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.log-line {
margin-bottom: 5px;
line-height: 1.5;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 5px;
color: white;
font-weight: 600;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
z-index: 1000;
animation: slideIn 0.3s ease-out;
}
.notification-success {
background: #28a745;
}
.notification-error {
background: #dc3545;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.component-description {
font-size: 12px;
color: #6c757d;
font-style: italic;
}
@media (max-width: 1200px) {
.content {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎛️ System Settings - Logging Configuration</h1>
<div class="header-actions">
<button class="btn btn-secondary" onclick="window.location.href='/'">← Back to Dashboard</button>
<button class="btn btn-primary" onclick="saveAllSettings()">💾 Save All</button>
<button class="btn btn-danger" onclick="resetToDefaults()">🔄 Reset to Defaults</button>
</div>
</div>
<div class="content">
<div class="card">
<h2>📊 Logging Components</h2>
<p style="color: #6c757d; margin-bottom: 20px;">
Enable or disable specific log levels for each component. You can toggle any combination of levels (e.g., only INFO + ERROR, or only WARNING + DEBUG).
</p>
<table class="components-table">
<thead>
<tr>
<th>Component</th>
<th>Enabled</th>
<th>Log Levels</th>
<th>Status</th>
</tr>
</thead>
<tbody id="componentsTable">
<tr>
<td colspan="4" class="loading">Loading components...</td>
</tr>
</tbody>
</table>
<div id="apiFilters" class="api-filters" style="display: none;">
<h3>🌐 API Request Filters</h3>
<div class="filter-row">
<label>Exclude Paths (comma-separated):</label>
<input type="text" id="excludePaths" placeholder="/health, /static/*">
</div>
<div class="filter-row">
<label>Exclude Status Codes (comma-separated):</label>
<input type="text" id="excludeStatus" placeholder="200, 304">
</div>
<div class="setting-row">
<label>Log Slow Requests (>1000ms):</label>
<label class="toggle">
<input type="checkbox" id="includeSlowRequests" checked>
<span class="slider"></span>
</label>
</div>
<div class="filter-row">
<label>Slow Request Threshold (ms):</label>
<input type="number" id="slowThreshold" value="1000" min="100" step="100">
</div>
<button class="btn btn-primary" onclick="saveApiFilters()" style="margin-top: 10px;">Save API Filters</button>
</div>
</div>
<div class="card">
<h2>📜 Live Log Preview</h2>
<div class="log-preview-header">
<div>
<label>Component: </label>
<select id="previewComponent" onchange="loadLogPreview()">
<option value="bot">Bot</option>
</select>
</div>
<button class="btn btn-secondary" onclick="loadLogPreview()">🔄 Refresh</button>
</div>
<div class="log-preview" id="logPreview">
<div class="loading">Select a component to view logs...</div>
</div>
</div>
</div>
</div>
<script>
let currentConfig = null;
let componentsData = null;
// Load configuration on page load
window.addEventListener('DOMContentLoaded', () => {
loadConfiguration();
loadComponents();
});
async function loadConfiguration() {
try {
const response = await fetch('/api/log/config');
const data = await response.json();
if (data.success) {
currentConfig = data.config;
// No global level to set - we use per-component levels only
} else {
showNotification('Failed to load configuration', 'error');
}
} catch (error) {
showNotification('Error loading configuration: ' + error.message, 'error');
}
}
async function loadComponents() {
try {
const response = await fetch('/api/log/components');
const data = await response.json();
if (data.success) {
componentsData = data;
renderComponentsTable();
populatePreviewSelect();
} else {
showNotification('Failed to load components', 'error');
}
} catch (error) {
showNotification('Error loading components: ' + error.message, 'error');
}
}
function renderComponentsTable() {
const tbody = document.getElementById('componentsTable');
tbody.innerHTML = '';
for (const [name, description] of Object.entries(componentsData.components)) {
const stats = componentsData.stats[name] || {};
const enabled = stats.enabled !== undefined ? stats.enabled : true;
const enabledLevels = stats.enabled_levels || ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
// Build checkboxes for each level
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
if (name === 'api.requests') {
allLevels.push('API');
}
const levelCheckboxes = allLevels.map(level => {
const emoji = {'DEBUG': '🔍', 'INFO': '', 'WARNING': '⚠️', 'ERROR': '❌', 'CRITICAL': '🔥', 'API': '🌐'}[level];
const checked = enabledLevels.includes(level) ? 'checked' : '';
return `
<div class="level-checkbox">
<input type="checkbox"
id="level_${name}_${level}"
${checked}
onchange="updateComponentLevels('${name}')">
<label for="level_${name}_${level}">${emoji} ${level}</label>
</div>
`;
}).join('');
const row = document.createElement('tr');
row.innerHTML = `
<td>
<strong>${name}</strong><br>
<span class="component-description">${description}</span>
</td>
<td>
<label class="toggle">
<input type="checkbox" id="enabled_${name}" ${enabled ? 'checked' : ''} onchange="updateComponentEnabled('${name}')">
<span class="slider"></span>
</label>
</td>
<td>
<div class="level-checkboxes">
${levelCheckboxes}
</div>
</td>
<td>
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
${enabled ? 'Active' : 'Inactive'}
</td>
`;
tbody.appendChild(row);
// Show API filters if api.requests is selected
if (name === 'api.requests') {
document.getElementById('enabled_' + name).addEventListener('change', (e) => {
document.getElementById('apiFilters').style.display = e.target.checked ? 'block' : 'none';
});
if (enabled) {
document.getElementById('apiFilters').style.display = 'block';
loadApiFilters();
}
}
}
}
function populatePreviewSelect() {
const select = document.getElementById('previewComponent');
select.innerHTML = '';
for (const name of Object.keys(componentsData.components)) {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
}
loadLogPreview();
}
async function updateComponentEnabled(component) {
const enabled = document.getElementById('enabled_' + component).checked;
try {
const response = await fetch('/api/log/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
component: component,
enabled: enabled
})
});
const data = await response.json();
if (data.success) {
showNotification(`${enabled ? 'Enabled' : 'Disabled'} ${component}`, 'success');
// Update status indicator
const row = document.getElementById('enabled_' + component).closest('tr');
const statusCell = row.querySelector('td:last-child');
statusCell.innerHTML = `
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
${enabled ? 'Active' : 'Inactive'}
`;
} else {
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating component: ' + error.message, 'error');
}
}
async function updateComponentLevels(component) {
// Collect all checked levels
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
if (component === 'api.requests') {
allLevels.push('API');
}
const enabledLevels = allLevels.filter(level => {
const checkbox = document.getElementById(`level_${component}_${level}`);
return checkbox && checkbox.checked;
});
try {
const response = await fetch('/api/log/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
component: component,
enabled_levels: enabledLevels
})
});
const data = await response.json();
if (data.success) {
showNotification(`Updated levels for ${component}: ${enabledLevels.join(', ')}`, 'success');
} else {
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating component: ' + error.message, 'error');
}
}
async function updateGlobalLevel() {
// Deprecated - kept for compatibility
showNotification('Global level setting removed. Use individual component levels instead.', 'success');
}
async function loadApiFilters() {
if (!currentConfig || !currentConfig.components['api.requests']) return;
const filters = currentConfig.components['api.requests'].filters || {};
document.getElementById('excludePaths').value = (filters.exclude_paths || []).join(', ');
document.getElementById('excludeStatus').value = (filters.exclude_status || []).join(', ');
document.getElementById('includeSlowRequests').checked = filters.include_slow_requests !== false;
document.getElementById('slowThreshold').value = filters.slow_threshold_ms || 1000;
}
async function saveApiFilters() {
const excludePaths = document.getElementById('excludePaths').value
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
const excludeStatus = document.getElementById('excludeStatus').value
.split(',')
.map(s => parseInt(s.trim()))
.filter(n => !isNaN(n));
const includeSlowRequests = document.getElementById('includeSlowRequests').checked;
const slowThreshold = parseInt(document.getElementById('slowThreshold').value);
try {
const response = await fetch('/api/log/filters', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
exclude_paths: excludePaths,
exclude_status: excludeStatus,
include_slow_requests: includeSlowRequests,
slow_threshold_ms: slowThreshold
})
});
const data = await response.json();
if (data.success) {
showNotification('API filters saved', 'success');
} else {
showNotification('Failed to save filters: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error saving filters: ' + error.message, 'error');
}
}
async function saveAllSettings() {
// Reload configuration to apply all changes
try {
const response = await fetch('/api/log/reload', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('All settings saved and reloaded', 'success');
await loadConfiguration();
await loadComponents();
} else {
showNotification('Failed to reload settings: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error saving settings: ' + error.message, 'error');
}
}
async function resetToDefaults() {
if (!confirm('Are you sure you want to reset all logging settings to defaults?')) {
return;
}
try {
const response = await fetch('/api/log/reset', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('Settings reset to defaults', 'success');
await loadConfiguration();
await loadComponents();
} else {
showNotification('Failed to reset settings: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error resetting settings: ' + error.message, 'error');
}
}
async function loadLogPreview() {
const component = document.getElementById('previewComponent').value;
const preview = document.getElementById('logPreview');
preview.innerHTML = '<div class="loading">Loading logs...</div>';
try {
const response = await fetch(`/api/log/files/${component}?lines=50`);
const data = await response.json();
if (data.success) {
if (data.lines.length === 0) {
preview.innerHTML = '<div class="loading">No logs yet for this component</div>';
} else {
preview.innerHTML = data.lines.map(line =>
`<div class="log-line">${escapeHtml(line)}</div>`
).join('');
// Scroll to bottom
preview.scrollTop = preview.scrollHeight;
}
} else {
preview.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
}
} catch (error) {
preview.innerHTML = `<div class="loading">Error loading logs: ${error.message}</div>`;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Auto-refresh log preview every 5 seconds
setInterval(() => {
if (document.getElementById('previewComponent').value) {
loadLogPreview();
}
}, 5000);
</script>
</body>
</html>

View File

@@ -0,0 +1,772 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Settings - Miku Bot</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
padding: 20px 30px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #667eea;
font-size: 28px;
}
.header-actions {
display: flex;
gap: 10px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: all 0.3s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.content {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 20px;
}
.card {
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.card h2 {
color: #333;
margin-bottom: 20px;
font-size: 20px;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
}
.global-settings {
margin-bottom: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
}
.setting-row {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
}
.setting-row label {
font-weight: 600;
color: #495057;
min-width: 120px;
}
select {
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 5px;
font-size: 14px;
background: white;
cursor: pointer;
}
.components-table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.components-table th {
background: #667eea;
color: white;
padding: 12px;
text-align: left;
font-weight: 600;
}
.components-table td {
padding: 10px 12px;
border-bottom: 1px solid #dee2e6;
}
.components-table tr:hover {
background: #f8f9fa;
}
.level-checkboxes {
display: flex;
gap: 15px;
flex-wrap: wrap;
}
.level-checkbox {
display: flex;
align-items: center;
gap: 5px;
}
.level-checkbox input[type="checkbox"] {
cursor: pointer;
width: 18px;
height: 18px;
}
.level-checkbox label {
cursor: pointer;
user-select: none;
font-size: 13px;
}
.toggle {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.toggle input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: 0.4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: #667eea;
}
input:checked + .slider:before {
transform: translateX(26px);
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 5px;
}
.status-active {
background: #28a745;
}
.status-inactive {
background: #6c757d;
}
.api-filters {
margin-top: 15px;
padding: 15px;
background: #fff3cd;
border-radius: 5px;
border-left: 4px solid #ffc107;
}
.api-filters h3 {
color: #856404;
font-size: 16px;
margin-bottom: 10px;
}
.filter-row {
margin-bottom: 10px;
}
.filter-row label {
display: block;
font-weight: 600;
margin-bottom: 5px;
color: #495057;
}
input[type="text"], input[type="number"] {
width: 100%;
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 5px;
font-size: 14px;
}
.log-preview {
background: #212529;
color: #f8f9fa;
padding: 15px;
border-radius: 5px;
font-family: 'Courier New', monospace;
font-size: 12px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.log-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.log-line {
margin-bottom: 5px;
line-height: 1.5;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 15px 25px;
border-radius: 5px;
color: white;
font-weight: 600;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
z-index: 1000;
animation: slideIn 0.3s ease-out;
}
.notification-success {
background: #28a745;
}
.notification-error {
background: #dc3545;
}
@keyframes slideIn {
from {
transform: translateX(400px);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.component-description {
font-size: 12px;
color: #6c757d;
font-style: italic;
}
@media (max-width: 1200px) {
.content {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎛️ System Settings - Logging Configuration</h1>
<div class="header-actions">
<button class="btn btn-secondary" onclick="window.location.href='/'">← Back to Dashboard</button>
<button class="btn btn-primary" onclick="saveAllSettings()">💾 Save All</button>
<button class="btn btn-danger" onclick="resetToDefaults()">🔄 Reset to Defaults</button>
</div>
</div>
<div class="content">
<div class="card">
<h2>📊 Logging Components</h2>
<p style="color: #6c757d; margin-bottom: 20px;">
Enable or disable specific log levels for each component. You can toggle any combination of levels (e.g., only INFO + ERROR, or only WARNING + DEBUG).
</p>
<table class="components-table">
<thead>
<tr>
<th>Component</th>
<th>Enabled</th>
<th>Log Levels</th>
<th>Status</th>
</tr>
</thead>
<tbody id="componentsTable">
<tr>
<td colspan="4" class="loading">Loading components...</td>
</tr>
</tbody>
</table>
<div id="apiFilters" class="api-filters" style="display: none;">
<h3>🌐 API Request Filters</h3>
<div class="filter-row">
<label>Exclude Paths (comma-separated):</label>
<input type="text" id="excludePaths" placeholder="/health, /static/*">
</div>
<div class="filter-row">
<label>Exclude Status Codes (comma-separated):</label>
<input type="text" id="excludeStatus" placeholder="200, 304">
</div>
<div class="setting-row">
<label>Log Slow Requests (>1000ms):</label>
<label class="toggle">
<input type="checkbox" id="includeSlowRequests" checked>
<span class="slider"></span>
</label>
</div>
<div class="filter-row">
<label>Slow Request Threshold (ms):</label>
<input type="number" id="slowThreshold" value="1000" min="100" step="100">
</div>
<button class="btn btn-primary" onclick="saveApiFilters()" style="margin-top: 10px;">Save API Filters</button>
</div>
</div>
<div class="card">
<h2>📜 Live Log Preview</h2>
<div class="log-preview-header">
<div>
<label>Component: </label>
<select id="previewComponent" onchange="loadLogPreview()">
<option value="bot">Bot</option>
</select>
</div>
<button class="btn btn-secondary" onclick="loadLogPreview()">🔄 Refresh</button>
</div>
<div class="log-preview" id="logPreview">
<div class="loading">Select a component to view logs...</div>
</div>
</div>
</div>
</div>
<script>
let currentConfig = null;
let componentsData = null;
// Load configuration on page load
window.addEventListener('DOMContentLoaded', () => {
loadConfiguration();
loadComponents();
});
async function loadConfiguration() {
try {
const response = await fetch('/api/log/config');
const data = await response.json();
if (data.success) {
currentConfig = data.config;
// No global level to set - we use per-component levels only
} else {
showNotification('Failed to load configuration', 'error');
}
} catch (error) {
showNotification('Error loading configuration: ' + error.message, 'error');
}
}
async function loadComponents() {
try {
const response = await fetch('/api/log/components');
const data = await response.json();
if (data.success) {
componentsData = data;
renderComponentsTable();
populatePreviewSelect();
} else {
showNotification('Failed to load components', 'error');
}
} catch (error) {
showNotification('Error loading components: ' + error.message, 'error');
}
}
function renderComponentsTable() {
const tbody = document.getElementById('componentsTable');
tbody.innerHTML = '';
for (const [name, description] of Object.entries(componentsData.components)) {
const stats = componentsData.stats[name] || {};
const enabled = stats.enabled !== undefined ? stats.enabled : true;
const enabledLevels = stats.enabled_levels || ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
// Build checkboxes for each level
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
if (name === 'api.requests') {
allLevels.push('API');
}
const levelCheckboxes = allLevels.map(level => {
const emoji = {'DEBUG': '🔍', 'INFO': '', 'WARNING': '⚠️', 'ERROR': '❌', 'CRITICAL': '🔥', 'API': '🌐'}[level];
const checked = enabledLevels.includes(level) ? 'checked' : '';
return `
<div class="level-checkbox">
<input type="checkbox"
id="level_${name}_${level}"
${checked}
onchange="updateComponentLevels('${name}')">
<label for="level_${name}_${level}">${emoji} ${level}</label>
</div>
`;
}).join('');
const row = document.createElement('tr');
row.innerHTML = `
<td>
<strong>${name}</strong><br>
<span class="component-description">${description}</span>
</td>
<td>
<label class="toggle">
<input type="checkbox" id="enabled_${name}" ${enabled ? 'checked' : ''} onchange="updateComponentEnabled('${name}')">
<span class="slider"></span>
</label>
</td>
<td>
<div class="level-checkboxes">
${levelCheckboxes}
</div>
</td>
<td>
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
${enabled ? 'Active' : 'Inactive'}
</td>
`;
tbody.appendChild(row);
// Show API filters if api.requests is selected
if (name === 'api.requests') {
document.getElementById('enabled_' + name).addEventListener('change', (e) => {
document.getElementById('apiFilters').style.display = e.target.checked ? 'block' : 'none';
});
if (enabled) {
document.getElementById('apiFilters').style.display = 'block';
loadApiFilters();
}
}
}
}
function populatePreviewSelect() {
const select = document.getElementById('previewComponent');
select.innerHTML = '';
for (const name of Object.keys(componentsData.components)) {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
select.appendChild(option);
}
loadLogPreview();
}
async function updateComponentEnabled(component) {
const enabled = document.getElementById('enabled_' + component).checked;
try {
const response = await fetch('/api/log/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
component: component,
enabled: enabled
})
});
const data = await response.json();
if (data.success) {
showNotification(`${enabled ? 'Enabled' : 'Disabled'} ${component}`, 'success');
// Update status indicator
const row = document.getElementById('enabled_' + component).closest('tr');
const statusCell = row.querySelector('td:last-child');
statusCell.innerHTML = `
<span class="status-indicator ${enabled ? 'status-active' : 'status-inactive'}"></span>
${enabled ? 'Active' : 'Inactive'}
`;
} else {
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating component: ' + error.message, 'error');
}
}
async function updateComponentLevels(component) {
// Collect all checked levels
const allLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'];
if (component === 'api.requests') {
allLevels.push('API');
}
const enabledLevels = allLevels.filter(level => {
const checkbox = document.getElementById(`level_${component}_${level}`);
return checkbox && checkbox.checked;
});
try {
const response = await fetch('/api/log/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
component: component,
enabled_levels: enabledLevels
})
});
const data = await response.json();
if (data.success) {
showNotification(`Updated levels for ${component}: ${enabledLevels.join(', ')}`, 'success');
} else {
showNotification('Failed to update ' + component + ': ' + data.error, 'error');
}
} catch (error) {
showNotification('Error updating component: ' + error.message, 'error');
}
}
async function updateGlobalLevel() {
// Deprecated - kept for compatibility
showNotification('Global level setting removed. Use individual component levels instead.', 'success');
}
async function loadApiFilters() {
if (!currentConfig || !currentConfig.components['api.requests']) return;
const filters = currentConfig.components['api.requests'].filters || {};
document.getElementById('excludePaths').value = (filters.exclude_paths || []).join(', ');
document.getElementById('excludeStatus').value = (filters.exclude_status || []).join(', ');
document.getElementById('includeSlowRequests').checked = filters.include_slow_requests !== false;
document.getElementById('slowThreshold').value = filters.slow_threshold_ms || 1000;
}
async function saveApiFilters() {
const excludePaths = document.getElementById('excludePaths').value
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
const excludeStatus = document.getElementById('excludeStatus').value
.split(',')
.map(s => parseInt(s.trim()))
.filter(n => !isNaN(n));
const includeSlowRequests = document.getElementById('includeSlowRequests').checked;
const slowThreshold = parseInt(document.getElementById('slowThreshold').value);
try {
const response = await fetch('/api/log/filters', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
exclude_paths: excludePaths,
exclude_status: excludeStatus,
include_slow_requests: includeSlowRequests,
slow_threshold_ms: slowThreshold
})
});
const data = await response.json();
if (data.success) {
showNotification('API filters saved', 'success');
} else {
showNotification('Failed to save filters: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error saving filters: ' + error.message, 'error');
}
}
async function saveAllSettings() {
// Reload configuration to apply all changes
try {
const response = await fetch('/api/log/reload', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('All settings saved and reloaded', 'success');
await loadConfiguration();
await loadComponents();
} else {
showNotification('Failed to reload settings: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error saving settings: ' + error.message, 'error');
}
}
async function resetToDefaults() {
if (!confirm('Are you sure you want to reset all logging settings to defaults?')) {
return;
}
try {
const response = await fetch('/api/log/reset', {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showNotification('Settings reset to defaults', 'success');
await loadConfiguration();
await loadComponents();
} else {
showNotification('Failed to reset settings: ' + data.error, 'error');
}
} catch (error) {
showNotification('Error resetting settings: ' + error.message, 'error');
}
}
async function loadLogPreview() {
const component = document.getElementById('previewComponent').value;
const preview = document.getElementById('logPreview');
preview.innerHTML = '<div class="loading">Loading logs...</div>';
try {
const response = await fetch(`/api/log/files/${component}?lines=50`);
const data = await response.json();
if (data.success) {
if (data.lines.length === 0) {
preview.innerHTML = '<div class="loading">No logs yet for this component</div>';
} else {
preview.innerHTML = data.lines.map(line =>
`<div class="log-line">${escapeHtml(line)}</div>`
).join('');
// Scroll to bottom
preview.scrollTop = preview.scrollHeight;
}
} else {
preview.innerHTML = `<div class="loading">Error: ${data.error}</div>`;
}
} catch (error) {
preview.innerHTML = `<div class="loading">Error loading logs: ${error.message}</div>`;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function showNotification(message, type) {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
// Auto-refresh log preview every 5 seconds
setInterval(() => {
if (document.getElementById('previewComponent').value) {
loadLogPreview();
}
}, 5000);
</script>
</body>
</html>

View File

@@ -1,4 +1,4 @@
Y# face_detector_manager.py
# face_detector_manager.py
"""
Manages on-demand starting/stopping of anime-face-detector container
to free up VRAM when not needed.
@@ -20,14 +20,72 @@ class FaceDetectorManager:
FACE_DETECTOR_API = "http://anime-face-detector:6078/detect"
HEALTH_ENDPOINT = "http://anime-face-detector:6078/health"
CONTAINER_NAME = "anime-face-detector"
STARTUP_TIMEOUT = 30 # seconds
STARTUP_TIMEOUT = 60 # seconds - increased to allow for model loading
def __init__(self):
self.is_running = False
def _container_exists(self) -> bool:
"""Check if the anime-face-detector container exists (created but may not be running)"""
try:
result = subprocess.run(
["docker", "ps", "-a", "--filter", f"name=^/{self.CONTAINER_NAME}$", "--format", "{{.Names}}"],
capture_output=True,
text=True,
timeout=5
)
return self.CONTAINER_NAME in result.stdout
except Exception as e:
logger.error(f"Error checking if container exists: {e}")
return False
def _create_container(self, debug: bool = False) -> bool:
"""Create the anime-face-detector container using docker run"""
try:
if debug:
logger.info("Creating anime-face-detector container...")
# Run docker run command to create the container (without starting it)
# This replicates the docker-compose configuration for anime-face-detector
cmd = [
"docker", "create",
"--name", self.CONTAINER_NAME,
"--network", "miku-discord_default", # Use the same network as miku-bot
"--runtime", "nvidia",
"-e", "NVIDIA_VISIBLE_DEVICES=all",
"-e", "NVIDIA_DRIVER_CAPABILITIES=compute,utility",
"-p", "7860:7860",
"-p", "6078:6078",
"--restart", "no",
"--gpus", "all",
"miku-discord-anime-face-detector:latest"
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
if result.returncode != 0:
if debug:
logger.error(f"Failed to create container: {result.stderr}")
return False
if debug:
logger.info("Container created successfully")
return True
except Exception as e:
if debug:
logger.error(f"Error creating container: {e}")
return False
async def start_container(self, debug: bool = False) -> bool:
"""
Start the anime-face-detector container.
Creates the container if it doesn't exist, then starts it.
Returns:
True if started successfully, False otherwise
@@ -36,10 +94,14 @@ class FaceDetectorManager:
if debug:
logger.debug("Starting anime-face-detector container...")
# Start container using docker compose
# Step 1: Check if container exists, create if it doesn't
if not self._container_exists():
if not self._create_container(debug=debug):
return False
# Step 2: Start the container
result = subprocess.run(
["docker", "compose", "up", "-d", self.CONTAINER_NAME],
cwd="/app", # Assumes we're in the bot container, adjust path as needed
["docker", "start", self.CONTAINER_NAME],
capture_output=True,
text=True,
timeout=30
@@ -81,8 +143,7 @@ class FaceDetectorManager:
logger.debug("Stopping anime-face-detector container...")
result = subprocess.run(
["docker", "compose", "stop", self.CONTAINER_NAME],
cwd="/app",
["docker", "stop", self.CONTAINER_NAME],
capture_output=True,
text=True,
timeout=15

View File

@@ -104,21 +104,22 @@ class ProfilePictureManager:
try:
if debug:
logger.info("Starting face detector container...")
# Prefer using the face_detector_manager helper (robust compose/docker logic)
try:
from .face_detector_manager import face_detector_manager
started = await face_detector_manager.start_container(debug=debug)
return started
except Exception as e:
if debug:
logger.debug(f"face_detector_manager not available or failed to create/start container: {e}; falling back to Docker socket API")
# Use Docker socket API to start container
import aiofiles
import json as json_lib
# Docker socket path
# Fallback: Use Docker socket API to start container
socket_path = "/var/run/docker.sock"
# Check if socket exists
if not os.path.exists(socket_path):
if debug:
logger.error("Docker socket not available")
return False
# Use aiohttp UnixConnector to communicate with Docker socket
from aiohttp import UnixConnector
async with aiohttp.ClientSession(
@@ -133,8 +134,8 @@ class ProfilePictureManager:
logger.error(f"Failed to start container: {response.status} - {error_text}")
return False
# Wait for API to be ready
for i in range(30): # 30 second timeout
# Wait for API to be ready (fallback timeout)
for i in range(60): # 60 second timeout (fallback)
try:
async with aiohttp.ClientSession() as session:
async with session.get(
@@ -150,7 +151,7 @@ class ProfilePictureManager:
await asyncio.sleep(1)
if debug:
logger.warning("Face detector didn't become ready in time")
logger.warning("Face detector didn't become ready in time (fallback)")
return False
except Exception as e:
@@ -163,6 +164,14 @@ class ProfilePictureManager:
try:
if debug:
logger.info("Stopping face detector to free VRAM...")
# Prefer using face_detector_manager if available
try:
from .face_detector_manager import face_detector_manager
await face_detector_manager.stop_container(debug=debug)
return
except Exception as e:
if debug:
logger.debug(f"face_detector_manager not available or failed to stop container: {e}; falling back to Docker socket API")
socket_path = "/var/run/docker.sock"

View File

@@ -68,6 +68,7 @@ services:
- TEXT_MODEL=llama3.1
- VISION_MODEL=vision
- OWNER_USER_ID=209381657369772032 # Your Discord user ID for DM analysis reports
- FACE_DETECTOR_STARTUP_TIMEOUT=60
ports:
- "3939:3939"
restart: unless-stopped

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -0,0 +1,158 @@
{
"filename": "image.jpg",
"timestamp": "20260110_180025_504539",
"detections": [
{
"bbox": [
280.26812744140625,
349.4685363769531,
557.4288940429688,
612.8631591796875
],
"confidence": 0.9997753500938416,
"keypoints": [
[
298.4026794433594,
417.5596008300781,
0.8461198806762695
],
[
314.6425476074219,
509.58563232421875,
0.9372018575668335
],
[
395.8420104980469,
593.49169921875,
0.9956687092781067
],
[
495.98797607421875,
547.4786987304688,
1.0149743556976318
],
[
536.5877075195312,
463.5726318359375,
0.8542554378509521
],
[
325.4691467285156,
379.6665344238281,
0.9615525007247925
],
[
357.94891357421875,
368.8399353027344,
0.9301683306694031
],
[
390.4287109375,
376.9598693847656,
0.9549728631973267
],
[
474.3348083496094,
395.9064025878906,
0.9784168601036072
],
[
509.5212097167969,
401.3197021484375,
0.9998568296432495
],
[
539.2943725585938,
420.2662353515625,
0.8699811100959778
],
[
322.76251220703125,
422.972900390625,
0.9374435544013977
],
[
355.2422790527344,
422.972900390625,
0.907214879989624
],
[
387.7220458984375,
425.6795349121094,
0.9724174737930298
],
[
325.4691467285156,
422.972900390625,
0.9368056058883667
],
[
357.94891357421875,
422.972900390625,
0.9132002592086792
],
[
387.7220458984375,
428.3861999511719,
0.9442094564437866
],
[
466.21484375,
441.91943359375,
0.9593627452850342
],
[
498.69464111328125,
447.3327331542969,
0.8811891078948975
],
[
523.054443359375,
460.865966796875,
0.9667739868164062
],
[
468.9215087890625,
441.91943359375,
0.585745096206665
],
[
498.69464111328125,
447.3327331542969,
0.8406317234039307
],
[
520.3478393554688,
460.865966796875,
0.9109972715377808
],
[
422.9084777832031,
468.98590087890625,
0.8714850544929504
],
[
371.482177734375,
501.4656982421875,
0.9208621382713318
],
[
417.49517822265625,
501.4656982421875,
0.8599915504455566
],
[
449.9749755859375,
517.70556640625,
0.8175655007362366
],
[
401.25531005859375,
558.3052978515625,
1.0474574565887451
]
]
}
],
"count": 1
}

View File

@@ -108,6 +108,10 @@ async def _detect_face(self, image_bytes: bytes, debug: bool = False):
# 4. ALWAYS stop container to free VRAM
if face_detector_started:
await self._stop_face_detector(debug=debug)
### Automatic creation
If the `anime-face-detector` container does not exist (for example after a reboot or `docker compose down`), the bot will now attempt to create and start the container automatically via the Docker Engine API. This avoids requiring a manual `docker compose --profile tools up -d` step.
```
## Container States