@barbapapazes/video-toolkit 0.18.9 → 0.20.0

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 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
- ## Features
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
- The toolkit uses `fontconfig` for font discovery when rendering SVG text. On macOS systems using Homebrew:
7
+ ```bash
8
+ pnpm install @barbapapazes/video-toolkit
9
+ ```
28
10
 
29
- 1. The toolkit automatically sets `PANGOCAIRO_BACKEND=fontconfig` for proper font rendering
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
- If you encounter font-related issues, ensure fontconfig is properly configured on your system.
34
-
35
- ## Installation
13
+ Use the built-in help to discover the available commands and subcommands:
36
14
 
37
15
  ```bash
38
- pnpm install @barbapapazes/video-toolkit
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
- ### Configuration Options
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
- | Option | Type | Default | Description |
73
- |--------|------|---------|-------------|
74
- | `openaiApiKey` | string | - | **Required.** Your OpenAI API key |
75
- | `language` | string | `'fr'` | Language code for transcription (e.g., 'en', 'fr', 'es', 'de') |
76
- | `model` | string | `'whisper-1'` | OpenAI Whisper model to use |
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
- ## Usage
61
+ ## Commands
80
62
 
81
- You can run the toolkit directly using `npx` or by installing it globally.
63
+ ### `video process`
82
64
 
83
- ### Process a specific video
65
+ Process a video file and generate its transcription.
84
66
 
85
67
  ```bash
86
- video-toolkit path/to/your-video.mp4
68
+ video-toolkit video process --video ./content/2026/05/24/demo.mp4
87
69
  ```
88
70
 
89
- ### Extract audio only
71
+ Variant with thumbnail generation:
90
72
 
91
- Provide a full or relative path to the video file (no folder search):
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 extract-audio path/to/your-video.mp4
80
+ video-toolkit --video ./content/2026/05/24/demo.mp4
95
81
  ```
96
82
 
97
- ### Interactive selection
83
+ ### `video extract-audio`
98
84
 
99
- If you run the command without arguments, it will list all `.mp4` files in the current directory and let you choose one:
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
- ### Add a template
91
+ ### `video generate-thumbnail`
106
92
 
107
- Add an SVG template to your templates directory:
93
+ Generate thumbnails for a video file.
108
94
 
109
95
  ```bash
110
- video-toolkit add-template path/to/your-template.svg
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
- You'll be prompted to name the template (use lowercase letters, numbers, and hyphens, e.g., `series-x`, `youtube-intro`).
99
+ ### `template add`
114
100
 
115
- ### Workflow
101
+ Add an SVG template to the templates directory.
116
102
 
117
- 1. **Select Video**: Provide a path or choose from the list.
118
- 2. **Transcription**: The tool extracts audio and sends it to OpenAI.
119
- 3. **Thumbnail Text**: You will be prompted to enter text for the thumbnails.
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
- ### Template Location
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 e}from"./config-Cuh3n7mj.mjs";import t from"node:process";import{intro as n,isCancel as r,log as i,outro as a,select as o,spinner as s,text as c}from"@clack/prompts";import{cac as l}from"cac";import{copyFileSync as u,createReadStream as d,existsSync as f,mkdirSync as p,readFileSync as m,readdirSync as h,statSync as g,unlinkSync as _,writeFileSync as v}from"node:fs";import{basename as y,dirname as b,extname as x,isAbsolute as S,join as C,resolve as w}from"node:path";import{Buffer as T}from"node:buffer";import{homedir as E}from"node:os";import{ofetch as D}from"ofetch";import O from"sharp";import{execSync as k}from"node:child_process";import ee from"openai";import{format as A,parse as j}from"date-fns";var M=`0.18.9`;function N(e){return S(e)?e:w(t.cwd(),e)}function te(e){let t=N(e);if(!f(t))return[];try{return h(t,{withFileTypes:!0}).filter(e=>e.isFile()&&x(e.name).toLowerCase()===`.svg`).map(e=>e.name.replace(/\.svg$/i,``))}catch{return[]}}function ne(e,t){return C(N(t),`${e}.svg`)}function P(){let e=C(E(),`.config`,`video-toolkit`,`fonts`);return f(e)||p(e,{recursive:!0}),e}function re(){let e=P(),n=b(e),r=C(n,`fonts.conf`);f(r)||v(r,`<?xml version="1.0"?>
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,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&apos;`);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.0`,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/utils/config.d.ts
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 e}from"./config-Cuh3n7mj.mjs";function t(e){return e}export{t as defineConfig,e as loadVideoToolkitConfig};
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,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&apos;`);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.18.9",
4
+ "version": "0.20.0",
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": ".dist/index.mjs",
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": "^0.10.1",
34
+ "@clack/prompts": "^1.4.0",
34
35
  "c12": "^3.3.4",
35
- "cac": "^6.7.14",
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.18.4",
45
+ "tsdown": "^0.22.0",
45
46
  "typescript": "^5.9.3"
46
47
  },
47
48
  "scripts": {
@@ -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};