@barbapapazes/video-toolkit 0.9.0 → 0.10.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.
Files changed (3) hide show
  1. package/README.md +43 -34
  2. package/dist/cli.mjs +7 -13
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,11 +17,21 @@ A simple and efficient CLI toolkit for video automation. It automates the proces
17
17
 
18
18
  ## Prerequisites
19
19
 
20
- - [Node.js](https://nodejs.org/) (v22 or later recommended)
21
- - [ffmpeg](https://ffmpeg.org/) installed and available in your PATH.
20
+ - [Node.js](https://nodejs.org/) (v24 or later required)
21
+ - [ffmpeg](https://ffmpeg.org/) installed and available in your PATH
22
22
  - [OpenAI API Key](https://platform.openai.com/)
23
23
  - macOS (this tool is designed for macOS only)
24
24
 
25
+ ### Font Configuration (macOS)
26
+
27
+ The toolkit uses `fontconfig` for font discovery when rendering SVG text. On macOS systems using Homebrew:
28
+
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
32
+
33
+ If you encounter font-related issues, ensure fontconfig is properly configured on your system.
34
+
25
35
  ## Installation
26
36
 
27
37
  ```bash
@@ -143,9 +153,21 @@ The `{{text}}` placeholder will be replaced with the actual text you provide dur
143
153
 
144
154
  ### Using Custom Fonts
145
155
 
146
- To use custom fonts in your SVG templates, place the font files (`.ttf`, `.woff`, or `.woff2`) in the same directory as your SVG template. The toolkit will automatically embed them when rendering.
156
+ 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.
157
+
158
+ #### How It Works
147
159
 
148
- **Example structure:**
160
+ When you first use a template, the toolkit:
161
+ 1. Checks if fonts referenced in the SVG are available
162
+ 2. Looks for font files in the template directory or system fonts
163
+ 3. If not found, downloads fonts from Google Fonts
164
+ 4. Saves downloaded fonts to `~/.config/video-toolkit/fonts/`
165
+ 5. Configures fontconfig to discover fonts from this directory
166
+ 6. All future uses automatically find and use these fonts
167
+
168
+ #### Option 1: Local Font Files
169
+
170
+ Place font files (`.ttf`, `.woff`, or `.woff2`) in the same directory as your SVG template:
149
171
 
150
172
  ```
151
173
  ~/.config/video-toolkit/templates/
@@ -155,51 +177,38 @@ To use custom fonts in your SVG templates, place the font files (`.ttf`, `.woff`
155
177
  └── SofiaSans-Bold-700.ttf
156
178
  ```
157
179
 
158
- **In your SVG:**
159
-
160
- ```xml
161
- <text font-family="Sofia Sans" font-size="128" font-weight="500">
162
- {{text}}
163
- </text>
164
- ```
165
-
166
- The toolkit will:
167
- 1. Detect the `font-family="Sofia Sans"` attribute in your SVG
168
- 2. Look for matching font files (e.g., `SofiaSans*.ttf`, `SofiaSans*.woff`)
169
- 3. Automatically embed them as base64 data URIs
170
- 4. Extract font weights from filenames (e.g., `-300`, `-Light` → weight 300)
171
-
172
180
  **Font file naming conventions:**
173
-
174
181
  - `FontName.ttf` → default weight (400)
175
182
  - `FontName-300.ttf` or `FontName-Light.ttf` → weight 300
176
183
  - `FontName-500.ttf` or `FontName-Medium.ttf` → weight 500
177
184
  - `FontName-700.ttf` or `FontName-Bold.ttf` → weight 700
178
185
 
179
- #### Google Fonts Fallback
180
-
181
- If font files are not found locally, the toolkit will **automatically download them from Google Fonts**. This works for any font available on [fonts.google.com](https://fonts.google.com/).
186
+ #### Option 2: Google Fonts (Automatic)
182
187
 
183
- **Example:**
188
+ If fonts aren't found locally, they're automatically downloaded from [Google Fonts](https://fonts.google.com/):
184
189
 
185
190
  ```xml
186
- <text font-family="Roboto" font-size="80" font-weight="700">
191
+ <text font-family="Sofia Sans" font-size="128" font-weight="500">
187
192
  {{text}}
188
193
  </text>
189
194
  ```
190
195
 
191
- The toolkit will:
192
- 1. Detect that "Roboto" isn't available locally
193
- 2. Automatically download the font from Google Fonts with weight 700
194
- 3. Embed it in your SVG for rendering
196
+ **What happens:**
197
+ 1. First use: Downloads "Sofia Sans" weight 500 from Google Fonts
198
+ 2. Saves it to `~/.config/video-toolkit/fonts/SofiaSans-500.woff2`
199
+ 3. Configures fontconfig to discover this directory
200
+ 4. Subsequent uses: fontconfig finds the font instantly (no download)
195
201
 
196
202
  **Benefits:**
197
- - No need to manually download font files for Google Fonts
198
- - Works with 1400+ font families
199
- - Automatically downloads only the weights used in your SVG
200
- - Local font files take precedence (for custom/modified fonts)
201
-
202
- This ensures your SVG text renders with the correct fonts as designed, whether using custom fonts or popular web fonts.
203
+ - **One-time download** - Fonts downloaded once, reused forever
204
+ - **Proper font rendering** - Uses fontconfig (Sharp's recommended approach)
205
+ - **System-wide availability** - Downloaded fonts work across all templates
206
+ - **Works offline** - After first download, no internet needed
207
+ - ✅ **1400+ font families** from Google Fonts
208
+ - **Automatic fontconfig setup** - Creates configuration automatically
209
+
210
+ **Technical Note:**
211
+ 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.
203
212
 
204
213
  ### Managing Multiple Templates
205
214
 
package/dist/cli.mjs CHANGED
@@ -1,14 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import{basename as e,dirname as t,extname as n,isAbsolute as r,join as i,resolve as a}from"node:path";import o from"node:process";import{intro as s,isCancel as c,log as l,outro as u,select as d,spinner as f,text as p}from"@clack/prompts";import{cac as ee}from"cac";import{homedir as te}from"node:os";import{loadConfig as m}from"c12";import{copyFileSync as h,createReadStream as g,existsSync as _,mkdirSync as v,readFileSync as y,readdirSync as b,statSync as x,unlinkSync as S,writeFileSync as C}from"node:fs";import{format as w,parse as T}from"date-fns";import{Buffer as E}from"node:buffer";import{ofetch as D}from"ofetch";import O from"sharp";import{execSync as k}from"node:child_process";import A from"openai";var j=`0.9.0`;const M={openaiApiKey:``,language:`fr`,model:`whisper-1`,templatesDir:i(te(),`.config`,`video-toolkit`,`templates`)};async function N(){let{config:e}=await m({name:`video-toolkit`,defaults:M,globalRc:!0});return{openaiApiKey:e.openaiApiKey||``,language:e.language||`fr`,model:e.model||`whisper-1`,templatesDir:e.templatesDir}}function P(t){let r=a(t),o=e(r,n(r)),s=a(r,`..`),c=i(s,`${o}_audio.mp3`),l=i(s,`${o}.srt`),u=i(s,`thumbnails`);_(u)||v(u,{recursive:!0});let d=[`first`,`25`,`50`,`75`,`last`];return{videoPath:r,audioPath:c,srtPath:l,thumbnailTempPaths:d.map(e=>i(u,`${o}_thumbnail_${e}_temp.png`)),thumbnailPaths:d.map(e=>i(u,`${o}_thumbnail_${e}.png`))}}function F(e){return r(e)?e:a(o.cwd(),e)}function I(e){let t=F(e);if(!_(t))return[];try{return b(t,{withFileTypes:!0}).filter(e=>e.isFile()&&n(e.name).toLowerCase()===`.svg`).map(e=>e.name.replace(/\.svg$/i,``))}catch{return[]}}function L(e,t){return i(F(t),`${e}.svg`)}async function R(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`});return{base64:E.from(s).toString(`base64`),format:o,mimeType:{woff2:`font/woff2`,woff:`font/woff`,truetype:`font/ttf`,opentype:`font/otf`}[o]||`font/woff2`}}catch(n){return console.warn(`Could not download font "${e}" weight ${t} from Google Fonts:`,n),null}}function z(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 B(r,a){let o=t(a),s=/font-family="([^"]+)"/g,c=new Set,l=s.exec(r);for(;l!==null;)c.add(l[1]),l=s.exec(r);if(c.size===0)return r;let u=``;for(let t of c){let a=t.replace(/\s+/g,``),s=!1;try{let r=b(o,{withFileTypes:!0});for(let c of r){if(!c.isFile())continue;let r=c.name,l=n(r).toLowerCase();if([`.ttf`,`.woff`,`.woff2`].includes(l)&&e(r,l).toLowerCase().includes(a.toLowerCase())){s=!0;let e=y(i(o,r)).toString(`base64`),n={".ttf":`font/ttf`,".woff":`font/woff`,".woff2":`font/woff2`}[l]||`font/ttf`,a=`400`,c=r.match(/-(\d{3})/);c?a=c[1]:r.toLowerCase().includes(`light`)?a=`300`:r.toLowerCase().includes(`bold`)?a=`700`:r.toLowerCase().includes(`medium`)&&(a=`500`),u+=`
3
- @font-face {
4
- font-family: '${t}';
5
- font-weight: ${a};
6
- src: url(data:${n};base64,${e}) format('${l.slice(1)}');
7
- }`}}}catch(e){console.warn(`Could not load fonts from ${o}:`,e)}if(!s){console.warn(`Font "${t}" not found locally, attempting to download from Google Fonts...`);let e=z(r,t);for(let n of e){let e=await R(t,n);e&&(u+=`
8
- @font-face {
9
- font-family: '${t}';
10
- font-weight: ${n};
11
- src: url(data:${e.mimeType};base64,${e.base64}) format('${e.format}');
12
- }`,console.warn(`Successfully downloaded "${t}" weight ${n} from Google Fonts`))}}}return u?r.includes(`<defs>`)?r.replace(`<defs>`,`<defs><style>${u}\n </style>`):r.includes(`<svg`)?r.replace(`<svg`,`<svg><defs><style>${u}\n </style></defs>`).replace(`><defs>`,`>
13
- <defs>`):r:r}async function V(e,t,n){let i=r(e)?e:L(e,n);if(!_(i))throw Error(`SVG template not found at ${i}. Please ensure the template exists in ${F(n)}.`);let a;try{a=y(i,`utf-8`)}catch(e){let t=e instanceof Error?e.message:String(e);throw Error(`Failed to load SVG template from ${i}: ${t}`)}let o=t.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&apos;`);return await B(a.replace(/\{\{text\}\}/g,o),i)}async function H(e,t,n,r,i){try{let a=await V(r,n,i),o=await O(e).metadata();if(!o.width||!o.height)throw Error(`Could not determine image dimensions`);let s=await O(E.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}}function U(e=o.cwd()){let t=[];try{let r=b(e).filter(t=>x(i(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let a of r){let r=i(e,a),o=b(r).filter(e=>x(i(r,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of o){let o=i(r,e),s=b(o).filter(e=>x(i(o,e)).isDirectory()&&/^\d{2}$/.test(e));for(let r of s){let s=i(o,r),c=b(s,{withFileTypes:!0}).filter(e=>e.isFile()&&n(e.name).toLowerCase()===`.mp4`).map(e=>e.name);if(c.length>0){let n=`${a}-${e}-${r}`;t.push({path:s,date:new Date(n),displayPath:`${a}/${e}/${r}`,videoFile:c[0]})}}}}}catch(e){return l.error(`Error reading directories: ${e}`),[]}return t.sort((e,t)=>t.date.getTime()-e.date.getTime()).slice(0,8)}async function W(e=o.cwd()){let t=U(e);t.length===0&&(l.error(`No dated folders with video files found.`),l.info(`Expected structure: YYYY/MM/DD/*.mp4`),o.exit(1));let r=t.map(e=>({label:`${e.displayPath} (${e.videoFile})`,value:e.path,hint:`Process video from ${e.displayPath}`}));r.push({label:`Custom date`,value:`custom`,hint:`Enter a specific date`});let a=await d({message:`Select a content folder with video:`,options:r});if(c(a)&&(l.error(`Operation cancelled.`),o.exit(0)),a===`custom`){let t=await p({message:`Enter date (YYYY-MM-DD):`,placeholder:w(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=T(e,`yyyy-MM-dd`,new Date);if(Number.isNaN(t.getTime()))return`Invalid date`}catch{return`Invalid date`}}});c(t)&&(l.error(`Operation cancelled.`),o.exit(0));let r=T(t,`yyyy-MM-dd`,new Date),a=i(e,w(r,`yyyy`),w(r,`MM`),w(r,`dd`));_(a)||(l.error(`Folder does not exist: ${a}`),o.exit(1));let s=b(a,{withFileTypes:!0}).filter(e=>e.isFile()&&n(e.name).toLowerCase()===`.mp4`).map(e=>e.name);return s.length===0&&(l.error(`No .mp4 files found in: ${a}`),o.exit(1)),{folderPath:a,videoPath:i(a,s[0])}}let s=t.find(e=>e.path===a);s||(l.error(`Selected folder not found.`),o.exit(1));let u=i(s.path,s.videoFile);return{folderPath:s.path,videoPath:u}}async function G(){let e=await p({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 K(e){let t=I(e);if(t.length===0){l.error(`No templates found in ${e}`),l.info(`Please add SVG templates to ${e} before proceeding.`);return}let n=await d({message:`Choose a template:`,options:t.map(e=>({label:e,value:e}))});if(typeof n!=`symbol`)return n}function q(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 J(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 Y(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 ${J(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 X(e,t){return await new A({apiKey:t.openaiApiKey}).audio.transcriptions.create({file:g(e),model:t.model,language:t.language,response_format:`srt`})}async function Z(e,t){let n=f();n.start(`Extracting audio...`);try{q(e,t),n.stop(`Audio extracted successfully`)}catch(e){n.stop(`Audio extraction failed`),l.error(`Error during audio extraction: ${e}`),o.exit(1)}}async function ne(e,t,n){let r=f();r.start(`Generating transcription with OpenAI...`);try{C(t,await X(e,n)),r.stop(`Transcription generated successfully`)}catch(t){r.stop(`Transcription generation failed`),l.error(`Error during transcription generation: ${t}`);try{S(e)}catch{}o.exit(1)}}async function re(e,t,n){let r=f(),i=n===`first`?`start`:n===`last`?`end`:`${n}%`;r.start(`Extracting thumbnail at ${i}...`);try{Y(e,t,n),r.stop(`Thumbnail extracted successfully`)}catch(e){r.stop(`Thumbnail extraction failed`),l.error(`Error during thumbnail extraction: ${e}`),o.exit(1)}}async function ie(e,t,n,r,i){let a=f();a.start(`Adding text to thumbnail...`);try{await H(e,t,n,r,i),a.stop(`Text added to thumbnail successfully`)}catch(e){a.stop(`Failed to add text to thumbnail`),l.error(`Error adding text to thumbnail: ${e}`),o.exit(1)}}function Q(e,t=`temporary file`){try{S(e)}catch{l.warn(`Could not delete ${t}: ${e}`)}}async function ae(e){s(`Video Toolkit - Add Template`);let t=await N(),n=a(e);_(n)||(l.error(`Source file not found: ${n}`),o.exit(1)),n.toLowerCase().endsWith(`.svg`)||(l.error(`Source file must be an SVG file`),o.exit(1));let r=await p({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 r==`symbol`&&(l.error(`Template name is required. Exiting.`),o.exit(1));let i=F(t.templatesDir);_(i)||(v(i,{recursive:!0}),l.success(`Created templates directory: ${i}`));let c=a(i,`${r}.svg`);_(c)&&l.warn(`Template '${r}' already exists. Overwriting...`);try{h(n,c),u(`✓ Template '${r}' added successfully at ${c}`)}catch(e){let t=e instanceof Error?e.message:String(e);l.error(`Failed to copy template: ${t}`),o.exit(1)}}const $=ee(`video-toolkit`);async function oe(t){let n=await N();n.openaiApiKey||(l.error(`OpenAI API key is required. Please set it in:`),l.error(` - Config file: ~/.video-toolkitrc`),o.exit(1));let{videoPath:r,audioPath:i,srtPath:a,thumbnailTempPaths:c,thumbnailPaths:d}=P(t);s(`Video Toolkit - Processing: ${e(r)}`),await Z(r,i),await ne(i,a,n),Q(i,`audio file`);let f=await G();if(f){let e=await K(n.templatesDir);if(!e){l.warn(`No template selected. Skipping thumbnail generation.`),u(`✓ Transcription saved to: ${a}`);return}let t=[`first`,`25`,`50`,`75`,`last`];for(let i=0;i<t.length;i++)await re(r,c[i],t[i]),await ie(c[i],d[i],f,e,n.templatesDir),Q(c[i],`temporary thumbnail`);u(`✓ Transcription saved to: ${a}\n✓ Thumbnails saved:\n - ${d.join(`
14
- - `)}`)}else u(`✓ Transcription saved to: ${a}`)}$.command(`[path]`,`Process a video from dated content folders`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(e,t)=>{let n=e||t?.path||o.cwd();s(`Video Toolkit - Process video from content folder`);let{videoPath:r}=await W(n);await oe(r)}),$.command(`add-template <file>`,`Add an SVG template to the templates directory`).action(async e=>{await ae(e)}),$.help(),$.version(j),$.parse();export{};
2
+ import{basename as e,dirname as t,extname as n,isAbsolute as r,join as i,resolve as a}from"node:path";import o from"node:process";import{intro as s,isCancel as c,log as l,outro as u,select as d,spinner as f,text as p}from"@clack/prompts";import{cac as m}from"cac";import{homedir as h}from"node:os";import{loadConfig as g}from"c12";import{copyFileSync as _,createReadStream as ee,existsSync as v,mkdirSync as y,readFileSync as b,readdirSync as x,statSync as S,unlinkSync as C,writeFileSync as w}from"node:fs";import{format as T,parse as E}from"date-fns";import{Buffer as D}from"node:buffer";import{ofetch as O}from"ofetch";import k from"sharp";import{execSync as A}from"node:child_process";import te from"openai";var j=`0.10.0`;const M={openaiApiKey:``,language:`fr`,model:`whisper-1`,templatesDir:i(h(),`.config`,`video-toolkit`,`templates`)};async function N(){let{config:e}=await g({name:`video-toolkit`,defaults:M,globalRc:!0});return{openaiApiKey:e.openaiApiKey||``,language:e.language||`fr`,model:e.model||`whisper-1`,templatesDir:e.templatesDir}}function P(t){let r=a(t),o=e(r,n(r)),s=a(r,`..`),c=i(s,`${o}_audio.mp3`),l=i(s,`${o}.srt`),u=i(s,`thumbnails`);v(u)||y(u,{recursive:!0});let d=[`first`,`25`,`50`,`75`,`last`];return{videoPath:r,audioPath:c,srtPath:l,thumbnailTempPaths:d.map(e=>i(u,`${o}_thumbnail_${e}_temp.png`)),thumbnailPaths:d.map(e=>i(u,`${o}_thumbnail_${e}.png`))}}function F(e){return r(e)?e:a(o.cwd(),e)}function I(e){let t=F(e);if(!v(t))return[];try{return x(t,{withFileTypes:!0}).filter(e=>e.isFile()&&n(e.name).toLowerCase()===`.svg`).map(e=>e.name.replace(/\.svg$/i,``))}catch{return[]}}function L(e,t){return i(F(t),`${e}.svg`)}function R(){let e=i(h(),`.config`,`video-toolkit`,`fonts`);return v(e)||y(e,{recursive:!0}),e}function z(){let e=R(),n=t(e),r=i(n,`fonts.conf`);v(r)||w(r,`<?xml version="1.0"?>
3
+ <!DOCTYPE fontconfig SYSTEM "fonts.dtd">
4
+ <fontconfig>
5
+ <dir>${e}</dir>
6
+ <cachedir>${i(n,`fontcache`)}</cachedir>
7
+ </fontconfig>`,`utf-8`),o.env.FONTCONFIG_PATH=n,o.platform===`darwin`&&(o.env.PANGOCAIRO_BACKEND=`fontconfig`)}async function B(e,t){try{let n=await O(`https://fonts.googleapis.com/css2?family=${e.replace(/\s+/g,`+`)}:wght@${t}&display=swap`),r=n.match(/url\((https:\/\/fonts\.gstatic\.com\/[^)]+)\)/),a=n.match(/format\('([^']+)'\)/);if(!r||!a)return null;let o=r[1],s=a[1],c=await O(o,{responseType:`arrayBuffer`}),l=i(R(),`${e.replace(/\s+/g,``)}-${t}.${s===`woff2`?`woff2`:s===`woff`?`woff`:`ttf`}`);return w(l,D.from(c)),console.warn(`Downloaded and saved "${e}" weight ${t} to ${l}`),l}catch(n){return console.warn(`Could not download font "${e}" weight ${t} from Google Fonts:`,n),null}}function V(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 H(r,i){let a=t(i),o=R(),s=/font-family="([^"]+)"/g,c=new Set,l=s.exec(r);for(;l!==null;)c.add(l[1]),l=s.exec(r);if(c.size===0)return r;for(let t of c){let i=t.replace(/\s+/g,``),s=!1;try{let t=x(a,{withFileTypes:!0});for(let r of t){if(!r.isFile())continue;let t=r.name,a=n(t).toLowerCase();[`.ttf`,`.woff`,`.woff2`].includes(a)&&e(t,a).toLowerCase().includes(i.toLowerCase())&&(s=!0)}}catch{}if(!s)try{let e=x(o,{withFileTypes:!0});for(let t of e)if(t.isFile()&&t.name.toLowerCase().includes(i.toLowerCase())){s=!0;break}}catch{}if(!s){console.warn(`Font "${t}" not found, downloading from Google Fonts...`);let e=V(r,t);for(let n of e)await B(t,n)}}return r}async function U(e,t,n){let i=r(e)?e:L(e,n);if(!v(i))throw Error(`SVG template not found at ${i}. Please ensure the template exists in ${F(n)}.`);let a;try{a=b(i,`utf-8`)}catch(e){let t=e instanceof Error?e.message:String(e);throw Error(`Failed to load SVG template from ${i}: ${t}`)}await H(a,i);let o=t.replace(/&/g,`&amp;`).replace(/</g,`&lt;`).replace(/>/g,`&gt;`).replace(/"/g,`&quot;`).replace(/'/g,`&apos;`);return a.replace(/\{\{text\}\}/g,o)}async function W(e,t,n,r,i){try{z();let a=await U(r,n,i),o=await k(e).metadata();if(!o.width||!o.height)throw Error(`Could not determine image dimensions`);let s=await k(D.from(a),{density:300}).resize(o.width,o.height,{fit:`fill`}).png().toBuffer();await k(e).composite([{input:s,blend:`over`}]).toFile(t)}catch(e){throw console.error(`Error adding text to image:`,e),e}}function G(e=o.cwd()){let t=[];try{let r=x(e).filter(t=>S(i(e,t)).isDirectory()&&/^\d{4}$/.test(t));for(let a of r){let r=i(e,a),o=x(r).filter(e=>S(i(r,e)).isDirectory()&&/^\d{2}$/.test(e));for(let e of o){let o=i(r,e),s=x(o).filter(e=>S(i(o,e)).isDirectory()&&/^\d{2}$/.test(e));for(let r of s){let s=i(o,r),c=x(s,{withFileTypes:!0}).filter(e=>e.isFile()&&n(e.name).toLowerCase()===`.mp4`).map(e=>e.name);if(c.length>0){let n=`${a}-${e}-${r}`;t.push({path:s,date:new Date(n),displayPath:`${a}/${e}/${r}`,videoFile:c[0]})}}}}}catch(e){return l.error(`Error reading directories: ${e}`),[]}return t.sort((e,t)=>t.date.getTime()-e.date.getTime()).slice(0,8)}async function K(e=o.cwd()){let t=G(e);t.length===0&&(l.error(`No dated folders with video files found.`),l.info(`Expected structure: YYYY/MM/DD/*.mp4`),o.exit(1));let r=t.map(e=>({label:`${e.displayPath} (${e.videoFile})`,value:e.path,hint:`Process video from ${e.displayPath}`}));r.push({label:`Custom date`,value:`custom`,hint:`Enter a specific date`});let a=await d({message:`Select a content folder with video:`,options:r});if(c(a)&&(l.error(`Operation cancelled.`),o.exit(0)),a===`custom`){let t=await p({message:`Enter date (YYYY-MM-DD):`,placeholder:T(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=E(e,`yyyy-MM-dd`,new Date);if(Number.isNaN(t.getTime()))return`Invalid date`}catch{return`Invalid date`}}});c(t)&&(l.error(`Operation cancelled.`),o.exit(0));let r=E(t,`yyyy-MM-dd`,new Date),a=i(e,T(r,`yyyy`),T(r,`MM`),T(r,`dd`));v(a)||(l.error(`Folder does not exist: ${a}`),o.exit(1));let s=x(a,{withFileTypes:!0}).filter(e=>e.isFile()&&n(e.name).toLowerCase()===`.mp4`).map(e=>e.name);return s.length===0&&(l.error(`No .mp4 files found in: ${a}`),o.exit(1)),{folderPath:a,videoPath:i(a,s[0])}}let s=t.find(e=>e.path===a);s||(l.error(`Selected folder not found.`),o.exit(1));let u=i(s.path,s.videoFile);return{folderPath:s.path,videoPath:u}}async function q(){let e=await p({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 J(e){let t=I(e);if(t.length===0){l.error(`No templates found in ${e}`),l.info(`Please add SVG templates to ${e} before proceeding.`);return}let n=await d({message:`Choose a template:`,options:t.map(e=>({label:e,value:e}))});if(typeof n!=`symbol`)return n}function Y(e,t){let n=`ffmpeg -i ${e} -q:a 0 -map a ${t}`;try{A(n,{stdio:`inherit`})}catch(e){console.error(`Error extracting audio:`,e)}}function X(e){try{let t=A(`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 Z(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 ${X(e)*(Number.parseInt(n)/100)} -i "${e}" -vframes 1 "${t}" -y`;try{A(r,{stdio:`inherit`})}catch(e){throw console.error(`Error extracting thumbnail:`,e),e}}async function ne(e,t){return await new te({apiKey:t.openaiApiKey}).audio.transcriptions.create({file:ee(e),model:t.model,language:t.language,response_format:`srt`})}async function re(e,t){let n=f();n.start(`Extracting audio...`);try{Y(e,t),n.stop(`Audio extracted successfully`)}catch(e){n.stop(`Audio extraction failed`),l.error(`Error during audio extraction: ${e}`),o.exit(1)}}async function ie(e,t,n){let r=f();r.start(`Generating transcription with OpenAI...`);try{w(t,await ne(e,n)),r.stop(`Transcription generated successfully`)}catch(t){r.stop(`Transcription generation failed`),l.error(`Error during transcription generation: ${t}`);try{C(e)}catch{}o.exit(1)}}async function ae(e,t,n){let r=f(),i=n===`first`?`start`:n===`last`?`end`:`${n}%`;r.start(`Extracting thumbnail at ${i}...`);try{Z(e,t,n),r.stop(`Thumbnail extracted successfully`)}catch(e){r.stop(`Thumbnail extraction failed`),l.error(`Error during thumbnail extraction: ${e}`),o.exit(1)}}async function oe(e,t,n,r,i){let a=f();a.start(`Adding text to thumbnail...`);try{await W(e,t,n,r,i),a.stop(`Text added to thumbnail successfully`)}catch(e){a.stop(`Failed to add text to thumbnail`),l.error(`Error adding text to thumbnail: ${e}`),o.exit(1)}}function Q(e,t=`temporary file`){try{C(e)}catch{l.warn(`Could not delete ${t}: ${e}`)}}async function se(e){s(`Video Toolkit - Add Template`);let t=await N(),n=a(e);v(n)||(l.error(`Source file not found: ${n}`),o.exit(1)),n.toLowerCase().endsWith(`.svg`)||(l.error(`Source file must be an SVG file`),o.exit(1));let r=await p({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 r==`symbol`&&(l.error(`Template name is required. Exiting.`),o.exit(1));let i=F(t.templatesDir);v(i)||(y(i,{recursive:!0}),l.success(`Created templates directory: ${i}`));let c=a(i,`${r}.svg`);v(c)&&l.warn(`Template '${r}' already exists. Overwriting...`);try{_(n,c),u(`✓ Template '${r}' added successfully at ${c}`)}catch(e){let t=e instanceof Error?e.message:String(e);l.error(`Failed to copy template: ${t}`),o.exit(1)}}const $=m(`video-toolkit`);async function ce(t){let n=await N();n.openaiApiKey||(l.error(`OpenAI API key is required. Please set it in:`),l.error(` - Config file: ~/.video-toolkitrc`),o.exit(1));let{videoPath:r,audioPath:i,srtPath:a,thumbnailTempPaths:c,thumbnailPaths:d}=P(t);s(`Video Toolkit - Processing: ${e(r)}`),await re(r,i),await ie(i,a,n),Q(i,`audio file`);let f=await q();if(f){let e=await J(n.templatesDir);if(!e){l.warn(`No template selected. Skipping thumbnail generation.`),u(`✓ Transcription saved to: ${a}`);return}let t=[`first`,`25`,`50`,`75`,`last`];for(let i=0;i<t.length;i++)await ae(r,c[i],t[i]),await oe(c[i],d[i],f,e,n.templatesDir),Q(c[i],`temporary thumbnail`);u(`✓ Transcription saved to: ${a}\n✓ Thumbnails saved:\n - ${d.join(`
8
+ - `)}`)}else u(`✓ Transcription saved to: ${a}`)}$.command(`[path]`,`Process a video from dated content folders`).option(`--path <path>`,`Base path for content directory (defaults to current directory)`).action(async(e,t)=>{let n=e||t?.path||o.cwd();s(`Video Toolkit - Process video from content folder`);let{videoPath:r}=await K(n);await ce(r)}),$.command(`add-template <file>`,`Add an SVG template to the templates directory`).action(async e=>{await se(e)}),$.help(),$.version(j),$.parse();export{};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@barbapapazes/video-toolkit",
3
3
  "type": "module",
4
- "version": "0.9.0",
4
+ "version": "0.10.0",
5
5
  "author": "Estéban Soubiran <esteban@soubiran.dev>",
6
6
  "license": "MIT",
7
7
  "funding": "https://github.com/sponsors/Barbapapazes",