diff --git a/README.md b/README.md index 0e9ed4df78..dc6f7e9c12 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 573aee8af0..83442b9bdb 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 78d83448ae..b6fc5aa871 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 8f1836bd84..97042c1fd0 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 70e33952e6..06bd6b3650 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 c367f9ed12..5e90b0a4a8 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 0000000000..26d124f132 --- /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 0000000000..a4f6e77576 --- /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 0000000000..1210d6893e 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 0000000000..571e8860bd 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 0000000000..d0b21a78f3 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 0000000000..816856dc49 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 0000000000..70d1dac460 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 0000000000..68b8828d5f 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 0000000000..f466000a31 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 0000000000..59c0d70261 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 0000000000..ade3743ef9 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 0000000000..4473500531 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 0000000000..3c1d3b9dbf 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 0000000000..3c26433cc7 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 0000000000..b57cdf736a 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 0000000000..fac70adc71 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 0000000000..7755faabbb 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 0000000000..d6855bea20 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 0000000000..c6d4edaf83 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 0000000000..decf7e8ae1 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 0000000000..4062d581ed 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 0000000000..6b51a67b8d 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 0000000000..ce53e7e969 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 0000000000..cff9c1af63 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 0000000000..92333931f5 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 0000000000..77a682b5f7 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 0000000000..796f8ff194 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 0000000000..fe2a093e71 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 0000000000..f5ae16d399 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 0000000000..7f0d948dcf 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 0000000000..ef8d4b14f1 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 0000000000..2a5ec4c1c9 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 0000000000..76877ff4ff 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 0000000000..7daa645703 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 0000000000..de9b6efc33 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 0000000000..633adc0874 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 0000000000..8ac37fc0b9 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 0000000000..d0b8872770 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 0000000000..22685e2e4b 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 0000000000..80bab7a567 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 0000000000..f48f38a1b1 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 0000000000..aafe2b3181 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 0000000000..34290d2831 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 0000000000..88c8962e85 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 0000000000..3ea9140acf 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 0000000000..1c3f152de6 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 0000000000..f81e63d586 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 0000000000..30633007cb 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 0000000000..c040b87073 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 0000000000..d5d2d92e4b 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 0000000000..a312c0e51a 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 0000000000..22d6ef4f30 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 0000000000..4bd9e68822 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 0000000000..c7594f05ad 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 0000000000..f868925550 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 0000000000..310076ea3b 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 0000000000..95f6e8a3a9 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 0000000000..9153c32dbd 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 0000000000..4d994c14ef 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 0000000000..6f53751e9c 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 0000000000..02074257fd 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 0000000000..4031db5948 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 0000000000..52a21d3c85 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 0000000000..bb079267ea 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 0000000000..bb4d1921dc 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 0000000000..66a0612747 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 0000000000..c3d184a963 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 0000000000..ac4ed08d24 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 0000000000..63705bfb46 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 0000000000..fefb43d58f 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 0000000000..164554f434 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 0000000000..e458ddaddf 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 0000000000..a06700a562 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 0000000000..a651b56d97 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 0000000000..d966c52c9e 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 0000000000..cb9f6c2a6c 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 0000000000..5df30f64a3 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 0000000000..e16fc61f3f 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 0000000000..8e38837256 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 0000000000..9258630583 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 0000000000..166a1affd1 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 0000000000..3df57302cb 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 0000000000..da2586d95c 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 0000000000..9c4769a91a 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 0000000000..5a23796577 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 0000000000..e271744c39 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 0000000000..eae2d1ebdb 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 0000000000..45a510b571 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 0000000000..a7b47e9d7a 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 0000000000..bd24220ec4 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 0000000000..c45ee7bf19 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 0000000000..786c06e23e 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 0000000000..256eae69b4 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 0000000000..02b4dcd89c 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 0000000000..2068cbaeac 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 0000000000..8ecb821640 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 0000000000..55c05ab7f7 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 0000000000..aec43b67da 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 0000000000..ed5ab30853 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 0000000000..8068ecac75 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 0000000000..fec8abf670 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 0000000000..28d62669d6 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 0000000000..dcd81612dc 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 0000000000..50020af969 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 0000000000..02e4e34458 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 0000000000..06956cddce 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 0000000000..96a716c486 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 0000000000..217a53b2e8 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 0000000000..20edfa32bf 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 0000000000..32291a6efc 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 0000000000..404a9e739d 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 0000000000..29ee64d810 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 0000000000..ba98ecc1f5 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 0000000000..46fdc3ba59 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 0000000000..13b91033e6 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 0000000000..cd0a8ee225 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 0000000000..93a8261e36 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 0000000000..bfa25dc50a 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 0000000000..b930d5a3b7 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 0000000000..83041c6d29 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 0000000000..bcd47dcfd8 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 0000000000..ebde6895de 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 0000000000..1c8a5c2436 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 0000000000..521a6fc682 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 0000000000..f73018668d 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 0000000000..32b9849365 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 0000000000..54c44a832f 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 0000000000..66e960af11 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 0000000000..e1aa2cf44d 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 0000000000..db374ecd0f 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 0000000000..0174f24133 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 0000000000..2e8fd8bdbc 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 0000000000..4d1d13a577 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 0000000000..0719201e5f 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 0000000000..f72dace184 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 0000000000..d62abd4d57 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 0000000000..0c0cc8e185 --- /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 11548c7419..182e59881b 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 b289392e21..2b585de1ae 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}
+