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
44 changes: 44 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,48 @@ 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)

A product photo becomes a 9:16 vertical clip (720×1280, 5 s) with camera motion and ambient
audio, using the fixed workflow `workflows/ocean_ugc_product.json`. Needs a CUDA GPU with
48 GB+ VRAM.

Pick a persistent-storage bucket: it holds ComfyUI's whole base directory, so the ~38 GiB
weight set downloads only on the first launch, and clips are written to the bucket root where
the storage API's `listFiles` (top-level files only) can see them. Without a bucket everything
goes to `/tmp` and is lost on stop.

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

Same image and bucket behavior, but builds a 15-shot vertical reel (704×1280, 5 s per shot)
with one consistent character, using `workflows/ocean_ugc_multishot.json`.

Upload a character reference sheet, type one prompt per line into the shot-list box, then set
ComfyUI's Queue **batch count to 15**. Each shot becomes its own queue item, cancellable
individually from the queue panel. Clips save to the bucket root as `shot_00.mp4` ..
`shot_14.mp4` for assembly in your own editor.

The shot index wraps at the shot count, so a second batch in the same session starts again at
line 0 rather than running off the end of the list. Update that count if you change the number
of lines.

## The shared bootstrap (`ltx-video-ugc-bootstrap.sh`)

Both templates inline this script via `commandFile`. It runs ComfyUI from the image's
read-only bundle with `--base-directory` pointed at the bucket, bypassing the image entrypoint,
which writes to `/root/ComfyUI` and fails wherever the container is not uid 0.

The workflow arrives as userData (`COMFY_WORKFLOW_ID` + gzipped `COMFY_WORKFLOW`) and is
installed at `custom_nodes/<id>/example_workflows/<id>.json`, which ComfyUI serves at
`/api/workflow_templates/<id>/<id>.json` — so `?template=<id>&source=<id>` deep-links it.
`source` must name the module; `source=all` only searches ComfyUI's own templates. It is also
copied into `user/default/workflows/` so it appears in the Workflows sidebar.

Weights are not listed here: the script downloads the HuggingFace URLs carried by the installed
graph itself, so each template fetches only what it loads and a new workflow needs no change
here. A template's `envVars` cannot drive this — nothing merges them into a service container
(`SERVICE_START` has no template id; the container env comes only from `userData`).

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

The classic AUTOMATIC1111 UI (`universonic/stable-diffusion-webui`). The image entrypoint
Expand Down
127 changes: 127 additions & 0 deletions docs/serviceTemplates/ltx-video-ugc-bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/bin/bash
# Bypasses the image entrypoint, which writes to /root/ComfyUI and fails where the container
# is not uid 0.
set -euo pipefail

WF_ID="${COMFY_WORKFLOW_ID:-}"
# Client-supplied, and becomes a directory + filename. userConfigurableEnvVars.validation is not
# enforced node-side, so this is the only guard against traversal. No dots: it becomes a Python
# module name.
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"

# .part then rename: a truncated file in a persistent bucket would look cached forever.
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
# A proxy or HF error page returns a few hundred bytes with HTTP 200; without this floor that
# body would be cached as a model. Smallest real file is ~300 MB.
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"
}

WORKFLOW_JSON=""
if [ -n "$WF_ID" ] && [ -n "${COMFY_WORKFLOW:-}" ]; then
# Pack dir and filename are both $WF_ID, so ?template=<id>&source=<id> resolves with no
# hard-coded name on either side.
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: a corrupt payload must not kill the container.
if printf '%s' "$COMFY_WORKFLOW" | base64 -d | gunzip > "$PACK/example_workflows/$WF_ID.json"; then
cp "$PACK/example_workflows/$WF_ID.json" "$SAVED/$WF_ID.json"
WORKFLOW_JSON="$PACK/example_workflows/$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

# The graph carries the HuggingFace URLs for everything it loads, so each template fetches only
# what it uses and a new workflow needs no edit here. Template envVars can't drive this: nothing
# merges them into a service container.
if [ -n "$WORKFLOW_JSON" ]; then
# `|| true` and the except are both needed: a payload that gunzips but isn't a graph must not
# abort the bootstrap after escrow is claimed.
MODEL_URLS=$(python3.13 - "$WORKFLOW_JSON" <<'PY' || true
import json, re, sys
seen = []
def walk(o):
if isinstance(o, dict):
for v in o.values(): walk(v)
elif isinstance(o, list):
for v in o: walk(v)
elif isinstance(o, str):
for m in re.findall(r'https://huggingface\.co/[^\s\)\]"]+?\.safetensors', o):
if m not in seen: seen.append(m)
try:
walk(json.load(open(sys.argv[1])))
except Exception as e:
print(f'[ocean] cannot read model URLs from the workflow: {e}', file=sys.stderr)
print('\n'.join(seen))
PY
)
if [ -z "$MODEL_URLS" ]; then
echo "[ocean] no model URLs found in the workflow — ComfyUI will start without weights" >&2
fi
for url in $MODEL_URLS; do
case "$url" in
*/text_encoders/*) sub=text_encoders ;;
*/loras/*) sub=loras ;;
*upscaler*) sub=latent_upscale_models ;;
*) sub=checkpoints ;;
esac
# One renamed file must not cost the whole paid session.
get "$url" "$MODELS/$sub/$(basename "$url")" ||
echo "[ocean] could not fetch $(basename "$url") — ComfyUI will start without it" >&2
done
else
echo "[ocean] no workflow supplied — skipping model download" >&2
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-multishot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"id": "ltx-video-ugc-multishot",
"name": "ComfyUI — UGC multishot reel (LTX-2.3)",
"description": "ComfyUI preloaded with LTX-2.3 (IC-LoRA Ingredients) for a 15-shot vertical UGC product reel with one consistent character. Usage: upload a character reference sheet as the workflow's image input, type one prompt per line in its shot-list text box (each line a distinct camera angle/action, e.g. close-up on the product, hands unboxing, walking outdoors), then set ComfyUI's Queue batch count to 15 and hit Queue — each shot runs as its own separate queue item, individually cancellable, and the shot index advances automatically between them. Clips are 9:16 vertical (704×1280, both divisible by 32 as LTX requires) at 5 seconds each (125 frames at 25 fps), saved to the bucket root as shot_00.mp4 .. shot_14.mp4 in order for you to assemble in your own editor — there is no in-container concatenation. Select a persistent-storage bucket: it holds ComfyUI's whole base directory, so the ~39 GiB of weights download once and are reused, and the numbered shot clips land in the bucket root where the storage API's listFiles can see them. 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_multishot",
"name": "Multishot reel (15 shots)",
"description": "Upload a character reference sheet and a 15-line shot list, set Queue batch count to 15, get 15 numbered 9:16 clips (shot_00..shot_14) with one consistent character.",
"file": "workflows/ocean_ugc_multishot.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": 45,
"recommended": 85,
"unit": "GB"
},
{
"kind": "discrete",
"type": "gpu",
"min": 1,
"recommended": 1,
"unit": "count",
"description": "CUDA GPU, 48 GB+ VRAM recommended (LTX-2.3 22B distilled fp8 + Gemma-3-12B encoder)"
}
]
}
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)"
}
]
}
Loading
Loading