diff --git a/README.md b/README.md index 0e9ed4df7..dc6f7e9c1 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,22 @@ OpenScreen is 100% free for personal and commercial use. Use it, modify it, dist - Add annotations (text, arrows, images). - Trim sections of the clip. - Customize speed at different segments. +- **Webcam Focus** — draw attention to your face at key moments. - Export in different aspect ratios and resolutions. +### Webcam Focus + +When you record with a webcam, you can mark specific time ranges on the timeline where the webcam should take center stage. During those segments the screen recording blurs and dims while the webcam expands to fill most of the frame. Outside the region everything returns to the normal layout. Both transitions animate smoothly. + +**How to use it:** +1. Make sure your recording includes a webcam feed. +2. In the editor, click the **camera icon** (🎥) in the timeline toolbar to place a Webcam Focus region at the current playhead position. +3. Drag the edges of the indigo region to set the start and end times. +4. Press **Play** to preview — the webcam enlarges to portrait near-full-screen and the screen recording fades behind it. +5. Delete a region by selecting it and pressing `Delete` / `Backspace`. + +The effect is saved with your project and included in the exported video. + ## Installation Download the latest installer for your platform from the [GitHub Releases](https://github.com/siddharthvaddem/openscreen/releases) page. diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index 573aee8af..83442b9bd 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -137,6 +137,14 @@ interface Window { setHasUnsavedChanges: (hasChanges: boolean) => void; onRequestSaveBeforeClose: (callback: () => Promise | boolean) => () => void; setLocale: (locale: string) => Promise; + generateSubtitles: ( + videoPath: string, + lang?: string, + ) => Promise<{ + success: boolean; + subtitles?: Array<{ id: string; startMs: number; endMs: number; text: string }>; + error?: string; + }>; }; } diff --git a/electron/ipc/handlers.ts b/electron/ipc/handlers.ts index 78d83448a..b6fc5aa87 100644 --- a/electron/ipc/handlers.ts +++ b/electron/ipc/handlers.ts @@ -1,3 +1,4 @@ +import { execFile } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; @@ -781,4 +782,65 @@ export function registerIpcHandlers( return { success: false, error: String(error) }; } }); + + ipcMain.handle("generate-subtitles", async (_, videoPath: string, lang = "pt") => { + const scriptPath = path.join(app.getAppPath(), "scripts", "extract-subtitles.mjs"); + try { + await fs.access(scriptPath); + } catch { + return { success: false, error: "extract-subtitles.mjs script not found" }; + } + + const nodeBin = + process.env.NODE_BINARY || + (process.platform === "win32" ? "node.exe" : "node"); + + return new Promise<{ + success: boolean; + subtitles?: Array<{ id: string; startMs: number; endMs: number; text: string }>; + error?: string; + }>((resolve) => { + const child = execFile( + nodeBin, + [scriptPath, videoPath, "--json", "--lang", lang], + { + maxBuffer: 10 * 1024 * 1024, + timeout: 300_000, + cwd: app.getAppPath(), + }, + (error, stdout, stderr) => { + if (error) { + console.error("Subtitle generation error:", stderr || error.message); + resolve({ success: false, error: error.message }); + return; + } + try { + const jsonStart = stdout.indexOf("{"); + const jsonEnd = stdout.lastIndexOf("}"); + if (jsonStart === -1 || jsonEnd === -1) { + resolve({ success: false, error: "No JSON output from script" }); + return; + } + const config = JSON.parse(stdout.slice(jsonStart, jsonEnd + 1)); + const items = config?.subtitles?.items ?? []; + const FPS = 30; + const subtitles = items.map( + (item: { text: string; startFrame: number; endFrame: number }, idx: number) => ({ + id: `sub-${idx + 1}`, + startMs: Math.round((item.startFrame / FPS) * 1000), + endMs: Math.round((item.endFrame / FPS) * 1000), + text: item.text, + }), + ); + resolve({ success: true, subtitles }); + } catch (parseError) { + resolve({ success: false, error: `Failed to parse output: ${String(parseError)}` }); + } + }, + ); + child.stderr?.on("data", (data) => { + console.log("[subtitle-gen]", String(data).trim()); + }); + }); + }); } diff --git a/electron/preload.ts b/electron/preload.ts index 8f1836bd8..97042c1fd 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -118,6 +118,9 @@ contextBridge.exposeInMainWorld("electronAPI", { setLocale: (locale: string) => { return ipcRenderer.invoke("set-locale", locale); }, + generateSubtitles: (videoPath: string, lang?: string) => { + return ipcRenderer.invoke("generate-subtitles", videoPath, lang ?? "pt"); + }, setMicrophoneExpanded: (expanded: boolean) => { ipcRenderer.send("hud:setMicrophoneExpanded", expanded); }, diff --git a/package-lock.json b/package-lock.json index 70e33952e..06bd6b365 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "openscreen", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openscreen", - "version": "1.2.0", + "version": "1.3.0", "dependencies": { "@fix-webm-duration/fix": "^1.0.1", "@pixi/filter-drop-shadow": "^5.2.0", @@ -60,6 +60,7 @@ "@types/react-dom": "^18.2.21", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.2.1", + "@xenova/transformers": "^2.17.2", "autoprefixer": "^10.4.21", "electron": "^39.2.7", "electron-builder": "^26.7.0", @@ -76,7 +77,8 @@ "vite": "^5.1.6", "vite-plugin-electron": "^0.28.6", "vite-plugin-electron-renderer": "^0.14.5", - "vitest": "^4.0.16" + "vitest": "^4.0.16", + "wavefile": "^11.0.0" }, "engines": { "node": "22.22.1", @@ -2132,6 +2134,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@huggingface/jinja": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz", + "integrity": "sha512-/KPde26khDUIPkTGU82jdtTW9UAuvUTumCAbFs/7giR0SxsvZC4hru51PBvpijH6BVkHcROcvZM/lpy5h1jRRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -3205,6 +3217,80 @@ "node": ">=18" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -4770,6 +4856,13 @@ "@types/node": "*" } }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -5036,6 +5129,21 @@ "integrity": "sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==", "license": "BSD-3-Clause" }, + "node_modules/@xenova/transformers": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/@xenova/transformers/-/transformers-2.17.2.tgz", + "integrity": "sha512-lZmHqzrVIkSvZdKZEx7IYY51TK0WDrC8eR0c5IMnBsO8di8are1zzw8BlLhyO2TklZKLN5UffNGs1IJwT6oOqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@huggingface/jinja": "^0.2.2", + "onnxruntime-web": "1.14.0", + "sharp": "^0.32.0" + }, + "optionalDependencies": { + "onnxruntime-node": "1.14.0" + } + }, "node_modules/@xmldom/xmldom": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", @@ -5839,6 +5947,103 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", + "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz", + "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6571,6 +6776,20 @@ "node": ">=0.10.0" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -6589,6 +6808,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -6881,6 +7111,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/defaults": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", @@ -7826,12 +8066,32 @@ "node": ">=0.8.x" } }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, "node_modules/exif-parser": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", "dev": true }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -7918,6 +8178,13 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -8047,6 +8314,13 @@ "integrity": "sha512-IKlE+pNvL2R+kVL1kEhUYqRxVqeFnjiIvHWDMLFXNaqyUdFXQM2wte44EfMYJNHkW16X991t2Zg8apKkhv7OBA==", "license": "MIT" }, + "node_modules/flatbuffers": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-1.12.0.tgz", + "integrity": "sha512-c7CZADjRcl6j0PlvFy0ZqXQ67qSEZfrVPynmnL+2zPc+NtMvrF8Y0QceMo7QqnSPc7+uWjUIAbvCQ5WIKlMVdQ==", + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt" + }, "node_modules/follow-redirects": { "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", @@ -8152,6 +8426,13 @@ } } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, "node_modules/fs-extra": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", @@ -8389,6 +8670,13 @@ "omggif": "^1.0.10" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "dev": true, + "license": "MIT" + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8567,6 +8855,13 @@ "integrity": "sha512-QL7MJ2WMjm1PHWsoFrAQH/J8wUeqZvMtHO58qdekHpCfhvhSL4gSiz6vJf5EeMP0LOn3ZCprL2ki/gjED8ghVw==", "license": "Standard 'no charge' license: https://gsap.com/standard-license." }, + "node_modules/guid-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/guid-typescript/-/guid-typescript-1.0.9.tgz", + "integrity": "sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==", + "dev": true, + "license": "ISC" + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -8981,6 +9276,13 @@ "dev": true, "license": "ISC" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, "node_modules/invert-kv": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", @@ -10026,6 +10328,13 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -10587,6 +10896,13 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, "node_modules/motion": { "version": "12.23.24", "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz", @@ -10683,6 +10999,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "dev": true, + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -10970,6 +11293,54 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/onnx-proto": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/onnx-proto/-/onnx-proto-4.0.4.tgz", + "integrity": "sha512-aldMOB3HRoo6q/phyB6QRQxSt895HNNw82BNyZ2CMh4bjeKv7g/c+VpAFtJuEMVfYLMbRx61hbuqnKceLeDcDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "protobufjs": "^6.8.8" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.14.0.tgz", + "integrity": "sha512-3LJpegM2iMNRX2wUmtYfeX/ytfOzNwAWKSq1HbRrKc9+uqG/FsEA0bbKZl1btQeZaXhC26l44NWpNUeXPII7Ew==", + "dev": true, + "license": "MIT" + }, + "node_modules/onnxruntime-node": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.14.0.tgz", + "integrity": "sha512-5ba7TWomIV/9b6NH/1x/8QEeowsb+jBEvFzU6z0T4mNsFwdPqXeFUM7uxC6QeSRkEbWu3qEB0VMjrvzN/0S9+w==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "onnxruntime-common": "~1.14.0" + } + }, + "node_modules/onnxruntime-web": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/onnxruntime-web/-/onnxruntime-web-1.14.0.tgz", + "integrity": "sha512-Kcqf43UMfW8mCydVGcX9OMXI2VN17c0p6XvR7IPSZzBf/6lteBzXHvcEVWDPmCKuGombl997HgLqj91F11DzXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatbuffers": "^1.12.0", + "guid-typescript": "^1.0.9", + "long": "^4.0.0", + "onnx-proto": "^4.0.4", + "onnxruntime-common": "~1.14.0", + "platform": "^1.3.6" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -11471,6 +11842,13 @@ "url": "https://opencollective.com/pixijs" } }, + "node_modules/platform": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz", + "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", + "dev": true, + "license": "MIT" + }, "node_modules/playwright": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", @@ -11733,6 +12111,71 @@ "node": "^12.20.0 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prebuild-install/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/prebuild-install/node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/prebuild-install/node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", @@ -11859,6 +12302,33 @@ "dev": true, "license": "ISC" }, + "node_modules/protobufjs": { + "version": "6.11.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.5.tgz", + "integrity": "sha512-OKjVH3hDoXdIZ/s5MLv8O2X0s+wOxGfV7ar6WFSKGaSAxi/6gYn3px5POS4vi+mc/0zCOdL7Jkwrj0oT1Yst2A==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/long": "^4.0.1", + "@types/node": ">=13.7.0", + "long": "^4.0.0" + }, + "bin": { + "pbjs": "bin/pbjs", + "pbts": "bin/pbts" + } + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -11959,6 +12429,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, "node_modules/re-resizable": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.11.2.tgz", @@ -12787,6 +13273,37 @@ "dev": true, "license": "ISC" }, + "node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/sharp/node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -12903,6 +13420,70 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", + "dev": true, + "license": "MIT" + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -13150,6 +13731,18 @@ "dev": true, "license": "MIT" }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -13250,6 +13843,16 @@ "node": ">=8" } }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strtok3": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz", @@ -13596,6 +14199,49 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar-stream/node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/tar/node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -13603,6 +14249,16 @@ "dev": true, "license": "ISC" }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", @@ -13722,6 +14378,31 @@ "dev": true, "license": "MIT" }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-decoder/node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -14991,6 +15672,19 @@ "node": ">=18" } }, + "node_modules/wavefile": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/wavefile/-/wavefile-11.0.0.tgz", + "integrity": "sha512-/OBiAALgWU24IG7sC84cDO/KfFuvajWc5Uec0oV2zrpOOZZDgGdOwHwgEzOrwh8jkubBk7PtZfQBIcI1OaE5Ng==", + "dev": true, + "license": "MIT", + "bin": { + "wavefile": "bin/wavefile.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", diff --git a/package.json b/package.json index c367f9ed1..5e90b0a4a 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@types/react-dom": "^18.2.21", "@types/uuid": "^10.0.0", "@vitejs/plugin-react": "^4.2.1", + "@xenova/transformers": "^2.17.2", "autoprefixer": "^10.4.21", "electron": "^39.2.7", "electron-builder": "^26.7.0", @@ -94,7 +95,8 @@ "vite": "^5.1.6", "vite-plugin-electron": "^0.28.6", "vite-plugin-electron-renderer": "^0.14.5", - "vitest": "^4.0.16" + "vitest": "^4.0.16", + "wavefile": "^11.0.0" }, "main": "dist-electron/main.js", "lint-staged": { diff --git a/public/Saira_Stencil/OFL.txt b/public/Saira_Stencil/OFL.txt new file mode 100644 index 000000000..26d124f13 --- /dev/null +++ b/public/Saira_Stencil/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Saira Project Authors (https://github.com/Omnibus-Type/Saira) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/Saira_Stencil/README.txt b/public/Saira_Stencil/README.txt new file mode 100644 index 000000000..a4f6e7757 --- /dev/null +++ b/public/Saira_Stencil/README.txt @@ -0,0 +1,190 @@ +Saira Stencil Variable Font +=========================== + +This download contains Saira Stencil as both variable fonts and static fonts. + +Saira Stencil is a variable font with these axes: + wdth + wght + +This means all the styles are contained in these files: + SairaStencil-VariableFont_wdth,wght.ttf + SairaStencil-Italic-VariableFont_wdth,wght.ttf + +If your app fully supports variable fonts, you can now pick intermediate styles +that aren’t available as static fonts. Not all apps support variable fonts, and +in those cases you can use the static font files for Saira Stencil: + static/SairaStencil_UltraCondensed-Thin.ttf + static/SairaStencil_UltraCondensed-ExtraLight.ttf + static/SairaStencil_UltraCondensed-Light.ttf + static/SairaStencil_UltraCondensed-Regular.ttf + static/SairaStencil_UltraCondensed-Medium.ttf + static/SairaStencil_UltraCondensed-SemiBold.ttf + static/SairaStencil_UltraCondensed-Bold.ttf + static/SairaStencil_UltraCondensed-ExtraBold.ttf + static/SairaStencil_UltraCondensed-Black.ttf + static/SairaStencil_ExtraCondensed-Thin.ttf + static/SairaStencil_ExtraCondensed-ExtraLight.ttf + static/SairaStencil_ExtraCondensed-Light.ttf + static/SairaStencil_ExtraCondensed-Regular.ttf + static/SairaStencil_ExtraCondensed-Medium.ttf + static/SairaStencil_ExtraCondensed-SemiBold.ttf + static/SairaStencil_ExtraCondensed-Bold.ttf + static/SairaStencil_ExtraCondensed-ExtraBold.ttf + static/SairaStencil_ExtraCondensed-Black.ttf + static/SairaStencil_Condensed-Thin.ttf + static/SairaStencil_Condensed-ExtraLight.ttf + static/SairaStencil_Condensed-Light.ttf + static/SairaStencil_Condensed-Regular.ttf + static/SairaStencil_Condensed-Medium.ttf + static/SairaStencil_Condensed-SemiBold.ttf + static/SairaStencil_Condensed-Bold.ttf + static/SairaStencil_Condensed-ExtraBold.ttf + static/SairaStencil_Condensed-Black.ttf + static/SairaStencil_SemiCondensed-Thin.ttf + static/SairaStencil_SemiCondensed-ExtraLight.ttf + static/SairaStencil_SemiCondensed-Light.ttf + static/SairaStencil_SemiCondensed-Regular.ttf + static/SairaStencil_SemiCondensed-Medium.ttf + static/SairaStencil_SemiCondensed-SemiBold.ttf + static/SairaStencil_SemiCondensed-Bold.ttf + static/SairaStencil_SemiCondensed-ExtraBold.ttf + static/SairaStencil_SemiCondensed-Black.ttf + static/SairaStencil-Thin.ttf + static/SairaStencil-ExtraLight.ttf + static/SairaStencil-Light.ttf + static/SairaStencil-Regular.ttf + static/SairaStencil-Medium.ttf + static/SairaStencil-SemiBold.ttf + static/SairaStencil-Bold.ttf + static/SairaStencil-ExtraBold.ttf + static/SairaStencil-Black.ttf + static/SairaStencil_SemiExpanded-Thin.ttf + static/SairaStencil_SemiExpanded-ExtraLight.ttf + static/SairaStencil_SemiExpanded-Light.ttf + static/SairaStencil_SemiExpanded-Regular.ttf + static/SairaStencil_SemiExpanded-Medium.ttf + static/SairaStencil_SemiExpanded-SemiBold.ttf + static/SairaStencil_SemiExpanded-Bold.ttf + static/SairaStencil_SemiExpanded-ExtraBold.ttf + static/SairaStencil_SemiExpanded-Black.ttf + static/SairaStencil_Expanded-Thin.ttf + static/SairaStencil_Expanded-ExtraLight.ttf + static/SairaStencil_Expanded-Light.ttf + static/SairaStencil_Expanded-Regular.ttf + static/SairaStencil_Expanded-Medium.ttf + static/SairaStencil_Expanded-SemiBold.ttf + static/SairaStencil_Expanded-Bold.ttf + static/SairaStencil_Expanded-ExtraBold.ttf + static/SairaStencil_Expanded-Black.ttf + static/SairaStencil_UltraCondensed-ThinItalic.ttf + static/SairaStencil_UltraCondensed-ExtraLightItalic.ttf + static/SairaStencil_UltraCondensed-LightItalic.ttf + static/SairaStencil_UltraCondensed-Italic.ttf + static/SairaStencil_UltraCondensed-MediumItalic.ttf + static/SairaStencil_UltraCondensed-SemiBoldItalic.ttf + static/SairaStencil_UltraCondensed-BoldItalic.ttf + static/SairaStencil_UltraCondensed-ExtraBoldItalic.ttf + static/SairaStencil_UltraCondensed-BlackItalic.ttf + static/SairaStencil_ExtraCondensed-ThinItalic.ttf + static/SairaStencil_ExtraCondensed-ExtraLightItalic.ttf + static/SairaStencil_ExtraCondensed-LightItalic.ttf + static/SairaStencil_ExtraCondensed-Italic.ttf + static/SairaStencil_ExtraCondensed-MediumItalic.ttf + static/SairaStencil_ExtraCondensed-SemiBoldItalic.ttf + static/SairaStencil_ExtraCondensed-BoldItalic.ttf + static/SairaStencil_ExtraCondensed-ExtraBoldItalic.ttf + static/SairaStencil_ExtraCondensed-BlackItalic.ttf + static/SairaStencil_Condensed-ThinItalic.ttf + static/SairaStencil_Condensed-ExtraLightItalic.ttf + static/SairaStencil_Condensed-LightItalic.ttf + static/SairaStencil_Condensed-Italic.ttf + static/SairaStencil_Condensed-MediumItalic.ttf + static/SairaStencil_Condensed-SemiBoldItalic.ttf + static/SairaStencil_Condensed-BoldItalic.ttf + static/SairaStencil_Condensed-ExtraBoldItalic.ttf + static/SairaStencil_Condensed-BlackItalic.ttf + static/SairaStencil_SemiCondensed-ThinItalic.ttf + static/SairaStencil_SemiCondensed-ExtraLightItalic.ttf + static/SairaStencil_SemiCondensed-LightItalic.ttf + static/SairaStencil_SemiCondensed-Italic.ttf + static/SairaStencil_SemiCondensed-MediumItalic.ttf + static/SairaStencil_SemiCondensed-SemiBoldItalic.ttf + static/SairaStencil_SemiCondensed-BoldItalic.ttf + static/SairaStencil_SemiCondensed-ExtraBoldItalic.ttf + static/SairaStencil_SemiCondensed-BlackItalic.ttf + static/SairaStencil-ThinItalic.ttf + static/SairaStencil-ExtraLightItalic.ttf + static/SairaStencil-LightItalic.ttf + static/SairaStencil-Italic.ttf + static/SairaStencil-MediumItalic.ttf + static/SairaStencil-SemiBoldItalic.ttf + static/SairaStencil-BoldItalic.ttf + static/SairaStencil-ExtraBoldItalic.ttf + static/SairaStencil-BlackItalic.ttf + static/SairaStencil_SemiExpanded-ThinItalic.ttf + static/SairaStencil_SemiExpanded-ExtraLightItalic.ttf + static/SairaStencil_SemiExpanded-LightItalic.ttf + static/SairaStencil_SemiExpanded-Italic.ttf + static/SairaStencil_SemiExpanded-MediumItalic.ttf + static/SairaStencil_SemiExpanded-SemiBoldItalic.ttf + static/SairaStencil_SemiExpanded-BoldItalic.ttf + static/SairaStencil_SemiExpanded-ExtraBoldItalic.ttf + static/SairaStencil_SemiExpanded-BlackItalic.ttf + static/SairaStencil_Expanded-ThinItalic.ttf + static/SairaStencil_Expanded-ExtraLightItalic.ttf + static/SairaStencil_Expanded-LightItalic.ttf + static/SairaStencil_Expanded-Italic.ttf + static/SairaStencil_Expanded-MediumItalic.ttf + static/SairaStencil_Expanded-SemiBoldItalic.ttf + static/SairaStencil_Expanded-BoldItalic.ttf + static/SairaStencil_Expanded-ExtraBoldItalic.ttf + static/SairaStencil_Expanded-BlackItalic.ttf + +Get started +----------- + +1. Install the font files you want to use + +2. Use your app's font picker to view the font family and all the +available styles + +Learn more about variable fonts +------------------------------- + + https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts + https://variablefonts.typenetwork.com + https://medium.com/variable-fonts + +In desktop apps + + https://theblog.adobe.com/can-variable-fonts-illustrator-cc + https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts + +Online + + https://developers.google.com/fonts/docs/getting_started + https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide + https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts + +Installing fonts + + MacOS: https://support.apple.com/en-us/HT201749 + Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux + Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows + +Android Apps + + https://developers.google.com/fonts/docs/android + https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts + +License +------- +Please read the full license text (OFL.txt) to understand the permissions, +restrictions and requirements for usage, redistribution, and modification. + +You can use them in your products & projects – print or digital, +commercial or otherwise. + +This isn't legal advice, please consider consulting a lawyer and see the full +license for all details. diff --git a/public/Saira_Stencil/SairaStencil-Italic-VariableFont_wdth,wght.ttf b/public/Saira_Stencil/SairaStencil-Italic-VariableFont_wdth,wght.ttf new file mode 100644 index 000000000..1210d6893 Binary files /dev/null and b/public/Saira_Stencil/SairaStencil-Italic-VariableFont_wdth,wght.ttf differ diff --git a/public/Saira_Stencil/SairaStencil-VariableFont_wdth,wght.ttf b/public/Saira_Stencil/SairaStencil-VariableFont_wdth,wght.ttf new file mode 100644 index 000000000..571e8860b Binary files /dev/null and b/public/Saira_Stencil/SairaStencil-VariableFont_wdth,wght.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-Black.ttf b/public/Saira_Stencil/static/SairaStencil-Black.ttf new file mode 100644 index 000000000..d0b21a78f Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-Black.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-BlackItalic.ttf b/public/Saira_Stencil/static/SairaStencil-BlackItalic.ttf new file mode 100644 index 000000000..816856dc4 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-BlackItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-Bold.ttf b/public/Saira_Stencil/static/SairaStencil-Bold.ttf new file mode 100644 index 000000000..70d1dac46 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-Bold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-BoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil-BoldItalic.ttf new file mode 100644 index 000000000..68b8828d5 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-BoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-ExtraBold.ttf b/public/Saira_Stencil/static/SairaStencil-ExtraBold.ttf new file mode 100644 index 000000000..f466000a3 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-ExtraBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-ExtraBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil-ExtraBoldItalic.ttf new file mode 100644 index 000000000..59c0d7026 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-ExtraBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-ExtraLight.ttf b/public/Saira_Stencil/static/SairaStencil-ExtraLight.ttf new file mode 100644 index 000000000..ade3743ef Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-ExtraLight.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-ExtraLightItalic.ttf b/public/Saira_Stencil/static/SairaStencil-ExtraLightItalic.ttf new file mode 100644 index 000000000..447350053 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-ExtraLightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-Italic.ttf b/public/Saira_Stencil/static/SairaStencil-Italic.ttf new file mode 100644 index 000000000..3c1d3b9db Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-Italic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-Light.ttf b/public/Saira_Stencil/static/SairaStencil-Light.ttf new file mode 100644 index 000000000..3c26433cc Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-Light.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-LightItalic.ttf b/public/Saira_Stencil/static/SairaStencil-LightItalic.ttf new file mode 100644 index 000000000..b57cdf736 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-LightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-Medium.ttf b/public/Saira_Stencil/static/SairaStencil-Medium.ttf new file mode 100644 index 000000000..fac70adc7 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-Medium.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-MediumItalic.ttf b/public/Saira_Stencil/static/SairaStencil-MediumItalic.ttf new file mode 100644 index 000000000..7755faabb Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-MediumItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-Regular.ttf b/public/Saira_Stencil/static/SairaStencil-Regular.ttf new file mode 100644 index 000000000..d6855bea2 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-Regular.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-SemiBold.ttf b/public/Saira_Stencil/static/SairaStencil-SemiBold.ttf new file mode 100644 index 000000000..c6d4edaf8 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-SemiBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-SemiBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil-SemiBoldItalic.ttf new file mode 100644 index 000000000..decf7e8ae Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-SemiBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-Thin.ttf b/public/Saira_Stencil/static/SairaStencil-Thin.ttf new file mode 100644 index 000000000..4062d581e Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-Thin.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil-ThinItalic.ttf b/public/Saira_Stencil/static/SairaStencil-ThinItalic.ttf new file mode 100644 index 000000000..6b51a67b8 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil-ThinItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-Black.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-Black.ttf new file mode 100644 index 000000000..ce53e7e96 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-Black.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-BlackItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-BlackItalic.ttf new file mode 100644 index 000000000..cff9c1af6 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-BlackItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-Bold.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-Bold.ttf new file mode 100644 index 000000000..92333931f Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-Bold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-BoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-BoldItalic.ttf new file mode 100644 index 000000000..77a682b5f Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-BoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraBold.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraBold.ttf new file mode 100644 index 000000000..796f8ff19 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraBoldItalic.ttf new file mode 100644 index 000000000..fe2a093e7 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraLight.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraLight.ttf new file mode 100644 index 000000000..f5ae16d39 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraLight.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraLightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraLightItalic.ttf new file mode 100644 index 000000000..7f0d948dc Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-ExtraLightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-Italic.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-Italic.ttf new file mode 100644 index 000000000..ef8d4b14f Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-Italic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-Light.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-Light.ttf new file mode 100644 index 000000000..2a5ec4c1c Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-Light.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-LightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-LightItalic.ttf new file mode 100644 index 000000000..76877ff4f Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-LightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-Medium.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-Medium.ttf new file mode 100644 index 000000000..7daa64570 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-Medium.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-MediumItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-MediumItalic.ttf new file mode 100644 index 000000000..de9b6efc3 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-MediumItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-Regular.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-Regular.ttf new file mode 100644 index 000000000..633adc087 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-Regular.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-SemiBold.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-SemiBold.ttf new file mode 100644 index 000000000..8ac37fc0b Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-SemiBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-SemiBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-SemiBoldItalic.ttf new file mode 100644 index 000000000..d0b887277 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-SemiBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-Thin.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-Thin.ttf new file mode 100644 index 000000000..22685e2e4 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-Thin.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Condensed-ThinItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Condensed-ThinItalic.ttf new file mode 100644 index 000000000..80bab7a56 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Condensed-ThinItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-Black.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-Black.ttf new file mode 100644 index 000000000..f48f38a1b Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-Black.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-BlackItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-BlackItalic.ttf new file mode 100644 index 000000000..aafe2b318 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-BlackItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-Bold.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-Bold.ttf new file mode 100644 index 000000000..34290d283 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-Bold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-BoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-BoldItalic.ttf new file mode 100644 index 000000000..88c8962e8 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-BoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraBold.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraBold.ttf new file mode 100644 index 000000000..3ea9140ac Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraBoldItalic.ttf new file mode 100644 index 000000000..1c3f152de Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraLight.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraLight.ttf new file mode 100644 index 000000000..f81e63d58 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraLight.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraLightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraLightItalic.ttf new file mode 100644 index 000000000..30633007c Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-ExtraLightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-Italic.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-Italic.ttf new file mode 100644 index 000000000..c040b8707 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-Italic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-Light.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-Light.ttf new file mode 100644 index 000000000..d5d2d92e4 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-Light.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-LightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-LightItalic.ttf new file mode 100644 index 000000000..a312c0e51 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-LightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-Medium.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-Medium.ttf new file mode 100644 index 000000000..22d6ef4f3 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-Medium.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-MediumItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-MediumItalic.ttf new file mode 100644 index 000000000..4bd9e6882 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-MediumItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-Regular.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-Regular.ttf new file mode 100644 index 000000000..c7594f05a Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-Regular.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-SemiBold.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-SemiBold.ttf new file mode 100644 index 000000000..f86892555 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-SemiBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-SemiBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-SemiBoldItalic.ttf new file mode 100644 index 000000000..310076ea3 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-SemiBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-Thin.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-Thin.ttf new file mode 100644 index 000000000..95f6e8a3a Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-Thin.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_Expanded-ThinItalic.ttf b/public/Saira_Stencil/static/SairaStencil_Expanded-ThinItalic.ttf new file mode 100644 index 000000000..9153c32db Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_Expanded-ThinItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Black.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Black.ttf new file mode 100644 index 000000000..4d994c14e Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Black.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-BlackItalic.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-BlackItalic.ttf new file mode 100644 index 000000000..6f53751e9 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-BlackItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Bold.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Bold.ttf new file mode 100644 index 000000000..02074257f Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Bold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-BoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-BoldItalic.ttf new file mode 100644 index 000000000..4031db594 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-BoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraBold.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraBold.ttf new file mode 100644 index 000000000..52a21d3c8 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraBoldItalic.ttf new file mode 100644 index 000000000..bb079267e Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraLight.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraLight.ttf new file mode 100644 index 000000000..bb4d1921d Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraLight.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraLightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraLightItalic.ttf new file mode 100644 index 000000000..66a061274 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ExtraLightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Italic.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Italic.ttf new file mode 100644 index 000000000..c3d184a96 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Italic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Light.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Light.ttf new file mode 100644 index 000000000..ac4ed08d2 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Light.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-LightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-LightItalic.ttf new file mode 100644 index 000000000..63705bfb4 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-LightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Medium.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Medium.ttf new file mode 100644 index 000000000..fefb43d58 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Medium.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-MediumItalic.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-MediumItalic.ttf new file mode 100644 index 000000000..164554f43 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-MediumItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Regular.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Regular.ttf new file mode 100644 index 000000000..e458ddadd Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Regular.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-SemiBold.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-SemiBold.ttf new file mode 100644 index 000000000..a06700a56 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-SemiBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-SemiBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-SemiBoldItalic.ttf new file mode 100644 index 000000000..a651b56d9 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-SemiBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Thin.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Thin.ttf new file mode 100644 index 000000000..d966c52c9 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-Thin.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ThinItalic.ttf b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ThinItalic.ttf new file mode 100644 index 000000000..cb9f6c2a6 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_ExtraCondensed-ThinItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Black.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Black.ttf new file mode 100644 index 000000000..5df30f64a Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Black.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-BlackItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-BlackItalic.ttf new file mode 100644 index 000000000..e16fc61f3 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-BlackItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Bold.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Bold.ttf new file mode 100644 index 000000000..8e3883725 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Bold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-BoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-BoldItalic.ttf new file mode 100644 index 000000000..925863058 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-BoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraBold.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraBold.ttf new file mode 100644 index 000000000..166a1affd Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraBoldItalic.ttf new file mode 100644 index 000000000..3df57302c Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraLight.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraLight.ttf new file mode 100644 index 000000000..da2586d95 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraLight.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraLightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraLightItalic.ttf new file mode 100644 index 000000000..9c4769a91 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ExtraLightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Italic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Italic.ttf new file mode 100644 index 000000000..5a2379657 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Italic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Light.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Light.ttf new file mode 100644 index 000000000..e271744c3 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Light.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-LightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-LightItalic.ttf new file mode 100644 index 000000000..eae2d1ebd Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-LightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Medium.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Medium.ttf new file mode 100644 index 000000000..45a510b57 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Medium.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-MediumItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-MediumItalic.ttf new file mode 100644 index 000000000..a7b47e9d7 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-MediumItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Regular.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Regular.ttf new file mode 100644 index 000000000..bd24220ec Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Regular.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-SemiBold.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-SemiBold.ttf new file mode 100644 index 000000000..c45ee7bf1 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-SemiBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-SemiBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-SemiBoldItalic.ttf new file mode 100644 index 000000000..786c06e23 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-SemiBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Thin.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Thin.ttf new file mode 100644 index 000000000..256eae69b Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-Thin.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ThinItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ThinItalic.ttf new file mode 100644 index 000000000..02b4dcd89 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiCondensed-ThinItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Black.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Black.ttf new file mode 100644 index 000000000..2068cbaea Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Black.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-BlackItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-BlackItalic.ttf new file mode 100644 index 000000000..8ecb82164 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-BlackItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Bold.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Bold.ttf new file mode 100644 index 000000000..55c05ab7f Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Bold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-BoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-BoldItalic.ttf new file mode 100644 index 000000000..aec43b67d Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-BoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraBold.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraBold.ttf new file mode 100644 index 000000000..ed5ab3085 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraBoldItalic.ttf new file mode 100644 index 000000000..8068ecac7 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraLight.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraLight.ttf new file mode 100644 index 000000000..fec8abf67 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraLight.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraLightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraLightItalic.ttf new file mode 100644 index 000000000..28d62669d Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ExtraLightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Italic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Italic.ttf new file mode 100644 index 000000000..dcd81612d Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Italic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Light.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Light.ttf new file mode 100644 index 000000000..50020af96 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Light.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-LightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-LightItalic.ttf new file mode 100644 index 000000000..02e4e3445 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-LightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Medium.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Medium.ttf new file mode 100644 index 000000000..06956cddc Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Medium.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-MediumItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-MediumItalic.ttf new file mode 100644 index 000000000..96a716c48 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-MediumItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Regular.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Regular.ttf new file mode 100644 index 000000000..217a53b2e Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Regular.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-SemiBold.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-SemiBold.ttf new file mode 100644 index 000000000..20edfa32b Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-SemiBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-SemiBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-SemiBoldItalic.ttf new file mode 100644 index 000000000..32291a6ef Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-SemiBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Thin.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Thin.ttf new file mode 100644 index 000000000..404a9e739 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-Thin.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ThinItalic.ttf b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ThinItalic.ttf new file mode 100644 index 000000000..29ee64d81 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_SemiExpanded-ThinItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Black.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Black.ttf new file mode 100644 index 000000000..ba98ecc1f Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Black.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-BlackItalic.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-BlackItalic.ttf new file mode 100644 index 000000000..46fdc3ba5 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-BlackItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Bold.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Bold.ttf new file mode 100644 index 000000000..13b91033e Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Bold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-BoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-BoldItalic.ttf new file mode 100644 index 000000000..cd0a8ee22 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-BoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraBold.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraBold.ttf new file mode 100644 index 000000000..93a8261e3 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraBoldItalic.ttf new file mode 100644 index 000000000..bfa25dc50 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraLight.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraLight.ttf new file mode 100644 index 000000000..b930d5a3b Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraLight.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraLightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraLightItalic.ttf new file mode 100644 index 000000000..83041c6d2 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ExtraLightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Italic.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Italic.ttf new file mode 100644 index 000000000..bcd47dcfd Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Italic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Light.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Light.ttf new file mode 100644 index 000000000..ebde6895d Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Light.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-LightItalic.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-LightItalic.ttf new file mode 100644 index 000000000..1c8a5c243 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-LightItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Medium.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Medium.ttf new file mode 100644 index 000000000..521a6fc68 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Medium.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-MediumItalic.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-MediumItalic.ttf new file mode 100644 index 000000000..f73018668 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-MediumItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Regular.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Regular.ttf new file mode 100644 index 000000000..32b984936 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Regular.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-SemiBold.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-SemiBold.ttf new file mode 100644 index 000000000..54c44a832 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-SemiBold.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-SemiBoldItalic.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-SemiBoldItalic.ttf new file mode 100644 index 000000000..66e960af1 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-SemiBoldItalic.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Thin.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Thin.ttf new file mode 100644 index 000000000..e1aa2cf44 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-Thin.ttf differ diff --git a/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ThinItalic.ttf b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ThinItalic.ttf new file mode 100644 index 000000000..db374ecd0 Binary files /dev/null and b/public/Saira_Stencil/static/SairaStencil_UltraCondensed-ThinItalic.ttf differ diff --git a/public/short-example/IMG_5204.jpg b/public/short-example/IMG_5204.jpg new file mode 100644 index 000000000..0174f2413 Binary files /dev/null and b/public/short-example/IMG_5204.jpg differ diff --git a/public/short-example/IMG_5205.jpg b/public/short-example/IMG_5205.jpg new file mode 100644 index 000000000..2e8fd8bdb Binary files /dev/null and b/public/short-example/IMG_5205.jpg differ diff --git a/public/short-example/IMG_5206.jpg b/public/short-example/IMG_5206.jpg new file mode 100644 index 000000000..4d1d13a57 Binary files /dev/null and b/public/short-example/IMG_5206.jpg differ diff --git a/public/short-example/IMG_5207.jpg b/public/short-example/IMG_5207.jpg new file mode 100644 index 000000000..0719201e5 Binary files /dev/null and b/public/short-example/IMG_5207.jpg differ diff --git a/public/short-example/IMG_5208.jpg b/public/short-example/IMG_5208.jpg new file mode 100644 index 000000000..f72dace18 Binary files /dev/null and b/public/short-example/IMG_5208.jpg differ diff --git a/public/short-example/IMG_5209.jpg b/public/short-example/IMG_5209.jpg new file mode 100644 index 000000000..d62abd4d5 Binary files /dev/null and b/public/short-example/IMG_5209.jpg differ diff --git a/scripts/extract-subtitles.mjs b/scripts/extract-subtitles.mjs new file mode 100644 index 000000000..0c0cc8e18 --- /dev/null +++ b/scripts/extract-subtitles.mjs @@ -0,0 +1,603 @@ +#!/usr/bin/env node + +import { execSync } from "child_process"; +import { existsSync, mkdirSync, unlinkSync, writeFileSync, readFileSync } from "fs"; +import { join, basename, dirname } from "path"; +import { fileURLToPath } from "url"; +import wavefile from "wavefile"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const FPS = 30; + +// Words/patterns that suggest emphasis or impact (for zoom effects) +// Portuguese (Brazilian) impact patterns +const IMPACT_PATTERNS_PT = [ + // Exclamations + /!+$/, + /\?!$/, + // Numbers and statistics + /\d+%/, + /R\$\s?\d+/, + /\d+x/i, + // Portuguese action/emphasis words + /\b(incrível|inacreditável|insano|absurdo|bizarro|épico|lendário|surreal)\b/i, + /\b(urgente|importante|crítico|atenção|perigo|alerta|cuidado)\b/i, + /\b(segredo|revelado|exposto|verdade|finalmente|agora|hoje)\b/i, + /\b(melhor|pior|primeiro|último|único|nunca|sempre|jamais)\b/i, + /\b(ganhar|ganhei|ganhamos|perder|perdi|perdemos|vencer|sucesso|consegui|dominei)\b/i, + /\b(milhão|milhões|bilhão|bilhões|mil)\b/i, + /\b(grátis|gratuito|novo|nova|exclusivo|exclusiva|limitado|especial)\b/i, + /\b(olha|olhem|cara|mano|gente|galera|pessoal)\b/i, + /\b(demais|muito|super|mega|ultra|hiper)\b/i, + /\b(quebrou|explodiu|bombou|viralizou|estourou)\b/i, + // All caps words (emphasis) + /\b[A-Z]{3,}\b/, +]; + +// English impact patterns (fallback) +const IMPACT_PATTERNS_EN = [ + /!+$/, + /\?!$/, + /\d+%/, + /\$\d+/, + /\d+x/i, + /\b(amazing|incredible|unbelievable|insane|crazy|huge|massive|epic|legendary)\b/i, + /\b(breaking|urgent|important|critical|warning|danger|alert)\b/i, + /\b(secret|revealed|exposed|truth|finally|now|today)\b/i, + /\b(best|worst|top|first|last|only|never|always|ever)\b/i, + /\b(win|won|lose|lost|fail|success|achieve|dominate)\b/i, + /\b(million|billion|thousand|hundred)\b/i, + /\b(free|new|exclusive|limited|special)\b/i, + /\b[A-Z]{3,}\b/, +]; + +// Detect if text is impactful and deserves a zoom +function detectImpact(text, language = "pt") { + const patterns = language === "pt" ? IMPACT_PATTERNS_PT : IMPACT_PATTERNS_EN; + + const impactScore = patterns.reduce((score, pattern) => { + return score + (pattern.test(text) ? 1 : 0); + }, 0); + + if (impactScore >= 2) { + return { type: "zoomCut", intensity: 2 }; + } else if (impactScore === 1) { + return { type: "zoomHold", intensity: 1 }; + } + return null; +} + +// Convert seconds to frame number +function secondsToFrame(seconds, fps = FPS) { + return Math.round(seconds * fps); +} + +// Extract audio from video using ffmpeg +async function extractAudio(videoPath, outputPath) { + console.log("Extraindo áudio do vídeo..."); + + try { + execSync( + `ffmpeg -y -i "${videoPath}" -vn -acodec pcm_s16le -ar 16000 -ac 1 "${outputPath}"`, + { stdio: "pipe" } + ); + console.log("Áudio extraído com sucesso."); + return true; + } catch (error) { + console.error("Erro ao extrair áudio:", error.message); + return false; + } +} + +// Read WAV file and convert to Float32Array for Whisper +function readWavAsFloat32(audioPath) { + console.log("Lendo arquivo de áudio..."); + const buffer = readFileSync(audioPath); + const wav = new wavefile.WaveFile(buffer); + + // Ensure 16-bit PCM format + wav.toBitDepth("16"); + + // Get samples and convert to Float32Array normalized to [-1, 1] + const samples = wav.getSamples(false, Int16Array); + const float32 = new Float32Array(samples.length); + + for (let i = 0; i < samples.length; i++) { + float32[i] = samples[i] / 32768.0; + } + + return float32; +} + +// Transcribe audio using Whisper via transformers.js +async function transcribeAudio(audioPath, language = "portuguese", modelSize = "small") { + console.log("Carregando modelo Whisper (pode demorar na primeira execução)..."); + + const { pipeline } = await import("@xenova/transformers"); + + const modelName = `Xenova/whisper-${modelSize}`; + console.log(`Usando modelo: ${modelName}`); + + const transcriber = await pipeline("automatic-speech-recognition", modelName, { + chunk_length_s: 30, + stride_length_s: 5, + }); + + // Read audio file as Float32Array (required for Node.js) + const audioData = readWavAsFloat32(audioPath); + + console.log("Transcrevendo áudio em português..."); + + const result = await transcriber(audioData, { + return_timestamps: "word", + chunk_length_s: 30, + stride_length_s: 5, + language: language, + task: "transcribe", + sampling_rate: 16000, + }); + + return result; +} + +// Group words into subtitle chunks (by sentence or time gaps) +function groupIntoSubtitles(chunks, maxWordsPerSubtitle = 8, maxDuration = 3) { + const subtitles = []; + let currentSubtitle = { + words: [], + startTime: null, + endTime: null, + }; + + for (const chunk of chunks) { + if (!chunk.timestamp) continue; + + const [start, end] = chunk.timestamp; + const word = chunk.text.trim(); + + if (!word) continue; + + // Skip bracketed annotations like [Música], [Music], [Applause], etc. + if (/^\[.*\]$/.test(word)) continue; + + // Start new subtitle if: + // 1. Current is empty + // 2. Too many words + // 3. Gap > 0.5s between words + // 4. Duration would exceed max + // 5. Sentence ends (., !, ?) + const shouldStartNew = + currentSubtitle.words.length === 0 || + currentSubtitle.words.length >= maxWordsPerSubtitle || + (currentSubtitle.endTime && start - currentSubtitle.endTime > 0.5) || + (currentSubtitle.startTime && end - currentSubtitle.startTime > maxDuration) || + (currentSubtitle.words.length > 0 && + /[.!?]$/.test(currentSubtitle.words[currentSubtitle.words.length - 1])); + + if (shouldStartNew && currentSubtitle.words.length > 0) { + subtitles.push({ ...currentSubtitle }); + currentSubtitle = { words: [], startTime: null, endTime: null }; + } + + currentSubtitle.words.push(word); + if (currentSubtitle.startTime === null) { + currentSubtitle.startTime = start; + } + currentSubtitle.endTime = end; + } + + // Push the last subtitle + if (currentSubtitle.words.length > 0) { + subtitles.push(currentSubtitle); + } + + return subtitles; +} + +// Words that should not end a subtitle (orphan words) - only applies to short words (< 4 letters) +const ORPHAN_WORDS_PT = new Set([ + // Articles + "o", "a", "os", "as", "um", "uma", "uns", + // Prepositions + "de", "da", "do", "das", "dos", "em", "na", "no", "nas", "nos", + "por", "com", "sem", "sob", "ao", "aos", "à", "às", + // Conjunctions + "e", "ou", "mas", "que", "se", "nem", + // Pronouns + "eu", "tu", "ele", "ela", "nós", "vós", + "me", "te", "se", "nos", "vos", "lhe", + "meu", "teu", "seu", + // Other common short words + "não", "já", "só", +]); + +const ORPHAN_WORDS_EN = new Set([ + // Articles + "a", "an", "the", + // Prepositions + "of", "to", "in", "on", "at", "by", "for", + // Conjunctions + "and", "or", "but", "if", "as", "so", "yet", "nor", + // Pronouns + "i", "we", "he", "she", "it", "my", "our", "his", "her", "its", + // Other + "is", "are", "was", "be", "has", +]); + +// Check if text ends with punctuation +function endsWithPunctuation(text) { + return /[.,!?;:"""'']$/.test(text.trim()); +} + +// Fix orphan words at the end of subtitles +function fixOrphanWords(subtitles, language = "pt") { + const orphanWords = language === "pt" ? ORPHAN_WORDS_PT : ORPHAN_WORDS_EN; + const result = [...subtitles]; + + for (let i = 0; i < result.length - 1; i++) { + const current = result[i]; + const next = result[i + 1]; + + if (current.words.length <= 1) continue; + + // Keep moving words while the last word is an orphan + while (current.words.length > 1) { + const lastWord = current.words[current.words.length - 1]; + const lastWordClean = lastWord.replace(/[.,!?;:"""'']/g, ""); + const lastWordLower = lastWordClean.toLowerCase(); + + // Stop if word ends with punctuation (good break point) + if (endsWithPunctuation(lastWord)) break; + + // Words with 4+ letters are fine + if (lastWordClean.length >= 4) break; + + // Check if it's an orphan word + const isOrphan = orphanWords.has(lastWordLower); + + if (isOrphan) { + // Move word to next subtitle + const wordToMove = current.words.pop(); + next.words.unshift(wordToMove); + } else { + break; + } + } + } + + return result; +} + +// Format subtitles as TypeScript code +function formatAsTypeScript(subtitles, fps = FPS, language = "pt") { + const items = subtitles.map((sub) => { + const text = sub.words.join(" "); + const startFrame = secondsToFrame(sub.startTime, fps); + const endFrame = secondsToFrame(sub.endTime, fps); + const zoom = detectImpact(text, language); + + let item = ` { text: "${text}", startFrame: ${startFrame}, endFrame: ${endFrame}`; + + if (zoom) { + item += `, zoom: { type: "${zoom.type}" as ZoomType, intensity: ${zoom.intensity} }`; + } + + item += " }"; + return item; + }); + + return ` subtitles: { + transition: "slideUp" as TransitionType, + items: [ +${items.join(",\n")}, + ], + },`; +} + +// Group subtitles into sentences (for audio splitting) +function groupIntoSentences(subtitles) { + const sentences = []; + let currentSentence = []; + + for (let i = 0; i < subtitles.length; i++) { + currentSentence.push(i); + const text = subtitles[i].words.join(" "); + + // End sentence if we hit punctuation or it's the last subtitle + if (text.match(/[.!?]$/) || i === subtitles.length - 1) { + sentences.push([...currentSentence]); + currentSentence = []; + } + } + + // Push any remaining subtitles as a sentence + if (currentSentence.length > 0) { + sentences.push(currentSentence); + } + + return sentences; +} + +// Split audio file into sentence chunks +async function splitAudioIntoSentences(audioPath, subtitles, sentences, sessionDir, fps = FPS) { + const audioDir = join(sessionDir, 'audio'); + if (!existsSync(audioDir)) { + mkdirSync(audioDir, { recursive: true }); + } + + console.log(`\nDividindo áudio em ${sentences.length} sentenças...`); + + const sentenceAudioFiles = []; + + for (let sentenceIdx = 0; sentenceIdx < sentences.length; sentenceIdx++) { + const subtitleIndices = sentences[sentenceIdx]; + const firstSubIndex = subtitleIndices[0]; + const lastSubIndex = subtitleIndices[subtitleIndices.length - 1]; + + const startTime = subtitles[firstSubIndex].startTime; + const endTime = subtitles[lastSubIndex].endTime; + const duration = endTime - startTime; + + const outputPath = join(audioDir, `sentence_${sentenceIdx}.wav`); + + try { + // Extract audio chunk using ffmpeg + execSync( + `ffmpeg -y -i "${audioPath}" -ss ${startTime} -t ${duration} -acodec pcm_s16le -ar 44100 -ac 1 "${outputPath}"`, + { stdio: 'pipe' } + ); + + const durationInFrames = Math.round(duration * fps); + sentenceAudioFiles.push({ + sentenceId: sentenceIdx, + path: `/sessions/${basename(sessionDir)}/audio/sentence_${sentenceIdx}.wav`, + durationInFrames + }); + } catch (error) { + console.error(`Erro ao extrair áudio da sentença ${sentenceIdx}:`, error.message); + sentenceAudioFiles.push(null); + } + } + + return sentenceAudioFiles; +} + +// Format subtitles as JSON for video-subtitles.json with voice chunks +function formatAsJSON(subtitles, fps = FPS, language = "pt", backgroundVideo = null, titleText = null, template = "bold", sentenceAudioFiles = null) { + // Group subtitles into sentences + const sentences = groupIntoSentences(subtitles); + + const items = subtitles.map((sub, index) => { + const text = sub.words.join(" "); + const startFrame = secondsToFrame(sub.startTime, fps); + const endFrame = secondsToFrame(sub.endTime, fps); + const zoom = detectImpact(text, language); + + const item = { text, startFrame, endFrame }; + if (index === 0) { + item.zoom = { type: "zoomHold", intensity: 2 }; + } else if (zoom) { + item.zoom = zoom; + } + + // Find which sentence this subtitle belongs to + if (sentenceAudioFiles) { + for (let sentenceIdx = 0; sentenceIdx < sentences.length; sentenceIdx++) { + if (sentences[sentenceIdx].includes(index)) { + item.sentenceId = sentenceIdx; + + // Add voice reference to first chunk of sentence + const isFirstChunk = sentences[sentenceIdx][0] === index; + if (isFirstChunk && sentenceAudioFiles[sentenceIdx]) { + item.voice = { + src: sentenceAudioFiles[sentenceIdx].path, + volume: 1.0, + durationInFrames: sentenceAudioFiles[sentenceIdx].durationInFrames + }; + } + break; + } + } + } + + return item; + }); + + // Calculate last frame for title end + const lastFrame = items.length > 0 ? items[items.length - 1].endFrame : 90; + + const config = { + background: backgroundVideo + ? { type: "video", src: `/${backgroundVideo}` } + : { type: "color", color: "#1a1815" }, + colors: { text: "#ffffff" }, + title: { + show: !!titleText, + text: titleText || "", + startFrame: 0, + endFrame: 90, + transition: "slideDown", + template + }, + subtitles: { + transition: "slideUp", + template, + items + }, + style: { + position: "bottom", + bottomOffset: 80 + } + }; + + return config; +} + +// Main function +async function main() { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.log(` +Uso: node scripts/extract-subtitles.mjs [opções] + +Opções: + --output, -o Caminho do arquivo de saída (padrão: exibe no console) + --json Saída em formato JSON (para video-subtitles.json) + --background Caminho do vídeo de fundo (relativo a public/) + --title, -t Título do vídeo (exibido no topo) + --template Template: bold, classic, minimal, stacked, fullWidthStacked, etc (padrão: bold) + --model, -m Tamanho do modelo Whisper: tiny, base, small, medium (padrão: small) + --max-words Máximo de palavras por legenda (padrão: 8) + --fps FPS do vídeo para cálculo de frames (padrão: 30) + --lang, -l Idioma: pt, en (padrão: pt para português brasileiro) + --split-audio Dividir áudio em sentenças e gerar arquivos separados (padrão: false) + --session-dir Diretório da sessão (necessário se --split-audio for usado) + +Exemplos: + node scripts/extract-subtitles.mjs public/video.mp4 + node scripts/extract-subtitles.mjs public/video.mp4 -o subtitles.ts + node scripts/extract-subtitles.mjs public/video.mp4 --json -o video-subtitles.json + node scripts/extract-subtitles.mjs public/video.mp4 --json --background uploads/bg.mp4 + node scripts/extract-subtitles.mjs public/video.mp4 --model tiny --fps 60 +`); + process.exit(1); + } + + const videoPath = args[0]; + + // Parse options + let outputPath = null; + let modelSize = "small"; + let maxWords = 8; + let fps = FPS; + let language = "pt"; + let jsonFormat = false; + let backgroundVideo = null; + let titleText = null; + let template = "bold"; + let splitAudio = false; + let sessionDir = null; + + for (let i = 1; i < args.length; i++) { + if (args[i] === "--output" || args[i] === "-o") { + outputPath = args[++i]; + } else if (args[i] === "--model" || args[i] === "-m") { + modelSize = args[++i]; + } else if (args[i] === "--max-words") { + maxWords = parseInt(args[++i]); + } else if (args[i] === "--fps") { + fps = parseInt(args[++i]); + } else if (args[i] === "--lang" || args[i] === "-l") { + language = args[++i]; + } else if (args[i] === "--json") { + jsonFormat = true; + } else if (args[i] === "--background") { + backgroundVideo = args[++i]; + } else if (args[i] === "--title" || args[i] === "-t") { + titleText = args[++i]; + } else if (args[i] === "--template") { + template = args[++i]; + } else if (args[i] === "--split-audio") { + splitAudio = true; + } else if (args[i] === "--session-dir") { + sessionDir = args[++i]; + } + } + + // Auto-detect session dir from output path if not provided + if (splitAudio && !sessionDir && outputPath) { + // Try to extract from path like /path/to/datalake/session-xxx/video-subtitles.json + const match = outputPath.match(/(.*\/session-[^/]+)/); + if (match) { + sessionDir = match[1]; + } + } + + const whisperLang = language === "pt" ? "portuguese" : "english"; + + if (!existsSync(videoPath)) { + console.error(`Erro: Arquivo de vídeo não encontrado: ${videoPath}`); + process.exit(1); + } + + // Create temp directory for audio + const tempDir = join(__dirname, "..", ".temp"); + if (!existsSync(tempDir)) { + mkdirSync(tempDir, { recursive: true }); + } + + const videoBasename = basename(videoPath).replace(/\.[^.]+$/, ""); + const audioPath = join(tempDir, `${videoBasename}.wav`); + + try { + // Step 1: Extract audio + const audioExtracted = await extractAudio(videoPath, audioPath); + if (!audioExtracted) { + process.exit(1); + } + + // Step 2: Transcribe + const transcription = await transcribeAudio(audioPath, whisperLang, modelSize); + + console.log("\nTranscrição:", transcription.text); + console.log(`\nEncontrados ${transcription.chunks?.length || 0} timestamps de palavras`); + + // Step 3: Group into subtitles + const rawSubtitles = groupIntoSubtitles(transcription.chunks || [], maxWords); + + // Step 3.5: Fix orphan words at the end of subtitles + const subtitles = fixOrphanWords(rawSubtitles, language); + + console.log(`\nAgrupados em ${subtitles.length} legendas`); + + // Step 4: Split audio into sentences if requested + let sentenceAudioFiles = null; + if (splitAudio && jsonFormat) { + if (!sessionDir) { + console.error('\nErro: --session-dir é necessário quando --split-audio é usado'); + process.exit(1); + } + + const sentences = groupIntoSentences(subtitles); + sentenceAudioFiles = await splitAudioIntoSentences(audioPath, subtitles, sentences, sessionDir, fps); + console.log(`\nCriados ${sentenceAudioFiles.filter(f => f).length} arquivos de áudio`); + } + + // Step 5: Format output + let output; + if (jsonFormat) { + const jsonConfig = formatAsJSON(subtitles, fps, language, backgroundVideo, titleText, template, sentenceAudioFiles); + output = JSON.stringify(jsonConfig, null, 2); + } else { + output = formatAsTypeScript(subtitles, fps, language); + } + + // Step 6: Output + if (outputPath) { + writeFileSync(outputPath, output); + console.log(`\nLegendas escritas em: ${outputPath}`); + } else { + if (jsonFormat) { + console.log("\n--- JSON Config ---\n"); + console.log(output); + console.log("\n--- Copie para video-subtitles.json ---\n"); + } else { + console.log("\n--- TypeScript Gerado ---\n"); + console.log(output); + console.log("\n--- Copie o código acima para seu constants.ts ---\n"); + } + } + + // Cleanup + if (existsSync(audioPath)) { + unlinkSync(audioPath); + } + } catch (error) { + console.error("Erro:", error.message); + console.error(error.stack); + process.exit(1); + } +} + +main(); diff --git a/src/components/video-editor/AnnotationOverlay.tsx b/src/components/video-editor/AnnotationOverlay.tsx index 11548c741..182e59881 100644 --- a/src/components/video-editor/AnnotationOverlay.tsx +++ b/src/components/video-editor/AnnotationOverlay.tsx @@ -14,6 +14,7 @@ interface AnnotationOverlayProps { onClick: (id: string) => void; zIndex: number; isSelectedBoost: boolean; // Boost z-index when selected for easy editing + currentTimeMs?: number; } export function AnnotationOverlay({ @@ -26,6 +27,7 @@ export function AnnotationOverlay({ onClick, zIndex, isSelectedBoost, + currentTimeMs = 0, }: AnnotationOverlayProps) { const x = (annotation.position.x / 100) * containerWidth; const y = (annotation.position.y / 100) * containerHeight; @@ -43,8 +45,193 @@ export function AnnotationOverlay({ return ; }; + const renderCaption = () => { + const data = annotation.captionData; + if (!data) return null; + + const timeIntoAnnotation = currentTimeMs - annotation.startMs; + const totalDuration = annotation.endMs - annotation.startMs; + const fadeOutStart = Math.max(0, totalDuration - 500); + const globalOpacity = + timeIntoAnnotation >= fadeOutStart + ? Math.max(0, 1 - (timeIntoAnnotation - fadeOutStart) / 500) + : 1; + + const renderWords = (text: string, startWordIndex: number, color: string) => { + const words = text.split(" ").filter((w) => w.length > 0); + return words.map((word, i) => { + const wordIndex = startWordIndex + i; + const wordStartMs = wordIndex * data.wordDelay; + const progress = Math.min( + 1, + Math.max(0, (timeIntoAnnotation - wordStartMs) / data.animationDuration), + ); + return ( + + {word} + + ); + }); + }; + + const gradientMap: Record = { + bottom: "linear-gradient(to top, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.4) 60%, transparent 100%)", + top: "linear-gradient(to bottom, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.4) 60%, transparent 100%)", + left: "linear-gradient(to right, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.4) 60%, transparent 100%)", + right: "linear-gradient(to left, rgba(0,0,0,0.88) 0%, rgba(0,0,0,0.4) 60%, transparent 100%)", + none: "none", + }; + + const textAlignToFlex = (ta: string) => + ta === "left" ? "flex-start" : ta === "right" ? "flex-end" : "center"; + + // For left/right gradients, text is pinned to the dark side; otherwise use textAlign + const alignItems = + data.gradientDirection === "left" + ? "flex-start" + : data.gradientDirection === "right" + ? "flex-end" + : textAlignToFlex(data.textAlign ?? "center"); + + const justifyMap: Record = { + bottom: "flex-end", + top: "flex-start", + left: "center", + right: "center", + none: "center", + }; + + const primaryWords = data.primaryText.split(" ").filter((w) => w.length > 0); + const fadeInOpacity = Math.min(1, Math.max(0, timeIntoAnnotation / 400)); + const backgroundOpacity = fadeInOpacity * globalOpacity; + + return ( +
+ {/* Gradient layer — fades in independently over 400 ms, extends 4 px below to cover edge */} +
+ + {/* Content */} +
+ {data.imageUrl && ( + + )} + {data.primaryText && ( +
+ {renderWords(data.primaryText, 0, data.primaryColor)} +
+ )} + {data.secondaryText && ( +
+ {renderWords(data.secondaryText, primaryWords.length, data.secondaryColor)} +
+ )} +
+
+ ); + }; + + const renderMarker = () => { + const data = annotation.markerData; + if (!data) return null; + + const timeIntoAnnotation = currentTimeMs - annotation.startMs; + const totalDuration = annotation.endMs - annotation.startMs; + const fadeOutStart = Math.max(0, totalDuration - 500); + const globalOpacity = + timeIntoAnnotation >= fadeOutStart + ? Math.max(0, 1 - (timeIntoAnnotation - fadeOutStart) / 500) + : 1; + + const sweepProgress = Math.min(1, Math.max(0, timeIntoAnnotation / data.animationDuration)); + + const clipRight = data.direction === "left" ? `${(1 - sweepProgress) * 100}%` : "0%"; + const clipLeft = data.direction === "right" ? `${(1 - sweepProgress) * 100}%` : "0%"; + + return ( +
+ ); + }; + const renderContent = () => { switch (annotation.type) { + case "marker": + return renderMarker(); + + case "caption": + return renderCaption(); + case "text": return (
); - case "image": + case "image": { + const radius = annotation.style.borderRadius ?? 0; + const imgData = annotation.imageData; + const timeIntoAnnotation = currentTimeMs - annotation.startMs; + const totalDuration = annotation.endMs - annotation.startMs; + const animDuration = imgData?.animationDuration ?? 500; + + // entrance + const rawProgress = Math.min(1, Math.max(0, timeIntoAnnotation / animDuration)); + const p = 1 - Math.pow(1 - rawProgress, 3); + + // fade-out + const fadeOutStart = Math.max(0, totalDuration - animDuration); + const exitRaw = imgData?.fadeOut + ? Math.min(1, Math.max(0, (timeIntoAnnotation - fadeOutStart) / animDuration)) + : 0; + const exitOpacity = imgData?.fadeOut ? 1 - exitRaw : 1; + + let animOpacity = exitOpacity; + let animTransform = "none"; + const animType = imgData?.animationType ?? "none"; + if (animType !== "none" && rawProgress < 1) { + animOpacity = p * exitOpacity; + if (animType === "slide-up") animTransform = `translateY(${(1 - p) * 50}%)`; + else if (animType === "slide-down") animTransform = `translateY(${-(1 - p) * 50}%)`; + else if (animType === "slide-left") animTransform = `translateX(${(1 - p) * 50}%)`; + else if (animType === "slide-right") animTransform = `translateX(${-(1 - p) * 50}%)`; + else if (animType === "zoom") animTransform = `scale(${0.75 + 0.25 * p})`; + } + if (annotation.content && annotation.content.startsWith("data:image")) { return ( - Annotation +
0 ? `${radius}px` : undefined, opacity: animOpacity }} + > +
+ Annotation +
+
); } return ( @@ -99,6 +323,7 @@ export function AnnotationOverlay({ No image
); + } case "figure": if (!annotation.figureData) { diff --git a/src/components/video-editor/AnnotationSettingsPanel.tsx b/src/components/video-editor/AnnotationSettingsPanel.tsx index b289392e2..2b585de1a 100644 --- a/src/components/video-editor/AnnotationSettingsPanel.tsx +++ b/src/components/video-editor/AnnotationSettingsPanel.tsx @@ -3,11 +3,11 @@ import { AlignCenter, AlignLeft, AlignRight, - Bold, ChevronDown, Image as ImageIcon, Info, Italic, + Subtitles, Trash2, Type, Underline, @@ -25,6 +25,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Slider } from "@/components/ui/slider"; +import { Switch } from "@/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; import { useScopedT } from "@/contexts/I18nContext"; @@ -32,17 +33,57 @@ import { type CustomFont, getCustomFonts } from "@/lib/customFonts"; import { cn } from "@/lib/utils"; import { AddCustomFontDialog } from "./AddCustomFontDialog"; import { getArrowComponent } from "./ArrowSvgs"; -import type { AnnotationRegion, AnnotationType, ArrowDirection, FigureData } from "./types"; +import type { + AnnotationRegion, + AnnotationType, + ArrowDirection, + CaptionData, + CaptionGradientDirection, + FigureData, + ImageAnimationType, + ImageData, + MarkerData, + MarkerDirection, +} from "./types"; interface AnnotationSettingsPanelProps { annotation: AnnotationRegion; onContentChange: (content: string) => void; onTypeChange: (type: AnnotationType) => void; onStyleChange: (style: Partial) => void; + onPositionChange?: (position: { x: number; y: number }) => void; + onSizeChange?: (size: { width: number; height: number }) => void; + onImageDataChange?: (imageData: ImageData) => void; onFigureDataChange?: (figureData: FigureData) => void; + onCaptionDataChange?: (captionData: CaptionData) => void; + onMarkerDataChange?: (markerData: MarkerData) => void; onDelete: () => void; } +const IMAGE_PRESETS = [ + { + label: "Full width", + icon: "▬", + position: { x: 5, y: 57 }, + size: { width: 90, height: 38 }, + borderRadius: 10, + }, + { + label: "Card", + icon: "▪", + position: { x: 12, y: 50 }, + size: { width: 76, height: 43 }, + borderRadius: 24, + }, + { + label: "Phone", + icon: "▯", + position: { x: 25, y: 37 }, + size: { width: 50, height: 55 }, + borderRadius: 28, + }, +]; + const FONT_FAMILIES = [ { value: "system-ui, -apple-system, sans-serif", labelKey: "classic" }, { value: "Georgia, serif", labelKey: "editor" }, @@ -52,16 +93,46 @@ const FONT_FAMILIES = [ { value: "Arial, sans-serif", labelKey: "simple" }, { value: "Verdana, sans-serif", labelKey: "modern" }, { value: "Trebuchet MS, sans-serif", labelKey: "clean" }, + { value: "'Saira Stencil', sans-serif", labelKey: "stencil" }, ]; const FONT_SIZES = [12, 14, 16, 18, 20, 24, 28, 32, 36, 40, 48, 56, 64, 72, 80, 96, 128]; +const FONT_WEIGHTS = [ + { value: "100", label: "Thin" }, + { value: "200", label: "Extra Light" }, + { value: "300", label: "Light" }, + { value: "400", label: "Regular" }, + { value: "500", label: "Medium" }, + { value: "600", label: "Semi Bold" }, + { value: "700", label: "Bold" }, + { value: "800", label: "Extra Bold" }, + { value: "900", label: "Black" }, +]; + +const FONT_STRETCHES = [ + { value: "ultra-condensed", label: "Ultra Condensed" }, + { value: "extra-condensed", label: "Extra Condensed" }, + { value: "condensed", label: "Condensed" }, + { value: "semi-condensed", label: "Semi Condensed" }, + { value: "normal", label: "Normal" }, + { value: "semi-expanded", label: "Semi Expanded" }, + { value: "expanded", label: "Expanded" }, + { value: "extra-expanded", label: "Extra Expanded" }, + { value: "ultra-expanded", label: "Ultra Expanded" }, +]; + export function AnnotationSettingsPanel({ annotation, onContentChange, onTypeChange, onStyleChange, + onPositionChange, + onSizeChange, + onImageDataChange, onFigureDataChange, + onCaptionDataChange, + onMarkerDataChange, onDelete, }: AnnotationSettingsPanelProps) { const t = useScopedT("settings"); @@ -77,6 +148,7 @@ export function AnnotationSettingsPanel({ simple: t("fontStyles.simple"), modern: t("fontStyles.modern"), clean: t("fontStyles.clean"), + stencil: t("fontStyles.stencil"), }; // Load custom fonts on mount @@ -155,27 +227,27 @@ export function AnnotationSettingsPanel({ onValueChange={(value) => onTypeChange(value as AnnotationType)} className="mb-6" > - + - + {t("annotation.typeText")} - + {t("annotation.typeImage")} {t("annotation.typeArrow")} + + + Caption + + + + + + Marker + {/* Text Content */} @@ -278,25 +366,56 @@ export function AnnotationSettingsPanel({ />
+ {/* Weight & Stretch */} +
+
+ + +
+
+ + +
+
+ {/* Formatting Toggles */}
- - onStyleChange({ - fontWeight: annotation.style.fontWeight === "bold" ? "normal" : "bold", - }) - } - className="h-8 w-8 data-[state=on]:bg-[#34B27B] data-[state=on]:text-white text-slate-400 hover:bg-white/5 hover:text-slate-200" - > - - )} + {/* Layout presets */} +
+ +
+ {IMAGE_PRESETS.map((preset) => ( + + ))} +
+
+ + {/* Border radius */} +
+ + onStyleChange({ borderRadius: v })} + className="w-full" + /> +
+ + {/* Entrance animation */} + {(() => { + const imgData = annotation.imageData ?? { animationType: "none" as ImageAnimationType, animationDuration: 500 }; + const update = (patch: Partial) => + onImageDataChange?.({ ...imgData, ...patch }); + const ANIM_PRESETS: { type: ImageAnimationType; label: string; icon: React.ReactNode }[] = [ + { + type: "none", + label: "None", + icon: ( + + + + ), + }, + { + type: "fade", + label: "Fade", + icon: ( + + + + + + + + + + ), + }, + { + type: "slide-up", + label: "Up", + icon: ( + + + + ), + }, + { + type: "slide-down", + label: "Down", + icon: ( + + + + ), + }, + { + type: "slide-left", + label: "Left", + icon: ( + + + + ), + }, + { + type: "slide-right", + label: "Right", + icon: ( + + + + ), + }, + { + type: "zoom", + label: "Zoom", + icon: ( + + + + + ), + }, + ]; + return ( +
+ +
+ {ANIM_PRESETS.map((anim) => ( + + ))} +
+ {imgData.animationType !== "none" && ( +
+ + update({ animationDuration: v })} + className="w-full" + /> +
+ )} +
+ Fade out + update({ fadeOut: v })} + /> +
+
+ ); + })()} +

