Skip to content
Draft
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
48 changes: 48 additions & 0 deletions docs/serviceTemplates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ Templates are validated against `ServiceTemplateSchema`
| `image` + exactly one of `tag` / `checksum` / `dockerfile` | Image spec (`dockerfile` triggers a build and is gated per daemon by `allowImageBuild`) |
| `exposedPorts` | Container ports forwarded to host ports and returned as service endpoints |
| `command` / `entrypoint` | Docker CMD / ENTRYPOINT overrides |
| `commandFile` | Path to a script, resolved relative to this directory and inlined as `command[0]` at load time (mutually exclusive with `command`) |
| `envVars` | Fixed operator-set env vars (values never returned to callers) |
| `userConfigurableEnvVars` | Env vars the consumer supplies via ECIES-encrypted `userData` (optional regex `validation`, `sensitive` UI hint) |
| `requiredResources` / `recommendedResources` | Gate/score environment selection (`min` is enforced at `SERVICE_START`) |
| `workflows` | Selectable graphs for UI-driven templates; each entry is `id` / `name` / `file` (a path to the graph JSON, resolved relative to this directory and inlined into `graph` at load time) |

## Templates in this folder

Expand Down Expand Up @@ -101,6 +103,52 @@ so no `command` override is needed. Bundles ComfyUI-Manager for installing check
custom nodes from the UI; `HF_TOKEN` / `CIVITAI_TOKEN` are optional user env vars for gated
downloads. ~10 GB VRAM for SDXL.

### `ltx-video-ugc-product.json` — ComfyUI, LTX-2.3 product video (GPU)

