main.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. import re
  2. import requests
  3. import base64
  4. from fastapi import (
  5. FastAPI,
  6. Request,
  7. Depends,
  8. HTTPException,
  9. status,
  10. UploadFile,
  11. File,
  12. Form,
  13. )
  14. from fastapi.middleware.cors import CORSMiddleware
  15. from constants import ERROR_MESSAGES
  16. from utils.utils import (
  17. get_verified_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 mimetypes
  26. import uuid
  27. import base64
  28. import json
  29. import logging
  30. from config import (
  31. SRC_LOG_LEVELS,
  32. CACHE_DIR,
  33. IMAGE_GENERATION_ENGINE,
  34. ENABLE_IMAGE_GENERATION,
  35. AUTOMATIC1111_BASE_URL,
  36. AUTOMATIC1111_API_AUTH,
  37. COMFYUI_BASE_URL,
  38. COMFYUI_CFG_SCALE,
  39. COMFYUI_SAMPLER,
  40. COMFYUI_SCHEDULER,
  41. COMFYUI_SD3,
  42. IMAGES_OPENAI_API_BASE_URL,
  43. IMAGES_OPENAI_API_KEY,
  44. IMAGE_GENERATION_MODEL,
  45. IMAGE_SIZE,
  46. IMAGE_STEPS,
  47. AppConfig,
  48. )
  49. log = logging.getLogger(__name__)
  50. log.setLevel(SRC_LOG_LEVELS["IMAGES"])
  51. IMAGE_CACHE_DIR = Path(CACHE_DIR).joinpath("./image/generations/")
  52. IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
  53. app = FastAPI()
  54. app.add_middleware(
  55. CORSMiddleware,
  56. allow_origins=["*"],
  57. allow_credentials=True,
  58. allow_methods=["*"],
  59. allow_headers=["*"],
  60. )
  61. app.state.config = AppConfig()
  62. app.state.config.ENGINE = IMAGE_GENERATION_ENGINE
  63. app.state.config.ENABLED = ENABLE_IMAGE_GENERATION
  64. app.state.config.OPENAI_API_BASE_URL = IMAGES_OPENAI_API_BASE_URL
  65. app.state.config.OPENAI_API_KEY = IMAGES_OPENAI_API_KEY
  66. app.state.config.MODEL = IMAGE_GENERATION_MODEL
  67. app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
  68. app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
  69. app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
  70. app.state.config.IMAGE_SIZE = IMAGE_SIZE
  71. app.state.config.IMAGE_STEPS = IMAGE_STEPS
  72. app.state.config.COMFYUI_CFG_SCALE = COMFYUI_CFG_SCALE
  73. app.state.config.COMFYUI_SAMPLER = COMFYUI_SAMPLER
  74. app.state.config.COMFYUI_SCHEDULER = COMFYUI_SCHEDULER
  75. app.state.config.COMFYUI_SD3 = COMFYUI_SD3
  76. def get_automatic1111_api_auth():
  77. if app.state.config.AUTOMATIC1111_API_AUTH == None:
  78. return ""
  79. else:
  80. auth1111_byte_string = app.state.config.AUTOMATIC1111_API_AUTH.encode("utf-8")
  81. auth1111_base64_encoded_bytes = base64.b64encode(auth1111_byte_string)
  82. auth1111_base64_encoded_string = auth1111_base64_encoded_bytes.decode("utf-8")
  83. return f"Basic {auth1111_base64_encoded_string}"
  84. @app.get("/config")
  85. async def get_config(request: Request, user=Depends(get_admin_user)):
  86. return {
  87. "engine": app.state.config.ENGINE,
  88. "enabled": app.state.config.ENABLED,
  89. }
  90. class ConfigUpdateForm(BaseModel):
  91. engine: str
  92. enabled: bool
  93. @app.post("/config/update")
  94. async def update_config(form_data: ConfigUpdateForm, user=Depends(get_admin_user)):
  95. app.state.config.ENGINE = form_data.engine
  96. app.state.config.ENABLED = form_data.enabled
  97. return {
  98. "engine": app.state.config.ENGINE,
  99. "enabled": app.state.config.ENABLED,
  100. }
  101. class EngineUrlUpdateForm(BaseModel):
  102. AUTOMATIC1111_BASE_URL: Optional[str] = None
  103. AUTOMATIC1111_API_AUTH: Optional[str] = None
  104. COMFYUI_BASE_URL: Optional[str] = None
  105. @app.get("/url")
  106. async def get_engine_url(user=Depends(get_admin_user)):
  107. return {
  108. "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
  109. "AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
  110. "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
  111. }
  112. @app.post("/url/update")
  113. async def update_engine_url(
  114. form_data: EngineUrlUpdateForm, user=Depends(get_admin_user)
  115. ):
  116. if form_data.AUTOMATIC1111_BASE_URL == None:
  117. app.state.config.AUTOMATIC1111_BASE_URL = AUTOMATIC1111_BASE_URL
  118. else:
  119. url = form_data.AUTOMATIC1111_BASE_URL.strip("/")
  120. try:
  121. r = requests.head(url)
  122. app.state.config.AUTOMATIC1111_BASE_URL = url
  123. except Exception as e:
  124. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  125. if form_data.COMFYUI_BASE_URL == None:
  126. app.state.config.COMFYUI_BASE_URL = COMFYUI_BASE_URL
  127. else:
  128. url = form_data.COMFYUI_BASE_URL.strip("/")
  129. try:
  130. r = requests.head(url)
  131. app.state.config.COMFYUI_BASE_URL = url
  132. except Exception as e:
  133. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  134. if form_data.AUTOMATIC1111_API_AUTH == None:
  135. app.state.config.AUTOMATIC1111_API_AUTH = AUTOMATIC1111_API_AUTH
  136. else:
  137. app.state.config.AUTOMATIC1111_API_AUTH = form_data.AUTOMATIC1111_API_AUTH
  138. return {
  139. "AUTOMATIC1111_BASE_URL": app.state.config.AUTOMATIC1111_BASE_URL,
  140. "AUTOMATIC1111_API_AUTH": app.state.config.AUTOMATIC1111_API_AUTH,
  141. "COMFYUI_BASE_URL": app.state.config.COMFYUI_BASE_URL,
  142. "status": True,
  143. }
  144. class OpenAIConfigUpdateForm(BaseModel):
  145. url: str
  146. key: str
  147. @app.get("/openai/config")
  148. async def get_openai_config(user=Depends(get_admin_user)):
  149. return {
  150. "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
  151. "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
  152. }
  153. @app.post("/openai/config/update")
  154. async def update_openai_config(
  155. form_data: OpenAIConfigUpdateForm, user=Depends(get_admin_user)
  156. ):
  157. if form_data.key == "":
  158. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.API_KEY_NOT_FOUND)
  159. app.state.config.OPENAI_API_BASE_URL = form_data.url
  160. app.state.config.OPENAI_API_KEY = form_data.key
  161. return {
  162. "status": True,
  163. "OPENAI_API_BASE_URL": app.state.config.OPENAI_API_BASE_URL,
  164. "OPENAI_API_KEY": app.state.config.OPENAI_API_KEY,
  165. }
  166. class ImageSizeUpdateForm(BaseModel):
  167. size: str
  168. @app.get("/size")
  169. async def get_image_size(user=Depends(get_admin_user)):
  170. return {"IMAGE_SIZE": app.state.config.IMAGE_SIZE}
  171. @app.post("/size/update")
  172. async def update_image_size(
  173. form_data: ImageSizeUpdateForm, user=Depends(get_admin_user)
  174. ):
  175. pattern = r"^\d+x\d+$" # Regular expression pattern
  176. if re.match(pattern, form_data.size):
  177. app.state.config.IMAGE_SIZE = form_data.size
  178. return {
  179. "IMAGE_SIZE": app.state.config.IMAGE_SIZE,
  180. "status": True,
  181. }
  182. else:
  183. raise HTTPException(
  184. status_code=400,
  185. detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 512x512)."),
  186. )
  187. class ImageStepsUpdateForm(BaseModel):
  188. steps: int
  189. @app.get("/steps")
  190. async def get_image_size(user=Depends(get_admin_user)):
  191. return {"IMAGE_STEPS": app.state.config.IMAGE_STEPS}
  192. @app.post("/steps/update")
  193. async def update_image_size(
  194. form_data: ImageStepsUpdateForm, user=Depends(get_admin_user)
  195. ):
  196. if form_data.steps >= 0:
  197. app.state.config.IMAGE_STEPS = form_data.steps
  198. return {
  199. "IMAGE_STEPS": app.state.config.IMAGE_STEPS,
  200. "status": True,
  201. }
  202. else:
  203. raise HTTPException(
  204. status_code=400,
  205. detail=ERROR_MESSAGES.INCORRECT_FORMAT(" (e.g., 50)."),
  206. )
  207. @app.get("/models")
  208. def get_models(user=Depends(get_verified_user)):
  209. try:
  210. if app.state.config.ENGINE == "openai":
  211. return [
  212. {"id": "dall-e-2", "name": "DALL·E 2"},
  213. {"id": "dall-e-3", "name": "DALL·E 3"},
  214. ]
  215. elif app.state.config.ENGINE == "comfyui":
  216. r = requests.get(url=f"{app.state.config.COMFYUI_BASE_URL}/object_info")
  217. info = r.json()
  218. return list(
  219. map(
  220. lambda model: {"id": model, "name": model},
  221. info["CheckpointLoaderSimple"]["input"]["required"]["ckpt_name"][0],
  222. )
  223. )
  224. else:
  225. r = requests.get(
  226. url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/sd-models",
  227. headers={"authorization": get_automatic1111_api_auth()},
  228. )
  229. models = r.json()
  230. return list(
  231. map(
  232. lambda model: {"id": model["title"], "name": model["model_name"]},
  233. models,
  234. )
  235. )
  236. except Exception as e:
  237. app.state.config.ENABLED = False
  238. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  239. @app.get("/models/default")
  240. async def get_default_model(user=Depends(get_admin_user)):
  241. try:
  242. if app.state.config.ENGINE == "openai":
  243. return {
  244. "model": (
  245. app.state.config.MODEL if app.state.config.MODEL else "dall-e-2"
  246. )
  247. }
  248. elif app.state.config.ENGINE == "comfyui":
  249. return {"model": (app.state.config.MODEL if app.state.config.MODEL else "")}
  250. else:
  251. r = requests.get(
  252. url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
  253. headers={"authorization": get_automatic1111_api_auth()},
  254. )
  255. options = r.json()
  256. return {"model": options["sd_model_checkpoint"]}
  257. except Exception as e:
  258. app.state.config.ENABLED = False
  259. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(e))
  260. class UpdateModelForm(BaseModel):
  261. model: str
  262. def set_model_handler(model: str):
  263. if app.state.config.ENGINE in ["openai", "comfyui"]:
  264. app.state.config.MODEL = model
  265. return app.state.config.MODEL
  266. else:
  267. api_auth = get_automatic1111_api_auth()
  268. r = requests.get(
  269. url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
  270. headers={"authorization": api_auth},
  271. )
  272. options = r.json()
  273. if model != options["sd_model_checkpoint"]:
  274. options["sd_model_checkpoint"] = model
  275. r = requests.post(
  276. url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/options",
  277. json=options,
  278. headers={"authorization": api_auth},
  279. )
  280. return options
  281. @app.post("/models/default/update")
  282. def update_default_model(
  283. form_data: UpdateModelForm,
  284. user=Depends(get_verified_user),
  285. ):
  286. return set_model_handler(form_data.model)
  287. class GenerateImageForm(BaseModel):
  288. model: Optional[str] = None
  289. prompt: str
  290. n: int = 1
  291. size: Optional[str] = None
  292. negative_prompt: Optional[str] = None
  293. def save_b64_image(b64_str):
  294. try:
  295. image_id = str(uuid.uuid4())
  296. if "," in b64_str:
  297. header, encoded = b64_str.split(",", 1)
  298. mime_type = header.split(";")[0]
  299. img_data = base64.b64decode(encoded)
  300. image_format = mimetypes.guess_extension(mime_type)
  301. image_filename = f"{image_id}{image_format}"
  302. file_path = IMAGE_CACHE_DIR / f"{image_filename}"
  303. with open(file_path, "wb") as f:
  304. f.write(img_data)
  305. return image_filename
  306. else:
  307. image_filename = f"{image_id}.png"
  308. file_path = IMAGE_CACHE_DIR.joinpath(image_filename)
  309. img_data = base64.b64decode(b64_str)
  310. # Write the image data to a file
  311. with open(file_path, "wb") as f:
  312. f.write(img_data)
  313. return image_filename
  314. except Exception as e:
  315. log.exception(f"Error saving image: {e}")
  316. return None
  317. def save_url_image(url):
  318. image_id = str(uuid.uuid4())
  319. try:
  320. r = requests.get(url)
  321. r.raise_for_status()
  322. if r.headers["content-type"].split("/")[0] == "image":
  323. mime_type = r.headers["content-type"]
  324. image_format = mimetypes.guess_extension(mime_type)
  325. if not image_format:
  326. raise ValueError("Could not determine image type from MIME type")
  327. image_filename = f"{image_id}{image_format}"
  328. file_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}")
  329. with open(file_path, "wb") as image_file:
  330. for chunk in r.iter_content(chunk_size=8192):
  331. image_file.write(chunk)
  332. return image_filename
  333. else:
  334. log.error(f"Url does not point to an image.")
  335. return None
  336. except Exception as e:
  337. log.exception(f"Error saving image: {e}")
  338. return None
  339. @app.post("/generations")
  340. def generate_image(
  341. form_data: GenerateImageForm,
  342. user=Depends(get_verified_user),
  343. ):
  344. width, height = tuple(map(int, app.state.config.IMAGE_SIZE.split("x")))
  345. r = None
  346. try:
  347. if app.state.config.ENGINE == "openai":
  348. headers = {}
  349. headers["Authorization"] = f"Bearer {app.state.config.OPENAI_API_KEY}"
  350. headers["Content-Type"] = "application/json"
  351. data = {
  352. "model": (
  353. app.state.config.MODEL
  354. if app.state.config.MODEL != ""
  355. else "dall-e-2"
  356. ),
  357. "prompt": form_data.prompt,
  358. "n": form_data.n,
  359. "size": (
  360. form_data.size if form_data.size else app.state.config.IMAGE_SIZE
  361. ),
  362. "response_format": "b64_json",
  363. }
  364. r = requests.post(
  365. url=f"{app.state.config.OPENAI_API_BASE_URL}/images/generations",
  366. json=data,
  367. headers=headers,
  368. )
  369. r.raise_for_status()
  370. res = r.json()
  371. images = []
  372. for image in res["data"]:
  373. image_filename = save_b64_image(image["b64_json"])
  374. images.append({"url": f"/cache/image/generations/{image_filename}"})
  375. file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
  376. with open(file_body_path, "w") as f:
  377. json.dump(data, f)
  378. return images
  379. elif app.state.config.ENGINE == "comfyui":
  380. data = {
  381. "prompt": form_data.prompt,
  382. "width": width,
  383. "height": height,
  384. "n": form_data.n,
  385. }
  386. if app.state.config.IMAGE_STEPS is not None:
  387. data["steps"] = app.state.config.IMAGE_STEPS
  388. if form_data.negative_prompt is not None:
  389. data["negative_prompt"] = form_data.negative_prompt
  390. if app.state.config.COMFYUI_CFG_SCALE:
  391. data["cfg_scale"] = app.state.config.COMFYUI_CFG_SCALE
  392. if app.state.config.COMFYUI_SAMPLER is not None:
  393. data["sampler"] = app.state.config.COMFYUI_SAMPLER
  394. if app.state.config.COMFYUI_SCHEDULER is not None:
  395. data["scheduler"] = app.state.config.COMFYUI_SCHEDULER
  396. if app.state.config.COMFYUI_SD3 is not None:
  397. data["sd3"] = app.state.config.COMFYUI_SD3
  398. data = ImageGenerationPayload(**data)
  399. res = comfyui_generate_image(
  400. app.state.config.MODEL,
  401. data,
  402. user.id,
  403. app.state.config.COMFYUI_BASE_URL,
  404. )
  405. log.debug(f"res: {res}")
  406. images = []
  407. for image in res["data"]:
  408. image_filename = save_url_image(image["url"])
  409. images.append({"url": f"/cache/image/generations/{image_filename}"})
  410. file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
  411. with open(file_body_path, "w") as f:
  412. json.dump(data.model_dump(exclude_none=True), f)
  413. log.debug(f"images: {images}")
  414. return images
  415. else:
  416. if form_data.model:
  417. set_model_handler(form_data.model)
  418. data = {
  419. "prompt": form_data.prompt,
  420. "batch_size": form_data.n,
  421. "width": width,
  422. "height": height,
  423. }
  424. if app.state.config.IMAGE_STEPS is not None:
  425. data["steps"] = app.state.config.IMAGE_STEPS
  426. if form_data.negative_prompt is not None:
  427. data["negative_prompt"] = form_data.negative_prompt
  428. r = requests.post(
  429. url=f"{app.state.config.AUTOMATIC1111_BASE_URL}/sdapi/v1/txt2img",
  430. json=data,
  431. headers={"authorization": get_automatic1111_api_auth()},
  432. )
  433. res = r.json()
  434. log.debug(f"res: {res}")
  435. images = []
  436. for image in res["images"]:
  437. image_filename = save_b64_image(image)
  438. images.append({"url": f"/cache/image/generations/{image_filename}"})
  439. file_body_path = IMAGE_CACHE_DIR.joinpath(f"{image_filename}.json")
  440. with open(file_body_path, "w") as f:
  441. json.dump({**data, "info": res["info"]}, f)
  442. return images
  443. except Exception as e:
  444. error = e
  445. if r != None:
  446. data = r.json()
  447. if "error" in data:
  448. error = data["error"]["message"]
  449. raise HTTPException(status_code=400, detail=ERROR_MESSAGES.DEFAULT(error))