├── public ├── images │ └── icons │ │ ├── icon-384x384.png │ │ ├── icon-72x72.png │ │ ├── icon-96x96.png │ │ ├── icon-128x128.png │ │ ├── icon-144x144.png │ │ ├── icon-152x152.png │ │ ├── icon-192x192.png │ │ └── icon-512x512.png ├── js │ ├── ffprobe-wasm.wasm │ ├── libmain.worker.js │ ├── ffprobe-wasm.worker.js │ └── ffprobe-worker.js ├── manifest.json └── vite.svg ├── settings.gradle ├── manifest-checksum.txt ├── .gitattributes ├── src ├── app.css ├── vite-env.d.ts ├── main.ts ├── lib │ ├── components │ │ ├── errors.js │ │ ├── Notifications.svelte │ │ └── FileHandler.svelte │ ├── audio │ │ ├── errors.js │ │ ├── FindAudioTracks.svelte │ │ └── ExtractAudioTracks.svelte │ ├── pickers │ │ ├── file_sizes.js │ │ ├── errors.js │ │ ├── Languages.svelte │ │ └── Model.svelte │ ├── subtitle_conversions │ │ ├── srt.spec.ts │ │ ├── txt.spec.ts │ │ ├── webvtt.spec.ts │ │ ├── TXT.svelte │ │ ├── WebVTT.svelte │ │ ├── SRT.svelte │ │ └── test_data.ts │ └── get_seconds.ts ├── assets │ └── svelte.svg └── App.svelte ├── .vscode └── extensions.json ├── store_icon.png ├── app ├── src │ └── main │ │ ├── res │ │ ├── xml │ │ │ ├── shortcuts.xml │ │ │ └── filepaths.xml │ │ ├── drawable-hdpi │ │ │ ├── splash.png │ │ │ └── ic_notification_icon.png │ │ ├── drawable-mdpi │ │ │ ├── splash.png │ │ │ └── ic_notification_icon.png │ │ ├── drawable-xhdpi │ │ │ ├── splash.png │ │ │ └── ic_notification_icon.png │ │ ├── drawable-xxhdpi │ │ │ ├── splash.png │ │ │ └── ic_notification_icon.png │ │ ├── drawable-xxxhdpi │ │ │ ├── splash.png │ │ │ └── ic_notification_icon.png │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ ├── raw │ │ │ └── web_app_manifest.json │ │ ├── values │ │ │ ├── colors.xml │ │ │ └── strings.xml │ │ └── drawable-anydpi │ │ │ └── shortcut_legacy_background.xml │ │ ├── java │ │ └── org │ │ │ └── ccextractor │ │ │ └── video2srt │ │ │ └── twa │ │ │ ├── DelegationService.java │ │ │ ├── Application.java │ │ │ └── LauncherActivity.java │ │ └── AndroidManifest.xml └── build.gradle ├── _headers ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── postcss.config.js ├── cypress ├── component │ └── FindAudioTracks.cy.ts ├── fixtures │ └── example.json ├── e2e │ ├── video_mp4.cy.ts │ ├── notifications.cy.ts │ └── check_for_features.cy.ts └── support │ ├── component-index.html │ ├── e2e.ts │ ├── component.ts │ └── commands.ts ├── lighthouserc.cjs ├── env.sample ├── tsconfig.node.json ├── svelte.config.js ├── tailwind.config.js ├── vite.config.ts ├── .github ├── actions │ └── pwa2Apk │ │ ├── Dockerfile │ │ ├── entrypoint.sh │ │ └── action.yml └── workflows │ ├── lighthouse.yml │ ├── build_apk.yml │ └── node.js.yml ├── README.md ├── tsconfig.json ├── cypress.config.ts ├── gradle.properties ├── privacy-policy.txt ├── twa-manifest.json ├── package.json ├── .gitignore ├── gradlew.bat ├── index.html ├── gradlew └── LICENSE /public/images/icons/icon-384x384.png: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /manifest-checksum.txt: -------------------------------------------------------------------------------- 1 | cb00feb98226f35e65f0857fae10039f50aacc8b -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.bin filter=lfs diff=lfs merge=lfs -text 2 | 3 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["svelte.svelte-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /store_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/store_icon.png -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | -------------------------------------------------------------------------------- /app/src/main/res/xml/shortcuts.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/js/ffprobe-wasm.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/public/js/ffprobe-wasm.wasm -------------------------------------------------------------------------------- /_headers: -------------------------------------------------------------------------------- 1 | /* 2 | Cross-Origin-Opener-Policy: same-origin 3 | Cross-Origin-Embedder-Policy: require-corp 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/images/icons/icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/public/images/icons/icon-72x72.png -------------------------------------------------------------------------------- /public/images/icons/icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/public/images/icons/icon-96x96.png -------------------------------------------------------------------------------- /public/images/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/public/images/icons/icon-128x128.png -------------------------------------------------------------------------------- /public/images/icons/icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/public/images/icons/icon-144x144.png -------------------------------------------------------------------------------- /public/images/icons/icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/public/images/icons/icon-152x152.png -------------------------------------------------------------------------------- /public/images/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/public/images/icons/icon-192x192.png -------------------------------------------------------------------------------- /public/images/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/public/images/icons/icon-512x512.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-hdpi/splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-mdpi/splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-xhdpi/splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-xxhdpi/splash.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-xxxhdpi/splash.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /cypress/component/FindAudioTracks.cy.ts: -------------------------------------------------------------------------------- 1 | describe('FindAudioTracks.cy.ts', () => { 2 | it('playground', () => { 3 | // cy.mount() 4 | }) 5 | }) -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-hdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-mdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-xhdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-xxhdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_notification_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CCExtractor/video2srt/HEAD/app/src/main/res/drawable-xxxhdpi/ic_notification_icon.png -------------------------------------------------------------------------------- /lighthouserc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ci: { 3 | collect: { 4 | /* Add configuration here */ 5 | staticDistDir: './dist', 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /env.sample: -------------------------------------------------------------------------------- 1 | VITE_WHISPER_MODEL_URL_LOCATION="https://s3.eu-central-1.amazonaws.com/video2srt-resources.ccextractor.org" 2 | VITE_GITHUB_URL = "https://github.com/ccextractor/video2srt" -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import './app.css' 2 | import App from './App.svelte' 3 | 4 | const app = new App({ 5 | target: document.getElementById('app'), 6 | }) 7 | 8 | export default app 9 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip 6 | -------------------------------------------------------------------------------- /src/lib/components/errors.js: -------------------------------------------------------------------------------- 1 | export const NO_AUDIO_TRACK = ` 2 | The Video file you have uploaded contains no audio track. 3 | Make sure to upload a video that contains one or check if the video is corrupted. 4 | If you believe this is an error submit an issue 5 | ` -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 2 | 3 | export default { 4 | // Consult https://svelte.dev/docs#compile-time-svelte-preprocess 5 | // for more information about preprocessors 6 | preprocess: vitePreprocess(), 7 | } 8 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | "./index.html", 5 | "./src/**/*.{js,ts,jsx,tsx,svelte}", 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [require("daisyui")], 11 | } 12 | 13 | -------------------------------------------------------------------------------- /src/lib/audio/errors.js: -------------------------------------------------------------------------------- 1 | export const INVALID_FILE = ` 2 | FFProbe failed to look up audio tracks on the file you have submitted. 3 | 1. You may have submitted a file that isn't a Video 4 | 2. The video file is not supported by FFProbe. 5 | If you believe this is an error, please submit an Issue report :) 6 | ` -------------------------------------------------------------------------------- /cypress/e2e/video_mp4.cy.ts: -------------------------------------------------------------------------------- 1 | Cypress.on('uncaught:exception', (err, runnable) => { 2 | // returning false here prevents Cypress from 3 | // failing the test 4 | return false 5 | }) 6 | 7 | describe('template spec', () => { 8 | it('passes', () => { 9 | cy.visit('/') 10 | cy.wait(5000) 11 | }) 12 | }) -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | import crossOriginIsolation from 'vite-plugin-cross-origin-isolation' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [ 8 | svelte(), 9 | crossOriginIsolation() 10 | ], 11 | }) 12 | -------------------------------------------------------------------------------- /src/lib/pickers/file_sizes.js: -------------------------------------------------------------------------------- 1 | export const MODEL_TO_SIZE = { 2 | "ggml-model-whisper-base.bin": 142, 3 | "ggml-model-whisper-base.en.bin": 142, 4 | "ggml-model-whisper-small.bin": 466, 5 | "ggml-model-whisper-small.en.bin": 466, 6 | "ggml-model-whisper-tiny.bin": 75, 7 | "ggml-model-whisper-tiny.en.bin": 75 8 | } 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/org/ccextractor/video2srt/twa/DelegationService.java: -------------------------------------------------------------------------------- 1 | package org.ccextractor.video2srt.twa; 2 | 3 | 4 | 5 | public class DelegationService extends 6 | com.google.androidbrowserhelper.trusted.DelegationService { 7 | @Override 8 | public void onCreate() { 9 | super.onCreate(); 10 | 11 | 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /.github/actions/pwa2Apk/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | USER root 3 | COPY entrypoint.sh /entrypoint.sh 4 | RUN apt update 5 | RUN apt -y install curl gnupg wget unzip 6 | RUN curl -sL https://deb.nodesource.com/setup_20.x | bash - 7 | RUN chmod +x /entrypoint.sh 8 | RUN apt -y install nodejs 9 | RUN npm install npm@latest -g 10 | RUN npm i -g @bubblewrap/cli 11 | ENTRYPOINT ["/entrypoint.sh"] -------------------------------------------------------------------------------- /.github/workflows/lighthouse.yml: -------------------------------------------------------------------------------- 1 | name: Lighthouse 2 | on: [push] 3 | jobs: 4 | lighthouseci: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | - uses: actions/setup-node@v3 9 | with: 10 | node-version: 16 11 | - run: npm install && npm install -g @lhci/cli@0.11.x 12 | - run: npm run build 13 | - run: lhci autorun 14 | -------------------------------------------------------------------------------- /src/lib/subtitle_conversions/srt.spec.ts: -------------------------------------------------------------------------------- 1 | import {describe, test, expect, vi } from 'vitest'; 2 | import { convert_to_srt } from './SRT.svelte'; 3 | import {SRT_OUTPUT, subs} from './test_data'; 4 | 5 | describe("SRT Component", () => { 6 | test("It should generate a SRT file", () => { 7 | const result = convert_to_srt(subs) 8 | expect(result).equal(SRT_OUTPUT); 9 | }) 10 | }); -------------------------------------------------------------------------------- /src/lib/subtitle_conversions/txt.spec.ts: -------------------------------------------------------------------------------- 1 | import {describe, test, expect, vi } from 'vitest'; 2 | import { convert_to_txt } from './TXT.svelte'; 3 | import {subs, TXT_OUTPUT} from './test_data'; 4 | 5 | describe("WebVTT Component", () => { 6 | test("It should generate a TXT file", () => { 7 | const result = convert_to_txt(subs) 8 | expect(result).equal(TXT_OUTPUT); 9 | }) 10 | }); -------------------------------------------------------------------------------- /src/lib/subtitle_conversions/webvtt.spec.ts: -------------------------------------------------------------------------------- 1 | import {describe, test, expect, vi } from 'vitest'; 2 | import { convert_to_webvtt } from './WebVTT.svelte'; 3 | import {subs, WEB_VTT_OUTPUT} from './test_data'; 4 | 5 | describe("WebVTT Component", () => { 6 | test("It should generate a SRT file", () => { 7 | const result = convert_to_webvtt(subs) 8 | expect(result).equal(WEB_VTT_OUTPUT); 9 | }) 10 | }); -------------------------------------------------------------------------------- /.github/actions/pwa2Apk/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -l 2 | 3 | echo "========================= Changing directory to $1 =========================" 4 | cd $1 5 | echo "========================= Building APK, it may take 3-4 minutes or more =========================" 6 | ( sleep 5 && while [ 1 ]; do sleep 1; echo y; done ) | bubblewrap build --skipPwaValidation --skipSigning 7 | cp ./*.apk .. 8 | cp ./app/build/outputs/bundle/release/*.aab .. 9 | echo "========================= APK building finished =========================" 10 | -------------------------------------------------------------------------------- /.github/actions/pwa2Apk/action.yml: -------------------------------------------------------------------------------- 1 | name: 'PWA to APK action' 2 | description: 'GitHub Action for Converting your PWA to APK.' 3 | branding: 4 | icon: 'package' 5 | color: 'orange' 6 | inputs: 7 | project-root-folder: 8 | description: 'Root folder for project generated by bubblewrap-cli' 9 | required: true 10 | outputs: 11 | message: 12 | description: "Status message from build script" 13 | 14 | runs: 15 | using: 'docker' 16 | image: 'Dockerfile' 17 | args: 18 | - ${{ inputs.project-root-folder }} 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Video2SRT 2 | Video2SRT is a tool that uses WebAssembly to transcribe video and audio files locally on your machine. The goal of Video2SRT is to make a universal open source private tool for transcribing all video and audio files. The current project is built with OpenAI’s whisper model 3 | 4 | This project is built with Svelte and uses WebAssembly along with GGerganov’s Whisper’s CPP bindings and FFMPEG.WASM for behind the scenes processing to develop the transcriptions 5 | 6 | # Installation 7 | 8 | `npm install` 9 | 10 | # How to Run locally? 11 | 12 | `npm run dev` 13 | 14 | 15 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "resolveJsonModule": true, 8 | /** 9 | * Typecheck JS in `.svelte` and `.js` files by default. 10 | * Disable checkJs if you'd like to use dynamic types in JS. 11 | * Note that setting allowJs false does not prevent the use 12 | * of JS in `.svelte` files. 13 | */ 14 | "allowJs": true, 15 | "checkJs": true, 16 | "isolatedModules": true, 17 | "sourceMap": false, 18 | "esModuleInterop": true, 19 | }, 20 | "include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"], 21 | "references": [{ "path": "./tsconfig.node.json" }] 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/res/xml/filepaths.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/raw/web_app_manifest.json: -------------------------------------------------------------------------------- 1 | {"name":"Video2SRT","short_name":"Video2SRT","theme_color":"#87F6FF","background_color":"#87F6FF","display":"standalone","orientation":"landscape","scope":"/","start_url":"/","icons":[{"src":"images/icons/icon-72x72.png","sizes":"72x72","type":"image/png"},{"src":"images/icons/icon-96x96.png","sizes":"96x96","type":"image/png"},{"src":"images/icons/icon-128x128.png","sizes":"128x128","type":"image/png"},{"src":"images/icons/icon-144x144.png","sizes":"144x144","type":"image/png"},{"src":"images/icons/icon-152x152.png","sizes":"152x152","type":"image/png"},{"src":"images/icons/icon-192x192.png","sizes":"192x192","type":"image/png"},{"src":"images/icons/icon-384x384.png","sizes":"384x384","type":"image/png"},{"src":"images/icons/icon-512x512.png","sizes":"512x512","type":"image/png"}]} -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | #F5F5F5 18 | 19 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | import vitePreprocessor from 'cypress-vite' 3 | import path from 'path' 4 | 5 | export default defineConfig({ 6 | component: { 7 | devServer: { 8 | framework: "svelte", 9 | bundler: "vite", 10 | }, 11 | }, 12 | 13 | e2e: { 14 | baseUrl: "http://127.0.0.1:5173", 15 | setupNodeEvents(on) { 16 | on('file:preprocessor', vitePreprocessor()); 17 | on('before:browser:launch', (browser = {}, launchOptions) => { 18 | if (browser.family === 'chromium' && browser.name !== 'electron') { 19 | // auto open devtools 20 | console.log("THIS SHOULD EXEC") 21 | launchOptions.args.push('--enable-experimental-web-platform-features') 22 | } 23 | }) 24 | }, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx1536m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | android.useAndroidX=true 15 | -------------------------------------------------------------------------------- /src/lib/get_seconds.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate the seconds of a subtitle block 3 | * @param last_line The subtitle block with the text 4 | * @returns The seconds of the last subtitle blocks 5 | */ 6 | export const getFinishedSeconds = function (last_line: string) : number { 7 | const pattern = /\d{2}:\d{2}:\d{2}\.\d{3}/g; // Regular expression to match timestamps 8 | 9 | const timestamps = last_line.match(pattern); // Find all timestamps in the range 10 | const endTimestamp: string = timestamps[1]; 11 | 12 | const endTimestampParts = endTimestamp.split(/[:.]/); 13 | const endTimestampSeconds: number = 14 | parseInt(endTimestampParts[0]) * 3600 + 15 | parseInt(endTimestampParts[1]) * 60 + 16 | parseInt(endTimestampParts[2]) + 17 | parseInt(endTimestampParts[3]) / 1000; 18 | 19 | return endTimestampSeconds; 20 | 21 | } -------------------------------------------------------------------------------- /.github/workflows/build_apk.yml: -------------------------------------------------------------------------------- 1 | on: [push] 2 | 3 | jobs: 4 | pwa_to_apk_job: 5 | runs-on: ubuntu-latest 6 | name: PWA to APK 7 | steps: 8 | - name: Checkout 9 | uses: actions/checkout@v2 10 | # Running PWA to apk action for apk generation 11 | - name: Running PWA2Apk 12 | uses: ./.github/actions/pwa2Apk 13 | with: 14 | project-root-folder: "." 15 | # This one below is a seperate action which I'm using for releasing apk 16 | - name: Release 17 | uses: softprops/action-gh-release@v1 18 | with: 19 | files: | 20 | *.apk 21 | *.aab 22 | # Selecting all the apk, aab files 23 | token: ${{ secrets.GITHUB_TOKEN }} 24 | tag_name: v1.0.0 # this is used in releases and has nothing to do with the pwa-to-apk-action 25 | # it is only used in softprops/action-gh-release action 26 | -------------------------------------------------------------------------------- /app/src/main/java/org/ccextractor/video2srt/twa/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.ccextractor.video2srt.twa; 17 | 18 | 19 | 20 | public class Application extends android.app.Application { 21 | 22 | 23 | 24 | @Override 25 | public void onCreate() { 26 | super.onCreate(); 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm install 30 | - run: npm run test 31 | -------------------------------------------------------------------------------- /src/lib/pickers/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The responsible error messages related to downloading a model 3 | */ 4 | export const REQUEST_FAILED = ` 5 | There was an error downloading this particular model of whisper! The Client could not reach the server, to retrieve the model. 6 | ` 7 | 8 | export const NOT_SUPPORTED = ` 9 | Cannot store data! 10 | ` 11 | 12 | export const CANNOT_ACCESS_INDEXDB = ` 13 | There was an error accessing Index DB! 14 | ` 15 | export const CANNOT_RETRIEVE_DATA = ` 16 | An Error occured while retrieving data from the store! 17 | ` 18 | export const INDEXDB_BLOCKED = ` 19 | Failed to Access IndexDB: Blocked 20 | ` 21 | 22 | export const INDEXDB_ABORT = ` 23 | Failed to Access IndexDB: Aborted 24 | ` 25 | export const FAILED_TO_STORE_IN_DB = ` 26 | Failed to put model in DB! 27 | ` 28 | 29 | export const FAILED_QUERYING_MODELS = ` 30 | Failed to query Models. 31 | ` 32 | 33 | export const FAILED_DELETING_DB = ` 34 | There was an error deleting all the models 35 | ` -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi/shortcut_legacy_background.xml: -------------------------------------------------------------------------------- 1 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/svelte' 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount) 37 | 38 | // Example use: 39 | // cy.mount(MyComponent) -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Video2SRT", 3 | "short_name": "Video2SRT", 4 | "theme_color": "#87F6FF", 5 | "background_color": "#87F6FF", 6 | "display": "standalone", 7 | "orientation": "landscape", 8 | "scope": "/", 9 | "start_url": "/", 10 | "icons": [ 11 | { 12 | "src": "images/icons/icon-72x72.png", 13 | "sizes": "72x72", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "images/icons/icon-96x96.png", 18 | "sizes": "96x96", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "images/icons/icon-128x128.png", 23 | "sizes": "128x128", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "images/icons/icon-144x144.png", 28 | "sizes": "144x144", 29 | "type": "image/png" 30 | }, 31 | { 32 | "src": "images/icons/icon-152x152.png", 33 | "sizes": "152x152", 34 | "type": "image/png" 35 | }, 36 | { 37 | "src": "images/icons/icon-192x192.png", 38 | "sizes": "192x192", 39 | "type": "image/png" 40 | }, 41 | { 42 | "src": "images/icons/icon-384x384.png", 43 | "sizes": "384x384", 44 | "type": "image/png" 45 | }, 46 | { 47 | "src": "images/icons/icon-512x512.png", 48 | "sizes": "512x512", 49 | "type": "image/png" 50 | } 51 | ] 52 | } -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 26 | 27 | [{ 28 | \"relation\": [\"delegate_permission/common.handle_all_urls\"], 29 | \"target\": { 30 | \"namespace\": \"web\", 31 | \"site\": \"https://video2srt.ccextractor.org\" 32 | } 33 | }] 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /privacy-policy.txt: -------------------------------------------------------------------------------- 1 | Privacy Policy 2 | 3 | This Privacy Policy describes how Video2SRT ("App") collects, uses, and discloses your information. 4 | 5 | Data Collection and Use 6 | 7 | Our app does not collect or store any personal information about you. This includes: 8 | Name 9 | Email address 10 | Location 11 | Device identifiers 12 | Any other personally identifiable information 13 | The app may process temporary data on your device for internal functionality, but this data is not stored or transmitted elsewhere. 14 | 15 | Information from Third Parties 16 | 17 | If you use any third-party services through the app, such as social media logins or analytics tools, please review their privacy policies to understand their data practices. 18 | Video2SRT is not responsible for the data collection or practices of any third-party services. 19 | 20 | Security 21 | 22 | We take steps to protect your information from unauthorized access, disclosure, alteration, or destruction, but no internet or mobile application is completely secure. 23 | We cannot guarantee the security of your information transmitted through the app. 24 | 25 | Changes to This Policy 26 | 27 | We may update this Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. 28 | You are advised to review this Privacy Policy periodically for any changes. 29 | -------------------------------------------------------------------------------- /src/lib/subtitle_conversions/TXT.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | 43 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /twa-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageId": "org.ccextractor.video2srt.twa", 3 | "host": "video2srt.ccextractor.org", 4 | "name": "Video2SRT", 5 | "launcherName": "Video2SRT", 6 | "display": "standalone", 7 | "themeColor": "#87F6FF", 8 | "themeColorDark": "#000000", 9 | "navigationColor": "#000000", 10 | "navigationColorDark": "#000000", 11 | "navigationDividerColor": "#000000", 12 | "navigationDividerColorDark": "#000000", 13 | "backgroundColor": "#87F6FF", 14 | "enableNotifications": true, 15 | "startUrl": "/", 16 | "iconUrl": "https://video2srt.ccextractor.org/images/icons/icon-512x512.png", 17 | "splashScreenFadeOutDuration": 300, 18 | "signingKey": { 19 | "path": "/Users/matejplavevski/Documents/Projects/GSoC/whisper-transcribe/android.keystore", 20 | "alias": "android" 21 | }, 22 | "appVersionName": "1", 23 | "appVersionCode": 1, 24 | "shortcuts": [], 25 | "generatorApp": "bubblewrap-cli", 26 | "webManifestUrl": "https://video2srt.ccextractor.org/manifest.json", 27 | "fallbackType": "customtabs", 28 | "features": {}, 29 | "alphaDependencies": { 30 | "enabled": false 31 | }, 32 | "enableSiteSettingsShortcut": true, 33 | "isChromeOSOnly": false, 34 | "isMetaQuest": false, 35 | "fullScopeUrl": "https://video2srt.ccextractor.org/", 36 | "minSdkVersion": 19, 37 | "orientation": "any", 38 | "fingerprints": [], 39 | "additionalTrustedOrigins": [], 40 | "retainedBundles": [], 41 | "appVersion": "1" 42 | } -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whisper-transcribe", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "[ -f .env ] || cp env.sample .env && vite", 8 | "build": "[ -f .env ] || cp env.sample .env && vite build && cp _headers dist/_headers", 9 | "preview": "vite preview", 10 | "check": "svelte-check --tsconfig ./tsconfig.json", 11 | "test": "vitest", 12 | "coverage": "vitest run --coverage" 13 | }, 14 | "devDependencies": { 15 | "@rollup/plugin-replace": "^5.0.2", 16 | "@smui/circular-progress": "^7.0.0-beta.8", 17 | "@smui/dialog": "^7.0.0-beta.8", 18 | "@smui/select": "^7.0.0-beta.8", 19 | "@sveltejs/vite-plugin-svelte": "^2.0.4", 20 | "@testing-library/svelte": "^4.0.3", 21 | "@tsconfig/svelte": "^4.0.1", 22 | "@vitest/coverage-v8": "^0.33.0", 23 | "autoprefixer": "^10.4.14", 24 | "cypress": "^12.13.0", 25 | "cypress-vite": "^1.4.2", 26 | "daisyui": "^3.2.1", 27 | "dotenv": "^16.3.1", 28 | "postcss": "^8.4.25", 29 | "reflect-metadata": "^0.1.13", 30 | "svelte": "^3.58.0", 31 | "svelte-check": "^3.3.1", 32 | "tailwindcss": "^3.3.2", 33 | "tslib": "^2.5.0", 34 | "typescript": "^5.0.2", 35 | "vite": "^4.3.9", 36 | "vite-plugin-cross-origin-isolation": "^0.1.6", 37 | "vitest": "^0.33.0" 38 | }, 39 | "dependencies": { 40 | "@ffmpeg/ffmpeg": "^0.11.6", 41 | "github-fork-ribbon-css": "^0.2.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/svelte,macos 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=svelte,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | 14 | # Thumbnails 15 | ._* 16 | 17 | # Files that might appear in the root of a volume 18 | .DocumentRevisions-V100 19 | .fseventsd 20 | .Spotlight-V100 21 | .TemporaryItems 22 | .Trashes 23 | .VolumeIcon.icns 24 | .com.apple.timemachine.donotpresent 25 | 26 | # Directories potentially created on remote AFP share 27 | .AppleDB 28 | .AppleDesktop 29 | Network Trash Folder 30 | Temporary Items 31 | .apdisk 32 | 33 | ### macOS Patch ### 34 | # iCloud generated files 35 | *.icloud 36 | 37 | ### Svelte ### 38 | # gitignore template for the SvelteKit, frontend web component framework 39 | # website: https://kit.svelte.dev/ 40 | 41 | .svelte-kit/ 42 | package 43 | 44 | # End of https://www.toptal.com/developers/gitignore/api/svelte,macos 45 | 46 | 47 | node_modules/ 48 | 49 | src/models/ 50 | node_modules 51 | dist 52 | coverage 53 | 54 | .env 55 | 56 | ### Android ### 57 | # Gradle files 58 | .gradle/ 59 | build/ 60 | 61 | 62 | # Local configuration file (sdk path, etc) 63 | local.properties 64 | 65 | 66 | # Log/OS Files 67 | *.log 68 | 69 | # Android Studio generated files and folders 70 | captures/ 71 | .externalNativeBuild/ 72 | .cxx/ 73 | *.apk 74 | *.aab 75 | *.idsig 76 | output.json 77 | 78 | # IntelliJ 79 | *.iml 80 | .idea/ 81 | misc.xml 82 | deploymentTargetDropDown.xml 83 | render.experimental.xml 84 | 85 | # Keystore files 86 | *.jks 87 | *.keystore 88 | -------------------------------------------------------------------------------- /src/lib/subtitle_conversions/WebVTT.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 | 51 | 52 | 55 | -------------------------------------------------------------------------------- /src/assets/svelte.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/subtitle_conversions/SRT.svelte: -------------------------------------------------------------------------------- 1 | 2 | 45 | 46 | 53 | 54 | -------------------------------------------------------------------------------- /src/lib/audio/FindAudioTracks.svelte: -------------------------------------------------------------------------------- 1 | 52 | -------------------------------------------------------------------------------- /app/src/main/java/org/ccextractor/video2srt/twa/LauncherActivity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Google Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.ccextractor.video2srt.twa; 17 | 18 | import android.content.pm.ActivityInfo; 19 | import android.net.Uri; 20 | import android.os.Build; 21 | import android.os.Bundle; 22 | 23 | 24 | 25 | public class LauncherActivity 26 | extends com.google.androidbrowserhelper.trusted.LauncherActivity { 27 | 28 | 29 | 30 | 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | // Setting an orientation crashes the app due to the transparent background on Android 8.0 35 | // Oreo and below. We only set the orientation on Oreo and above. This only affects the 36 | // splash screen and Chrome will still respect the orientation. 37 | // See https://github.com/GoogleChromeLabs/bubblewrap/issues/496 for details. 38 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { 39 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); 40 | } else { 41 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); 42 | } 43 | } 44 | 45 | @Override 46 | protected Uri getLaunchingUrl() { 47 | // Get the original launch Url. 48 | Uri uri = super.getLaunchingUrl(); 49 | 50 | 51 | 52 | return uri; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /cypress/e2e/notifications.cy.ts: -------------------------------------------------------------------------------- 1 | Cypress.on('uncaught:exception', (err, runnable) => { 2 | // returning false here prevents Cypress from 3 | // failing the test 4 | return false 5 | }) 6 | 7 | describe('Test if Features are present', () => { 8 | it('Notifications Denied', () => { 9 | cy.visit('/', { 10 | onBeforeLoad (win) { 11 | cy.stub(win.Notification, 'permission', 'denied') 12 | cy.stub(win.Notification, 'requestPermission').resolves('denied').as('ask') 13 | cy.stub(win, 'Notification').as('Notification') 14 | }, 15 | }) 16 | cy.get('#subscribe-notifications-button').contains("Notifications denied") 17 | }) 18 | 19 | it('Ask for Permission', () => { 20 | cy.visit('index.html', { 21 | onBeforeLoad (win) { 22 | cy.stub(win.Notification, 'permission', 'unknown') 23 | cy.stub(win.Notification, 'requestPermission').resolves('granted').as('ask') 24 | cy.stub(win, 'Notification').as('Notification') 25 | }, 26 | }) 27 | 28 | cy.get('#subscribe-notifications-button').click() 29 | cy.get('@ask').should('have.been.calledOnce') 30 | cy.get('#subscribe-notifications-button').should('not.exist'); 31 | }) 32 | 33 | it('Permission Previously Granted', () => { 34 | // see cy.visit options in https://on.cypress.io/visit 35 | cy.visit('index.html', { 36 | onBeforeLoad (win) { 37 | // https://on.cypress.io/stub 38 | cy.stub(win.Notification, 'permission', 'granted') 39 | cy.stub(win, 'Notification').as('Notification') 40 | }, 41 | }) 42 | 43 | cy.get('#subscribe-notifications-button').should('not.exist'); 44 | 45 | }); 46 | 47 | it('shows alert if the browser does not support notifications', () => { 48 | cy.visit('/', { 49 | onBeforeLoad (win) { 50 | delete win.Notification 51 | }, 52 | }) 53 | 54 | cy.get('#subscribe-notifications-button').should('not.exist'); 55 | 56 | }) 57 | 58 | }) -------------------------------------------------------------------------------- /src/lib/subtitle_conversions/test_data.ts: -------------------------------------------------------------------------------- 1 | export const subs = [ 2 | "[00:00:00.000 --> 00:00:02.560] Hello World!", 3 | "[00:00:02.560 --> 00:00:07.440] This is how subtitles look like from whisper", 4 | "[00:00:07.440 --> 00:00:13.160] 42 is the meaning to the life the universe and everything", 5 | "[00:00:13.160 --> 00:00:17.360] And you should carry a towel everywhere", 6 | "[00:00:17.360 --> 00:00:19.760] but now I have to go", 7 | "[00:00:19.760 --> 00:00:21.200] So Long, and Thanks for All the Fish" 8 | ] 9 | 10 | export const WEB_VTT_OUTPUT = `WEBVTT 11 | - 1 12 | 00:00:00.000 --> 00:00:02.560 13 | Hello World! 14 | 15 | - 2 16 | 00:00:02.560 --> 00:00:07.440 17 | This is how subtitles look like from whisper 18 | 19 | - 3 20 | 00:00:07.440 --> 00:00:13.160 21 | 42 is the meaning to the life the universe and everything 22 | 23 | - 4 24 | 00:00:13.160 --> 00:00:17.360 25 | And you should carry a towel everywhere 26 | 27 | - 5 28 | 00:00:17.360 --> 00:00:19.760 29 | but now I have to go 30 | 31 | - 6 32 | 00:00:19.760 --> 00:00:21.200 33 | So Long, and Thanks for All the Fish 34 | 35 | ` 36 | 37 | export const SRT_OUTPUT =`1 38 | 00:00:00,000 --> 00:00:02,560 39 | Hello World! 40 | 41 | 2 42 | 00:00:02,560 --> 00:00:07,440 43 | This is how subtitles look like from whisper 44 | 45 | 3 46 | 00:00:07,440 --> 00:00:13,160 47 | 42 is the meaning to the life the universe and everything 48 | 49 | 4 50 | 00:00:13,160 --> 00:00:17,360 51 | And you should carry a towel everywhere 52 | 53 | 5 54 | 00:00:17,360 --> 00:00:19,760 55 | but now I have to go 56 | 57 | 6 58 | 00:00:19,760 --> 00:00:21,200 59 | So Long, and Thanks for All the Fish 60 | 61 | ` 62 | 63 | export const TXT_OUTPUT = `[00:00:00.000 --> 00:00:02.560] Hello World! 64 | [00:00:02.560 --> 00:00:07.440] This is how subtitles look like from whisper 65 | [00:00:07.440 --> 00:00:13.160] 42 is the meaning to the life the universe and everything 66 | [00:00:13.160 --> 00:00:17.360] And you should carry a towel everywhere 67 | [00:00:17.360 --> 00:00:19.760] but now I have to go 68 | [00:00:19.760 --> 00:00:21.200] So Long, and Thanks for All the Fish 69 | ` -------------------------------------------------------------------------------- /cypress/e2e/check_for_features.cy.ts: -------------------------------------------------------------------------------- 1 | Cypress.on('uncaught:exception', (err, runnable) => { 2 | // returning false here prevents Cypress from 3 | // failing the test 4 | return false 5 | }) 6 | 7 | describe('Test if Features are present', () => { 8 | it('Site Loads', () => { 9 | cy.visit('/') 10 | }) 11 | 12 | it('Contains Models', () => { 13 | cy.visit('/') 14 | cy.get('body').find('#models-selector') 15 | }); 16 | 17 | it('Contains Title', () => { 18 | cy.visit('/') 19 | cy.contains('Video 2 SRT') 20 | 21 | }) 22 | 23 | it('Contains Fork', () => { 24 | cy.visit('/') 25 | cy.contains('Fork') 26 | 27 | }) 28 | 29 | it('Contains Delete Models Button', () => { 30 | cy.visit('/') 31 | cy.get('body').find('#delete-models-button').contains('Delete Models') 32 | }) 33 | 34 | it('Contains File Upload', () => { 35 | cy.visit('/') 36 | cy.get('body').find("input[type='file']") 37 | }) 38 | 39 | it('Contains Convert Button', () => { 40 | cy.visit('/') 41 | cy.get('body').find('#convert-button').contains('Convert') 42 | }) 43 | 44 | it('Contains Slider', () => { 45 | // Check if the Slider is Present 46 | cy.visit('/') 47 | cy.get('body').find('#stepper').should('have.value', 16) 48 | 49 | // Check if it reflects the label 50 | cy.get('body').find('#label-stepper').should('have.text', 'Threads in use: 16') 51 | 52 | // Set value 53 | cy.get('#stepper').invoke('val', 2).trigger('change') 54 | cy.get('body').find('#label-stepper').should('have.text', 'Threads in use: 2') 55 | }) 56 | 57 | it('Contains Language Selector', () => { 58 | // Check if the language field is present 59 | cy.visit('/') 60 | cy.get('body').find('#languages').should('have.value', 'en') 61 | 62 | // Check if the Checkbox shows up 63 | cy.get('#languages').select('mk').should('have.value', 'mk') 64 | cy.get('body').find("input[type='checkbox']") 65 | 66 | }) 67 | 68 | it('Check for a notification button', () => { 69 | // Check if present with notifications 70 | cy.visit('/') 71 | cy.get('body').find("#subscribe-notifications-button").contains('Subscribe to Notifications') 72 | }) 73 | }) -------------------------------------------------------------------------------- /src/lib/components/Notifications.svelte: -------------------------------------------------------------------------------- 1 | 58 | 59 | {#if !HIDE_NOTIFICATION_BUTTON } 60 | 61 | {/if} -------------------------------------------------------------------------------- /public/js/libmain.worker.js: -------------------------------------------------------------------------------- 1 | "use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",data=>onmessage({data:data}));var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(fs.readFileSync(f,"utf8")+"//# sourceURL="+f)},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var module=Module["wasmModule"];Module["wasmModule"]=null;var instance=new WebAssembly.Instance(module,info);return receiveInstance(instance)};self.onunhandledrejection=e=>{throw e.reason??e};function handleMessage(e){try{if(e.data.cmd==="load"){let messageQueue=[];self.onmessage=e=>messageQueue.push(e);self.startWorker=instance=>{postMessage({"cmd":"loaded"});for(let msg of messageQueue){handleMessage(msg)}self.onmessage=handleMessage};Module["wasmModule"]=e.data.wasmModule;for(const handler of e.data.handlers){Module[handler]=function(){postMessage({cmd:"callHandler",handler:handler,args:[...arguments]})}}Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}}else if(e.data.cmd==="run"){Module["__emscripten_thread_init"](e.data.pthread_ptr,0,0,1);Module["__emscripten_thread_mailbox_await"](e.data.pthread_ptr);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){Module["__embind_initialize_bindings"]();initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){throw ex}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="checkMailbox"){if(initializedJS){Module["checkMailbox"]()}}else if(e.data.cmd){err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}self.onmessage=handleMessage; 2 | -------------------------------------------------------------------------------- /public/js/ffprobe-wasm.worker.js: -------------------------------------------------------------------------------- 1 | "use strict";var Module={};var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){var nodeWorkerThreads=require("worker_threads");var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",function(data){onmessage({data:data})});var fs=require("fs");Object.assign(global,{self:global,require:require,Module:Module,location:{href:__filename},Worker:nodeWorkerThreads.Worker,importScripts:function(f){(0,eval)(fs.readFileSync(f,"utf8"))},postMessage:function(msg){parentPort.postMessage(msg)},performance:global.performance||{now:function(){return Date.now()}}})}var initializedJS=false;var pendingNotifiedProxyingQueues=[];function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");if(ENVIRONMENT_IS_NODE){fs.writeSync(2,text+"\n");return}console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:Module["_pthread_self"]()})}var err=threadPrintErr;self.alert=threadAlert;Module["instantiateWasm"]=(info,receiveInstance)=>{var instance=new WebAssembly.Instance(Module["wasmModule"],info);receiveInstance(instance);Module["wasmModule"]=null;return instance.exports};self.onmessage=e=>{try{if(e.data.cmd==="load"){Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob=="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;Module["__emscripten_thread_init"](e.data.threadInfoStruct,0,0,1);Module["establishStackSpace"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].threadInitTLS();if(!initializedJS){Module["___embind_register_native_and_builtin_types"]();pendingNotifiedProxyingQueues.forEach(queue=>{Module["executeNotifiedProxyingQueue"](queue)});pendingNotifiedProxyingQueues=[];initializedJS=true}try{Module["invokeEntryPoint"](e.data.start_routine,e.data.arg)}catch(ex){if(ex!="unwind"){if(ex instanceof Module["ExitStatus"]){if(Module["keepRuntimeAlive"]()){}else{Module["__emscripten_thread_exit"](ex.status)}}else{throw ex}}}}else if(e.data.cmd==="cancel"){if(Module["_pthread_self"]()){Module["__emscripten_thread_exit"](-1)}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processProxyingQueue"){if(initializedJS){Module["executeNotifiedProxyingQueue"](e.data.queue)}else{pendingNotifiedProxyingQueues.push(e.data.queue)}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex&&ex.stack)err(ex.stack);if(Module["__emscripten_thread_crashed"]){Module["__emscripten_thread_crashed"]()}throw ex}}; 2 | -------------------------------------------------------------------------------- /src/lib/pickers/Languages.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /public/js/ffprobe-worker.js: -------------------------------------------------------------------------------- 1 | onmessage = (e) => { 2 | const type = e.data[0]; 3 | const file = e.data[1]; 4 | 5 | let data; 6 | 7 | switch (type) { 8 | case 'get_file_info': 9 | // Mount FS for files. 10 | if (!FS.analyzePath('/work').exists) { 11 | FS.mkdir('/work'); 12 | } 13 | FS.mount(WORKERFS, { files: [file] }, '/work'); 14 | 15 | // Call the wasm module. 16 | const info = Module.get_file_info('/work/' + file.name); 17 | 18 | // Remap streams into collection. 19 | const s = []; 20 | for (let i = 0; i < info.streams.size(); i++) { 21 | const tags = {}; 22 | for (let j = 0; j < info.streams.get(i).tags.size(); j++) { 23 | const t = info.streams.get(i).tags.get(j); 24 | tags[t.key] = t.value; 25 | } 26 | s.push({...info.streams.get(i), ...{ tags}}); 27 | } 28 | 29 | // Remap chapters into collection. 30 | const c = []; 31 | for (let i = 0; i < info.chapters.size(); i++) { 32 | const tags = {}; 33 | for (let j = 0; j < info.chapters.get(i).tags.size(); j++) { 34 | const t = info.chapters.get(i).tags.get(j); 35 | tags[t.key] = t.value; 36 | } 37 | c.push({...info.chapters.get(i), ...{tags}}); 38 | } 39 | 40 | const versions = { 41 | libavutil: Module.avutil_version(), 42 | libavcodec: Module.avcodec_version(), 43 | libavformat: Module.avformat_version(), 44 | }; 45 | 46 | // Send back data response. 47 | data = { 48 | ...info, 49 | streams: s, 50 | chapters: c, 51 | versions, 52 | } 53 | postMessage(data); 54 | 55 | // Cleanup mount. 56 | FS.unmount('/work'); 57 | break; 58 | 59 | case 'get_frames': 60 | if (!FS.analyzePath('/work').exists) { 61 | FS.mkdir('/work'); 62 | } 63 | FS.mount(WORKERFS, { files: [file] }, '/work'); 64 | 65 | const offset = e.data[2]; 66 | const frames = Module.get_frames('/work/' + file.name, offset); 67 | 68 | // Remap frames into collection. 69 | const f = []; 70 | for (let i = 0; i < frames.frames.size(); i++) { 71 | f.push(frames.frames.get(i)); 72 | } 73 | 74 | data = { 75 | ...frames, 76 | frames: f, 77 | } 78 | postMessage(data); 79 | 80 | // Cleanup mount. 81 | FS.unmount('/work'); 82 | break; 83 | 84 | default: 85 | break; 86 | } 87 | 88 | } 89 | self.importScripts('ffprobe-wasm.js'); // Load ffprobe into worker context. -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /src/lib/audio/ExtractAudioTracks.svelte: -------------------------------------------------------------------------------- 1 | 98 | 99 | {#if started} 100 |
{progress.toFixed(2)}%
101 | {/if} 102 | 103 | {#if show_audio} 104 |