cast-studio 0.1.0__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.
- cast_studio-0.1.0/LICENSE +21 -0
- cast_studio-0.1.0/MANIFEST.in +4 -0
- cast_studio-0.1.0/PKG-INFO +208 -0
- cast_studio-0.1.0/README.md +175 -0
- cast_studio-0.1.0/cast_studio/__init__.py +0 -0
- cast_studio-0.1.0/cast_studio/cli.py +20 -0
- cast_studio-0.1.0/cast_studio/cli_render.py +24 -0
- cast_studio-0.1.0/cast_studio/cli_run.py +14 -0
- cast_studio-0.1.0/cast_studio/commands/__init__.py +0 -0
- cast_studio-0.1.0/cast_studio/commands/cast.py +40 -0
- cast_studio-0.1.0/cast_studio/commands/init.py +65 -0
- cast_studio-0.1.0/cast_studio/commands/render.py +114 -0
- cast_studio-0.1.0/cast_studio/encoder.py +110 -0
- cast_studio-0.1.0/cast_studio/renderer.py +66 -0
- cast_studio-0.1.0/cast_studio/templates/demo.cfg.example +70 -0
- cast_studio-0.1.0/cast_studio/templates/run_demo.sh +110 -0
- cast_studio-0.1.0/cast_studio/terminal.py +141 -0
- cast_studio-0.1.0/cast_studio.egg-info/PKG-INFO +208 -0
- cast_studio-0.1.0/cast_studio.egg-info/SOURCES.txt +24 -0
- cast_studio-0.1.0/cast_studio.egg-info/dependency_links.txt +1 -0
- cast_studio-0.1.0/cast_studio.egg-info/entry_points.txt +5 -0
- cast_studio-0.1.0/cast_studio.egg-info/not-zip-safe +1 -0
- cast_studio-0.1.0/cast_studio.egg-info/requires.txt +10 -0
- cast_studio-0.1.0/cast_studio.egg-info/top_level.txt +1 -0
- cast_studio-0.1.0/pyproject.toml +61 -0
- cast_studio-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Avi Zaguri
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cast-studio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Convert asciinema .cast files to GIF and MP4, with a generic demo-recording engine
|
|
5
|
+
Author: Avi Zaguri
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aviz92/cast-studio
|
|
8
|
+
Project-URL: Repository, https://github.com/aviz92/cast-studio
|
|
9
|
+
Project-URL: Issues, https://github.com/aviz92/cast-studio/issues
|
|
10
|
+
Keywords: asciinema,cast,gif,mp4,terminal,demo,recording
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Multimedia :: Video :: Conversion
|
|
18
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
19
|
+
Requires-Python: >=3.12
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: custom-python-logger>=2.0.13
|
|
23
|
+
Requires-Dist: pillow>=12.1.1
|
|
24
|
+
Requires-Dist: pre-commit>=4.5.0
|
|
25
|
+
Requires-Dist: pytest>=9.0.1
|
|
26
|
+
Requires-Dist: pytest-plugins>=2.0.0
|
|
27
|
+
Requires-Dist: python-base-command>=0.1.3
|
|
28
|
+
Requires-Dist: python-base-toolkit>=0.0.17
|
|
29
|
+
Requires-Dist: setuptools>=80.9.0
|
|
30
|
+
Requires-Dist: tenacity>=9.1.2
|
|
31
|
+
Requires-Dist: wheel>=0.45.1
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+

