@barbapapazes/video-toolkit 0.8.2 → 0.9.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 +60 -0
- package/dist/cli.mjs +13 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,6 +141,66 @@ Create an SVG file with a `{{text}}` placeholder where you want the text to appe
|
|
|
141
141
|
|
|
142
142
|
The `{{text}}` placeholder will be replaced with the actual text you provide during thumbnail generation. The SVG should match the dimensions of your video frames (typically 1920x1080 for HD video).
|
|
143
143
|
|
|
144
|
+
### Using Custom Fonts
|
|
145
|
+
|
|
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.
|
|
147
|
+
|
|
148
|
+
**Example structure:**
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
~/.config/video-toolkit/templates/
|
|
152
|
+
├── my-template.svg
|
|
153
|
+
├── SofiaSans-Light-300.ttf
|
|
154
|
+
├── SofiaSans-Medium-500.ttf
|
|
155
|
+
└── SofiaSans-Bold-700.ttf
|
|
156
|
+
```
|
|
157
|
+
|
|
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
|
+
**Font file naming conventions:**
|
|
173
|
+
|
|
174
|
+
- `FontName.ttf` → default weight (400)
|
|
175
|
+
- `FontName-300.ttf` or `FontName-Light.ttf` → weight 300
|
|
176
|
+
- `FontName-500.ttf` or `FontName-Medium.ttf` → weight 500
|
|
177
|
+
- `FontName-700.ttf` or `FontName-Bold.ttf` → weight 700
|
|
178
|
+
|
|
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/).
|
|
182
|
+
|
|
183
|
+
**Example:**
|
|
184
|
+
|
|
185
|
+
```xml
|
|
186
|
+
<text font-family="Roboto" font-size="80" font-weight="700">
|
|
187
|
+
{{text}}
|
|
188
|
+
</text>
|
|
189
|
+
```
|
|
190
|
+
|
|
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
|
|
195
|
+
|
|
196
|
+
**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
|
+
|
|
144
204
|
### Managing Multiple Templates
|
|
145
205
|
|
|
146
206
|
You can have multiple templates for different purposes:
|
package/dist/cli.mjs
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{basename as e,
|
|
3
|
-
|
|
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 ee}from"node:os";import{loadConfig as h}from"c12";import{copyFileSync as g,createReadStream as te,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.1`;const M={openaiApiKey:``,language:`fr`,model:`whisper-1`,templatesDir:i(ee(),`.config`,`video-toolkit`,`templates`)};async function N(){let{config:e}=await h({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`]}function B(e){return/<style[^>]*>[\s\S]*@font-face[\s\S]*<\/style>/.test(e)}async function V(r,a){if(B(r))return r;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=``,d=!1;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)d=!0;else{console.warn(`Font "${t}" not found locally, downloading 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(`Downloaded "${t}" weight ${n} from Google Fonts`),d=!0)}}}if(!u)return r;let f=r;if(r.includes(`<defs>`)?f=r.replace(`<defs>`,`<defs><style>${u}\n </style>`):r.includes(`<svg`)&&(f=r.replace(`<svg`,`<svg><defs><style>${u}\n </style></defs>`).replace(`><defs>`,`>
|
|
13
|
+
<defs>`)),d&&f!==r)try{C(a,f,`utf-8`),console.warn(`Updated template file with embedded fonts: ${a}`)}catch(e){console.warn(`Could not update template file ${a}:`,e)}return f}async function H(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}`)}a=await V(a,i);let o=t.replace(/&/g,`&`).replace(/</g,`<`).replace(/>/g,`>`).replace(/"/g,`"`).replace(/'/g,`'`);return a.replace(/\{\{text\}\}/g,o)}async function U(e,t,n,r,i){try{o.platform===`darwin`&&!o.env.PANGOCAIRO_BACKEND&&(o.env.PANGOCAIRO_BACKEND=`fontconfig`);let a=await H(r,n,i),s=await O(e).metadata();if(!s.width||!s.height)throw Error(`Could not determine image dimensions`);let c=await O(E.from(a),{density:300}).resize(s.width,s.height,{fit:`fill`}).png().toBuffer();await O(e).composite([{input:c,blend:`over`}]).toFile(t)}catch(e){throw console.error(`Error adding text to image:`,e),e}}function W(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 G(e=o.cwd()){let t=W(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 K(){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 q(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 J(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 Y(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 X(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 ${Y(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 Z(e,t){return await new A({apiKey:t.openaiApiKey}).audio.transcriptions.create({file:te(e),model:t.model,language:t.language,response_format:`srt`})}async function ne(e,t){let n=f();n.start(`Extracting audio...`);try{J(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 re(e,t,n){let r=f();r.start(`Generating transcription with OpenAI...`);try{C(t,await Z(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 ie(e,t,n){let r=f(),i=n===`first`?`start`:n===`last`?`end`:`${n}%`;r.start(`Extracting thumbnail at ${i}...`);try{X(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 ae(e,t,n,r,i){let a=f();a.start(`Adding text to thumbnail...`);try{await U(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 oe(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{g(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 se(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 ne(r,i),await re(i,a,n),Q(i,`audio file`);let f=await K();if(f){let e=await q(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 ie(r,c[i],t[i]),await ae(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 G(n);await se(r)}),$.command(`add-template <file>`,`Add an SVG template to the templates directory`).action(async e=>{await oe(e)}),$.help(),$.version(j),$.parse();export{};
|
package/package.json
CHANGED