Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 63 additions & 11 deletions src/app/src/renderer/ModelOptionsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const ModelOptionsModal: React.FC<SettingsModalProps> = ({ isOpen, onCancel, onS
const [exportError, setExportError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [isModelNameCopied, setIsModelNameCopied] = useState(false);
const [upscalerModels, setUpscalerModels] = useState<string[]>([]);
const cardRef = useRef<HTMLDivElement>(null);
const modelNameCopyTimeoutIdRef = useRef<ReturnType<typeof setTimeout> | null>(null);

Expand All @@ -115,6 +116,7 @@ const ModelOptionsModal: React.FC<SettingsModalProps> = ({ isOpen, onCancel, onS
setModelName(model ?? "");
setModelUrl("");
setOptions(undefined);
setUpscalerModels([]);
void ensureSystemInfoLoaded();

const fetchOptions = async () => {
Expand Down Expand Up @@ -153,6 +155,24 @@ const ModelOptionsModal: React.FC<SettingsModalProps> = ({ isOpen, onCancel, onS
const recipe = data.recipe as string;
const recipeOptions = data.recipe_options ?? {};
setOptions(apiToRecipeOptions(recipe, recipeOptions));

// Fetch upscaler models for sd-cpp recipe
if (recipe === 'sd-cpp' && isMounted) {
try {
const modelsRes = await serverFetch('/models?show_all=true');
if (modelsRes.ok) {
const allModels = await modelsRes.json();
const models = Array.isArray(allModels.data) ? allModels.data : Object.entries(allModels).map(([name, info]) => ({ id: name, ...(info as object) }));
const upscalers = models
.filter((m: any) => (m.labels || []).includes('upscaling') && m.recipe === 'sd-cpp')
.map((m: any) => m.id)
.sort();
setUpscalerModels(upscalers);
}
} catch (error) {
console.error('Failed to fetch upscaler models:', error);
}
}
} catch (error) {
console.error('Failed to load options:', error);
if (isMounted) {
Expand Down Expand Up @@ -540,6 +560,7 @@ const ModelOptionsModal: React.FC<SettingsModalProps> = ({ isOpen, onCancel, onS
const renderStringField = (key: string) => {
const def = getOptionDefinition(key);
if (!def || def.type !== 'string' || def.isBackendOption) return null;
if (key === 'upscaleModel') return null; // Rendered as dropdown below

const value = getOptionValue<string>(key);
if (value === undefined) return null;
Expand Down Expand Up @@ -586,6 +607,32 @@ const ModelOptionsModal: React.FC<SettingsModalProps> = ({ isOpen, onCancel, onS
);
};

// Render the upscale model selector for sd-cpp recipe
const renderUpscaleModelSelector = (): React.ReactNode => {
if (options?.recipe !== 'sd-cpp' || upscalerModels.length === 0) return null;

const def = getOptionDefinition('upscaleModel');
const effectiveValue = getOptionValue<string>('upscaleModel') ?? '';

return (
<div className="form-section" key="upscaleModel">
<label className="form-label" title={def!.description}>
{def!.label}
</label>
<select
className="form-input form-select"
value={effectiveValue}
onChange={(e) => handleStringChange('upscaleModel', e.target.value)}
>
<option value="">None</option>
{upscalerModels.map((model) => (
<option key={model} value={model}>{model}</option>
))}
</select>
</div>
);
};

// Render a boolean field (checkbox)
const renderBooleanField = (key: string) => {
const def = getOptionDefinition(key);
Expand Down Expand Up @@ -630,22 +677,27 @@ const ModelOptionsModal: React.FC<SettingsModalProps> = ({ isOpen, onCancel, onS

// Render all options for the current recipe
const renderOptions = () => {
return availableOptions.map(key => {
const upscaledIdx = availableOptions.indexOf('upscaleModel');
const items = availableOptions.map((key, i) => {
// Render upscaleModel here instead of at filter+splice
if (key === 'upscaleModel') return renderUpscaleModelSelector();
const def = getOptionDefinition(key);
if (!def) return null;

if (def.type === 'numeric') {
return renderNumericField(key);
} else if (def.type === 'string') {
if (def.isBackendOption) {
return renderBackendSelector(key);
}
if (def.type === 'numeric') return renderNumericField(key);
if (def.type === 'string') {
if (def.isBackendOption) return renderBackendSelector(key);
return renderStringField(key);
} else if (def.type === 'boolean') {
return renderBooleanField(key);
}
if (def.type === 'boolean') return renderBooleanField(key);
return null;
});
}).filter(Boolean);

// Move upscaleModel right before mergeArgs (index 5 in sd-cpp order)
if (upscaledIdx > 5 && upscaledIdx >= 0) {
const [node] = items.splice(upscaledIdx, 1);
items.splice(5, 0, node);
}
return items;
};

return (
Expand Down
12 changes: 11 additions & 1 deletion src/app/src/renderer/recipes/recipeOptionsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export interface StableDiffusionOptions {
cfgScale: NumericOption;
width: NumericOption;
height: NumericOption;
upscaleModel: StringOption;
mergeArgs: BooleanOption;
pinned: BooleanOption;
saveOptions: BooleanOption;
Expand Down Expand Up @@ -163,6 +164,7 @@ export interface StringOptionDef {
label: string;
description?: string;
isBackendOption?: boolean;
isUpscalerOption?: boolean;
backendRecipe?: string;
}

Expand Down Expand Up @@ -264,6 +266,13 @@ export const OPTION_DEFINITIONS: Record<string, OptionDef> = {
isBackendOption: true,
backendRecipe: 'sd-cpp',
},
upscaleModel: {
type: 'string',
default: '',
label: 'Upscale Model',
description: 'ESRGAN upscaler to apply after image generation (set model options, not in-chat)',
isUpscalerOption: true,
},
steps: {
type: 'numeric',
default: 20,
Expand Down Expand Up @@ -368,7 +377,7 @@ export const RECIPE_OPTIONS_MAP: Record<RecipeName, string[]> = {
'moonshine': ['moonshineArgs', 'mergeArgs', 'pinned', 'saveOptions'],
'flm': ['ctxSize', 'mergeArgs', 'pinned', 'saveOptions'],
'ryzenai-llm': ['ctxSize', 'pinned', 'saveOptions'],
'sd-cpp': ['sdcppBackend', 'steps', 'cfgScale', 'width', 'height', 'mergeArgs', 'pinned', 'saveOptions'],
'sd-cpp': ['sdcppBackend', 'steps', 'cfgScale', 'width', 'height', 'upscaleModel', 'mergeArgs', 'pinned', 'saveOptions'],
'vllm': ['ctxSize', 'vllmBackend', 'vllmArgs', 'mergeArgs', 'pinned', 'saveOptions'],
'thinksound': ['thinksoundBackend', 'pinned', 'saveOptions'],
'acestep': ['acestepBackend', 'pinned', 'saveOptions'],
Expand Down Expand Up @@ -414,6 +423,7 @@ const FRONTEND_TO_API_MAP: Record<string, string> = {
vllmBackend: 'vllm_backend',
vllmArgs: 'vllm_args',
saveOptions: 'save_options',
upscaleModel: 'upscale_model',
};

const API_TO_FRONTEND_MAP: Record<string, string> = Object.fromEntries(
Expand Down
1 change: 1 addition & 0 deletions src/cpp/include/lemon/backends/sdcpp/sdcpp.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ inline const BackendDescriptor descriptor = {
{"cfg_scale", "", 7.0, "SIZE", "Classifier-free guidance scale", "Stable Diffusion Options"},
{"width", "", 512, "SIZE", "Output image width", "Stable Diffusion Options"},
{"height", "", 512, "SIZE", "Output image height", "Stable Diffusion Options"},
{"upscale_model", "", "", "MODEL", "ESRGAN upscaler to apply after generation (set model options, not in-chat)", "Stable Diffusion Options"},
{"sampling_method", "", "", "ARGS", "Sampling method", "Stable Diffusion Options"},
{"flow_shift", "", 0.0, "SIZE", "Flow shift", "Stable Diffusion Options"},
},
Expand Down
1 change: 1 addition & 0 deletions src/cpp/include/lemon/backends/sdcpp/sdcpp_server.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ class SDServer : public WrappedServer, public IImageServer {
const std::string& upscale_model_path,
const std::string& cli_exe_path,
const std::vector<std::pair<std::string, std::string>>& env_vars,
const std::string& backend,
bool debug = false);

private:
Expand Down
21 changes: 21 additions & 0 deletions src/cpp/include/lemon/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,27 @@ class Server {
bool extract_image_from_form(const httplib::Request& req, httplib::Response& res, nlohmann::json& out);
bool load_image_model(const nlohmann::json& request_json, httplib::Response& res);

// Auto-upscale an image response if the source model has an "upscale_model"
// recipe option configured. Returns a new JSON response with the upscaled
// image, or the original response unchanged if no upscaler is set.
// On failure, sets res status/body and returns false.
bool apply_upscale_if_configured(
const std::string& model_name,
nlohmann::json& response,
httplib::Response& res);

// Internal helper: resolve upscale model path, pick backend, run sd-cli.
// Returns the upscaled base64 image, or empty on failure (with error
// already written to res if res is not null).
std::string do_upscale(
const std::string& b64_image,
const std::string& upscale_model_name,
const std::string& main_model_name,
httplib::Response* res);

// Map recipe backend name to sd-cli device identifier (vulkan0, rocm0, etc).
static std::string sd_backend_for_recipe(const std::string& recipe_backend);

bool parse_required_json_body(const httplib::Request& req,
httplib::Response& res,
nlohmann::json& out);
Expand Down
8 changes: 7 additions & 1 deletion src/cpp/server/backends/sdcpp/sdcpp_server.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ std::string SDServer::upscale_via_cli(
const std::string& upscale_model_path,
const std::string& cli_exe_path,
const std::vector<std::pair<std::string, std::string>>& env_vars,
const std::string& backend,
bool debug) {

if (!fs::exists(cli_exe_path)) {
Expand Down Expand Up @@ -703,10 +704,15 @@ std::string SDServer::upscale_via_cli(

std::vector<std::string> cli_args = {
"-M", "upscale",
};
if (!backend.empty()) {
cli_args.insert(cli_args.end(), {"--backend", backend});
}
cli_args.insert(cli_args.end(), {
"--upscale-model", upscale_model_path,
"-i", input_path.string(),
"-o", output_path.string()
};
});

// inherit_output = true so subprocess stderr/stdout is visible in server
// logs for debugging failed upscale operations
Expand Down
Loading