├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── help_wanted.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── ci.yml ├── .gitignore ├── .npmrc ├── .vscode ├── .debug.script.mjs ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_zh.md ├── auto-imports.d.ts ├── components.d.ts ├── electron-builder.json5 ├── images ├── 1.png ├── 2.png ├── 3.png └── 4.png ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── electron-vite-vue.gif ├── favicon.ico ├── node.png └── resources │ ├── adobe-lib │ └── esdebugger-core │ │ ├── mac │ │ ├── Resources.tgz │ │ └── esdcorelibinterface.node │ │ └── win │ │ ├── win32 │ │ ├── Resources │ │ │ ├── adobe_caps.dll │ │ │ └── localization │ │ │ │ ├── extendscript-de_DE.dat │ │ │ │ ├── extendscript-en_US.dat │ │ │ │ ├── extendscript-es_ES.dat │ │ │ │ ├── extendscript-fr_FR.dat │ │ │ │ ├── extendscript-it_IT.dat │ │ │ │ └── extendscript-ja_JP.dat │ │ └── esdcorelibinterface.node │ │ └── x64 │ │ ├── Resources │ │ ├── adobe_caps.dll │ │ └── localization │ │ │ ├── extendscript-de_DE.dat │ │ │ ├── extendscript-en_US.dat │ │ │ ├── extendscript-es_ES.dat │ │ │ ├── extendscript-fr_FR.dat │ │ │ ├── extendscript-it_IT.dat │ │ │ └── extendscript-ja_JP.dat │ │ └── esdcorelibinterface.node │ ├── autocut.jsx.template │ └── temp.prproj ├── src ├── env.d.ts ├── main.ts ├── main │ ├── adobe │ │ ├── core.ts │ │ └── index.ts │ ├── autocut │ │ ├── check.ts │ │ ├── download.ts │ │ └── index.ts │ ├── electron-env.d.ts │ ├── ffmpeg │ │ └── index.ts │ ├── main │ │ ├── autocut.ts │ │ └── index.ts │ ├── preload │ │ └── index.ts │ ├── subtitle │ │ └── index.ts │ └── utils │ │ ├── index.ts │ │ └── path.ts ├── renderer │ ├── App.vue │ ├── assets │ │ ├── FFmpeg.png │ │ ├── electron.svg │ │ ├── vite.svg │ │ └── vue.svg │ ├── components │ │ └── HelloWorld.vue │ ├── hooks │ │ ├── useAutoCut.ts │ │ ├── useFFmpeg.ts │ │ └── usePrVersions.ts │ ├── i18n │ │ ├── en.json │ │ ├── index.ts │ │ └── zh.json │ ├── interface │ │ ├── adobe.ts │ │ ├── autocut.ts │ │ └── ffmpeg.ts │ ├── layout │ │ └── index.vue │ ├── router │ │ └── index.ts │ ├── samples │ │ └── node-api.ts │ ├── store │ │ ├── config.ts │ │ ├── index.ts │ │ └── status.ts │ └── views │ │ ├── edit │ │ ├── Subtitle.vue │ │ ├── components │ │ │ ├── SubtitleItem.vue │ │ │ └── exportToPr.vue │ │ └── index.vue │ │ ├── setup │ │ └── autocut.vue │ │ └── status │ │ └── index.vue └── types.d.ts ├── test └── spec │ └── utls_path.spec.ts ├── tsconfig.json ├── tsconfig.node.json ├── unocss.config.ts └── vite.config.ts /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: ["plugin:vue/vue3-essential"], 7 | parserOptions: { 8 | ecmaVersion: 12, 9 | parser: "@typescript-eslint/parser", 10 | sourceType: "module", 11 | }, 12 | plugins: ["vue", "@typescript-eslint"], 13 | rules: { 14 | indent: ["error", 2, { SwitchCase: 1 }], 15 | "max-len": [ 16 | "error", 17 | { 18 | code: 120, 19 | tabWidth: 2, 20 | ignoreRegExpLiterals: true, 21 | }, 22 | ], 23 | "comma-dangle": ["error", "always-multiline"], 24 | "vue/multi-word-component-names": [0], 25 | quotes: ["error", "double"], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: 🐞 Bug report 4 | about: Create a report to help us improve 5 | title: "[Bug] the title of bug report" 6 | labels: bug 7 | assignees: '' 8 | 9 | --- 10 | 11 | #### Describe the bug 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/help_wanted.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🥺 Help wanted 3 | about: Confuse about the use of autocut-alient 4 | title: "[Help] the title of help wanted report" 5 | labels: help wanted 6 | assignees: '' 7 | 8 | --- 9 | 10 | #### Describe the problem you confuse 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### What is the purpose of this pull request? 8 | 9 | - [ ] Bug fix 10 | - [ ] New Feature 11 | - [ ] Documentation update 12 | - [ ] Other 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 7 | 8 | jobs: 9 | createrelease: 10 | name: Create Release 11 | runs-on: [ubuntu-latest] 12 | steps: 13 | - name: Create Release 14 | id: create_release 15 | uses: actions/create-release@v1 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | with: 19 | tag_name: ${{ github.ref }} 20 | release_name: Release ${{ github.ref }} 21 | draft: false 22 | prerelease: false 23 | - name: Output Release URL File 24 | run: echo "${{ steps.create_release.outputs.upload_url }}" > release_url.txt 25 | - name: Save Release URL File for publish 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: release_url 29 | path: release_url.txt 30 | 31 | build: 32 | runs-on: ${{ matrix.os }} 33 | 34 | strategy: 35 | matrix: 36 | include: 37 | - os: macos-12 38 | TARGET: macos 39 | FORMAT: dmg 40 | ASSET_MIME: application/x-apple-diskimage 41 | - os: windows-latest 42 | TARGET: windows 43 | FORMAT: exe 44 | ASSET_MIME: application/vnd.microsoft.portable-executable 45 | 46 | steps: 47 | - name: Checkout Code 48 | uses: actions/checkout@v2 49 | 50 | - name: Setup Node.js 51 | uses: actions/setup-node@v2 52 | with: 53 | node-version: 18 54 | 55 | - name: Install Dependencies 56 | run: npm install 57 | 58 | - name: Build Release Files 59 | run: npm run build 60 | env: 61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 62 | 63 | # - name: Upload Artifact 64 | # uses: actions/upload-artifact@v4 65 | # with: 66 | # name: release_on_${{ matrix. os }} 67 | # path: release/ 68 | # retention-days: 5 69 | 70 | - name: Load Release URL File from release job 71 | uses: actions/download-artifact@v4 72 | with: 73 | name: release_url 74 | 75 | - name: Get Release File Name & Upload URL 76 | id: get_release_info 77 | shell: bash 78 | run: | 79 | value=`cat release_url.txt` 80 | echo ::set-output name=upload_url::$value 81 | 82 | - name: Set env 83 | shell: bash 84 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 85 | 86 | - name: Upload Release Asset 87 | id: upload-release-asset 88 | uses: softprops/action-gh-release@v2 89 | env: 90 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 91 | with: 92 | files: | 93 | ./release/${{ env.RELEASE_VERSION }}/autocut-client_${{ env.RELEASE_VERSION }}.${{ matrix.FORMAT }} 94 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - master 7 | 8 | permissions: 9 | pull-requests: write 10 | 11 | jobs: 12 | job1: 13 | name: Check Not Allowed File Changes 14 | runs-on: ubuntu-latest 15 | outputs: 16 | markdown_change: ${{ steps.filter_markdown.outputs.change }} 17 | markdown_files: ${{ steps.filter_markdown.outputs.change_files }} 18 | steps: 19 | 20 | - name: Check Not Allowed File Changes 21 | uses: dorny/paths-filter@v2 22 | id: filter_not_allowed 23 | with: 24 | list-files: json 25 | filters: | 26 | change: 27 | - 'package-lock.json' 28 | - 'yarn.lock' 29 | - 'pnpm-lock.yaml' 30 | 31 | # ref: https://github.com/github/docs/blob/main/.github/workflows/triage-unallowed-contributions.yml 32 | - name: Comment About Changes We Can't Accept 33 | if: ${{ steps.filter_not_allowed.outputs.change == 'true' }} 34 | uses: actions/github-script@v6 35 | with: 36 | script: | 37 | let workflowFailMessage = "It looks like you've modified some files that we can't accept as contributions." 38 | try { 39 | const badFilesArr = [ 40 | 'package-lock.json', 41 | 'yarn.lock', 42 | 'pnpm-lock.yaml', 43 | ] 44 | const badFiles = badFilesArr.join('\n- ') 45 | const reviewMessage = `👋 Hey there spelunker. It looks like you've modified some files that we can't accept as contributions. The complete list of files we can't accept are:\n- ${badFiles}\n\nYou'll need to revert all of the files you changed in that list using [GitHub Desktop](https://docs.github.com/en/free-pro-team@latest/desktop/contributing-and-collaborating-using-github-desktop/managing-commits/reverting-a-commit) or \`git checkout origin/main \`. Once you get those files reverted, we can continue with the review process. :octocat:\n\nMore discussion:\n- https://github.com/electron-vite/electron-vite-vue/issues/192` 46 | createdComment = await github.rest.issues.createComment({ 47 | owner: context.repo.owner, 48 | repo: context.repo.repo, 49 | issue_number: context.payload.number, 50 | body: reviewMessage, 51 | }) 52 | workflowFailMessage = `${workflowFailMessage} Please see ${createdComment.data.html_url} for details.` 53 | } catch(err) { 54 | console.log("Error creating comment.", err) 55 | } 56 | core.setFailed(workflowFailMessage) 57 | 58 | - name: Check Not Linted Markdown 59 | if: ${{ always() }} 60 | uses: dorny/paths-filter@v2 61 | id: filter_markdown 62 | with: 63 | list-files: shell 64 | filters: | 65 | change: 66 | - added|modified: '*.md' 67 | 68 | 69 | job2: 70 | name: Lint Markdown 71 | runs-on: ubuntu-latest 72 | needs: job1 73 | if: ${{ always() && needs.job1.outputs.markdown_change == 'true' }} 74 | steps: 75 | - name: Checkout Code 76 | uses: actions/checkout@v3 77 | with: 78 | ref: ${{ github.event.pull_request.head.sha }} 79 | 80 | - name: Lint markdown 81 | run: npx markdownlint-cli ${{ needs.job1.outputs.markdown_files }} --ignore node_modules 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | 24 | release 25 | .vscode/.debug.env 26 | dist-electron 27 | 28 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers=true 2 | -------------------------------------------------------------------------------- /.vscode/.debug.script.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { fileURLToPath } from 'url' 4 | import { createRequire } from 'module' 5 | import { spawn } from 'child_process' 6 | 7 | const pkg = createRequire(import.meta.url)('../package.json') 8 | const __dirname = path.dirname(fileURLToPath(import.meta.url)) 9 | 10 | // write .debug.env 11 | const envContent = Object.entries(pkg.debug.env).map(([key, val]) => `${key}=${val}`) 12 | fs.writeFileSync(path.join(__dirname, '.debug.env'), envContent.join('\n')) 13 | 14 | // bootstrap 15 | spawn( 16 | // TODO: terminate `npm run dev` when Debug exits. 17 | process.platform === 'win32' ? 'npm.cmd' : 'npm', 18 | ['run', 'dev'], 19 | { 20 | stdio: 'inherit', 21 | env: Object.assign(process.env, { VSCODE_DEBUG: 'true' }), 22 | }, 23 | ) 24 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Vue.volar", 4 | "Vue.vscode-typescript-vue-plugin", 5 | "lokalise.i18n-ally" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "compounds": [ 7 | { 8 | "name": "Debug App", 9 | "preLaunchTask": "Before Debug", 10 | "configurations": [ 11 | "Debug Main Process", 12 | "Debug Renderer Process" 13 | ], 14 | "presentation": { 15 | "hidden": false, 16 | "group": "", 17 | "order": 1 18 | }, 19 | "stopAll": true 20 | } 21 | ], 22 | "configurations": [ 23 | { 24 | "name": "Debug Main Process", 25 | "type": "pwa-node", 26 | "request": "launch", 27 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 28 | "windows": { 29 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 30 | }, 31 | "runtimeArgs": [ 32 | "--remote-debugging-port=9229", 33 | "." 34 | ], 35 | "envFile": "${workspaceFolder}/.vscode/.debug.env", 36 | "console": "integratedTerminal" 37 | }, 38 | { 39 | "name": "Debug Renderer Process", 40 | "port": 9229, 41 | "request": "attach", 42 | "type": "pwa-chrome", 43 | "timeout": 60000, 44 | "skipFiles": [ 45 | "/**", 46 | "${workspaceRoot}/node_modules/**", 47 | "${workspaceRoot}/dist-electron/**", 48 | // Skip files in host(VITE_DEV_SERVER_URL) 49 | "http://127.0.0.1:3344/**" 50 | ] 51 | }, 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "json.schemas": [ 5 | { 6 | "fileMatch": [ 7 | "/*electron-builder.json5" 8 | ], 9 | "url": "https://json.schemastore.org/electron-builder" 10 | } 11 | ], 12 | "prettier.enable": false, 13 | "eslint.format.enable": true, 14 | "editor.formatOnSave": false, 15 | "editor.codeActionsOnSave": { 16 | "source.fixAll.eslint": true 17 | }, 18 | "i18n-ally.localesPaths": [ 19 | "src/renderer/i18n" 20 | ], 21 | "i18n-ally.keystyle": "nested" 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "Before Debug", 8 | "type": "shell", 9 | "command": "node .vscode/.debug.script.mjs", 10 | "isBackground": true, 11 | "problemMatcher": { 12 | "owner": "typescript", 13 | "fileLocation": "relative", 14 | "pattern": { 15 | // TODO: correct "regexp" 16 | "regexp": "^([a-zA-Z]\\:\/?([\\w\\-]\/?)+\\.\\w+):(\\d+):(\\d+): (ERROR|WARNING)\\: (.*)$", 17 | "file": 1, 18 | "line": 3, 19 | "column": 4, 20 | "code": 5, 21 | "message": 6 22 | }, 23 | "background": { 24 | "activeOnStart": true, 25 | "beginsPattern": "^.*VITE v.* ready in \\d* ms.*$", 26 | "endsPattern": "^.*\\[startup\\] Electron App.*$" 27 | } 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) huali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoCut Client 2 | 3 | [中文文档](README_zh.md) 4 | 5 | This repo is an out-of-the-box client for [AutoCut](https://github.com/mli/autocut). 6 | 7 | The project can quickly generate video subtitles and edit the video by selecting subtitle clips. The editing results can be saved directly as video, and the project supports exporting the editing results directly to Adobe Premiere Pro for easy secondary creation. 8 | 9 | [Click to check prototype](https://js.design/f/T0LLLh?p=g8rtx09zle). 10 | 11 | # Screenshot 12 | 13 | ![Home](./images/1.png) 14 | ![Start](./images/2.png) 15 | ![Edit](./images/3.png) 16 | ![ExportToPR](./images/4.png) 17 | 18 | # Download 19 | 20 | [Get newest release](https://github.com/zcf0508/autocut-client/releases). 21 | 22 | # RoadMap 23 | 24 | - [x] Installation status 25 | - [x] AutoCut installation guide page 26 | - [x] Select a file and transcribe 27 | - [x] Select subtitle clips 28 | - [x] Generate video 29 | - [x] Export to Adobe Premiere Pro 30 | - [x] Edit subtitle 31 | - [x] i18n 32 | - [x] Support for MacOS 33 | - [x] Video clip preview 34 | 35 | 36 | # Reference 37 | 38 | - [AutoCut](https://github.com/mli/autocut) 39 | - [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue) 40 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # AutoCut Client 2 | 3 | 该项目为 [AutoCut](https://github.com/mli/autocut) 提供一个开箱即用的客户端。 4 | 5 | 该项目可以快速识别视频字幕,并通过选择字幕片段的方式来对视频进行剪辑。编辑结果可直接保存为视频,同时该项目支持直接将编辑结果导出到 Adobe Premiere Pro 中,方便进行二次创作。 6 | 7 | 查看 [原型图](https://js.design/f/T0LLLh?p=g8rtx09zle) 8 | 9 | # 截图 10 | 11 | ![Home](./images/1.png) 12 | ![Start](./images/2.png) 13 | ![Edit](./images/3.png) 14 | ![ExportToPR](./images/4.png) 15 | 16 | # 下载 17 | 18 | 查看最新[下载地址](https://github.com/zcf0508/autocut-client/releases)。 19 | 20 | # RoadMap 21 | 22 | - [x] 安装状态监测 23 | - [x] AutoCut 安装引导页 24 | - [x] 选择视频文件并生成字幕页面 25 | - [x] 字幕剪辑页面 26 | - [x] 导出视频 27 | - [x] 编辑结果导入到 Pr 中 28 | - [x] 字幕编辑修改 29 | - [x] i18n 30 | - [x] MacOS 系统支持 31 | - [x] 剪辑视频预览 32 | 33 | 34 | # 参考 35 | 36 | - [AutoCut](https://github.com/mli/autocut) 37 | - [electron-vite-vue](https://github.com/electron-vite/electron-vite-vue) 38 | -------------------------------------------------------------------------------- /auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | const CONFIG_NAME: typeof import('./src/renderer/store/config')['CONFIG_NAME'] 5 | const EffectScope: typeof import('vue')['EffectScope'] 6 | const afterAll: typeof import('vitest')['afterAll'] 7 | const afterEach: typeof import('vitest')['afterEach'] 8 | const assert: typeof import('vitest')['assert'] 9 | const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] 10 | const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] 11 | const beforeAll: typeof import('vitest')['beforeAll'] 12 | const beforeEach: typeof import('vitest')['beforeEach'] 13 | const chai: typeof import('vitest')['chai'] 14 | const computed: typeof import('vue')['computed'] 15 | const computedAsync: typeof import('@vueuse/core')['computedAsync'] 16 | const computedEager: typeof import('@vueuse/core')['computedEager'] 17 | const computedInject: typeof import('@vueuse/core')['computedInject'] 18 | const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] 19 | const configStore: typeof import('./src/renderer/store/config')['configStore'] 20 | const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] 21 | const controlledRef: typeof import('@vueuse/core')['controlledRef'] 22 | const createApp: typeof import('vue')['createApp'] 23 | const createEventHook: typeof import('@vueuse/core')['createEventHook'] 24 | const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] 25 | const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] 26 | const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] 27 | const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] 28 | const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] 29 | const customRef: typeof import('vue')['customRef'] 30 | const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] 31 | const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] 32 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 33 | const defineComponent: typeof import('vue')['defineComponent'] 34 | const describe: typeof import('vitest')['describe'] 35 | const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] 36 | const effectScope: typeof import('vue')['effectScope'] 37 | const expect: typeof import('vitest')['expect'] 38 | const extendRef: typeof import('@vueuse/core')['extendRef'] 39 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 40 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 41 | const h: typeof import('vue')['h'] 42 | const hamiVuex: typeof import('./src/renderer/store/index')['hamiVuex'] 43 | const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] 44 | const inject: typeof import('vue')['inject'] 45 | const isDefined: typeof import('@vueuse/core')['isDefined'] 46 | const isProxy: typeof import('vue')['isProxy'] 47 | const isReactive: typeof import('vue')['isReactive'] 48 | const isReadonly: typeof import('vue')['isReadonly'] 49 | const isRef: typeof import('vue')['isRef'] 50 | const it: typeof import('vitest')['it'] 51 | const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] 52 | const markRaw: typeof import('vue')['markRaw'] 53 | const nextTick: typeof import('vue')['nextTick'] 54 | const onActivated: typeof import('vue')['onActivated'] 55 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 56 | const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] 57 | const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 58 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 59 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 60 | const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] 61 | const onDeactivated: typeof import('vue')['onDeactivated'] 62 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 63 | const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] 64 | const onLongPress: typeof import('@vueuse/core')['onLongPress'] 65 | const onMounted: typeof import('vue')['onMounted'] 66 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 67 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 68 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 69 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 70 | const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] 71 | const onUnmounted: typeof import('vue')['onUnmounted'] 72 | const onUpdated: typeof import('vue')['onUpdated'] 73 | const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] 74 | const provide: typeof import('vue')['provide'] 75 | const reactify: typeof import('@vueuse/core')['reactify'] 76 | const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] 77 | const reactive: typeof import('vue')['reactive'] 78 | const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] 79 | const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] 80 | const reactivePick: typeof import('@vueuse/core')['reactivePick'] 81 | const readonly: typeof import('vue')['readonly'] 82 | const ref: typeof import('vue')['ref'] 83 | const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] 84 | const refDebounced: typeof import('@vueuse/core')['refDebounced'] 85 | const refDefault: typeof import('@vueuse/core')['refDefault'] 86 | const refThrottled: typeof import('@vueuse/core')['refThrottled'] 87 | const refWithControl: typeof import('@vueuse/core')['refWithControl'] 88 | const resolveComponent: typeof import('vue')['resolveComponent'] 89 | const resolveDirective: typeof import('vue')['resolveDirective'] 90 | const resolveRef: typeof import('@vueuse/core')['resolveRef'] 91 | const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] 92 | const shallowReactive: typeof import('vue')['shallowReactive'] 93 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 94 | const shallowRef: typeof import('vue')['shallowRef'] 95 | const statusStore: typeof import('./src/renderer/store/status')['statusStore'] 96 | const suite: typeof import('vitest')['suite'] 97 | const syncRef: typeof import('@vueuse/core')['syncRef'] 98 | const syncRefs: typeof import('@vueuse/core')['syncRefs'] 99 | const templateRef: typeof import('@vueuse/core')['templateRef'] 100 | const test: typeof import('vitest')['test'] 101 | const throttledRef: typeof import('@vueuse/core')['throttledRef'] 102 | const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] 103 | const toRaw: typeof import('vue')['toRaw'] 104 | const toReactive: typeof import('@vueuse/core')['toReactive'] 105 | const toRef: typeof import('vue')['toRef'] 106 | const toRefs: typeof import('vue')['toRefs'] 107 | const triggerRef: typeof import('vue')['triggerRef'] 108 | const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] 109 | const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] 110 | const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] 111 | const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] 112 | const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] 113 | const unref: typeof import('vue')['unref'] 114 | const unrefElement: typeof import('@vueuse/core')['unrefElement'] 115 | const until: typeof import('@vueuse/core')['until'] 116 | const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] 117 | const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery'] 118 | const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter'] 119 | const useArrayFind: typeof import('@vueuse/core')['useArrayFind'] 120 | const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex'] 121 | const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin'] 122 | const useArrayMap: typeof import('@vueuse/core')['useArrayMap'] 123 | const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce'] 124 | const useArraySome: typeof import('@vueuse/core')['useArraySome'] 125 | const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] 126 | const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] 127 | const useAttrs: typeof import('vue')['useAttrs'] 128 | const useAutoCut: typeof import('./src/renderer/hooks/useAutoCut')['useAutoCut'] 129 | const useBase64: typeof import('@vueuse/core')['useBase64'] 130 | const useBattery: typeof import('@vueuse/core')['useBattery'] 131 | const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] 132 | const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] 133 | const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] 134 | const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] 135 | const useCached: typeof import('@vueuse/core')['useCached'] 136 | const useClipboard: typeof import('@vueuse/core')['useClipboard'] 137 | const useCloned: typeof import('@vueuse/core')['useCloned'] 138 | const useColorMode: typeof import('@vueuse/core')['useColorMode'] 139 | const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] 140 | const useCounter: typeof import('@vueuse/core')['useCounter'] 141 | const useCssModule: typeof import('vue')['useCssModule'] 142 | const useCssVar: typeof import('@vueuse/core')['useCssVar'] 143 | const useCssVars: typeof import('vue')['useCssVars'] 144 | const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] 145 | const useCycleList: typeof import('@vueuse/core')['useCycleList'] 146 | const useDark: typeof import('@vueuse/core')['useDark'] 147 | const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] 148 | const useDebounce: typeof import('@vueuse/core')['useDebounce'] 149 | const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] 150 | const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] 151 | const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] 152 | const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] 153 | const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] 154 | const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] 155 | const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] 156 | const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] 157 | const useDraggable: typeof import('@vueuse/core')['useDraggable'] 158 | const useDropZone: typeof import('@vueuse/core')['useDropZone'] 159 | const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] 160 | const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] 161 | const useElementHover: typeof import('@vueuse/core')['useElementHover'] 162 | const useElementSize: typeof import('@vueuse/core')['useElementSize'] 163 | const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] 164 | const useEventBus: typeof import('@vueuse/core')['useEventBus'] 165 | const useEventListener: typeof import('@vueuse/core')['useEventListener'] 166 | const useEventSource: typeof import('@vueuse/core')['useEventSource'] 167 | const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] 168 | const useFFmpeg: typeof import('./src/renderer/hooks/useFFmpeg')['useFFmpeg'] 169 | const useFavicon: typeof import('@vueuse/core')['useFavicon'] 170 | const useFetch: typeof import('@vueuse/core')['useFetch'] 171 | const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] 172 | const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] 173 | const useFocus: typeof import('@vueuse/core')['useFocus'] 174 | const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] 175 | const useFps: typeof import('@vueuse/core')['useFps'] 176 | const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] 177 | const useGamepad: typeof import('@vueuse/core')['useGamepad'] 178 | const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] 179 | const useI18n: typeof import('vue-i18n')['useI18n'] 180 | const useIdle: typeof import('@vueuse/core')['useIdle'] 181 | const useImage: typeof import('@vueuse/core')['useImage'] 182 | const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] 183 | const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] 184 | const useInterval: typeof import('@vueuse/core')['useInterval'] 185 | const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] 186 | const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] 187 | const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] 188 | const useLink: typeof import('vue-router')['useLink'] 189 | const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] 190 | const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] 191 | const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] 192 | const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] 193 | const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] 194 | const useMemoize: typeof import('@vueuse/core')['useMemoize'] 195 | const useMemory: typeof import('@vueuse/core')['useMemory'] 196 | const useMounted: typeof import('@vueuse/core')['useMounted'] 197 | const useMouse: typeof import('@vueuse/core')['useMouse'] 198 | const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] 199 | const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] 200 | const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] 201 | const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] 202 | const useNetwork: typeof import('@vueuse/core')['useNetwork'] 203 | const useNow: typeof import('@vueuse/core')['useNow'] 204 | const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] 205 | const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] 206 | const useOnline: typeof import('@vueuse/core')['useOnline'] 207 | const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] 208 | const useParallax: typeof import('@vueuse/core')['useParallax'] 209 | const usePermission: typeof import('@vueuse/core')['usePermission'] 210 | const usePointer: typeof import('@vueuse/core')['usePointer'] 211 | const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] 212 | const usePrVersions: typeof import('./src/renderer/hooks/usePrVersions')['usePrVersions'] 213 | const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] 214 | const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast'] 215 | const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] 216 | const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] 217 | const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion'] 218 | const useRafFn: typeof import('@vueuse/core')['useRafFn'] 219 | const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] 220 | const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] 221 | const useRoute: typeof import('vue-router')['useRoute'] 222 | const useRouter: typeof import('vue-router')['useRouter'] 223 | const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] 224 | const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] 225 | const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] 226 | const useScroll: typeof import('@vueuse/core')['useScroll'] 227 | const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] 228 | const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] 229 | const useShare: typeof import('@vueuse/core')['useShare'] 230 | const useSlots: typeof import('vue')['useSlots'] 231 | const useSorted: typeof import('@vueuse/core')['useSorted'] 232 | const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] 233 | const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] 234 | const useStepper: typeof import('@vueuse/core')['useStepper'] 235 | const useStorage: typeof import('@vueuse/core')['useStorage'] 236 | const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] 237 | const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] 238 | const useSupported: typeof import('@vueuse/core')['useSupported'] 239 | const useSwipe: typeof import('@vueuse/core')['useSwipe'] 240 | const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] 241 | const useTextDirection: typeof import('@vueuse/core')['useTextDirection'] 242 | const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] 243 | const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] 244 | const useThrottle: typeof import('@vueuse/core')['useThrottle'] 245 | const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] 246 | const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] 247 | const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] 248 | const useTimeout: typeof import('@vueuse/core')['useTimeout'] 249 | const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] 250 | const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] 251 | const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] 252 | const useTitle: typeof import('@vueuse/core')['useTitle'] 253 | const useToNumber: typeof import('@vueuse/core')['useToNumber'] 254 | const useToString: typeof import('@vueuse/core')['useToString'] 255 | const useToggle: typeof import('@vueuse/core')['useToggle'] 256 | const useTransition: typeof import('@vueuse/core')['useTransition'] 257 | const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] 258 | const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] 259 | const useVModel: typeof import('@vueuse/core')['useVModel'] 260 | const useVModels: typeof import('@vueuse/core')['useVModels'] 261 | const useVibrate: typeof import('@vueuse/core')['useVibrate'] 262 | const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] 263 | const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] 264 | const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] 265 | const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] 266 | const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] 267 | const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] 268 | const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] 269 | const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] 270 | const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] 271 | const vi: typeof import('vitest')['vi'] 272 | const vitest: typeof import('vitest')['vitest'] 273 | const watch: typeof import('vue')['watch'] 274 | const watchArray: typeof import('@vueuse/core')['watchArray'] 275 | const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] 276 | const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] 277 | const watchEffect: typeof import('vue')['watchEffect'] 278 | const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] 279 | const watchOnce: typeof import('@vueuse/core')['watchOnce'] 280 | const watchPausable: typeof import('@vueuse/core')['watchPausable'] 281 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 282 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 283 | const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] 284 | const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] 285 | const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] 286 | const whenever: typeof import('@vueuse/core')['whenever'] 287 | } 288 | -------------------------------------------------------------------------------- /components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | HelloWorld: typeof import('./src/renderer/components/HelloWorld.vue')['default'] 11 | RouterLink: typeof import('vue-router')['RouterLink'] 12 | RouterView: typeof import('vue-router')['RouterView'] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /electron-builder.json5: -------------------------------------------------------------------------------- 1 | /** 2 | * @see https://www.electron.build/configuration/configuration 3 | */ 4 | { 5 | "appId": "com.dubai.autocut-client", 6 | "asar": true, 7 | "directories": { 8 | "output": "release/v${version}" 9 | }, 10 | extraResources: { 11 | from: './public/resources', 12 | to: './' 13 | }, 14 | "files": [ 15 | "dist-electron", 16 | "dist" 17 | ], 18 | "mac": { 19 | "artifactName": "${productName}_v${version}.${ext}", 20 | "target": [ 21 | "dmg" 22 | ] 23 | }, 24 | "win": { 25 | "target": [ 26 | { 27 | "target": "nsis", 28 | "arch": [ 29 | "x64" 30 | ] 31 | } 32 | ], 33 | "artifactName": "${productName}_v${version}.${ext}" 34 | }, 35 | "nsis": { 36 | "oneClick": false, 37 | "perMachine": false, 38 | "allowToChangeInstallationDirectory": true, 39 | "deleteAppDataOnUninstall": false 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/images/3.png -------------------------------------------------------------------------------- /images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/images/4.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | AutoCut 客户端 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "autocut-client", 3 | "version": "0.1.12", 4 | "main": "dist-electron/main/index.js", 5 | "description": "AutoCut Client", 6 | "author": "huali ", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "dev": "vite", 11 | "format": "eslint --fix --ext .ts --ext .vue ./src ./electron", 12 | "build": "vue-tsc --noEmit && vite build && electron-builder", 13 | "test": "vitest", 14 | "coverage": "vitest run --coverage", 15 | "release": "release-it" 16 | }, 17 | "engines": { 18 | "node": "^14.18.0 || >=16.0.0" 19 | }, 20 | "devDependencies": { 21 | "@iconify/json": "^2.1.136", 22 | "@intlify/vite-plugin-vue-i18n": "^7.0.0", 23 | "@types/lodash-es": "^4.17.6", 24 | "@types/uuid": "^9.0.0", 25 | "@typescript-eslint/eslint-plugin": "^5.42.1", 26 | "@typescript-eslint/parser": "^5.42.1", 27 | "@unocss/preset-icons": "^0.46.5", 28 | "@vitejs/plugin-vue": "^3.1.2", 29 | "@vueuse/core": "^9.5.0", 30 | "electron": "^21.1.0", 31 | "electron-builder": "^23.3.3", 32 | "electron-devtools-installer": "^3.2.0", 33 | "eslint": "^8.27.0", 34 | "eslint-plugin-import": "^2.26.0", 35 | "eslint-plugin-vue": "^9.7.0", 36 | "fix-path": "^4.0.0", 37 | "got": "^12.5.3", 38 | "hami-vuex": "^0.1.4", 39 | "lodash-es": "^4.17.21", 40 | "release-it": "^15.6.0", 41 | "subtitle": "^4.2.1", 42 | "typescript": "^4.9.3", 43 | "unocss": "^0.46.5", 44 | "unplugin-auto-import": "^0.11.4", 45 | "unplugin-vue-components": "^0.22.9", 46 | "uuid": "^9.0.0", 47 | "vite": "^3.2.2", 48 | "vite-plugin-electron": "^0.10.4", 49 | "vite-plugin-electron-renderer": "^0.11.1", 50 | "vitest": "^0.25.3", 51 | "vue": "^3.2.40", 52 | "vue-audio-visual": "^3.0.1", 53 | "vue-i18n": "9", 54 | "vue-router": "^4.1.6", 55 | "vue-tsc": "^1.0.9", 56 | "vuex": "^4.1.0" 57 | }, 58 | "debug": { 59 | "env": { 60 | "VITE_DEV_SERVER_URL": "http://127.0.0.1:3344" 61 | } 62 | }, 63 | "keywords": [ 64 | "electron", 65 | "rollup", 66 | "vite", 67 | "vue3", 68 | "vue" 69 | ], 70 | "dependencies": { 71 | "buffer": "^6.0.3", 72 | "fast-xml-parser": "3.21.1", 73 | "node-stream-zip": "^1.15.0", 74 | "strnum": "^1.0.5" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /public/electron-vite-vue.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/electron-vite-vue.gif -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/favicon.ico -------------------------------------------------------------------------------- /public/node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/node.png -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/mac/Resources.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/mac/Resources.tgz -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/mac/esdcorelibinterface.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/mac/esdcorelibinterface.node -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/win32/Resources/adobe_caps.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/win32/Resources/adobe_caps.dll -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-de_DE.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-de_DE.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-en_US.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-en_US.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-es_ES.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-es_ES.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-fr_FR.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-fr_FR.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-it_IT.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-it_IT.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-ja_JP.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/win32/Resources/localization/extendscript-ja_JP.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/win32/esdcorelibinterface.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/win32/esdcorelibinterface.node -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/x64/Resources/adobe_caps.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/x64/Resources/adobe_caps.dll -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-de_DE.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-de_DE.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-en_US.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-en_US.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-es_ES.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-es_ES.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-fr_FR.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-fr_FR.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-it_IT.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-it_IT.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-ja_JP.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/x64/Resources/localization/extendscript-ja_JP.dat -------------------------------------------------------------------------------- /public/resources/adobe-lib/esdebugger-core/win/x64/esdcorelibinterface.node: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/adobe-lib/esdebugger-core/win/x64/esdcorelibinterface.node -------------------------------------------------------------------------------- /public/resources/autocut.jsx.template: -------------------------------------------------------------------------------- 1 | var projPath = "{{projPath}}"; 2 | var videoPath = "{{videoPath}}"; 3 | var srtPath = "{{srtPath}}"; 4 | var clipPoints = "{{clipPoints}}".split(","); 5 | 6 | 7 | app.enableQE(); 8 | 9 | app.openDocument(projPath); 10 | var root = app.project.rootItem; 11 | app.project.createNewSequence("序列 01", "seq01"); 12 | 13 | app.project.importFiles([videoPath]); 14 | var video = root.children[1]; 15 | var seq = app.project.activeSequence; 16 | var time = seq.zeroPoint; 17 | seq.videoTracks[0].insertClip(video, time); 18 | // 获取视频轨道 19 | var videoTrack = qe.project.getActiveSequence().getVideoTrackAt(0); 20 | for (var i = 0; i < clipPoints.length; i++) { 21 | var point = new Time() 22 | point.seconds = Number(clipPoints[i]) 23 | videoTrack.razor( 24 | point.getFormatted(seq.getSettings().videoFrameRate, seq.getSettings().videoDisplayFormat), 25 | true, 26 | true 27 | ); 28 | } 29 | 30 | app.project.importFiles([srtPath]); 31 | var srt = root.children[2]; 32 | var seq = app.project.activeSequence; 33 | var time = seq.zeroPoint; 34 | seq.videoTracks[1].insertClip(srt, time); 35 | // 获取字幕轨道 36 | var srtTrack = qe.project.getActiveSequence().getVideoTrackAt(1); 37 | for (var i = 0; i < clipPoints.length; i++) { 38 | var point = new Time() 39 | point.seconds = Number(clipPoints[i]) 40 | srtTrack.razor(point.getFormatted(seq.getSettings().videoFrameRate, seq.getSettings().videoDisplayFormat), true, true); 41 | } -------------------------------------------------------------------------------- /public/resources/temp.prproj: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/public/resources/temp.prproj -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module "*.vue" { 4 | import type { DefineComponent } from "vue" 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | declare module "*.json" { 11 | const jsonValue: any; 12 | export default jsonValue; 13 | } 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue" 2 | import App from "@/App.vue" 3 | import { hamiVuex } from "@/store"; 4 | import router from "@/router"; 5 | import "@/samples/node-api" 6 | import "uno.css" 7 | // @ts-ignore 8 | import { AVPlugin } from "vue-audio-visual" 9 | import i18n from "@/i18n"; 10 | 11 | createApp(App) 12 | .use(hamiVuex) 13 | .use(router) 14 | .use(AVPlugin) 15 | .use(i18n) 16 | .mount("#app") 17 | .$nextTick(() => { 18 | postMessage({ payload: "removeLoading" }, "*") 19 | }) 20 | -------------------------------------------------------------------------------- /src/main/adobe/core.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | 3 | const prod = import.meta.env.PROD 4 | 5 | type EXTENSION_SPEC_NAME = "vscesd"; 6 | type CoreLib = { 7 | /** 8 | * 初始化, 返回值为 0 时表示成功 9 | */ 10 | esdInitialize: (name: EXTENSION_SPEC_NAME, processId: number) => {status: number}; 11 | /** 12 | * 获取已安装应用的 spec 13 | */ 14 | esdGetInstalledApplicationSpecifiers: () => {status: number, specifiers: Array}; 15 | /** 16 | * 通过 spec 获取应用名称 17 | */ 18 | esdGetDisplayNameForApplication: (spec: string) => {status:number, name: string}; 19 | /** 20 | * 检查指定程序是否正在运行 21 | */ 22 | esdGetApplicationRunning:(spen: string) => {status:number, isRunning: boolean}; 23 | /** 24 | * 转换文件地址格式 25 | */ 26 | esdPathToUri:(path: string)=> {status:number, uri: string}; 27 | /** 28 | * 发送调试信息,运行 jsx 29 | */ 30 | esdSendDebugMessage: ( 31 | appSpecifier: string, 32 | body: string, 33 | bringToFront: boolean, 34 | timeout: number 35 | ) => {status: number, serialNumber: number}; 36 | /** 37 | * 清理 38 | */ 39 | esdCleanup: () => {status: number}; 40 | esdPumpSession: (handler: Function) => any 41 | } 42 | 43 | let coreLib = undefined as CoreLib 44 | 45 | export function GetCoreLib() { 46 | if (coreLib === undefined) { 47 | const platform = process.platform; 48 | let core = undefined; 49 | if (platform === "darwin") { 50 | core = require( 51 | path.join( 52 | prod ? process.resourcesPath : path.join(__dirname, "../../public/resources"), 53 | "./adobe-lib/esdebugger-core/mac/esdcorelibinterface.node", 54 | ), 55 | ); 56 | } 57 | else if (platform === "win32") { 58 | const arch = process.arch; 59 | if (arch === "x64" || arch === "arm64") { 60 | core = require( 61 | path.join( 62 | prod ? process.resourcesPath : path.join(__dirname, "../../public/resources"), 63 | "./adobe-lib/esdebugger-core/win/x64/esdcorelibinterface.node", 64 | ), 65 | ); 66 | } 67 | else { 68 | core = require( 69 | path.join( 70 | prod ? process.resourcesPath : path.join(__dirname, "../../public/resources"), 71 | "./adobe-lib/esdebugger-core/win/win32/esdcorelibinterface.node", 72 | ), 73 | ); 74 | } 75 | } 76 | if (core === undefined) { 77 | throw new Error("Could not initialize Core Library! Is this running on a supported platform?"); 78 | } 79 | 80 | coreLib = core 81 | } 82 | return coreLib; 83 | } 84 | 85 | // 初始化 core 86 | export function initCore(){ 87 | return new Promise((resolve, reject) => { 88 | try { 89 | const core = GetCoreLib(); 90 | const result = core.esdInitialize("vscesd", process.pid); 91 | console.log("init result " + JSON.stringify(result)) 92 | if (result.status === 0 || result.status === 11) { 93 | resolve(core) 94 | } else { 95 | reject() 96 | } 97 | } catch(e) { 98 | console.log("init error" + e) 99 | reject() 100 | } 101 | }) 102 | } 103 | -------------------------------------------------------------------------------- /src/main/adobe/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import * as fs from "fs" 3 | import * as fastXMLParser from "fast-xml-parser" 4 | import { GetCoreLib, initCore } from "./core"; 5 | 6 | const prod = import.meta.env.PROD 7 | 8 | const ATTR_PREFIX = "@"; 9 | const TEXT_NODE_NAME = "#value"; 10 | const XML_OPTIONS = { 11 | attributeNamePrefix: ATTR_PREFIX, 12 | ignoreAttributes: false, 13 | parseAttributeValue: true, 14 | textNodeName: TEXT_NODE_NAME, 15 | }; 16 | 17 | function createPrProject(targetDir: string, videoPath: string){ 18 | const videoName = path.basename(videoPath).slice( 19 | 0, 20 | path.basename(videoPath).indexOf(".") || path.basename(videoPath).length, 21 | ) 22 | const tempFile = path.join( 23 | prod ? process.resourcesPath : path.join(__dirname, "../../public/resources"), 24 | "temp.prproj", 25 | ) 26 | try{ 27 | !fs.existsSync(targetDir) && fs.mkdirSync(targetDir) 28 | // 复制到目标文件夹中 29 | fs.copyFileSync(tempFile, path.join(targetDir, videoName + ".prproj")) 30 | fs.copyFileSync(videoPath, path.join(targetDir, path.basename(videoPath))) 31 | return { 32 | proj: path.join(targetDir, videoName + ".prproj"), 33 | video: path.join(targetDir, path.basename(videoPath)), 34 | } 35 | }catch(e){ 36 | console.error(e) 37 | return { 38 | proj: "", 39 | video: "", 40 | } 41 | } 42 | } 43 | 44 | 45 | // 生成 jsx 文件 46 | function createJsxFile( 47 | targetDir: string, proj: string, videoFile: string, srtFile: string, clipPoints: Array, 48 | ) { 49 | let jsxTemplate = fs.readFileSync( 50 | path.join(prod ? process.resourcesPath : path.join(__dirname, "../../public/resources"), "./autocut.jsx.template"), 51 | "utf-8", 52 | ) 53 | 54 | jsxTemplate = jsxTemplate.replace("{{projPath}}", proj.replaceAll("\\","\\\\").replaceAll(" ","\ ")) 55 | jsxTemplate = jsxTemplate.replace("{{videoPath}}", videoFile.replaceAll("\\","\\\\").replaceAll(" ","\ ")) 56 | jsxTemplate = jsxTemplate.replace("{{srtPath}}", srtFile.replaceAll("\\","\\\\").replaceAll(" ","\ ")) 57 | jsxTemplate = jsxTemplate.replace("{{clipPoints}}", clipPoints.join(",")) 58 | 59 | fs.writeFileSync(path.join(targetDir, "autocut.jsx"), jsxTemplate) 60 | 61 | return path.join(targetDir, "autocut.jsx") 62 | } 63 | 64 | 65 | const genderOnUnfilteredMessageReceived = (serialNumber:number, cb:({ reason, message })=>any) => { 66 | return (reason, message) => { 67 | console.log("OnUnfilteredMessageReceived reason", reason) 68 | console.log("OnUnfilteredMessageReceived message", message) 69 | if(message.serialNumber === serialNumber && reason === 3) { 70 | cb({ reason, message }) 71 | } 72 | } 73 | } 74 | 75 | function connectPromise(cb:()=>number, next:(message:any)=>any){ 76 | return new Promise(async (resolve, reject) => { 77 | const core = await initCore() 78 | 79 | const serialNumber = cb() 80 | if(!serialNumber) { 81 | reject() 82 | } 83 | 84 | const timer = setInterval( 85 | (handler)=>{ 86 | GetCoreLib().esdPumpSession(handler); 87 | }, 88 | 5, 89 | genderOnUnfilteredMessageReceived(serialNumber, async ({ reason, message })=>{ 90 | const _message = JSON.parse(JSON.stringify(message)) 91 | clearInterval(timer) 92 | await next(_message) 93 | core.esdCleanup() 94 | resolve() 95 | }), 96 | ) 97 | timer.unref(); 98 | 99 | }) 100 | } 101 | 102 | export async function exportToPr( 103 | targetDir: string, 104 | videoFile: string, 105 | srtFile: string, 106 | clipPoints: Array, 107 | spec:string, 108 | cb:(status: string, msg: string) => any, 109 | ) { 110 | const { proj, video } = createPrProject(targetDir, videoFile) 111 | if(!proj || !video){ 112 | cb("error", "项目创建失败") 113 | return 114 | } 115 | const jsx = createJsxFile(targetDir, proj, video, srtFile, clipPoints) 116 | if(!jsx){ 117 | cb("error", "脚本创建失败") 118 | return 119 | } 120 | 121 | 122 | let requestEngine = "" 123 | try{ 124 | await connectPromise(()=>{ 125 | 126 | const connectRes = GetCoreLib().esdSendDebugMessage(spec, "", true, 0) 127 | console.log("connectRes", connectRes) 128 | if(connectRes.status !== 0) { 129 | GetCoreLib().esdCleanup() 130 | cb("error", "Connect 脚本运行失败") 131 | } 132 | return connectRes.serialNumber 133 | 134 | }, async (message)=>{ 135 | console.log(message.body) 136 | if(!message){ 137 | cb("error", "无法识别引擎") 138 | return 139 | } 140 | const res = fastXMLParser.parse(message.body, XML_OPTIONS); 141 | const engineDefs = res.engines.engine; 142 | const engineNames = Array.isArray(engineDefs) 143 | ? engineDefs.map(engine => { return engine["@name"]; }) : [engineDefs["@name"]]; 144 | console.log(engineNames) 145 | // TODO: 可能存在多个引擎 146 | if (engineNames.length === 1) { 147 | requestEngine = engineNames[0]; 148 | } 149 | return 150 | }) 151 | }catch(e) { 152 | console.error(e) 153 | cb("error", "发生异常") 154 | GetCoreLib().esdCleanup() 155 | return 156 | } 157 | 158 | console.log(requestEngine) 159 | 160 | 161 | const core = await initCore() 162 | const runingRes = core.esdGetApplicationRunning(spec) 163 | console.log("runingRes ", JSON.stringify(runingRes)) 164 | 165 | if(runingRes.status!==0 || runingRes.isRunning === false) { 166 | core.esdCleanup() 167 | cb("error", "请先打开 Premiere Pro " + spec) 168 | return 169 | } 170 | // eslint-disable-next-line max-len 171 | const body = `` 172 | console.log("body: " + body) 173 | const result = core.esdSendDebugMessage(spec, body, true, 0) 174 | console.log("exportToPr result: " + JSON.stringify(result)) 175 | core.esdCleanup() 176 | if(result.status!==0) { 177 | cb("error", "脚本运行失败") 178 | return 179 | } 180 | cb("success", "") 181 | } 182 | 183 | const prVersions = [ 184 | { 185 | specifier: "premierepro-23.0", 186 | name: "Adobe Premiere Pro CC 2023", 187 | }, 188 | { 189 | specifier: "premierepro-22.0", 190 | name: "Adobe Premiere Pro CC 2022", 191 | }, 192 | { 193 | specifier: "premierepro-15.0", 194 | name: "Adobe Premiere Pro CC 2021", 195 | }, 196 | { 197 | specifier: "premierepro-14.3", 198 | name: "Adobe Premiere Pro CC 2020", 199 | }, 200 | { 201 | specifier: "premierepro-14.0", 202 | name: "Adobe Premiere Pro CC 2020", 203 | }, 204 | { 205 | specifier: "premierepro-13.1.5", 206 | name: "Adobe Premiere Pro CC 2019", 207 | }, 208 | { 209 | specifier: "premierepro-13.1.4", 210 | name: "Adobe Premiere Pro CC 2019", 211 | }, 212 | { 213 | specifier: "premierepro-13.1.3", 214 | name: "Adobe Premiere Pro CC 2019", 215 | }, 216 | { 217 | specifier: "premierepro-13.1.2", 218 | name: "Adobe Premiere Pro CC 2019", 219 | }, 220 | { 221 | specifier: "premierepro-13.1.1", 222 | name: "Adobe Premiere Pro CC 2019", 223 | }, 224 | { 225 | specifier: "premierepro-13.1", 226 | name: "Adobe Premiere Pro CC 2019", 227 | }, 228 | { 229 | specifier: "premierepro-13.0", 230 | name: "Adobe Premiere Pro CC 2019", 231 | }, 232 | { 233 | specifier: "premierepro-12.0", 234 | name: "Adobe Premiere Pro CC 2019", 235 | }, 236 | { 237 | specifier: "premierepro-11.0", 238 | name: "Adobe Premiere Pro CC 2017", 239 | }, 240 | ] 241 | 242 | export async function getSpec() { 243 | let appDefs = [] as Array<{ 244 | specifier: string, 245 | name:string 246 | }> 247 | const core = await initCore() 248 | const res = core.esdGetInstalledApplicationSpecifiers() 249 | console.log("InstalledApplicationSpecifiers", JSON.stringify(res)) 250 | if(res.status === 0) { 251 | let specifiers = res.specifiers 252 | if(specifiers.length === 0){ 253 | const installedProDir = path.join(process.env.APPDATA, "/Adobe/Premiere Pro") 254 | fs.readdirSync(installedProDir).forEach((dir) => { 255 | if(!isNaN(Number(dir.replaceAll(".", "")))){ 256 | specifiers.push(`premierepro-${dir}`) 257 | } 258 | }) 259 | } 260 | specifiers.map((spec) => { 261 | const result = core.esdGetDisplayNameForApplication(spec); 262 | if (result.status === 0) { 263 | if(result.name.indexOf("Adobe Premiere Pro") >= 0){ 264 | appDefs.push({ 265 | specifier: spec, 266 | name: result.name, 267 | }) 268 | } else { 269 | prVersions.map(item=>{ 270 | if(item.specifier === spec){ 271 | appDefs.push(item) 272 | } 273 | }) 274 | } 275 | } 276 | }) 277 | } 278 | core.esdCleanup() 279 | return appDefs 280 | } 281 | -------------------------------------------------------------------------------- /src/main/autocut/check.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "child_process" 2 | import { safePath } from "~~/utils" 3 | 4 | export function ffmpegCheck() { 5 | return new Promise((resolve, reject) => { 6 | const ffmpeg = exec("ffmpeg -version") 7 | let success = false 8 | 9 | ffmpeg.stdout.on("data", (res) => { 10 | // console.log(`stdout: ${res}`) 11 | if(res.indexOf("ffmpeg version") >= 0){ 12 | success = true 13 | resolve(true) 14 | return 15 | } 16 | }) 17 | 18 | ffmpeg.stderr.on("data", (err) => { 19 | // console.log(`stderr: ${err}`) 20 | if(err.indexOf("ffmpeg version") >= 0){ 21 | success = true 22 | resolve(true) 23 | return 24 | } 25 | }) 26 | 27 | ffmpeg.on("close", (code, signal) => { 28 | console.log(`ffmpeg exit. ${code}: ${signal}`) 29 | if (!success) { 30 | resolve(false) 31 | } 32 | return 33 | }) 34 | }) 35 | } 36 | 37 | /** 38 | * 39 | * @param excutePath AutoCut 可执行文件路径 40 | */ 41 | export function autocutCheck(excutePath:string){ 42 | return new Promise((resolve, reject) => { 43 | const commad = `"${safePath(excutePath)}" -h` 44 | const autocut = exec(commad) 45 | let success = false 46 | autocut.stdout.on("data", (res) => { 47 | // console.log(`stdout: ${res}`) 48 | if(res.indexOf("usage: autocut") >= 0){ 49 | resolve(true) 50 | success = true 51 | return 52 | } 53 | }) 54 | 55 | autocut.stderr.on("data", (err) => { 56 | // console.log(`stderr: ${err}`) 57 | if(err.indexOf("usage: autocut") >= 0){ 58 | resolve(true) 59 | success = true 60 | return 61 | } 62 | }) 63 | 64 | autocut.on("close", (code, signal) => { 65 | console.log(`AutoCut exit. ${code}: ${signal}`) 66 | if (!success) { 67 | resolve(false) 68 | } 69 | return 70 | }) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /src/main/autocut/download.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os" 2 | import * as fs from "fs" 3 | import * as https from "https" 4 | import * as path from "path" 5 | import StreamZip from "node-stream-zip" 6 | import got from "got"; 7 | import { execSync } from "child_process" 8 | 9 | import { autocutCheck } from "./check" 10 | 11 | const AUTOCUT_VERSION = "v0.0.3-build.2023.04.20" 12 | 13 | const DOWNLOAD_URL = { 14 | github: { 15 | win32: `https://github.com/zcf0508/autocut/releases/download/${AUTOCUT_VERSION}/autocut_windows.zip`, 16 | darwin: `https://github.com/zcf0508/autocut/releases/download/${AUTOCUT_VERSION}/autocut_macos.zip`, 17 | }, 18 | cdn: { 19 | win32: `https://dubai.huali.cafe/autocut/${AUTOCUT_VERSION}/autocut_windows.zip`, 20 | darwin: `https://dubai.huali.cafe/autocut/${AUTOCUT_VERSION}/autocut_macos.zip`, 21 | }, 22 | } 23 | 24 | /** 25 | * 测试可否正常访问 github 26 | */ 27 | function testNetwork(){ 28 | return new Promise((resolve, reject)=>{ 29 | const req = https.request("https://www.github.com", (res)=>{ 30 | if (res.statusCode === 200) { 31 | resolve(true) 32 | } else { 33 | resolve(false) 34 | } 35 | }) 36 | req.on("error", (e)=>{ 37 | resolve(false) 38 | }) 39 | req.end() 40 | }) 41 | } 42 | 43 | /** 44 | * 下载 autocut 程序 45 | * @param savePath autocut 存放地址 46 | */ 47 | export async function downloadAutoCut( 48 | savePath: string, 49 | cb: (status: "downloading" | "extracting" | "error" | "success", msg: string, process?: number) => any, 50 | ) { 51 | 52 | const platform = os.platform() 53 | const arch = os.arch() 54 | console.log(`platform: ${platform}`) 55 | console.log(`arch: ${arch}`) 56 | if((platform.indexOf("win") === 0 || platform.indexOf("darwin") === 0) && arch === "x64") { 57 | const zipFilePath = path.join(savePath, "autocut.zip").replaceAll("\\","\\\\").replaceAll(" ","\ ") 58 | const excutePath = path.join( 59 | savePath, 60 | "autocut", 61 | `autocut${platform.indexOf("win") === 0? ".exe" : ""}`, 62 | ).replaceAll("\\","\\\\").replaceAll(" ","\ ") 63 | 64 | if(fs.existsSync(excutePath)){ 65 | if(await autocutCheck(excutePath)){ 66 | cb("success", excutePath, 100) 67 | return 68 | } 69 | } 70 | if(fs.existsSync(zipFilePath)){ 71 | unzip(zipFilePath, savePath, excutePath, cb) 72 | return 73 | } 74 | 75 | cb("downloading", "下载中...", 0) 76 | 77 | const file = fs.createWriteStream(zipFilePath); 78 | 79 | const download_url = DOWNLOAD_URL[ 80 | await testNetwork() ? "github": "cdn" 81 | ][platform] 82 | 83 | if (!download_url || typeof download_url !== "string") { 84 | alert("sorry, not support your platform") 85 | cb("error", "sorry, not support your platform") 86 | return 87 | } 88 | 89 | got.stream(download_url).on("downloadProgress", ({ transferred, total }) => { 90 | const progress = (100.0 * transferred / total).toFixed(2) // 当前进度 91 | const currProgress = (transferred / 1048576).toFixed(2) // 当前下了多少 92 | console.log("data", progress, currProgress, total / 1048576) 93 | cb("downloading", "下载中...", parseFloat(progress)) 94 | }).pipe(file).on("finish", ()=>{ 95 | file.close(); 96 | unzip(zipFilePath, savePath, excutePath, cb) 97 | }).on("error", (err)=>{ 98 | console.error(err) 99 | file.close(); 100 | cb("error", "文件保存失败") 101 | fs.unlinkSync(zipFilePath) 102 | return 103 | }) 104 | 105 | } else { 106 | console.log("暂不支持") 107 | cb("error", "sorry, not support your platform") 108 | return 109 | } 110 | 111 | } 112 | 113 | 114 | function unzip( 115 | zipFilePath: string, 116 | savePath: string, 117 | excutePath: string, 118 | cb: (status: "extracting" | "success" | "error", msg: string, process?: number) => any, 119 | ){ 120 | const zip = new StreamZip({ 121 | file: zipFilePath, 122 | storeEntries: true, 123 | }); 124 | const extractedPath = savePath 125 | 126 | 127 | zip.on("ready", () => { 128 | cb("extracting", "解压中", 99 ) 129 | zip.extract(null, extractedPath, (err, count) => { 130 | 131 | console.log(err ? "Extract error" : `Extracted ${count} entries`); 132 | zip.close(); 133 | 134 | if (err) { 135 | cb("error", `解压失败:${err},请重试`) 136 | } else { 137 | if(os.platform().indexOf("darwin") === 0){ 138 | execSync(`chmod -R 777 "${path.resolve(excutePath, "..")}"`) 139 | execSync(`cd "${path.resolve(excutePath, "..")}" && bash ./build.sh`) 140 | } 141 | cb("success", excutePath ) 142 | } 143 | return 144 | }); 145 | }) 146 | 147 | zip.on("error",(err)=>{ 148 | console.error(err) 149 | zip.close(); 150 | if(fs.existsSync(zipFilePath)){ 151 | fs.unlinkSync(zipFilePath) 152 | } 153 | cb("error", `解压失败:${err},请重试`) 154 | return 155 | }) 156 | } 157 | -------------------------------------------------------------------------------- /src/main/autocut/index.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from "child_process" 2 | import readline from "readline" 3 | import fs from "fs" 4 | import { safePath } from "~~/utils" 5 | import { AutocutConfig } from "~~/../types" 6 | type GenerateStatus = "processing" | "error" | "success" 7 | 8 | /** 9 | * 生成字幕文件 10 | */ 11 | export function generateSubtitle( 12 | excutePath: string, 13 | filePath: string, 14 | config: AutocutConfig, 15 | cb: (status: GenerateStatus, msg: string, process?: number) => any, 16 | ) { 17 | const srtFile = filePath.slice(0, filePath.lastIndexOf(".")) + ".srt" 18 | 19 | if (fs.existsSync(srtFile)) { 20 | cb("success", "srt file already exist") 21 | return 22 | } 23 | 24 | let fail = false 25 | // let commad = `${excutePath} -t ${filePath}` 26 | const p = spawn( 27 | safePath(excutePath), 28 | [ 29 | "-t", safePath(filePath), 30 | "--device", config.device || "cpu", 31 | "--whisper-model", config.whisperModel || "tiny", 32 | "--lang", config.lang || "zh", 33 | ], 34 | ) 35 | 36 | const stdoutLineReader = readline.createInterface({ 37 | input: p.stdout, 38 | output: p.stdin, 39 | terminal: false, 40 | }); 41 | stdoutLineReader.on("line", (line) => { 42 | console.log(`stdout: ${line}`) 43 | }) 44 | 45 | const stderrLineReader = readline.createInterface({ 46 | input: p.stderr, 47 | output: p.stdin, 48 | terminal: false, 49 | }); 50 | stderrLineReader.on("line", (line) => { 51 | console.log(`stderr: ${line}`) 52 | 53 | if (line.indexOf("exists, skipping")>= 0){ 54 | fail = true 55 | cb("success", "md file already exist") 56 | return 57 | } 58 | if (line.indexOf("Transcribing") >= 0){ 59 | cb( 60 | "processing", 61 | "transcribing", 62 | 0, 63 | ) 64 | } 65 | if (line.match(/[0-9]*%/)?.length > 0 && line.match(/B\/s/)?.length > 0) { 66 | const process = parseInt(line.match(/[0-9]*%/)[0]) 67 | cb( 68 | "processing", 69 | "downloading", 70 | process, 71 | ) 72 | } 73 | else if (line.match(/[0-9]*%/)?.length > 0) { 74 | const process = parseInt(line.match(/[0-9]*%/)[0]) 75 | cb( 76 | "processing", 77 | "transcribing", 78 | process, 79 | ) 80 | } 81 | if (line.indexOf("Saved texts to") >= 0){ 82 | cb( 83 | "success", 84 | "saved", 85 | 100, 86 | ) 87 | } 88 | }) 89 | 90 | p.on("error", (err) => { 91 | console.log(`err: ${err}`) 92 | fail = true 93 | cb( 94 | "error", 95 | `unknown error: ${err}`, 96 | ) 97 | return 98 | }); 99 | 100 | p.on("close", (code) => { 101 | console.log(`child process exited with code ${code}`); 102 | if(!fail){ 103 | cb("success", "close") 104 | } 105 | }); 106 | 107 | } 108 | 109 | type CutStatus = "error" | "success" | "processing" 110 | 111 | export function cutVideo( 112 | excutePath: string, 113 | videoFilePath: string, 114 | srtFilePath:string, 115 | cb: (status: CutStatus, msg: string, process?: number) => any, 116 | ){ 117 | let fail = false 118 | const p = spawn( 119 | safePath(excutePath), 120 | ["-c", safePath(videoFilePath), safePath(srtFilePath)], 121 | ) 122 | 123 | const stdoutLineReader = readline.createInterface({ 124 | input: p.stdout, 125 | output: p.stdin, 126 | terminal: false, 127 | }); 128 | stdoutLineReader.on("line", (line) => { 129 | console.log(`stdout: ${line}`) 130 | }) 131 | 132 | const stderrLineReader = readline.createInterface({ 133 | input: p.stderr, 134 | output: p.stdin, 135 | terminal: false, 136 | }); 137 | stderrLineReader.on("line", (line) => { 138 | console.log(`stderr: ${line}`) 139 | 140 | if (line.indexOf("exists, skipping")>= 0){ 141 | fail = true 142 | cb("success", "cuted file already exist") 143 | return 144 | } 145 | 146 | if (line.indexOf("based on") >= 0){ 147 | cb( 148 | "processing", 149 | "transcribing", 150 | 0, 151 | ) 152 | } 153 | if (line.match(/[0-9]*%/)?.length > 0) { 154 | const process = parseInt(line.match(/[0-9]*%/)[0]) 155 | cb( 156 | "processing", 157 | "transcribing", 158 | process, 159 | ) 160 | } 161 | if (line.indexOf("Saved media to") >= 0){ 162 | 163 | const time = new Date().getTime() 164 | // 修改保存后的文件名 165 | const cutedVideoPath = safePath(videoFilePath).slice(0, safePath(videoFilePath).lastIndexOf(".")) + "_cut.mp4" 166 | const renamedCutedVideoPath = safePath(videoFilePath) 167 | .slice(0, safePath(videoFilePath).lastIndexOf(".")) + `_cut_${time}.mp4` 168 | 169 | fs.renameSync(cutedVideoPath, renamedCutedVideoPath) 170 | 171 | const renamedCutedSrtPath = safePath(srtFilePath) 172 | .slice(0, safePath(srtFilePath).lastIndexOf(".")) + `_${time}.srt` 173 | 174 | fs.renameSync(safePath(srtFilePath), renamedCutedSrtPath) 175 | 176 | 177 | cb( 178 | "success", 179 | "saved", 180 | 100, 181 | ) 182 | return 183 | } 184 | }) 185 | 186 | p.on("error", (err) => { 187 | console.log(`err: ${err}`) 188 | fail = true 189 | cb( 190 | "error", 191 | `unknown error: ${err}`, 192 | ) 193 | return 194 | }); 195 | 196 | p.on("close", (code) => { 197 | console.log(`child process exited with code ${code}`); 198 | if(!fail){ 199 | cb("success", "close") 200 | } 201 | }); 202 | 203 | } 204 | -------------------------------------------------------------------------------- /src/main/electron-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare namespace NodeJS { 4 | interface ProcessEnv { 5 | VSCODE_DEBUG?: "true" 6 | DIST_ELECTRON: string 7 | DIST: string 8 | /** /dist/ or /public/ */ 9 | PUBLIC: string 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/ffmpeg/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import { spawn } from "child_process" 3 | import readline from "readline" 4 | import { safePath } from "~~/utils" 5 | 6 | type ProcessStatus = "error" | "processing" | "success" 7 | 8 | /** 9 | * 生成视频的音频文件,用于页面显示 10 | * @param video 视频地址 11 | * @param cb 12 | */ 13 | export function getAudio( 14 | video: string, 15 | cb: (status: ProcessStatus, msg: string, process?: number) => any, 16 | ){ 17 | let success = false 18 | const exportPath = `${video.slice(0, video.lastIndexOf("."))}.wav` 19 | if(fs.existsSync(exportPath)){ 20 | cb( 21 | "success", 22 | "exists, skipping", 23 | ) 24 | return 25 | } 26 | const p = spawn( 27 | "ffmpeg", 28 | [ 29 | "-i", safePath(video), "-y", 30 | "-vn", "-acodec", "pcm_s16le", "-ac", "1", "-ar", "8000", 31 | exportPath, 32 | ], 33 | ) 34 | const stdoutLineReader = readline.createInterface({ 35 | input: p.stdout, 36 | output: p.stdin, 37 | terminal: false, 38 | }); 39 | stdoutLineReader.on("line", (line) => { 40 | console.log(`stdout: ${line}`) 41 | }) 42 | 43 | const stderrLineReader = readline.createInterface({ 44 | input: p.stderr, 45 | output: p.stdin, 46 | terminal: false, 47 | }); 48 | stderrLineReader.on("line", (line) => { 49 | console.log(`stderr: ${line}`) 50 | if(line.match(/audio:[0-9]*kB/)?.length>0) { 51 | success = true 52 | cb("success", "success") 53 | } 54 | }) 55 | 56 | p.on("close", (code) => { 57 | console.log(`child process exited with code ${code}`); 58 | if(!success){ 59 | cb("error", "close") 60 | } 61 | }); 62 | } 63 | 64 | /** 65 | * 转换视频格式,用于页面显示 66 | * @param video 视频地址 67 | * @param cb 68 | */ 69 | export function convertVideo( 70 | video: string, 71 | cb: (status: ProcessStatus, msg: string, process?: number) => any, 72 | ){ 73 | let success = false 74 | const exportPath = `${video.slice(0, video.lastIndexOf("."))}.mp4` 75 | if(fs.existsSync(exportPath)){ 76 | cb( 77 | "success", 78 | "exists, skipping", 79 | ) 80 | return 81 | } 82 | const p = spawn( 83 | "ffmpeg", 84 | [ 85 | "-i", safePath(video), "-y", 86 | "-c:v", "libx264", "-c:a", "acc", "-pix_fmt", "yuv420p", "-ac", "2", "-movflags", "faststart", 87 | "-threads", "8", "-preset", "ultrafast", 88 | safePath(exportPath), 89 | ], 90 | ) 91 | const stdoutLineReader = readline.createInterface({ 92 | input: p.stdout, 93 | output: p.stdin, 94 | terminal: false, 95 | }); 96 | stdoutLineReader.on("line", (line) => { 97 | console.log(`stdout: ${line}`) 98 | }) 99 | 100 | const stderrLineReader = readline.createInterface({ 101 | input: p.stderr, 102 | output: p.stdin, 103 | terminal: false, 104 | }); 105 | stderrLineReader.on("line", (line) => { 106 | console.log(`stderr: ${line}`) 107 | if(line.match(/video:[0-9]*kB/)?.length>0) { 108 | success = true 109 | cb("success", "success") 110 | } 111 | }) 112 | 113 | p.on("close", (code) => { 114 | console.log(`child process exited with code ${code}`); 115 | if(!success){ 116 | cb("error", "close") 117 | } 118 | }); 119 | } 120 | -------------------------------------------------------------------------------- /src/main/main/autocut.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain, dialog } from "electron" 2 | import { exportToPr, getSpec } from "~~/adobe" 3 | import { cutVideo, generateSubtitle } from "~~/autocut" 4 | import { AutocutConfig } from "~~/../types" 5 | import { autocutCheck, ffmpegCheck } from "~~/autocut/check" 6 | import { downloadAutoCut } from "~~/autocut/download" 7 | import { convertVideo, getAudio } from "~~/ffmpeg" 8 | let excutePath = "" 9 | 10 | export function registerAutoCut(win: BrowserWindow){ 11 | ipcMain.on("check-ffmpeg",async (e) => { 12 | const res = await ffmpegCheck() 13 | e.reply("report-ffmpeg-status", res) 14 | }) 15 | 16 | ipcMain.on("check-autocut",async (e, ...args) => { 17 | const path = Buffer.from(args[0] as string, "base64").toString() 18 | let res = false 19 | if(path){ 20 | res = await autocutCheck(path) 21 | if(res) { 22 | excutePath = path 23 | } 24 | } 25 | e.reply("report-autocut-status", res) 26 | }) 27 | 28 | ipcMain.handle("select-autocut-save-directory", async (e, ...args) => { 29 | const res = await dialog.showOpenDialog(win, { 30 | title: "请选择 AutoCut 安装路径", 31 | properties: ["openDirectory", "createDirectory"], 32 | 33 | }) 34 | return res 35 | }) 36 | 37 | ipcMain.on("download-autocut", async (e, uuid: string, path: string) => { 38 | const downloadPath = Buffer.from(path, "base64").toString() 39 | downloadAutoCut(downloadPath, (status, msg, process) => { 40 | e.reply( 41 | "report-download", 42 | uuid, 43 | { 44 | status, 45 | msg, 46 | process, 47 | }, 48 | ) 49 | }) 50 | }) 51 | 52 | ipcMain.on("start-transcribe", async (e, uuid: string, path: string, config: AutocutConfig) => { 53 | const filePath = Buffer.from(path, "base64").toString() 54 | console.log(config) 55 | generateSubtitle( 56 | excutePath, 57 | filePath, 58 | config, 59 | (status, msg, process) => { 60 | e.reply( 61 | "report-transcribe", 62 | uuid, 63 | { 64 | status, 65 | msg, 66 | process, 67 | }, 68 | ) 69 | }) 70 | }) 71 | 72 | ipcMain.on("convert-video", async (e, uuid: string, path: string) => { 73 | const filePath = Buffer.from(path, "base64").toString() 74 | convertVideo(filePath, (status, msg, process) => { 75 | e.reply( 76 | "report-convert-video", 77 | uuid, 78 | { 79 | status, 80 | msg, 81 | process, 82 | }, 83 | ) 84 | }) 85 | }) 86 | 87 | ipcMain.on("convert-audio", async (e, uuid: string, path: string) => { 88 | const filePath = Buffer.from(path, "base64").toString() 89 | getAudio(filePath, (status, msg, process) => { 90 | e.reply( 91 | "report-convert-audio", 92 | uuid, 93 | { 94 | status, 95 | msg, 96 | process, 97 | }, 98 | ) 99 | }) 100 | }) 101 | 102 | ipcMain.on("start-cut", async (e, uuid:string, video:string, srt:string) => { 103 | const videoFilePath = Buffer.from(video, "base64").toString() 104 | const srtFilePath = Buffer.from(srt, "base64").toString() 105 | cutVideo(excutePath, videoFilePath, srtFilePath, (status, msg, process) => { 106 | e.reply( 107 | "report-cut", 108 | uuid, 109 | { 110 | status, 111 | msg, 112 | process, 113 | }, 114 | ) 115 | }) 116 | }) 117 | 118 | ipcMain.on("check-pr-versions", async (e,...args) => { 119 | const version = await getSpec() 120 | e.reply("report-pr-versions", version) 121 | }) 122 | 123 | ipcMain.handle("select-prproj-save-directory", async (e, ...args) => { 124 | const res = await dialog.showOpenDialog(win, { 125 | title: "请选择 Pr 工程路径", 126 | properties: ["openDirectory", "createDirectory"], 127 | 128 | }) 129 | return res 130 | }) 131 | 132 | ipcMain.on("export-to-pr", (e,...args) => { 133 | const targetDir = Buffer.from(args[0] as string, "base64").toString() 134 | const videoFile = Buffer.from(args[1] as string, "base64").toString() 135 | const srtFile = Buffer.from(args[2] as string, "base64").toString() 136 | const clipPoints = args[3] as Array 137 | const spec = args[4] as string 138 | 139 | exportToPr(targetDir, videoFile, srtFile, clipPoints, spec, (status, msg)=>{ 140 | e.sender.send("export-to-pr", {status, msg}) 141 | }) 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /src/main/main/index.ts: -------------------------------------------------------------------------------- 1 | // The built directory structure 2 | // 3 | // ├─┬ dist-electron 4 | // │ ├─┬ main 5 | // │ │ └── index.js > Electron-Main 6 | // │ └─┬ preload 7 | // │ └── index.js > Preload-Scripts 8 | // ├─┬ dist 9 | // │ └── index.html > Electron-Renderer 10 | // 11 | import fixPath from "fix-path"; 12 | 13 | fixPath() 14 | 15 | process.env.DIST_ELECTRON = join(__dirname, "..") 16 | process.env.DIST = join(process.env.DIST_ELECTRON, "../dist") 17 | process.env.PUBLIC = app.isPackaged ? process.env.DIST : join(process.env.DIST_ELECTRON, "../public") 18 | 19 | 20 | import { app, BrowserWindow, shell, ipcMain } from "electron" 21 | import installExtension, { VUEJS_DEVTOOLS } from "electron-devtools-installer"; 22 | import { release } from "os" 23 | import { join } from "path" 24 | import { registerAutoCut } from "./autocut" 25 | 26 | // Disable GPU Acceleration for Windows 7 27 | if (release().startsWith("6.1")) app.disableHardwareAcceleration() 28 | 29 | // Set application name for Windows 10+ notifications 30 | if (process.platform === "win32") app.setAppUserModelId(app.getName()) 31 | 32 | if (!app.requestSingleInstanceLock()) { 33 | app.quit() 34 | process.exit(0) 35 | } 36 | 37 | // Remove electron security warnings 38 | // This warning only shows in development mode 39 | // Read more on https://www.electronjs.org/docs/latest/tutorial/security 40 | // process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = 'true' 41 | 42 | let win: BrowserWindow | null = null 43 | // Here, you can also use other preload 44 | const preload = join(__dirname, "../preload/index.js") 45 | const url = process.env.VITE_DEV_SERVER_URL 46 | const indexHtml = join(process.env.DIST, "index.html") 47 | 48 | async function createWindow() { 49 | win = new BrowserWindow({ 50 | title: "Main window", 51 | icon: join(process.env.PUBLIC, "favicon.ico"), 52 | webPreferences: { 53 | preload, 54 | // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production 55 | // Consider using contextBridge.exposeInMainWorld 56 | // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation 57 | nodeIntegration: true, 58 | contextIsolation: false, 59 | webSecurity: false, 60 | }, 61 | width: 1440, 62 | height: 1024, 63 | }) 64 | 65 | if (process.env.VITE_DEV_SERVER_URL) { // electron-vite-vue#298 66 | win.loadURL(url) 67 | 68 | installExtension(VUEJS_DEVTOOLS) 69 | .then((name) => console.log(`Added Extension: ${name}`)) 70 | .catch((err) => console.log("An error occurred: ", err)); 71 | // Open devTool if the app is not packaged 72 | win.webContents.openDevTools() 73 | } else { 74 | win.loadFile(indexHtml) 75 | } 76 | 77 | // Test actively push message to the Electron-Renderer 78 | win.webContents.on("did-finish-load", () => { 79 | win?.webContents.send("main-process-message", new Date().toLocaleString()) 80 | }) 81 | 82 | // Make all links open with the browser, not with the application 83 | win.webContents.setWindowOpenHandler(({ url }) => { 84 | if (url.startsWith("https:")) shell.openExternal(url) 85 | return { action: "deny" } 86 | }) 87 | 88 | registerAutoCut(win) 89 | } 90 | 91 | app.whenReady().then(createWindow) 92 | 93 | app.on("window-all-closed", () => { 94 | win = null 95 | if (process.platform !== "darwin") app.quit() 96 | }) 97 | 98 | app.on("second-instance", () => { 99 | if (win) { 100 | // Focus on the main window if the user tried to open another 101 | if (win.isMinimized()) win.restore() 102 | win.focus() 103 | } 104 | }) 105 | 106 | app.on("activate", () => { 107 | const allWindows = BrowserWindow.getAllWindows() 108 | if (allWindows.length) { 109 | allWindows[0].focus() 110 | } else { 111 | createWindow() 112 | } 113 | }) 114 | 115 | // new window example arg: new windows url 116 | ipcMain.handle("open-win", (event, arg) => { 117 | const childWindow = new BrowserWindow({ 118 | webPreferences: { 119 | preload, 120 | nodeIntegration: true, 121 | contextIsolation: false, 122 | }, 123 | }) 124 | 125 | if (app.isPackaged) { 126 | childWindow.loadFile(indexHtml, { hash: arg }) 127 | } else { 128 | childWindow.loadURL(`${url}#${arg}`) 129 | // childWindow.webContents.openDevTools({ mode: "undocked", activate: true }) 130 | } 131 | }) 132 | -------------------------------------------------------------------------------- /src/main/preload/index.ts: -------------------------------------------------------------------------------- 1 | function domReady(condition: DocumentReadyState[] = ["complete", "interactive"]) { 2 | return new Promise(resolve => { 3 | if (condition.includes(document.readyState)) { 4 | resolve(true) 5 | } else { 6 | document.addEventListener("readystatechange", () => { 7 | if (condition.includes(document.readyState)) { 8 | resolve(true) 9 | } 10 | }) 11 | } 12 | }) 13 | } 14 | 15 | const safeDOM = { 16 | append(parent: HTMLElement, child: HTMLElement) { 17 | if (!Array.from(parent.children).find(e => e === child)) { 18 | return parent.appendChild(child) 19 | } 20 | }, 21 | remove(parent: HTMLElement, child: HTMLElement) { 22 | if (Array.from(parent.children).find(e => e === child)) { 23 | return parent.removeChild(child) 24 | } 25 | }, 26 | } 27 | 28 | /** 29 | * https://tobiasahlin.com/spinkit 30 | * https://connoratherton.com/loaders 31 | * https://projects.lukehaas.me/css-loaders 32 | * https://matejkustec.github.io/SpinThatShit 33 | */ 34 | function useLoading() { 35 | const className = "loaders-css__square-spin" 36 | const styleContent = ` 37 | @keyframes square-spin { 38 | 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } 39 | 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); } 40 | 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); } 41 | 100% { transform: perspective(100px) rotateX(0) rotateY(0); } 42 | } 43 | .${className} > div { 44 | animation-fill-mode: both; 45 | width: 50px; 46 | height: 50px; 47 | background: #fff; 48 | animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite; 49 | } 50 | .app-loading-wrap { 51 | position: fixed; 52 | top: 0; 53 | left: 0; 54 | width: 100vw; 55 | height: 100vh; 56 | display: flex; 57 | align-items: center; 58 | justify-content: center; 59 | background: #282c34; 60 | z-index: 9; 61 | } 62 | ` 63 | const oStyle = document.createElement("style") 64 | const oDiv = document.createElement("div") 65 | 66 | oStyle.id = "app-loading-style" 67 | oStyle.innerHTML = styleContent 68 | oDiv.className = "app-loading-wrap" 69 | oDiv.innerHTML = `
` 70 | 71 | return { 72 | appendLoading() { 73 | safeDOM.append(document.head, oStyle) 74 | safeDOM.append(document.body, oDiv) 75 | }, 76 | removeLoading() { 77 | safeDOM.remove(document.head, oStyle) 78 | safeDOM.remove(document.body, oDiv) 79 | }, 80 | } 81 | } 82 | 83 | // ---------------------------------------------------------------------- 84 | 85 | const { appendLoading, removeLoading } = useLoading() 86 | domReady().then(appendLoading) 87 | 88 | window.onmessage = ev => { 89 | ev.data.payload === "removeLoading" && removeLoading() 90 | } 91 | 92 | setTimeout(removeLoading, 4999) 93 | -------------------------------------------------------------------------------- /src/main/subtitle/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import { parseSync, parseTimestamp } from "subtitle" 3 | 4 | -------------------------------------------------------------------------------- /src/main/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./path" -------------------------------------------------------------------------------- /src/main/utils/path.ts: -------------------------------------------------------------------------------- 1 | export function safePath(path: string){ 2 | let res = path 3 | // 已经处理过 4 | if(path.indexOf("\\ ") >= 0 || path.indexOf("\\\\") >= 0) { 5 | return res 6 | } 7 | 8 | if(path.indexOf("\\") >= 0){ 9 | res = res.replaceAll("\\", "\\\\") 10 | } 11 | if(path.indexOf(" ") >= 0){ 12 | res = res.replaceAll(" ", "\ ") 13 | } 14 | 15 | return res 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 32 | -------------------------------------------------------------------------------- /src/renderer/assets/FFmpeg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zcf0508/autocut-client/b0d054e979229b44e84c5ffb148d9dadd047da0b/src/renderer/assets/FFmpeg.png -------------------------------------------------------------------------------- /src/renderer/assets/electron.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/assets/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 33 | 34 | 39 | -------------------------------------------------------------------------------- /src/renderer/hooks/useAutoCut.ts: -------------------------------------------------------------------------------- 1 | import * as os from "os" 2 | import { checkStatus } from "@/interface/autocut" 3 | import path from "path" 4 | 5 | export function useAutoCut(){ 6 | const excutePath = computed(() => path.join( 7 | configStore.installPath, 8 | "autocut", 9 | `autocut${os.platform().indexOf("win") === 0? ".exe" : ""}`, 10 | )) 11 | const autocutStatus = computed(() => statusStore.autocutStatus) 12 | 13 | const checkAutocut = async ()=>{ 14 | if (configStore.installPath && !configStore.installPath.match(/[^a-zA-z0-9\:\\\/\-\_\ ]+/)) { 15 | statusStore.setAutocut(await checkStatus(excutePath.value)) 16 | } else { 17 | statusStore.setAutocut(false) 18 | } 19 | } 20 | 21 | watch( 22 | () => excutePath.value, 23 | () => { 24 | checkAutocut() 25 | }, 26 | ) 27 | 28 | 29 | return { 30 | autocutStatus, 31 | checkAutocut, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/hooks/useFFmpeg.ts: -------------------------------------------------------------------------------- 1 | import { checkStatus } from "@/interface/ffmpeg" 2 | 3 | export function useFFmpeg(){ 4 | const ffmpegStatus = computed(() => statusStore.ffmpegStatus) 5 | 6 | const checkFFmpeg = async ()=>{ 7 | statusStore.setFFmpeg(await checkStatus()) 8 | } 9 | 10 | return { 11 | ffmpegStatus, 12 | checkFFmpeg, 13 | } 14 | } -------------------------------------------------------------------------------- /src/renderer/hooks/usePrVersions.ts: -------------------------------------------------------------------------------- 1 | import type { PrVersion } from "@/interface/adobe" 2 | import { getPrVersions } from "@/interface/adobe" 3 | 4 | export function usePrVersions(){ 5 | const prVersions = ref([] as Array) 6 | 7 | const checkPrVersions = async () => { 8 | prVersions.value = await getPrVersions() 9 | } 10 | 11 | return { 12 | prVersions, 13 | checkPrVersions, 14 | } 15 | } -------------------------------------------------------------------------------- /src/renderer/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusBar": "Status", 3 | "ffmepegInstalled": { 4 | "success": "FFmpeg is correctly installed.", 5 | "error": "Please check if FFmpeg is installed correctly", 6 | "tip": "Please click to download FFmpeg and install it" 7 | }, 8 | "autocutInstalled": { 9 | "success": "AucoCut is correctly installed.", 10 | "error": "Please check if AutoCut is installed correctly", 11 | "tip": "AutoCut Configure", 12 | "installPathPlaceholder": "Please select an installation folder", 13 | "step1": "1. Select an installation folder", 14 | "selectBtn": "Select", 15 | "downloadBtn": "Download", 16 | "step2": "2. Install AutoCut", 17 | "downloading": "Downloading now, please do not do anything else.", 18 | "extracting": "Unzipping now, it will spend some time, please DO NOT do anything else.", 19 | "downloadTip": "Paths are allowed to contain only numbers, English, dashes, and underscores", 20 | "reDownloadBtn": "Redownload", 21 | "reDownloadTip": "Deleting the old files, please wait..." 22 | }, 23 | "start": "Start", 24 | "back": "Back", 25 | "dropIn": "Drag in a file to start editing.", 26 | "selectedFile": "Selected File", 27 | "transcribeTask": "Transcribe: ", 28 | "success": "success", 29 | "fail": "fail", 30 | "convertVideoTask": "Convert Video: ", 31 | "convertAudioTask": "Convert Audio: ", 32 | "exportSuccess": "Export Success", 33 | "exportFail": "Export Fail", 34 | "export": "Export", 35 | "exporting": "Exporting, please be patient...", 36 | "exportToPr": { 37 | "success": "The task has been sent to Premiere. Please switch to Premiere to check it.", 38 | "btn": "Export to PR project", 39 | "desc": "Please open Adobe Premiere Pro before exporting.", 40 | "selectVersion": "Please select Adobe Premiere Pro version", 41 | "selectDirectory": "Please select the save folder", 42 | "selectBtn": "Select", 43 | "startExport": "Start" 44 | }, 45 | "setup": { 46 | "title": "AutoCut Setting" 47 | }, 48 | "preview": "preview mode, skip the unselected sections", 49 | "config": { 50 | "lang": "Language Of Video", 51 | "size": "Device" 52 | }, 53 | "selectAll": "Select All" 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from "vue-i18n"; 2 | import zh from "./zh.json" 3 | import en from "./en.json" 4 | 5 | export const LOCALES = [ 6 | { 7 | name: "English", 8 | locale: "en", 9 | }, { 10 | name: "中文", 11 | locale: "zh", 12 | }, 13 | ] 14 | 15 | const i18n = createI18n({ 16 | legacy: false, 17 | locale: "en", 18 | messages: { 19 | en: en, 20 | zh: zh, 21 | }, 22 | }) 23 | 24 | export default i18n 25 | -------------------------------------------------------------------------------- /src/renderer/i18n/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "statusBar": "状态", 3 | "ffmepegInstalled": { 4 | "success": "已正确安装 FFmpeg", 5 | "error": "请检查 FFmpeg 是否已正确安装", 6 | "tip": "请点击下载 FFmpeg 并安装" 7 | }, 8 | "autocutInstalled": { 9 | "success": "已正确安装 AutoCut", 10 | "error": "请检查 AutoCut 是否已正确安装", 11 | "tip": "配置AutoCut", 12 | "installPathPlaceholder": "请选择安装路径", 13 | "step1": "1. 选择安装路径", 14 | "selectBtn": "选择文件夹", 15 | "downloadBtn": "点击下载", 16 | "step2": "2. 安装 AutoCut", 17 | "downloading": "正在下载中,请勿进行其它操作", 18 | "extracting": "正在解压中,可能需要一点时间,请勿进行其它操作", 19 | "downloadTip": "路径仅允许包含数字、英文、短横、下划线", 20 | "reDownloadBtn": "重新下载", 21 | "reDownloadTip": "正在删除旧文件,请稍后..." 22 | }, 23 | "start": "开始", 24 | "back": "返回", 25 | "setup": { 26 | "title": "配置 AutoCut" 27 | }, 28 | "dropIn": "拖入文件开始剪辑", 29 | "selectedFile": "已选文件:", 30 | "transcribeTask": "字幕生成: ", 31 | "success": "成功", 32 | "fail": "失败", 33 | "convertVideoTask": "视频转码: ", 34 | "convertAudioTask": "音频转码: ", 35 | "exportSuccess": "导出成功", 36 | "exportFail": "导出失败", 37 | "export": "导出视频", 38 | "exporting": "导出中,请耐心等待...", 39 | "exportToPr": { 40 | "success": "任务已发送到 Pr ,请切换至 Pr 查看", 41 | "btn": "导出到 Pr 工程", 42 | "desc": "请先打开 Pr 再使用导出功能", 43 | "selectVersion": "请选择当前 Pr 版本", 44 | "selectDirectory": "请选择保存位置", 45 | "selectBtn": "选择文件夹", 46 | "startExport": "开始导出" 47 | }, 48 | "preview": "预览模式,自动跳过未选择的部分", 49 | "config": { 50 | "lang": "视频语言", 51 | "size": "设备" 52 | }, 53 | "selectAll": "全选" 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/interface/adobe.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from "electron" 2 | 3 | 4 | export interface PrVersion { 5 | specifier: string 6 | name: string 7 | } 8 | 9 | /** 10 | * 获取 pr 版本列表 11 | */ 12 | export function getPrVersions() { 13 | return new Promise>((resolve,reject) => { 14 | ipcRenderer.once("report-pr-versions", (e, versions: Array) => { 15 | resolve(versions) 16 | }) 17 | 18 | ipcRenderer.send("check-pr-versions") 19 | }) 20 | } 21 | 22 | export function selectPrprojSaveDirectory() { 23 | return new Promise(async (resolve, reject) => { 24 | resolve( 25 | await ipcRenderer.invoke("select-prproj-save-directory"), 26 | ) 27 | }) 28 | } 29 | 30 | export function exportToPr( 31 | targetPath: string, 32 | videoPath: string, 33 | srtPath: string, 34 | clipPoints: Array, 35 | version: string, 36 | ) { 37 | return new Promise((resolve, reject) => { 38 | 39 | ipcRenderer.once("export-to-pr", (e, {status, msg}) => { 40 | if (status === "success") { 41 | resolve() 42 | } else if(status === "error") { 43 | reject(msg) 44 | } 45 | }) 46 | 47 | ipcRenderer.send( 48 | "export-to-pr", 49 | Buffer.from(targetPath).toString("base64"), 50 | Buffer.from(videoPath).toString("base64"), 51 | Buffer.from(srtPath).toString("base64"), 52 | clipPoints, 53 | version, 54 | ) 55 | }) 56 | } -------------------------------------------------------------------------------- /src/renderer/interface/autocut.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | import { ipcRenderer } from "electron" 3 | import { AutocutConfig } from "src/types" 4 | 5 | /** 6 | * 检查 AuctoCut 可执行状态 7 | * @param excutePath 执行路径 8 | */ 9 | export function checkStatus(excutePath: string) { 10 | return new Promise((resolve,reject) => { 11 | ipcRenderer.once("report-autocut-status", (e, status: boolean) => { 12 | resolve(status) 13 | }) 14 | 15 | ipcRenderer.send("check-autocut", Buffer.from(excutePath).toString("base64")) 16 | }) 17 | } 18 | 19 | /** 20 | * 选择 AutoCut 安装目录 21 | */ 22 | export function selectAutocutSaveDirectory() { 23 | return new Promise(async (resolve,reject) => { 24 | resolve( 25 | await ipcRenderer.invoke("select-autocut-save-directory"), 26 | ) 27 | }) 28 | } 29 | 30 | interface DownloadReport { 31 | status: "downloading"| "extracting" | "error" | "success" 32 | msg: string 33 | process?: number 34 | } 35 | 36 | /** 37 | * 正在进行的下载任务 38 | */ 39 | const downloadTasks = new Set() 40 | 41 | /** 42 | * 下载 AutoCut 43 | * @param installPath 安装路径 44 | * @param processCallback 进度回调 45 | * @returns 46 | */ 47 | export function downloadAutoCut(installPath: string, processCallback: (task: string, process: number) => any) { 48 | return new Promise((resolve, reject) => { 49 | const uuid = uuidv4() 50 | 51 | const taskCount = 2 // 下载和解压 52 | ipcRenderer.on("report-download",(e, _uuid: string, res: DownloadReport) => { 53 | if (_uuid !== uuid) { 54 | return 55 | } 56 | if (res.status === "error") { 57 | downloadTasks.delete(uuid) 58 | reject(new Error(res.msg)) 59 | } 60 | else if (res.status === "success" ) { 61 | downloadTasks.delete(uuid) 62 | resolve() 63 | } 64 | else { 65 | if(res.status === "downloading") { 66 | processCallback("downloading", res.process!/taskCount) 67 | } 68 | else if(res.status === "extracting") { 69 | processCallback("extracting", 100/taskCount + res.process!/taskCount) 70 | } 71 | } 72 | }) 73 | 74 | ipcRenderer.send("download-autocut", uuid, Buffer.from(installPath).toString("base64")) 75 | downloadTasks.add(uuid) 76 | }).finally(() => { 77 | if (downloadTasks.size === 0) { 78 | ipcRenderer.removeAllListeners("report-download") 79 | } 80 | }) 81 | } 82 | 83 | interface TranscribeReport { 84 | status: "processing" | "error" | "success" 85 | msg: string 86 | process?: number 87 | } 88 | 89 | /** 90 | * 正在进行的转录任务 91 | */ 92 | const transcribeTasks = new Set() 93 | 94 | /** 95 | * 转录视频 96 | * @param filePath 文件路径 97 | * @param processCallback 进度回调 98 | * @returns 返回转录成功后的字幕地址 99 | */ 100 | export function startTranscribe( 101 | filePath: string, 102 | config: AutocutConfig, 103 | processCallback: (task: string, process: number) => any, 104 | ) { 105 | return new Promise((resolve, reject) => { 106 | const uuid = uuidv4() 107 | 108 | let taskCount = 1 // 默认为转录 109 | 110 | ipcRenderer.on("report-transcribe", (e, _uuid: string, res: TranscribeReport) => { 111 | if (_uuid !== uuid) { 112 | return 113 | } 114 | if (res.status === "error") { 115 | transcribeTasks.delete(uuid) 116 | reject(new Error(res.msg)) 117 | } else if (res.status === "success") { 118 | transcribeTasks.delete(uuid) 119 | resolve(filePath.slice(0, filePath.lastIndexOf(".")) + ".srt") 120 | } else if (res.status === "processing") { 121 | if (res.msg === "transcribing") { 122 | processCallback( 123 | "processing", 124 | taskCount === 1 125 | ? res.process! // 仅转录 126 | : 100/taskCount + res.process!/taskCount, // 下载和转录 127 | ) 128 | } else if (res.msg === "downloading") { 129 | if (taskCount === 1) { 130 | taskCount++ // 增加一个下载任务 131 | } 132 | processCallback("processing", res.process!/taskCount) 133 | } 134 | } 135 | 136 | }) 137 | 138 | ipcRenderer.send("start-transcribe", uuid, Buffer.from(filePath).toString("base64"), config) 139 | transcribeTasks.add(uuid) 140 | }).finally(() => { 141 | if (transcribeTasks.size === 0) { 142 | ipcRenderer.removeAllListeners("report-transcribe") 143 | } 144 | }) 145 | } 146 | 147 | /** 148 | * 正在进行的剪辑任务 149 | */ 150 | const cutTasks = new Set() 151 | 152 | /** 153 | * 按字幕剪辑视频 154 | * @param videoPath 原视频地址 155 | * @param cutSrtPath 剪辑后的字幕文件地址 156 | * @returns 157 | */ 158 | export function startCut(videoPath: string, cutSrtPath: string) { 159 | return new Promise((resolve, reject) => { 160 | const uuid = uuidv4() 161 | 162 | ipcRenderer.on("report-cut",(e, _uuid: string, res) => { 163 | if(_uuid !== uuid) { 164 | return 165 | } 166 | 167 | if(res.status === "error"){ 168 | cutTasks.delete(uuid) 169 | reject(res.msg) 170 | } else if(res.status === "success") { 171 | cutTasks.delete(uuid) 172 | resolve(res.msg) 173 | } 174 | }) 175 | 176 | ipcRenderer.send( 177 | "start-cut", 178 | uuid, 179 | Buffer.from(videoPath).toString("base64"), 180 | Buffer.from(cutSrtPath).toString("base64"), 181 | ) 182 | cutTasks.add(uuid) 183 | }).finally(() => { 184 | if (cutTasks.size === 0) { 185 | ipcRenderer.removeAllListeners("report-cut") 186 | } 187 | }) 188 | } 189 | -------------------------------------------------------------------------------- /src/renderer/interface/ffmpeg.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from "uuid"; 2 | import { ipcRenderer } from "electron" 3 | 4 | /** 5 | * 检查 ffmpeg 可执行状态 6 | */ 7 | export function checkStatus() { 8 | return new Promise((resolve,reject) => { 9 | ipcRenderer.once("report-ffmpeg-status", (e, status: boolean) => { 10 | resolve(status) 11 | }) 12 | 13 | ipcRenderer.send("check-ffmpeg") 14 | }) 15 | } 16 | 17 | /** 18 | * 正在进行的视频转换任务 19 | */ 20 | const convertVideoTasks = new Set() 21 | 22 | /** 23 | * 将输入的视频文件转为 mp4 24 | * @param filePath 视频文件路径 25 | * @returns 转换后的视频地址 26 | */ 27 | export function convertVideo(filePath: string) { 28 | return new Promise((resolve, reject) => { 29 | const uuid = uuidv4() 30 | 31 | ipcRenderer.on("report-convert-video",(e, _uuid: string, res)=>{ 32 | if (_uuid !== uuid) { 33 | return 34 | } 35 | if (res.status === "error") { 36 | convertVideoTasks.delete(uuid) 37 | reject() 38 | } else if (res.status === "success") { 39 | convertVideoTasks.delete(uuid) 40 | resolve(filePath.slice(0,filePath.lastIndexOf(".")) + ".mp4") 41 | } 42 | }) 43 | 44 | ipcRenderer.send("convert-video", uuid, Buffer.from(filePath).toString("base64")) 45 | convertVideoTasks.add(uuid) 46 | }).finally(() => { 47 | if (convertVideoTasks.size === 0) { 48 | ipcRenderer.removeAllListeners("report-convert-video") 49 | } 50 | }) 51 | } 52 | 53 | /** 54 | * 正在进行的视频转换任务 55 | */ 56 | const convertAudioTasks = new Set() 57 | 58 | /** 59 | * 将输入的视频文件转为 wav 60 | * @param filePath 视频文件路径 61 | * @returns 转换后的音频地址 62 | */ 63 | export function convertAudio(filePath: string) { 64 | return new Promise((resolve, reject) => { 65 | const uuid = uuidv4() 66 | 67 | ipcRenderer.on("report-convert-audio",(e, _uuid: string, res)=>{ 68 | if (_uuid !== uuid) { 69 | return 70 | } 71 | if (res.status === "error") { 72 | convertAudioTasks.delete(uuid) 73 | reject() 74 | } else if (res.status === "success") { 75 | convertAudioTasks.delete(uuid) 76 | resolve(filePath.slice(0,filePath.lastIndexOf(".")) + ".wav") 77 | } 78 | }) 79 | 80 | ipcRenderer.send("convert-audio", uuid, Buffer.from(filePath).toString("base64")) 81 | convertAudioTasks.add(uuid) 82 | }).finally(() => { 83 | if (convertAudioTasks.size === 0) { 84 | ipcRenderer.removeAllListeners("report-convert-audio") 85 | } 86 | }) 87 | } -------------------------------------------------------------------------------- /src/renderer/layout/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 55 | 56 | 59 | -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import Layout from "@/layout/index.vue" 3 | import Status from "@/views/status/index.vue" 4 | import Edit from "@/views/edit/index.vue" 5 | import SetupAutocut from "@/views/setup/autocut.vue" 6 | 7 | export const routes = [ 8 | { 9 | path: "/", 10 | component: Layout, 11 | redirect: "/status", 12 | children: [ 13 | { 14 | path: "/status", 15 | component: Status, 16 | }, 17 | { 18 | path: "/edit", 19 | component: Edit, 20 | }, 21 | ], 22 | }, 23 | { 24 | path: "/setup/autocut", 25 | component: SetupAutocut, 26 | }, 27 | ]; 28 | 29 | const router = createRouter({ 30 | history: createWebHashHistory(), 31 | routes, 32 | }); 33 | 34 | export default router; 35 | -------------------------------------------------------------------------------- /src/renderer/samples/node-api.ts: -------------------------------------------------------------------------------- 1 | import { lstat } from "fs/promises" 2 | import { cwd } from "process" 3 | import { ipcRenderer } from "electron" 4 | 5 | ipcRenderer.on("main-process-message", (_event, ...args) => { 6 | console.log("[Receive Main-process message]:", ...args) 7 | }) 8 | 9 | lstat(cwd()).then(stats => { 10 | console.log("[fs.lstat]", stats) 11 | }).catch(err => { 12 | console.error(err) 13 | }) 14 | -------------------------------------------------------------------------------- /src/renderer/store/config.ts: -------------------------------------------------------------------------------- 1 | import { hamiVuex } from "@/store"; 2 | 3 | export const CONFIG_NAME = "ct-config" 4 | 5 | const localConfig = localStorage.getItem(CONFIG_NAME) 6 | 7 | interface ConfigState { 8 | locale: string, 9 | installPath: string 10 | } 11 | 12 | export const configStore = hamiVuex.store({ 13 | $name: "config", 14 | $state: () => { 15 | 16 | return { 17 | locale: "en", 18 | installPath:"", 19 | ...( 20 | localConfig ? JSON.parse(localConfig) : {} 21 | ), 22 | } as ConfigState 23 | }, 24 | setLocale(locale: string) { 25 | this.$patch((state) => { 26 | state.locale = locale; 27 | }) 28 | }, 29 | async setInstallPath(path: string) { 30 | this.$patch((state) => { 31 | state.installPath = path 32 | }) 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /src/renderer/store/index.ts: -------------------------------------------------------------------------------- 1 | // store/index.js 2 | import { createHamiVuex } from "hami-vuex"; 3 | import { CONFIG_NAME } from "./config"; 4 | 5 | type State = { 6 | config: typeof import("./config").configStore 7 | status: typeof import("./status").statusStore 8 | } 9 | 10 | export const hamiVuex = createHamiVuex({ 11 | /* 可选:Vuex Store 的构造参数 */ 12 | }); 13 | 14 | 15 | hamiVuex.vuexStore.subscribe((mutation, state) => { 16 | // config 更新之后 17 | if (mutation.type.startsWith("config")){ 18 | // 自动保存到 localStorage 19 | localStorage.setItem(CONFIG_NAME, JSON.stringify(state.config)) 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /src/renderer/store/status.ts: -------------------------------------------------------------------------------- 1 | import { hamiVuex } from "@/store"; 2 | 3 | export const statusStore = hamiVuex.store({ 4 | $name:"status", 5 | $state: () => { 6 | return { 7 | ffmpegStatus: false, 8 | autocutStatus: false, 9 | } 10 | }, 11 | setFFmpeg(status: boolean) { 12 | this.$patch(state => { 13 | state.ffmpegStatus = status 14 | }) 15 | }, 16 | setAutocut(status: boolean) { 17 | this.$patch(state => { 18 | state.autocutStatus = status 19 | }) 20 | }, 21 | }) -------------------------------------------------------------------------------- /src/renderer/views/edit/Subtitle.vue: -------------------------------------------------------------------------------- 1 | 210 | 211 | 286 | 287 | 305 | -------------------------------------------------------------------------------- /src/renderer/views/edit/components/SubtitleItem.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 |