Worked examples#
Four archetypes covering the axes that matter: kind (local vs api) and
target (video/audio/face/generator). Each main.py follows the stdout
protocol from authoring-guide.md. The stub-* folders next
to this skills/ dir are runnable versions of the first three.
A shared preamble every main.py uses:
import argparse, json, os, sys
def emit(line): sys.stdout.write(line + "\n"); sys.stdout.flush()
def load_job():
p = argparse.ArgumentParser(); p.add_argument("--job", required=True)
with open(p.parse_args().job, encoding="utf-8") as f: return json.load(f)
A. Local, target: video — process each selected clip#
plugin.json (excerpt): "kind":"local", "venv":"private", "target":"video",
"input":{"clip":"video","multiple":true}, "result":{"type":"video","place":"bin"},
params for tunables. requirements.txt: your torch/onnx/etc. deps.
def main():
job = load_job()
clips = job["input"]["clips"]
params = job.get("params", {})
outputs = []
for i, clip in enumerate(clips):
emit(f"info:processing {clip['path']}")
out = os.path.join(job["output_dir"], f"out_{i}.mp4")
# ... run the model on clip['path'][clip['in']:clip['out']] → out ...
outputs.append({"type": "video", "path": out})
emit(f"progress:{int((i+1)*100/len(clips))}")
emit("result:" + json.dumps({"outputs": outputs}))
return 0
if __name__ == "__main__": sys.exit(main())
B. Local, target: audio, with a downloaded model#
plugin.json: "target":"audio", "models":[{"name":"sep.onnx","url":"https://…","sha256":"…","size_mb":40}],
"result":{"type":"audio","place":"bin"}.
def main():
job = load_job()
model = os.path.join(os.path.dirname(__file__), "models", "sep.onnx")
if not os.path.exists(model):
emit('need:{"kind":"model","name":"sep.onnx"}'); return 4
clip = job["input"]["clips"][0]
out = os.path.join(job["output_dir"], "vocals.wav")
emit("progress:10")
# ... onnxruntime.InferenceSession(model) → separate clip['path'] → out ...
emit("progress:100")
emit("result:" + json.dumps({"outputs": [{"type": "audio", "path": out}]}))
return 0
C. API, target: generator — text → audio via a paid provider#
plugin.json: "kind":"api", "target":"generator",
"provider":{"name":"elevenlabs","key_setting":"WUNJO_KEY_ELEVENLABS","signup_url":"https://elevenlabs.io"},
"input":{"clip":"none"}, "result":{"type":"audio","place":"bin"},
params for the text and voice. requirements.txt: httpx (light ⇒ venv may
be shared).
def main():
job = load_job()
key = os.environ.get("WUNJO_KEY_ELEVENLABS")
if not key:
emit('need:{"kind":"api_key","provider":"elevenlabs"}'); return 3
text = job["params"].get("text", "")
out = os.path.join(job["output_dir"], "speech.mp3")
emit("info:calling ElevenLabs")
# import httpx; r = httpx.post(url, headers={"xi-api-key": key}, json={...})
# open(out, "wb").write(r.content)
emit("result:" + json.dumps({"outputs": [{"type": "audio", "path": out}]}))
return 0
Note: the key is read from the environment only; it is never in job.json,
argv, or any output line.
D. target: face — act on one detected face#
plugin.json: "target":"face", "input":{"clip":"video"},
"result":{"type":"video","place":"bin"} (or none to just report).
def main():
job = load_job()
face = job["input"].get("face", {})
rect, frame = face.get("rect"), face.get("position") # [x,y,w,h] in 0..1
clip = job["input"]["clips"][0]
emit(f"info:face at {rect} on frame {frame}")
# ... blur / replace / retouch that region across the clip → out ...
emit("result:" + json.dumps({"outputs": [], "message": "done"}))
return 0
Packaging & testing#
python ../pack.py --check my-plugin # validate the manifest
python ../pack.py my-plugin # → dist/my-plugin-<version>.wmplugin
python my-plugin/main.py --job /tmp/job.json # dry-run against a hand-written job
Then in the app: Settings ▸ Plugins ▸ Load Plugins ▸ From archive/folder, review the metadata, Import; the plugin gets its own tab where its environment, models and key are set up before first use.