@acedatacloud/skills 2026.805.0 → 2026.806.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acedatacloud/skills",
3
- "version": "2026.805.0",
3
+ "version": "2026.806.0",
4
4
  "description": "Agent Skills for AceDataCloud AI services — music, image, video generation, LLM chat, web search. Compatible with Claude Code, GitHub Copilot, Gemini CLI, OpenAI Codex, and 30+ AI coding agents.",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -150,7 +150,7 @@ For best results follow this multi-step workflow:
150
150
  | Parameter | Type | Description |
151
151
  |-----------|------|-------------|
152
152
  | `lyric_prompt` | object | Structured prompt payload for auto-generating lyrics (used when `custom: true` without explicit `lyric`) |
153
- | `style_negative` | string | Style tags to avoid (e.g., `"heavy metal, distortion"`) |
153
+ | `negative_tags` | string | Style or genre tags to avoid (e.g., `"heavy metal, distortion"`); used in custom mode |
154
154
  | `style_influence` | number | Strength of style influence (advanced custom mode, v5+ only) |
155
155
  | `audio_weight` | number | Weight for audio reference when covering (advanced, v5+ only) |
156
156
  | `duration` | integer | Target track length in seconds (typically 10–360). Best supported on `generate` with `custom: true` on newer models such as `chirp-v5-5` |
@@ -46,7 +46,16 @@ they will fail with `scope_not_authorized`.
46
46
 
47
47
  This is the working path. The video lands in the creator's TikTok **inbox /
48
48
  drafts**; they open the TikTok app to add caption, sound and privacy, then post.
49
- Confirm with the user before uploading.
49
+
50
+ Required order:
51
+
52
+ 1. Download the exact video locally, but do not upload it yet.
53
+ 2. Call `request_action_confirmation` with `kind: "generic"`, the real video
54
+ preview, and a summary that says this uploads to drafts rather than publishing.
55
+ 3. If cancelled, stop. If confirmed, run exactly one upload:
56
+ `python3 skills/tiktok/scripts/tiktok.py upload video.mp4`.
57
+ 4. Poll with `python3 skills/tiktok/scripts/tiktok.py status PUBLISH_ID` until
58
+ terminal. Never call `upload` again while polling.
50
59
 
51
60
  Two source modes. **`FILE_UPLOAD` is the reliable one** — `PULL_FROM_URL` only
52
61
  works from a domain verified in the developer portal, and ours is not verified
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env python3
2
+ """Upload one video to TikTok inbox using FILE_UPLOAD, or fetch status."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import os
9
+ import sys
10
+ import urllib.error
11
+ import urllib.request
12
+
13
+ BASE_URL = "https://open.tiktokapis.com/v2"
14
+ MAX_CHUNK_SIZE = 64 * 1024 * 1024
15
+
16
+
17
+ def output(value: object) -> None:
18
+ print(json.dumps(value, ensure_ascii=False, indent=2))
19
+
20
+
21
+ def fail(message: str) -> None:
22
+ output({"ok": False, "error": message})
23
+ raise SystemExit(1)
24
+
25
+
26
+ def token() -> str:
27
+ value = os.environ.get("TIKTOK_TOKEN")
28
+ if not value:
29
+ fail("TIKTOK_TOKEN is not set — reconnect TikTok and retry")
30
+ return value
31
+
32
+
33
+ def api(path: str, body: dict) -> dict:
34
+ request = urllib.request.Request(
35
+ f"{BASE_URL}{path}",
36
+ data=json.dumps(body).encode(),
37
+ headers={"Authorization": f"Bearer {token()}", "Content-Type": "application/json"},
38
+ method="POST",
39
+ )
40
+ try:
41
+ with urllib.request.urlopen(request, timeout=30) as response:
42
+ payload = json.loads(response.read().decode())
43
+ except urllib.error.HTTPError as error:
44
+ raw = error.read().decode("utf-8", "replace")
45
+ try:
46
+ payload = json.loads(raw)
47
+ except json.JSONDecodeError:
48
+ fail(f"TikTok returned HTTP {error.code}")
49
+ except urllib.error.URLError as error:
50
+ fail(f"network error reaching TikTok: {error.reason}")
51
+ api_error = payload.get("error") or {}
52
+ if api_error.get("code") not in (None, "ok"):
53
+ fail(f"TikTok API error {api_error.get('code')}: {api_error.get('message') or 'request failed'}")
54
+ return payload.get("data") or {}
55
+
56
+
57
+ def upload_file(path: str) -> dict:
58
+ size = os.path.getsize(path)
59
+ if size <= 0:
60
+ fail("video file is empty")
61
+ if size > MAX_CHUNK_SIZE:
62
+ fail("video exceeds the 64 MB single-upload limit")
63
+ init = api(
64
+ "/post/publish/inbox/video/init/",
65
+ {"source_info": {"source": "FILE_UPLOAD", "video_size": size, "chunk_size": size, "total_chunk_count": 1}},
66
+ )
67
+ upload_url = init.get("upload_url")
68
+ publish_id = init.get("publish_id")
69
+ if not upload_url or not publish_id:
70
+ fail("TikTok init response did not contain upload_url and publish_id")
71
+ with open(path, "rb") as video:
72
+ body = video.read()
73
+ request = urllib.request.Request(
74
+ upload_url,
75
+ data=body,
76
+ headers={
77
+ "Content-Type": "video/mp4",
78
+ "Content-Length": str(size),
79
+ "Content-Range": f"bytes 0-{size - 1}/{size}",
80
+ },
81
+ method="PUT",
82
+ )
83
+ try:
84
+ with urllib.request.urlopen(request, timeout=120) as response:
85
+ status = response.status
86
+ except urllib.error.HTTPError as error:
87
+ status = error.code
88
+ except urllib.error.URLError as error:
89
+ fail(f"network error uploading video: {error.reason}")
90
+ if status != 201:
91
+ fail(f"TikTok upload returned HTTP {status}, expected 201")
92
+ return {"publish_id": publish_id, "status": "UPLOADED"}
93
+
94
+
95
+ def main() -> None:
96
+ parser = argparse.ArgumentParser()
97
+ sub = parser.add_subparsers(dest="command", required=True)
98
+ upload = sub.add_parser("upload")
99
+ upload.add_argument("file")
100
+ status = sub.add_parser("status")
101
+ status.add_argument("publish_id")
102
+ args = parser.parse_args()
103
+ if args.command == "upload":
104
+ data = upload_file(args.file)
105
+ else:
106
+ data = api("/post/publish/status/fetch/", {"publish_id": args.publish_id})
107
+ output({"ok": True, "data": data})
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()