initial
Browse files- Dockerfile +16 -0
- app.py +262 -0
- requirements.txt +4 -0
Dockerfile
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Read the doc: https://huggingface.co/docs/hub/spaces-sdks-docker
|
2 |
+
# you will also find guides on how best to write your Dockerfile
|
3 |
+
|
4 |
+
FROM python:3.10
|
5 |
+
|
6 |
+
RUN useradd -m -u 1000 user
|
7 |
+
USER user
|
8 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
9 |
+
|
10 |
+
WORKDIR /app
|
11 |
+
|
12 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
13 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
14 |
+
|
15 |
+
COPY --chown=user . /app
|
16 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
ADDED
@@ -0,0 +1,262 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# discord_bot.py
|
2 |
+
|
3 |
+
import asyncio
|
4 |
+
import logging
|
5 |
+
import os
|
6 |
+
import re
|
7 |
+
import sys
|
8 |
+
|
9 |
+
import discord
|
10 |
+
import requests # Ensure 'requests' is installed
|
11 |
+
from discord import Embed
|
12 |
+
from discord.ext import commands
|
13 |
+
from gradio_client import Client
|
14 |
+
from gradio_client.exceptions import AppError # Updated import
|
15 |
+
|
16 |
+
# **Fetch Discord Bot Token from Environment Variable**
|
17 |
+
DISCORD_BOT_TOKEN = os.environ.get('DISCORD_BOT_TOKEN')
|
18 |
+
HF_TOKEN = os.environ.get('HF_TOKEN') # Fetch the HF_TOKEN
|
19 |
+
|
20 |
+
if not DISCORD_BOT_TOKEN:
|
21 |
+
print("Error: The environment variable 'DISCORD_BOT_TOKEN' is not set.")
|
22 |
+
sys.exit(1)
|
23 |
+
|
24 |
+
if not HF_TOKEN:
|
25 |
+
print("Error: The environment variable 'HF_TOKEN' is not set.")
|
26 |
+
sys.exit(1)
|
27 |
+
|
28 |
+
# Configure logging
|
29 |
+
logging.basicConfig(
|
30 |
+
level=logging.INFO, # Change to DEBUG for more detailed logs
|
31 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
32 |
+
handlers=[
|
33 |
+
logging.StreamHandler(sys.stdout)
|
34 |
+
]
|
35 |
+
)
|
36 |
+
logger = logging.getLogger(__name__)
|
37 |
+
|
38 |
+
# Intents are required for accessing certain Discord gateway events
|
39 |
+
intents = discord.Intents.default()
|
40 |
+
intents.message_content = True # Enable access to message content
|
41 |
+
|
42 |
+
# Initialize the bot with command prefix '!' and the specified intents
|
43 |
+
bot = commands.Bot(command_prefix='!', intents=intents, help_command=None)
|
44 |
+
|
45 |
+
# Regular expression to parse the prompt parameter
|
46 |
+
PROMPT_REGEX = re.compile(r'prompt\s*=\s*"(.*?)"')
|
47 |
+
|
48 |
+
# Initialize the Gradio client with hf_token
|
49 |
+
GRADIO_CLIENT = Client("Nevaehni/FLUX.1-schnell", hf_token=HF_TOKEN)
|
50 |
+
|
51 |
+
|
52 |
+
@bot.event
|
53 |
+
async def on_ready():
|
54 |
+
"""Event handler triggered when the bot is ready."""
|
55 |
+
logger.info(f'Logged in as {bot.user} (ID: {bot.user.id})')
|
56 |
+
logger.info('------')
|
57 |
+
|
58 |
+
|
59 |
+
def parse_prompt(command: str) -> str:
|
60 |
+
"""
|
61 |
+
Parse the prompt from the command string.
|
62 |
+
|
63 |
+
Args:
|
64 |
+
command (str): The command message content.
|
65 |
+
|
66 |
+
Returns:
|
67 |
+
str: The extracted prompt or an empty string if not found.
|
68 |
+
"""
|
69 |
+
match = PROMPT_REGEX.search(command)
|
70 |
+
if match:
|
71 |
+
return match.group(1).strip()
|
72 |
+
return ''
|
73 |
+
|
74 |
+
|
75 |
+
def create_example_embed() -> Embed:
|
76 |
+
"""
|
77 |
+
Create an embed message with an example !generate command.
|
78 |
+
|
79 |
+
Returns:
|
80 |
+
Embed: The Discord embed object.
|
81 |
+
"""
|
82 |
+
example_command = '!generate prompt="High resolution serene landscape with text \'cucolina\'. seed:1"'
|
83 |
+
embed = Embed(
|
84 |
+
description=f"```\n{example_command}\n```",
|
85 |
+
color=discord.Color.blue()
|
86 |
+
)
|
87 |
+
return embed
|
88 |
+
|
89 |
+
|
90 |
+
@bot.command(name='generate')
|
91 |
+
async def generate(ctx: commands.Context, *, args: str = None):
|
92 |
+
"""
|
93 |
+
Command handler for !generate. Generates content based on the provided prompt.
|
94 |
+
|
95 |
+
Args:
|
96 |
+
ctx (commands.Context): The context in which the command was invoked.
|
97 |
+
args (str, optional): The arguments passed with the command.
|
98 |
+
"""
|
99 |
+
if not args:
|
100 |
+
# No parameters provided, send example command without copy button
|
101 |
+
embed = create_example_embed()
|
102 |
+
await ctx.send(embed=embed)
|
103 |
+
return
|
104 |
+
|
105 |
+
# Parse the prompt from the arguments
|
106 |
+
prompt = parse_prompt(args)
|
107 |
+
|
108 |
+
if not prompt:
|
109 |
+
# Prompt parameter not found or empty
|
110 |
+
await ctx.send("β **Error:** Prompt cannot be empty. Please provide a valid input.")
|
111 |
+
return
|
112 |
+
|
113 |
+
# Acknowledge the command and indicate processing
|
114 |
+
processing_message = await ctx.send("π Generating your content, please wait...")
|
115 |
+
|
116 |
+
try:
|
117 |
+
logger.info(f"Received prompt: {prompt}")
|
118 |
+
|
119 |
+
# Define an asynchronous wrapper for the predict call
|
120 |
+
async def call_predict():
|
121 |
+
return GRADIO_CLIENT.predict(param_0=prompt, api_name="/predict")
|
122 |
+
|
123 |
+
# Set a timeout for the predict call (e.g., 60 seconds)
|
124 |
+
response = await asyncio.wait_for(call_predict(), timeout=60)
|
125 |
+
|
126 |
+
logger.info(f"API response: {response}")
|
127 |
+
|
128 |
+
# **Debugging: Log the actual response**
|
129 |
+
logger.debug(f"API Response: {response}")
|
130 |
+
|
131 |
+
# Reconstruct the exact command used by the user
|
132 |
+
command_used = ctx.message.content.strip()
|
133 |
+
|
134 |
+
# Handle different response structures
|
135 |
+
if isinstance(response, dict):
|
136 |
+
# Check if 'url' key exists
|
137 |
+
url = response.get('url')
|
138 |
+
if url:
|
139 |
+
if isinstance(url, str):
|
140 |
+
# Embed the image if it's an image URL
|
141 |
+
if url.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp')):
|
142 |
+
embed = Embed(title="π¨ Generated Image", color=discord.Color.green())
|
143 |
+
embed.set_image(url=url)
|
144 |
+
# Add prompt as a field with code block
|
145 |
+
embed.add_field(name="Prompt", value=f"```\n{command_used}\n```", inline=False)
|
146 |
+
# Send the embed and mention the user
|
147 |
+
await ctx.send(content=f"{ctx.author.mention}", embed=embed)
|
148 |
+
else:
|
149 |
+
# If not an image, send the URL directly
|
150 |
+
embed = Embed(title="π Generated Content", description=url, color=discord.Color.green())
|
151 |
+
embed.add_field(name="Prompt", value=f"```\n{command_used}\n```", inline=False)
|
152 |
+
await ctx.send(content=f"{ctx.author.mention}", embed=embed)
|
153 |
+
else:
|
154 |
+
# 'url' exists but is not a string
|
155 |
+
await ctx.send("β **Error:** Received an invalid URL format from the API.")
|
156 |
+
else:
|
157 |
+
# 'url' key does not exist
|
158 |
+
await ctx.send("β **Error:** The API response does not contain a 'url' key.")
|
159 |
+
elif isinstance(response, str):
|
160 |
+
# Assume the response is a file path
|
161 |
+
file_path = response
|
162 |
+
if os.path.isfile(file_path):
|
163 |
+
try:
|
164 |
+
# Extract the file name
|
165 |
+
file_name = os.path.basename(file_path)
|
166 |
+
|
167 |
+
# Create a Discord File object
|
168 |
+
discord_file = discord.File(file_path, filename=file_name)
|
169 |
+
|
170 |
+
# Create an embed with the image
|
171 |
+
embed = Embed(title="π¨ Generated Image", color=discord.Color.green())
|
172 |
+
embed.set_image(url=f"attachment://{file_name}")
|
173 |
+
|
174 |
+
# Add prompt as a field with code block
|
175 |
+
embed.add_field(name="Prompt", value=f"```\n{command_used}\n```", inline=False)
|
176 |
+
|
177 |
+
# Send the embed with the file and mention the user
|
178 |
+
await ctx.send(content=f"{ctx.author.mention}", embed=embed, file=discord_file)
|
179 |
+
|
180 |
+
logger.info(f"Sent image from {file_path} to Discord.")
|
181 |
+
except Exception as e:
|
182 |
+
logger.error(f"Failed to send image to Discord: {e}")
|
183 |
+
await ctx.send("β **Error:** Failed to send the generated image to Discord.")
|
184 |
+
else:
|
185 |
+
await ctx.send("β **Error:** The API returned an invalid file path.")
|
186 |
+
elif isinstance(response, list):
|
187 |
+
# Handle list responses if applicable
|
188 |
+
if len(response) > 0 and isinstance(response[0], dict):
|
189 |
+
first_item = response[0]
|
190 |
+
url = first_item.get('url')
|
191 |
+
if url and isinstance(url, str):
|
192 |
+
if url.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp')):
|
193 |
+
embed = Embed(title="π¨ Generated Image", color=discord.Color.green())
|
194 |
+
embed.set_image(url=url)
|
195 |
+
embed.add_field(name="Prompt", value=f"```\n{command_used}\n```", inline=False)
|
196 |
+
await ctx.send(content=f"{ctx.author.mention}", embed=embed)
|
197 |
+
else:
|
198 |
+
embed = Embed(title="π Generated Content", description=url, color=discord.Color.green())
|
199 |
+
embed.add_field(name="Prompt", value=f"```\n{command_used}\n```", inline=False)
|
200 |
+
await ctx.send(content=f"{ctx.author.mention}", embed=embed)
|
201 |
+
else:
|
202 |
+
await ctx.send("β **Error:** Received an invalid URL format from the API.")
|
203 |
+
else:
|
204 |
+
await ctx.send("β **Error:** The API response is an unexpected list structure.")
|
205 |
+
else:
|
206 |
+
# Response is neither dict, str, nor list
|
207 |
+
await ctx.send("β **Error:** Unexpected response format from the API.")
|
208 |
+
except asyncio.TimeoutError:
|
209 |
+
logger.error("API request timed out.")
|
210 |
+
await ctx.send("β° **Error:** The request to the API timed out. Please try again later.")
|
211 |
+
except AppError as e:
|
212 |
+
logger.error(f"API Error: {str(e)}")
|
213 |
+
await ctx.send(f"β **API Error:** {str(e)}")
|
214 |
+
except requests.exceptions.ConnectionError:
|
215 |
+
logger.error("Failed to connect to the API.")
|
216 |
+
await ctx.send("β οΈ **Error:** Failed to connect to the API. Please check your network connection.")
|
217 |
+
except Exception as e:
|
218 |
+
logger.exception("An unexpected error occurred.")
|
219 |
+
await ctx.send(f"β **Error:** An unexpected error occurred: {str(e)}")
|
220 |
+
finally:
|
221 |
+
# Delete the processing message
|
222 |
+
await processing_message.delete()
|
223 |
+
|
224 |
+
|
225 |
+
@bot.event
|
226 |
+
async def on_command_error(ctx: commands.Context, error):
|
227 |
+
"""
|
228 |
+
Global error handler for command errors.
|
229 |
+
|
230 |
+
Args:
|
231 |
+
ctx (commands.Context): The context in which the error occurred.
|
232 |
+
error (Exception): The exception that was raised.
|
233 |
+
"""
|
234 |
+
if isinstance(error, commands.CommandNotFound):
|
235 |
+
await ctx.send("β **Error:** Unknown command. Please use `!generate` to generate content.")
|
236 |
+
elif isinstance(error, commands.CommandOnCooldown):
|
237 |
+
await ctx.send(f"β³ **Please wait {error.retry_after:.2f} seconds before using this command again.**")
|
238 |
+
else:
|
239 |
+
await ctx.send(f"β **Error:** {str(error)}")
|
240 |
+
logger.error(f"Unhandled command error: {str(error)}")
|
241 |
+
|
242 |
+
|
243 |
+
async def handle_root(request):
|
244 |
+
return web.Response(text="DarkMuse GOES VROOOOM", status=200)
|
245 |
+
|
246 |
+
|
247 |
+
async def start_web_server():
|
248 |
+
app = web.Application()
|
249 |
+
app.router.add_get('/', handle_root)
|
250 |
+
runner = web.AppRunner(app)
|
251 |
+
await runner.setup()
|
252 |
+
site = web.TCPSite(runner, '0.0.0.0', 7860)
|
253 |
+
await site.start()
|
254 |
+
|
255 |
+
|
256 |
+
# Run the bot
|
257 |
+
if __name__ == '__main__':
|
258 |
+
try:
|
259 |
+
bot.run(DISCORD_BOT_TOKEN)
|
260 |
+
bot.loop.create_task(start_web_server())
|
261 |
+
except Exception as e:
|
262 |
+
logger.exception(f"Failed to run the bot: {e}")
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
discord.py==2.4.0
|
2 |
+
requests==2.32.3
|
3 |
+
gradio-client==1.5.1
|
4 |
+
aiohttp==3.11.9
|