video: AI upscale/interpolate, township outcomes, configurable tmp dir

Make video enhancement fully AI-on-CoderAI and rework township outcomes.

Upscaling (Real-ESRGAN / SD upscalers):
- Support diffusers-style .safetensors weights + config.json (e.g.
  hlky/RealESRGAN_*), not just classic .pth; infer RRDBNet arch/scale from
  config. fp16 + tiling for performance.
- AI-or-fail: no ffmpeg fallback. Auto-select a configured upscaler when the
  request omits a model (find_capable_model).
- Fix a registry-pollution bug: cache upscalers in a private dict, never under
  a synthetic 'upscale:<id>' key in multi_model_manager.models (which made a
  later request_model() resolve/reload the bogus key -> 400).
- Per-frame progress + a first-class "upscale" task (pause/cancel/thermal),
  with a periodic thermal re-check through the frame loop.

Interpolation (RIFE):
- AI-or-fail: removed the ffmpeg minterpolate fallback. Resolve the
  rife-ncnn-vulkan binary + bundled model robustly, pass exact -n frame count,
  and pin -g to the SAME GPU CoderAI uses (matched by CUDA device name, not a
  hardcoded index). Progress + "interpolate" task + thermal guard.

Township generator:
- One draw per match (not per fighter); longer, configurable outcome videos
  built as a finish -> victory two-shot sequence; richer, more brutal,
  camera-aware prompts (finish/victory templates editable on the Prompts page).
- Stream large results via response_format=url instead of base64-in-JSON;
  per-frame progress for both upscale and interpolate.

Configurable temp dir:
- New --tmp CLI flag and config.tmp_dir (+ admin Settings field, applied live).
  Sets tempfile.tempdir and TMPDIR/TMP/TEMP so all scratch (frame extraction,
  upscaling, interpolation) and child processes use it — fixes
  "[Errno 28] No space left on device" when /tmp is small.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 06e61257
......@@ -2420,6 +2420,7 @@ async def api_get_settings(username: str = Depends(require_admin)):
"tools_closer_prompt": c.tools_closer_prompt,
"grammar_guided": c.grammar_guided,
"parser": c.parser,
"tmp_dir": c.tmp_dir,
}
......@@ -2491,6 +2492,20 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad
c.grammar_guided = bool(data["grammar_guided"])
if "parser" in data:
c.parser = data["parser"]
if "tmp_dir" in data:
# Persisted now and applied live so it takes effect without a restart.
c.tmp_dir = (data["tmp_dir"] or "").strip() or None
if c.tmp_dir:
try:
import tempfile as _tf, os as _os
_td = _os.path.abspath(_os.path.expanduser(c.tmp_dir))
_os.makedirs(_td, exist_ok=True)
_tf.tempdir = _td
_os.environ["TMPDIR"] = _td
_os.environ["TMP"] = _td
_os.environ["TEMP"] = _td
except Exception:
pass
if "archive" in data:
import os as _os
......
......@@ -68,6 +68,11 @@
<input type="text" id="s-offload-dir" class="form-input" placeholder="./offload">
<span class="form-hint">Models will inherit this as default when configured</span>
</div>
<div class="form-row" style="margin-top:.75rem">
<label class="form-label">Temporary working directory <span class="muted">(default: system /tmp)</span></label>
<input type="text" id="s-tmp-dir" class="form-input" placeholder="e.g. /data/tmp">
<span class="form-hint">Scratch space for frame extraction, upscaling and interpolation. Point it at a large volume — 4× upscaling can exhaust a small /tmp ("No space left on device"). Applied live on save.</span>
</div>
</div>
<!-- Archive -->
......@@ -330,6 +335,7 @@ async function loadSettings(){
document.getElementById('s-hf-cache').value = d.models?.hf_cache_dir ?? '';
document.getElementById('s-gguf-cache').value = d.models?.gguf_cache_dir ?? '';
document.getElementById('s-offload-dir').value = d.offload?.directory ?? './offload';
document.getElementById('s-tmp-dir').value = d.tmp_dir ?? '';
toggleHttps();
// Archive
const arc = d.archive || {};
......@@ -397,6 +403,7 @@ async function saveSettings(){
offload:{
directory: document.getElementById('s-offload-dir').value.trim() || './offload',
},
tmp_dir: strOrNull('s-tmp-dir'),
archive:{
enabled: document.getElementById('s-arc-enabled').checked,
directory: document.getElementById('s-arc-dir').value.trim(),
......
......@@ -76,7 +76,7 @@ function fmtTime(s) {
} catch { return ''; }
}
const KIND_LABEL = {training:'Training', image:'Image', video:'Video', audio:'Audio', text:'Text', pipeline:'Pipeline', request:'Request', loading:'Loading'};
const KIND_LABEL = {training:'Training', image:'Image', video:'Video', upscale:'Upscale', interpolate:'Interpolate', audio:'Audio', text:'Text', pipeline:'Pipeline', request:'Request', loading:'Loading'};
const STATUS_BADGE = {
running:'badge-admin', queued:'badge-user', done:'badge-ok', error:'badge-err',
cancelled:'badge-user', interrupted:'badge-warn'
......
This diff is collapsed.
This diff is collapsed.
......@@ -184,6 +184,16 @@ configuration directory (--config DIR, default: OS-specific CoderAI directory).
default=default_config,
help=f"Configuration directory (default: {default_config})",
)
parser.add_argument(
"--tmp",
type=str,
default=None,
metavar="DIR",
help="Base directory for temporary working files (frame extraction, "
"upscaling, interpolation). Overrides config.tmp_dir. Use a large "
"volume when /tmp is small — 4x upscaling can exhaust a small /tmp "
"('No space left on device').",
)
parser.add_argument(
"--debug",
action="store_true",
......
......@@ -163,6 +163,11 @@ class Config:
tools_closer_prompt: bool = False
grammar_guided: bool = False
file_path: Optional[str] = None
# Base directory for temporary working files (frame extraction, upscaling,
# interpolation, etc.). None/empty = the OS default (usually /tmp). Point it at
# a large-capacity volume when /tmp is small — 4× upscaling extracts many large
# frames and can exhaust a small /tmp ("No space left on device").
tmp_dir: Optional[str] = None
hf_chat_templates: list = field(default_factory=list)
reasoning_options: list = field(default_factory=list)
parser: str = "auto"
......@@ -318,6 +323,7 @@ class ConfigManager:
tools_closer_prompt=config_data.get("tools_closer_prompt", False),
grammar_guided=config_data.get("grammar_guided", False),
file_path=config_data.get("file_path"),
tmp_dir=config_data.get("tmp_dir"),
hf_chat_templates=config_data.get("hf_chat_templates", []),
reasoning_options=config_data.get("reasoning_options", []),
parser=config_data.get("parser", "auto")
......@@ -457,6 +463,7 @@ class ConfigManager:
"tools_closer_prompt": self.config.tools_closer_prompt,
"grammar_guided": self.config.grammar_guided,
"file_path": self.config.file_path,
"tmp_dir": self.config.tmp_dir,
"hf_chat_templates": self.config.hf_chat_templates,
"reasoning_options": self.config.reasoning_options,
"parser": self.config.parser
......
......@@ -272,6 +272,24 @@ def main():
config_mgr = ConfigManager(config_dir)
config = config_mgr.load()
# Configure the temporary-working-files directory as early as possible (before
# any tempfile.* call). CLI --tmp overrides config.tmp_dir. Setting both
# tempfile.tempdir and TMPDIR/TMP/TEMP makes every tempfile.* call AND child
# processes (ffmpeg, rife) use it — so 4x upscaling doesn't fill a small /tmp.
_tmp_dir = getattr(args, "tmp", None) or getattr(config, "tmp_dir", None)
if _tmp_dir:
try:
_tmp_dir = os.path.abspath(os.path.expanduser(_tmp_dir))
os.makedirs(_tmp_dir, exist_ok=True)
import tempfile as _tempfile
_tempfile.tempdir = _tmp_dir
os.environ["TMPDIR"] = _tmp_dir
os.environ["TMP"] = _tmp_dir
os.environ["TEMP"] = _tmp_dir
print(f"Temporary working directory: {_tmp_dir}")
except Exception as _e:
print(f"WARNING: could not use tmp dir '{_tmp_dir}': {_e} — using OS default")
# Apply cache directory overrides from config before any cache module is used.
# We set env vars AND patch huggingface_hub.constants in case the library was
# already imported (constants are computed once at import time from env vars).
......
......@@ -3263,6 +3263,22 @@ class MultiModelManager:
return self.models.get(self.default_model)
return None
def find_capable_model(self, *capabilities: str) -> Optional[str]:
"""Return the id of the first configured model whose capabilities include
any of the requested ones, in the priority order given. Used to auto-pick
a default for endpoints that have no per-type default list (e.g. video
upscaling). Returns None when nothing configured matches."""
try:
infos = self.list_models()
except Exception:
return None
for want in capabilities:
for info in infos:
caps = getattr(info, "capabilities", None) or []
if want in caps:
return info.id
return None
def list_models(self) -> List[ModelInfo]:
"""List all available models (configured + runtime aliases) with type/capability metadata."""
from codai.models.capabilities import detect_model_capabilities, ModelCapabilities
......
......@@ -162,7 +162,7 @@ class VideoGenerationResponse(BaseModel):
# ── Standalone operation requests ─────────────────────────────────────────────
class VideoUpscaleRequest(BaseModel):
model: str
model: Optional[str] = None # omit → server auto-selects a configured upscaler
video: str # base64/URL input video
upscale_factor: Optional[int] = 2
response_format: Optional[str] = "url"
......@@ -182,7 +182,7 @@ class VideoSubtitleRequest(BaseModel):
class VideoInterpolateRequest(BaseModel):
model: str
model: Optional[str] = None # interpolation runs via RIFE/ffmpeg; model is advisory
video: Optional[str] = None # base64/URL input video (mutually exclusive with init/end)
init_image: Optional[str] = None # first frame
end_image: Optional[str] = None # last frame
......
This source diff could not be displayed because it is too large. You can view the blob instead.
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