ComfyUI preloaded with LTX-2.3 for one fixed workflow (`workflows/ocean_ugc_product.json`): a
product photo becomes a vertical 9:16 (720×1280) clip with camera motion and ambient audio,
capped at the graph's 5 seconds (126 frames at 25 fps — trim longer voiceovers or raise the
frame count). `entrypoint`/`commandFile` override to a bootstrap script
(`ltx-video-ugc-bootstrap.sh`, inlined as `command[0]` at load time) that downloads the
~38 GiB weight set (LTX-2.3 22B fp8 checkpoint, Gemma-3-12B text encoder, two LoRAs, the x2
spatial upscaler) on first launch. Pick a persistent-storage bucket for this template: weights
live under the bucket, so only the first launch pays the download, and generated clips are
written to the bucket root (via ComfyUI's `--output-directory`) so the storage API's
`listFiles` — which only lists the bucket's top-level files — can see them. Without a bucket
the weights land inside the container and are lost on stop (in `/tmp`, not the bucket).

The bootstrap runs `main.py` directly from the image's read-only bundle with ComfyUI's own
`--base-directory` flag, pointed at the bucket, so models/input/user/custom_nodes all live
there and nothing is written under `/root`. This deliberately bypasses the image's entrypoint,
which copies its bundle into `/root/ComfyUI` and therefore fails on hosts where the service
container is not uid 0. Relocating `custom_nodes` also means the image's bundled
ComfyUI-Manager is not loaded — this template ships one fixed workflow and installs no nodes at
runtime.

The workflow graph is delivered via userData: `COMFY_WORKFLOW_ID` (the workflow's id, e.g.
`ocean_ugc_product`) plus `COMFY_WORKFLOW` (the graph JSON, gzipped then base64-encoded). The
bootstrap writes it into a minimal custom-node pack named after that id —
`custom_nodes/<id>/example_workflows/<id>.json` — which ComfyUI serves at
`/api/workflow_templates/<id>/<id>.json`, so `?template=<id>&source=<id>` on the ComfyUI URL loads
it directly. Naming the pack after the id is what lets the client build that link from the template
alone, without either side hard-coding a path; `source` must be the module name, since ComfyUI
resolves `source=all` only against its own core templates. The id therefore becomes a directory
name, so its validation excludes dots (it is imported as a Python module). The graph is also copied
into `user/default/workflows/`, where it appears in ComfyUI's Workflows sidebar as an ordinary saved
workflow — no template-browser resolution or URL parameters needed, and the fallback if the deep
link does not fire.
Needs a CUDA GPU with 48 GB+ VRAM (LTX-2.3 22B fp8 + Gemma-3-12B encoder).

### `ltx-video-ugc-testimonial.json` — ComfyUI, LTX-2.3 creator testimonial (GPU)

Same image, bootstrap script, and bucket/weight-download behavior as
`ltx-video-ugc-product.json` above (see that section for details), but with a different fixed
workflow (`workflows/ocean_ugc_testimonial.json`, delivered via `COMFY_WORKFLOW_ID=
ocean_ugc_testimonial` + `COMFY_WORKFLOW`): a creator photo plus a voice clip becomes a
lip-synced vertical 9:16 (720×1280) testimonial, capped at the same 5 seconds (126 frames at
25 fps). Generated clips likewise land in the bucket root so the storage API can list them.
Needs a CUDA GPU with 48 GB+ VRAM (LTX-2.3 22B fp8 + Gemma-3-12B encoder).

### `automatic1111.json` — Stable Diffusion WebUI (A1111) (GPU)

The classic AUTOMATIC1111 UI (`universonic/stable-diffusion-webui`). The image entrypoint
Expand Down
101 changes: 101 additions & 0 deletions docs/serviceTemplates/ltx-video-ugc-bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#!/bin/bash
# The image's own entrypoint copies its ComfyUI bundle into /root/ComfyUI, which fails on
# hosts where the service container is not uid 0 — bypassed here in favor of running ComfyUI
# straight from the read-only bundle via --base-directory.
set -euo pipefail

WF_ID="${COMFY_WORKFLOW_ID:-}"
# COMFY_WORKFLOW_ID is client-supplied and becomes both a directory and a filename.
# ServiceStartHandler never loads templates, so userConfigurableEnvVars.validation is NOT enforced
# node-side — this check is the only thing standing between userData and a path traversal. No dots:
# the directory becomes a Python module name that ComfyUI imports.
if [ -n "$WF_ID" ] && ! [[ "$WF_ID" =~ ^[A-Za-z0-9_-]{1,64}$ ]]; then
echo "[ocean] invalid COMFY_WORKFLOW_ID" >&2
exit 1
fi

echo "[ocean] $(id) | /data/outputs: $(ls -ld /data/outputs 2>&1 | head -1)"

if [ -d /data/outputs ]; then
BASE=/data/outputs/comfy
OUTPUT_DIR_ARGS="--output-directory /data/outputs"
else
BASE=/tmp/comfy
OUTPUT_DIR_ARGS=""
echo "[ocean] no bucket mounted — models download to the container and are lost on stop." \
"If you did select a bucket, the node is not applying outputBucketId: check that it runs" \
"ocean-node with service bucket-mount support, and that persistentStorage is configured." >&2
fi

MODELS="$BASE/models"
mkdir -p "$MODELS/checkpoints" "$MODELS/loras" "$MODELS/text_encoders" \
"$MODELS/latent_upscale_models" "$BASE/output" "$BASE/input" "$BASE/temp" "$BASE/user"

# Download to .part then rename: a truncated file in a persistent bucket would be treated as
# cached by every future launch.
get() {
if [ -f "$2" ]; then
echo "[ocean] cached $(basename "$2")"
return 0
fi
echo "[ocean] downloading $(basename "$2")"
http_code=$(curl -L --retry 5 --retry-delay 5 -C - -o "$2.part" -w '%{http_code}' "$1")
if [ "$http_code" = "416" ] && [ -f "$2.part" ]; then
echo "[ocean] $(basename "$2").part already complete"
elif [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
echo "[ocean] download failed for $(basename "$2") (HTTP $http_code)" >&2
return 22
fi
# HTTP 200 is not proof of a model file: a proxy or HF error page returns a few hundred bytes
# with a success status. Without this floor that body gets renamed into place and every later
# launch treats it as cached. The smallest real file here is ~300 MB, so 10 MB is unambiguous.
size=$(wc -c < "$2.part")
if [ "$size" -lt 10485760 ]; then
echo "[ocean] $(basename "$2") is only $size bytes — not a model file. First bytes:" >&2
head -c 300 "$2.part" >&2 || true
echo >&2
rm -f "$2.part"
return 22
fi
mv "$2.part" "$2"
}

HF=https://huggingface.co
get "$HF/Lightricks/LTX-2.3-fp8/resolve/main/ltx-2.3-22b-dev-fp8.safetensors" \
"$MODELS/checkpoints/ltx-2.3-22b-dev-fp8.safetensors"
get "$HF/Comfy-Org/ltx-2/resolve/main/split_files/text_encoders/gemma_3_12B_it_fp4_mixed.safetensors" \
"$MODELS/text_encoders/gemma_3_12B_it_fp4_mixed.safetensors"
get "$HF/Comfy-Org/ltx-2.3/resolve/main/split_files/loras/ltx_2.3_22b_distilled_1.1_lora_dynamic_fro09_avg_rank_111_bf16.safetensors" \
"$MODELS/loras/ltx_2.3_22b_distilled_1.1_lora_dynamic_fro09_avg_rank_111_bf16.safetensors"
get "$HF/Comfy-Org/ltx-2/resolve/main/split_files/loras/gemma-3-12b-it-abliterated_lora_rank64_bf16.safetensors" \
"$MODELS/loras/gemma-3-12b-it-abliterated_lora_rank64_bf16.safetensors"
get "$HF/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors" \
"$MODELS/latent_upscale_models/ltx-2.3-spatial-upscaler-x2-1.1.safetensors"

if [ -n "$WF_ID" ] && [ -n "${COMFY_WORKFLOW:-}" ]; then
# Pack directory and graph filename are both $WF_ID, so the client's deep link
# (?template=<id>&source=<id>) resolves without either side hard-coding a name.
PACK="$BASE/custom_nodes/$WF_ID"
SAVED="$BASE/user/default/workflows"
mkdir -p "$PACK/example_workflows" "$SAVED"
echo 'NODE_CLASS_MAPPINGS = {}' > "$PACK/__init__.py"
# Escrow is already claimed by the time this runs — a corrupt payload must not take the container
# down with it. Degrade to no workflow and keep going; the decode's stderr lands in the logs.
if printf '%s' "$COMFY_WORKFLOW" | base64 -d | gunzip > "$PACK/example_workflows/$WF_ID.json"; then
# Also a saved workflow, so it appears in ComfyUI's sidebar without the template browser.
cp "$PACK/example_workflows/$WF_ID.json" "$SAVED/$WF_ID.json"
echo "[ocean] installed workflow $WF_ID (template pack + Workflows sidebar)"
else
echo "[ocean] failed to decode COMFY_WORKFLOW — starting ComfyUI without a workflow" >&2
rm -f "$PACK/example_workflows/$WF_ID.json"
fi
fi

export HOME="$BASE"
export PYTHONPYCACHEPREFIX="$BASE/.cache/pycache"
export XDG_CACHE_HOME="$BASE/.cache"
export HF_HOME="$BASE/.cache/huggingface"

echo "[ocean] starting ComfyUI with base directory $BASE"
exec python3.13 /default-comfyui-bundle/ComfyUI/main.py \
--base-directory "$BASE" ${OUTPUT_DIR_ARGS} --listen --port 8188 ${CLI_ARGS:-}
60 changes: 60 additions & 0 deletions docs/serviceTemplates/ltx-video-ugc-product.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"id": "ltx-video-ugc-product",
"name": "ComfyUI — UGC product video (LTX-2.3)",
"description": "ComfyUI preloaded with LTX-2.3 for vertical short-form product video: a product photo becomes a 9:16 clip with camera motion and ambient audio, capped at the graph's 5 seconds (126 frames at 25 fps). Select a persistent-storage bucket: it holds ComfyUI's whole base directory, so the 38 GiB of weights download once and are reused, and generated clips land in the bucket root. Needs a CUDA GPU with 48 GB+ VRAM.",
"image": "yanwk/comfyui-boot",
"tag": "cu130-megapak-pt211-20260803",
"exposedPorts": [
8188
],
"entrypoint": [
"/bin/bash",
"-c"
],
"commandFile": "ltx-video-ugc-bootstrap.sh",
"workflows": [
{
"id": "ocean_ugc_product",
"name": "Product → vertical clip",
"description": "Upload a product photo, get a 9:16 clip with camera motion and ambient audio.",
"file": "workflows/ocean_ugc_product.json"
}
],
"userConfigurableEnvVars": [
{
"key": "COMFY_WORKFLOW_ID",
"validation": "^[A-Za-z0-9_-]+$"
},
{
"key": "COMFY_WORKFLOW"
}
],
"requiredResources": [
{
"id": "cpu",
"min": 8,
"recommended": 16,
"unit": "cores"
},
{
"id": "ram",
"min": 32,
"recommended": 64,
"unit": "GB"
},
{
"id": "disk",
"min": 40,
"recommended": 80,
"unit": "GB"
},
{
"kind": "discrete",
"type": "gpu",
"min": 1,
"recommended": 1,
"unit": "count",
"description": "CUDA GPU, 48 GB+ VRAM recommended (LTX-2.3 22B fp8 + Gemma-3-12B encoder)"
}
]
}
60 changes: 60 additions & 0 deletions docs/serviceTemplates/ltx-video-ugc-testimonial.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"id": "ltx-video-ugc-testimonial",
"name": "ComfyUI — UGC creator testimonial (LTX-2.3)",
"description": "ComfyUI preloaded with LTX-2.3 for vertical short-form UGC video: a creator photo plus a voice clip becomes a lip-synced 9:16 testimonial, capped at the graph's 5 seconds (126 frames at 25 fps). Select a persistent-storage bucket: it holds ComfyUI's whole base directory, so the 38 GiB of weights download once and are reused, and generated clips land in the bucket root. Needs a CUDA GPU with 48 GB+ VRAM.",
"image": "yanwk/comfyui-boot",
"tag": "cu130-megapak-pt211-20260803",
"exposedPorts": [
8188
],
"entrypoint": [
"/bin/bash",
"-c"
],
"commandFile": "ltx-video-ugc-bootstrap.sh",
"workflows": [
{
"id": "ocean_ugc_testimonial",
"name": "Creator testimonial (lip-sync)",
"description": "Upload a person photo and a voice clip, get a lip-synced 9:16 testimonial.",
"file": "workflows/ocean_ugc_testimonial.json"
}
],
"userConfigurableEnvVars": [
{
"key": "COMFY_WORKFLOW_ID",
"validation": "^[A-Za-z0-9_-]+$"
},
{
"key": "COMFY_WORKFLOW"
}
],
"requiredResources": [
{
"id": "cpu",
"min": 8,
"recommended": 16,
"unit": "cores"
},
{
"id": "ram",
"min": 32,
"recommended": 64,
"unit": "GB"
},
{
"id": "disk",
"min": 40,
"recommended": 80,
"unit": "GB"
},
{
"kind": "discrete",
"type": "gpu",
"min": 1,
"recommended": 1,
"unit": "count",
"description": "CUDA GPU, 48 GB+ VRAM recommended (LTX-2.3 22B fp8 + Gemma-3-12B encoder)"
}
]
}
Loading
Loading