@barbapapazes/video-toolkit 0.19.0 → 0.20.1
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.
- package/README.md +41 -146
- package/dist/cli.mjs +1 -8
- package/dist/index.d.mts +102 -2
- package/dist/index.mjs +1 -1
- package/dist/modules-BG4hzM3o.mjs +8 -0
- package/package.json +8 -7
- package/dist/config-Cuh3n7mj.mjs +0 -1
package/README.md
CHANGED
|
@@ -2,40 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
A simple and efficient CLI toolkit for video automation. It automates the process of extracting audio, generating transcriptions using OpenAI Whisper, and creating video thumbnails with text overlays.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- **Audio Extraction**: Automatically extracts audio from video files using `ffmpeg`.
|
|
8
|
-
- **AI Transcription**: Generates high-quality SRT subtitles using OpenAI's `whisper-1` model (defaulted to French).
|
|
9
|
-
- **Thumbnail Generation**:
|
|
10
|
-
- Extracts 5 frames from the video (Start, 25%, 50%, 75%, and End).
|
|
11
|
-
- Overlays custom text using SVG templates with `sharp` for high-quality rendering.
|
|
12
|
-
- Supports multiple SVG templates - choose different templates for different video series.
|
|
13
|
-
- Generates 5 distinct PNG thumbnails for you to choose from.
|
|
14
|
-
- **Template Management**: Easily add and manage SVG templates for different use cases.
|
|
15
|
-
- **Automatic Cleanup**: Removes temporary audio and image files after processing.
|
|
16
|
-
- **Global Configuration**: Use a global configuration file to set up your preferences once and use the tool from anywhere.
|
|
17
|
-
|
|
18
|
-
## Prerequisites
|
|
19
|
-
|
|
20
|
-
- [Node.js](https://nodejs.org/) (v24 or later required)
|
|
21
|
-
- [ffmpeg](https://ffmpeg.org/) installed and available in your PATH
|
|
22
|
-
- [OpenAI API Key](https://platform.openai.com/)
|
|
23
|
-
- macOS (this tool is designed for macOS only)
|
|
24
|
-
|
|
25
|
-
### Font Configuration (macOS)
|
|
5
|
+
## Installation
|
|
26
6
|
|
|
27
|
-
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install @barbapapazes/video-toolkit
|
|
9
|
+
```
|
|
28
10
|
|
|
29
|
-
|
|
30
|
-
2. System fonts are automatically available for use
|
|
31
|
-
3. Custom fonts can be placed in the templates directory or installed system-wide
|
|
11
|
+
## CLI help
|
|
32
12
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
## Installation
|
|
13
|
+
Use the built-in help to discover the available commands and subcommands:
|
|
36
14
|
|
|
37
15
|
```bash
|
|
38
|
-
|
|
16
|
+
video-toolkit --help
|
|
17
|
+
video-toolkit <command> --help
|
|
39
18
|
```
|
|
40
19
|
|
|
41
20
|
## Configuration
|
|
@@ -67,77 +46,69 @@ export default defineConfig({
|
|
|
67
46
|
})
|
|
68
47
|
```
|
|
69
48
|
|
|
70
|
-
|
|
49
|
+
For full configuration options, refer to the [config.ts](./src/config.ts) file.
|
|
50
|
+
|
|
51
|
+
### Font Configuration (macOS)
|
|
52
|
+
|
|
53
|
+
The toolkit uses `fontconfig` for font discovery when rendering SVG text. On macOS systems using Homebrew:
|
|
71
54
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
| `templatesDir` | string | `undefined` | Directory where SVG templates are stored. Can be absolute or relative to current working directory. If not set, uses `~/.config/video-toolkit/templates/`. |
|
|
55
|
+
1. The toolkit automatically sets `PANGOCAIRO_BACKEND=fontconfig` for proper font rendering
|
|
56
|
+
2. System fonts are automatically available for use
|
|
57
|
+
3. Custom fonts can be placed in the templates directory or installed system-wide
|
|
58
|
+
|
|
59
|
+
If you encounter font-related issues, ensure fontconfig is properly configured on your system.
|
|
78
60
|
|
|
79
|
-
##
|
|
61
|
+
## Commands
|
|
80
62
|
|
|
81
|
-
|
|
63
|
+
### `video process`
|
|
82
64
|
|
|
83
|
-
|
|
65
|
+
Process a video file and generate its transcription.
|
|
84
66
|
|
|
85
67
|
```bash
|
|
86
|
-
video-toolkit
|
|
68
|
+
video-toolkit video process --video ./content/2026/05/24/demo.mp4
|
|
87
69
|
```
|
|
88
70
|
|
|
89
|
-
|
|
71
|
+
Variant with thumbnail generation:
|
|
90
72
|
|
|
91
|
-
|
|
73
|
+
```bash
|
|
74
|
+
video-toolkit video process --video ./content/2026/05/24/demo.mp4 --thumbnail-text "Build once, ship twice" --template series-x
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
You can also use the root command:
|
|
92
78
|
|
|
93
79
|
```bash
|
|
94
|
-
video-toolkit
|
|
80
|
+
video-toolkit --video ./content/2026/05/24/demo.mp4
|
|
95
81
|
```
|
|
96
82
|
|
|
97
|
-
###
|
|
83
|
+
### `video extract-audio`
|
|
98
84
|
|
|
99
|
-
|
|
85
|
+
Extract the audio track from a video file.
|
|
100
86
|
|
|
101
87
|
```bash
|
|
102
|
-
video-toolkit
|
|
88
|
+
video-toolkit video extract-audio ./content/2026/05/24/demo.mp4
|
|
103
89
|
```
|
|
104
90
|
|
|
105
|
-
###
|
|
91
|
+
### `video generate-thumbnail`
|
|
106
92
|
|
|
107
|
-
|
|
93
|
+
Generate thumbnails for a video file.
|
|
108
94
|
|
|
109
95
|
```bash
|
|
110
|
-
video-toolkit
|
|
96
|
+
video-toolkit video generate-thumbnail --video ./content/2026/05/24/demo.mp4 --thumbnail-text "Fresh thumbnail" --template series-x
|
|
111
97
|
```
|
|
112
98
|
|
|
113
|
-
|
|
99
|
+
### `template add`
|
|
114
100
|
|
|
115
|
-
|
|
101
|
+
Add an SVG template to the templates directory.
|
|
116
102
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
- If you provide text, you'll be asked to select a template.
|
|
121
|
-
- 5 thumbnails will be generated with that text using the selected template.
|
|
122
|
-
- If you leave it empty, thumbnail generation will be skipped.
|
|
123
|
-
4. **Results**: SRT subtitles and PNG thumbnails are saved in the same directory as the video.
|
|
103
|
+
```bash
|
|
104
|
+
video-toolkit template add ./templates/series-x.svg --name series-x
|
|
105
|
+
```
|
|
124
106
|
|
|
125
107
|
## SVG Templates
|
|
126
108
|
|
|
127
109
|
The toolkit uses SVG templates to render text overlays on thumbnails using the `sharp` library.
|
|
128
110
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
Templates are stored in `~/.config/video-toolkit/templates/`. Each template is a separate SVG file.
|
|
132
|
-
|
|
133
|
-
### Template Naming Convention
|
|
134
|
-
|
|
135
|
-
Template names should use lowercase letters, numbers, and hyphens only:
|
|
136
|
-
- ✅ `series-x.svg`
|
|
137
|
-
- ✅ `youtube-intro.svg`
|
|
138
|
-
- ✅ `podcast-2024.svg`
|
|
139
|
-
- ❌ `Series X.svg`
|
|
140
|
-
- ❌ `MyTemplate.svg`
|
|
111
|
+
Templates are stored in `~/.config/video-toolkit/templates/`. Each template is a separate SVG file and should be named using lowercase letters, numbers, and hyphens only.
|
|
141
112
|
|
|
142
113
|
### Creating a Template
|
|
143
114
|
|
|
@@ -163,82 +134,6 @@ The `{{text}}` placeholder will be replaced with the actual text you provide dur
|
|
|
163
134
|
|
|
164
135
|
The toolkit uses **fontconfig** for font discovery, following Sharp's recommended approach. Fonts are stored as actual font files that fontconfig can discover, rather than being embedded in SVGs.
|
|
165
136
|
|
|
166
|
-
#### How It Works
|
|
167
|
-
|
|
168
|
-
When you first use a template, the toolkit:
|
|
169
|
-
1. Checks if fonts referenced in the SVG are available
|
|
170
|
-
2. Looks for font files in the template directory or system fonts
|
|
171
|
-
3. If not found, downloads fonts from Google Fonts
|
|
172
|
-
4. Saves downloaded fonts to `~/.config/video-toolkit/fonts/`
|
|
173
|
-
5. Configures fontconfig to discover fonts from this directory
|
|
174
|
-
6. All future uses automatically find and use these fonts
|
|
175
|
-
|
|
176
|
-
#### Option 1: Local Font Files
|
|
177
|
-
|
|
178
|
-
Place font files (`.ttf`, `.woff`, or `.woff2`) in the same directory as your SVG template:
|
|
179
|
-
|
|
180
|
-
```
|
|
181
|
-
~/.config/video-toolkit/templates/
|
|
182
|
-
├── my-template.svg
|
|
183
|
-
├── SofiaSans-Light-300.ttf
|
|
184
|
-
├── SofiaSans-Medium-500.ttf
|
|
185
|
-
└── SofiaSans-Bold-700.ttf
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
**Font file naming conventions:**
|
|
189
|
-
- `FontName.ttf` → default weight (400)
|
|
190
|
-
- `FontName-300.ttf` or `FontName-Light.ttf` → weight 300
|
|
191
|
-
- `FontName-500.ttf` or `FontName-Medium.ttf` → weight 500
|
|
192
|
-
- `FontName-700.ttf` or `FontName-Bold.ttf` → weight 700
|
|
193
|
-
|
|
194
|
-
#### Option 2: Google Fonts (Automatic)
|
|
195
|
-
|
|
196
|
-
If fonts aren't found locally, they're automatically downloaded from [Google Fonts](https://fonts.google.com/):
|
|
197
|
-
|
|
198
|
-
```xml
|
|
199
|
-
<text font-family="Sofia Sans" font-size="128" font-weight="500">
|
|
200
|
-
{{text}}
|
|
201
|
-
</text>
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
**What happens:**
|
|
205
|
-
1. First use: Downloads "Sofia Sans" weight 500 from Google Fonts
|
|
206
|
-
2. Saves it to `~/.config/video-toolkit/fonts/SofiaSans-500.woff2`
|
|
207
|
-
3. Configures fontconfig to discover this directory
|
|
208
|
-
4. Subsequent uses: fontconfig finds the font instantly (no download)
|
|
209
|
-
|
|
210
|
-
**Benefits:**
|
|
211
|
-
- ✅ **One-time download** - Fonts downloaded once, reused forever
|
|
212
|
-
- ✅ **Proper font rendering** - Uses fontconfig (Sharp's recommended approach)
|
|
213
|
-
- ✅ **System-wide availability** - Downloaded fonts work across all templates
|
|
214
|
-
- ✅ **Works offline** - After first download, no internet needed
|
|
215
|
-
- ✅ **1400+ font families** from Google Fonts
|
|
216
|
-
- ✅ **Automatic fontconfig setup** - Creates configuration automatically
|
|
217
|
-
|
|
218
|
-
**Technical Note:**
|
|
219
|
-
The toolkit creates a fontconfig configuration at `~/.config/video-toolkit/fonts.conf` that points to the fonts directory. On macOS, it sets `PANGOCAIRO_BACKEND=fontconfig` to ensure proper font discovery. This follows Sharp's documentation for optimal font rendering with SVGs.
|
|
220
|
-
|
|
221
|
-
### Managing Multiple Templates
|
|
222
|
-
|
|
223
|
-
You can have multiple templates for different purposes:
|
|
224
|
-
- `series-x.svg` - For your X video series
|
|
225
|
-
- `series-y.svg` - For your Y video series
|
|
226
|
-
- `youtube-intro.svg` - For YouTube introductions
|
|
227
|
-
- `podcast.svg` - For podcast episodes
|
|
228
|
-
|
|
229
|
-
Use the `add-template` command to easily add new templates, and during thumbnail generation, you'll be prompted to select which template to use.
|
|
230
|
-
|
|
231
|
-
## Output Files
|
|
232
|
-
|
|
233
|
-
For an input file `my-video.mp4`, the tool generates:
|
|
234
|
-
- `my-video_audio.mp3`: The extracted audio (when using `extract-audio`).
|
|
235
|
-
- `my-video.srt`: The generated subtitles.
|
|
236
|
-
- `my-video_thumbnail_first.png`: Thumbnail from the first frame.
|
|
237
|
-
- `my-video_thumbnail_25.png`: Thumbnail at 25% duration.
|
|
238
|
-
- `my-video_thumbnail_50.png`: Thumbnail at 50% duration.
|
|
239
|
-
- `my-video_thumbnail_75.png`: Thumbnail at 75% duration.
|
|
240
|
-
- `my-video_thumbnail_last.png`: Thumbnail from the last frame.
|
|
241
|
-
|
|
242
137
|
## License
|
|
243
138
|
|
|
244
139
|
MIT
|
package/dist/cli.mjs
CHANGED
|
@@ -1,9 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{t as
|
|
3
|
-
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
|
4
|
-
<fontconfig>
|
|
5
|
-
<dir>${e}</dir>
|
|
6
|
-
<cachedir>${C(n,`fontcache`)}</cachedir>
|
|
7
|
-
</fontconfig>`,`utf-8`),t.env.FONTCONFIG_PATH=n,t.platform===`darwin`&&(t.env.PANGOCAIRO_BACKEND=`fontconfig`)}async function ie(e,t){try{let n=await D(`https://fonts.googleapis.com/css2?family=${e.replace(/\s+/g,`+`)}:wght@${t}&display=swap`),r=n.match(/url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)/),i=n.match(/format\('([^']+)'\)/);if(!r||!i)return null;let a=r[1],o=i[1],s=await D(a,{responseType:`arrayBuffer`}),c=C(P(),`${e.replace(/\s+/g,``)}-${t}.${o===`woff2`?`woff2`:o===`woff`?`woff`:`ttf`}`);return v(c,T.from(s)),console.warn(`Downloaded and saved "${e}" weight ${t} to ${c}`),c}catch(n){return console.warn(`Could not download font "${e}" weight ${t} from Google Fonts:`,n),null}}function ae(e,t){let n=new Set,r=RegExp(`<text[^>]*font-family="${t.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}"[^>]*>`,`g`),i=r.exec(e);for(;i!==null;){let t=i[0].match(/font-weight="(\d+)"/);t?n.add(t[1]):n.add(`400`),i=r.exec(e)}return n.size>0?Array.from(n):[`400`]}async function oe(e,t){let n=b(t),r=P(),i=/font-family="([^"]+)"/g,a=new Set,o=i.exec(e);for(;o!==null;)a.add(o[1]),o=i.exec(e);if(a.size===0)return e;for(let t of a){let i=t.replace(/\s+/g,``),a=!1;try{let e=h(n,{withFileTypes:!0});for(let t of e){if(!t.isFile())continue;let e=t.name,n=x(e).toLowerCase();[`.ttf`,`.woff`,`.woff2`].includes(n)&&y(e,n).toLowerCase().includes(i.toLowerCase())&&(a=!0)}}catch{}if(!a)try{let e=h(r,{withFileTypes:!0});for(let t of e)if(t.isFile()&&t.name.toLowerCase().includes(i.toLowerCase())){a=!0;break}}catch{}if(!a){console.warn(`Font "${t}" not found, downloading from Google Fonts...`);let n=ae(e,t);for(let e of n)await ie(t,e)}}return e}async function se(e,t,n){let r=S(e)?e:ne(e,n);if(!f(r))throw Error(`SVG template not found at ${r}. Please ensure the template exists in ${N(n)}.`);let i;try{i=m(r,`utf-8`)}catch(e){let t=e instanceof Error?e.message:String(e);throw Error(`Failed to load SVG template from ${r}: ${t}`)}await oe(i,r);let a=t.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`);return i.replace(/\{\{text\}\}/g,a)}async function F(e,t,n,r,i){try{re();let a=await se(r,n,i),o=await O(e).metadata();if(!o.width||!o.height)throw Error(`Could not determine image dimensions`);let s=await O(T.from(a),{density:300}).resize(o.width,o.height,{fit:`fill`}).png().toBuffer();await O(e).composite([{input:s,blend:`over`}]).toFile(t)}catch(e){throw console.error(`Error adding text to image:`,e),e}}async function I(r){n(`Video Toolkit - Add Template`);let o=await e(),s=w(r);f(s)||(i.error(`Source file not found: ${s}`),t.exit(1)),s.toLowerCase().endsWith(`.svg`)||(i.error(`Source file must be an SVG file`),t.exit(1));let l=await c({message:`Enter a name for this template:`,placeholder:`e.g., series-x, youtube-intro`,validate:e=>{if(!e||e.trim()===``)return`Template name is required`;if(!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(e))return`Template name should start and end with lowercase letters or numbers, and can contain hyphens in between`}});typeof l==`symbol`&&(i.error(`Template name is required. Exiting.`),t.exit(1));let d=N(o.templatesDir);f(d)||(p(d,{recursive:!0}),i.success(`Created templates directory: ${d}`));let m=w(d,`${l}.svg`);f(m)&&i.warn(`Template '${l}' already exists. Overwriting...`);try{u(s,m),a(`✓ Template '${l}' added successfully at ${m}`)}catch(e){let n=e instanceof Error?e.message:String(e);i.error(`Failed to copy template: ${n}`),t.exit(1)}}function L(e){let t=w(e),n=y(t,x(t)),r=w(t,`..`),i=C(r,`${n}_audio.mp3`),a=C(r,`${n}.srt`),o=C(r,`thumbnails`);f(o)||p(o,{recursive:!0});let s=[`first`,`25`,`50`,`75`,`last`];return{videoPath:t,audioPath:i,srtPath:a,thumbnailTempPaths:s.map(e=>C(o,`${n}_thumbnail_${e}_temp.png`)),thumbnailPaths:s.map(e=>C(o,`${n}_thumbnail_${e}.png`))}}function R(e,t){let n=`ffmpeg -i ${e} -q:a 0 -map a ${t}`;try{k(n,{stdio:`inherit`})}catch(e){console.error(`Error extracting audio:`,e)}}function z(e){try{let t=k(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${e}"`,{encoding:`utf-8`});return Number.parseFloat(t.trim())}catch(e){throw console.error(`Error getting video duration:`,e),e}}function B(e,t,n=`first`){let r=``;r=n===`first`?`ffmpeg -i "${e}" -vf "select=eq(n\\,0)" -vframes 1 "${t}" -y`:n===`last`?`ffmpeg -sseof -0.1 -i "${e}" -vframes 1 "${t}" -y`:`ffmpeg -ss ${z(e)*(Number.parseInt(n)/100)} -i "${e}" -vframes 1 "${t}" -y`;try{k(r,{stdio:`inherit`})}catch(e){throw console.error(`Error extracting thumbnail:`,e),e}}async function V(e,t){return await new ee({apiKey:t.openaiApiKey}).audio.transcriptions.create({file:d(e),model:t.model,language:t.language,response_format:`srt`})}async function H(e,n){let r=s();r.start(`Extracting audio...`);try{R(e,n),r.stop(`Audio extracted successfully`)}catch(e){r.stop(`Audio extraction failed`),i.error(`Error during audio extraction: ${e}`),t.exit(1)}}async function U(e,n,r){let a=s();a.start(`Generating transcription with OpenAI...`);try{v(n,await V(e,r)),a.stop(`Transcription generated successfully`)}catch(n){a.stop(`Transcription generation failed`),i.error(`Error during transcription generation: ${n}`);try{_(e)}catch{}t.exit(1)}}async function W(e,n,r){let a=s(),o=r===`first`?`start`:r===`last`?`end`:`${r}%`;a.start(`Extracting thumbnail at ${o}...`);try{B(e,n,r),a.stop(`Thumbnail extracted successfully`)}catch(e){a.stop(`Thumbnail extraction failed`),i.error(`Error during thumbnail extraction: ${e}`),t.exit(1)}}async function G(e,n,r,a,o){let c=s();c.start(`Adding text to thumbnail...`);try{await F(e,n,r,a,o),c.stop(`Text added to thumbnail successfully`)}catch(e){c.stop(`Failed to add text to thumbnail`),i.error(`Error adding text to thumbnail: ${e}`),t.exit(1)}}function K(e,t=`temporary file`){try{_(e)}catch{i.warn(`Could not delete ${t}: ${e}`)}}async function ce(e){let{videoPath:t,audioPath:r}=L(e);n(`Video Toolkit - Extract Audio: ${y(t)}`),await H(t,r),a(`✓ Audio saved to: ${r}`)}function le(e=t.cwd()){let n=[];try{let t=h(e).filter(t=>g(C(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let r of t){let t=C(e,r),i=h(t).filter(e=>g(C(t,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of i){let i=C(t,e),a=h(i).filter(e=>g(C(i,e)).isDirectory()&&/^\d{2}$/.test(e));for(let t of a){let a=C(i,t),o=h(a,{withFileTypes:!0}).filter(e=>e.isFile()&&x(e.name).toLowerCase()===`.mp4`).map(e=>e.name);if(o.length>0){let i=`${r}-${e}-${t}`;n.push({path:a,date:new Date(i),displayPath:`${r}/${e}/${t}`,videoFile:o[0]})}}}}}catch(e){return i.error(`Error reading directories: ${e}`),[]}return n.sort((e,t)=>t.date.getTime()-e.date.getTime()).slice(0,8)}async function q(e=t.cwd()){let n=le(e);n.length===0&&(i.error(`No dated folders with video files found.`),i.info(`Expected structure: YYYY/MM/DD/*.mp4`),t.exit(1));let a=n.map(e=>({label:`${e.displayPath} (${e.videoFile})`,value:e.path,hint:`Process video from ${e.displayPath}`}));a.push({label:`Custom date`,value:`custom`,hint:`Enter a specific date`});let s=await o({message:`Select a content folder with video:`,options:a});if(r(s)&&(i.error(`Operation cancelled.`),t.exit(0)),s===`custom`){let n=await c({message:`Enter date (YYYY-MM-DD):`,placeholder:A(new Date,`yyyy-MM-dd`),validate:e=>{if(!e)return`Date is required`;if(!/^\d{4}-\d{2}-\d{2}$/.test(e))return`Invalid date format. Please use YYYY-MM-DD`;try{let t=j(e,`yyyy-MM-dd`,new Date);if(Number.isNaN(t.getTime()))return`Invalid date`}catch{return`Invalid date`}}});r(n)&&(i.error(`Operation cancelled.`),t.exit(0));let a=j(n,`yyyy-MM-dd`,new Date),o=C(e,A(a,`yyyy`),A(a,`MM`),A(a,`dd`));f(o)||(i.error(`Folder does not exist: ${o}`),t.exit(1));let s=h(o,{withFileTypes:!0}).filter(e=>e.isFile()&&x(e.name).toLowerCase()===`.mp4`).map(e=>e.name);return s.length===0&&(i.error(`No .mp4 files found in: ${o}`),t.exit(1)),{folderPath:o,videoPath:C(o,s[0])}}let l=n.find(e=>e.path===s);l||(i.error(`Selected folder not found.`),t.exit(1));let u=C(l.path,l.videoFile);return{folderPath:l.path,videoPath:u}}async function J(){let e=await c({message:`Enter text for the thumbnail (or press Enter to skip):`,placeholder:`Optional thumbnail text`});if(typeof e!=`symbol`)return e.trim()||void 0}async function Y(e){let t=te(e);if(t.length===0){i.error(`No templates found in ${e}`),i.info(`Please add SVG templates to ${e} before proceeding.`);return}let n=await o({message:`Choose a template:`,options:t.map(e=>({label:e,value:e}))});if(typeof n!=`symbol`)return n}const X=[`first`,`25`,`50`,`75`,`last`];async function Z(e,t,n,r,i,a){for(let o=0;o<X.length;o++)await W(e,t[o],X[o]),await G(t[o],n[o],r,i,a),K(t[o],`temporary thumbnail`)}async function ue(e,t){let{videoPath:r,thumbnailTempPaths:o,thumbnailPaths:s}=L(e);n(`Video Toolkit - Generate Thumbnails: ${y(r)}`);let c=await J();if(!c){i.warn(`No text provided. Thumbnails require text overlay.`),a(`Operation cancelled`);return}let l=await Y(t.templatesDir);if(!l){i.warn(`No template selected. Cannot generate thumbnails.`),a(`Operation cancelled`);return}await Z(r,o,s,c,l,t.templatesDir),a(`✓ Thumbnails saved:\n - ${s.join(`
|
|
8
|
-
- `)}`)}async function Q(e,t){let{videoPath:r,audioPath:o,srtPath:s,thumbnailTempPaths:c,thumbnailPaths:l}=L(e);n(`Video Toolkit - Processing: ${y(r)}`),await H(r,o),await U(o,s,t),K(o,`audio file`);let u=await J();if(u){let e=await Y(t.templatesDir);if(!e){i.warn(`No template selected. Skipping thumbnail generation.`),a(`✓ Transcription saved to: ${s}`);return}await Z(r,c,l,u,e,t.templatesDir),a(`✓ Transcription saved to: ${s}\n✓ Thumbnails saved:\n - ${l.join(`
|
|
9
|
-
- `)}`)}else a(`✓ Transcription saved to: ${s}`)}const $=l(`video-toolkit`);$.command(`[path]`,`Process a video from dated content folders`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(r,a)=>{let o=r||a?.path||t.cwd();n(`Video Toolkit - Process video from content folder`);let s=await e();s.openaiApiKey||(i.error(`OpenAI API key is required. Please set it in:`),i.error(` - Config file: ~/.video-toolkitrc`),t.exit(1));let{videoPath:c}=await q(o);await Q(c,s)}),$.command(`add-template <file>`,`Add an SVG template to the templates directory`).action(async e=>{await I(e)}),$.command(`extract-audio <videoPath>`,`Extract audio from a video file`).action(async e=>{await ce(e)}),$.command(`generate-thumbnail [path]`,`Generate thumbnails for a video without regenerating transcription`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(n,r)=>{let i=n||r?.path||t.cwd(),a=await e(),{videoPath:o}=await q(i);await ue(o,a)}),$.help(),$.version(M),$.parse();export{};
|
|
2
|
+
import{m as e,n as t,r as n,t as r}from"./modules-BG4hzM3o.mjs";import i from"node:process";import{log as a}from"@clack/prompts";import{Command as o}from"commander";var s=`@barbapapazes/video-toolkit`,c=`0.20.1`,l=`CLI toolkit for video processing and thumbnail automation`;const u=new o;u.name(s).description(l).version(c).showHelpAfterError();const d=u.command(`video`);d.description(`Manage video processing workflows`),d.command(`process`).description(`Process an explicit video file`).requiredOption(`--video <path>`,`Direct path to the video file`).option(`--thumbnail-text <text>`,`Optional thumbnail text to generate thumbnails during processing`).option(`--template <nameOrPath>`,`Template name or absolute SVG path used for thumbnail generation`).action(async e=>{await r({videoPath:e?.video,thumbnailText:e?.thumbnailText,templateName:e?.template})}),d.command(`extract-audio <videoPath>`).description(`Extract audio from a video file`).action(async e=>{await n(e)}),d.command(`generate-thumbnail`).description(`Generate thumbnails for an explicit video path`).requiredOption(`--video <path>`,`Direct path to the video file`).requiredOption(`--thumbnail-text <text>`,`Text to render on generated thumbnails`).requiredOption(`--template <nameOrPath>`,`Template name or absolute SVG path used for thumbnail generation`).action(async e=>{await t({videoPath:e?.video,thumbnailText:e?.thumbnailText||``,templateName:e?.template||``})});const f=u.command(`template`);f.description(`Manage thumbnail templates`),f.command(`add <file>`).description(`Add an SVG template to the templates directory`).requiredOption(`--name <name>`,`Template name (lowercase letters, numbers, and hyphens only)`).action(async(t,n)=>{await e(t,n?.name||``)}),u.parseAsync().catch(e=>{a.error(e instanceof Error?e.message:String(e)),i.exit(1)});export{};
|
package/dist/index.d.mts
CHANGED
|
@@ -25,11 +25,111 @@ interface Config {
|
|
|
25
25
|
templatesDir?: string;
|
|
26
26
|
}
|
|
27
27
|
type ResolvedConfig = Required<Config>;
|
|
28
|
+
type ThumbnailPosition = 'first' | '25' | '50' | '75' | 'last';
|
|
28
29
|
//#endregion
|
|
29
|
-
//#region src/
|
|
30
|
+
//#region src/common/files.d.ts
|
|
31
|
+
/**
|
|
32
|
+
* Cleans up a temporary file.
|
|
33
|
+
*/
|
|
34
|
+
declare function cleanupFile(filePath: string, description?: string): void;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/config.d.ts
|
|
30
37
|
declare function loadVideoToolkitConfig(): Promise<ResolvedConfig>;
|
|
31
38
|
//#endregion
|
|
39
|
+
//#region src/modules/template/steps/add-template.d.ts
|
|
40
|
+
declare function addTemplate(sourcePath: string, templateName: string, templatesDirConfigPath: string): string;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/modules/template/workflows/template-add.d.ts
|
|
43
|
+
declare function templateAddWorkflow(sourcePath: string, templateName: string): Promise<void>;
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/modules/video/lib/date-folders.d.ts
|
|
46
|
+
interface DateFolder {
|
|
47
|
+
path: string;
|
|
48
|
+
date: Date;
|
|
49
|
+
displayPath: string;
|
|
50
|
+
videoFiles: string[];
|
|
51
|
+
}
|
|
52
|
+
interface ResolveVideoTargetOptions {
|
|
53
|
+
basePath?: string;
|
|
54
|
+
videoPath?: string;
|
|
55
|
+
}
|
|
56
|
+
interface ResolveVideoTargetResult {
|
|
57
|
+
folderPath: string;
|
|
58
|
+
videoPath: string;
|
|
59
|
+
videoFiles: string[];
|
|
60
|
+
}
|
|
61
|
+
declare function getRecentDateFoldersWithVideos(basePath?: string, limit?: number): DateFolder[];
|
|
62
|
+
declare function resolveVideoTarget(options?: ResolveVideoTargetOptions): ResolveVideoTargetResult;
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/modules/video/lib/generate-thumbnails.d.ts
|
|
65
|
+
declare function generateThumbnails(videoPath: string, thumbnailTempPaths: string[], thumbnailPaths: string[], text: string, templateName: string, templatesDir: string): Promise<void>;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/modules/video/lib/list-video-files.d.ts
|
|
68
|
+
declare function listVideoFiles(directoryPath: string): string[];
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/modules/video/lib/process-paths.d.ts
|
|
71
|
+
declare const THUMBNAIL_POSITIONS: readonly ThumbnailPosition[];
|
|
72
|
+
interface ProcessPaths {
|
|
73
|
+
videoPath: string;
|
|
74
|
+
audioPath: string;
|
|
75
|
+
srtPath: string;
|
|
76
|
+
thumbnailTempPaths: string[];
|
|
77
|
+
thumbnailPaths: string[];
|
|
78
|
+
}
|
|
79
|
+
declare function getProcessPaths(videoPath: string): ProcessPaths;
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/modules/video/steps/extract-audio.d.ts
|
|
82
|
+
interface ExtractAudioResult {
|
|
83
|
+
videoPath: string;
|
|
84
|
+
audioPath: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extract audio from a video file.
|
|
88
|
+
*/
|
|
89
|
+
declare function extractAudio(videoPath: string): Promise<ExtractAudioResult>;
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/modules/video/steps/generate-thumbnail.d.ts
|
|
92
|
+
interface GenerateThumbnailResult {
|
|
93
|
+
videoPath: string;
|
|
94
|
+
thumbnailPaths: string[];
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Generate thumbnails for a video file without regenerating the transcription.
|
|
98
|
+
* This allows regenerating thumbnails when the template changes.
|
|
99
|
+
*/
|
|
100
|
+
declare function generateThumbnail(videoPath: string, thumbnailText: string, templateName: string, templatesDir: string): Promise<GenerateThumbnailResult>;
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/modules/video/steps/generate-transcription.d.ts
|
|
103
|
+
interface GenerateTranscriptionResult {
|
|
104
|
+
srtPath: string;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Generate an SRT transcription file from an extracted audio file.
|
|
108
|
+
*/
|
|
109
|
+
declare function generateTranscription(videoPath: string, audioPath: string, config: ResolvedConfig): Promise<GenerateTranscriptionResult>;
|
|
110
|
+
//#endregion
|
|
111
|
+
//#region src/modules/video/workflows/video-extract-audio.d.ts
|
|
112
|
+
declare function videoExtractAudioWorkflow(videoPath: string): Promise<void>;
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/modules/video/workflows/video-generate-thumbnail.d.ts
|
|
115
|
+
interface VideoGenerateThumbnailWorkflowOptions {
|
|
116
|
+
basePath?: string;
|
|
117
|
+
videoPath?: string;
|
|
118
|
+
thumbnailText: string;
|
|
119
|
+
templateName: string;
|
|
120
|
+
}
|
|
121
|
+
declare function videoGenerateThumbnailWorkflow(optionsOrBasePath: string | VideoGenerateThumbnailWorkflowOptions): Promise<void>;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/modules/video/workflows/video-process.d.ts
|
|
124
|
+
interface VideoProcessWorkflowOptions {
|
|
125
|
+
basePath?: string;
|
|
126
|
+
videoPath?: string;
|
|
127
|
+
thumbnailText?: string;
|
|
128
|
+
templateName?: string;
|
|
129
|
+
}
|
|
130
|
+
declare function videoProcessWorkflow(optionsOrBasePath: string | VideoProcessWorkflowOptions): Promise<void>;
|
|
131
|
+
//#endregion
|
|
32
132
|
//#region src/index.d.ts
|
|
33
133
|
declare function defineConfig(config: Config): Config;
|
|
34
134
|
//#endregion
|
|
35
|
-
export { type Config, type ResolvedConfig, defineConfig, loadVideoToolkitConfig };
|
|
135
|
+
export { type Config, DateFolder, ProcessPaths, ResolveVideoTargetOptions, ResolveVideoTargetResult, type ResolvedConfig, THUMBNAIL_POSITIONS, addTemplate, cleanupFile, defineConfig, extractAudio, generateThumbnail, generateThumbnails, generateTranscription, getProcessPaths, getRecentDateFoldersWithVideos, listVideoFiles, loadVideoToolkitConfig, resolveVideoTarget, templateAddWorkflow, videoExtractAudioWorkflow, videoGenerateThumbnailWorkflow, videoProcessWorkflow };
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{t as
|
|
1
|
+
import{a as e,c as t,d as n,f as r,g as i,h as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,s as p,t as m,u as h}from"./modules-BG4hzM3o.mjs";function g(e){return e}export{t as THUMBNAIL_POSITIONS,i as addTemplate,h as cleanupFile,g as defineConfig,u as extractAudio,e as generateThumbnail,p as generateThumbnails,o as generateTranscription,s as getProcessPaths,n as getRecentDateFoldersWithVideos,d as listVideoFiles,a as loadVideoToolkitConfig,r as resolveVideoTarget,c as templateAddWorkflow,f as videoExtractAudioWorkflow,l as videoGenerateThumbnailWorkflow,m as videoProcessWorkflow};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import e from"node:process";import{intro as t,log as n,outro as r,spinner as i}from"@clack/prompts";import{copyFileSync as a,createReadStream as o,existsSync as s,mkdirSync as c,readFileSync as l,readdirSync as u,statSync as d,unlinkSync as f,writeFileSync as p}from"node:fs";import{basename as m,dirname as h,extname as g,isAbsolute as _,join as v,resolve as y}from"node:path";import{Buffer as b}from"node:buffer";import{homedir as x}from"node:os";import{ofetch as S}from"ofetch";import C from"sharp";import{loadConfig as w}from"c12";import{xSync as T}from"tinyexec";import ee from"openai";function E(e){return e instanceof Error?e.message:String(e)}const te=/\{\{text\}\}/g;function D(t){return _(t)?t:y(e.cwd(),t)}function ne(e,t){return v(D(t),`${e}.svg`)}function O(){let e=v(x(),`.config`,`video-toolkit`,`fonts`);return s(e)||c(e,{recursive:!0}),e}function k(){let t=O(),n=h(t),r=v(n,`fonts.conf`);s(r)||p(r,`<?xml version="1.0"?>
|
|
2
|
+
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
|
3
|
+
<fontconfig>
|
|
4
|
+
<dir>${t}</dir>
|
|
5
|
+
<cachedir>${v(n,`fontcache`)}</cachedir>
|
|
6
|
+
</fontconfig>`,`utf-8`),e.env.FONTCONFIG_PATH=n,e.platform===`darwin`&&(e.env.PANGOCAIRO_BACKEND=`fontconfig`)}async function A(t,n){try{let r=await S(`https://fonts.googleapis.com/css2?family=${t.replace(/\s+/g,`+`)}:wght@${n}&display=swap`),i=r.match(/url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)/),a=r.match(/format\('([^']+)'\)/);if(!i||!a)return null;let o=i[1],s=a[1],c=await S(o,{responseType:`arrayBuffer`}),l=v(O(),`${t.replace(/\s+/g,``)}-${n}.${s===`woff2`?`woff2`:s===`woff`?`woff`:`ttf`}`);return p(l,b.from(c)),e.emitWarning(`Downloaded and saved "${t}" weight ${n} to ${l}`),l}catch(r){return e.emitWarning(`Could not download font "${t}" weight ${n} from Google Fonts: ${E(r)}`),null}}function j(e,t){let n=new Set,r=RegExp(`<text[^>]*font-family="${t.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)}"[^>]*>`,`g`),i=r.exec(e);for(;i!==null;){let t=i[0].match(/font-weight="(\d+)"/);t?n.add(t[1]):n.add(`400`),i=r.exec(e)}return n.size>0?Array.from(n):[`400`]}async function M(t,n){let r=h(n),i=O(),a=/font-family="([^"]+)"/g,o=new Set,s=a.exec(t);for(;s!==null;)o.add(s[1]),s=a.exec(t);if(o.size===0)return t;for(let n of o){let a=n.replace(/\s+/g,``),o=!1;try{let e=u(r,{withFileTypes:!0});for(let t of e){if(!t.isFile())continue;let e=t.name,n=g(e).toLowerCase();[`.ttf`,`.woff`,`.woff2`].includes(n)&&m(e,n).toLowerCase().includes(a.toLowerCase())&&(o=!0)}}catch{}if(!o)try{let e=u(i,{withFileTypes:!0});for(let t of e)if(t.isFile()&&t.name.toLowerCase().includes(a.toLowerCase())){o=!0;break}}catch{}if(!o){e.emitWarning(`Font "${n}" not found locally, downloading from Google Fonts...`);let r=j(t,n);for(let e of r)await A(n,e)}}return t}async function N(e,t,n){let r=_(e)?e:ne(e,n);if(!s(r))throw Error(`SVG template not found at ${r}. Please ensure the template exists in ${D(n)}.`);let i;try{i=l(r,`utf-8`)}catch(e){throw Error(`Failed to load SVG template from ${r}: ${E(e)}`)}if(await M(i,r),!i.includes(`{{text}}`))throw Error(`SVG template at ${r} must include a {{text}} placeholder`);let a=t.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`);return i.replace(te,a)}async function re(e,t,n,r,i){k();try{let a=await N(r,n,i),o=await C(e).metadata();if(!o.width||!o.height)throw Error(`Could not determine image dimensions`);let s=await C(b.from(a),{density:300}).resize(o.width,o.height,{fit:`fill`}).png().toBuffer();await C(e).composite([{input:s,blend:`over`}]).toFile(t)}catch(e){throw Error(`Failed to add text to image: ${E(e)}`)}}function P(e,t,r){let i=y(e),o=t.trim();if(!o)throw Error(`Template name is required`);if(!/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(o))throw Error(`Template name should start and end with lowercase letters or numbers, and can contain hyphens in between`);if(!s(i))throw Error(`Source file not found: ${i}`);if(!i.toLowerCase().endsWith(`.svg`))throw Error(`Source file must be an SVG file`);let l=D(r);s(l)||(c(l,{recursive:!0}),n.success(`Created templates directory: ${l}`));let u=y(l,`${o}.svg`);return s(u)&&n.warn(`Template '${o}' already exists. Overwriting...`),a(i,u),u}const F=v(x(),`.config`,`video-toolkit`,`templates`),I={openaiApiKey:``,language:`fr`,model:`whisper-1`,templatesDir:F};function ie(t){return!t||t.trim()===``?F:_(t)?t:y(e.cwd(),t)}async function L(){let{config:e}=await w({name:`video-toolkit`,defaults:I,globalRc:!0});return{openaiApiKey:e.openaiApiKey?.trim()??``,language:e.language?.trim()||I.language||`fr`,model:e.model?.trim()||I.model||`whisper-1`,templatesDir:ie(e.templatesDir)}}async function R(e,n){t(`Video Toolkit - Add Template`);let i=await L(),a=n.trim();if(!a)throw Error(`Template name is required.`);r(`✓ Template '${a}' added successfully at ${P(e,a,i.templatesDir)}`)}function z(e){return u(e,{withFileTypes:!0}).filter(e=>e.isFile()&&g(e.name).toLowerCase()===`.mp4`).map(e=>e.name).sort((e,t)=>e.localeCompare(t))}function B(e,t,n){let r=new Date(Date.UTC(e,t-1,n));if(!(r.getUTCFullYear()!==e||r.getUTCMonth()!==t-1||r.getUTCDate()!==n))return r}function V(t=e.cwd(),n=8){let r=y(t),i=[];try{let e=u(r).filter(e=>d(v(r,e)).isDirectory()&&/^\d{4}$/.test(e));for(let t of e){let e=v(r,t),n=u(e).filter(t=>d(v(e,t)).isDirectory()&&/^\d{2}$/.test(t));for(let r of n){let n=v(e,r),a=u(n).filter(e=>d(v(n,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of a){let a=B(Number(t),Number(r),Number(e));if(!a)continue;let o=v(n,e),s=z(o);s.length!==0&&i.push({path:o,date:a,displayPath:`${t}/${r}/${e}`,videoFiles:s})}}}}catch(e){throw Error(`Error reading dated folders in ${r}: ${E(e)}`)}return i.sort((e,t)=>t.date.getTime()-e.date.getTime()).slice(0,n)}function H(e={}){let{videoPath:t}=e;if(t){let e=y(t);if(!s(e))throw Error(`Video file not found: ${e}`);return{folderPath:h(e),videoPath:e,videoFiles:[m(e)]}}throw Error(`A video path is required. Pass --video <path> to select the file to process.`)}function U(e,t=`temporary file`){try{f(e)}catch{n.warn(`Could not delete ${t}: ${e}`)}}function W(e){try{let t=T(`ffprobe`,[`-v`,`error`,`-show_entries`,`format=duration`,`-of`,`default=noprint_wrappers=1:nokey=1`,e],{throwOnError:!0}),n=Number.parseFloat(t.stdout.trim());if(!Number.isFinite(n)||n<=0)throw Error(`Invalid video duration returned by ffprobe: ${t.stdout.trim()}`);return n}catch(e){throw Error(`Failed to determine video duration: ${E(e)}`)}}function G(e,t,n=`first`){let r;if(n===`first`)r=[`-i`,e,`-vf`,`select=eq(n\\,0)`,`-vframes`,`1`,t,`-y`];else if(n===`last`)r=[`-sseof`,`-0.1`,`-i`,e,`-vframes`,`1`,t,`-y`];else{let i=W(e)*(Number.parseInt(n)/100);r=[`-ss`,String(i),`-i`,e,`-vframes`,`1`,t,`-y`]}try{T(`ffmpeg`,r,{throwOnError:!0,nodeOptions:{stdio:`inherit`}})}catch(e){throw Error(`Failed to extract thumbnail with ffmpeg: ${E(e)}`)}}const K=[`first`,`25`,`50`,`75`,`last`];function q(e){let t=y(e),n=m(t,g(t)),r=h(t),i=v(r,`${n}_audio.mp3`),a=v(r,`${n}.srt`),o=v(r,`thumbnails`);return s(o)||c(o,{recursive:!0}),{videoPath:t,audioPath:i,srtPath:a,thumbnailTempPaths:K.map(e=>v(o,`${n}_thumbnail_${e}_temp.png`)),thumbnailPaths:K.map(e=>v(o,`${n}_thumbnail_${e}.png`))}}async function J(e,t,n){let r=i(),a=n===`first`?`start`:n===`last`?`end`:`${n}%`;r.start(`Extracting thumbnail at ${a}...`);try{G(e,t,n),r.stop(`Thumbnail extracted successfully`)}catch(e){throw r.stop(`Thumbnail extraction failed`),Error(`Error during thumbnail extraction: ${E(e)}`)}}async function ae(e,t,n,r,a){let o=i();o.start(`Adding text to thumbnail...`);try{await re(e,t,n,r,a),o.stop(`Text added to thumbnail successfully`)}catch(e){throw o.stop(`Failed to add text to thumbnail`),Error(`Error adding text to thumbnail: ${E(e)}`)}}async function Y(e,t,n,r,i,a){if(t.length!==K.length||n.length!==K.length)throw Error(`Expected ${K.length} thumbnail paths for generation`);for(let o=0;o<K.length;o++)await J(e,t[o],K[o]),await ae(t[o],n[o],r,i,a),U(t[o],`temporary thumbnail`)}function oe(e,t){try{T(`ffmpeg`,[`-i`,e,`-q:a`,`0`,`-map`,`a`,t,`-y`],{throwOnError:!0,nodeOptions:{stdio:`inherit`}})}catch(e){throw Error(`Failed to extract audio with ffmpeg: ${E(e)}`)}}async function X(e){let{videoPath:t,audioPath:n}=q(e),r=i();r.start(`Extracting audio...`);try{oe(t,n),r.stop(`Audio extracted successfully`)}catch(e){throw r.stop(`Audio extraction failed`),Error(`Error during audio extraction: ${E(e)}`)}return{videoPath:t,audioPath:n}}async function Z(e,t,n,r){let{videoPath:i,thumbnailTempPaths:a,thumbnailPaths:o}=q(e);return await Y(i,a,o,t,n,r),{videoPath:i,thumbnailPaths:o}}async function se(e,t){return await new ee({apiKey:t.openaiApiKey}).audio.transcriptions.create({file:o(e),model:t.model,language:t.language,response_format:`srt`})}async function Q(e,t,n){let{srtPath:r}=q(e),a=i();a.start(`Generating transcription with OpenAI...`);try{p(r,await se(t,n)),a.stop(`Transcription generated successfully`)}catch(e){a.stop(`Transcription generation failed`);try{f(t)}catch{}throw Error(`Error during transcription generation: ${E(e)}`)}return{srtPath:r}}async function ce(e){t(`Video Toolkit - Extract Audio: ${m(e)}`);let{audioPath:n}=await X(e);r(`✓ Audio saved to: ${n}`)}function le(e){if(typeof e==`string`)throw TypeError(`Thumbnail generation now requires thumbnail text and template name arguments.`);return e}async function $(e){let n=le(e),i=await L(),a=n.thumbnailText.trim(),o=n.templateName.trim();if(!a)throw Error(`Thumbnail text is required.`);if(!o)throw Error(`Template name is required.`);let{videoPath:s}=H(n);t(`Video Toolkit - Generate Thumbnails: ${m(s)}`);let{thumbnailPaths:c}=await Z(s,a,o,i.templatesDir);r(`✓ Thumbnails saved:\n - ${c.join(`
|
|
7
|
+
- `)}`)}function ue(e){return typeof e==`string`?{basePath:e}:e}function de(e){let t=e.thumbnailText?.trim()||void 0,n=e.templateName?.trim()||void 0;if(t&&!n||!t&&n)throw Error(`Both thumbnail text and template name are required to generate thumbnails.`);return{thumbnailText:t,templateName:n}}async function fe(e){let n=ue(e),i=await L();if(!i.openaiApiKey)throw Error(`OpenAI API key is required. Please set it in ~/.video-toolkitrc or video-toolkit.config.{ts,js,mjs,json}.`);let{videoPath:a}=H(n);t(`Video Toolkit - Processing: ${m(a)}`);let{videoPath:o,audioPath:s}=await X(a),{srtPath:c}=await Q(o,s,i);U(s,`audio file`);let{thumbnailText:l,templateName:u}=de(n);if(!l){r(`✓ Transcription saved to: ${c}`);return}let{thumbnailPaths:d}=await Z(o,l,u,i.templatesDir);r(`✓ Transcription saved to: ${c}\n✓ Thumbnails saved:\n - ${d.join(`
|
|
8
|
+
- `)}`)}export{Z as a,K as c,V as d,H as f,P as g,L as h,Q as i,q as l,R as m,$ as n,X as o,z as p,ce as r,Y as s,fe as t,U as u};
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barbapapazes/video-toolkit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.20.1",
|
|
5
|
+
"description": "CLI toolkit for video processing and thumbnail automation",
|
|
5
6
|
"author": "Estéban Soubiran <esteban@soubiran.dev>",
|
|
6
7
|
"license": "MIT",
|
|
7
8
|
"funding": "https://github.com/sponsors/Barbapapazes",
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"import": "./dist/index.mjs"
|
|
19
20
|
}
|
|
20
21
|
},
|
|
21
|
-
"main": "
|
|
22
|
+
"main": "./dist/index.mjs",
|
|
22
23
|
"types": "./dist/index.d.mts",
|
|
23
24
|
"bin": {
|
|
24
25
|
"video-toolkit": "./dist/cli.mjs"
|
|
@@ -30,18 +31,18 @@
|
|
|
30
31
|
"node": ">=24"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@clack/prompts": "^
|
|
34
|
+
"@clack/prompts": "^1.4.0",
|
|
34
35
|
"c12": "^3.3.4",
|
|
35
|
-
"
|
|
36
|
-
"date-fns": "^4.1.0",
|
|
36
|
+
"commander": "15.0.0-0",
|
|
37
37
|
"ofetch": "^1.5.1",
|
|
38
38
|
"openai": "^6.15.0",
|
|
39
|
-
"sharp": "^0.34.5"
|
|
39
|
+
"sharp": "^0.34.5",
|
|
40
|
+
"tinyexec": "1.1.2"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
43
|
"@tsconfig/node24": "^24.0.4",
|
|
43
44
|
"@types/node": "^22.19.17",
|
|
44
|
-
"tsdown": "^0.
|
|
45
|
+
"tsdown": "^0.22.0",
|
|
45
46
|
"typescript": "^5.9.3"
|
|
46
47
|
},
|
|
47
48
|
"scripts": {
|
package/dist/config-Cuh3n7mj.mjs
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{join as e}from"node:path";import{homedir as t}from"node:os";import{loadConfig as n}from"c12";const r={openaiApiKey:``,language:`fr`,model:`whisper-1`,templatesDir:e(t(),`.config`,`video-toolkit`,`templates`)};async function i(){let{config:e}=await n({name:`video-toolkit`,defaults:r,globalRc:!0});return{openaiApiKey:e.openaiApiKey||``,language:e.language||`fr`,model:e.model||`whisper-1`,templatesDir:e.templatesDir}}export{i as t};
|