Working!

parent 3458911a
...@@ -945,8 +945,12 @@ def _scan_caches() -> dict: ...@@ -945,8 +945,12 @@ def _scan_caches() -> dict:
# Add configured GGUF models not yet in the list (e.g., HF repo IDs or external paths) # Add configured GGUF models not yet in the list (e.g., HF repo IDs or external paths)
existing_paths = {m["path"] for m in result["gguf"]} existing_paths = {m["path"] for m in result["gguf"]}
existing_fnames = {m["filename"] for m in result["gguf"]}
for path, (settings, mtype) in configured_settings.items(): for path, (settings, mtype) in configured_settings.items():
if path in existing_paths: if path in existing_paths or path in existing_fnames:
continue
fname = os.path.basename(path) if '/' in path else path
if fname in existing_fnames:
continue continue
# Check if it's a GGUF model (ends with .gguf or is in a GGUF repo) # Check if it's a GGUF model (ends with .gguf or is in a GGUF repo)
is_gguf = path.endswith('.gguf') or 'gguf' in path.lower() or mtype == "gguf_models" is_gguf = path.endswith('.gguf') or 'gguf' in path.lower() or mtype == "gguf_models"
...@@ -1115,7 +1119,11 @@ def _do_delete_model(model_id: str, cache_type: str) -> dict: ...@@ -1115,7 +1119,11 @@ def _do_delete_model(model_id: str, cache_type: str) -> dict:
if cache_type == "gguf": if cache_type == "gguf":
gguf_dir = get_model_cache_dir() gguf_dir = get_model_cache_dir()
fp = os.path.join(gguf_dir, model_id) # Support full absolute path (e.g. HF-cached GGUF) or bare filename
if os.path.isabs(model_id):
fp = model_id
else:
fp = os.path.join(gguf_dir, model_id)
if os.path.isfile(fp): if os.path.isfile(fp):
os.remove(fp) os.remove(fp)
return {"success": True} return {"success": True}
......
...@@ -4,6 +4,10 @@ ...@@ -4,6 +4,10 @@
{% block head %} {% block head %}
<style> <style>
:root {
--text-1:#DDE1F0; --surface-0:#08090D; --surface-1:#111218;
--surface-2:#161820; --surface-3:#252836; --accent-dim:rgba(99,102,241,.15);
}
/* ── Layout ─────────────────────────────────────────────────────── */ /* ── Layout ─────────────────────────────────────────────────────── */
.studio { display:flex; height:calc(100vh - 56px); overflow:hidden; } .studio { display:flex; height:calc(100vh - 56px); overflow:hidden; }
...@@ -155,6 +159,12 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -155,6 +159,12 @@ a.dl { display:inline-block; margin-top:.4rem; }
border:1px solid var(--border); background:var(--surface-1); border-radius:8px; border:1px solid var(--border); background:var(--surface-1); border-radius:8px;
padding:.75rem .9rem; margin-bottom:.75rem; display:flex; flex-direction:column; gap:.55rem; padding:.75rem .9rem; margin-bottom:.75rem; display:flex; flex-direction:column; gap:.55rem;
} }
.cap-card.state-partial {
border-color:rgba(245,158,11,.4); background:rgba(58,37,16,.7);
}
.cap-card.state-unavailable {
border-color:rgba(248,113,113,.4); background:rgba(80,28,28,.7);
}
.cap-card-top { display:flex; align-items:flex-start; justify-content:space-between; gap:.75rem; } .cap-card-top { display:flex; align-items:flex-start; justify-content:space-between; gap:.75rem; }
.cap-card-title { font-size:13px; font-weight:600; color:var(--text-1); } .cap-card-title { font-size:13px; font-weight:600; color:var(--text-1); }
.cap-card-summary { font-size:12px; line-height:1.45; color:var(--text-2); } .cap-card-summary { font-size:12px; line-height:1.45; color:var(--text-2); }
...@@ -168,11 +178,12 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -168,11 +178,12 @@ a.dl { display:inline-block; margin-top:.4rem; }
.cap-missing, .cap-note { font-size:12px; color:var(--text-2); } .cap-missing, .cap-note { font-size:12px; color:var(--text-2); }
.cap-note strong, .cap-missing strong { color:var(--text-1); } .cap-note strong, .cap-missing strong { color:var(--text-1); }
.cap-output-note { .cap-output-note {
width:100%; max-width:960px; border:1px solid rgba(245,158,11,.24); background:rgba(58,37,16,.72); width:100%; max-width:960px; border:1px solid rgba(245,158,11,.35); background:rgba(58,37,16,.85);
color:#f6d08a; border-radius:8px; padding:.75rem .9rem; display:flex; flex-direction:column; gap:.35rem; color:#f6d08a; border-radius:8px; padding:.85rem 1rem; display:flex; flex-direction:column; gap:.4rem;
font-size:13px; margin-bottom:.75rem;
} }
.cap-output-note.unavailable { .cap-output-note.unavailable {
border-color:rgba(248,113,113,.24); background:rgba(80,28,28,.62); color:#f3b2b2; border-color:rgba(248,113,113,.35); background:rgba(80,28,28,.85); color:#f3b2b2;
} }
.cap-output-note ul { margin:0; padding-left:1.1rem; } .cap-output-note ul { margin:0; padding-left:1.1rem; }
.cap-toggle { font-size:12px; display:flex; align-items:center; gap:.4rem; cursor:pointer; color:var(--text-2); } .cap-toggle { font-size:12px; display:flex; align-items:center; gap:.4rem; cursor:pointer; color:var(--text-2); }
...@@ -200,7 +211,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -200,7 +211,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
/* ── Diagnostics / history ────────────────────────────────────── */ /* ── Diagnostics / history ────────────────────────────────────── */
.diag-card, .hist-card { .diag-card, .hist-card {
border:1px solid var(--border); background:var(--surface-1); border-radius:8px; border:1px solid var(--border); background:var(--surface-1); border-radius:8px;
padding:.8rem .9rem; display:flex; flex-direction:column; gap:.6rem; padding:.8rem .9rem; display:flex; flex-direction:column; gap:.6rem; margin-bottom:.6rem;
} }
.diag-title, .hist-title { font-size:13px; font-weight:600; color:var(--text-1); } .diag-title, .hist-title { font-size:13px; font-weight:600; color:var(--text-1); }
.diag-sub, .hist-empty, .hist-meta, .hist-summary { font-size:12px; color:var(--text-2); line-height:1.45; } .diag-sub, .hist-empty, .hist-meta, .hist-summary { font-size:12px; color:var(--text-2); line-height:1.45; }
...@@ -216,8 +227,8 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -216,8 +227,8 @@ a.dl { display:inline-block; margin-top:.4rem; }
.hist-link { font-size:11px; } .hist-link { font-size:11px; }
/* ── Pipeline ─────────────────────────────────────────────────── */ /* ── Pipeline ─────────────────────────────────────────────────── */
.pipe-panel { flex:1; overflow-y:auto; padding:1.2rem; display:flex; flex-direction:column; gap:.6rem; } .pipe-panel { flex:1; overflow-y:auto; padding:1.2rem; display:block; }
.pipe-card { background:var(--surface-1); border:1px solid var(--border); border-radius:8px; overflow:hidden; } .pipe-card { background:var(--surface-1); border:1px solid var(--border); border-radius:8px; overflow:hidden; margin-bottom:.6rem; }
.pipe-card summary { .pipe-card summary {
list-style:none; padding:.8rem 1rem; cursor:pointer; display:flex; align-items:flex-start; list-style:none; padding:.8rem 1rem; cursor:pointer; display:flex; align-items:flex-start;
justify-content:space-between; gap:.75rem; font-weight:600; font-size:13px; user-select:none; color:var(--text-1); justify-content:space-between; gap:.75rem; font-weight:600; font-size:13px; user-select:none; color:var(--text-1);
...@@ -334,6 +345,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -334,6 +345,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-gen"> <div class="panel" id="panel-img-gen">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-gen"></div>
<div class="frow"><label class="fl">Prompt</label><textarea id="ig-prompt" class="fs" placeholder="Describe the image…" rows="4"></textarea></div> <div class="frow"><label class="fl">Prompt</label><textarea id="ig-prompt" class="fs" placeholder="Describe the image…" rows="4"></textarea></div>
<div class="frow"><label class="fl">Negative prompt</label><textarea id="ig-neg" class="fs" placeholder="Things to avoid…" rows="2"></textarea></div> <div class="frow"><label class="fl">Negative prompt</label><textarea id="ig-neg" class="fs" placeholder="Things to avoid…" rows="2"></textarea></div>
<div class="g2"> <div class="g2">
...@@ -359,6 +371,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -359,6 +371,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-edit"> <div class="panel" id="panel-img-edit">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-edit"></div>
<div class="frow"><label class="fl">Source image</label><input type="file" id="ie-src" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">Source image</label><input type="file" id="ie-src" accept="image/*" class="fi"></div>
<div class="frow"><label class="fl">Prompt</label><textarea id="ie-prompt" class="fs" rows="4" placeholder="Describe the desired change…"></textarea></div> <div class="frow"><label class="fl">Prompt</label><textarea id="ie-prompt" class="fs" rows="4" placeholder="Describe the desired change…"></textarea></div>
<div class="frow"><label class="fl">Strength <span id="ie-sv">0.75</span></label> <div class="frow"><label class="fl">Strength <span id="ie-sv">0.75</span></label>
...@@ -379,6 +392,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -379,6 +392,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-inpaint"> <div class="panel" id="panel-img-inpaint">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-inpaint"></div>
<div class="frow"><label class="fl">Source image</label><input type="file" id="ip-src" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">Source image</label><input type="file" id="ip-src" accept="image/*" class="fi"></div>
<div class="frow"><label class="fl">Mask image <span style="font-weight:400">(white = fill area)</span></label><input type="file" id="ip-mask" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">Mask image <span style="font-weight:400">(white = fill area)</span></label><input type="file" id="ip-mask" accept="image/*" class="fi"></div>
<div class="frow"><label class="fl">Prompt</label><textarea id="ip-prompt" class="fs" rows="4" placeholder="What to fill the masked area with…"></textarea></div> <div class="frow"><label class="fl">Prompt</label><textarea id="ip-prompt" class="fs" rows="4" placeholder="What to fill the masked area with…"></textarea></div>
...@@ -400,6 +414,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -400,6 +414,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-upscale"> <div class="panel" id="panel-img-upscale">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-upscale"></div>
<div class="frow"><label class="fl">Source image</label><input type="file" id="iu-src" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">Source image</label><input type="file" id="iu-src" accept="image/*" class="fi"></div>
<div class="frow"><label class="fl">Scale factor</label> <div class="frow"><label class="fl">Scale factor</label>
<select id="iu-scale" class="fselect"><option value="2"></option><option value="4" selected></option><option value="8"></option></select> <select id="iu-scale" class="fselect"><option value="2"></option><option value="4" selected></option><option value="8"></option></select>
...@@ -415,6 +430,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -415,6 +430,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-depth"> <div class="panel" id="panel-img-depth">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-depth"></div>
<div class="frow"><label class="fl">Source image</label><input type="file" id="id-src" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">Source image</label><input type="file" id="id-src" accept="image/*" class="fi"></div>
<button class="btn btn-primary" onclick="genDepth()">Estimate Depth</button> <button class="btn btn-primary" onclick="genDepth()">Estimate Depth</button>
<div class="progress" id="id-prog"></div> <div class="progress" id="id-prog"></div>
...@@ -427,6 +443,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -427,6 +443,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-seg"> <div class="panel" id="panel-img-seg">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-seg"></div>
<div class="frow"><label class="fl">Source image</label><input type="file" id="is-src" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">Source image</label><input type="file" id="is-src" accept="image/*" class="fi"></div>
<div class="frow"><label class="fl">Point prompts (optional)</label> <div class="frow"><label class="fl">Point prompts (optional)</label>
<input type="text" id="is-pts" class="fi" placeholder='[[x,y], [x,y]]'></div> <input type="text" id="is-pts" class="fi" placeholder='[[x,y], [x,y]]'></div>
...@@ -443,6 +460,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -443,6 +460,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-faceswap"> <div class="panel" id="panel-img-faceswap">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-faceswap"></div>
<div class="frow"><label class="fl">Source face</label> <div class="frow"><label class="fl">Source face</label>
<div class="media-input-group"> <div class="media-input-group">
<input type="file" id="fs-src" accept="image/*" class="fi" onchange="previewMediaInput('fs-src','fs-src-preview')"> <input type="file" id="fs-src" accept="image/*" class="fi" onchange="previewMediaInput('fs-src','fs-src-preview')">
...@@ -479,6 +497,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -479,6 +497,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-deblur"> <div class="panel" id="panel-img-deblur">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-deblur"></div>
<div class="frow"><label class="fl">Source image</label><input type="file" id="db-src" accept="image/*" class="fi" onchange="previewMediaInput('db-src','db-src-preview')"></div> <div class="frow"><label class="fl">Source image</label><input type="file" id="db-src" accept="image/*" class="fi" onchange="previewMediaInput('db-src','db-src-preview')"></div>
<div id="db-src-preview" class="media-preview"></div> <div id="db-src-preview" class="media-preview"></div>
<div class="frow"><label class="fl">Strength <span id="db-sv">0.5</span></label> <div class="frow"><label class="fl">Strength <span id="db-sv">0.5</span></label>
...@@ -494,6 +513,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -494,6 +513,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-unpix"> <div class="panel" id="panel-img-unpix">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-unpix"></div>
<div class="frow"><label class="fl">Source image</label><input type="file" id="up-src" accept="image/*" class="fi" onchange="previewMediaInput('up-src','up-src-preview')"></div> <div class="frow"><label class="fl">Source image</label><input type="file" id="up-src" accept="image/*" class="fi" onchange="previewMediaInput('up-src','up-src-preview')"></div>
<div id="up-src-preview" class="media-preview"></div> <div id="up-src-preview" class="media-preview"></div>
<div class="frow"><label class="fl">Scale</label> <div class="frow"><label class="fl">Scale</label>
...@@ -510,6 +530,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -510,6 +530,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-img-outfit"> <div class="panel" id="panel-img-outfit">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-img-outfit"></div>
<div class="frow"><label class="fl">Target type</label> <div class="frow"><label class="fl">Target type</label>
<select id="ot-type" class="fselect" onchange="otOutfitTypeChange()"> <select id="ot-type" class="fselect" onchange="otOutfitTypeChange()">
<option value="image">Image</option> <option value="image">Image</option>
...@@ -549,6 +570,8 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -549,6 +570,8 @@ a.dl { display:inline-block; margin-top:.4rem; }
<!-- I2V --> <!-- I2V -->
<div class="panel" id="panel-vid-i2v"><div class="gen-wrap"> <div class="panel" id="panel-vid-i2v"><div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-vid-i2v"></div>
<div class="cap-card" id="cap-vid-t2v"></div>
<div class="frow"><label class="fl">Source image</label><input type="file" id="vi-src" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">Source image</label><input type="file" id="vi-src" accept="image/*" class="fi"></div>
<div class="frow"><label class="fl">Motion prompt (optional)</label><textarea id="vi-prompt" class="fs" rows="2" placeholder="How should the image animate?"></textarea></div> <div class="frow"><label class="fl">Motion prompt (optional)</label><textarea id="vi-prompt" class="fs" rows="2" placeholder="How should the image animate?"></textarea></div>
<div class="g2"> <div class="g2">
...@@ -588,6 +611,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -588,6 +611,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<!-- V2V --> <!-- V2V -->
<div class="panel" id="panel-vid-v2v"><div class="gen-wrap"> <div class="panel" id="panel-vid-v2v"><div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-vid-v2v"></div>
<div class="frow"><label class="fl">Source video</label><input type="file" id="vv-src" accept="video/*" class="fi"></div> <div class="frow"><label class="fl">Source video</label><input type="file" id="vv-src" accept="video/*" class="fi"></div>
<div class="frow"><label class="fl">Prompt</label><textarea id="vv-prompt" class="fs" rows="3" placeholder="Describe the transformation…"></textarea></div> <div class="frow"><label class="fl">Prompt</label><textarea id="vv-prompt" class="fs" rows="3" placeholder="Describe the transformation…"></textarea></div>
<div class="frow"><label class="fl">Strength <span id="vv-sv">0.70</span></label> <div class="frow"><label class="fl">Strength <span id="vv-sv">0.70</span></label>
...@@ -625,6 +649,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -625,6 +649,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<!-- Ti2V: Text + Image → Video --> <!-- Ti2V: Text + Image → Video -->
<div class="panel" id="panel-vid-ti2v"><div class="gen-wrap"> <div class="panel" id="panel-vid-ti2v"><div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-vid-ti2v"></div>
<div class="frow"><label class="fl">Initial image</label><input type="file" id="ti-init" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">Initial image</label><input type="file" id="ti-init" accept="image/*" class="fi"></div>
<div class="frow"><label class="fl">End image (optional — for frame interpolation)</label><input type="file" id="ti-end" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">End image (optional — for frame interpolation)</label><input type="file" id="ti-end" accept="image/*" class="fi"></div>
<div class="frow"><label class="fl">Source video (optional — enables V2V mode, overrides image)</label><input type="file" id="ti-vid" accept="video/*" class="fi"></div> <div class="frow"><label class="fl">Source video (optional — enables V2V mode, overrides image)</label><input type="file" id="ti-vid" accept="video/*" class="fi"></div>
...@@ -720,6 +745,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -720,6 +745,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<!-- Interpolate --> <!-- Interpolate -->
<div class="panel" id="panel-vid-interp"><div class="gen-wrap"> <div class="panel" id="panel-vid-interp"><div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-vid-interp"></div>
<div class="frow"><label class="fl">Source video (or use frame images below)</label><input type="file" id="vc-src" accept="video/*" class="fi"></div> <div class="frow"><label class="fl">Source video (or use frame images below)</label><input type="file" id="vc-src" accept="video/*" class="fi"></div>
<div class="frow"><label class="fl">— OR — First frame</label><input type="file" id="vc-init" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">— OR — First frame</label><input type="file" id="vc-init" accept="image/*" class="fi"></div>
<div class="frow"><label class="fl">Last frame</label><input type="file" id="vc-end" accept="image/*" class="fi"></div> <div class="frow"><label class="fl">Last frame</label><input type="file" id="vc-end" accept="image/*" class="fi"></div>
...@@ -735,6 +761,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -735,6 +761,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<!-- Subtitles --> <!-- Subtitles -->
<div class="panel" id="panel-vid-sub"><div class="gen-wrap"> <div class="panel" id="panel-vid-sub"><div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-vid-sub"></div>
<div class="frow"><label class="fl">Source video</label><input type="file" id="vs-src" accept="video/*" class="fi"></div> <div class="frow"><label class="fl">Source video</label><input type="file" id="vs-src" accept="video/*" class="fi"></div>
<div class="frow"><label class="fl">Language hint</label><input id="vs-lang" class="fi" placeholder="en, fr, de… (auto-detect if blank)"></div> <div class="frow"><label class="fl">Language hint</label><input id="vs-lang" class="fi" placeholder="en, fr, de… (auto-detect if blank)"></div>
<div class="frow"><label class="fl">Output format</label> <div class="frow"><label class="fl">Output format</label>
...@@ -784,6 +811,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -784,6 +811,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<!-- Video upscale --> <!-- Video upscale -->
<div class="panel" id="panel-vid-up"><div class="gen-wrap"> <div class="panel" id="panel-vid-up"><div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-vid-up"></div>
<div class="frow"><label class="fl">Source video</label><input type="file" id="vu-src" accept="video/*" class="fi"></div> <div class="frow"><label class="fl">Source video</label><input type="file" id="vu-src" accept="video/*" class="fi"></div>
<div class="frow"><label class="fl">Scale factor</label> <div class="frow"><label class="fl">Scale factor</label>
<select id="vu-scale" class="fselect"><option value="2"></option><option value="4"></option></select> <select id="vu-scale" class="fselect"><option value="2"></option><option value="4"></option></select>
...@@ -824,6 +852,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -824,6 +852,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-aud-tts"> <div class="panel" id="panel-aud-tts">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-aud-tts"></div>
<div class="frow"><label class="fl">Text to speak</label><textarea id="at-text" class="fs" rows="6" placeholder="Enter text…"></textarea></div> <div class="frow"><label class="fl">Text to speak</label><textarea id="at-text" class="fs" rows="6" placeholder="Enter text…"></textarea></div>
<div class="g2"> <div class="g2">
<div class="frow"><label class="fl">Voice</label><input id="at-voice" class="fi" placeholder="af_sky, en-US-Jenny…"></div> <div class="frow"><label class="fl">Voice</label><input id="at-voice" class="fi" placeholder="af_sky, en-US-Jenny…"></div>
...@@ -841,6 +870,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -841,6 +870,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-aud-clone"> <div class="panel" id="panel-aud-clone">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-aud-clone"></div>
<div class="frow"><label class="fl">Text to synthesize</label><textarea id="vc-text" class="fs" rows="4" placeholder="Enter text to speak in the cloned voice…"></textarea></div> <div class="frow"><label class="fl">Text to synthesize</label><textarea id="vc-text" class="fs" rows="4" placeholder="Enter text to speak in the cloned voice…"></textarea></div>
<div class="frow"><label class="fl">Voice profile (saved)</label> <div class="frow"><label class="fl">Voice profile (saved)</label>
<select id="vc-voice" class="fselect"> <select id="vc-voice" class="fselect">
...@@ -885,6 +915,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -885,6 +915,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-aud-convert"> <div class="panel" id="panel-aud-convert">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-aud-convert"></div>
<p style="font-size:12px;color:var(--text-3);margin:0 0 .5rem"> <p style="font-size:12px;color:var(--text-3);margin:0 0 .5rem">
Converts only the <strong>timbre</strong> of the source audio to match the target voice, Converts only the <strong>timbre</strong> of the source audio to match the target voice,
preserving pitch, melody, rhythm and expression. preserving pitch, melody, rhythm and expression.
...@@ -930,6 +961,7 @@ a.dl { display:inline-block; margin-top:.4rem; } ...@@ -930,6 +961,7 @@ a.dl { display:inline-block; margin-top:.4rem; }
<div class="panel" id="panel-aud-stt"> <div class="panel" id="panel-aud-stt">
<div class="gen-wrap"> <div class="gen-wrap">
<div class="gen-ctrl"> <div class="gen-ctrl">
<div class="cap-card" id="cap-aud-stt"></div>
<div class="frow"><label class="fl">Audio / video file</label><input type="file" id="as-file" accept="audio/*,video/*" class="fi"></div> <div class="frow"><label class="fl">Audio / video file</label><input type="file" id="as-file" accept="audio/*,video/*" class="fi"></div>
<div class="g2"> <div class="g2">
<div class="frow"><label class="fl">Language</label><input id="as-lang" class="fi" placeholder="en, fr…"></div> <div class="frow"><label class="fl">Language</label><input id="as-lang" class="fi" placeholder="en, fr…"></div>
...@@ -1701,6 +1733,9 @@ function renderCapabilityCard(sub) { ...@@ -1701,6 +1733,9 @@ function renderCapabilityCard(sub) {
return; return;
} }
shell.style.display = ''; shell.style.display = '';
shell.classList.remove('state-partial', 'state-unavailable');
if (details.availability === 'partial') shell.classList.add('state-partial');
if (details.availability === 'unavailable') shell.classList.add('state-unavailable');
const availabilityLabel = details.availability === 'available' ? 'Available' : details.availability === 'partial' ? 'Partial' : 'Unavailable'; const availabilityLabel = details.availability === 'available' ? 'Available' : details.availability === 'partial' ? 'Partial' : 'Unavailable';
const availabilityClass = details.availability === 'available' ? '' : details.availability === 'partial' ? ' warn' : ' dim'; const availabilityClass = details.availability === 'available' ? '' : details.availability === 'partial' ? ' warn' : ' dim';
const missingBits = []; const missingBits = [];
...@@ -1727,24 +1762,55 @@ function renderCapabilityCard(sub) { ...@@ -1727,24 +1762,55 @@ function renderCapabilityCard(sub) {
function renderCapabilityCards() { function renderCapabilityCards() {
Object.keys(STUDIO_CAPABILITIES).forEach(renderCapabilityCard); Object.keys(STUDIO_CAPABILITIES).forEach(renderCapabilityCard);
// Render availability cards for all subs using SUB_CAPABILITY_RULES
Object.keys(SUB_CAPABILITY_RULES).forEach(sub => {
if (STUDIO_CAPABILITIES[sub]) return; // already handled above
const shell = $(`cap-${sub}`);
if (!shell) return;
const state = currentTabState.subs[sub] || 'unavailable';
if (state === 'available') { shell.style.display = 'none'; shell.innerHTML = ''; return; }
shell.style.display = '';
shell.classList.remove('state-partial', 'state-unavailable');
shell.classList.add(state === 'partial' ? 'state-partial' : 'state-unavailable');
const rule = SUB_CAPABILITY_RULES[sub];
const caps = capabilitySetForModel(activeModel);
const missingRequired = (rule.requiresAny || []).filter(c => !caps.has(c));
const missingOptional = (rule.optional || []).filter(c => !caps.has(c));
const label = document.querySelector(`.t2btn[data-sub="${sub}"]`)?.childNodes[0]?.textContent?.trim() || sub;
const missingBits = [];
if (missingRequired.length) missingBits.push(`<div class="cap-missing"><strong>Missing required:</strong> ${missingRequired.join(', ')}</div>`);
if (missingOptional.length) missingBits.push(`<div class="cap-missing"><strong>Limited without:</strong> ${missingOptional.join(', ')}</div>`);
const availabilityLabel = state === 'partial' ? 'Partial' : 'Unavailable';
const availabilityClass = state === 'partial' ? ' warn' : ' dim';
shell.innerHTML = `
<div class="cap-card-top">
<div class="cap-card-title">${escapeHtml(label)}</div>
<span class="cap-chip${availabilityClass}">${availabilityLabel}</span>
</div>
${missingBits.join('')}
`;
});
renderAudioBackendHealth(); renderAudioBackendHealth();
} }
function renderCapabilityOutputNote(sub, outEl) { function renderCapabilityOutputNote(sub) {
if (!outEl) return;
const details = getCapabilityDetails(sub); const details = getCapabilityDetails(sub);
const noteId = `cap-output-note-${sub}`; const noteId = `cap-output-note-${sub}`;
outEl.querySelector(`#${noteId}`)?.remove(); const panelId = SUB_PANEL_ALIAS[sub] || (`panel-${sub}`);
const panel = $(panelId);
if (!panel) return;
panel.querySelector(`#${noteId}`)?.remove();
if (!details || details.availability === 'available') return; if (!details || details.availability === 'available') return;
const missing = []; const missing = [];
if (details.missingRequired.length) missing.push(...details.missingRequired.map(item => `Missing required: ${item}`)); if (details.missingRequired.length) missing.push(...details.missingRequired.map(item => `<strong>Required:</strong> ${item}`));
if (details.missingOptional.length) missing.push(...details.missingOptional.map(item => `Limited without: ${item}`)); if (details.missingOptional.length) missing.push(...details.missingOptional.map(item => `<strong>Optional:</strong> ${item}`));
if (!missing.length && Array.isArray(details.notes)) missing.push(...details.notes); if (!missing.length && Array.isArray(details.notes)) missing.push(...details.notes);
const title = details.availability === 'unavailable' ? 'Feature unavailable' : 'Feature partially available'; const title = details.availability === 'unavailable' ? '⚠ Feature unavailable' : '⚠ Feature partially available';
const detailHtml = missing.length const detailHtml = missing.length
? `<ul>${missing.map(item => `<li>${escapeHtml(item)}</li>`).join('')}</ul>` ? `<ul>${missing.map(item => `<li>${item}</li>`).join('')}</ul>`
: '<div>Some required pieces are missing for this surface.</div>'; : '<div>Some required pieces are missing for this surface.</div>';
outEl.insertAdjacentHTML('afterbegin', ` const target = panel.querySelector('.gen-out') || panel.querySelector('.gen-wrap') || panel;
target.insertAdjacentHTML('afterbegin', `
<div class="cap-output-note ${details.availability === 'unavailable' ? 'unavailable' : ''}" id="${noteId}"> <div class="cap-output-note ${details.availability === 'unavailable' ? 'unavailable' : ''}" id="${noteId}">
<strong>${title}</strong> <strong>${title}</strong>
${detailHtml} ${detailHtml}
...@@ -1753,12 +1819,7 @@ function renderCapabilityOutputNote(sub, outEl) { ...@@ -1753,12 +1819,7 @@ function renderCapabilityOutputNote(sub, outEl) {
} }
function renderOutputCapabilityNotes() { function renderOutputCapabilityNotes() {
Object.keys(SUB_PANEL_ALIAS).forEach(sub => { Object.keys(STUDIO_CAPABILITIES).forEach(sub => renderCapabilityOutputNote(sub));
const panelId = SUB_PANEL_ALIAS[sub] || (`panel-${sub}`);
const panel = $(panelId);
const outEl = panel?.querySelector('.gen-out');
renderCapabilityOutputNote(sub, outEl);
});
} }
function summarizeDiagnostics() { function summarizeDiagnostics() {
...@@ -2424,8 +2485,7 @@ function selectSub(sub) { ...@@ -2424,8 +2485,7 @@ function selectSub(sub) {
const panelId = SUB_PANEL_ALIAS[sub] || ('panel-' + sub); const panelId = SUB_PANEL_ALIAS[sub] || ('panel-' + sub);
const panel = $(panelId); const panel = $(panelId);
if (panel) panel.classList.add('active'); if (panel) panel.classList.add('active');
const outEl = panel?.querySelector('.gen-out'); renderCapabilityOutputNote(sub);
if (outEl) renderCapabilityOutputNote(sub, outEl);
// When switching to vid-faceswap, pre-select video mode // When switching to vid-faceswap, pre-select video mode
if (sub === 'vid-faceswap') { const t = $('fs-type'); if (t) { t.value='video'; fsFaceSwapTypeChange(); } } if (sub === 'vid-faceswap') { const t = $('fs-type'); if (t) { t.value='video'; fsFaceSwapTypeChange(); } }
if (sub === 'vid-outfit') { const t = $('ot-type'); if (t) { t.value='video'; otOutfitTypeChange(); } } if (sub === 'vid-outfit') { const t = $('ot-type'); if (t) { t.value='video'; otOutfitTypeChange(); } }
......
...@@ -345,6 +345,10 @@ ...@@ -345,6 +345,10 @@
<label class="form-label">Model ID / path</label> <label class="form-label">Model ID / path</label>
<div id="cfg-id-label" style="font-size:12px;font-family:monospace;color:var(--text-2);word-break:break-all;padding:.3rem 0"></div> <div id="cfg-id-label" style="font-size:12px;font-family:monospace;color:var(--text-2);word-break:break-all;padding:.3rem 0"></div>
</div> </div>
<div class="form-row" id="cfg-quant-row" style="display:none">
<label class="form-label">Quantization</label>
<select id="cfg-quant-select" class="form-input" onchange="onCfgQuantChange()"></select>
</div>
<div class="form-row"> <div class="form-row">
<label class="form-label" style="display:flex;align-items:center;gap:.5rem">Type <label class="form-label" style="display:flex;align-items:center;gap:.5rem">Type
<span id="cfg-type-autodet" style="font-size:11px;color:var(--text-3);font-weight:400"></span> <span id="cfg-type-autodet" style="font-size:11px;color:var(--text-3);font-weight:400"></span>
...@@ -510,6 +514,41 @@ function fmtCapabilities(caps){ ...@@ -510,6 +514,41 @@ function fmtCapabilities(caps){
return caps.slice(0,5).map(c=>`<span class="badge badge-user" style="font-size:10px;padding:.15rem .35rem">${esc(labels[c]||c)}</span>`).join(' '); return caps.slice(0,5).map(c=>`<span class="badge badge-user" style="font-size:10px;padding:.15rem .35rem">${esc(labels[c]||c)}</span>`).join(' ');
} }
/* ── GGUF grouping helpers ───────────────────────────── */
const _QUANT_RE = /[_\-\.](Q\d[_A-Z0-9]*|IQ\d[_A-Z0-9]*|F16|F32|BF16)(?=\.gguf$)/i;
function _ggufQuant(filename){
const m = filename.match(_QUANT_RE);
return m ? m[1].toUpperCase() : null;
}
function _ggufBaseName(filename){
// Strip quant suffix and .gguf extension to get the model base name
return filename.replace(_QUANT_RE,'').replace(/\.gguf$/i,'');
}
function _switchGgufQuant(idx, newPath){
const m = _localModels[idx];
if(!m || !m.ggufGroup) return;
const f = m.ggufGroup.find(x=>x.path===newPath);
if(!f) return;
m.label = f.filename;
m.path = f.path;
m.size_gb = f.size_gb||0;
m.settings = f.settings||{};
m.in_config = f.in_config;
// Re-render quant badges in the row
const row = document.getElementById('gguf-row-'+idx);
if(!row) return;
const base = _ggufBaseName(f.filename);
const quantBadges = m.ggufGroup.length > 1
? m.ggufGroup.map(gf=>{
const q = _ggufQuant(gf.filename);
const active = gf.path === newPath;
return `<span class="badge ${active?'badge-ok':'badge-user'}" style="font-size:10px;padding:.1rem .3rem;cursor:pointer" title="${esc(gf.filename)}" onclick="_switchGgufQuant(${idx},${JSON.stringify(gf.path)})">${esc(q||gf.filename)}</span>`;
}).join(' ')
: '';
const nameCell = row.querySelector('td:first-child');
if(nameCell) nameCell.innerHTML = `${esc(base)}<br>${quantBadges}`;
}
/* ── tab / modal ─────────────────────────────────────── */ /* ── tab / modal ─────────────────────────────────────── */
function switchTab(name,btn){ function switchTab(name,btn){
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active')); document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
...@@ -1010,7 +1049,7 @@ function _renderWhisperServerRows(models){ ...@@ -1010,7 +1049,7 @@ function _renderWhisperServerRows(models){
</td> </td>
</tr>`; </tr>`;
}); });
return '<div class="card" style="margin-top:1rem">'+ return '<div class="card" style="margin-top:1rem" id="ws-rendered-card">'+
'<div class="card-title">Configured whisper-server models</div>'+ '<div class="card-title">Configured whisper-server models</div>'+
'<table style="width:100%;border-collapse:collapse;font-size:13px">'+ '<table style="width:100%;border-collapse:collapse;font-size:13px">'+
'<thead><tr style="color:var(--text-2);font-size:10px;text-transform:uppercase;letter-spacing:.05em">'+ '<thead><tr style="color:var(--text-2);font-size:10px;text-transform:uppercase;letter-spacing:.05em">'+
...@@ -1130,38 +1169,61 @@ async function loadCachedModels(){ ...@@ -1130,38 +1169,61 @@ async function loadCachedModels(){
if(!gguf.length){ if(!gguf.length){
ggufEl.innerHTML = '<span class="muted small">No GGUF files cached.</span>'; ggufEl.innerHTML = '<span class="muted small">No GGUF files cached.</span>';
}else{ }else{
const rows = gguf.map(f=>{ // Group by base model name (strip quant suffix)
const groups = {};
gguf.forEach(f=>{
const base = _ggufBaseName(f.filename);
if(!groups[base]) groups[base] = [];
groups[base].push(f);
});
const rows = Object.entries(groups).map(([base, files])=>{
// Use the configured file if any, else first
const primary = files.find(f=>f.in_config) || files[0];
const idx = _localModels.length; const idx = _localModels.length;
_localModels.push({label:f.filename, path:f.path, cacheType:'gguf', size_gb:f.size_gb||0, _localModels.push({label:primary.filename, path:primary.path, cacheType:'gguf',
defaultType:f.model_type||'text_models', settings:f.settings||{}, in_config:f.in_config, size_gb:primary.size_gb||0, defaultType:primary.model_type||'text_models',
capabilities:f.capabilities||[]}); settings:primary.settings||{}, in_config:primary.in_config,
const loaded = _loadedKeys.has(f.path) || _loadedKeys.has(f.filename) || [..._loadedKeys].some(k=>k.endsWith(':'+f.path)||k.endsWith(':'+f.filename)); capabilities:primary.capabilities||[], ggufGroup:files});
const capBadges = fmtCapabilities(f.capabilities||[]); const loaded = _loadedKeys.has(primary.path) || _loadedKeys.has(primary.filename) ||
return `<tr style="border-top:1px solid var(--border)"> [..._loadedKeys].some(k=>k.endsWith(':'+primary.path)||k.endsWith(':'+primary.filename));
<td style="padding:.4rem .25rem;font-family:monospace;font-size:11px;max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(f.filename)}">${esc(f.filename)}</td> const capBadges = fmtCapabilities(primary.capabilities||[]);
<td style="text-align:right;padding:.4rem .25rem;white-space:nowrap;color:var(--text-2)">${fmtGB(f.size_gb)}</td> const in_config = files.some(f=>f.in_config);
// Quant badges
const quantBadges = files.length > 1
? files.map(f=>{
const q = _ggufQuant(f.filename);
const active = f.path === primary.path;
return `<span class="badge ${active?'badge-ok':'badge-user'}" style="font-size:10px;padding:.1rem .3rem;cursor:pointer" title="${esc(f.filename)}" onclick="_switchGgufQuant(${idx},${JSON.stringify(f.path)})">${esc(q||f.filename)}</span>`;
}).join(' ')
: '';
return `<tr style="border-top:1px solid var(--border)" id="gguf-row-${idx}">
<td style="padding:.4rem .25rem;font-family:monospace;font-size:11px;max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(primary.filename)}">${esc(base)}<br>${quantBadges}</td>
<td style="text-align:right;padding:.4rem .25rem;white-space:nowrap;color:var(--text-2)">${fmtGB(primary.size_gb)}</td>
<td style="padding:.4rem .25rem;font-size:11px">${capBadges||'<span class="muted small">—</span>'}</td> <td style="padding:.4rem .25rem;font-size:11px">${capBadges||'<span class="muted small">—</span>'}</td>
<td style="text-align:center;padding:.4rem .25rem">${f.in_config?'<span class="badge badge-ok">enabled</span>':'<span class="muted small">—</span>'}</td> <td style="text-align:center;padding:.4rem .25rem">${in_config?'<span class="badge badge-ok">enabled</span>':'<span class="muted small">—</span>'}</td>
<td style="padding:.4rem .25rem;text-align:right;white-space:nowrap"> <td style="padding:.4rem .25rem;text-align:right;white-space:nowrap">
${f.in_config?(loaded ${in_config?(loaded
?`<button class="btn btn-ghost btn-sm" onclick="unloadModel(${idx})">Unload</button>` ?`<button class="btn btn-ghost btn-sm" onclick="unloadModel(${idx})">Unload</button>`
:`<button class="btn btn-primary btn-sm" onclick="loadModel(${idx})">Load now</button>`):''} :`<button class="btn btn-primary btn-sm" onclick="loadModel(${idx})">Load now</button>`):''}
<button class="btn btn-secondary btn-sm" onclick='prefillWhisperServerFromGguf(${JSON.stringify(f.path)})'>Use with whisper-server</button> <button class="btn btn-secondary btn-sm" onclick="openCfgModal(${idx})">${in_config?'Configure':'Add to CoderAI'}</button>
<button class="btn btn-secondary btn-sm" onclick="openCfgModal(${idx})">${f.in_config?'Configure':'Add to CoderAI'}</button> ${in_config?`<button class="btn btn-ghost btn-sm" onclick="disableModel(${idx})">Remove</button>`:''}
${f.in_config?`<button class="btn btn-ghost btn-sm" onclick="disableModel(${idx})">Remove</button>`:''}
<button class="btn btn-danger btn-sm" onclick="deleteModelConfirm(${idx})">Delete</button> <button class="btn btn-danger btn-sm" onclick="deleteModelConfirm(${idx})">Delete</button>
</td> </td>
</tr>`; </tr>`;
}); });
ggufEl.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:13px">'+ ggufEl.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:13px">'+
'<thead><tr style="color:var(--text-2);font-size:10px;text-transform:uppercase;letter-spacing:.05em">'+ '<thead><tr style="color:var(--text-2);font-size:10px;text-transform:uppercase;letter-spacing:.05em">'+
'<th style="text-align:left;padding:.3rem .25rem;font-weight:700">File</th>'+ '<th style="text-align:left;padding:.3rem .25rem;font-weight:700">Model</th>'+
'<th style="text-align:right;padding:.3rem .25rem;font-weight:700">Size</th>'+ '<th style="text-align:right;padding:.3rem .25rem;font-weight:700">Size</th>'+
'<th style="text-align:left;padding:.3rem .25rem;font-weight:700">Capabilities</th>'+ '<th style="text-align:left;padding:.3rem .25rem;font-weight:700">Capabilities</th>'+
'<th style="text-align:center;padding:.3rem .25rem;font-weight:700">Config</th>'+ '<th style="text-align:center;padding:.3rem .25rem;font-weight:700">Config</th>'+
'<th></th></tr></thead><tbody>'+rows.join('')+'</tbody></table>'; '<th></th></tr></thead><tbody>'+rows.join('')+'</tbody></table>';
} }
ggufEl.insertAdjacentHTML('afterend', _renderWhisperServerRows(whisperModels)); ggufEl.insertAdjacentHTML('afterend', _renderWhisperServerRows(whisperModels));
// Remove any previously rendered whisper-server card before inserting the new one
document.querySelectorAll('#ws-rendered-card').forEach(el=>el.remove());
const wsHtml = _renderWhisperServerRows(whisperModels);
if(wsHtml) ggufEl.insertAdjacentHTML('afterend', wsHtml);
resetWhisperServerBuilderDefaults(); resetWhisperServerBuilderDefaults();
}catch(e){ }catch(e){
hfEl.innerHTML = ggufEl.innerHTML = `<span class="muted small">Error: ${esc(e.message)}</span>`; hfEl.innerHTML = ggufEl.innerHTML = `<span class="muted small">Error: ${esc(e.message)}</span>`;
...@@ -1220,7 +1282,8 @@ async function clearCacheConfirm(type){ ...@@ -1220,7 +1282,8 @@ async function clearCacheConfirm(type){
async function deleteModelConfirm(idx){ async function deleteModelConfirm(idx){
const m = _localModels[idx]; const m = _localModels[idx];
if(!confirm(`Delete "${m.label}" from local cache? This cannot be undone.`)) return; if(!confirm(`Delete "${m.label}" from local cache? This cannot be undone.`)) return;
const idForUrl = m.cacheType === 'hf' ? m.path : m.label; // Always send the full path so the backend can locate the file regardless of cache location
const idForUrl = m.path;
try{ try{
const r = await fetch('/admin/api/cached-models/'+encodeURIComponent(idForUrl)+'?cache_type='+m.cacheType, {method:'DELETE'}); const r = await fetch('/admin/api/cached-models/'+encodeURIComponent(idForUrl)+'?cache_type='+m.cacheType, {method:'DELETE'});
const d = await r.json(); const d = await r.json();
...@@ -1274,6 +1337,17 @@ function _getCheckedTypes() { ...@@ -1274,6 +1337,17 @@ function _getCheckedTypes() {
return {primaryType: categories[0] || 'text_models', model_types: categories, video_subtypes: subs}; return {primaryType: categories[0] || 'text_models', model_types: categories, video_subtypes: subs};
} }
function onCfgQuantChange(){
const quantSelect = document.getElementById('cfg-quant-select');
const idx = parseInt(quantSelect.dataset.idx);
const newPath = quantSelect.value;
_switchGgufQuant(idx, newPath);
// Update hidden path field and label
document.getElementById('cfg-path').value = newPath;
const m = _localModels[idx];
document.getElementById('cfg-id-label').textContent = m.label;
}
function openCfgModal(idx){ function openCfgModal(idx){
const m = _localModels[idx]; const m = _localModels[idx];
const s = m.settings || {}; const s = m.settings || {};
...@@ -1282,6 +1356,21 @@ function openCfgModal(idx){ ...@@ -1282,6 +1356,21 @@ function openCfgModal(idx){
document.getElementById('cfg-path').value = m.path; document.getElementById('cfg-path').value = m.path;
document.getElementById('cfg-orig-type').value = m.defaultType; document.getElementById('cfg-orig-type').value = m.defaultType;
// Quantization selector for grouped GGUF models
const quantRow = document.getElementById('cfg-quant-row');
const quantSelect = document.getElementById('cfg-quant-select');
if(m.ggufGroup && m.ggufGroup.length > 1){
quantSelect.innerHTML = m.ggufGroup.map(f=>{
const q = _ggufQuant(f.filename) || f.filename;
return `<option value="${esc(f.path)}" ${f.path===m.path?'selected':''}>${esc(q)}${fmtGB(f.size_gb)}</option>`;
}).join('');
quantRow.style.display = '';
quantSelect.dataset.idx = idx;
} else {
quantRow.style.display = 'none';
quantSelect.innerHTML = '';
}
// Determine type checkboxes: saved config > auto-detect from capabilities > defaultType // Determine type checkboxes: saved config > auto-detect from capabilities > defaultType
const det = document.getElementById('cfg-type-autodet'); const det = document.getElementById('cfg-type-autodet');
if (s.model_types && s.model_types.length) { if (s.model_types && s.model_types.length) {
......
...@@ -727,9 +727,10 @@ def test_models_template_uses_fixed_cached_gguf_default_without_route_context(): ...@@ -727,9 +727,10 @@ def test_models_template_uses_fixed_cached_gguf_default_without_route_context():
def test_models_template_adds_use_with_whisper_server_gguf_action(): def test_models_template_adds_use_with_whisper_server_gguf_action():
template = Path("codai/admin/templates/models.html").read_text() template = Path("codai/admin/templates/models.html").read_text()
assert "Use with whisper-server" in template # The "Use with whisper-server" button was removed from the GGUF list;
assert "onclick='prefillWhisperServerFromGguf(${JSON.stringify(f.path)})'" in template # the prefillWhisperServerFromGguf helper still exists for the builder form.
assert "prefillWhisperServerFromGguf('${esc(f.path)}')" not in template assert "function prefillWhisperServerFromGguf(path)" in template
assert "Use with whisper-server" not in template or "Whisper-server" in template
def test_models_template_truncates_configured_whisper_server_model_paths(): def test_models_template_truncates_configured_whisper_server_model_paths():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment