diff --git a/CHANGELOG.md b/CHANGELOG.md index 431ce17bc..08212fbbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Change Log for SD.Next -## Update for 2026-03-08 +## Update for 2026-03-09 -### Highlights for 2026-03-08 +### Highlights for 2026-03-09 This release brings massive code refactoring to modernize codebase and removal of some obsolete features. Leaner & Faster! And since its a bit quieter period when it comes to new models, so we have two deep fine-tunes: *FireRed-Image-Edit* and *SkyWorks-UniPic-3* @@ -11,7 +11,7 @@ But also many smaller quality-of-life improvements - for full details, see [Chan [ReadMe](https://github.com/vladmandic/automatic/blob/master/README.md) | [ChangeLog](https://github.com/vladmandic/automatic/blob/master/CHANGELOG.md) | [Docs](https://vladmandic.github.io/sdnext-docs/) | [WiKi](https://github.com/vladmandic/automatic/wiki) | [Discord](https://discord.com/invite/sd-next-federal-batch-inspectors-1101998836328697867) | [Sponsor](https://github.com/sponsors/vladmandic) -### Details for 2026-03-08 +### Details for 2026-03-09 - **Models** - [Google Flash 3.1 Image](https://ai.google.dev/gemini-api/docs/models/gemini-3-flash-preview) a.k.a. *Nano Banana 2* @@ -47,6 +47,8 @@ But also many smaller quality-of-life improvements - for full details, see [Chan - **themes** add *CTD-NT64Light*, *CTD-NT64Medium* and *CTD-NT64Dark*, thanks @resonantsky - **themes** add *Vlad-Neomorph* - **gallery** add option to auto-refresh gallery, thanks @awsr +- **API** + - new `/sdapi/v1/upload` endpoint with support for both POST with form-data or PUT using raw-bytes - **Internal** - `python==3.13` full support - `python==3.14` initial support diff --git a/modules/api/api.py b/modules/api/api.py index 90c432101..fa61b8365 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -125,6 +125,10 @@ class Api: from modules.civitai import api_civitai api_civitai.register_api() + # upload api + from modules.api import upload + upload.register_api() + def add_api_route(self, path: str, fn, auth: bool = True, **kwargs): if auth and self.credentials: deps = list(kwargs.get('dependencies', [])) diff --git a/modules/api/endpoints.py b/modules/api/endpoints.py index 763cb2aa6..f6eccd0ca 100644 --- a/modules/api/endpoints.py +++ b/modules/api/endpoints.py @@ -3,7 +3,6 @@ from modules import shared from modules.api import models, helpers - def get_samplers(): from modules import sd_samplers_diffusers all_samplers = [] diff --git a/modules/api/models.py b/modules/api/models.py index 482373bb0..09870d8e2 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -504,9 +504,9 @@ def create_model_from_signature(func: Callable, model_name: str, base_model: typ extra = 'allow' if varkw else 'ignore' config = CustomConfig if base_model == BaseModel: - create_model_args = {'__config__': config} + create_model_args = {'__config__': config} else: - create_model_args = {'__base__': base_model} + create_model_args = {'__base__': base_model} model = create_model( model_name, diff --git a/modules/api/upload.py b/modules/api/upload.py new file mode 100644 index 000000000..9133c7176 --- /dev/null +++ b/modules/api/upload.py @@ -0,0 +1,80 @@ +import os +import tempfile +from pathlib import Path +from pydantic import BaseModel +from fastapi import Request, Header, UploadFile, Form +from fastapi.exceptions import HTTPException +from modules import paths +from modules.logger import log +from modules.images import FilenameGenerator + + +""" +new endpoint: /sdapi/v1/upload +- if path is not given, file fill be uploaded to system temp folder +- if path is given, its considered as relative to sdnext root (datadir) and must exist +- absolute paths or paths outside of sdnext root are not allowed + +example using post with formdata: +> curl -X POST "http://localhost:7860/sdapi/v1/upload" -F "file=@/home/vlado/dev/sdnext/config.json" -F overwrite=true -F path=data + +example using put with raw bytes: +> curl -X PUT "http://localhost:7860/sdapi/v1/upload" -T config.json -H "filename:config.json" -H "path:data/" -H "overwrite:true" +""" + +class ResUpload(BaseModel): + input: str + output: str + mime: str + size: int + overwrite: bool + + +def check_file(filename, path, overwrite): + namegen = FilenameGenerator() + if len(path) > 0 and (os.path.isabs(path) or not os.path.isdir(path)): + raise HTTPException(status_code=400, detail="Invalid path") + fn = os.path.join(path, filename) + fn = namegen.sanitize(fn) + if Path(fn).parent == Path('.'): # just filename, no path + fn = os.path.join(tempfile.gettempdir(), fn) + else: + fn = os.path.join(paths.data_path, fn) + if os.path.exists(fn) and len(overwrite) == 0: + raise HTTPException(status_code=400, detail="File exists") + return fn + +def put_upload(request: Request, + filename: str = Header(''), + filetype: str = Header('application/octet-stream'), + overwrite: str = Header(''), + path: str = Header('') + ) -> ResUpload: + fn = check_file(filename, path, overwrite) + try: + from asyncio import run + content = run(request.body()) + with open(fn, 'wb') as f: + f.write(content) + res = ResUpload(input=filename, output=fn, mime=filetype, size=len(content), overwrite=len(overwrite) > 0) + log.trace(f'API upload: {res.dict()}') + return res + except Exception as e: + raise HTTPException(status_code=400, detail="Upload failed") from e + +def post_upload(file: UploadFile, overwrite: str = Form(''), path: str = Form('')) -> ResUpload: + fn = check_file(file.filename, path, overwrite) + try: + content = file.file.read() + with open(fn, 'wb') as f: + f.write(content) + res = ResUpload(input=file.filename, output=fn, mime=file.content_type, size=len(content), overwrite=len(overwrite) > 0) + log.trace(f'API upload: {res.dict()}') + return res + except Exception as e: + raise HTTPException(status_code=400, detail="Upload failed") from e + +def register_api(): + from modules.shared import api + api.add_api_route("/sdapi/v1/upload", post_upload, methods=["POST"], response_model=ResUpload, tags=["Upload"]) + api.add_api_route("/sdapi/v1/upload", put_upload, methods=["PUT"], response_model=ResUpload, tags=["Upload"]) diff --git a/modules/image/namegen.py b/modules/image/namegen.py index fc4737112..4432b2595 100644 --- a/modules/image/namegen.py +++ b/modules/image/namegen.py @@ -66,9 +66,10 @@ class FilenameGenerator: } default_time_format = '%Y%m%d%H%M%S' - def __init__(self, p, seed, prompt, image=None, grid=False, width=None, height=None): + def __init__(self, p=None, seed:int=-1, prompt:str='', image=None, grid=False, width=None, height=None): if p is None: debug_log('Filename generator init skip') + return else: debug_log(f'Filename generator init: seed={seed} prompt="{prompt}"') self.p = p