{t("annotation.supportedFormats")}

@@ -595,6 +893,444 @@ export function AnnotationSettingsPanel({
+ + {/* Caption / Lower-Third */} + + {(() => { + const data = annotation.captionData; + if (!data || !onCaptionDataChange) return null; + const update = (patch: Partial) => + onCaptionDataChange({ ...data, ...patch }); + return ( + <> +
+ + update({ primaryText: e.target.value })} + className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-slate-200 text-sm placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-[#34B27B]" + /> +
+
+ + update({ secondaryText: e.target.value })} + className="w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-slate-200 text-sm placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-[#34B27B]" + /> +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+ + + + + + + update({ primaryColor: c.hex })} + style={{ borderRadius: "8px" }} + /> + + +
+
+ + + + + + + update({ secondaryColor: c.hex })} + style={{ borderRadius: "8px" }} + /> + + +
+
+
+
+ + +
+
+ + +
+
+
+ +
+ {( + ["bottom", "top", "left", "right", "none"] as CaptionGradientDirection[] + ).map((dir) => ( + + ))} +
+
+
+ + + update({ textAlign: "left" })} + className="h-8 w-8 data-[state=on]:bg-[#34B27B] data-[state=on]:text-white text-slate-400 hover:bg-white/5 hover:text-slate-200" + > + + + update({ textAlign: "center" })} + className="h-8 w-8 data-[state=on]:bg-[#34B27B] data-[state=on]:text-white text-slate-400 hover:bg-white/5 hover:text-slate-200" + > + + + update({ textAlign: "right" })} + className="h-8 w-8 data-[state=on]:bg-[#34B27B] data-[state=on]:text-white text-slate-400 hover:bg-white/5 hover:text-slate-200" + > + + + +
+
+
+ + update({ wordDelay: v })} + min={50} + max={500} + step={25} + className="w-full" + /> +
+
+ + update({ animationDuration: v })} + min={50} + max={600} + step={25} + className="w-full" + /> +
+
+
+ + {data.imageUrl ? ( +
+ Caption image + +
+ ) : ( + + )} +
+ + ); + })()} +
+ + {/* Marker highlight */} + + {(() => { + const data = annotation.markerData; + if (!data || !onMarkerDataChange) return null; + const update = (patch: Partial) => + onMarkerDataChange({ ...data, ...patch }); + return ( + <> +
+ + + + + + + update({ color: c.hex })} + style={{ borderRadius: "8px" }} + /> + + +
+
+ + update({ opacity: v / 100 })} + min={10} + max={90} + step={5} + className="w-full" + /> +
+
+ +
+ {(["left", "right"] as MarkerDirection[]).map((dir) => ( + + ))} +
+
+
+ + update({ animationDuration: v })} + min={100} + max={1000} + step={50} + className="w-full" + /> +
+ + ); + })()} +
+ ); + })} +
+ {selectedZoomFocusMode === "auto" && ( +

+ {t("zoom.focusMode.autoDescription")} +

+ )} + + )} {zoomEnabled && ( + ) : ( + <> +
+ +
- - - - - )} +
+ +
+
+
+ {t("layout.preset")} +
+ +
+
+
+ {t("layout.position")} +
+ {webcamLayoutPreset === "picture-in-picture" ? ( +
+ {( + [ + ["top-left", "top-right"], + ["center-left", "center-right"], + ["bottom-left", "bottom-right"], + ] as WebcamCornerPreset[][] + ).map((row) => + row.map((corner) => ( + + )), + )} +
+ ) : ( +
+ {(["top", "bottom"] as WebcamStackPosition[]).map((pos) => ( + + ))} +
+ )} +
+ {webcamLayoutPreset === "vertical-stack" && ( +
+
+ Focus zoom + {Math.round(webcamFocusZoom * 100)}% +
+ onWebcamFocusZoomChange?.(v)} + className="w-full" + /> +
+ )} + {webcamLayoutPreset === "picture-in-picture" && ( +
+
+ {t("layout.webcamShape")} +
+
+ {( + [ + { value: "rectangle", label: "Rect" }, + { value: "circle", label: "Circle" }, + { value: "square", label: "Square" }, + { value: "rounded", label: "Rounded" }, + { value: "portrait", label: "Portrait" }, + ] as Array<{ value: WebcamMaskShape; label: string }> + ).map((shape) => ( + + ))} +
+
+
+ {t("layout.webcamSize")} +
+
+ {( + [ + { value: "small", label: t("layout.webcamSizeSmall") }, + { value: "medium", label: t("layout.webcamSizeMedium") }, + { value: "large", label: t("layout.webcamSizeLarge") }, + ] as Array<{ value: WebcamSizePreset; label: string }> + ).map((size) => ( + + ))} +
+
+ {selectedWebcamFocusId && ( +
+
+ {t("layout.focusShape")} +
+
+ {( + [ + { value: "rectangle", label: "Rect" }, + { value: "circle", label: "Circle" }, + { value: "square", label: "Square" }, + { value: "rounded", label: "Rounded" }, + { value: "portrait", label: "Portrait" }, + ] as Array<{ value: WebcamMaskShape; label: string }> + ).map((shape) => ( + + ))} +
+ +
+ )} +
+ )} + {/* Webcam sync offset */} +
+
+ Sync offset + + {webcamSyncOffsetMs > 0 ? `+${webcamSyncOffsetMs}` : webcamSyncOffsetMs}ms + +
+ onWebcamSyncOffsetMsChange?.(v)} + className="w-full" + /> +

