openclacky 1.3.11 → 1.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/clacky/agent/skill_manager.rb +1 -0
- data/lib/clacky/default_extensions/ext-studio/api/handler.rb +58 -2
- data/lib/clacky/default_extensions/ext-studio/panels/studio/view.js +355 -44
- data/lib/clacky/default_skills/media-gen/SKILL.md +182 -7
- data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +5 -3
- data/lib/clacky/extension/packager.rb +11 -0
- data/lib/clacky/extension/verifier.rb +1 -1
- data/lib/clacky/media/generator.rb +41 -0
- data/lib/clacky/media/volcengine.rb +394 -0
- data/lib/clacky/server/http_server.rb +48 -4
- data/lib/clacky/tools/terminal.rb +24 -8
- data/lib/clacky/ui2/layout_manager.rb +4 -0
- data/lib/clacky/ui2/screen_buffer.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -8
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +110 -16
- data/lib/clacky/web/features/extensions/view.js +42 -7
- data/lib/clacky/web/features/skills/view.js +61 -1
- data/lib/clacky/web/i18n.js +24 -0
- data/lib/clacky/web/index.html +6 -11
- data/lib/clacky/web/sessions.js +12 -3
- metadata +2 -1
|
@@ -60,7 +60,8 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/ima
|
|
|
60
60
|
-d '{
|
|
61
61
|
"prompt": "A clean, modern hero illustration for a tech startup landing page. Soft gradient background, abstract geometric shapes in blue and purple, minimal style, 4K quality.",
|
|
62
62
|
"aspect_ratio": "landscape",
|
|
63
|
-
"output_dir": "'"$(pwd)"'"
|
|
63
|
+
"output_dir": "'"$(pwd)"'",
|
|
64
|
+
"session_id": "<%= session_id %>"
|
|
64
65
|
}'
|
|
65
66
|
```
|
|
66
67
|
|
|
@@ -75,6 +76,7 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/ima
|
|
|
75
76
|
| `prompt` | yes | string | Be detailed and concrete. See prompt tips below. |
|
|
76
77
|
| `aspect_ratio` | no | `landscape` / `square` / `portrait` | Defaults to `landscape`. |
|
|
77
78
|
| `output_dir` | yes | absolute path | Always pass `$(pwd)` so files land in the current session workspace. The image is saved under `<output_dir>/assets/generated/`. |
|
|
79
|
+
| `session_id` | yes | string | Current Clacky session ID. Always pass the rendered value shown in the request example. |
|
|
78
80
|
| `image` | no | file path / base64 / data URL | A single input image to **edit**. Triggers image-edit mode (see below). |
|
|
79
81
|
| `images` | no | array of the above | Multiple input images for a multi-image edit. Takes precedence over `image`. |
|
|
80
82
|
|
|
@@ -90,7 +92,8 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/ima
|
|
|
90
92
|
-H "Content-Type: application/json" \
|
|
91
93
|
-d '{
|
|
92
94
|
"prompt": "change the background to a starry night sky, keep the cat unchanged",
|
|
93
|
-
"image": "/abs/path/to/input.png"
|
|
95
|
+
"image": "/abs/path/to/input.png",
|
|
96
|
+
"session_id": "<%= session_id %>"
|
|
94
97
|
}'
|
|
95
98
|
```
|
|
96
99
|
|
|
@@ -206,7 +209,8 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/vid
|
|
|
206
209
|
"prompt": "A cinematic drone shot flying over a misty mountain range at sunrise, golden light, 4K.",
|
|
207
210
|
"aspect_ratio": "landscape",
|
|
208
211
|
"duration_seconds": 8,
|
|
209
|
-
"output_dir": "'"$(pwd)"'"
|
|
212
|
+
"output_dir": "'"$(pwd)"'",
|
|
213
|
+
"session_id": "<%= session_id %>"
|
|
210
214
|
}'
|
|
211
215
|
```
|
|
212
216
|
|
|
@@ -217,6 +221,7 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/vid
|
|
|
217
221
|
| `duration_seconds` | no | 4–8 | Defaults to 8. |
|
|
218
222
|
| `image` | no | `{ "b64_json": "...", "mime_type": "image/png" }` | Optional first frame for image-to-video. |
|
|
219
223
|
| `output_dir` | yes | absolute path | Always pass `$(pwd)` so files land in the current session workspace. MP4 saved under `<output_dir>/assets/generated/`. |
|
|
224
|
+
| `session_id` | yes | string | Current Clacky session ID. Always pass the rendered value shown in the request example. |
|
|
220
225
|
|
|
221
226
|
### Response (success)
|
|
222
227
|
|
|
@@ -278,11 +283,11 @@ Workflow for an N-segment continuous video:
|
|
|
278
283
|
writes a ready-to-send JSON file:
|
|
279
284
|
```bash
|
|
280
285
|
"$SEQ" payload /tmp/seg2.json /tmp/seg1_last.jpg 8 landscape "$OUT_DIR" \
|
|
281
|
-
"Continuing the same scene, the camera keeps pushing forward…"
|
|
286
|
+
"Continuing the same scene, the camera keeps pushing forward…" "<%= session_id %>"
|
|
282
287
|
curl -s -X POST .../api/media/video -H "Content-Type: application/json" \
|
|
283
288
|
--data @/tmp/seg2.json
|
|
284
289
|
```
|
|
285
|
-
(`payload <out.json> <frame> <duration_seconds> <aspect_ratio> <output_dir> <prompt
|
|
290
|
+
(`payload <out.json> <frame> <duration_seconds> <aspect_ratio> <output_dir> <prompt> [session_id]`)
|
|
286
291
|
5. **Repeat** steps 3–4 for each subsequent segment, always chaining off the
|
|
287
292
|
*previous* segment's last frame.
|
|
288
293
|
6. **Stitch** all clips in order into one file:
|
|
@@ -305,6 +310,175 @@ Rules & caveats:
|
|
|
305
310
|
chaining is the practical option today; Veo's native server-side `extend`
|
|
306
311
|
(148s) is not wired into this endpoint yet.
|
|
307
312
|
|
|
313
|
+
### Seedance (Volcengine Ark) — multimodal video
|
|
314
|
+
|
|
315
|
+
When the configured `type=video` model is a ByteDance **Doubao Seedance**
|
|
316
|
+
model on Volcengine Ark (its Base URL is under `*.volces.com`, e.g.
|
|
317
|
+
`https://ark.cn-beijing.volces.com/api/v3`), the **same**
|
|
318
|
+
`POST /api/media/video` endpoint drives it. No separate endpoint — the server
|
|
319
|
+
routes by Base URL automatically. Seedance adds richer inputs on top of the
|
|
320
|
+
common fields; all are optional and only apply to Seedance:
|
|
321
|
+
|
|
322
|
+
> **Cost gate — ask before EVERY generation.** Resolution is the main driver
|
|
323
|
+
> of Seedance's price (4k costs far more than 720p). So **once you've confirmed
|
|
324
|
+
> via `GET /api/media/types` that the `type=video` Base URL is under
|
|
325
|
+
> `*.volces.com`, you MUST ask the user which resolution they want before EACH
|
|
326
|
+
> AND EVERY billable call** — this covers not just a brand-new clip but also
|
|
327
|
+
> editing, multimodal reference, and extending/continuing an existing video
|
|
328
|
+
> (they all cost the same as a fresh render). Offer `480p` / `720p` / `1080p` /
|
|
329
|
+
> `4k` and state the default is `720p`. Only after they answer (or explicitly
|
|
330
|
+
> say "use the default") do you proceed, passing their choice as `resolution`.
|
|
331
|
+
> **Ask again every single time — a resolution the user picked for one clip is
|
|
332
|
+
> NEVER carried over to the next generation. Do not assume, do not reuse a
|
|
333
|
+
> prior answer, do not batch. One generation = one fresh resolution question.**
|
|
334
|
+
> **When editing or continuing/extending an existing video, default to that
|
|
335
|
+
> source video's resolution — never silently upgrade it (e.g. don't turn a
|
|
336
|
+
> 720p source into a 4k render).** If the user gave no answer and you didn't
|
|
337
|
+
> ask, the server pins `720p`. **These Seedance-only fields (`resolution`,
|
|
338
|
+
> `generate_audio`, `watermark`, `seed`, `first_frame`, `last_frame`,
|
|
339
|
+
> `reference_*`) have NO effect on Veo or Qwen/DashScope backends — never send
|
|
340
|
+
> them unless the Base URL is `*.volces.com`.**
|
|
341
|
+
|
|
342
|
+
| Field | Values | Notes |
|
|
343
|
+
|--------------------|------------------------------------------|-------|
|
|
344
|
+
| `aspect_ratio` | `landscape`/`portrait`/`square`, or a raw Ark ratio like `16:9`, `9:16`, `4:3`, `3:4`, `21:9`, `adaptive` | Raw ratios pass through unchanged. |
|
|
345
|
+
| `duration_seconds` | integer, or `-1` | `-1` lets the model pick the length (Seedance 2.0 / 1.5 Pro). |
|
|
346
|
+
| `resolution` | `480p` / `720p` / `1080p` / `4k` | **Defaults to `720p` when omitted** (cost control). Ask the user before every generation — never reuse a prior answer. See the cost gate above. Model-dependent; unsupported values are rejected upstream. |
|
|
347
|
+
| `generate_audio` | `true` / `false` | Seedance 2.0 / 1.5 Pro can synthesize a synced audio track. |
|
|
348
|
+
| `watermark` | `true` / `false` | |
|
|
349
|
+
| `seed` | integer | Reproducibility. |
|
|
350
|
+
| `first_frame` | media ref (see below) | First frame → image-to-video. |
|
|
351
|
+
| `last_frame` | media ref | Together with `first_frame` → first+last-frame video. |
|
|
352
|
+
| `reference_images` | array of media refs (0–9) | Reference images. |
|
|
353
|
+
| `reference_videos` | array of media refs (0–3) | Reference videos. |
|
|
354
|
+
| `reference_audios` | array of media refs (0–3) | Reference audio (background music / voice). |
|
|
355
|
+
|
|
356
|
+
**Which fields for which task** — Seedance covers six capabilities; pick the
|
|
357
|
+
fields by intent, and never mix the two families below:
|
|
358
|
+
|
|
359
|
+
| Task | What you want | Fields to send |
|
|
360
|
+
|------|---------------|----------------|
|
|
361
|
+
| Text-to-video | a clip from a prompt only | `prompt` (no media) |
|
|
362
|
+
| Image-to-video (first frame) | animate a still image forward | `first_frame` |
|
|
363
|
+
| Image-to-video (first + last frame) | interpolate between two stills | `first_frame` + `last_frame` |
|
|
364
|
+
| Multimodal generation | new clip guided by reference images/videos/audio | `reference_images` / `reference_videos` / `reference_audios` |
|
|
365
|
+
| **Edit an existing video** | replace/add/remove/repaint something *inside* a given video | `reference_videos: [<the video to edit>]` (+ optional `reference_images` / `reference_audios`) + a prompt describing the edit |
|
|
366
|
+
| **Extend / continue a video** | prepend/append or stitch clips into one | `reference_videos: [<clip1>, <clip2>, ...]` (up to 3) + a prompt describing the join |
|
|
367
|
+
|
|
368
|
+
> **🚫 Hard rule — the two families are mutually exclusive.**
|
|
369
|
+
> `first_frame`/`last_frame` **cannot** be combined with any `reference_*`
|
|
370
|
+
> field; Ark rejects the request. If the user wants to **edit or extend an
|
|
371
|
+
> existing video, that is a `reference_videos` task — do NOT fall back to
|
|
372
|
+
> extracting a frame and using `first_frame`** (that produces a brand-new clip
|
|
373
|
+
> and silently loses the "edit the original" intent). The server also enforces
|
|
374
|
+
> this and returns a clear `invalid_argument` error if you mix them.
|
|
375
|
+
|
|
376
|
+
A **media ref** may be:
|
|
377
|
+
- a public `http(s)://` URL, or a `data:` URL, or
|
|
378
|
+
- a local file path (the server reads and base64-encodes it), or
|
|
379
|
+
- a `{ "b64_json": "...", "mime_type": "image/png" }` hash.
|
|
380
|
+
|
|
381
|
+
Note: audio cannot be sent alone — pair it with at least one image or video.
|
|
382
|
+
Prefer passing large videos/audios as public URLs; base64-encoding a big local
|
|
383
|
+
file can exceed upstream size limits.
|
|
384
|
+
|
|
385
|
+
Example — **first + last frame** (image-to-video, no `reference_*`):
|
|
386
|
+
|
|
387
|
+
```bash
|
|
388
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/video \
|
|
389
|
+
-H "Content-Type: application/json" \
|
|
390
|
+
-d '{
|
|
391
|
+
"prompt": "First-person POV, a hand raises a cup of fruit tea toward the camera, bright and refreshing lighting",
|
|
392
|
+
"aspect_ratio": "9:16",
|
|
393
|
+
"duration_seconds": 8,
|
|
394
|
+
"resolution": "720p",
|
|
395
|
+
"first_frame": "'"$(pwd)"'/assets/frame_first.jpg",
|
|
396
|
+
"last_frame": "'"$(pwd)"'/assets/frame_last.jpg",
|
|
397
|
+
"output_dir": "'"$(pwd)"'",
|
|
398
|
+
"session_id": "<%= session_id %>"
|
|
399
|
+
}'
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
Example — **edit an existing video** (replace/add/remove something inside it;
|
|
403
|
+
uses `reference_videos`, NOT `first_frame`):
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/video \
|
|
407
|
+
-H "Content-Type: application/json" \
|
|
408
|
+
-d '{
|
|
409
|
+
"prompt": "Add a small wooden fishing boat with a warm lantern drifting slowly across the lake in the foreground, keep everything else unchanged",
|
|
410
|
+
"resolution": "720p",
|
|
411
|
+
"reference_videos": ["'"$(pwd)"'/assets/original.mp4"],
|
|
412
|
+
"output_dir": "'"$(pwd)"'",
|
|
413
|
+
"session_id": "<%= session_id %>"
|
|
414
|
+
}'
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**Seedance is asynchronous — POST only submits, it does NOT return the video.**
|
|
418
|
+
Unlike Veo (which blocks and returns the mp4 in one call), the Seedance POST
|
|
419
|
+
returns immediately with a task id:
|
|
420
|
+
|
|
421
|
+
```json
|
|
422
|
+
{ "success": true, "status": "submitted", "task_id": "cgt-2024...-xxxx", "provider": "volcengine" }
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
`status: "submitted"` means the render is now running on Volcengine's servers
|
|
426
|
+
and **is already being billed** — it does NOT mean it is done. You MUST now
|
|
427
|
+
poll for completion: sleep ~15 seconds, then query the status endpoint, and
|
|
428
|
+
repeat until it is `succeeded` (or `failed`):
|
|
429
|
+
|
|
430
|
+
```bash
|
|
431
|
+
curl -s "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/video/status?task_id=cgt-2024...-xxxx&output_dir=$(pwd)&session_id=<%= session_id %>"
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Status responses:
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
{ "success": true, "status": "running" } // keep polling
|
|
438
|
+
{ "success": true, "status": "succeeded", "video": "/abs/path.mp4" } // done — this is the file
|
|
439
|
+
{ "success": false, "status": "failed", "error": "..." } // give up, report to user
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Only once you receive `status: "succeeded"` and the absolute `video` path may
|
|
443
|
+
you present the result to the user. Do NOT end your turn while the task is
|
|
444
|
+
still `submitted`/`running` — the user is waiting for the finished video.
|
|
445
|
+
|
|
446
|
+
A minimal poll loop:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
TASK_ID=$(curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/video \
|
|
450
|
+
-H "Content-Type: application/json" \
|
|
451
|
+
-d '{"prompt":"...","resolution":"720p","output_dir":"'"$(pwd)"'","session_id":"<%= session_id %>"}' \
|
|
452
|
+
| sed -n 's/.*"task_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
453
|
+
|
|
454
|
+
while true; do
|
|
455
|
+
sleep 15
|
|
456
|
+
RESP=$(curl -s "http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/video/status?task_id=${TASK_ID}&output_dir=$(pwd)&session_id=<%= session_id %>")
|
|
457
|
+
STATUS=$(echo "$RESP" | sed -n 's/.*"status"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
|
|
458
|
+
echo "poll: $STATUS"
|
|
459
|
+
[ "$STATUS" = "succeeded" ] && echo "$RESP" && break
|
|
460
|
+
[ "$STATUS" = "failed" ] && echo "$RESP" && break
|
|
461
|
+
done
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Hard rules — a broken version of this once doubled a user's bill:**
|
|
465
|
+
|
|
466
|
+
- ❌ **Never POST the same generation twice.** Once you have a `task_id`, the
|
|
467
|
+
only valid next action is polling `/api/media/video/status`. A slow render
|
|
468
|
+
is not a failed one.
|
|
469
|
+
- ⚠️ **A timeout or error is NOT proof the task failed.** The task keeps
|
|
470
|
+
running and billing on Volcengine's side. Always query the status endpoint
|
|
471
|
+
to find out the real state before doing anything else — never resubmit.
|
|
472
|
+
- ❌ **Never kill the poll to "cancel" the job.** Killing your curl/session
|
|
473
|
+
does not stop the Volcengine task; it keeps running and billing. A running
|
|
474
|
+
task also cannot be deleted upstream.
|
|
475
|
+
- ❌ **Never bypass `/api/media/*` to call Volcengine's native API directly.**
|
|
476
|
+
All submission and status checks must go through this server (it meters
|
|
477
|
+
cost). There is no reason to touch the raw Ark API.
|
|
478
|
+
- ⏱️ If polling exceeds ~15 minutes and status is still `running`, stop
|
|
479
|
+
polling and tell the user the task is still rendering in the background,
|
|
480
|
+
give them the `task_id`, and let them check again later — do NOT resubmit.
|
|
481
|
+
|
|
308
482
|
## Generating speech (Gemini TTS)
|
|
309
483
|
|
|
310
484
|
The same `/api/media/` namespace serves text-to-speech. The user must
|
|
@@ -328,7 +502,8 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/aud
|
|
|
328
502
|
-d '{
|
|
329
503
|
"input": "Hello and welcome to openclacky. Today we will explore...",
|
|
330
504
|
"voice": "Kore",
|
|
331
|
-
"output_dir": "'"$(pwd)"'"
|
|
505
|
+
"output_dir": "'"$(pwd)"'",
|
|
506
|
+
"session_id": "<%= session_id %>"
|
|
332
507
|
}'
|
|
333
508
|
```
|
|
334
509
|
|
|
@@ -337,6 +512,7 @@ curl -s -X POST http://${CLACKY_SERVER_HOST}:${CLACKY_SERVER_PORT}/api/media/aud
|
|
|
337
512
|
| `input` | yes | string | The text to speak. Plain prose works best; you can prefix with style cues like "Say cheerfully:" or "In a calm tone:". |
|
|
338
513
|
| `voice` | no | string voice name | Defaults to `Kore`. Common Gemini voices: `Kore`, `Puck`, `Charon`, `Fenrir`, `Aoede`. |
|
|
339
514
|
| `output_dir` | yes | absolute path | Always pass `$(pwd)` so files land in the current session workspace. WAV saved under `<output_dir>/assets/generated/`. |
|
|
515
|
+
| `session_id` | yes | string | Current Clacky session ID. Always pass the rendered value shown in the request example. |
|
|
340
516
|
|
|
341
517
|
Generation typically takes 2–10 seconds depending on length. The request
|
|
342
518
|
blocks until the WAV is ready.
|
|
@@ -388,4 +564,3 @@ Same shape and `error_type` values as image generation, but with `"audio": null`
|
|
|
388
564
|
- Voice consistency: Gemini TTS does not currently support voice cloning;
|
|
389
565
|
use the same `voice` name across calls in one project to keep the
|
|
390
566
|
narrator consistent.
|
|
391
|
-
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# Subcommands:
|
|
11
11
|
# lastframe <video.mp4> <out.jpg> extract the final frame (JPEG by default)
|
|
12
12
|
# tob64 <image> print base64 (no newlines) to stdout
|
|
13
|
-
# payload <out.json> <frame.jpg> <dur> <aspect> <output_dir> <prompt>
|
|
13
|
+
# payload <out.json> <frame.jpg> <dur> <aspect> <output_dir> <prompt> [session_id]
|
|
14
14
|
# build an image-to-video JSON body
|
|
15
15
|
# for `curl --data @out.json`
|
|
16
16
|
# concat <out.mp4> <clip1.mp4> [clip2 …] losslessly join clips in order
|
|
@@ -43,7 +43,7 @@ cmd_tob64() {
|
|
|
43
43
|
# Build the image-to-video request body as a file so curl can send it with
|
|
44
44
|
# `--data @file`, avoiding "Argument list too long" from inlining base64.
|
|
45
45
|
cmd_payload() {
|
|
46
|
-
local out="$1" frame="$2" dur="$3" aspect="$4" odir="$5" prompt="$6"
|
|
46
|
+
local out="$1" frame="$2" dur="$3" aspect="$4" odir="$5" prompt="$6" session_id="${7:-}"
|
|
47
47
|
[[ -f "$frame" ]] || die "no such frame: $frame"
|
|
48
48
|
need ffprobe
|
|
49
49
|
local mime b64
|
|
@@ -53,7 +53,7 @@ cmd_payload() {
|
|
|
53
53
|
esac
|
|
54
54
|
b64="$(base64 < "$frame" | tr -d '\n')"
|
|
55
55
|
FRAME_B64="$b64" FRAME_MIME="$mime" P_PROMPT="$prompt" P_DUR="$dur" \
|
|
56
|
-
P_ASPECT="$aspect" P_ODIR="$odir" python3 - "$out" <<'PY'
|
|
56
|
+
P_ASPECT="$aspect" P_ODIR="$odir" P_SESSION_ID="$session_id" python3 - "$out" <<'PY'
|
|
57
57
|
import json, os, sys
|
|
58
58
|
body = {
|
|
59
59
|
"prompt": os.environ["P_PROMPT"],
|
|
@@ -62,6 +62,8 @@ body = {
|
|
|
62
62
|
"output_dir": os.environ["P_ODIR"],
|
|
63
63
|
"image": {"b64_json": os.environ["FRAME_B64"], "mime_type": os.environ["FRAME_MIME"]},
|
|
64
64
|
}
|
|
65
|
+
if os.environ["P_SESSION_ID"]:
|
|
66
|
+
body["session_id"] = os.environ["P_SESSION_ID"]
|
|
65
67
|
open(sys.argv[1], "w").write(json.dumps(body))
|
|
66
68
|
PY
|
|
67
69
|
[[ -f "$out" ]] || die "failed to write payload"
|
|
@@ -17,6 +17,9 @@ module Clacky
|
|
|
17
17
|
MANIFEST = "ext.yml"
|
|
18
18
|
MAX_ZIP_SIZE = 50 * 1024 * 1024
|
|
19
19
|
|
|
20
|
+
# Platform metadata that leaks in from the developer's OS; never ship it.
|
|
21
|
+
SYSTEM_METADATA = [".DS_Store", "__MACOSX", "Thumbs.db", "desktop.ini"].freeze
|
|
22
|
+
|
|
20
23
|
Result = Struct.new(:ext_id, :path, :units, keyword_init: true)
|
|
21
24
|
|
|
22
25
|
class Error < StandardError; end
|
|
@@ -111,6 +114,8 @@ module Clacky
|
|
|
111
114
|
next if base == "." || base == ".."
|
|
112
115
|
|
|
113
116
|
rel = relative(container_dir, abs)
|
|
117
|
+
next if system_metadata?(rel)
|
|
118
|
+
|
|
114
119
|
entry = File.join(File.basename(container_dir), rel)
|
|
115
120
|
if File.directory?(abs)
|
|
116
121
|
zip.mkdir(entry) unless zip.find_entry(entry)
|
|
@@ -121,6 +126,12 @@ module Clacky
|
|
|
121
126
|
end
|
|
122
127
|
end
|
|
123
128
|
|
|
129
|
+
# True if any path segment is a platform metadata file/dir (e.g. a nested
|
|
130
|
+
# agents/.DS_Store or a whole __MACOSX/ tree).
|
|
131
|
+
private def system_metadata?(rel)
|
|
132
|
+
rel.split(File::SEPARATOR).any? { |seg| SYSTEM_METADATA.include?(seg) }
|
|
133
|
+
end
|
|
134
|
+
|
|
124
135
|
private def local_zip_for(source, tmp)
|
|
125
136
|
src = source.to_s
|
|
126
137
|
if src.match?(%r{\Ahttps?://})
|
|
@@ -19,7 +19,7 @@ module Clacky
|
|
|
19
19
|
KNOWN_TOP_KEYS = %w[id name title description version origin author homepage license public license_required keywords contributes].freeze
|
|
20
20
|
KNOWN_CONTRIBUTES = %w[panels api skills agents channels patches hooks].freeze
|
|
21
21
|
|
|
22
|
-
PANEL_KEYS = %w[id title title_zh description description_zh view order attach].freeze
|
|
22
|
+
PANEL_KEYS = %w[id title title_zh description description_zh view order attach entry_points].freeze
|
|
23
23
|
API_KEYS = %w[id handler].freeze
|
|
24
24
|
SKILL_KEYS = %w[id dir protected].freeze
|
|
25
25
|
AGENT_KEYS = %w[id title title_zh description description_zh order prompt panels skills avatar].freeze
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "openai_compat"
|
|
4
4
|
require_relative "gemini"
|
|
5
5
|
require_relative "dashscope"
|
|
6
|
+
require_relative "volcengine"
|
|
6
7
|
|
|
7
8
|
module Clacky
|
|
8
9
|
module Media
|
|
@@ -34,6 +35,13 @@ module Clacky
|
|
|
34
35
|
"aliyuncs.com"
|
|
35
36
|
].freeze
|
|
36
37
|
|
|
38
|
+
# Hosts that speak Volcengine Ark's native async video-task API
|
|
39
|
+
# (ByteDance Doubao Seedance) instead of an OpenAI-compatible facade.
|
|
40
|
+
# Matched as a substring so regional variants are caught.
|
|
41
|
+
VOLCENGINE_NATIVE_HOSTS = [
|
|
42
|
+
"volces.com"
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
37
45
|
# @param agent_config [Clacky::AgentConfig]
|
|
38
46
|
def initialize(agent_config)
|
|
39
47
|
@agent_config = agent_config
|
|
@@ -101,6 +109,34 @@ module Clacky
|
|
|
101
109
|
)
|
|
102
110
|
end
|
|
103
111
|
|
|
112
|
+
def video_status(task_id:, output_dir: nil)
|
|
113
|
+
entry = video_model_entry
|
|
114
|
+
if entry.nil?
|
|
115
|
+
return {
|
|
116
|
+
"success" => false,
|
|
117
|
+
"status" => "failed",
|
|
118
|
+
"error" => "No video model configured. Add a model with type=video in settings.",
|
|
119
|
+
"error_type" => "not_configured",
|
|
120
|
+
"provider" => "",
|
|
121
|
+
"model" => ""
|
|
122
|
+
}
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
provider = build_provider_for(entry)
|
|
126
|
+
unless provider.respond_to?(:video_status)
|
|
127
|
+
return {
|
|
128
|
+
"success" => false,
|
|
129
|
+
"status" => "failed",
|
|
130
|
+
"error" => "Status queries are only supported for Volcengine (Seedance) video; this provider generates synchronously.",
|
|
131
|
+
"error_type" => "unsupported",
|
|
132
|
+
"provider" => "",
|
|
133
|
+
"model" => ""
|
|
134
|
+
}
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
provider.video_status(task_id: task_id, output_dir: output_dir)
|
|
138
|
+
end
|
|
139
|
+
|
|
104
140
|
def generate_speech(input:, voice: nil, output_dir: nil, **kwargs)
|
|
105
141
|
entry = audio_model_entry
|
|
106
142
|
if entry.nil?
|
|
@@ -185,6 +221,9 @@ module Clacky
|
|
|
185
221
|
# DashScope (native /api/v1/.../multimodal-generation schema for
|
|
186
222
|
# Qwen-Image). Third-party aggregators re-exposing qwen-image behind
|
|
187
223
|
# an OpenAI-compatible facade are NOT on aliyuncs.com and fall through.
|
|
224
|
+
# • base_url points at a Volcengine Ark host (*.volces.com) →
|
|
225
|
+
# Volcengine (native async /api/v3/contents/generations/tasks schema
|
|
226
|
+
# for Doubao Seedance video).
|
|
188
227
|
# • everything else → OpenAICompat. This covers OpenAI itself, the
|
|
189
228
|
# openclacky gateway, OpenRouter, and any third-party proxy that
|
|
190
229
|
# re-exposes Gemini / Imagen / DALL-E behind /v1/images/generations.
|
|
@@ -196,6 +235,8 @@ module Clacky
|
|
|
196
235
|
Gemini.new(entry)
|
|
197
236
|
elsif DASHSCOPE_NATIVE_HOSTS.any? { |host| url.include?(host) }
|
|
198
237
|
DashScope.new(entry)
|
|
238
|
+
elsif VOLCENGINE_NATIVE_HOSTS.any? { |host| url.include?(host) }
|
|
239
|
+
Volcengine.new(entry)
|
|
199
240
|
else
|
|
200
241
|
OpenAICompat.new(entry)
|
|
201
242
|
end
|