|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
# 🎬 cast-studio
|
|
44
|
+
|
|
45
|
+
Convert [asciinema](https://asciinema.org/) `.cast` recordings into **GIF** and **MP4** — and scaffold a generic demo-recording engine for any Python project.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 📦 Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv add cast-studio
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> **System requirement:** `ffmpeg` must be installed.
|
|
56
|
+
> ```bash
|
|
57
|
+
> brew install ffmpeg # macOS
|
|
58
|
+
> apt-get install ffmpeg # Ubuntu / Debian
|
|
59
|
+
> ```
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## 🚀 Features
|
|
64
|
+
|
|
65
|
+
- ✅ **cast → GIF** — High-quality 256-colour GIF via ffmpeg palette pass
|
|
66
|
+
- ✅ **cast → MP4** — H.264/x264 CRF-18 MP4 ready for GitHub Releases
|
|
67
|
+
- ✅ **Catppuccin Mocha theme** — Beautiful dark terminal rendering with Pillow
|
|
68
|
+
- ✅ **Generic demo engine** — `cast-run` + `demo.cfg` for any project
|
|
69
|
+
- ✅ **Any shell command** — Record pytest runs, scripts, CLIs — not just pytest
|
|
70
|
+
- ✅ **Multi-line descriptions** — Pipe-separated description lines per run
|
|
71
|
+
- ✅ **`python-base-command` CLI** — Structured, loggable CLI with `cast` / `cast-render` / `cast-init`
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## ⚙️ Configuration
|
|
76
|
+
|
|
77
|
+
No `.env` needed. All config lives in `demo.cfg`:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
PROJECT="my-library"
|
|
81
|
+
SUBTITLE="A short description"
|
|
82
|
+
INSTALL_CMD="pip install my-library"
|
|
83
|
+
REPO_URL="github.com/you/my-library"
|
|
84
|
+
PYPI_URL="pypi.org/project/my-library"
|
|
85
|
+
|
|
86
|
+
PYTEST=".venv/bin/pytest"
|
|
87
|
+
TESTS="tests/"
|
|
88
|
+
|
|
89
|
+
PAUSE_INTRO=2 # seconds after intro screen
|
|
90
|
+
PAUSE_BETWEEN=2 # seconds between runs
|
|
91
|
+
PAUSE_OUTRO=3 # seconds on outro screen
|
|
92
|
+
|
|
93
|
+
define_runs() {
|
|
94
|
+
add_run "RUN 1 — feature A" "Short description." "$PYTEST $TESTS --flag"
|
|
95
|
+
add_run "RUN 2 — script" "Another feature." "python scripts/my_script.py"
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> `add_run "Title" "Line 1|Line 2" "any shell command"` — use `|` for multi-line descriptions.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🛠️ How to Use
|
|
104
|
+
|
|
105
|
+
1. **Install** — `uv add cast-studio` (and `brew install ffmpeg asciinema`)
|
|
106
|
+
3. **Create .cfg** — customise `demo.cfg` with your project's runs
|
|
107
|
+
4. **Record** — `asciinema rec -c "cast-run demo/demo.cfg" demo.cast`
|
|
108
|
+
5. **Render** — `cast-render demo.cast assets/demo --gif-only --title "my demo"` → `assets/demo.gif`
|
|
109
|
+
6. **Embed** — add `` to your README
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## 🚀 Quick Start
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
# 1. Install
|
|
117
|
+
uv add cast-studio
|
|
118
|
+
brew install ffmpeg asciinema # macOS
|
|
119
|
+
|
|
120
|
+
# 2. Create demo/demo.cfg — set PROJECT, SUBTITLE, INSTALL_CMD, define_runs()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`demo/demo.cfg` structure:
|
|
124
|
+
```bash
|
|
125
|
+
PROJECT="my-library"
|
|
126
|
+
SUBTITLE="A short description"
|
|
127
|
+
INSTALL_CMD="uv add my-library"
|
|
128
|
+
REPO_URL="github.com/you/my-library"
|
|
129
|
+
PYPI_URL="pypi.org/project/my-library"
|
|
130
|
+
|
|
131
|
+
define_runs() {
|
|
132
|
+
add_run "RUN 1 — feature A" "Short description." "pytest tests/ --flag"
|
|
133
|
+
add_run "RUN 2 — script" "Another feature." "python scripts/my_script.py"
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# 3. Record
|
|
139
|
+
asciinema rec -c "bash cast-run demo/demo.cfg" assets/demo/demo.cast
|
|
140
|
+
|
|
141
|
+
# 4. Render to GIF and MP4
|
|
142
|
+
cast-render assets/demo/demo.cast assets/demo/demo --gif-only --title "my-library demo" # -> `assets/demo/demo.gif`
|
|
143
|
+
cast-render assets/demo/demo.cast assets/demo/demo --mp4-only --title "my-library demo" # -> `assets/demo/demo.mp4`
|
|
144
|
+
|
|
145
|
+
# 5. Embed in README
|
|
146
|
+
# 
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 🎥 Demo
|
|
152
|
+
|
|
153
|
+
[//]: # ([](https://your-video-url.mp4))
|
|
154
|
+

|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## CLI Reference
|
|
159
|
+
|
|
160
|
+
### `cast-render`
|
|
161
|
+
|
|
162
|
+
| Flag | Default | Description |
|
|
163
|
+
|------|---------|-------------|
|
|
164
|
+
| `cast_file` | — | Path to `.cast` file |
|
|
165
|
+
| `output_base` | — | Output path without extension |
|
|
166
|
+
| `--title` | `""` | Title bar text |
|
|
167
|
+
| `--gif-only` | — | Produce GIF only |
|
|
168
|
+
| `--mp4-only` | — | Produce MP4 only |
|
|
169
|
+
| `--render-fps` | `30` | Internal PNG frame rate |
|
|
170
|
+
| `--gif-fps` | `10` | GIF output FPS |
|
|
171
|
+
| `--mp4-fps` | `30` | MP4 output FPS |
|
|
172
|
+
| `--hold` | `3.0` | Seconds to hold last frame |
|
|
173
|
+
| `--keep-frames` | — | Keep temporary PNG frames |
|
|
174
|
+
|
|
175
|
+
### `cast-init`
|
|
176
|
+
|
|
177
|
+
| Flag | Default | Description |
|
|
178
|
+
|------|---------|-------------|
|
|
179
|
+
| `--dest` | `scripts/record_demo` | Directory to scaffold into |
|
|
180
|
+
| `--force` | — | Overwrite existing files |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 🤝 Contributing
|
|
185
|
+
|
|
186
|
+
If you have a helpful pattern or improvement to suggest:
|
|
187
|
+
Fork the repo
|
|
188
|
+
Create a new branch
|
|
189
|
+
Submit a pull request
|
|
190
|
+
I welcome additions that promote clean, productive, and maintainable development.
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 📄 License
|
|
195
|
+
|
|
196
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## 🙏 Thanks
|
|
201
|
+
|
|
202
|
+
Thanks for exploring this repository! <br>
|
|
203
|
+
Happy coding!
|
|
204
|
+
|
|
205
|
+
[](https://github.com/aviz92)
|
|
206
|
+
[](https://pypi.org/user/aviz/)
|
|
207
|
+
[](https://aviz92.github.io/)
|
|
208
|
+
[](https://www.linkedin.com/in/avi-zaguri-41869b11b)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+

|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# 🎬 cast-studio
|
|
11
|
+
|
|
12
|
+
Convert [asciinema](https://asciinema.org/) `.cast` recordings into **GIF** and **MP4** — and scaffold a generic demo-recording engine for any Python project.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 📦 Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv add cast-studio
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> **System requirement:** `ffmpeg` must be installed.
|
|
23
|
+
> ```bash
|
|
24
|
+
> brew install ffmpeg # macOS
|
|
25
|
+
> apt-get install ffmpeg # Ubuntu / Debian
|
|
26
|
+
> ```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 🚀 Features
|
|
31
|
+
|
|
32
|
+
- ✅ **cast → GIF** — High-quality 256-colour GIF via ffmpeg palette pass
|
|
33
|
+
- ✅ **cast → MP4** — H.264/x264 CRF-18 MP4 ready for GitHub Releases
|
|
34
|
+
- ✅ **Catppuccin Mocha theme** — Beautiful dark terminal rendering with Pillow
|
|
35
|
+
- ✅ **Generic demo engine** — `cast-run` + `demo.cfg` for any project
|
|
36
|
+
- ✅ **Any shell command** — Record pytest runs, scripts, CLIs — not just pytest
|
|
37
|
+
- ✅ **Multi-line descriptions** — Pipe-separated description lines per run
|
|
38
|
+
- ✅ **`python-base-command` CLI** — Structured, loggable CLI with `cast` / `cast-render` / `cast-init`
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ⚙️ Configuration
|
|
43
|
+
|
|
44
|
+
No `.env` needed. All config lives in `demo.cfg`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
PROJECT="my-library"
|
|
48
|
+
SUBTITLE="A short description"
|
|
49
|
+
INSTALL_CMD="pip install my-library"
|
|
50
|
+
REPO_URL="github.com/you/my-library"
|
|
51
|
+
PYPI_URL="pypi.org/project/my-library"
|
|
52
|
+
|
|
53
|
+
PYTEST=".venv/bin/pytest"
|
|
54
|
+
TESTS="tests/"
|
|
55
|
+
|
|
56
|
+
PAUSE_INTRO=2 # seconds after intro screen
|
|
57
|
+
PAUSE_BETWEEN=2 # seconds between runs
|
|
58
|
+
PAUSE_OUTRO=3 # seconds on outro screen
|
|
59
|
+
|
|
60
|
+
define_runs() {
|
|
61
|
+
add_run "RUN 1 — feature A" "Short description." "$PYTEST $TESTS --flag"
|
|
62
|
+
add_run "RUN 2 — script" "Another feature." "python scripts/my_script.py"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> `add_run "Title" "Line 1|Line 2" "any shell command"` — use `|` for multi-line descriptions.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## 🛠️ How to Use
|
|
71
|
+
|
|
72
|
+
1. **Install** — `uv add cast-studio` (and `brew install ffmpeg asciinema`)
|
|
73
|
+
3. **Create .cfg** — customise `demo.cfg` with your project's runs
|
|
74
|
+
4. **Record** — `asciinema rec -c "cast-run demo/demo.cfg" demo.cast`
|
|
75
|
+
5. **Render** — `cast-render demo.cast assets/demo --gif-only --title "my demo"` → `assets/demo.gif`
|
|
76
|
+
6. **Embed** — add `` to your README
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🚀 Quick Start
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# 1. Install
|
|
84
|
+
uv add cast-studio
|
|
85
|
+
brew install ffmpeg asciinema # macOS
|
|
86
|
+
|
|
87
|
+
# 2. Create demo/demo.cfg — set PROJECT, SUBTITLE, INSTALL_CMD, define_runs()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`demo/demo.cfg` structure:
|
|
91
|
+
```bash
|
|
92
|
+
PROJECT="my-library"
|
|
93
|
+
SUBTITLE="A short description"
|
|
94
|
+
INSTALL_CMD="uv add my-library"
|
|
95
|
+
REPO_URL="github.com/you/my-library"
|
|
96
|
+
PYPI_URL="pypi.org/project/my-library"
|
|
97
|
+
|
|
98
|
+
define_runs() {
|
|
99
|
+
add_run "RUN 1 — feature A" "Short description." "pytest tests/ --flag"
|
|
100
|
+
add_run "RUN 2 — script" "Another feature." "python scripts/my_script.py"
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# 3. Record
|
|
106
|
+
asciinema rec -c "bash cast-run demo/demo.cfg" assets/demo/demo.cast
|
|
107
|
+
|
|
108
|
+
# 4. Render to GIF and MP4
|
|
109
|
+
cast-render assets/demo/demo.cast assets/demo/demo --gif-only --title "my-library demo" # -> `assets/demo/demo.gif`
|
|
110
|
+
cast-render assets/demo/demo.cast assets/demo/demo --mp4-only --title "my-library demo" # -> `assets/demo/demo.mp4`
|
|
111
|
+
|
|
112
|
+
# 5. Embed in README
|
|
113
|
+
# 
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 🎥 Demo
|
|
119
|
+
|
|
120
|
+
[//]: # ([](https://your-video-url.mp4))
|
|
121
|
+

|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## CLI Reference
|
|
126
|
+
|
|
127
|
+
### `cast-render`
|
|
128
|
+
|
|
129
|
+
| Flag | Default | Description |
|
|
130
|
+
|------|---------|-------------|
|
|
131
|
+
| `cast_file` | — | Path to `.cast` file |
|
|
132
|
+
| `output_base` | — | Output path without extension |
|
|
133
|
+
| `--title` | `""` | Title bar text |
|
|
134
|
+
| `--gif-only` | — | Produce GIF only |
|
|
135
|
+
| `--mp4-only` | — | Produce MP4 only |
|
|
136
|
+
| `--render-fps` | `30` | Internal PNG frame rate |
|
|
137
|
+
| `--gif-fps` | `10` | GIF output FPS |
|
|
138
|
+
| `--mp4-fps` | `30` | MP4 output FPS |
|
|
139
|
+
| `--hold` | `3.0` | Seconds to hold last frame |
|
|
140
|
+
| `--keep-frames` | — | Keep temporary PNG frames |
|
|
141
|
+
|
|
142
|
+
### `cast-init`
|
|
143
|
+
|
|
144
|
+
| Flag | Default | Description |
|
|
145
|
+
|------|---------|-------------|
|
|
146
|
+
| `--dest` | `scripts/record_demo` | Directory to scaffold into |
|
|
147
|
+
| `--force` | — | Overwrite existing files |
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 🤝 Contributing
|
|
152
|
+
|
|
153
|
+
If you have a helpful pattern or improvement to suggest:
|
|
154
|
+
Fork the repo
|
|
155
|
+
Create a new branch
|
|
156
|
+
Submit a pull request
|
|
157
|
+
I welcome additions that promote clean, productive, and maintainable development.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## 📄 License
|
|
162
|
+
|
|
163
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## 🙏 Thanks
|
|
168
|
+
|
|
169
|
+
Thanks for exploring this repository! <br>
|
|
170
|
+
Happy coding!
|
|
171
|
+
|
|
172
|
+
[](https://github.com/aviz92)
|
|
173
|
+
[](https://pypi.org/user/aviz/)
|
|
174
|
+
[](https://aviz92.github.io/)
|
|
175
|
+
[](https://www.linkedin.com/in/avi-zaguri-41869b11b)
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cast — main entry point, auto-discovers all commands.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
cast render demo.cast assets/demo
|
|
6
|
+
cast init
|
|
7
|
+
cast --help
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from python_base_command import Runner
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main():
|
|
16
|
+
Runner(commands_dir=str(Path(__file__).parent / "commands")).run()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if __name__ == "__main__":
|
|
20
|
+
main()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cast-render — convenience alias for `cast render`.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
cast-render demo.cast assets/demo
|
|
6
|
+
cast-render demo.cast assets/demo --gif-only
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
from python_base_command import CommandRegistry
|
|
12
|
+
|
|
13
|
+
from cast_studio.commands.render import Command
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
registry = CommandRegistry()
|
|
18
|
+
registry.register("render")(Command)
|
|
19
|
+
sys.argv.insert(1, "render")
|
|
20
|
+
registry.run()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
main()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Entry point for `cast-run` — alias for the run command."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from python_base_command import CommandRegistry
|
|
6
|
+
|
|
7
|
+
from cast_studio.commands.cast import Command
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main():
|
|
11
|
+
registry = CommandRegistry()
|
|
12
|
+
registry.register("run")(Command)
|
|
13
|
+
sys.argv.insert(1, "run")
|
|
14
|
+
registry.run()
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""cast-run — execute run_demo.sh with a user-supplied demo.cfg."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from python_base_command import BaseCommand, CommandError
|
|
7
|
+
|
|
8
|
+
TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Command(BaseCommand):
|
|
12
|
+
help = "Run the demo engine (run_demo.sh) with your demo.cfg"
|
|
13
|
+
version = "0.1.0"
|
|
14
|
+
|
|
15
|
+
def add_arguments(self, parser):
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"cfg",
|
|
18
|
+
nargs="?",
|
|
19
|
+
default="demo/demo.cfg",
|
|
20
|
+
help="Path to demo.cfg (default: demo/demo.cfg)",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def handle(self, **kwargs):
|
|
24
|
+
cfg = Path(kwargs["cfg"])
|
|
25
|
+
run_demo = TEMPLATES_DIR / "run_demo.sh"
|
|
26
|
+
|
|
27
|
+
if not run_demo.exists():
|
|
28
|
+
raise CommandError(f"run_demo.sh not found in package: {run_demo}")
|
|
29
|
+
|
|
30
|
+
if not cfg.exists():
|
|
31
|
+
raise CommandError(
|
|
32
|
+
f"Config file not found: {cfg}\n"
|
|
33
|
+
f" Run: cast-init --dest {cfg.parent}"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.logger.step(f"Running demo engine with config: {cfg}")
|
|
37
|
+
result = subprocess.run(["bash", str(run_demo), str(cfg)])
|
|
38
|
+
|
|
39
|
+
if result.returncode != 0:
|
|
40
|
+
raise CommandError(f"run_demo.sh exited with code {result.returncode}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from python_base_command import BaseCommand, CommandError
|
|
5
|
+
|
|
6
|
+
TEMPLATES_DIR = Path(__file__).parent.parent / "templates"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Command(BaseCommand):
|
|
10
|
+
help = "Scaffold cast-studio demo scripts into your project"
|
|
11
|
+
version = "0.1.0"
|
|
12
|
+
|
|
13
|
+
def add_arguments(self, parser):
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"--dest",
|
|
16
|
+
default="scripts/record_demo",
|
|
17
|
+
help="Destination directory (default: scripts/record_demo)",
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--force",
|
|
21
|
+
action="store_true",
|
|
22
|
+
help="Overwrite existing files",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def handle(self, **kwargs):
|
|
26
|
+
dest = Path(kwargs["dest"])
|
|
27
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
|
|
29
|
+
files = {
|
|
30
|
+
TEMPLATES_DIR / "run_demo.sh": dest / "run_demo.sh",
|
|
31
|
+
TEMPLATES_DIR / "demo.cfg.example": dest / "demo.cfg",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
created = []
|
|
35
|
+
skipped = []
|
|
36
|
+
|
|
37
|
+
for src, dst in files.items():
|
|
38
|
+
if not src.exists():
|
|
39
|
+
raise CommandError(f"Template not found: {src}")
|
|
40
|
+
if dst.exists() and not kwargs["force"]:
|
|
41
|
+
skipped.append(str(dst))
|
|
42
|
+
continue
|
|
43
|
+
shutil.copy(src, dst)
|
|
44
|
+
dst.chmod(dst.stat().st_mode | 0o755)
|
|
45
|
+
created.append(str(dst))
|
|
46
|
+
|
|
47
|
+
if created:
|
|
48
|
+
self.logger.info("Created:")
|
|
49
|
+
for f in created:
|
|
50
|
+
self.logger.info(f" {f}")
|
|
51
|
+
|
|
52
|
+
if skipped:
|
|
53
|
+
self.logger.warning("Already exists (use --force to overwrite):")
|
|
54
|
+
for f in skipped:
|
|
55
|
+
self.logger.warning(f" {f}")
|
|
56
|
+
|
|
57
|
+
if not created and not skipped:
|
|
58
|
+
raise CommandError("No template files found.")
|
|
59
|
+
|
|
60
|
+
self.logger.step(f"Next steps:")
|
|
61
|
+
self.logger.info(f" 1. Edit {dest}/demo.cfg")
|
|
62
|
+
self.logger.info(f" 2. Record:")
|
|
63
|
+
self.logger.info(f" asciinema rec -c \"bash {dest}/run_demo.sh {dest}/demo.cfg\" demo.cast")
|
|
64
|
+
self.logger.info(f" 3. Render:")
|
|
65
|
+
self.logger.info(f" cast-render demo.cast assets/demo")
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from python_base_command import BaseCommand, CommandError
|
|
5
|
+
|
|
6
|
+
from cast_studio.encoder import render_frames, encode_gif, encode_mp4
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Command(BaseCommand):
|
|
10
|
+
help = "Convert an asciinema .cast file to GIF and/or MP4"
|
|
11
|
+
version = "0.1.0"
|
|
12
|
+
|
|
13
|
+
def add_arguments(self, parser):
|
|
14
|
+
parser.add_argument(
|
|
15
|
+
"cast_file",
|
|
16
|
+
help="Path to the .cast input file",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"output_base",
|
|
20
|
+
help="Output base path, e.g. assets/demo → demo.gif + demo.mp4",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--title",
|
|
24
|
+
default="",
|
|
25
|
+
help="Window title bar text (default: empty)",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"--gif-only",
|
|
29
|
+
action="store_true",
|
|
30
|
+
help="Produce GIF only (skip MP4)",
|
|
31
|
+
)
|
|
32
|
+
parser.add_argument(
|
|
33
|
+
"--mp4-only",
|
|
34
|
+
action="store_true",
|
|
35
|
+
help="Produce MP4 only (skip GIF)",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--render-fps",
|
|
39
|
+
type=int,
|
|
40
|
+
default=30,
|
|
41
|
+
metavar="N",
|
|
42
|
+
help="Internal render FPS used for PNG frames (default: 30)",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--gif-fps",
|
|
46
|
+
type=int,
|
|
47
|
+
default=10,
|
|
48
|
+
metavar="N",
|
|
49
|
+
help="GIF output FPS (default: 10)",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--mp4-fps",
|
|
53
|
+
type=int,
|
|
54
|
+
default=30,
|
|
55
|
+
metavar="N",
|
|
56
|
+
help="MP4 output FPS (default: 30)",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--hold",
|
|
60
|
+
type=float,
|
|
61
|
+
default=3.0,
|
|
62
|
+
metavar="SEC",
|
|
63
|
+
help="Seconds to hold the last frame (default: 3.0)",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--keep-frames",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Keep temporary PNG frames after encoding (useful for debugging)",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def handle(self, **kwargs):
|
|
72
|
+
cast_file = kwargs["cast_file"]
|
|
73
|
+
output_base = kwargs["output_base"]
|
|
74
|
+
|
|
75
|
+
if not Path(cast_file).exists():
|
|
76
|
+
raise CommandError(f"Cast file not found: {cast_file}")
|
|
77
|
+
|
|
78
|
+
if kwargs["gif_only"] and kwargs["mp4_only"]:
|
|
79
|
+
raise CommandError("Cannot use --gif-only and --mp4-only together.")
|
|
80
|
+
|
|
81
|
+
Path(output_base).parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
do_gif = not kwargs["mp4_only"]
|
|
84
|
+
do_mp4 = not kwargs["gif_only"]
|
|
85
|
+
|
|
86
|
+
frames_dir = Path("/tmp/_cast_studio_frames")
|
|
87
|
+
if frames_dir.exists():
|
|
88
|
+
shutil.rmtree(frames_dir)
|
|
89
|
+
|
|
90
|
+
self.logger.step("Rendering PNG frames...")
|
|
91
|
+
render_frames(
|
|
92
|
+
cast_file=cast_file,
|
|
93
|
+
frames_dir=frames_dir,
|
|
94
|
+
title=kwargs["title"],
|
|
95
|
+
render_fps=kwargs["render_fps"],
|
|
96
|
+
hold_secs=kwargs["hold"],
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if do_gif:
|
|
100
|
+
self.logger.step("Encoding GIF...")
|
|
101
|
+
encode_gif(frames_dir, output_base + ".gif", kwargs["render_fps"], kwargs["gif_fps"])
|
|
102
|
+
|
|
103
|
+
if do_mp4:
|
|
104
|
+
self.logger.step("Encoding MP4...")
|
|
105
|
+
encode_mp4(frames_dir, output_base + ".mp4", kwargs["render_fps"], kwargs["mp4_fps"])
|
|
106
|
+
|
|
107
|
+
if not kwargs["keep_frames"]:
|
|
108
|
+
shutil.rmtree(frames_dir)
|
|
109
|
+
|
|
110
|
+
self.logger.info("Done.")
|
|
111
|
+
if do_gif:
|
|
112
|
+
self.logger.info(f" GIF → {output_base}.gif")
|
|
113
|
+
if do_mp4:
|
|
114
|
+
self.logger.info(f" MP4 → {output_base}.mp4")
|