main.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. import re
  2. import requests
  3. from fastapi import (
  4. FastAPI,
  5. Request,
  6. Depends,
  7. HTTPException,
  8. status,
  9. UploadFile,
  10. File,
  11. Form,
  12. )
  13. from fastapi.middleware.cors import CORSMiddleware
  14. from faster_whisper import WhisperModel
  15. from constants import ERROR_MESSAGES
  16. from utils.utils import (
  17. get_current_user,
  18. get_admin_user,
  19. )
  20. from apps.images.utils.comfyui import ImageGenerationPayload, comfyui_generate_image
  21. from utils.misc import calculate_sha256
  22. from typing import Optional
  23. from pydantic import BaseModel
  24. from pathlib import Path
  25. import uuid
  26. import base64
  27. import json
  28. import logging
  29. from config import SRC_LOG_LEVELS, CACHE_DIR, AUTOMATIC1111_BASE_URL, COMFYUI_BASE_URL
  30. log = logging.getLogger(__name__)
  31. log.setLevel(SRC_LOG_LEVELS["IMAGES"])
  32. IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
  33. IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
  34. app = FastAPI()
  35. app.add_middleware(
  36. CORSMiddleware,
  37. allow_origins=["*"],
  38. allow_credentials=True,
  39. allow_methods=["*"],
  40. allow_headers=["*"],
  41. )
  42. app.state.ENGINE = ""
  43. app.state.ENABLED = False
  44. app.state.OPENAI_API_KEY = ""
  45. app.state.MODEL = ""
  46. app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
  47. app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
  48. app.state.IMAGE_SIZE = "512x512"
  49. app.state.IMAGE_STEPS = 50
  50. @app.get("/config")
  51. async def get_config(request: Request, user=Depends(get_admin_user)):
  52. return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
  53. class ConfigUpdateForm(BaseModel):
  54. engine: str
  55. enabled: bool
  56. @app.post("/config/update")
  57. async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
  58. app.state.ENGINE = form_data.engine
  59. app.state.ENABLED = form_data.enabled
  60. return {"engine": app.state.ENGINE, "enabled": app.state.ENABLED}
  61. class EngineUrlUpdateForm(BaseModel):
  62. AUTOMATIC1111_BASE_URL: Optional[str] = None
  63. COMFYUI_BASE_URL: Optional[str] = None
  64. @app.get("/url")
  65. async def get_engine_url(user=Depends(get_admin_user)):
  66. return {
  67. "AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
  68. "COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
  69. }
  70. @app.post("/url/update")
  71. async def update_engine_url(
  72. form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
  73. ):
  74. if form_data.AUTOMATIC1111_BASE_URL == None:
  75. app.state.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
  76. else:
  77. url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
  78. try:
  79. r = requests.head(url)
  80. app.state.AUTOMATIC1111_BASE_URL = url
  81. except Exception as e:
  82. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  83. if form_data.COMFYUI_BASE_URL == None:
  84. app.state.COMFYUI_BASE_URL = COMFYUI_BASE_URL
  85. else:
  86. url = form_data.COMFYUI_BASE_URL.strip("/")
  87. try:
  88. r = requests.head(url)
  89. app.state.COMFYUI_BASE_URL = url
  90. except Exception as e:
  91. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  92. return {
  93. "AUTOMATIC1111_BASE_URL": app.state.AUTOMATIC1111_BASE_URL,
  94. "COMFYUI_BASE_URL": app.state.COMFYUI_BASE_URL,
  95. "status": True,
  96. }
  97. class OpenAIKeyUpdateForm(BaseModel):
  98. key: str
  99. @app.get("/key")
  100. async def get_openai_key(user=Depends(get_admin_user)):
  101. return {"OPENAI_API_KEY": app.state.OPENAI_API_KEY}
  102. @app.post("/key/update")
  103. async def update_openai_key(
  104. form_data: OpenAIKeyUpdateForm, user=Depends(get_admin_user)
  105. ):
  106. if form_data.key == "":
  107. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
  108. app.state.OPENAI_API_KEY = form_data.key
  109. return {
  110. "OPENAI_API_KEY": app.state.OPENAI_API_KEY,
  111. "status": True,
  112. }
  113. class ImageSizeUpdateForm(BaseModel):
  114. size: str
  115. @app.get("/size")
  116. async def get_image_size(user=Depends(get_admin_user)):
  117. return {"IMAGE_SIZE": app.state.IMAGE_SIZE}
  118. @app.post("/size/update")
  119. async def update_image_size(
  120. form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
  121. ):
  122. pattern = r"^\d+x\d+$" # Regular expression pattern
  123. if re.match(pattern, form_data.size):
  124. app.state.IMAGE_SIZE = form_data.size
  125. return {
  126. "IMAGE_SIZE": app.state.IMAGE_SIZE,
  127. "status": True,
  128. }
  129. else:
  130. raise HTTPException(
  131. status_code=400,
  132. detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
  133. )
  134. class ImageStepsUpdateForm(BaseModel):
  135. steps: int
  136. @app.get("/steps")
  137. async def get_image_size(user=Depends(get_admin_user)):
  138. return {"IMAGE_STEPS": app.state.IMAGE_STEPS}
  139. @app.post("/steps/update")
  140. async def update_image_size(
  141. form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
  142. ):
  143. if form_data.steps >= 0:
  144. app.state.IMAGE_STEPS = form_data.steps
  145. return {
  146. "IMAGE_STEPS": app.state.IMAGE_STEPS,
  147. "status": True,
  148. }
  149. else:
  150. raise HTTPException(
  151. status_code=400,
  152. detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
  153. )
  154. @app.get("/models")
  155. def get_models(user=Depends(get_current_user)):
  156. try:
  157. if app.state.ENGINE == "openai":
  158. return [
  159. {"id": "dall-e-2", "name": "DALL·E 2"},
  160. {"id": "dall-e-3", "name": "DALL·E 3"},
  161. ]
  162. elif app.state.ENGINE == "comfyui":
  163. r = requests.get(url=f"{app.state.COMFYUI_BASE_URL}/object_info")
  164. info = r.json()
  165. return list(
  166. map(
  167. lambda model: {"id": model, "name": model},
  168. info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
  169. )
  170. )
  171. else:
  172. r = requests.get(
  173. url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models"
  174. )
  175. models = r.json()
  176. return list(
  177. map(
  178. lambda model: {"id": model["title"], "name": model["model_name"]},
  179. models,
  180. )
  181. )
  182. except Exception as e:
  183. app.state.ENABLED = False
  184. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  185. @app.get("/models/default")
  186. async def get_default_model(user=Depends(get_admin_user)):
  187. try:
  188. if app.state.ENGINE == "openai":
  189. return {"model": app.state.MODEL if app.state.MODEL else "dall-e-2"}
  190. elif app.state.ENGINE == "comfyui":
  191. return {"model": app.state.MODEL if app.state.MODEL else ""}
  192. else:
  193. r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
  194. options = r.json()
  195. return {"model": options["sd_model_checkpoint"]}
  196. except Exception as e:
  197. app.state.ENABLED = False
  198. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  199. class UpdateModelForm(BaseModel):
  200. model: str
  201. def set_model_handler(model: str):
  202. if app.state.ENGINE == "openai":
  203. app.state.MODEL = model
  204. return app.state.MODEL
  205. if app.state.ENGINE == "comfyui":
  206. app.state.MODEL = model
  207. return app.state.MODEL
  208. else:
  209. r = requests.get(url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options")
  210. options = r.json()
  211. if model != options["sd_model_checkpoint"]:
  212. options["sd_model_checkpoint"] = model
  213. r = requests.post(
  214. url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/options", json=options
  215. )
  216. return options
  217. @app.post("/models/default/update")
  218. def update_default_model(
  219. form_data: UpdateModelForm,
  220. user=Depends(get_current_user),
  221. ):
  222. return set_model_handler(form_data.model)
  223. class GenerateImageForm(BaseModel):
  224. model: Optional[str] = None
  225. prompt: str
  226. n: int = 1
  227. size: Optional[str] = None
  228. negative_prompt: Optional[str] = None
  229. def save_b64_image(b64_str):
  230. image_id = str(uuid.uuid4())
  231. file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
  232. try:
  233. # Split the base64 string to get the actual image data
  234. img_data = base64.b64decode(b64_str)
  235. # Write the image data to a file
  236. with open(file_path, "wb") as f:
  237. f.write(img_data)
  238. return image_id
  239. except Exception as e:
  240. log.error(f"Error saving image: {e}")
  241. return None
  242. def save_url_image(url):
  243. image_id = str(uuid.uuid4())
  244. file_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.png")
  245. try:
  246. r = requests.get(url)
  247. r.raise_for_status()
  248. with open(file_path, "wb") as image_file:
  249. image_file.write(r.content)
  250. return image_id
  251. except Exception as e:
  252. log.exception(f"Error saving image: {e}")
  253. return None
  254. @app.post("/generations")
  255. def generate_image(
  256. form_data: GenerateImageForm,
  257. user=Depends(get_current_user),
  258. ):
  259. width, height = tuple(map(int, app.state.IMAGE_SIZE.split("x")))
  260. r = None
  261. try:
  262. if app.state.ENGINE == "openai":
  263. headers = {}
  264. headers["Authorization"] = f"Bearer {app.state.OPENAI_API_KEY}"
  265. headers["Content-Type"] = "application/json"
  266. data = {
  267. "model": app.state.MODEL if app.state.MODEL != "" else "dall-e-2",
  268. "prompt": form_data.prompt,
  269. "n": form_data.n,
  270. "size": form_data.size if form_data.size else app.state.IMAGE_SIZE,
  271. "response_format": "b64_json",
  272. }
  273. r = requests.post(
  274. url=f"https://api.openai.com/v1/images/generations",
  275. json=data,
  276. headers=headers,
  277. )
  278. r.raise_for_status()
  279. res = r.json()
  280. images = []
  281. for image in res["data"]:
  282. image_id = save_b64_image(image["b64_json"])
  283. images.append({"url": f"/cache/image/generations/{image_id}.png"})
  284. file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
  285. with open(file_body_path, "w") as f:
  286. json.dump(data, f)
  287. return images
  288. elif app.state.ENGINE == "comfyui":
  289. data = {
  290. "prompt": form_data.prompt,
  291. "width": width,
  292. "height": height,
  293. "n": form_data.n,
  294. }
  295. if app.state.IMAGE_STEPS != None:
  296. data["steps"] = app.state.IMAGE_STEPS
  297. if form_data.negative_prompt != None:
  298. data["negative_prompt"] = form_data.negative_prompt
  299. data = ImageGenerationPayload(**data)
  300. res = comfyui_generate_image(
  301. app.state.MODEL,
  302. data,
  303. user.id,
  304. app.state.COMFYUI_BASE_URL,
  305. )
  306. log.debug(f"res: {res}")
  307. images = []
  308. for image in res["data"]:
  309. image_id = save_url_image(image["url"])
  310. images.append({"url": f"/cache/image/generations/{image_id}.png"})
  311. file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
  312. with open(file_body_path, "w") as f:
  313. json.dump(data.model_dump(exclude_none=True), f)
  314. log.debug(f"images: {images}")
  315. return images
  316. else:
  317. if form_data.model:
  318. set_model_handler(form_data.model)
  319. data = {
  320. "prompt": form_data.prompt,
  321. "batch_size": form_data.n,
  322. "width": width,
  323. "height": height,
  324. }
  325. if app.state.IMAGE_STEPS != None:
  326. data["steps"] = app.state.IMAGE_STEPS
  327. if form_data.negative_prompt != None:
  328. data["negative_prompt"] = form_data.negative_prompt
  329. r = requests.post(
  330. url=f"{app.state.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
  331. json=data,
  332. )
  333. res = r.json()
  334. log.debug(f"res: {res}")
  335. images = []
  336. for image in res["images"]:
  337. image_id = save_b64_image(image)
  338. images.append({"url": f"/cache/image/generations/{image_id}.png"})
  339. file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_id}.json")
  340. with open(file_body_path, "w") as f:
  341. json.dump({**data, "info": res["info"]}, f)
  342. return images
  343. except Exception as e:
  344. error = e
  345. if r != None:
  346. data = r.json()
  347. if "error" in data:
  348. error = data["error"]["message"]
  349. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))