main.py 15 KB

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