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.
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/PKG-INFO +2 -2
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/README.md +1 -1
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/pyproject.toml +1 -1
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/SKILL.md +22 -12
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/troubleshooting.md +11 -8
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/__about__.py +1 -1
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/cli.py +81 -40
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/cost_tracker.py +21 -5
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/director.py +1 -1
- brandly_cli-0.3.6/src/brandly_cli/layout.py +274 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/project_manager.py +78 -38
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/utils.py +26 -21
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_cli.py +3 -3
- brandly_cli-0.3.6/tests/test_layout.py +149 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_reference.py +16 -15
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.env.example +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.github/workflows/ci.yml +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.github/workflows/release.yml +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.gitignore +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/.pre-commit-config.yaml +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/LICENSE +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/Makefile +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/assets/banner.png +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/AUDIT.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/DEVELOPMENT.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/GOAL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/IMPLEMENTATION.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/PROGRESS.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/SECURITY.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/dev-notes/SPEC.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/scripts/install-hooks.sh +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/README.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-animal-sheet/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-animal-sheet/references/habitats.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-animal-sheet/references/prompt-variants.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-animal-sheet/references/species-guide.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-character-sheet/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-character-sheet/references/common-types.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-character-sheet/references/consistency-checklist.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-character-sheet/references/prompt-variants.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-consistency/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-consistency/references/anchoring-system.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-consistency/references/drift-checklist.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-consistency/references/lock-strength.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-location-sheet/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-location-sheet/references/lighting-setups.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-location-sheet/references/location-types.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-location-sheet/references/prompt-variants.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-mecha-sheet/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-mecha-sheet/references/mecha-types.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-mecha-sheet/references/prompt-variants.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-mecha-sheet/references/technical-reference.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-object-sheet/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-object-sheet/references/photography-setups.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-object-sheet/references/product-categories.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-object-sheet/references/prompt-variants.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-plant-sheet/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-plant-sheet/references/plant-species.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-plant-sheet/references/prompt-variants.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-plant-sheet/references/seasonal-reference.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-production-bible/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-production-bible/references/asset-map.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-production-bible/references/bible-template.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-production-bible/references/example-bible.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-storyboard/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-storyboard/references/examples.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-storyboard/references/pacing-guide.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-storyboard/references/shot-formats.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-vehicle-sheet/SKILL.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-vehicle-sheet/references/prompt-variants.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-vehicle-sheet/references/shot-angles.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-vehicle-sheet/references/vehicle-types.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/camera-control.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/character-consistency.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/lighting-presets.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/skills/brandly-video-generation/references/video-styles.md +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/__init__.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/__main__.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/agent_tools.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/agnes_client.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/analyzer.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/ark_client.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/audio_client.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/autodirector.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/beat_sync.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/captions.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/constants.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/dubbing.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/edit.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/export_platforms.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/memory.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/minimax_client.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/sharing.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/stitch.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/style_presets.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/sync.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/templates.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/thumbnails.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/trends.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/types.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/video_prompts.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/src/brandly_cli/webhook.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/__init__.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/_check.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_agent_tools.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_agnes_client.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_analyzer.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_ark_client.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_autodirector.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_beat_sync.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_brandly_cli.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_dubbing.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_edit.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_export_platforms.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_minimax.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_sharing.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_stitch.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_templates.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_thumbnails.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_trends.py +0 -0
- {brandly_cli-0.3.4 → brandly_cli-0.3.6}/tests/test_video_prompts.py +0 -0
- {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.
|
|
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
|
[](https://pypi.org/project/brandly-cli/)
|
|
45
45
|
[](https://www.python.org/downloads/)
|
|
46
46
|
[](LICENSE)
|
|
47
|
-
[](https://github.com/Dream-Pixels-Forge/brandly-cli)
|
|
48
48
|
[](https://github.com/Dream-Pixels-Forge/brandly-cli)
|
|
49
49
|
[](https://github.com/Dream-Pixels-Forge/brandly-cli/actions)
|
|
50
50
|
[](https://github.com/Dream-Pixels-Forge/brandly-cli/stargazers)
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://pypi.org/project/brandly-cli/)
|
|
10
10
|
[](https://www.python.org/downloads/)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](https://github.com/Dream-Pixels-Forge/brandly-cli)
|
|
13
13
|
[](https://github.com/Dream-Pixels-Forge/brandly-cli)
|
|
14
14
|
[](https://github.com/Dream-Pixels-Forge/brandly-cli/actions)
|
|
15
15
|
[](https://github.com/Dream-Pixels-Forge/brandly-cli/stargazers)
|
|
@@ -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.
|
|
357
|
-
| `brandly video` | Agnes AI | `agnes-video-
|
|
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
|
-
- **
|
|
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.
|
|
370
|
-
- **Video models** (Agnes): `agnes-video-
|
|
371
|
-
- **MiniMax image**: `image-01`, `image-01-live` — subject reference
|
|
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
|
-
-
|
|
381
|
-
-
|
|
382
|
-
-
|
|
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
|
-
-
|
|
12
|
-
-
|
|
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
|
-
#
|
|
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
|
-
-
|
|
33
|
-
- Add delays between requests
|
|
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
|
-
#
|
|
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.
|
|
188
|
+
4. Check provider limits with `brandly rate-limits` and wait out the window
|
|
186
189
|
5. Consult documentation or support
|
|
@@ -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/
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
|
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
|
-
"
|
|
819
|
+
"refs",
|
|
809
820
|
root=root,
|
|
810
821
|
prompt_hint=f"reference_{subject_type}_{subject}",
|
|
811
822
|
)
|
|
812
823
|
if saved:
|
|
813
|
-
# Rename the
|
|
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
|
-
|
|
817
|
-
new_path = saved.parent /
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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/
|
|
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
|
-
|
|
1723
|
+
proj_dir = layout.resolve_project_dir(root, project_id)
|
|
1724
|
+
out_dir = Path(output) if output else proj_dir / "export"
|
|
1695
1725
|
|
|
1696
|
-
#
|
|
1697
|
-
#
|
|
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
|
-
|
|
1705
|
-
|
|
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(
|
|
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"
|
|
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"
|
|
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(
|
|
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(
|
|
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 ".")
|
|
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
|
-
|
|
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 / "
|
|
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 ".")
|
|
3108
|
-
|
|
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
|
|
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
|
-
|
|
65
|
-
|
|
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.
|
|
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"
|
|
142
|
+
self.ct = CostTracker(self.root / ".brandly")
|
|
143
143
|
self.mem = UserPreferences(self.root)
|
|
144
144
|
|
|
145
145
|
|