VibesAI-api 1.3.2__py3-none-any.whl

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.
vibes_api/cli.py ADDED
@@ -0,0 +1,852 @@
1
+ """CLI entrypoint for the Vibes API client.
2
+
3
+ Usage
4
+ -----
5
+ vibes-api --cookie <meta_session> me
6
+ vibes-api --cookie <meta_session> projects list
7
+ vibes-api --cookie <meta_session> projects create --name "My Video"
8
+ vibes-api --cookie <meta_session> videos generate \\
9
+ --project-id <id> \\
10
+ --prompt "sunset over ocean" \\
11
+ --aspect-ratio 16:9 \\
12
+ --resolution 720p \\
13
+ --variations 4 \\
14
+ --download-dir ./out
15
+ vibes-api --cookie <meta_session> images generate \\
16
+ --project-id <id> --prompt "cyberpunk city"
17
+ vibes-api --cookie <meta_session> voices list
18
+ vibes-api --cookie <meta_session> tts --voice play_ai_Marisol \\
19
+ --text "Hello world" --out hello.mp3
20
+ vibes-api --cookie <meta_session> media list
21
+ vibes-api --cookie <meta_session> media download --id <id> --out video.mp4
22
+ vibes-api --cookie <meta_session> prompts enhance --prompt "a cat"
23
+ vibes-api --cookie <meta_session> ingredients list
24
+ vibes-api --cookie <meta_session> share create --entity-type project --entity-id <id>
25
+
26
+ You can also set VIBES_META_SESSION env var instead of --cookie.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import os
34
+ import sys
35
+ from typing import Any
36
+
37
+ from .client import VibesClient, VibesAPIError
38
+
39
+
40
+ def _client(args: argparse.Namespace) -> VibesClient:
41
+ cookie = args.cookie or os.environ.get("VIBES_META_SESSION")
42
+ if not cookie:
43
+ sys.exit("error: --cookie is required (or set VIBES_META_SESSION env var)")
44
+ return VibesClient(meta_session=cookie)
45
+
46
+
47
+ def _print(obj: Any) -> None:
48
+ if isinstance(obj, (dict, list)):
49
+ print(json.dumps(obj, indent=2, default=str))
50
+ else:
51
+ print(obj)
52
+
53
+
54
+ def cmd_me(client: VibesClient, args: argparse.Namespace) -> None:
55
+ _print(client.get_me())
56
+
57
+
58
+ def cmd_projects_list(client: VibesClient, args: argparse.Namespace) -> None:
59
+ _print(client.list_projects(limit=args.limit, offset=args.offset, search=args.search))
60
+
61
+
62
+ def cmd_projects_create(client: VibesClient, args: argparse.Namespace) -> None:
63
+ _print(client.create_project(name=args.name))
64
+
65
+
66
+ def cmd_projects_get(client: VibesClient, args: argparse.Namespace) -> None:
67
+ _print(client.get_project(args.project_id))
68
+
69
+
70
+ def cmd_projects_delete(client: VibesClient, args: argparse.Namespace) -> None:
71
+ client.delete_project(args.project_id, delete_assets=args.delete_assets)
72
+ print(f"Deleted project {args.project_id}")
73
+
74
+
75
+ def cmd_videos_generate(client: VibesClient, args: argparse.Namespace) -> None:
76
+ print(f"Generating {args.variations} video variation(s)...", file=sys.stderr)
77
+ result = client.generate_video(
78
+ project_id=args.project_id,
79
+ prompt=args.prompt,
80
+ aspect_ratio=args.aspect_ratio,
81
+ resolution=args.resolution,
82
+ variations=args.variations,
83
+ poll=not args.no_poll,
84
+ )
85
+ if args.no_poll:
86
+ _print(result)
87
+ return
88
+ # Summarize
89
+ videos = [
90
+ {"id": c.get("id"), "videoUrl": c.get("videoUrl"),
91
+ "imageUrl": c.get("imageUrl"), "prompt": c.get("prompt")}
92
+ for c in result.get("content", []) if c.get("videoUrl")
93
+ ]
94
+ print(f"Generated {len(videos)} video(s):", file=sys.stderr)
95
+ for v in videos:
96
+ print(f" - id={v['id']}", file=sys.stderr)
97
+ print(f" videoUrl={v['videoUrl'][:80]}..." if v["videoUrl"] else "", file=sys.stderr)
98
+ if args.download_dir:
99
+ os.makedirs(args.download_dir, exist_ok=True)
100
+ for i, v in enumerate(videos):
101
+ out = os.path.join(args.download_dir, f"video_{i}.mp4")
102
+ try:
103
+ client.download_video(v["id"], out)
104
+ print(f" downloaded → {out}", file=sys.stderr)
105
+ except Exception as e:
106
+ print(f" download failed: {e}", file=sys.stderr)
107
+ _print({"videos": videos, "batch_id": result.get("id")})
108
+
109
+
110
+ def cmd_images_generate(client: VibesClient, args: argparse.Namespace) -> None:
111
+ print(f"Generating {args.variations} image(s)...", file=sys.stderr)
112
+ resp = client.generate_image(
113
+ project_id=args.project_id,
114
+ prompt=args.prompt,
115
+ aspect_ratio=args.aspect_ratio,
116
+ variations=args.variations,
117
+ )
118
+ images = [
119
+ {"url": d.get("url"), "prompt": d.get("prompt"),
120
+ "imageEntId": d.get("imageEntId"), "dimensions": d.get("dimensions")}
121
+ for d in resp.get("data", [])
122
+ ]
123
+ print(f"Generated {len(images)} image(s):", file=sys.stderr)
124
+ for im in images:
125
+ print(f" - {im['url'][:80]}...", file=sys.stderr)
126
+ if args.download_dir:
127
+ os.makedirs(args.download_dir, exist_ok=True)
128
+ for i, im in enumerate(images):
129
+ out = os.path.join(args.download_dir, f"image_{i}.png")
130
+ try:
131
+ # Download via direct URL
132
+ import requests
133
+ r = requests.get(im["url"], timeout=60)
134
+ r.raise_for_status()
135
+ with open(out, "wb") as f:
136
+ f.write(r.content)
137
+ print(f" downloaded → {out}", file=sys.stderr)
138
+ except Exception as e:
139
+ print(f" download failed: {e}", file=sys.stderr)
140
+ _print({"images": images, "batch": resp.get("updatedBatch", {}).get("id")})
141
+
142
+
143
+ def cmd_voices_list(client: VibesClient, args: argparse.Namespace) -> None:
144
+ voices = client.list_voices()
145
+ print(f"{len(voices)} voice(s) available:\n", file=sys.stderr)
146
+ for v in voices:
147
+ print(f" {v['id']:35s} {v['name']:20s} {v.get('description','')}", file=sys.stderr)
148
+ _print(voices)
149
+
150
+
151
+ def cmd_tts(client: VibesClient, args: argparse.Namespace) -> None:
152
+ print(f"Synthesizing with voice '{args.voice}'...", file=sys.stderr)
153
+ resp = client.tts(text=args.text, voice=args.voice, language=args.language)
154
+ if "audioBase64" not in resp:
155
+ _print(resp)
156
+ return
157
+ client.save_tts_audio(resp, args.out)
158
+ print(f"saved → {args.out}", file=sys.stderr)
159
+
160
+
161
+ def cmd_media_list(client: VibesClient, args: argparse.Namespace) -> None:
162
+ _print(client.list_media(limit=args.limit, offset=args.offset, type=args.type))
163
+
164
+
165
+ def cmd_media_download(client: VibesClient, args: argparse.Namespace) -> None:
166
+ if args.type == "image":
167
+ client.download_image(args.id, args.out)
168
+ else:
169
+ client.download_video(args.id, args.out)
170
+ print(f"saved → {args.out}", file=sys.stderr)
171
+
172
+
173
+ def cmd_media_delete(client: VibesClient, args: argparse.Namespace) -> None:
174
+ client.delete_content_items(args.ids)
175
+ print(f"deleted {len(args.ids)} item(s)")
176
+
177
+
178
+ def cmd_prompts_enhance(client: VibesClient, args: argparse.Namespace) -> None:
179
+ variations = client.enhance_prompt(prompt=args.prompt, batch_type=args.batch_type)
180
+ print(f"{len(variations)} variation(s):", file=sys.stderr)
181
+ _print(variations)
182
+
183
+
184
+ def cmd_ingredients_list(client: VibesClient, args: argparse.Namespace) -> None:
185
+ items = client.list_ingredients(owner_filter=args.owner_filter)
186
+ print(f"{len(items)} ingredient(s):", file=sys.stderr)
187
+ for it in items:
188
+ print(f" [{it.get('ingredientType')}] {it.get('name')} (id={it.get('ingredientId')})", file=sys.stderr)
189
+ _print(items)
190
+
191
+
192
+ def cmd_share_create(client: VibesClient, args: argparse.Namespace) -> None:
193
+ link = client.create_share_link(args.entity_type, args.entity_id)
194
+ print(f"Share URL: {link.get('url')}", file=sys.stderr)
195
+ _print(link)
196
+
197
+
198
+ def cmd_share_list(client: VibesClient, args: argparse.Namespace) -> None:
199
+ _print(client.list_share_links(args.entity_type, args.entity_id))
200
+
201
+
202
+ def cmd_share_revoke(client: VibesClient, args: argparse.Namespace) -> None:
203
+ client.revoke_share_link(args.share_link_id)
204
+ print(f"revoked {args.share_link_id}")
205
+
206
+
207
+ def cmd_batches_list(client: VibesClient, args: argparse.Namespace) -> None:
208
+ _print(client.list_batches(limit=args.limit, offset=args.offset, project_id=args.project_id))
209
+
210
+
211
+ def cmd_batches_get(client: VibesClient, args: argparse.Namespace) -> None:
212
+ _print(client.get_batch(args.batch_id))
213
+
214
+
215
+ def cmd_batches_poll(client: VibesClient, args: argparse.Namespace) -> None:
216
+ print(f"Polling batch {args.batch_id} (timeout {args.timeout}s)...", file=sys.stderr)
217
+ batch = client.poll_batch(args.batch_id, timeout=args.timeout)
218
+ _print(batch)
219
+
220
+
221
+ def cmd_music_search(client: VibesClient, args: argparse.Namespace) -> None:
222
+ _print(client.search_music(query=args.query, limit=args.limit))
223
+
224
+
225
+ def cmd_timeline_chat(client: VibesClient, args: argparse.Namespace) -> None:
226
+ print(f"Streaming timeline chat for: {args.input}", file=sys.stderr)
227
+ for event in client.timeline_chat(args.input):
228
+ etype = event.get("type")
229
+ if etype == "message_delta":
230
+ print(event.get("delta", ""), end="", flush=True)
231
+ elif etype == "message_done":
232
+ print()
233
+ elif etype == "tool_call":
234
+ print(f"\n[tool_call] {event.get('name')}({event.get('arguments_buffer','')[:200]})",
235
+ file=sys.stderr)
236
+ elif etype == "error":
237
+ print(f"\n[error] {event.get('code')}: {event.get('message')}", file=sys.stderr)
238
+ elif etype == "completed":
239
+ print(f"\n[completed] conversation_id={event.get('conversation_id')}", file=sys.stderr)
240
+ else:
241
+ _print(event)
242
+
243
+
244
+ def cmd_one_shot(client: VibesClient, args: argparse.Namespace) -> None:
245
+ print(f"Creating end-to-end video for: {args.prompt}", file=sys.stderr)
246
+ result = client.create_video_from_prompt(
247
+ prompt=args.prompt,
248
+ project_name=args.name,
249
+ aspect_ratio=args.aspect_ratio,
250
+ resolution=args.resolution,
251
+ variations=args.variations,
252
+ download_dir=args.download_dir,
253
+ )
254
+ _print(result)
255
+
256
+
257
+ # ----- NEW COMMANDS: extend, edit_video, animate, regenerate, ingredient CRUD, sync -----
258
+
259
+ def cmd_videos_extend(client: VibesClient, args: argparse.Namespace) -> None:
260
+ """Extend a video (auto or manual)."""
261
+ # Fetch the source content item from the batch
262
+ batch = client.get_batch(args.batch_id)
263
+ source = None
264
+ for c in batch.get("content", []):
265
+ if c["id"] == args.content_id:
266
+ source = c
267
+ break
268
+ if not source:
269
+ # Try to use the first content item
270
+ if batch.get("content"):
271
+ source = batch["content"][0]
272
+ else:
273
+ sys.exit(f"Content item {args.content_id} not found in batch {args.batch_id}")
274
+ mode = "manual" if args.prompt else "auto"
275
+ print(f"Extend ({mode}) on {source['id']}...", file=sys.stderr)
276
+ result = client.extend_video(
277
+ project_id=args.project_id,
278
+ source_video=source,
279
+ prompt=args.prompt,
280
+ poll=not args.no_poll,
281
+ )
282
+ if args.no_poll:
283
+ _print(result)
284
+ return
285
+ videos = [{"id": c.get("id"), "videoUrl": c.get("videoUrl")}
286
+ for c in result.get("content", []) if c.get("videoUrl")]
287
+ print(f"Extended: {len(videos)} video(s)", file=sys.stderr)
288
+ for v in videos:
289
+ print(f" - {v['id']}: {v['videoUrl'][:80]}...", file=sys.stderr)
290
+ _print({"videos": videos, "batch_id": result.get("id")})
291
+
292
+
293
+ def cmd_videos_edit(client: VibesClient, args: argparse.Namespace) -> None:
294
+ """Video-to-video edit (re-render with a directive)."""
295
+ batch = client.get_batch(args.batch_id)
296
+ source = None
297
+ for c in batch.get("content", []):
298
+ if c["id"] == args.content_id:
299
+ source = c
300
+ break
301
+ if not source:
302
+ if batch.get("content"):
303
+ source = batch["content"][0]
304
+ else:
305
+ sys.exit(f"Content item not found in batch {args.batch_id}")
306
+ print(f"Editing video {source['id']} with prompt: {args.prompt}", file=sys.stderr)
307
+ result = client.edit_video(
308
+ project_id=args.project_id,
309
+ source_video=source,
310
+ prompt=args.prompt,
311
+ poll=not args.no_poll,
312
+ )
313
+ _print(result if args.no_poll else {
314
+ "batch_id": result.get("id"),
315
+ "videos": [{"id": c.get("id"), "videoUrl": c.get("videoUrl")}
316
+ for c in result.get("content", []) if c.get("videoUrl")],
317
+ })
318
+
319
+
320
+ def cmd_images_animate(client: VibesClient, args: argparse.Namespace) -> None:
321
+ """Animate an image (auto or manual)."""
322
+ # Image content items are in image batches; we need to fetch them.
323
+ # Use the media library to find the source image.
324
+ media = client.list_media(limit=100)
325
+ source = None
326
+ for item in media["items"]:
327
+ if item["id"] == args.content_id:
328
+ # Reconstruct a content-item-shaped dict
329
+ source = {
330
+ "id": item["id"],
331
+ "imageUrl": item.get("thumbnailUrl") or item.get("fullUrl"),
332
+ "prompt": item.get("prompt", ""),
333
+ "type": "images",
334
+ }
335
+ break
336
+ if not source:
337
+ sys.exit(f"Content item {args.content_id} not found in media library")
338
+ mode = "manual" if args.prompt else "auto"
339
+ print(f"Animate ({mode}) on {source['id']}...", file=sys.stderr)
340
+ result = client.animate_image(
341
+ project_id=args.project_id,
342
+ source_image=source,
343
+ prompt=args.prompt,
344
+ poll=not args.no_poll,
345
+ )
346
+ _print(result if args.no_poll else {
347
+ "batch_id": result.get("id"),
348
+ "videos": [{"id": c.get("id"), "videoUrl": c.get("videoUrl")}
349
+ for c in result.get("content", []) if c.get("videoUrl")],
350
+ })
351
+
352
+
353
+ def cmd_batches_regenerate(client: VibesClient, args: argparse.Namespace) -> None:
354
+ """Regenerate a batch (re-roll)."""
355
+ print(f"Regenerating batch {args.batch_id}...", file=sys.stderr)
356
+ result = client.regenerate_batch(
357
+ project_id=args.project_id,
358
+ batch_id=args.batch_id,
359
+ prompt=args.prompt,
360
+ poll=not args.no_poll,
361
+ )
362
+ _print(result if args.no_poll else {
363
+ "batch_id": result.get("id"),
364
+ "isComplete": result.get("isComplete"),
365
+ "content_count": len(result.get("content", [])),
366
+ })
367
+
368
+
369
+ def cmd_ingredients_create(client: VibesClient, args: argparse.Namespace) -> None:
370
+ """Create a new ingredient."""
371
+ result = client.create_ingredient(
372
+ name=args.name,
373
+ ingredient_type=args.type,
374
+ source_image_ent_id=args.image_ent_id,
375
+ image_url=args.image_url,
376
+ description=args.description,
377
+ )
378
+ print(f"Created ingredient: {result.get('ingredient', {}).get('ingredientId')}", file=sys.stderr)
379
+ if result.get("usedExistingName"):
380
+ print("(used existing name — ingredient with this name already existed)", file=sys.stderr)
381
+ _print(result)
382
+
383
+
384
+ def cmd_ingredients_delete(client: VibesClient, args: argparse.Namespace) -> None:
385
+ """Delete an ingredient."""
386
+ client.delete_ingredient(args.ingredient_id)
387
+ print(f"Deleted ingredient {args.ingredient_id}", file=sys.stderr)
388
+
389
+
390
+ def cmd_sync_status(client: VibesClient, args: argparse.Namespace) -> None:
391
+ """Get sync status for an entity."""
392
+ result = client.get_sync_status(args.entity_type, args.entity_id)
393
+ _print(result)
394
+
395
+
396
+ def cmd_sync_stream(client: VibesClient, args: argparse.Namespace) -> None:
397
+ """Stream sync updates (Ctrl+C to stop)."""
398
+ print(f"Streaming updates for {args.entity_type} {args.entity_id} (Ctrl+C to stop)...",
399
+ file=sys.stderr)
400
+ try:
401
+ for event in client.stream_sync_updates(args.entity_type, args.entity_id):
402
+ _print(event)
403
+ except KeyboardInterrupt:
404
+ print("\nStopped.", file=sys.stderr)
405
+
406
+
407
+ def cmd_quota(client: VibesClient, args: argparse.Namespace) -> None:
408
+ """Get quota upsell info."""
409
+ result = client.get_quota_upsell()
410
+ if result is None:
411
+ print("No upsell info available (user not eligible or endpoint failed).")
412
+ else:
413
+ _print(result)
414
+
415
+
416
+ # ----- v1.2.0 commands -----
417
+
418
+ def cmd_publish(client: VibesClient, args: argparse.Namespace) -> None:
419
+ """Publish a content item to Vibes."""
420
+ print(f"Publishing content item {args.content_id}...", file=sys.stderr)
421
+ result = client.publish_to_vibes(
422
+ content_item_id=args.content_id,
423
+ batch_id=args.batch_id,
424
+ caption=args.caption,
425
+ prompt=args.prompt,
426
+ image_prompt=args.image_prompt,
427
+ video_prompt=args.video_prompt,
428
+ )
429
+ _print(result)
430
+
431
+
432
+ def cmd_moodboard_update(client: VibesClient, args: argparse.Namespace) -> None:
433
+ """Update a moodboard (rename / add / remove images)."""
434
+ result = client.update_moodboard(
435
+ moodboard_id=args.moodboard_id,
436
+ name=args.name,
437
+ )
438
+ _print(result)
439
+
440
+
441
+ def cmd_moodboard_lookup(client: VibesClient, args: argparse.Namespace) -> None:
442
+ """Look up a moodboard ID by code."""
443
+ moodboard_id = client.lookup_moodboard_by_code(args.code)
444
+ if moodboard_id:
445
+ print(moodboard_id)
446
+ else:
447
+ print(f"No moodboard found with code '{args.code}'", file=sys.stderr)
448
+ sys.exit(1)
449
+
450
+
451
+ def cmd_share_reset(client: VibesClient, args: argparse.Namespace) -> None:
452
+ """Reset share link (revoke existing + create new)."""
453
+ print(f"Resetting share link for {args.entity_type} {args.entity_id}...", file=sys.stderr)
454
+ result = client.reset_share_link(args.entity_type, args.entity_id)
455
+ print(f"New share URL: {result.get('url')}", file=sys.stderr)
456
+ _print(result)
457
+
458
+
459
+ def cmd_playables_list(client: VibesClient, args: argparse.Namespace) -> None:
460
+ """List playables."""
461
+ _print(client.list_playables(limit=args.limit, offset=args.offset, status=args.status))
462
+
463
+
464
+ def cmd_playables_get(client: VibesClient, args: argparse.Namespace) -> None:
465
+ """Get a playable."""
466
+ _print(client.get_playable(args.playable_id))
467
+
468
+
469
+ def cmd_playables_delete(client: VibesClient, args: argparse.Namespace) -> None:
470
+ """Delete a playable."""
471
+ client.delete_playable(args.playable_id)
472
+ print(f"Deleted playable {args.playable_id}", file=sys.stderr)
473
+
474
+
475
+ def cmd_ingredients_update(client: VibesClient, args: argparse.Namespace) -> None:
476
+ """Update an ingredient."""
477
+ result = client.update_ingredient(
478
+ ingredient_id=args.ingredient_id,
479
+ name=args.name,
480
+ description=args.description,
481
+ personality=args.personality,
482
+ backstory=args.backstory,
483
+ core_beliefs=args.core_beliefs,
484
+ )
485
+ _print(result)
486
+
487
+
488
+ def cmd_audio_resolve(client: VibesClient, args: argparse.Namespace) -> None:
489
+ """Resolve audio URLs for a list of audio cluster IDs."""
490
+ result = client.resolve_audio_urls(args.ids)
491
+ _print(result)
492
+
493
+
494
+ def cmd_audio_proxy(client: VibesClient, args: argparse.Namespace) -> None:
495
+ """Build a proxy audio URL."""
496
+ url = client.proxy_audio_url(args.audio_id, title=args.title)
497
+ print(url)
498
+
499
+
500
+ def cmd_check_token(client: VibesClient, args: argparse.Namespace) -> None:
501
+ """Check if the current session token is valid."""
502
+ if client.check_token():
503
+ print("✓ Token is valid")
504
+ else:
505
+ print("✗ Token is INVALID (session expired)")
506
+ sys.exit(1)
507
+
508
+
509
+ def cmd_rate_limit(client: VibesClient, args: argparse.Namespace) -> None:
510
+ """Show current rate limit status."""
511
+ _print(client.get_rate_limit_status())
512
+
513
+
514
+ def cmd_pending_export(client: VibesClient, args: argparse.Namespace) -> None:
515
+ """Check if a project has a pending export."""
516
+ result = client.get_pending_export(args.project_id)
517
+ if result:
518
+ print("Pending export found:", file=sys.stderr)
519
+ _print(result)
520
+ else:
521
+ print("No pending export.", file=sys.stderr)
522
+
523
+
524
+ def cmd_parse_midjourney(client: VibesClient, args: argparse.Namespace) -> None:
525
+ """Parse Midjourney parameters from a prompt."""
526
+ result = VibesClient.parse_midjourney_params(args.prompt)
527
+ _print(result)
528
+
529
+
530
+ def cmd_validate_prompt(client: VibesClient, args: argparse.Namespace) -> None:
531
+ """Validate a prompt's length."""
532
+ result = VibesClient.validate_prompt_length(args.prompt)
533
+ if result["success"]:
534
+ print(f"✓ Valid ({len(result['value'])} chars)")
535
+ else:
536
+ print(f"✗ {result['error']}")
537
+ sys.exit(1)
538
+
539
+
540
+ def build_parser() -> argparse.ArgumentParser:
541
+ p = argparse.ArgumentParser(prog="vibes-api", description="Vibes.ai API CLI")
542
+ p.add_argument("--cookie", help="meta_session cookie value (or set VIBES_META_SESSION env var)")
543
+ sub = p.add_subparsers(dest="cmd", required=True)
544
+
545
+ # me
546
+ sub.add_parser("me", help="Get current user").set_defaults(func=cmd_me)
547
+
548
+ # projects
549
+ proj = sub.add_parser("projects", help="Project management")
550
+ proj_sub = proj.add_subparsers(dest="subcmd", required=True)
551
+ proj_list = proj_sub.add_parser("list", help="List projects")
552
+ proj_list.add_argument("--limit", type=int, default=25)
553
+ proj_list.add_argument("--offset", type=int, default=0)
554
+ proj_list.add_argument("--search", type=str)
555
+ proj_list.set_defaults(func=cmd_projects_list)
556
+ proj_create = proj_sub.add_parser("create", help="Create a project")
557
+ proj_create.add_argument("--name", default="Untitled")
558
+ proj_create.set_defaults(func=cmd_projects_create)
559
+ proj_get = proj_sub.add_parser("get", help="Get a project")
560
+ proj_get.add_argument("project_id")
561
+ proj_get.set_defaults(func=cmd_projects_get)
562
+ proj_del = proj_sub.add_parser("delete", help="Delete a project")
563
+ proj_del.add_argument("project_id")
564
+ proj_del.add_argument("--delete-assets", action="store_true")
565
+ proj_del.set_defaults(func=cmd_projects_delete)
566
+
567
+ # videos
568
+ vid = sub.add_parser("videos", help="Video generation")
569
+ vid_sub = vid.add_subparsers(dest="subcmd", required=True)
570
+ vid_gen = vid_sub.add_parser("generate", help="Generate videos from a prompt")
571
+ vid_gen.add_argument("--project-id", required=True)
572
+ vid_gen.add_argument("--prompt", required=True)
573
+ vid_gen.add_argument("--aspect-ratio", default="9:16", choices=["1:1","9:16","16:9","4:5","3:2","2:3"])
574
+ vid_gen.add_argument("--resolution", default="480p", choices=["480p","720p"])
575
+ vid_gen.add_argument("--variations", type=int, default=4)
576
+ vid_gen.add_argument("--download-dir", help="If set, download MP4s to this dir")
577
+ vid_gen.add_argument("--no-poll", action="store_true", help="Return immediately without waiting")
578
+ vid_gen.set_defaults(func=cmd_videos_generate)
579
+
580
+ # videos extend (auto/manual)
581
+ vid_ext = vid_sub.add_parser("extend", help="Extend a video (auto or manual)")
582
+ vid_ext.add_argument("--project-id", required=True)
583
+ vid_ext.add_argument("--batch-id", required=True, help="Batch containing the source video")
584
+ vid_ext.add_argument("--content-id", help="Specific content item ID (default: first)")
585
+ vid_ext.add_argument("--prompt", help="Manual extend directive (omit for auto extend)")
586
+ vid_ext.add_argument("--no-poll", action="store_true")
587
+ vid_ext.set_defaults(func=cmd_videos_extend)
588
+
589
+ # videos edit (v2v)
590
+ vid_edit = vid_sub.add_parser("edit", help="Edit a video with a prompt (v2v)")
591
+ vid_edit.add_argument("--project-id", required=True)
592
+ vid_edit.add_argument("--batch-id", required=True)
593
+ vid_edit.add_argument("--content-id")
594
+ vid_edit.add_argument("--prompt", required=True, help="Edit directive")
595
+ vid_edit.add_argument("--no-poll", action="store_true")
596
+ vid_edit.set_defaults(func=cmd_videos_edit)
597
+
598
+ # images
599
+ img = sub.add_parser("images", help="Image generation")
600
+ img_sub = img.add_subparsers(dest="subcmd", required=True)
601
+ img_gen = img_sub.add_parser("generate", help="Generate images from a prompt")
602
+ img_gen.add_argument("--project-id", required=True)
603
+ img_gen.add_argument("--prompt", required=True)
604
+ img_gen.add_argument("--aspect-ratio", default="1:1", choices=["1:1","9:16","16:9","4:5","3:2","2:3"])
605
+ img_gen.add_argument("--variations", type=int, default=1)
606
+ img_gen.add_argument("--download-dir")
607
+ img_gen.set_defaults(func=cmd_images_generate)
608
+
609
+ # images animate (auto/manual)
610
+ img_anim = img_sub.add_parser("animate", help="Animate an image (auto or manual)")
611
+ img_anim.add_argument("--project-id", required=True)
612
+ img_anim.add_argument("--content-id", required=True, help="Image content item ID")
613
+ img_anim.add_argument("--prompt", help="Manual animate directive (omit for auto)")
614
+ img_anim.add_argument("--no-poll", action="store_true")
615
+ img_anim.set_defaults(func=cmd_images_animate)
616
+
617
+ # voices / tts
618
+ sub.add_parser("voices", help="List TTS voices").set_defaults(func=cmd_voices_list)
619
+ tts = sub.add_parser("tts", help="Text-to-speech")
620
+ tts.add_argument("--voice", required=True)
621
+ tts.add_argument("--text", required=True)
622
+ tts.add_argument("--out", required=True, help="Output mp3 path")
623
+ tts.add_argument("--language")
624
+ tts.set_defaults(func=cmd_tts)
625
+
626
+ # media
627
+ med = sub.add_parser("media", help="Media library")
628
+ med_sub = med.add_subparsers(dest="subcmd", required=True)
629
+ med_list = med_sub.add_parser("list", help="List media items")
630
+ med_list.add_argument("--limit", type=int, default=50)
631
+ med_list.add_argument("--offset", type=int, default=0)
632
+ med_list.add_argument("--type", choices=["video","image","audio"])
633
+ med_list.set_defaults(func=cmd_media_list)
634
+ med_dl = med_sub.add_parser("download", help="Download a media item")
635
+ med_dl.add_argument("--id", required=True)
636
+ med_dl.add_argument("--out", required=True)
637
+ med_dl.add_argument("--type", default="video", choices=["video","image"])
638
+ med_dl.set_defaults(func=cmd_media_download)
639
+ med_del = med_sub.add_parser("delete", help="Delete media items")
640
+ med_del.add_argument("--ids", nargs="+", required=True)
641
+ med_del.set_defaults(func=cmd_media_delete)
642
+
643
+ # prompts
644
+ pr = sub.add_parser("prompts", help="Prompt enhancement")
645
+ pr_sub = pr.add_subparsers(dest="subcmd", required=True)
646
+ pr_enh = pr_sub.add_parser("enhance", help="Generate 4 enhanced prompt variations")
647
+ pr_enh.add_argument("--prompt", required=True)
648
+ pr_enh.add_argument("--batch-type", default="videos", choices=["videos","images"])
649
+ pr_enh.set_defaults(func=cmd_prompts_enhance)
650
+
651
+ # ingredients
652
+ ing = sub.add_parser("ingredients", help="Studio ingredients")
653
+ ing_sub = ing.add_subparsers(dest="subcmd", required=True)
654
+ ing_list = ing_sub.add_parser("list", help="List ingredients")
655
+ ing_list.add_argument("--owner-filter", default="LIBRARY", choices=["LIBRARY","VIEWER"])
656
+ ing_list.add_argument("--type", choices=["CHARACTER","STYLE","SETTING"],
657
+ help="Filter to one type")
658
+ ing_list.set_defaults(func=cmd_ingredients_list)
659
+ ing_cr = ing_sub.add_parser("create", help="Create an ingredient")
660
+ ing_cr.add_argument("--name", required=True)
661
+ ing_cr.add_argument("--type", required=True, choices=["CHARACTER","STYLE","SETTING"])
662
+ ing_cr.add_argument("--image-ent-id", help="Source image entity ID (from upload_image)")
663
+ ing_cr.add_argument("--image-url", help="Source image URL")
664
+ ing_cr.add_argument("--description")
665
+ ing_cr.set_defaults(func=cmd_ingredients_create)
666
+ ing_del = ing_sub.add_parser("delete", help="Delete an ingredient")
667
+ ing_del.add_argument("ingredient_id")
668
+ ing_del.set_defaults(func=cmd_ingredients_delete)
669
+
670
+ # share
671
+ sh = sub.add_parser("share", help="Share links")
672
+ sh_sub = sh.add_subparsers(dest="subcmd", required=True)
673
+ sh_cr = sh_sub.add_parser("create", help="Create a share link")
674
+ sh_cr.add_argument("--entity-type", required=True, choices=["project","content-item"])
675
+ sh_cr.add_argument("--entity-id", required=True)
676
+ sh_cr.set_defaults(func=cmd_share_create)
677
+ sh_ls = sh_sub.add_parser("list", help="List share links")
678
+ sh_ls.add_argument("--entity-type", required=True)
679
+ sh_ls.add_argument("--entity-id", required=True)
680
+ sh_ls.set_defaults(func=cmd_share_list)
681
+ sh_rv = sh_sub.add_parser("revoke", help="Revoke a share link")
682
+ sh_rv.add_argument("share_link_id")
683
+ sh_rv.set_defaults(func=cmd_share_revoke)
684
+
685
+ # batches
686
+ bt = sub.add_parser("batches", help="Generation batches")
687
+ bt_sub = bt.add_subparsers(dest="subcmd", required=True)
688
+ bt_ls = bt_sub.add_parser("list", help="List batches")
689
+ bt_ls.add_argument("--limit", type=int, default=12)
690
+ bt_ls.add_argument("--offset", type=int, default=0)
691
+ bt_ls.add_argument("--project-id")
692
+ bt_ls.set_defaults(func=cmd_batches_list)
693
+ bt_gt = bt_sub.add_parser("get", help="Get a batch")
694
+ bt_gt.add_argument("batch_id")
695
+ bt_gt.set_defaults(func=cmd_batches_get)
696
+ bt_pl = bt_sub.add_parser("poll", help="Poll until a batch completes")
697
+ bt_pl.add_argument("batch_id")
698
+ bt_pl.add_argument("--timeout", type=float, default=180.0)
699
+ bt_pl.set_defaults(func=cmd_batches_poll)
700
+ bt_rg = bt_sub.add_parser("regenerate", help="Regenerate a batch (re-roll)")
701
+ bt_rg.add_argument("batch_id")
702
+ bt_rg.add_argument("--project-id", required=True)
703
+ bt_rg.add_argument("--prompt", help="Override the original prompt")
704
+ bt_rg.add_argument("--no-poll", action="store_true")
705
+ bt_rg.set_defaults(func=cmd_batches_regenerate)
706
+
707
+ # music
708
+ mu = sub.add_parser("music", help="Music library")
709
+ mu_sub = mu.add_subparsers(dest="subcmd", required=True)
710
+ mu_se = mu_sub.add_parser("search", help="Search music")
711
+ mu_se.add_argument("--query", default="")
712
+ mu_se.add_argument("--limit", type=int, default=30)
713
+ mu_se.set_defaults(func=cmd_music_search)
714
+
715
+ # timeline chat
716
+ tc = sub.add_parser("chat", help="Stream timeline AI chat")
717
+ tc.add_argument("input", help="Your request, e.g. 'add a 5s sunset clip'")
718
+ tc.set_defaults(func=cmd_timeline_chat)
719
+
720
+ # one-shot
721
+ os_p = sub.add_parser("one-shot", help="End-to-end: create project + generate + download")
722
+ os_p.add_argument("--prompt", required=True)
723
+ os_p.add_argument("--name")
724
+ os_p.add_argument("--aspect-ratio", default="16:9", choices=["1:1","9:16","16:9","4:5","3:2","2:3"])
725
+ os_p.add_argument("--resolution", default="720p", choices=["480p","720p"])
726
+ os_p.add_argument("--variations", type=int, default=4)
727
+ os_p.add_argument("--download-dir")
728
+ os_p.set_defaults(func=cmd_one_shot)
729
+
730
+ # sync (real-time SSE)
731
+ syn = sub.add_parser("sync", help="Real-time sync (collaborative editing)")
732
+ syn_sub = syn.add_subparsers(dest="subcmd", required=True)
733
+ syn_st = syn_sub.add_parser("status", help="Get last-updated timestamp")
734
+ syn_st.add_argument("--entity-type", required=True, choices=["project","content-item"])
735
+ syn_st.add_argument("--entity-id", required=True)
736
+ syn_st.set_defaults(func=cmd_sync_status)
737
+ syn_sm = syn_sub.add_parser("stream", help="Stream SSE updates (Ctrl+C to stop)")
738
+ syn_sm.add_argument("--entity-type", required=True, choices=["project","content-item"])
739
+ syn_sm.add_argument("--entity-id", required=True)
740
+ syn_sm.set_defaults(func=cmd_sync_stream)
741
+
742
+ # quota
743
+ sub.add_parser("quota", help="Show quota / upsell info").set_defaults(func=cmd_quota)
744
+
745
+ # ----- v1.2.0 subcommands -----
746
+
747
+ # publish
748
+ pub = sub.add_parser("publish", help="Publish a content item to Vibes")
749
+ pub.add_argument("--content-id", required=True)
750
+ pub.add_argument("--batch-id")
751
+ pub.add_argument("--caption")
752
+ pub.add_argument("--prompt")
753
+ pub.add_argument("--image-prompt")
754
+ pub.add_argument("--video-prompt")
755
+ pub.set_defaults(func=cmd_publish)
756
+
757
+ # moodboard update
758
+ mb_up = sub.add_parser("moodboard-update", help="Update a moodboard")
759
+ mb_up.add_argument("moodboard_id")
760
+ mb_up.add_argument("--name", help="New name for the moodboard")
761
+ mb_up.set_defaults(func=cmd_moodboard_update)
762
+
763
+ # moodboard lookup
764
+ mb_lk = sub.add_parser("moodboard-lookup", help="Look up moodboard ID by code")
765
+ mb_lk.add_argument("code")
766
+ mb_lk.set_defaults(func=cmd_moodboard_lookup)
767
+
768
+ # share reset
769
+ sh_rs = sub.add_parser("share-reset", help="Reset share link (revoke + create new)")
770
+ sh_rs.add_argument("--entity-type", required=True, choices=["project","content-item"])
771
+ sh_rs.add_argument("--entity-id", required=True)
772
+ sh_rs.set_defaults(func=cmd_share_reset)
773
+
774
+ # playables
775
+ plb = sub.add_parser("playables", help="Playables (interactive posts)")
776
+ plb_sub = plb.add_subparsers(dest="subcmd", required=True)
777
+ plb_ls = plb_sub.add_parser("list", help="List playables")
778
+ plb_ls.add_argument("--limit", type=int, default=100)
779
+ plb_ls.add_argument("--offset", type=int, default=0)
780
+ plb_ls.add_argument("--status")
781
+ plb_ls.set_defaults(func=cmd_playables_list)
782
+ plb_gt = plb_sub.add_parser("get", help="Get a playable")
783
+ plb_gt.add_argument("playable_id")
784
+ plb_gt.set_defaults(func=cmd_playables_get)
785
+ plb_del = plb_sub.add_parser("delete", help="Delete a playable")
786
+ plb_del.add_argument("playable_id")
787
+ plb_del.set_defaults(func=cmd_playables_delete)
788
+
789
+ # ingredient update
790
+ ing_up = sub.add_parser("ingredients-update", help="Update an ingredient")
791
+ ing_up.add_argument("ingredient_id")
792
+ ing_up.add_argument("--name")
793
+ ing_up.add_argument("--description")
794
+ ing_up.add_argument("--personality")
795
+ ing_up.add_argument("--backstory")
796
+ ing_up.add_argument("--core-beliefs")
797
+ ing_up.set_defaults(func=cmd_ingredients_update)
798
+
799
+ # audio resolve
800
+ aud_rs = sub.add_parser("audio-resolve", help="Resolve audio URLs")
801
+ aud_rs.add_argument("--ids", nargs="+", required=True)
802
+ aud_rs.set_defaults(func=cmd_audio_resolve)
803
+
804
+ # audio proxy
805
+ aud_px = sub.add_parser("audio-proxy", help="Build proxy audio URL")
806
+ aud_px.add_argument("audio_id")
807
+ aud_px.add_argument("--title")
808
+ aud_px.set_defaults(func=cmd_audio_proxy)
809
+
810
+ # check-token
811
+ sub.add_parser("check-token", help="Check if session token is valid").set_defaults(func=cmd_check_token)
812
+
813
+ # rate-limit
814
+ sub.add_parser("rate-limit", help="Show current rate limit status").set_defaults(func=cmd_rate_limit)
815
+
816
+ # pending-export
817
+ pe = sub.add_parser("pending-export", help="Check for pending export")
818
+ pe.add_argument("project_id")
819
+ pe.set_defaults(func=cmd_pending_export)
820
+
821
+ # parse midjourney
822
+ pm = sub.add_parser("parse-midjourney", help="Parse Midjourney params from a prompt")
823
+ pm.add_argument("--prompt", required=True)
824
+ pm.set_defaults(func=cmd_parse_midjourney)
825
+
826
+ # validate prompt
827
+ vp = sub.add_parser("validate-prompt", help="Validate prompt length")
828
+ vp.add_argument("--prompt", required=True)
829
+ vp.set_defaults(func=cmd_validate_prompt)
830
+
831
+ return p
832
+
833
+
834
+ def main() -> None:
835
+ p = build_parser()
836
+ args = p.parse_args()
837
+ # Some commands don't need a cookie (offline utilities)
838
+ offline_commands = {"parse-midjourney", "validate-prompt"}
839
+ if args.cmd in offline_commands:
840
+ client = None # type: ignore
841
+ else:
842
+ client = _client(args)
843
+ try:
844
+ args.func(client, args)
845
+ except VibesAPIError as e:
846
+ sys.exit(f"API error: {e}")
847
+ except KeyboardInterrupt:
848
+ sys.exit(130)
849
+
850
+
851
+ if __name__ == "__main__":
852
+ main()