cli-anything-transcode-api-v2 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (21) hide show
  1. cli_anything_transcode_api_v2-0.1.0/PKG-INFO +10 -0
  2. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/__init__.py +2 -0
  3. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/__main__.py +5 -0
  4. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/core/__init__.py +4 -0
  5. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/core/client.py +288 -0
  6. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/core/session.py +59 -0
  7. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/skills/SKILL.md +178 -0
  8. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/tests/__init__.py +0 -0
  9. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/tests/test_core.py +237 -0
  10. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/tests/test_full_e2e.py +218 -0
  11. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/transcode_api_v2_cli.py +1680 -0
  12. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/utils/__init__.py +1 -0
  13. cli_anything_transcode_api_v2-0.1.0/cli_anything/transcode_api_v2/utils/formatting.py +56 -0
  14. cli_anything_transcode_api_v2-0.1.0/cli_anything_transcode_api_v2.egg-info/PKG-INFO +10 -0
  15. cli_anything_transcode_api_v2-0.1.0/cli_anything_transcode_api_v2.egg-info/SOURCES.txt +19 -0
  16. cli_anything_transcode_api_v2-0.1.0/cli_anything_transcode_api_v2.egg-info/dependency_links.txt +1 -0
  17. cli_anything_transcode_api_v2-0.1.0/cli_anything_transcode_api_v2.egg-info/entry_points.txt +2 -0
  18. cli_anything_transcode_api_v2-0.1.0/cli_anything_transcode_api_v2.egg-info/requires.txt +2 -0
  19. cli_anything_transcode_api_v2-0.1.0/cli_anything_transcode_api_v2.egg-info/top_level.txt +1 -0
  20. cli_anything_transcode_api_v2-0.1.0/setup.cfg +4 -0
  21. cli_anything_transcode_api_v2-0.1.0/setup.py +21 -0
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: cli-anything-transcode-api-v2
3
+ Version: 0.1.0
4
+ Summary: CLI harness for transcode_api_v2 — video/audio transcoding orchestration API
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: click>=8.0
7
+ Requires-Dist: requests>=2.28
8
+ Dynamic: requires-dist
9
+ Dynamic: requires-python
10
+ Dynamic: summary
@@ -0,0 +1,2 @@
1
+ """cli_anything.transcode_api_v2 — CLI harness for transcode_api_v2."""
2
+ __version__ = "0.1.0"
@@ -0,0 +1,5 @@
1
+ """Allow python -m cli_anything.transcode_api_v2."""
2
+ from cli_anything.transcode_api_v2.transcode_api_v2_cli import cli
3
+
4
+ if __name__ == "__main__":
5
+ cli()
@@ -0,0 +1,4 @@
1
+ from .client import ApiClient, ApiError
2
+ from .session import Session
3
+
4
+ __all__ = ["ApiClient", "ApiError", "Session"]
@@ -0,0 +1,288 @@
1
+ """HTTP API client for transcode_api_v2.
2
+
3
+ All calls go to POST /intf.php?method=Module.Action.
4
+
5
+ Configuration via environment or constructor:
6
+ TRANSCODE_API_BASE_URL — server base URL (e.g. http://host:8080)
7
+ TRANSCODE_API_SNAME — default sname (service namespace)
8
+ TRANSCODE_API_AK — AccessKey (32-char hex string)
9
+ TRANSCODE_API_SK — SecretAccessKey
10
+
11
+ Auth notes
12
+ ----------
13
+ This client supports three auth modes selected by ``auth_mode``:
14
+ "none" — no Authorization header (MonitorApi)
15
+ "standard" — HMAC-SHA1:
16
+ auth_string = AK + "\\n" + auth_time + "\\n" + rand_num
17
+ Authorization = AK + ":" + base64(HMAC-SHA1(auth_string, SK))
18
+ + Auth-Time / Rand-Num headers
19
+ "media" — Authorization = md5(json_body + Auth-Time + Rand-Num + API_INNER_SIGN_TOKEN)
20
+ "inner" — md5(sorted_GET_params + INNER_SIGN_TOKEN), no extra headers
21
+
22
+ For development/testing without a live server, set TRANSCODE_API_MOCK=1 and the
23
+ client will return canned success responses instead of making HTTP calls.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import base64
28
+ import hashlib
29
+ import hmac
30
+ import json
31
+ import os
32
+ import random
33
+ import time
34
+ from typing import Any
35
+
36
+ import requests
37
+
38
+
39
+ class ApiError(Exception):
40
+ """Raised when the API returns errno != 0 or an HTTP error."""
41
+
42
+ def __init__(self, errno: int, errmsg: str, raw: dict | None = None):
43
+ super().__init__(f"[{errno}] {errmsg}")
44
+ self.errno = errno
45
+ self.errmsg = errmsg
46
+ self.raw = raw or {}
47
+
48
+
49
+ def _md5(s: str) -> str:
50
+ return hashlib.md5(s.encode()).hexdigest()
51
+
52
+
53
+ def _hmac_sha1_b64(key: str, message: str) -> str:
54
+ """Return base64-encoded HMAC-SHA1(message, key)."""
55
+ sig = hmac.new(key.encode(), message.encode(), hashlib.sha1).digest()
56
+ return base64.b64encode(sig).decode()
57
+
58
+
59
+ class ApiClient:
60
+ """Thin wrapper around the transcode_api_v2 /intf.php HTTP API."""
61
+
62
+ DEFAULT_TIMEOUT = 30
63
+
64
+ def __init__(
65
+ self,
66
+ base_url: str | None = None,
67
+ sname: str | None = None,
68
+ ak: str | None = None,
69
+ sk: str | None = None,
70
+ timeout: int = DEFAULT_TIMEOUT,
71
+ ):
72
+ self.base_url = (base_url or os.environ.get("TRANSCODE_API_BASE_URL", "http://localhost")).rstrip("/")
73
+ self.sname = sname or os.environ.get("TRANSCODE_API_SNAME", "default")
74
+ self.ak = ak or os.environ.get("TRANSCODE_API_AK", "")
75
+ self.sk = sk or os.environ.get("TRANSCODE_API_SK", "")
76
+ self.timeout = timeout
77
+ self._mock = os.environ.get("TRANSCODE_API_MOCK", "").strip() == "1"
78
+
79
+ # ── Core request machinery ────────────────────────────────────────────────
80
+
81
+ def _standard_auth_headers(self, auth_time: str, rand_num: str) -> dict[str, str]:
82
+ """Build Authorization / Auth-Time / Rand-Num headers (HMAC-SHA1)."""
83
+ auth_string = self.ak + "\n" + auth_time + "\n" + rand_num
84
+ encrypt = _hmac_sha1_b64(self.sk, auth_string)
85
+ return {
86
+ "Authorization": f"{self.ak}:{encrypt}",
87
+ "Auth-Time": auth_time,
88
+ "Rand-Num": rand_num,
89
+ }
90
+
91
+ def _call(
92
+ self,
93
+ method: str,
94
+ get_params: dict[str, Any] | None = None,
95
+ post_params: dict[str, Any] | None = None,
96
+ json_body: dict[str, Any] | None = None,
97
+ auth_mode: str = "standard",
98
+ ) -> dict[str, Any]:
99
+ """Send a request to /intf.php?method=<method>.
100
+
101
+ Returns the ``data`` field of the response on success.
102
+ Raises ApiError on errno != 0.
103
+ """
104
+ if self._mock:
105
+ return self._mock_response(method)
106
+
107
+ r = str(int(time.time()))
108
+ rand_num = str(random.randint(0, 2**31))
109
+ qs: dict[str, str] = {"method": method, "r": r}
110
+ if get_params:
111
+ qs.update({k: str(v) for k, v in get_params.items()})
112
+
113
+ headers: dict[str, str] = {}
114
+ _raw_body: bytes | None = None
115
+ if auth_mode == "standard":
116
+ headers.update(self._standard_auth_headers(r, rand_num))
117
+ elif auth_mode == "media":
118
+ body_str = json.dumps(json_body or {}, ensure_ascii=False, separators=(",", ":"))
119
+ _raw_body = body_str.encode("utf-8")
120
+ media_token = os.environ.get("TRANSCODE_API_MEDIA_TOKEN", "4321e921ea66f3515e445ffc1f793a6f")
121
+ headers["Authorization"] = _md5(body_str + r + rand_num + media_token)
122
+ headers["Auth-Time"] = r
123
+ headers["Rand-Num"] = rand_num
124
+ headers["Content-Type"] = "application/json"
125
+ # auth_mode == "none" → no headers
126
+
127
+ url = f"{self.base_url}/intf.php"
128
+ try:
129
+ if _raw_body is not None:
130
+ resp = requests.post(url, params=qs, data=_raw_body, headers=headers, timeout=self.timeout)
131
+ elif json_body is not None:
132
+ headers["Content-Type"] = "application/json"
133
+ resp = requests.post(url, params=qs, json=json_body, headers=headers, timeout=self.timeout)
134
+ elif post_params:
135
+ resp = requests.post(url, params=qs, data=post_params, headers=headers, timeout=self.timeout)
136
+ else:
137
+ resp = requests.get(url, params=qs, headers=headers, timeout=self.timeout)
138
+ except requests.exceptions.RequestException as exc:
139
+ raise ApiError(-1, f"HTTP error: {exc}")
140
+
141
+ try:
142
+ data = resp.json()
143
+ except ValueError:
144
+ raise ApiError(-1, f"Non-JSON response: {resp.text[:200]}")
145
+
146
+ errno = data.get("errno", 0)
147
+ if errno != 0:
148
+ raise ApiError(errno, data.get("errmsg", "unknown error"), data)
149
+
150
+ return data.get("data", data)
151
+
152
+ def _mock_response(self, method: str) -> dict[str, Any]:
153
+ """Return a canned success response for testing without a live server."""
154
+ if "getDetail" in method or "getById" in method:
155
+ return {"tid": 1, "status": 1, "progress": 100, "errno": 0}
156
+ if "getList" in method:
157
+ return {"list": [], "total_count": 0, "current_page": 1, "total_page": 1}
158
+ if "Health" in method:
159
+ return {"status": "ok"}
160
+ return {"tid": 42}
161
+
162
+ # ── Main module ───────────────────────────────────────────────────────────
163
+
164
+ def task_add(self, service: str, args: dict, bucket: str = "", weight: str = "low") -> dict:
165
+ """Submit a transcoding task. Returns {"tid": <int>}."""
166
+ import base64
167
+ encoded_args = base64.b64encode(json.dumps(args, ensure_ascii=False).encode()).decode()
168
+ return self._call(
169
+ "Main.add",
170
+ get_params={"sname": self.sname},
171
+ post_params={"service": service, "bucket": bucket, "args": encoded_args, "weight": weight},
172
+ )
173
+
174
+ def task_stop(self, tid: int) -> dict:
175
+ """Stop/cancel a transcoding task."""
176
+ return self._call("Main.stop", get_params={"sname": self.sname}, post_params={"tid": str(tid)})
177
+
178
+ def task_get(self, tid: int) -> dict:
179
+ """Get task detail by TID."""
180
+ return self._call("Main.getDetailByTid", get_params={"sname": self.sname}, post_params={"tid": str(tid)})
181
+
182
+ def task_get_many(self, tids: list[int]) -> dict:
183
+ """Get task details for multiple TIDs (comma-separated)."""
184
+ return self._call("Main.getDetailByTids", get_params={"sname": self.sname}, post_params={"tid": ",".join(str(t) for t in tids)})
185
+
186
+ # ── Media module (AI/advanced operations) ────────────────────────────────
187
+
188
+ def media_call(self, action: str, body: dict, service: str = "", bucket: str = "", oss_info: dict | None = None) -> dict:
189
+ """Call a Media.* method with a JSON body."""
190
+ body.setdefault("sname", self.sname)
191
+ if service:
192
+ body["service"] = service
193
+ if bucket:
194
+ body["bucket"] = bucket
195
+ if oss_info:
196
+ body["oss_info"] = oss_info
197
+ return self._call(f"Media.{action}", get_params={"sname": self.sname}, json_body=body, auth_mode="media")
198
+
199
+ def media_stop(self, tid: int) -> dict:
200
+ return self.media_call("stop", {"tid": tid})
201
+
202
+ def media_get(self, tid: int) -> dict:
203
+ return self.media_call("getDetailByTid", {"tid": tid})
204
+
205
+ def media_get_many(self, tids: list[int]) -> dict:
206
+ return self.media_call("getDetailByTids", {"tid": tids[0] if len(tids) == 1 else tids})
207
+
208
+ # ── Template module ───────────────────────────────────────────────────────
209
+
210
+ def template_list(self, eqid: str, page: int = 1, rows: int = 10) -> dict:
211
+ return self._call(
212
+ "Template.getList",
213
+ post_params={"eqid": eqid, "page": str(page), "row": str(rows)},
214
+ auth_mode="standard",
215
+ )
216
+
217
+ def template_get(self, eqid: str, template_id: int) -> dict:
218
+ return self._call(
219
+ "Template.getById",
220
+ post_params={"eqid": eqid, "template_id": str(template_id)},
221
+ auth_mode="standard",
222
+ )
223
+
224
+ def template_add(self, uqid: str, eqid: str, name: str, args: dict, template_type: int = 1, des: str = "") -> dict:
225
+ import base64
226
+ encoded = base64.b64encode(json.dumps(args, ensure_ascii=False).encode()).decode()
227
+ return self._call(
228
+ "Template.add",
229
+ post_params={"uqid": uqid, "eqid": eqid, "name": name, "args": encoded, "type": str(template_type), "des": des},
230
+ auth_mode="standard",
231
+ )
232
+
233
+ def template_delete(self, eqid: str, template_id: int) -> dict:
234
+ return self._call(
235
+ "Template.delById",
236
+ post_params={"eqid": eqid, "template_id": str(template_id)},
237
+ auth_mode="standard",
238
+ )
239
+
240
+ # ── TemplateTask module ───────────────────────────────────────────────────
241
+
242
+ def template_task_list(self, eqid: str, task_type: int = 1, page: int = 1, rows: int = 10) -> dict:
243
+ return self._call(
244
+ "TemplateTask.getList",
245
+ post_params={"eqid": eqid, "task_type": str(task_type), "page": str(page), "row": str(rows)},
246
+ auth_mode="standard",
247
+ )
248
+
249
+ def template_task_get(self, eqid: str, task_id: int) -> dict:
250
+ return self._call(
251
+ "TemplateTask.getById",
252
+ post_params={"eqid": eqid, "id": str(task_id)},
253
+ auth_mode="standard",
254
+ )
255
+
256
+ def template_task_presign(self, filename: str) -> dict:
257
+ return self._call(
258
+ "TemplateTask.getPreSignUrl",
259
+ post_params={"sname": self.sname, "filename": filename},
260
+ auth_mode="standard",
261
+ )
262
+
263
+ def template_task_download_url(self, filename: str) -> dict:
264
+ return self._call(
265
+ "TemplateTask.getDownloadUrl",
266
+ post_params={"sname": self.sname, "filename": filename},
267
+ auth_mode="standard",
268
+ )
269
+
270
+ # ── Monitor module ────────────────────────────────────────────────────────
271
+
272
+ def health(self) -> dict:
273
+ return self._call("MonitorApi.getHealth", auth_mode="none")
274
+
275
+ # ── LiveStreamTask module ─────────────────────────────────────────────────
276
+
277
+ def live_task_list(self) -> dict:
278
+ return self._call("LiveStreamTask.getTaskList", auth_mode="standard")
279
+
280
+ def live_task_get(self, tid: str) -> dict:
281
+ return self._call("LiveStreamTask.getTaskDetail", get_params={"tid": tid}, auth_mode="standard")
282
+
283
+ def live_task_stop(self, cid: str, tid: str) -> dict:
284
+ return self._call(
285
+ "LiveStreamTask.stop",
286
+ post_params={"cid": cid, "tid": tid},
287
+ auth_mode="standard",
288
+ )
@@ -0,0 +1,59 @@
1
+ """In-memory session for multi-step workflows."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Any
6
+
7
+
8
+ class Session:
9
+ """Store named task results across REPL commands.
10
+
11
+ Results are stored under user-supplied aliases so later commands can
12
+ reference the tid or status of a previous step.
13
+ """
14
+
15
+ def __init__(self) -> None:
16
+ self._store: dict[str, dict[str, Any]] = {}
17
+ self._history: list[dict[str, Any]] = []
18
+
19
+ def store(self, alias: str, result: dict[str, Any]) -> None:
20
+ self._store[alias] = result
21
+ self._history.append({"alias": alias, "result": result})
22
+
23
+ def get(self, alias: str) -> dict[str, Any]:
24
+ if alias not in self._store:
25
+ raise KeyError(
26
+ f"No result stored under {alias!r}. "
27
+ f"Available: {list(self._store)}"
28
+ )
29
+ return self._store[alias]
30
+
31
+ def get_tid(self, alias: str) -> int:
32
+ result = self.get(alias)
33
+ tid = result.get("tid")
34
+ if tid is None:
35
+ raise ValueError(
36
+ f"Stored result {alias!r} has no tid. "
37
+ f"Full result: {json.dumps(result)}"
38
+ )
39
+ return int(tid)
40
+
41
+ def list_aliases(self) -> list[str]:
42
+ return list(self._store)
43
+
44
+ def history(self) -> list[dict[str, Any]]:
45
+ return list(self._history)
46
+
47
+ def clear(self) -> None:
48
+ self._store.clear()
49
+ self._history.clear()
50
+
51
+ def summary(self) -> str:
52
+ if not self._store:
53
+ return "(empty session)"
54
+ lines = []
55
+ for alias, result in self._store.items():
56
+ tid = result.get("tid", result.get("id", "?"))
57
+ status = result.get("status", "?")
58
+ lines.append(f" {alias}: tid={tid} status={status}")
59
+ return "\n".join(lines)
@@ -0,0 +1,178 @@
1
+ ---
2
+ name: "cli-anything-transcode-api-v2"
3
+ description: "CLI for transcode_api_v2 — submit/track video transcoding tasks, call AI media ops (VideoClip, AudioExtract, WaterMark, etc.), manage templates, and monitor live-stream jobs via the /intf.php API."
4
+ triggers:
5
+ - "transcode video"
6
+ - "submit transcoding task"
7
+ - "task status"
8
+ - "media clip"
9
+ - "video processing api"
10
+ - "transcode_api_v2"
11
+ - "template transcoding"
12
+ - "live stream task"
13
+ ---
14
+
15
+ # cli-anything-transcode-api-v2
16
+
17
+ CLI harness for **transcode_api_v2** — a PHP video/audio transcoding API. Wraps
18
+ `POST /intf.php?method=Module.Action` for all task management, AI media processing,
19
+ template management, and monitoring operations.
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ cd agent-harness
25
+ pip install -e .
26
+ ```
27
+
28
+ ## Configuration
29
+
30
+ ```bash
31
+ export TRANSCODE_API_BASE_URL=http://your-server
32
+ export TRANSCODE_API_SNAME=my_service
33
+ export TRANSCODE_API_SECRET=signing_secret
34
+
35
+ # Development — no live server needed
36
+ export TRANSCODE_API_MOCK=1
37
+ ```
38
+
39
+ ## Base Syntax
40
+
41
+ ```
42
+ cli-anything-transcode-api-v2 [--base-url URL] [--sname NS] [--secret S] [--json] [--repl] \
43
+ <group> <subcommand> [OPTIONS]
44
+ ```
45
+
46
+ - `--json` — emit machine-readable JSON on stdout
47
+ - `--repl` — launch interactive REPL
48
+ - `--save-as ALIAS` — store result for later reference
49
+
50
+ ## Command Groups
51
+
52
+ ### task — VOD transcoding (Main module)
53
+
54
+ ```bash
55
+ cli-anything-transcode-api-v2 task add --service <svc> --args '<JSON>' [--bucket B] [--weight low|normal|high]
56
+ cli-anything-transcode-api-v2 task stop <tid>
57
+ cli-anything-transcode-api-v2 task get <tid>
58
+ cli-anything-transcode-api-v2 task get-many <tid1> <tid2> ...
59
+ cli-anything-transcode-api-v2 task wait <tid> [--timeout 300] [--interval 5]
60
+ ```
61
+
62
+ ### media — AI/advanced media (Media module)
63
+
64
+ ```bash
65
+ cli-anything-transcode-api-v2 media actions
66
+ cli-anything-transcode-api-v2 media call <ACTION> '<JSON_BODY>'
67
+ cli-anything-transcode-api-v2 media get <tid>
68
+ cli-anything-transcode-api-v2 media stop <tid>
69
+ ```
70
+
71
+ Available actions: Snapshot, AudioExtract, VideoClip, ConcatVideo, PicToVideo, WaterMark,
72
+ Subtitles, AutoSubtitles, ContentSummary, VideoAddBgm, VideoStretch, AudioConcat,
73
+ VideoCutGif, VideoCutByRatio, VideoFade, VideoAddMask, ImageEnlargementToVideo,
74
+ MediaAdjustVolume, VideoAddDub, VideoMute, AudioCutByTime, VideoReverse, VideoRotate,
75
+ ImageToVideo, VideoAddAudio, MediaSeparatiVoice, VideoQualityAssessment, AudioStretch,
76
+ AIRemoveWaterMark, PicToSpecialEffectsVideo.
77
+
78
+ ### template — Reusable templates
79
+
80
+ ```bash
81
+ cli-anything-transcode-api-v2 template list --eqid <eqid>
82
+ cli-anything-transcode-api-v2 template get --eqid <eqid> <id>
83
+ cli-anything-transcode-api-v2 template add --eqid <eqid> --uqid <uqid> --name <n> --args '<JSON>'
84
+ cli-anything-transcode-api-v2 template delete --eqid <eqid> <id>
85
+ ```
86
+
87
+ ### template-task — Template-based tasks
88
+
89
+ ```bash
90
+ cli-anything-transcode-api-v2 template-task list --eqid <eqid>
91
+ cli-anything-transcode-api-v2 template-task get --eqid <eqid> <id>
92
+ cli-anything-transcode-api-v2 template-task presign <filename>
93
+ cli-anything-transcode-api-v2 template-task download-url <filename>
94
+ ```
95
+
96
+ ### live — Live-stream tasks
97
+
98
+ ```bash
99
+ cli-anything-transcode-api-v2 live list
100
+ cli-anything-transcode-api-v2 live get <tid>
101
+ cli-anything-transcode-api-v2 live stop --cid <cid> --tid <tid>
102
+ ```
103
+
104
+ ### monitor — Health check (no auth)
105
+
106
+ ```bash
107
+ cli-anything-transcode-api-v2 monitor health
108
+ ```
109
+
110
+ ### session — In-memory result store
111
+
112
+ ```bash
113
+ cli-anything-transcode-api-v2 session list
114
+ cli-anything-transcode-api-v2 session get <alias>
115
+ cli-anything-transcode-api-v2 session clear
116
+ ```
117
+
118
+ ## Output Format
119
+
120
+ **Human** (default):
121
+ ```
122
+ tid: 12345
123
+ status: 1 (done)
124
+ progress: 100
125
+ ```
126
+
127
+ **JSON** (`--json`):
128
+ ```json
129
+ {"tid": 12345, "status": 1, "progress": 100, "errno": 0}
130
+ ```
131
+
132
+ **Error** (`--json`):
133
+ ```json
134
+ {"errno": 120001, "errmsg": "task not found"}
135
+ ```
136
+
137
+ ## Common Workflows
138
+
139
+ ### Submit and poll a transcoding job
140
+
141
+ ```bash
142
+ # Submit
143
+ cli-anything-transcode-api-v2 --json task add \
144
+ --service transcode \
145
+ --args '{"input":"https://cdn.example.com/source.mp4","output":"mp4"}' \
146
+ --save-as job1
147
+
148
+ # Poll until done (up to 10 minutes)
149
+ cli-anything-transcode-api-v2 task wait 12345 --timeout 600
150
+ ```
151
+
152
+ ### Call an AI media operation
153
+
154
+ ```bash
155
+ cli-anything-transcode-api-v2 --json media call VideoClip \
156
+ '{"sname":"my_service","args":{"video_url":"https://...","start":5,"end":30}}'
157
+ # Returns: {"tid": 99001}
158
+ ```
159
+
160
+ ### REPL session
161
+
162
+ ```
163
+ $ cli-anything-transcode-api-v2 --repl
164
+ transcode-api-v2> monitor health
165
+ transcode-api-v2> task get 12345 --save-as result
166
+ transcode-api-v2> session get result
167
+ transcode-api-v2> exit
168
+ ```
169
+
170
+ ## Agent Guidance
171
+
172
+ 1. Always use `--json` for programmatic output.
173
+ 2. `"errno": 0` means success; non-zero is an error — read `"errmsg"`.
174
+ 3. Task status: 0=queued, 1=done, 2=failed, 3=sending, 6=processing.
175
+ 4. Use `task wait` to block until a task completes rather than polling manually.
176
+ 5. Use `--save-as` to chain results without parsing JSON between steps.
177
+ 6. Use `TRANSCODE_API_MOCK=1` to test workflows without a live server.
178
+ 7. The `media call` command accepts any `Media.*` action — run `media actions` for the full list.