+ Applied to new recordings. Positive = webcam starts later. +

+
+ + )} + + @@ -902,6 +1330,172 @@ export function SettingsPanel({ + + + +
+ + Subtitles + {subtitleRegions.length > 0 && ( + + {subtitleRegions.length} + + )} +
+
+ +
+ {/* Generate / Clear buttons */} +
+ + {subtitleRegions.length > 0 && ( + + )} +
+ + {subtitleRegions.length === 0 && !isGeneratingSubtitles && ( +

+ Click Auto-Generate to create subtitles from your video audio using Whisper AI. +

+ )} + + {subtitleRegions.length > 0 && ( + <> + {/* Show/hide toggle */} +
+ Show subtitles + +
+ + {/* Template */} +
+ Template + +
+ + {/* Font size */} +
+
+ Font size + {subtitleStyle?.fontSize ?? 32}px +
+ onSubtitleStyleChange?.({ fontSize: v })} + className="w-full" + /> +
+ + {/* Position toggle + offset slider */} +
+
+ Position +
+ {(["bottom", "top"] as const).map((pos) => ( + + ))} +
+
+
+ Offset + {subtitleStyle?.bottomOffset ?? 8}% +
+ onSubtitleStyleChange?.({ bottomOffset: v })} + className="w-full" + /> +
+ + {/* Subtitle text list */} +
+
+ + {subtitleRegions.length} subtitle{subtitleRegions.length !== 1 ? "s" : ""} + +
+
+ {subtitleRegions.map((sub) => { + const startSec = sub.startMs / 1000; + const mm = Math.floor(startSec / 60); + const ss = String(Math.floor(startSec % 60)).padStart(2, "0"); + const ts = `${mm}:${ss}`; + return ( +
+
{ts}
+