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.
@@ -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,4 @@
1
+ pyproject.toml
2
+ include requirements.txt
3
+ include README.md
4
+ include LICENSE
@@ -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
+ ![PyPI version](https://img.shields.io/pypi/v/cast-studio)
35
+ ![Python](https://img.shields.io/badge/python->=3.9-blue)
36
+ ![Development Status](https://img.shields.io/badge/status-alpha-orange)
37
+ ![Maintenance](https://img.shields.io/maintenance/yes/2026)
38
+ ![PyPI](https://img.shields.io/pypi/dm/cast-studio)
39
+ ![License](https://img.shields.io/pypi/l/cast-studio)
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 `![demo](assets/demo.gif)` 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
+ # ![demo](assets/demo.gif)
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 🎥 Demo
152
+
153
+ [//]: # ([![Watch demo](assets/demo/demo.gif)](https://your-video-url.mp4))
154
+ ![Watch demo](assets/demo/demo.gif)
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
+ [![GitHub](https://img.shields.io/badge/GitHub-aviz92-181717?logo=github)](https://github.com/aviz92)
206
+ &nbsp; [![PyPI](https://img.shields.io/badge/PyPI-aviz-3775A9?logo=pypi)](https://pypi.org/user/aviz/)
207
+ &nbsp; [![Blog](https://img.shields.io/badge/Blog-aviz92.github.io-0066CC?logo=googlechrome)](https://aviz92.github.io/)
208
+ &nbsp; [![LinkedIn](https://img.shields.io/badge/LinkedIn-avi--zaguri-0A66C2?logo=linkedin)](https://www.linkedin.com/in/avi-zaguri-41869b11b)
@@ -0,0 +1,175 @@
1
+ ![PyPI version](https://img.shields.io/pypi/v/cast-studio)
2
+ ![Python](https://img.shields.io/badge/python->=3.9-blue)
3
+ ![Development Status](https://img.shields.io/badge/status-alpha-orange)
4
+ ![Maintenance](https://img.shields.io/maintenance/yes/2026)
5
+ ![PyPI](https://img.shields.io/pypi/dm/cast-studio)
6
+ ![License](https://img.shields.io/pypi/l/cast-studio)
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 `![demo](assets/demo.gif)` 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
+ # ![demo](assets/demo.gif)
114
+ ```
115
+
116
+ ---
117
+
118
+ ## 🎥 Demo
119
+
120
+ [//]: # ([![Watch demo]&#40;assets/demo/demo.gif&#41;]&#40;https://your-video-url.mp4&#41;)
121
+ ![Watch demo](assets/demo/demo.gif)
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
+ [![GitHub](https://img.shields.io/badge/GitHub-aviz92-181717?logo=github)](https://github.com/aviz92)
173
+ &nbsp; [![PyPI](https://img.shields.io/badge/PyPI-aviz-3775A9?logo=pypi)](https://pypi.org/user/aviz/)
174
+ &nbsp; [![Blog](https://img.shields.io/badge/Blog-aviz92.github.io-0066CC?logo=googlechrome)](https://aviz92.github.io/)
175
+ &nbsp; [![LinkedIn](https://img.shields.io/badge/LinkedIn-avi--zaguri-0A66C2?logo=linkedin)](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")