brandly-cli 0.3.4__tar.gz → 0.3.6__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 (122) hide show
  1. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/PKG-INFO +2 -2
  2. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/README.md +1 -1
  3. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/pyproject.toml +1 -1
  4. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/SKILL.md +22 -12
  5. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/troubleshooting.md +11 -8
  6. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/__about__.py +1 -1
  7. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/cli.py +81 -40
  8. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/cost_tracker.py +21 -5
  9. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/director.py +1 -1
  10. brandly_cli-0.3.6/src/brandly_cli/layout.py +274 -0
  11. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/project_manager.py +78 -38
  12. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/utils.py +26 -21
  13. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_cli.py +3 -3
  14. brandly_cli-0.3.6/tests/test_layout.py +149 -0
  15. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_reference.py +16 -15
  16. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.env.example +0 -0
  17. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.github/workflows/ci.yml +0 -0
  18. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.github/workflows/release.yml +0 -0
  19. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.gitignore +0 -0
  20. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.pre-commit-config.yaml +0 -0
  21. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/LICENSE +0 -0
  22. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/Makefile +0 -0
  23. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/assets/banner.png +0 -0
  24. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/AUDIT.md +0 -0
  25. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/DEVELOPMENT.md +0 -0
  26. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/GOAL.md +0 -0
  27. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/IMPLEMENTATION.md +0 -0
  28. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/PROGRESS.md +0 -0
  29. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/SECURITY.md +0 -0
  30. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/SPEC.md +0 -0
  31. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/scripts/install-hooks.sh +0 -0
  32. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/README.md +0 -0
  33. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-animal-sheet/SKILL.md +0 -0
  34. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-animal-sheet/references/habitats.md +0 -0
  35. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-animal-sheet/references/prompt-variants.md +0 -0
  36. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-animal-sheet/references/species-guide.md +0 -0
  37. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-character-sheet/SKILL.md +0 -0
  38. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-character-sheet/references/common-types.md +0 -0
  39. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-character-sheet/references/consistency-checklist.md +0 -0
  40. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-character-sheet/references/prompt-variants.md +0 -0
  41. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-consistency/SKILL.md +0 -0
  42. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-consistency/references/anchoring-system.md +0 -0
  43. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-consistency/references/drift-checklist.md +0 -0
  44. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-consistency/references/lock-strength.md +0 -0
  45. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-location-sheet/SKILL.md +0 -0
  46. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-location-sheet/references/lighting-setups.md +0 -0
  47. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-location-sheet/references/location-types.md +0 -0
  48. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-location-sheet/references/prompt-variants.md +0 -0
  49. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-mecha-sheet/SKILL.md +0 -0
  50. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-mecha-sheet/references/mecha-types.md +0 -0
  51. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-mecha-sheet/references/prompt-variants.md +0 -0
  52. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-mecha-sheet/references/technical-reference.md +0 -0
  53. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-object-sheet/SKILL.md +0 -0
  54. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-object-sheet/references/photography-setups.md +0 -0
  55. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-object-sheet/references/product-categories.md +0 -0
  56. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-object-sheet/references/prompt-variants.md +0 -0
  57. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-plant-sheet/SKILL.md +0 -0
  58. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-plant-sheet/references/plant-species.md +0 -0
  59. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-plant-sheet/references/prompt-variants.md +0 -0
  60. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-plant-sheet/references/seasonal-reference.md +0 -0
  61. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-production-bible/SKILL.md +0 -0
  62. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-production-bible/references/asset-map.md +0 -0
  63. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-production-bible/references/bible-template.md +0 -0
  64. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-production-bible/references/example-bible.md +0 -0
  65. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-storyboard/SKILL.md +0 -0
  66. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-storyboard/references/examples.md +0 -0
  67. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-storyboard/references/pacing-guide.md +0 -0
  68. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-storyboard/references/shot-formats.md +0 -0
  69. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-vehicle-sheet/SKILL.md +0 -0
  70. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-vehicle-sheet/references/prompt-variants.md +0 -0
  71. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-vehicle-sheet/references/shot-angles.md +0 -0
  72. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-vehicle-sheet/references/vehicle-types.md +0 -0
  73. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/camera-control.md +0 -0
  74. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/character-consistency.md +0 -0
  75. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/lighting-presets.md +0 -0
  76. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/video-styles.md +0 -0
  77. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/__init__.py +0 -0
  78. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/__main__.py +0 -0
  79. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/agent_tools.py +0 -0
  80. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/agnes_client.py +0 -0
  81. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/analyzer.py +0 -0
  82. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/ark_client.py +0 -0
  83. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/audio_client.py +0 -0
  84. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/autodirector.py +0 -0
  85. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/beat_sync.py +0 -0
  86. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/captions.py +0 -0
  87. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/constants.py +0 -0
  88. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/dubbing.py +0 -0
  89. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/edit.py +0 -0
  90. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/export_platforms.py +0 -0
  91. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/memory.py +0 -0
  92. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/minimax_client.py +0 -0
  93. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/sharing.py +0 -0
  94. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/stitch.py +0 -0
  95. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/style_presets.py +0 -0
  96. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/sync.py +0 -0
  97. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/templates.py +0 -0
  98. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/thumbnails.py +0 -0
  99. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/trends.py +0 -0
  100. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/types.py +0 -0
  101. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/video_prompts.py +0 -0
  102. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/webhook.py +0 -0
  103. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/__init__.py +0 -0
  104. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/_check.py +0 -0
  105. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_agent_tools.py +0 -0
  106. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_agnes_client.py +0 -0
  107. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_analyzer.py +0 -0
  108. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_ark_client.py +0 -0
  109. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_autodirector.py +0 -0
  110. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_beat_sync.py +0 -0
  111. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_brandly_cli.py +0 -0
  112. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_dubbing.py +0 -0
  113. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_edit.py +0 -0
  114. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_export_platforms.py +0 -0
  115. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_minimax.py +0 -0
  116. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_sharing.py +0 -0
  117. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_stitch.py +0 -0
  118. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_templates.py +0 -0
  119. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_thumbnails.py +0 -0
  120. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_trends.py +0 -0
  121. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_video_prompts.py +0 -0
  122. {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_webhook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: brandly-cli
3
- Version: 0.3.4
3
+ Version: 0.3.6
4
4
  Summary: AI product video orchestrator — add image, video, and sound generation to any AI tool
5
5
  Project-URL: Homepage, https://github.com/Dream-Pixels-Forge/brandly-cli
6
6
  Project-URL: Repository, https://github.com/Dream-Pixels-Forge/brandly-cli
@@ -44,7 +44,7 @@ Description-Content-Type: text/markdown
44
44
  [![PyPI version](https://img.shields.io/pypi/v/brandly-cli.svg)](https://pypi.org/project/brandly-cli/)
45
45
  [![Python >=3.10](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
46
46
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
47
- [![Tests](https://img.shields.io/badge/tests-303_passing-green.svg)](https://github.com/Dream-Pixels-Forge/brandly-cli)
47
+ [![Tests](https://img.shields.io/badge/tests-330_passing-green.svg)](https://github.com/Dream-Pixels-Forge/brandly-cli)
48
48
  [![Lint](https://img.shields.io/badge/lint-ruff_clean-brightgreen.svg)](https://github.com/Dream-Pixels-Forge/brandly-cli)
49
49
  [![CI](https://github.com/Dream-Pixels-Forge/brandly-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Dream-Pixels-Forge/brandly-cli/actions)
50
50
  [![GitHub stars](https://img.shields.io/github/stars/Dream-Pixels-Forge/brandly-cli?style=social)](https://github.com/Dream-Pixels-Forge/brandly-cli/stargazers)
@@ -9,7 +9,7 @@
9
9
  [![PyPI version](https://img.shields.io/pypi/v/brandly-cli.svg)](https://pypi.org/project/brandly-cli/)
10
10
  [![Python >=3.10](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
12
- [![Tests](https://img.shields.io/badge/tests-303_passing-green.svg)](https://github.com/Dream-Pixels-Forge/brandly-cli)
12
+ [![Tests](https://img.shields.io/badge/tests-330_passing-green.svg)](https://github.com/Dream-Pixels-Forge/brandly-cli)
13
13
  [![Lint](https://img.shields.io/badge/lint-ruff_clean-brightgreen.svg)](https://github.com/Dream-Pixels-Forge/brandly-cli)
14
14
  [![CI](https://github.com/Dream-Pixels-Forge/brandly-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Dream-Pixels-Forge/brandly-cli/actions)
15
15
  [![GitHub stars](https://img.shields.io/github/stars/Dream-Pixels-Forge/brandly-cli?style=social)](https://github.com/Dream-Pixels-Forge/brandly-cli/stargazers)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "brandly-cli"
7
- version = "0.3.4"
7
+ version = "0.3.6"
8
8
  description = "AI product video orchestrator — add image, video, and sound generation to any AI tool"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -353,33 +353,43 @@ brandly export <project_id>
353
353
 
354
354
  | Command | Provider | Model Default | Notes |
355
355
  |---------|----------|--------------|-------|
356
- | `brandly image` | Agnes AI | `agnes-image-2.1-flash` | Style presets, 2K/4K output |
357
- | `brandly video` | Agnes AI | `agnes-video-v2.0` | Requires project, ref-image anchoring |
358
- | `brandly minimax-image` | MiniMax | `image-01` | Subject reference, up to 2K |
359
- | `brandly minimax-video` | MiniMax | `MiniMax-H3` | First/last frame, ref video/audio |
356
+ | `brandly image` | Agnes AI | `agnes-image-2.5-flash` | Style presets, 1K/2K/3K/4K output |
357
+ | `brandly video` | Agnes AI | `agnes-video-2.5-flash` | Requires project, ref-image anchoring |
358
+ | `brandly minimax-image` | MiniMax | `image-01` | Subject reference, seed, up to 2K |
359
+ | `brandly minimax-video` | MiniMax | `MiniMax-H3` | First/last frame, ref video/audio, native stereo audio |
360
360
 
361
361
  **Choose based on your needs:**
362
362
  - **Character consistency** → `brandly video` (Agnes) with reference images
363
363
  - **Image generation** → `brandly image` for speed, `brandly minimax-image` for subject reference
364
364
  - **Reference video + audio** → `brandly minimax-video` (MiniMax H3 supports ref video input)
365
- - **Free prototyping** → `brandly video` with `--model agnes-video-2.5-flash` (rate-limited)
365
+ - **Prototyping** → `brandly video` default (`agnes-video-2.5-flash` — currently free; 720P, 4-12s)
366
+ - **Rate limits** → `brandly rate-limits` (Agnes RPM by tier; MiniMax H3 concurrent tasks)
366
367
 
367
368
  ## API Notes
368
369
 
369
- - **Image models** (Agnes): `agnes-image-2.1-flash` (fast, default), `agnes-image-2.0-flash` (premium), `agnes-image-2.5-flash` (latest)
370
- - **Video models** (Agnes): `agnes-video-v2.0` (reliable, 1080p max), `agnes-video-2.5-flash` (free, rate-limited)
371
- - **MiniMax image**: `image-01`, `image-01-live` — subject reference supported
372
- - **MiniMax video**: `MiniMax-H3` (ref video/audio, 2K), `MiniMax-H3-Max` (fast, first/last frame)
370
+ - **Image models** (Agnes): `agnes-image-2.5-flash` (current default — latest gen, same API contract as 2.1, sizes 1K-4K), `agnes-image-2.1-flash` / `agnes-image-2.0-flash` (legacy)
371
+ - **Video models** (Agnes): `agnes-video-2.5-flash` (current default — 720P, 4-12s, ≤5 ref images, ≤3 ref audios, no ref video), `agnes-video-v2.0` (legacy, 1080p max)
372
+ - **MiniMax image**: `image-01`, `image-01-live` — subject reference, `--seed` reproducibility, `--style` art settings (live)
373
+ - **MiniMax video**: `MiniMax-H3` (ref video/audio, 2K, native synchronized stereo audio via [sound] prompt tag), `MiniMax-H3-Max` (fast, first/last frame only)
373
374
  - **Negative prompts**: Not supported by Agnes API — use prompt engineering via style presets instead
374
375
  - **Output formats**: Images as URLs, videos as MP4 with direct download links
375
376
  - **Size options**: 1K, 2K, 3K, 4K for Agnes images; 720P/1080P/2K for MiniMax images; 480P/768P/2K for MiniMax video
376
377
 
378
+ ## Provider Rate Limits (v0.3.4+)
379
+
380
+ Run `brandly rate-limits` for the live table. Headlines:
381
+
382
+ - **Agnes AI** (free/default key): text 20 RPM; images 1K 20 RPM, 2K 10 RPM, 3K/4K 1 RPM; video free quota ~500s/day. Enterprise keys get ~2× on 1K/2K image RPM.
383
+ - **MiniMax**: H3 video is governed by *concurrent tasks* (2 free / 15 paid), not RPM; text tier 20 RPM free / 200 paid.
384
+ - **Batch safety**: MiniMax image `n > 1` batches automatically fall back to one-at-a-time generation on 429 or partial results; the `batch` video command continues past failed variants instead of aborting the run.
385
+
386
+
377
387
  ## Troubleshooting
378
388
 
379
389
  ### Video Takes Too Long
380
- - v2.0 model: ~20-60 seconds for 5s video
381
- - 2.5-flash: Free but rate-limited to 1 request per minute
382
- - Use `--wait` to poll until complete
390
+ - 2.5-flash (default): ~20-60s for a 5s clip; poll with `--wait`
391
+ - On 429s, check provider limits with `brandly rate-limits` before retrying
392
+ - MiniMax H3 is concurrency-limited (2 free / 15 paid parallel tasks), not per-minute RPM
383
393
 
384
394
  ### Character Drifts Between Shots
385
395
  - Add more detailed character description
@@ -8,13 +8,16 @@ Common issues and solutions for video generation.
8
8
 
9
9
  **Symptoms:** Generation hangs or takes excessive time
10
10
  **Causes:**
11
- - v2.0 model processing time (~20-60 seconds for 5s video)
12
- - 2.5-flash rate limiting (1 request per minute)
11
+ - Model processing time (~20-60 seconds for a 5s video)
12
+ - Agnes provider rate limits (free key: 20 text RPM, 2K images 10 RPM — see `brandly rate-limits`)
13
13
  - Large resolution requests
14
14
 
15
15
  **Solutions:**
16
16
  ```bash
17
- # Use v2.0 for reliable performance
17
+ # Default model is agnes-video-2.5-flash — poll with --wait
18
+ brandly video <id> --wait
19
+
20
+ # Or pin the legacy v2.0 model explicitly
18
21
  brandly video <id> --model agnes-video-v2.0 --wait
19
22
 
20
23
  # Check API status
@@ -29,9 +32,9 @@ brandly status
29
32
  - Rapid successive requests
30
33
 
31
34
  **Solutions:**
32
- - Switch to v2.0 model for consistent performance
33
- - Add delays between requests: `sleep 60`
34
- - Queue generation requests
35
+ - Check the limits table: `brandly rate-limits`
36
+ - Add delays between requests (e.g. `sleep 6` for 10 RPM at 2K images)
37
+ - Queue generation requests; MiniMax H3 keeps 2 (free) / 15 (paid) tasks in flight, not more
35
38
 
36
39
  ## Quality Issues
37
40
 
@@ -45,7 +48,7 @@ brandly status
45
48
 
46
49
  **Solutions:**
47
50
  ```bash
48
- # Use a higher-quality model (default is already v2.0; 2.5-flash is free but rate-limited)
51
+ # Default is agnes-video-2.5-flash (720P, 4-12s); pin legacy v2.0 for 1080p
49
52
  brandly video <id> --style cinematic --model agnes-video-v2.0 --duration 5
50
53
 
51
54
  # Add quality keywords to prompt
@@ -182,5 +185,5 @@ If issues persist:
182
185
  1. Check the API status page
183
186
  2. Review error logs: `brandly logs`
184
187
  3. Test with minimal prompt
185
- 4. Try different model (v2.0 vs 2.5-flash)
188
+ 4. Check provider limits with `brandly rate-limits` and wait out the window
186
189
  5. Consult documentation or support
@@ -1,3 +1,3 @@
1
1
  """Package metadata."""
2
2
 
3
- __version__ = "0.3.4"
3
+ __version__ = "0.3.6"
@@ -16,7 +16,7 @@ from rich.markdown import Markdown
16
16
  from rich.panel import Panel
17
17
  from rich.table import Table
18
18
 
19
- from brandly_cli import __version__
19
+ from brandly_cli import __version__, layout
20
20
  from brandly_cli.agent_tools import get_builtin_tools
21
21
  from brandly_cli.agnes_client import (
22
22
  agent_tool_loop,
@@ -150,12 +150,22 @@ def _save_artifact(
150
150
  *,
151
151
  root: Path,
152
152
  prompt_hint: str = "",
153
+ category: str | None = None,
153
154
  ) -> Path | None:
154
155
  """Download a generated asset and save it under
155
- .brandly/projects/{id}/artifacts/{type_label}/{timestamp}_{hash}.{ext}."""
156
+ .brandly/{project_id}/{images|videos|audio}/{category}/.
157
+
158
+ ``category`` routes the file into a sub-folder (e.g. 'scenes' for video,
159
+ 'soundtrack' for music). Omit it to use the 'general' default.
160
+ """
156
161
  if not url:
157
162
  return None
158
- artifacts_dir = root / ".brandly" / "projects" / project_id / "artifacts" / type_label
163
+ proj_dir = layout.project_dir(root, project_id)
164
+ if type_label == "refs":
165
+ artifacts_dir = layout.refs_dir(proj_dir)
166
+ else:
167
+ media_category = category if category else "general"
168
+ artifacts_dir = layout.media_dir(proj_dir, type_label, media_category)
159
169
  ext = Path(url.split("?")[0]).suffix or (".mp3" if type_label == "audio" else "")
160
170
  if not ext:
161
171
  ext = ".bin"
@@ -367,7 +377,7 @@ def list_projects(ctx: click.Context) -> None:
367
377
  def _check_budget(ctx: click.Context, project_id: str) -> None:
368
378
  """Warn if project budget has been exceeded."""
369
379
  root = _get_root(ctx)
370
- ct = CostTracker(root / ".brandly" / "projects")
380
+ ct = CostTracker(root / ".brandly")
371
381
  try:
372
382
  summary = asyncio.run(ct.get_summary(project_id))
373
383
  if summary["total"] >= summary["budget"] and summary["budget"] > 0:
@@ -716,7 +726,7 @@ def reference(
716
726
  key asset (object, character, location, etc.) across all subsequent
717
727
  `brandly video` generations. Should be the FIRST generation step.
718
728
 
719
- The generated image is saved to .brandly/projects/<id>/artifacts/images/
729
+ The generated image is saved to .brandly/<project_id>/refs/
720
730
  with the prefix `reference_<subject_type>_*` and is auto-detected by
721
731
  `brandly video` (auto-injected as the strongest reference image).
722
732
  """
@@ -779,7 +789,7 @@ def reference(
779
789
  except Exception as e:
780
790
  console.print(f"[red]Error generating reference: {e}[/red]")
781
791
  if project_id:
782
- docs_dir = root / ".brandly" / "projects" / project_id / "docs"
792
+ docs_dir = layout.docs_dir(layout.project_dir(root, project_id), "tmp")
783
793
  docs_dir.mkdir(parents=True, exist_ok=True)
784
794
  fail_doc = (
785
795
  docs_dir
@@ -802,28 +812,31 @@ def reference(
802
812
 
803
813
  from brandly_cli.utils import write_generation_doc
804
814
 
815
+ # Primary references live in refs/ — the identity-locking folder.
805
816
  saved = _save_artifact(
806
817
  url,
807
818
  project_id,
808
- "images",
819
+ "refs",
809
820
  root=root,
810
821
  prompt_hint=f"reference_{subject_type}_{subject}",
811
822
  )
812
823
  if saved:
813
- # Rename the saved file to reference_<subject_type>_<timestamp>.<ext>
824
+ # Rename to the conventional sheet name:
825
+ # character -> char_<name> · location -> loc_<name> · object -> prop_<name>
814
826
  timestamp = now_iso().replace(":", "-").replace(".", "_")
815
827
  ext = saved.suffix or ".png"
816
- new_name = f"reference_{subject_type}_{timestamp}{ext}"
817
- new_path = saved.parent / new_name
828
+ stem = layout.build_sheet_filename(subject_type, subject, timestamp)
829
+ new_path = saved.parent / f"{stem}{ext}"
818
830
  try:
819
831
  saved.rename(new_path)
820
832
  except OSError:
821
- new_path = saved # fall back to original name if rename fails
822
- console.print(f"[green]✓ Reference image saved:[/green] {new_path}")
833
+ new_path = saved # fall back to the original name if rename fails
834
+ saved = new_path
835
+ console.print(f"[green]✓ Reference image saved:[/green] {saved}")
823
836
  write_generation_doc(
824
837
  project_id,
825
838
  "reference",
826
- new_path,
839
+ saved,
827
840
  root=root,
828
841
  prompt=prompt,
829
842
  model=model,
@@ -842,7 +855,7 @@ def reference(
842
855
  "subject_type": subject_type,
843
856
  "skill": subject_skill,
844
857
  "subject": subject,
845
- "image_path": str(new_path),
858
+ "image_path": str(saved),
846
859
  "source_url": url,
847
860
  "generated_at": now_iso(),
848
861
  "model": model,
@@ -997,7 +1010,7 @@ def image(
997
1010
  # Update plan to show failure
998
1011
  from brandly_cli.utils import write_generation_doc
999
1012
 
1000
- docs_dir = Path(f".brandly/projects/{project_id}/docs")
1013
+ docs_dir = layout.docs_dir(layout.project_dir(root, project_id), "tmp")
1001
1014
  docs_dir.mkdir(parents=True, exist_ok=True)
1002
1015
  fail_doc = docs_dir / f"image_fail_{now_iso().replace(':', '-').replace('.', '_')}.md"
1003
1016
  fail_doc.write_text(
@@ -1345,7 +1358,14 @@ def video(
1345
1358
  # Always save video to disk if URL exists
1346
1359
  if url:
1347
1360
  root = _get_root(ctx)
1348
- saved = _save_artifact(url, project_id, "videos", root=root, prompt_hint=prompt)
1361
+ saved = _save_artifact(
1362
+ url,
1363
+ project_id,
1364
+ "videos",
1365
+ root=root,
1366
+ prompt_hint=prompt,
1367
+ category="scenes",
1368
+ )
1349
1369
  if saved:
1350
1370
  console.print(f" Saved → {saved}")
1351
1371
  # Write generation document
@@ -1480,7 +1500,9 @@ def music(
1480
1500
  # Save to disk
1481
1501
  pid = project_id or ""
1482
1502
  root = _get_root(ctx)
1483
- saved = _save_artifact(url, pid, "audio", root=root, prompt_hint=prompt)
1503
+ saved = _save_artifact(
1504
+ url, pid, "audio", root=root, prompt_hint=prompt, category="soundtrack"
1505
+ )
1484
1506
  if saved:
1485
1507
  console.print(f" Saved → {saved}")
1486
1508
  _print_json(result)
@@ -1520,7 +1542,14 @@ def tts(
1520
1542
  # Save to disk
1521
1543
  pid = project_id or ""
1522
1544
  root = _get_root(ctx)
1523
- saved = _save_artifact(url, pid, "audio", root=root, prompt_hint=text[:60])
1545
+ saved = _save_artifact(
1546
+ url,
1547
+ pid,
1548
+ "audio",
1549
+ root=root,
1550
+ prompt_hint=text[:60],
1551
+ category="voiceover",
1552
+ )
1524
1553
  if saved:
1525
1554
  console.print(f" Saved → {saved}")
1526
1555
  _print_json(result)
@@ -1676,7 +1705,7 @@ def memory(ctx: click.Context, action: str, hook: str | None) -> None:
1676
1705
  "--output",
1677
1706
  "-o",
1678
1707
  default=None,
1679
- help="Output directory (default: .brandly/projects/{id}/export/)",
1708
+ help="Output directory (default: .brandly/{id}/export/)",
1680
1709
  )
1681
1710
  @click.pass_context
1682
1711
  def export(ctx: click.Context, project_id: str, output: str | None) -> None:
@@ -1691,18 +1720,22 @@ def export(ctx: click.Context, project_id: str, output: str | None) -> None:
1691
1720
  console.print(f"[red]Project not found: {project_id}[/red]")
1692
1721
  sys.exit(1)
1693
1722
 
1694
- out_dir = Path(output) if output else root / ".brandly" / "projects" / project_id / "export"
1723
+ proj_dir = layout.resolve_project_dir(root, project_id)
1724
+ out_dir = Path(output) if output else proj_dir / "export"
1695
1725
 
1696
- # Files in the project root that are bookkeeping, not user-facing artifacts.
1697
- # These are excluded from the export.
1698
- internal_files = {"project.json", "cost.json"}
1726
+ # Bookkeeping files at the project root are excluded from the export
1727
+ # by only scanning the user-facing top folders (refs/images/videos/audio/docs).
1699
1728
 
1700
1729
  artifact_count = 0
1701
1730
  media_count = 0
1702
- media_exts = {".png", ".jpg", ".jpeg", ".mp4", ".webm", ".mp3", ".wav", ".mpga"}
1731
+ media_exts = {".png", ".jpg", ".jpeg", ".webp", ".mp4", ".webm", ".mp3", ".wav", ".mpga"}
1732
+ # User-facing media + docs + refs live in these top folders of the project dir.
1703
1733
  for search_dir in [
1704
- root / ".brandly" / "projects" / project_id / "artifacts",
1705
- root / ".brandly" / "projects" / project_id,
1734
+ proj_dir / "refs",
1735
+ proj_dir / "images",
1736
+ proj_dir / "videos",
1737
+ proj_dir / "audio",
1738
+ proj_dir / "docs",
1706
1739
  ]:
1707
1740
  if not search_dir.exists():
1708
1741
  continue
@@ -1715,11 +1748,8 @@ def export(ctx: click.Context, project_id: str, output: str | None) -> None:
1715
1748
  continue
1716
1749
  except ValueError:
1717
1750
  pass
1718
- # Skip bookkeeping files at the project root
1719
- if f.parent == root / ".brandly" / "projects" / project_id and f.name in internal_files:
1720
- continue
1721
1751
  ext = f.suffix.lower()
1722
- rel = f.relative_to(search_dir)
1752
+ rel = f.relative_to(proj_dir)
1723
1753
  dest = out_dir / rel
1724
1754
  dest.parent.mkdir(parents=True, exist_ok=True)
1725
1755
  import shutil
@@ -1775,7 +1805,7 @@ def cost(ctx: click.Context, project_id: str) -> None:
1775
1805
  console.print("[red]Invalid project ID format.[/red]")
1776
1806
  sys.exit(1)
1777
1807
  root = _get_root(ctx)
1778
- ct = CostTracker(root / ".brandly" / "projects")
1808
+ ct = CostTracker(root / ".brandly")
1779
1809
  try:
1780
1810
  summary = asyncio.run(ct.get_summary(project_id))
1781
1811
  except FileNotFoundError:
@@ -1803,7 +1833,7 @@ def record_cost(ctx: click.Context, project_id: str, phase: str, action: str, cr
1803
1833
  if not proj:
1804
1834
  console.print(f"[red]Project not found: {project_id}[/red]")
1805
1835
  sys.exit(1)
1806
- ct = CostTracker(root / ".brandly" / "projects")
1836
+ ct = CostTracker(root / ".brandly")
1807
1837
  try:
1808
1838
  result = asyncio.run(
1809
1839
  ct.record_spend(project_id, phase, action, credits, budget_credits=proj.budget)
@@ -2882,7 +2912,14 @@ def ark_image(
2882
2912
  # Save first URL to disk
2883
2913
  if urls and ctx.obj.get("root"):
2884
2914
  root = Path(ctx.obj["root"])
2885
- saved = _save_artifact(urls[0], "ark", "images", root=root, prompt_hint=prompt)
2915
+ saved = _save_artifact(
2916
+ urls[0],
2917
+ "ark",
2918
+ "images",
2919
+ root=root,
2920
+ prompt_hint=prompt,
2921
+ category="general",
2922
+ )
2886
2923
  if saved:
2887
2924
  console.print(f" Saved → {saved}")
2888
2925
  else:
@@ -2967,7 +3004,9 @@ def ark_video(
2967
3004
  console.print(f"[green]✓ Video ready:[/green] {url or 'no URL'}")
2968
3005
  if url:
2969
3006
  root = _get_root(ctx)
2970
- saved = _save_artifact(url, project_id, "videos", root=root, prompt_hint=prompt)
3007
+ saved = _save_artifact(
3008
+ url, project_id, "videos", root=root, prompt_hint=prompt, category="scenes"
3009
+ )
2971
3010
  if saved:
2972
3011
  console.print(f" Saved → {saved}")
2973
3012
 
@@ -3078,15 +3117,16 @@ def export_platforms(project_id, platforms, output, root):
3078
3117
 
3079
3118
  if not platforms:
3080
3119
  platforms = ("tiktok", "youtube_standard")
3081
- proj_dir = Path(root or ".") / ".brandly" / "projects" / project_id
3120
+ proj_dir = layout.resolve_project_dir(Path(root or "."), project_id)
3082
3121
  if not proj_dir.exists():
3083
3122
  console.print(f"[red]Project not found: {project_id}[/red]")
3084
3123
  sys.exit(1)
3085
- video_file = next(proj_dir.glob("*.mp4"), None)
3124
+ videos_root = proj_dir / "videos"
3125
+ video_file = next((videos_root.rglob("*.mp4")), None)
3086
3126
  if not video_file:
3087
3127
  console.print("[yellow]No video found in project[/yellow]")
3088
3128
  sys.exit(1)
3089
- out_dir = Path(output) if output else proj_dir / "exports"
3129
+ out_dir = Path(output) if output else proj_dir / "export"
3090
3130
  for platform in platforms:
3091
3131
  console.print(f"Exporting for [bold]{platform}[/bold]...")
3092
3132
  result = asyncio.run(export_for_platform(video_file, platform, out_dir, root=root))
@@ -3104,12 +3144,13 @@ def export_platforms(project_id, platforms, output, root):
3104
3144
  def thumbnail(project_id: str, count: int, style: str, root: str | None) -> None:
3105
3145
  """Generate thumbnails from project video."""
3106
3146
  from brandly_cli.thumbnails import generate_thumbnails
3107
- proj_dir = Path(root or ".") / ".brandly" / "projects" / project_id
3108
- video_file = next(proj_dir.glob("*.mp4"), None)
3147
+ proj_dir = layout.resolve_project_dir(Path(root or "."), project_id)
3148
+ videos_root = proj_dir / "videos"
3149
+ video_file = next((videos_root.rglob("*.mp4")), None)
3109
3150
  if not video_file:
3110
3151
  console.print(f"[red]No video found in project: {project_id}[/red]")
3111
3152
  sys.exit(1)
3112
- output_dir = proj_dir / "thumbnails"
3153
+ output_dir = layout.media_dir(proj_dir, "images", "general")
3113
3154
  result = asyncio.run(
3114
3155
  generate_thumbnails(
3115
3156
  video_file, output_dir, count=count, style_preset=style,
@@ -59,13 +59,29 @@ class ProjectCostState:
59
59
 
60
60
 
61
61
  class CostTracker:
62
- """Tracks credit spend per project and enforces budget gates."""
63
-
64
- def __init__(self, projects_dir: str | Path) -> None:
65
- self.base = Path(projects_dir)
62
+ """Tracks credit spend per project and enforces budget gates.
63
+
64
+ ``base`` is the ``.brandly`` directory. Each project's cost state lives at
65
+ ``.brandly/{project_id}/cost.json`` (new layout); the legacy
66
+ ``.brandly/projects/{project_id}/cost.json`` location is used as a
67
+ fallback for older installs.
68
+ """
69
+
70
+ def __init__(self, base: str | Path) -> None:
71
+ self.base = Path(base)
72
+
73
+ def _resolve_project_dir(self, project_id: str) -> Path:
74
+ """New layout first; fall back to the legacy ``projects/`` tree."""
75
+ new_dir = self.base / project_id
76
+ if (new_dir / "project.json").exists() or (new_dir / "cost.json").exists():
77
+ return new_dir
78
+ legacy_dir = self.base / "projects" / project_id
79
+ if (legacy_dir / "project.json").exists() or (legacy_dir / "cost.json").exists():
80
+ return legacy_dir
81
+ return new_dir
66
82
 
67
83
  def _cost_path(self, project_id: str) -> Path:
68
- return self.base / project_id / "cost.json"
84
+ return self._resolve_project_dir(project_id) / "cost.json"
69
85
 
70
86
  def _load(self, project_id: str) -> ProjectCostState:
71
87
  path = self._cost_path(project_id)
@@ -139,7 +139,7 @@ class DirectorConfig:
139
139
  self.default_budget = default_budget
140
140
  self.default_shots = default_shots
141
141
  self.pm = ProjectManager(self.root)
142
- self.ct = CostTracker(self.root / ".brandly" / "projects")
142
+ self.ct = CostTracker(self.root / ".brandly")
143
143
  self.mem = UserPreferences(self.root)
144
144
 
145
145