unsaid-video 0.2.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,44 @@
1
+ name: publish
2
+
3
+ # Publishing a GitHub release uploads that version to PyPI as unsaid-video.
4
+ # PyPI trusts this workflow directly (trusted publishing), so there is no API
5
+ # token to store: PyPI's publisher settings name this repository, this file
6
+ # (publish.yml) and the `pypi` environment below.
7
+
8
+ on:
9
+ release:
10
+ types: [published]
11
+ workflow_dispatch:
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v6
19
+ with:
20
+ python-version: "3.12"
21
+ - run: uv build
22
+ - name: Test the built wheel, not the checkout
23
+ run: |
24
+ uv venv /tmp/check
25
+ uv pip install --python /tmp/check/bin/python "$(ls dist/*.whl)" pytest
26
+ rm -rf src
27
+ /tmp/check/bin/python -m pytest
28
+ - uses: actions/upload-artifact@v4
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+
33
+ publish:
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ environment: pypi
37
+ permissions:
38
+ id-token: write
39
+ steps:
40
+ - uses: actions/download-artifact@v4
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,21 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ubuntu-latest, macos-latest, windows-latest]
15
+ python: ["3.10", "3.13"]
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ - uses: astral-sh/setup-uv@v6
19
+ with:
20
+ python-version: ${{ matrix.python }}
21
+ - run: uv run --extra dev pytest
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ .pytest_cache/
6
+ *.unsaid.json
7
+ samples/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 unsaid contributors
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,155 @@
1
+ Metadata-Version: 2.5
2
+ Name: unsaid-video
3
+ Version: 0.2.0
4
+ Summary: Edit video by editing text. An open-source, local-first Descript alternative.
5
+ Project-URL: Homepage, https://github.com/Necromancervbh/unsaid
6
+ Project-URL: Issues, https://github.com/Necromancervbh/unsaid/issues
7
+ Project-URL: Changelog, https://github.com/Necromancervbh/unsaid/releases
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: descript,editing,ffmpeg,podcast,transcript,video,whisper
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
17
+ Classifier: Topic :: Multimedia :: Video :: Non-Linear Editor
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: av>=12
20
+ Requires-Dist: faster-whisper>=1.1
21
+ Requires-Dist: imageio-ffmpeg>=0.5
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # unsaid
27
+
28
+ [![test](https://github.com/Necromancervbh/unsaid/actions/workflows/test.yml/badge.svg)](https://github.com/Necromancervbh/unsaid/actions/workflows/test.yml)
29
+
30
+ **Edit video by editing text.** unsaid is an open-source, local-first Descript alternative.
31
+
32
+ unsaid transcribes your video on your own machine. You delete words from the transcript and they're gone from the video. Filler words go in one command. No account, no API key, no upload.
33
+
34
+ ![Deleting a sentence from talk.unsaid.txt and running unsaid cut: the sentence and the filler words disappear from the video, which goes from 30.7s to 21.3s](https://raw.githubusercontent.com/Necromancervbh/unsaid/main/docs/demo.gif)
35
+
36
+ ```bash
37
+ unsaid edit talk.mp4 # edit the transcript in your browser, then export
38
+ unsaid cut talk.mp4 # or just remove "um", "uh", "hmm" -> talk.unsaid.mp4
39
+ unsaid cut talk.mp4 --max-pause 0.6 # ...and tighten long pauses
40
+ ```
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ uv tool install unsaid-video # or: pipx install unsaid-video
46
+ ```
47
+
48
+ The package is `unsaid-video` on PyPI; the command it installs is `unsaid`. For the latest from GitHub: `uv tool install git+https://github.com/Necromancervbh/unsaid`.
49
+
50
+ That's all you need. ffmpeg comes bundled through `imageio-ffmpeg`, and if an `ffmpeg` is on your `PATH` it's used instead. The Whisper model downloads on first run. The default `small` model is about 480 MB, and after that unsaid works offline.
51
+
52
+ On a laptop CPU, `small` transcribes about 3-4x faster than real time: a 4-minute interview takes about a minute. Rendering the cut takes seconds. An NVIDIA GPU with CUDA is used automatically when available.
53
+
54
+ ## Usage
55
+
56
+ ### Edit in your browser
57
+
58
+ ```bash
59
+ unsaid edit talk.mp4
60
+ ```
61
+
62
+ This opens the transcript next to the video in your browser. Select words and press <kbd>Delete</kbd> to cut them, or press it again to bring them back. Filler words start out cut. Playback skips the cuts, so you hear the edit as you make it, and **Export** renders `talk.unsaid.mp4`.
63
+
64
+ Everything stays on your machine: the editor is a small server on `127.0.0.1` that only serves this one file. Edits save as you go to `talk.unsaid.edit.json`, so you can close the tab and pick up later. Files a browser can't play, like `.mkv` or `.avi`, get a small preview copy, and the export still renders from the original. Use `--port` to pick the port and `--no-browser` to just print the URL.
65
+
66
+ ### Remove filler words
67
+
68
+ ```bash
69
+ unsaid cut talk.mp4 --dry-run # preview: prints the transcript with [um] cuts marked
70
+ unsaid cut talk.mp4 # render talk.unsaid.mp4
71
+ unsaid cut talk.mp4 --aggressive # also ", like," / ", you know," / ", I mean," when set off by commas
72
+ unsaid cut talk.mp4 --filler okay --filler "sort of" # your own verbal tics
73
+ ```
74
+
75
+ ### Edit the script
76
+
77
+ ```bash
78
+ unsaid transcribe talk.mp4 # writes talk.unsaid.txt, one sentence per line
79
+ # open talk.unsaid.txt, delete the sentences/words you don't want, save
80
+ unsaid cut talk.mp4 --script talk.unsaid.txt
81
+ ```
82
+
83
+ Deleted words are cut from the video. Fixing a typo or capitalization in the script doesn't cut anything: unsaid only cuts deletions, and it tells you how many changed words it left alone.
84
+
85
+ ### Options
86
+
87
+ | Flag | What it does |
88
+ |---|---|
89
+ | `--model` | Whisper model: `tiny`, `base`, `small` (default), `medium`, `large-v3`, `turbo`, or a local CTranslate2 model path |
90
+ | `--language en` | Skip language auto-detection |
91
+ | `--device cpu\|cuda` | `auto` (the default) uses CUDA when it works and falls back to CPU |
92
+ | `--max-pause S` | Shorten every pause longer than `S` seconds |
93
+ | `--no-fillers` | Only apply `--script` / `--max-pause` |
94
+ | `-o out.mp4` | Output path (default `<input>.unsaid.mp4`, or `.m4a` for audio-only input) |
95
+ | `--crf`, `--preset` | x264 quality and speed |
96
+
97
+ Transcripts are cached next to the input as `<input>.unsaid.json`, so re-cutting with different options is instant. Run `unsaid transcribe --force` to redo one.
98
+
99
+ ### Let a coding agent do the edit
100
+
101
+ [`skills/unsaid/SKILL.md`](skills/unsaid/SKILL.md) teaches an agent the unsaid workflow: transcribe, plan the cut, delete lines, dry-run, render. For Claude Code, copy the folder into your skills:
102
+
103
+ ```bash
104
+ git clone https://github.com/Necromancervbh/unsaid && cp -r unsaid/skills/unsaid ~/.claude/skills/
105
+ ```
106
+
107
+ Then ask: *"cut talk.mp4 down to a 60-second highlight about pricing"*. Other agents (Codex, Cursor, ...) can be pointed at the same file.
108
+
109
+ ## How it works
110
+
111
+ 1. **Transcribe.** [faster-whisper](https://github.com/SYSTRAN/faster-whisper) runs with word timestamps. Whisper normally "cleans up" disfluencies, so unsaid primes every 30-second window with a prompt full of fillers, which makes it keep them verbatim. Use `--no-verbatim` to turn this off. A voice-activity detector then checks for speech Whisper skipped. Whisper occasionally does, and that speech would otherwise look like a pause.
112
+ 2. **Plan cuts.** Cuts only ever land in the silence *between* kept words, and never inside speech Whisper skipped. When a word is removed, the pause left behind is the *shorter* of the two pauses around it, so the edit keeps the speaker's rhythm instead of jamming words together. Whisper's word timestamps can be off by a tenth of a second, so each cut edge then moves into the silence the audio actually shows next to the kept word, which keeps the first and last sounds of words intact.
113
+ 3. **Render.** ffmpeg trims and concatenates the kept ranges in one pass. Cut points snap to the video's frame grid, so audio and video stay exactly in sync however many cuts there are, and each audio cut gets an 8 ms fade to avoid clicks.
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ uv venv && uv pip install -e ".[dev]"
119
+ .venv/Scripts/python -m pytest # Windows; .venv/bin/python on macOS/Linux
120
+ ```
121
+
122
+ The tests render real video through ffmpeg but don't need a Whisper model.
123
+
124
+ The GIF at the top is drawn by `scripts/make_demo_gif.py` from a real `unsaid cut` run: its transcript, cut plan, waveform and output.
125
+
126
+ ## Roadmap
127
+
128
+ - [x] Browser editor: `unsaid edit talk.mp4` opens the transcript, and striking text cuts the video live
129
+ - [x] Agent skill: `SKILL.md`, so Claude Code, Codex or Cursor can make the edit ("cut this to a 60-second highlight")
130
+ - [ ] MCP server, for agents that prefer tools to a CLI
131
+ - [ ] Captions: SRT/VTT export retimed to the edit, plus animated burned-in captions for Shorts and Reels
132
+ - [ ] FCPXML/EDL export for Resolve, Premiere and Final Cut
133
+ - [ ] Stutter and repeated-word removal ("I I think")
134
+ - [ ] Overdub with a local voice clone, and "studio sound" cleanup
135
+
136
+ ## Tested on real speech
137
+
138
+ Two unscripted English interviews (CC BY, from Wikimedia Commons), with default settings on a laptop CPU:
139
+
140
+ | Clip | Transcribe | Fillers cut | Result | Render |
141
+ |---|---|---|---|---|
142
+ | 4:20 press Q&A, background noise | 85 s | 23 | 4:20.3 → 4:03.2 | 8 s |
143
+ | 3:55 one-on-one interview | 62 s | 15 | 3:54.9 → 3:41.8 | 10 s |
144
+
145
+ To check the cuts, the output files were transcribed again and compared with the words unsaid meant to keep. They matched at 98.6% and 99.2%. Every difference was Whisper hearing a name or a word differently, none of them within two seconds of a cut. Two fillers that Whisper missed the first time stayed in. Audio and video stayed in sync to the frame through every cut.
146
+
147
+ ## Known limitations
148
+
149
+ - Filler detection is only as good as Whisper's transcript. It's tuned for English: other languages are transcribed, but Whisper may clean up their fillers.
150
+ - Whisper can mishear words, or make one up. unsaid already drops the short, low-confidence segment Whisper tends to tack onto the end of a recording ("Good.", "Bye."). Delete any other made-up words in the script.
151
+ - Output is always re-encoded (H.264 + AAC) at the source's frame rate.
152
+
153
+ ## License
154
+
155
+ MIT
@@ -0,0 +1,130 @@
1
+ # unsaid
2
+
3
+ [![test](https://github.com/Necromancervbh/unsaid/actions/workflows/test.yml/badge.svg)](https://github.com/Necromancervbh/unsaid/actions/workflows/test.yml)
4
+
5
+ **Edit video by editing text.** unsaid is an open-source, local-first Descript alternative.
6
+
7
+ unsaid transcribes your video on your own machine. You delete words from the transcript and they're gone from the video. Filler words go in one command. No account, no API key, no upload.
8
+
9
+ ![Deleting a sentence from talk.unsaid.txt and running unsaid cut: the sentence and the filler words disappear from the video, which goes from 30.7s to 21.3s](https://raw.githubusercontent.com/Necromancervbh/unsaid/main/docs/demo.gif)
10
+
11
+ ```bash
12
+ unsaid edit talk.mp4 # edit the transcript in your browser, then export
13
+ unsaid cut talk.mp4 # or just remove "um", "uh", "hmm" -> talk.unsaid.mp4
14
+ unsaid cut talk.mp4 --max-pause 0.6 # ...and tighten long pauses
15
+ ```
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ uv tool install unsaid-video # or: pipx install unsaid-video
21
+ ```
22
+
23
+ The package is `unsaid-video` on PyPI; the command it installs is `unsaid`. For the latest from GitHub: `uv tool install git+https://github.com/Necromancervbh/unsaid`.
24
+
25
+ That's all you need. ffmpeg comes bundled through `imageio-ffmpeg`, and if an `ffmpeg` is on your `PATH` it's used instead. The Whisper model downloads on first run. The default `small` model is about 480 MB, and after that unsaid works offline.
26
+
27
+ On a laptop CPU, `small` transcribes about 3-4x faster than real time: a 4-minute interview takes about a minute. Rendering the cut takes seconds. An NVIDIA GPU with CUDA is used automatically when available.
28
+
29
+ ## Usage
30
+
31
+ ### Edit in your browser
32
+
33
+ ```bash
34
+ unsaid edit talk.mp4
35
+ ```
36
+
37
+ This opens the transcript next to the video in your browser. Select words and press <kbd>Delete</kbd> to cut them, or press it again to bring them back. Filler words start out cut. Playback skips the cuts, so you hear the edit as you make it, and **Export** renders `talk.unsaid.mp4`.
38
+
39
+ Everything stays on your machine: the editor is a small server on `127.0.0.1` that only serves this one file. Edits save as you go to `talk.unsaid.edit.json`, so you can close the tab and pick up later. Files a browser can't play, like `.mkv` or `.avi`, get a small preview copy, and the export still renders from the original. Use `--port` to pick the port and `--no-browser` to just print the URL.
40
+
41
+ ### Remove filler words
42
+
43
+ ```bash
44
+ unsaid cut talk.mp4 --dry-run # preview: prints the transcript with [um] cuts marked
45
+ unsaid cut talk.mp4 # render talk.unsaid.mp4
46
+ unsaid cut talk.mp4 --aggressive # also ", like," / ", you know," / ", I mean," when set off by commas
47
+ unsaid cut talk.mp4 --filler okay --filler "sort of" # your own verbal tics
48
+ ```
49
+
50
+ ### Edit the script
51
+
52
+ ```bash
53
+ unsaid transcribe talk.mp4 # writes talk.unsaid.txt, one sentence per line
54
+ # open talk.unsaid.txt, delete the sentences/words you don't want, save
55
+ unsaid cut talk.mp4 --script talk.unsaid.txt
56
+ ```
57
+
58
+ Deleted words are cut from the video. Fixing a typo or capitalization in the script doesn't cut anything: unsaid only cuts deletions, and it tells you how many changed words it left alone.
59
+
60
+ ### Options
61
+
62
+ | Flag | What it does |
63
+ |---|---|
64
+ | `--model` | Whisper model: `tiny`, `base`, `small` (default), `medium`, `large-v3`, `turbo`, or a local CTranslate2 model path |
65
+ | `--language en` | Skip language auto-detection |
66
+ | `--device cpu\|cuda` | `auto` (the default) uses CUDA when it works and falls back to CPU |
67
+ | `--max-pause S` | Shorten every pause longer than `S` seconds |
68
+ | `--no-fillers` | Only apply `--script` / `--max-pause` |
69
+ | `-o out.mp4` | Output path (default `<input>.unsaid.mp4`, or `.m4a` for audio-only input) |
70
+ | `--crf`, `--preset` | x264 quality and speed |
71
+
72
+ Transcripts are cached next to the input as `<input>.unsaid.json`, so re-cutting with different options is instant. Run `unsaid transcribe --force` to redo one.
73
+
74
+ ### Let a coding agent do the edit
75
+
76
+ [`skills/unsaid/SKILL.md`](skills/unsaid/SKILL.md) teaches an agent the unsaid workflow: transcribe, plan the cut, delete lines, dry-run, render. For Claude Code, copy the folder into your skills:
77
+
78
+ ```bash
79
+ git clone https://github.com/Necromancervbh/unsaid && cp -r unsaid/skills/unsaid ~/.claude/skills/
80
+ ```
81
+
82
+ Then ask: *"cut talk.mp4 down to a 60-second highlight about pricing"*. Other agents (Codex, Cursor, ...) can be pointed at the same file.
83
+
84
+ ## How it works
85
+
86
+ 1. **Transcribe.** [faster-whisper](https://github.com/SYSTRAN/faster-whisper) runs with word timestamps. Whisper normally "cleans up" disfluencies, so unsaid primes every 30-second window with a prompt full of fillers, which makes it keep them verbatim. Use `--no-verbatim` to turn this off. A voice-activity detector then checks for speech Whisper skipped. Whisper occasionally does, and that speech would otherwise look like a pause.
87
+ 2. **Plan cuts.** Cuts only ever land in the silence *between* kept words, and never inside speech Whisper skipped. When a word is removed, the pause left behind is the *shorter* of the two pauses around it, so the edit keeps the speaker's rhythm instead of jamming words together. Whisper's word timestamps can be off by a tenth of a second, so each cut edge then moves into the silence the audio actually shows next to the kept word, which keeps the first and last sounds of words intact.
88
+ 3. **Render.** ffmpeg trims and concatenates the kept ranges in one pass. Cut points snap to the video's frame grid, so audio and video stay exactly in sync however many cuts there are, and each audio cut gets an 8 ms fade to avoid clicks.
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ uv venv && uv pip install -e ".[dev]"
94
+ .venv/Scripts/python -m pytest # Windows; .venv/bin/python on macOS/Linux
95
+ ```
96
+
97
+ The tests render real video through ffmpeg but don't need a Whisper model.
98
+
99
+ The GIF at the top is drawn by `scripts/make_demo_gif.py` from a real `unsaid cut` run: its transcript, cut plan, waveform and output.
100
+
101
+ ## Roadmap
102
+
103
+ - [x] Browser editor: `unsaid edit talk.mp4` opens the transcript, and striking text cuts the video live
104
+ - [x] Agent skill: `SKILL.md`, so Claude Code, Codex or Cursor can make the edit ("cut this to a 60-second highlight")
105
+ - [ ] MCP server, for agents that prefer tools to a CLI
106
+ - [ ] Captions: SRT/VTT export retimed to the edit, plus animated burned-in captions for Shorts and Reels
107
+ - [ ] FCPXML/EDL export for Resolve, Premiere and Final Cut
108
+ - [ ] Stutter and repeated-word removal ("I I think")
109
+ - [ ] Overdub with a local voice clone, and "studio sound" cleanup
110
+
111
+ ## Tested on real speech
112
+
113
+ Two unscripted English interviews (CC BY, from Wikimedia Commons), with default settings on a laptop CPU:
114
+
115
+ | Clip | Transcribe | Fillers cut | Result | Render |
116
+ |---|---|---|---|---|
117
+ | 4:20 press Q&A, background noise | 85 s | 23 | 4:20.3 → 4:03.2 | 8 s |
118
+ | 3:55 one-on-one interview | 62 s | 15 | 3:54.9 → 3:41.8 | 10 s |
119
+
120
+ To check the cuts, the output files were transcribed again and compared with the words unsaid meant to keep. They matched at 98.6% and 99.2%. Every difference was Whisper hearing a name or a word differently, none of them within two seconds of a cut. Two fillers that Whisper missed the first time stayed in. Audio and video stayed in sync to the frame through every cut.
121
+
122
+ ## Known limitations
123
+
124
+ - Filler detection is only as good as Whisper's transcript. It's tuned for English: other languages are transcribed, but Whisper may clean up their fillers.
125
+ - Whisper can mishear words, or make one up. unsaid already drops the short, low-confidence segment Whisper tends to tack onto the end of a recording ("Good.", "Bye."). Delete any other made-up words in the script.
126
+ - Output is always re-encoded (H.264 + AAC) at the source's frame rate.
127
+
128
+ ## License
129
+
130
+ MIT
@@ -0,0 +1,48 @@
1
+ [project]
2
+ # "unsaid" is taken on PyPI by an unrelated project. The command and the
3
+ # import name are still `unsaid`.
4
+ name = "unsaid-video"
5
+ version = "0.2.0"
6
+ description = "Edit video by editing text. An open-source, local-first Descript alternative."
7
+ readme = "README.md"
8
+ license = "MIT"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "av>=12",
12
+ "faster-whisper>=1.1",
13
+ "imageio-ffmpeg>=0.5",
14
+ ]
15
+ keywords = ["video", "editing", "transcript", "whisper", "descript", "ffmpeg", "podcast"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Environment :: Console",
19
+ "Intended Audience :: End Users/Desktop",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Topic :: Multimedia :: Video :: Non-Linear Editor",
23
+ "Topic :: Multimedia :: Sound/Audio :: Speech",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/Necromancervbh/unsaid"
28
+ Issues = "https://github.com/Necromancervbh/unsaid/issues"
29
+ Changelog = "https://github.com/Necromancervbh/unsaid/releases"
30
+
31
+ [project.optional-dependencies]
32
+ dev = ["pytest>=8"]
33
+
34
+ [project.scripts]
35
+ unsaid = "unsaid.cli:main"
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/unsaid"]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ exclude = ["docs/demo.gif"]
46
+
47
+ [tool.pytest.ini_options]
48
+ testpaths = ["tests"]