Phase 2: Core transcription pipeline and audio playback
- Implement faster-whisper TranscribeService with word-level timestamps, progress reporting, and hardware auto-detection - Wire up Rust SidecarManager for Python process lifecycle (spawn, IPC, shutdown) - Add transcribe_file Tauri command bridging frontend to Python sidecar - Integrate wavesurfer.js WaveformPlayer with play/pause, skip, seek controls - Build TranscriptEditor with word-level click-to-seek and active highlighting - Connect file import flow: prompt → asset load → transcribe → display - Add typed tauri-bridge service with TranscriptionResult interface - Add Python tests for hardware detection and transcription result formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
715
package-lock.json
generated
715
package-lock.json
generated
@@ -10,7 +10,11 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2"
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0",
|
||||||
|
"@tiptap/starter-kit": "^3.20.0",
|
||||||
|
"wavesurfer.js": "^7.12.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.6",
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
@@ -523,6 +527,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@remirror/core-constants": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.59.0",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||||
@@ -1218,6 +1228,379 @@
|
|||||||
"@tauri-apps/api": "^2.8.0"
|
"@tauri-apps/api": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-aC9aROgia/SpJqhsXFiX9TsligL8d+oeoI8W3u00WI45s0VfsqjgeKQLDLF7Tu7hC+7F02teC84SAHuup003VQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-LQzn6aGtL4WXz2+rYshl/7/VnP2qJTpD7fWL96GXAzhqviPEY1bJES7poqJb3MU/gzl8VJUVzVzU1VoVfUKlbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-sQklEWiyf58yDjiHtm5vmkVjfIc/cBuSusmCsQ0q9vGYnEF1iOHKhGpvnCeEXNeqF3fiJQRlquzt/6ymle3Iwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-OcKMeopBbqWzhSi6o8nNz0aayogg1sfOAhto3NxJu3Ya32dwBFqmHXSYM6uW4jOphNvVPyjiq9aNRh3qTdd1dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-TYDWFeSQ9umiyrqsT6VecbuhL8XIHkUhO+gEk0sVvH67ZLwjFDhAIIgWIr1/dbIGPcvMZM19E7xUUhAdIaXaOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-lBbmNek14aCjrHcBcq3PRqWfNLvC6bcRa2Osc6e/LtmXlcpype4f6n+Yx+WZ+f2uUh0UmDRCz7BEyUETEsDmlQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-oJfLIG3vAtZo/wg29WiBcyWt22KUgddpP8wqtCE+kY5Dw8znLR9ehNmVWlSWJA5OJUMO0ntAHx4bBT+I2MBd5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-d+cxplRlktVgZPwatnc34IArlppM0IFKS1J5wLk+ba1jidizsbMVh45tP/BTK2flhyfRqcNoB5R0TArhUpbkNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-P/LasfvG9/qFq43ZAlNbAnPnXC+/RJf49buTrhtFvI9Zg0+Lbpjx1oh6oMHB19T88Y28KtrckfFZ8aTSUWDq6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-rqvhMOw4f+XQmEthncbvDjgLH6fz8L9splnKZC7OeS0eX8b0qd7+xI1u5kyxF3KA2Z0BnigES++jjWuecqV6mA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-JgJhurnCe3eN6a0lEsNQM/46R1bcwzwWWZEFDSb1P9dR8+t1/5v7cMZWsSInpD7R4/74iJn0+M5hcXLwCmBmYA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-6uvcutFMv+9wPZgptDkbRDjAm3YVxlibmkhWD5GuaWwS9L/yUtobpI3GycujRSUZ8D3q6Q9J7LqpmQtQRTalWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-/DhnKQF8yN8RxtuL8abZ28wd5281EaGoE2Oha35zXSOF1vNYnbyt8Ymkv/7u1BcWEWTvRPgaju0YCGXisPRLYw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-qI/5A+R0ZWBxo/8HxSn1uOyr7odr3xHBZ/gzOR1GUJaZqjlJxkWFX0RtXMbLKEGEvT25o345cF7b0wFznEh8qA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-+V0/gsVWAv+7vcY0MAe6D52LYTIicMSHw00wz3ISZgprSb2yQhJ4+4gurOnUrQ4Du3AnRQvxPROaofwxIQ66WQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-qEtjaaGPuqaFB4VpLrGDoIe9RHnckxPfu6d3rc22ap6TAHCDyRv05CEyJogqccnFceG/v5WN4znUBER8RWnWHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-Z4GvKy04Ms4cLFN+CY6wXswd36xYsT2p/YL0V89LYFMZTerOeTjFYlndzn6svqL8NV1PRT5Diw4WTTxJSmcJPA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-jVKnJvrizLk7etwBMfyoj6H2GE4M+PD4k7Bwp6Bh1ohBWtfIA1TlngdS842Mx5i1VB2e3UWIwr8ZH46gl6cwMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-mM99zK4+RnEXIMCv6akfNATAs0Iija6FgyFA9J9NZ6N4o8y9QiNLLa6HjLpAC+W+VoCgQIekyoF/Q9ftxmAYDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-0vcTZRRAiDfon3VM1mHBr9EFmTkkUXMhm0Xtdtn0bGe+sIqufyi+hUYTEw93EQOD9XNsPkrud6jzQNYpX2H3AQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-tf8bE8tSaOEWabCzPm71xwiUhyMFKqY9jkP5af3Kr1/F45jzZFIQAYZooHI/+zCHRrgJ99MQHKHe1ZNvODrKHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-LzNXuy2jwR/y+ymoUqC72TiGzbOCjioIjsDu0MNYpHuHqTWPK5aV9Mh0nbZcYFy/7fPlV1q0W139EbJeYBZEAQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-HIsXX942w3nbxEQBlMAAR/aa6qiMBEP7CsSMxaxmTIVAmW35p6yUASw6GdV1u0o3lCZjXq2OSRMTskzIqi5uLg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-jn+2KnQZn+b+VXr8EFOJKsnjVNaA4diAEr6FOazupMt8W8ro1hfpYtZ25JL87Kao/WbMze55sd8M8BDXLUKu1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-changeset": "^2.3.0",
|
||||||
|
"prosemirror-collab": "^1.3.1",
|
||||||
|
"prosemirror-commands": "^1.6.2",
|
||||||
|
"prosemirror-dropcursor": "^1.8.1",
|
||||||
|
"prosemirror-gapcursor": "^1.3.2",
|
||||||
|
"prosemirror-history": "^1.4.1",
|
||||||
|
"prosemirror-inputrules": "^1.4.0",
|
||||||
|
"prosemirror-keymap": "^1.2.2",
|
||||||
|
"prosemirror-markdown": "^1.13.1",
|
||||||
|
"prosemirror-menu": "^1.2.4",
|
||||||
|
"prosemirror-model": "^1.24.1",
|
||||||
|
"prosemirror-schema-basic": "^1.2.3",
|
||||||
|
"prosemirror-schema-list": "^1.5.0",
|
||||||
|
"prosemirror-state": "^1.4.3",
|
||||||
|
"prosemirror-tables": "^1.6.4",
|
||||||
|
"prosemirror-trailing-node": "^3.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2",
|
||||||
|
"prosemirror-view": "^1.38.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/starter-kit": {
|
||||||
|
"version": "3.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.20.0.tgz",
|
||||||
|
"integrity": "sha512-W4+1re35pDNY/7rpXVg+OKo/Fa4Gfrn08Bq3E3fzlJw6gjE3tYU8dY9x9vC2rK9pd9NOp7Af11qCFDaWpohXkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/extension-blockquote": "^3.20.0",
|
||||||
|
"@tiptap/extension-bold": "^3.20.0",
|
||||||
|
"@tiptap/extension-bullet-list": "^3.20.0",
|
||||||
|
"@tiptap/extension-code": "^3.20.0",
|
||||||
|
"@tiptap/extension-code-block": "^3.20.0",
|
||||||
|
"@tiptap/extension-document": "^3.20.0",
|
||||||
|
"@tiptap/extension-dropcursor": "^3.20.0",
|
||||||
|
"@tiptap/extension-gapcursor": "^3.20.0",
|
||||||
|
"@tiptap/extension-hard-break": "^3.20.0",
|
||||||
|
"@tiptap/extension-heading": "^3.20.0",
|
||||||
|
"@tiptap/extension-horizontal-rule": "^3.20.0",
|
||||||
|
"@tiptap/extension-italic": "^3.20.0",
|
||||||
|
"@tiptap/extension-link": "^3.20.0",
|
||||||
|
"@tiptap/extension-list": "^3.20.0",
|
||||||
|
"@tiptap/extension-list-item": "^3.20.0",
|
||||||
|
"@tiptap/extension-list-keymap": "^3.20.0",
|
||||||
|
"@tiptap/extension-ordered-list": "^3.20.0",
|
||||||
|
"@tiptap/extension-paragraph": "^3.20.0",
|
||||||
|
"@tiptap/extension-strike": "^3.20.0",
|
||||||
|
"@tiptap/extension-text": "^3.20.0",
|
||||||
|
"@tiptap/extension-underline": "^3.20.0",
|
||||||
|
"@tiptap/extensions": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/chai": {
|
"node_modules/@types/chai": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
|
||||||
@@ -1250,6 +1633,28 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/markdown-it": {
|
||||||
|
"version": "14.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
|
||||||
|
"integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/linkify-it": "^5",
|
||||||
|
"@types/mdurl": "^2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
@@ -1385,6 +1790,12 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
|
||||||
@@ -1488,6 +1899,12 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/crelt": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -1533,6 +1950,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-module-lexer": {
|
"node_modules/es-module-lexer": {
|
||||||
"version": "1.7.0",
|
"version": "1.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
|
||||||
@@ -1582,6 +2011,18 @@
|
|||||||
"@esbuild/win32-x64": "0.25.12"
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/escape-string-regexp": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esm-env": {
|
"node_modules/esm-env": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||||
@@ -1679,6 +2120,21 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linkify-it": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"uc.micro": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/locate-character": {
|
"node_modules/locate-character": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||||
@@ -1703,6 +2159,29 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.5"
|
"@jridgewell/sourcemap-codec": "^1.5.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/markdown-it": {
|
||||||
|
"version": "14.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
|
||||||
|
"integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1",
|
||||||
|
"entities": "^4.4.0",
|
||||||
|
"linkify-it": "^5.0.0",
|
||||||
|
"mdurl": "^2.0.0",
|
||||||
|
"punycode.js": "^2.3.1",
|
||||||
|
"uc.micro": "^2.1.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"markdown-it": "bin/markdown-it.mjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mdurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
@@ -1749,6 +2228,12 @@
|
|||||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/orderedmap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
@@ -1815,6 +2300,210 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-changeset": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-collab": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-commands": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-dropcursor": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0",
|
||||||
|
"prosemirror-view": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-gapcursor": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.0.0",
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-history": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.2.2",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.31.0",
|
||||||
|
"rope-sequence": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-inputrules": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-keymap": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"w3c-keyname": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-markdown": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/markdown-it": "^14.0.0",
|
||||||
|
"markdown-it": "^14.0.0",
|
||||||
|
"prosemirror-model": "^1.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-menu": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-TImyPXCHPcDsSka2/lwJ6WjTASr4re/qWq1yoTTuLOqfXucwF6VcRa2LWCkM/EyTD1UO3CUwiH8qURJoWJRxwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"crelt": "^1.0.0",
|
||||||
|
"prosemirror-commands": "^1.0.0",
|
||||||
|
"prosemirror-history": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-model": {
|
||||||
|
"version": "1.25.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||||
|
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"orderedmap": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-basic": {
|
||||||
|
"version": "1.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
|
||||||
|
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-list": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-state": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-tables": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.4",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-transform": "^1.10.5",
|
||||||
|
"prosemirror-view": "^1.41.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-trailing-node": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@remirror/core-constants": "3.0.0",
|
||||||
|
"escape-string-regexp": "^4.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"prosemirror-model": "^1.22.1",
|
||||||
|
"prosemirror-state": "^1.4.2",
|
||||||
|
"prosemirror-view": "^1.33.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-transform": {
|
||||||
|
"version": "1.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz",
|
||||||
|
"integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-view": {
|
||||||
|
"version": "1.41.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz",
|
||||||
|
"integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.20.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/punycode.js": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -1874,6 +2563,12 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rope-sequence": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/sade": {
|
"node_modules/sade": {
|
||||||
"version": "1.8.1",
|
"version": "1.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
|
||||||
@@ -2090,6 +2785,12 @@
|
|||||||
"node": ">=14.17"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uc.micro": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.4.1",
|
"version": "6.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
@@ -2281,6 +2982,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/wavesurfer.js": {
|
||||||
|
"version": "7.12.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/wavesurfer.js/-/wavesurfer.js-7.12.1.tgz",
|
||||||
|
"integrity": "sha512-NswPjVHxk0Q1F/VMRemCPUzSojjuHHisQrBqQiRXg7MVbe3f5vQ6r0rTTXA/a/neC/4hnOEC4YpXca4LpH0SUg==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/why-is-node-running": {
|
"node_modules/why-is-node-running": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||||
|
|||||||
@@ -16,7 +16,11 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-opener": "^2"
|
"@tauri-apps/plugin-opener": "^2",
|
||||||
|
"@tiptap/core": "^3.20.0",
|
||||||
|
"@tiptap/pm": "^3.20.0",
|
||||||
|
"@tiptap/starter-kit": "^3.20.0",
|
||||||
|
"wavesurfer.js": "^7.12.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-static": "^3.0.6",
|
"@sveltejs/adapter-static": "^3.0.6",
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ description = "Python sidecar for Voice to Notes — transcription, diarization,
|
|||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
dependencies = []
|
dependencies = [
|
||||||
|
"faster-whisper>=1.1.0",
|
||||||
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
|
|||||||
20
python/tests/test_hardware.py
Normal file
20
python/tests/test_hardware.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""Tests for hardware detection."""
|
||||||
|
|
||||||
|
from voice_to_notes.hardware.detect import HardwareInfo, detect_hardware
|
||||||
|
|
||||||
|
|
||||||
|
def test_hardware_info_defaults():
|
||||||
|
"""Test HardwareInfo default values."""
|
||||||
|
info = HardwareInfo()
|
||||||
|
assert info.has_cuda is False
|
||||||
|
assert info.recommended_model == "base"
|
||||||
|
assert info.recommended_device == "cpu"
|
||||||
|
|
||||||
|
|
||||||
|
def test_detect_hardware_returns_info():
|
||||||
|
"""Test that detect_hardware returns a valid HardwareInfo."""
|
||||||
|
info = detect_hardware()
|
||||||
|
assert isinstance(info, HardwareInfo)
|
||||||
|
assert info.cpu_cores > 0
|
||||||
|
# Should default to CPU since we're likely in a test env without GPU
|
||||||
|
assert info.recommended_device in ("cpu", "cuda")
|
||||||
51
python/tests/test_transcribe.py
Normal file
51
python/tests/test_transcribe.py
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"""Tests for transcription service."""
|
||||||
|
|
||||||
|
from voice_to_notes.services.transcribe import (
|
||||||
|
SegmentResult,
|
||||||
|
TranscriptionResult,
|
||||||
|
WordResult,
|
||||||
|
result_to_payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_to_payload():
|
||||||
|
"""Test converting TranscriptionResult to IPC payload."""
|
||||||
|
result = TranscriptionResult(
|
||||||
|
segments=[
|
||||||
|
SegmentResult(
|
||||||
|
text="hello world",
|
||||||
|
start_ms=0,
|
||||||
|
end_ms=2000,
|
||||||
|
words=[
|
||||||
|
WordResult(word="hello", start_ms=0, end_ms=500, confidence=0.95),
|
||||||
|
WordResult(word="world", start_ms=600, end_ms=2000, confidence=0.92),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
language="en",
|
||||||
|
language_probability=0.98,
|
||||||
|
duration_ms=2000,
|
||||||
|
)
|
||||||
|
|
||||||
|
payload = result_to_payload(result)
|
||||||
|
|
||||||
|
assert payload["language"] == "en"
|
||||||
|
assert payload["duration_ms"] == 2000
|
||||||
|
assert len(payload["segments"]) == 1
|
||||||
|
|
||||||
|
seg = payload["segments"][0]
|
||||||
|
assert seg["text"] == "hello world"
|
||||||
|
assert seg["start_ms"] == 0
|
||||||
|
assert seg["end_ms"] == 2000
|
||||||
|
assert len(seg["words"]) == 2
|
||||||
|
assert seg["words"][0]["word"] == "hello"
|
||||||
|
assert seg["words"][0]["confidence"] == 0.95
|
||||||
|
|
||||||
|
|
||||||
|
def test_result_to_payload_empty():
|
||||||
|
"""Test empty transcription result."""
|
||||||
|
result = TranscriptionResult()
|
||||||
|
payload = result_to_payload(result)
|
||||||
|
assert payload["segments"] == []
|
||||||
|
assert payload["language"] == ""
|
||||||
|
assert payload["duration_ms"] == 0
|
||||||
@@ -2,8 +2,74 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
# TODO: Implement hardware detection
|
import os
|
||||||
# - Check torch.cuda.is_available()
|
import sys
|
||||||
# - Detect VRAM size
|
from dataclasses import dataclass
|
||||||
# - Detect CPU cores and available RAM
|
|
||||||
# - Return recommended model configuration
|
|
||||||
|
@dataclass
|
||||||
|
class HardwareInfo:
|
||||||
|
"""Detected hardware capabilities."""
|
||||||
|
|
||||||
|
has_cuda: bool = False
|
||||||
|
cuda_device_name: str = ""
|
||||||
|
vram_mb: int = 0
|
||||||
|
ram_mb: int = 0
|
||||||
|
cpu_cores: int = 0
|
||||||
|
recommended_model: str = "base"
|
||||||
|
recommended_device: str = "cpu"
|
||||||
|
recommended_compute_type: str = "int8"
|
||||||
|
|
||||||
|
|
||||||
|
def detect_hardware() -> HardwareInfo:
|
||||||
|
"""Detect available hardware and recommend model configuration."""
|
||||||
|
info = HardwareInfo()
|
||||||
|
|
||||||
|
# CPU info
|
||||||
|
info.cpu_cores = os.cpu_count() or 1
|
||||||
|
|
||||||
|
# RAM info
|
||||||
|
try:
|
||||||
|
with open("/proc/meminfo") as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("MemTotal:"):
|
||||||
|
# Value is in kB
|
||||||
|
info.ram_mb = int(line.split()[1]) // 1024
|
||||||
|
break
|
||||||
|
except (FileNotFoundError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# CUDA detection
|
||||||
|
try:
|
||||||
|
import torch
|
||||||
|
|
||||||
|
if torch.cuda.is_available():
|
||||||
|
info.has_cuda = True
|
||||||
|
info.cuda_device_name = torch.cuda.get_device_name(0)
|
||||||
|
info.vram_mb = torch.cuda.get_device_properties(0).total_mem // (1024 * 1024)
|
||||||
|
except ImportError:
|
||||||
|
print("[sidecar] torch not available, GPU detection skipped", file=sys.stderr, flush=True)
|
||||||
|
|
||||||
|
# Model recommendation based on hardware
|
||||||
|
if info.has_cuda and info.vram_mb >= 8000:
|
||||||
|
info.recommended_model = "large-v3-turbo"
|
||||||
|
info.recommended_device = "cuda"
|
||||||
|
info.recommended_compute_type = "int8"
|
||||||
|
elif info.has_cuda and info.vram_mb >= 4000:
|
||||||
|
info.recommended_model = "medium"
|
||||||
|
info.recommended_device = "cuda"
|
||||||
|
info.recommended_compute_type = "int8"
|
||||||
|
elif info.ram_mb >= 16000:
|
||||||
|
info.recommended_model = "medium"
|
||||||
|
info.recommended_device = "cpu"
|
||||||
|
info.recommended_compute_type = "int8"
|
||||||
|
elif info.ram_mb >= 8000:
|
||||||
|
info.recommended_model = "small"
|
||||||
|
info.recommended_device = "cpu"
|
||||||
|
info.recommended_compute_type = "int8"
|
||||||
|
else:
|
||||||
|
info.recommended_model = "base"
|
||||||
|
info.recommended_device = "cpu"
|
||||||
|
info.recommended_compute_type = "int8"
|
||||||
|
|
||||||
|
return info
|
||||||
|
|||||||
@@ -37,3 +37,49 @@ class HandlerRegistry:
|
|||||||
def ping_handler(msg: IPCMessage) -> IPCMessage:
|
def ping_handler(msg: IPCMessage) -> IPCMessage:
|
||||||
"""Simple ping handler for testing connectivity."""
|
"""Simple ping handler for testing connectivity."""
|
||||||
return IPCMessage(id=msg.id, type="pong", payload={"echo": msg.payload})
|
return IPCMessage(id=msg.id, type="pong", payload={"echo": msg.payload})
|
||||||
|
|
||||||
|
|
||||||
|
def make_transcribe_handler() -> HandlerFunc:
|
||||||
|
"""Create a transcription handler with a persistent TranscribeService."""
|
||||||
|
from voice_to_notes.services.transcribe import TranscribeService, result_to_payload
|
||||||
|
|
||||||
|
service = TranscribeService()
|
||||||
|
|
||||||
|
def handler(msg: IPCMessage) -> IPCMessage:
|
||||||
|
payload = msg.payload
|
||||||
|
result = service.transcribe(
|
||||||
|
request_id=msg.id,
|
||||||
|
file_path=payload["file"],
|
||||||
|
model_name=payload.get("model", "base"),
|
||||||
|
device=payload.get("device", "cpu"),
|
||||||
|
compute_type=payload.get("compute_type", "int8"),
|
||||||
|
language=payload.get("language"),
|
||||||
|
)
|
||||||
|
return IPCMessage(
|
||||||
|
id=msg.id,
|
||||||
|
type="transcribe.result",
|
||||||
|
payload=result_to_payload(result),
|
||||||
|
)
|
||||||
|
|
||||||
|
return handler
|
||||||
|
|
||||||
|
|
||||||
|
def hardware_detect_handler(msg: IPCMessage) -> IPCMessage:
|
||||||
|
"""Detect hardware capabilities and return recommendations."""
|
||||||
|
from voice_to_notes.hardware.detect import detect_hardware
|
||||||
|
|
||||||
|
info = detect_hardware()
|
||||||
|
return IPCMessage(
|
||||||
|
id=msg.id,
|
||||||
|
type="hardware.info",
|
||||||
|
payload={
|
||||||
|
"has_cuda": info.has_cuda,
|
||||||
|
"cuda_device_name": info.cuda_device_name,
|
||||||
|
"vram_mb": info.vram_mb,
|
||||||
|
"ram_mb": info.ram_mb,
|
||||||
|
"cpu_cores": info.cpu_cores,
|
||||||
|
"recommended_model": info.recommended_model,
|
||||||
|
"recommended_device": info.recommended_device,
|
||||||
|
"recommended_compute_type": info.recommended_compute_type,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ from __future__ import annotations
|
|||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from voice_to_notes.ipc.handlers import HandlerRegistry, ping_handler
|
from voice_to_notes.ipc.handlers import (
|
||||||
|
HandlerRegistry,
|
||||||
|
hardware_detect_handler,
|
||||||
|
make_transcribe_handler,
|
||||||
|
ping_handler,
|
||||||
|
)
|
||||||
from voice_to_notes.ipc.messages import ready_message
|
from voice_to_notes.ipc.messages import ready_message
|
||||||
from voice_to_notes.ipc.protocol import read_message, write_message
|
from voice_to_notes.ipc.protocol import read_message, write_message
|
||||||
|
|
||||||
@@ -14,7 +19,9 @@ def create_registry() -> HandlerRegistry:
|
|||||||
"""Set up the message handler registry."""
|
"""Set up the message handler registry."""
|
||||||
registry = HandlerRegistry()
|
registry = HandlerRegistry()
|
||||||
registry.register("ping", ping_handler)
|
registry.register("ping", ping_handler)
|
||||||
# TODO: Register transcribe, diarize, pipeline, ai, export handlers
|
registry.register("transcribe.start", make_transcribe_handler())
|
||||||
|
registry.register("hardware.detect", hardware_detect_handler)
|
||||||
|
# TODO: Register diarize, pipeline, ai, export handlers
|
||||||
return registry
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,193 @@
|
|||||||
"""Transcription service — faster-whisper + wav2vec2 pipeline."""
|
"""Transcription service — faster-whisper pipeline with word-level timestamps."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from faster_whisper import WhisperModel
|
||||||
|
|
||||||
|
from voice_to_notes.ipc.messages import progress_message
|
||||||
|
from voice_to_notes.ipc.protocol import write_message
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class WordResult:
|
||||||
|
"""A single word with timestamp."""
|
||||||
|
|
||||||
|
word: str
|
||||||
|
start_ms: int
|
||||||
|
end_ms: int
|
||||||
|
confidence: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SegmentResult:
|
||||||
|
"""A transcription segment with words."""
|
||||||
|
|
||||||
|
text: str
|
||||||
|
start_ms: int
|
||||||
|
end_ms: int
|
||||||
|
words: list[WordResult] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TranscriptionResult:
|
||||||
|
"""Full transcription output."""
|
||||||
|
|
||||||
|
segments: list[SegmentResult] = field(default_factory=list)
|
||||||
|
language: str = ""
|
||||||
|
language_probability: float = 0.0
|
||||||
|
duration_ms: int = 0
|
||||||
|
|
||||||
|
|
||||||
class TranscribeService:
|
class TranscribeService:
|
||||||
"""Handles audio transcription via faster-whisper."""
|
"""Handles audio transcription via faster-whisper."""
|
||||||
|
|
||||||
# TODO: Implement faster-whisper integration
|
def __init__(self) -> None:
|
||||||
# - Load model based on hardware detection
|
self._model: WhisperModel | None = None
|
||||||
# - Transcribe audio with word-level timestamps
|
self._current_model_name: str = ""
|
||||||
# - Report progress via IPC
|
self._current_device: str = ""
|
||||||
pass
|
self._current_compute_type: str = ""
|
||||||
|
|
||||||
|
def _ensure_model(
|
||||||
|
self,
|
||||||
|
model_name: str = "base",
|
||||||
|
device: str = "cpu",
|
||||||
|
compute_type: str = "int8",
|
||||||
|
) -> WhisperModel:
|
||||||
|
"""Load or reuse the Whisper model."""
|
||||||
|
if (
|
||||||
|
self._model is not None
|
||||||
|
and self._current_model_name == model_name
|
||||||
|
and self._current_device == device
|
||||||
|
and self._current_compute_type == compute_type
|
||||||
|
):
|
||||||
|
return self._model
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"[sidecar] Loading model {model_name} on {device} ({compute_type})",
|
||||||
|
file=sys.stderr,
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
self._model = WhisperModel(
|
||||||
|
model_name,
|
||||||
|
device=device,
|
||||||
|
compute_type=compute_type,
|
||||||
|
)
|
||||||
|
self._current_model_name = model_name
|
||||||
|
self._current_device = device
|
||||||
|
self._current_compute_type = compute_type
|
||||||
|
return self._model
|
||||||
|
|
||||||
|
def transcribe(
|
||||||
|
self,
|
||||||
|
request_id: str,
|
||||||
|
file_path: str,
|
||||||
|
model_name: str = "base",
|
||||||
|
device: str = "cpu",
|
||||||
|
compute_type: str = "int8",
|
||||||
|
language: str | None = None,
|
||||||
|
) -> TranscriptionResult:
|
||||||
|
"""Transcribe an audio file with word-level timestamps.
|
||||||
|
|
||||||
|
Sends progress messages via IPC during processing.
|
||||||
|
"""
|
||||||
|
# Stage: loading model
|
||||||
|
write_message(progress_message(request_id, 0, "loading_model", f"Loading {model_name}..."))
|
||||||
|
model = self._ensure_model(model_name, device, compute_type)
|
||||||
|
|
||||||
|
# Stage: transcribing
|
||||||
|
write_message(progress_message(request_id, 10, "transcribing", "Starting transcription..."))
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
segments_iter, info = model.transcribe(
|
||||||
|
file_path,
|
||||||
|
language=language,
|
||||||
|
word_timestamps=True,
|
||||||
|
vad_filter=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = TranscriptionResult(
|
||||||
|
language=info.language,
|
||||||
|
language_probability=info.language_probability,
|
||||||
|
duration_ms=int(info.duration * 1000),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Process segments with progress reporting
|
||||||
|
total_duration = info.duration if info.duration > 0 else 1.0
|
||||||
|
segment_count = 0
|
||||||
|
|
||||||
|
for segment in segments_iter:
|
||||||
|
segment_count += 1
|
||||||
|
progress_pct = min(10 + int((segment.end / total_duration) * 80), 90)
|
||||||
|
|
||||||
|
words = []
|
||||||
|
if segment.words:
|
||||||
|
for w in segment.words:
|
||||||
|
words.append(
|
||||||
|
WordResult(
|
||||||
|
word=w.word.strip(),
|
||||||
|
start_ms=int(w.start * 1000),
|
||||||
|
end_ms=int(w.end * 1000),
|
||||||
|
confidence=round(w.probability, 4),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
result.segments.append(
|
||||||
|
SegmentResult(
|
||||||
|
text=segment.text.strip(),
|
||||||
|
start_ms=int(segment.start * 1000),
|
||||||
|
end_ms=int(segment.end * 1000),
|
||||||
|
words=words,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Send progress every few segments
|
||||||
|
if segment_count % 5 == 0:
|
||||||
|
write_message(
|
||||||
|
progress_message(
|
||||||
|
request_id,
|
||||||
|
progress_pct,
|
||||||
|
"transcribing",
|
||||||
|
f"Processed {segment_count} segments...",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
elapsed = time.time() - start_time
|
||||||
|
print(
|
||||||
|
f"[sidecar] Transcription complete: {segment_count} segments in {elapsed:.1f}s",
|
||||||
|
file=sys.stderr,
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
write_message(progress_message(request_id, 100, "done", "Transcription complete"))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def result_to_payload(result: TranscriptionResult) -> dict[str, Any]:
|
||||||
|
"""Convert TranscriptionResult to IPC payload dict."""
|
||||||
|
return {
|
||||||
|
"segments": [
|
||||||
|
{
|
||||||
|
"text": seg.text,
|
||||||
|
"start_ms": seg.start_ms,
|
||||||
|
"end_ms": seg.end_ms,
|
||||||
|
"words": [
|
||||||
|
{
|
||||||
|
"word": w.word,
|
||||||
|
"start_ms": w.start_ms,
|
||||||
|
"end_ms": w.end_ms,
|
||||||
|
"confidence": w.confidence,
|
||||||
|
}
|
||||||
|
for w in seg.words
|
||||||
|
],
|
||||||
|
}
|
||||||
|
for seg in result.segments
|
||||||
|
],
|
||||||
|
"language": result.language,
|
||||||
|
"language_probability": result.language_probability,
|
||||||
|
"duration_ms": result.duration_ms,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,52 @@
|
|||||||
// Transcription commands — start/stop/monitor transcription via Python sidecar
|
use serde_json::{json, Value};
|
||||||
// TODO: Implement when sidecar IPC is connected
|
|
||||||
|
use crate::sidecar::messages::IPCMessage;
|
||||||
|
use crate::sidecar::SidecarManager;
|
||||||
|
|
||||||
|
/// Start transcription of an audio file via the Python sidecar.
|
||||||
|
///
|
||||||
|
/// This is a blocking command — it starts the sidecar if needed,
|
||||||
|
/// sends the transcribe request, and waits for the result.
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn transcribe_file(
|
||||||
|
file_path: String,
|
||||||
|
model: Option<String>,
|
||||||
|
device: Option<String>,
|
||||||
|
language: Option<String>,
|
||||||
|
) -> Result<Value, String> {
|
||||||
|
// Determine Python sidecar path (relative to app)
|
||||||
|
let python_path = std::env::current_dir()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.join("../python")
|
||||||
|
.canonicalize()
|
||||||
|
.map_err(|e| format!("Cannot find python directory: {e}"))?;
|
||||||
|
|
||||||
|
let python_path_str = python_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let manager = SidecarManager::new();
|
||||||
|
manager.start(&python_path_str)?;
|
||||||
|
|
||||||
|
let request_id = uuid::Uuid::new_v4().to_string();
|
||||||
|
let msg = IPCMessage::new(
|
||||||
|
&request_id,
|
||||||
|
"transcribe.start",
|
||||||
|
json!({
|
||||||
|
"file": file_path,
|
||||||
|
"model": model.unwrap_or_else(|| "base".to_string()),
|
||||||
|
"device": device.unwrap_or_else(|| "cpu".to_string()),
|
||||||
|
"compute_type": "int8",
|
||||||
|
"language": language,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = manager.send_and_receive(&msg)?;
|
||||||
|
|
||||||
|
if response.msg_type == "error" {
|
||||||
|
return Err(format!(
|
||||||
|
"Transcription error: {}",
|
||||||
|
response.payload.get("message").and_then(|v| v.as_str()).unwrap_or("unknown")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response.payload)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
pub mod sidecar;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
|
|
||||||
use commands::project::{create_project, get_project, list_projects};
|
use commands::project::{create_project, get_project, list_projects};
|
||||||
|
use commands::transcribe::transcribe_file;
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
@@ -12,6 +14,7 @@ pub fn run() {
|
|||||||
create_project,
|
create_project,
|
||||||
get_project,
|
get_project,
|
||||||
list_projects,
|
list_projects,
|
||||||
|
transcribe_file,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
16
src-tauri/src/sidecar/ipc.rs
Normal file
16
src-tauri/src/sidecar/ipc.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use super::messages::IPCMessage;
|
||||||
|
|
||||||
|
/// Serialize and write an IPC message to a writer (stdin pipe).
|
||||||
|
pub fn send_message<W: Write>(writer: &mut W, msg: &IPCMessage) -> Result<(), String> {
|
||||||
|
let json = serde_json::to_string(msg).map_err(|e| e.to_string())?;
|
||||||
|
writer
|
||||||
|
.write_all(json.as_bytes())
|
||||||
|
.map_err(|e| format!("Write error: {e}"))?;
|
||||||
|
writer
|
||||||
|
.write_all(b"\n")
|
||||||
|
.map_err(|e| format!("Write error: {e}"))?;
|
||||||
|
writer.flush().map_err(|e| format!("Flush error: {e}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
21
src-tauri/src/sidecar/messages.rs
Normal file
21
src-tauri/src/sidecar/messages.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
/// IPC message exchanged between Rust and Python sidecar.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct IPCMessage {
|
||||||
|
pub id: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
pub msg_type: String,
|
||||||
|
pub payload: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IPCMessage {
|
||||||
|
pub fn new(id: &str, msg_type: &str, payload: Value) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.to_string(),
|
||||||
|
msg_type: msg_type.to_string(),
|
||||||
|
payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
150
src-tauri/src/sidecar/mod.rs
Normal file
150
src-tauri/src/sidecar/mod.rs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
pub mod ipc;
|
||||||
|
pub mod messages;
|
||||||
|
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::process::{Child, Command, Stdio};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::sidecar::messages::IPCMessage;
|
||||||
|
|
||||||
|
/// Manages the Python sidecar process lifecycle.
|
||||||
|
pub struct SidecarManager {
|
||||||
|
process: Mutex<Option<Child>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SidecarManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
process: Mutex::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn the Python sidecar process.
|
||||||
|
pub fn start(&self, python_path: &str) -> Result<(), String> {
|
||||||
|
let child = Command::new("python3")
|
||||||
|
.arg("-m")
|
||||||
|
.arg("voice_to_notes.main")
|
||||||
|
.current_dir(python_path)
|
||||||
|
.env("PYTHONPATH", python_path)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::inherit()) // Let sidecar logs go to parent's stderr
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to start sidecar: {e}"))?;
|
||||||
|
|
||||||
|
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||||
|
*proc = Some(child);
|
||||||
|
|
||||||
|
// Wait for the "ready" message
|
||||||
|
self.wait_for_ready()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wait for the sidecar to send its ready message.
|
||||||
|
fn wait_for_ready(&self) -> Result<(), String> {
|
||||||
|
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some(ref mut child) = *proc {
|
||||||
|
if let Some(ref mut stdout) = child.stdout {
|
||||||
|
let reader = BufReader::new(stdout);
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = line.map_err(|e| format!("Read error: {e}"))?;
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(msg) = serde_json::from_str::<IPCMessage>(&line) {
|
||||||
|
if msg.msg_type == "ready" {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we got a non-ready message, something's wrong but don't block forever
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err("Sidecar did not send ready message".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a message to the sidecar and read the response.
|
||||||
|
/// This is a blocking call.
|
||||||
|
pub fn send_and_receive(&self, msg: &IPCMessage) -> Result<IPCMessage, String> {
|
||||||
|
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some(ref mut child) = *proc {
|
||||||
|
// Write message to stdin
|
||||||
|
if let Some(ref mut stdin) = child.stdin {
|
||||||
|
let json = serde_json::to_string(msg).map_err(|e| e.to_string())?;
|
||||||
|
stdin
|
||||||
|
.write_all(json.as_bytes())
|
||||||
|
.map_err(|e| format!("Write error: {e}"))?;
|
||||||
|
stdin
|
||||||
|
.write_all(b"\n")
|
||||||
|
.map_err(|e| format!("Write error: {e}"))?;
|
||||||
|
stdin.flush().map_err(|e| format!("Flush error: {e}"))?;
|
||||||
|
} else {
|
||||||
|
return Err("Sidecar stdin not available".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read response from stdout
|
||||||
|
if let Some(ref mut stdout) = child.stdout {
|
||||||
|
let mut reader = BufReader::new(stdout);
|
||||||
|
let mut line = String::new();
|
||||||
|
// Read lines until we get a response (skip progress messages, collect them)
|
||||||
|
loop {
|
||||||
|
line.clear();
|
||||||
|
let bytes_read = reader
|
||||||
|
.read_line(&mut line)
|
||||||
|
.map_err(|e| format!("Read error: {e}"))?;
|
||||||
|
if bytes_read == 0 {
|
||||||
|
return Err("Sidecar closed stdout".to_string());
|
||||||
|
}
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let response: IPCMessage =
|
||||||
|
serde_json::from_str(trimmed).map_err(|e| format!("Parse error: {e}"))?;
|
||||||
|
|
||||||
|
// If it's a progress message, we could emit it as an event
|
||||||
|
// For now, skip progress and return the final result/error
|
||||||
|
if response.msg_type != "progress" {
|
||||||
|
return Ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Err("Sidecar stdout not available".to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("Sidecar not running".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stop the sidecar process.
|
||||||
|
pub fn stop(&self) -> Result<(), String> {
|
||||||
|
let mut proc = self.process.lock().map_err(|e| e.to_string())?;
|
||||||
|
if let Some(ref mut child) = proc.take() {
|
||||||
|
// Close stdin to signal EOF
|
||||||
|
drop(child.stdin.take());
|
||||||
|
// Wait briefly for clean exit, then kill
|
||||||
|
match child.wait() {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(e) => {
|
||||||
|
let _ = child.kill();
|
||||||
|
Err(format!("Sidecar did not exit cleanly: {e}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_running(&self) -> bool {
|
||||||
|
let proc = self.process.lock().ok();
|
||||||
|
proc.map_or(false, |p| p.is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for SidecarManager {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,154 @@
|
|||||||
<div class="transcript-editor">
|
<script lang="ts">
|
||||||
<p>Transcript Editor</p>
|
import { segments, speakers } from '$lib/stores/transcript';
|
||||||
<p class="placeholder">TipTap rich text editor will be integrated here</p>
|
import { currentTimeMs } from '$lib/stores/playback';
|
||||||
|
import type { Segment, Word, Speaker } from '$lib/types/transcript';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onWordClick?: (timeMs: number) => void;
|
||||||
|
onTextEdit?: (segmentId: string, newText: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { onWordClick, onTextEdit }: Props = $props();
|
||||||
|
|
||||||
|
let transcriptContainer: HTMLDivElement;
|
||||||
|
|
||||||
|
function getSpeakerName(speakerId: string | null, speakerList: Speaker[]): string {
|
||||||
|
if (!speakerId) return 'Unknown';
|
||||||
|
const speaker = speakerList.find(s => s.id === speakerId);
|
||||||
|
return speaker?.display_name || speaker?.label || 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSpeakerColor(speakerId: string | null, speakerList: Speaker[]): string {
|
||||||
|
if (!speakerId) return '#888';
|
||||||
|
const speaker = speakerList.find(s => s.id === speakerId);
|
||||||
|
return speaker?.color || '#888';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTimestamp(ms: number): string {
|
||||||
|
const totalSeconds = Math.floor(ms / 1000);
|
||||||
|
const m = Math.floor(totalSeconds / 60);
|
||||||
|
const s = totalSeconds % 60;
|
||||||
|
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWordActive(word: Word, currentMs: number): boolean {
|
||||||
|
return currentMs >= word.start_ms && currentMs <= word.end_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSegmentActive(segment: Segment, currentMs: number): boolean {
|
||||||
|
return currentMs >= segment.start_ms && currentMs <= segment.end_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWordClick(word: Word) {
|
||||||
|
onWordClick?.(word.start_ms);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="transcript-editor" bind:this={transcriptContainer}>
|
||||||
|
{#if $segments.length === 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>No transcript yet</p>
|
||||||
|
<p class="hint">Import an audio file and run transcription to get started</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
{#each $segments as segment (segment.id)}
|
||||||
|
<div
|
||||||
|
class="segment"
|
||||||
|
class:active={isSegmentActive(segment, $currentTimeMs)}
|
||||||
|
>
|
||||||
|
<div class="segment-header">
|
||||||
|
<span
|
||||||
|
class="speaker-label"
|
||||||
|
style="border-left-color: {getSpeakerColor(segment.speaker_id, $speakers)}"
|
||||||
|
>
|
||||||
|
{getSpeakerName(segment.speaker_id, $speakers)}
|
||||||
|
</span>
|
||||||
|
<span class="timestamp">{formatTimestamp(segment.start_ms)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="segment-text">
|
||||||
|
{#each segment.words as word (word.id)}
|
||||||
|
<span
|
||||||
|
class="word"
|
||||||
|
class:word-active={isWordActive(word, $currentTimeMs)}
|
||||||
|
onclick={() => handleWordClick(word)}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onkeydown={(e) => { if (e.key === 'Enter') handleWordClick(word); }}
|
||||||
|
>{word.word} </span>
|
||||||
|
{:else}
|
||||||
|
<span class="segment-plain-text">{segment.text}</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.transcript-editor {
|
.transcript-editor {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: #16213e;
|
background: #16213e;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
.placeholder {
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
color: #666;
|
color: #666;
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
.segment {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.segment.active {
|
||||||
|
background: rgba(233, 69, 96, 0.1);
|
||||||
|
}
|
||||||
|
.segment-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
.speaker-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
border-left: 3px solid;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
.timestamp {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.segment-text {
|
||||||
|
line-height: 1.6;
|
||||||
|
padding-left: 0.75rem;
|
||||||
|
}
|
||||||
|
.word {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 1px;
|
||||||
|
transition: background-color 0.15s;
|
||||||
|
}
|
||||||
|
.word:hover {
|
||||||
|
background: rgba(233, 69, 96, 0.2);
|
||||||
|
}
|
||||||
|
.word-active {
|
||||||
|
background: rgba(233, 69, 96, 0.35);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.segment-plain-text {
|
||||||
|
color: #ccc;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,17 +1,144 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount, onDestroy } from 'svelte';
|
||||||
|
import WaveSurfer from 'wavesurfer.js';
|
||||||
|
import { isPlaying, currentTimeMs, durationMs } from '$lib/stores/playback';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
audioUrl?: string;
|
||||||
|
onSeek?: (timeMs: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { audioUrl = '', onSeek }: Props = $props();
|
||||||
|
|
||||||
|
let container: HTMLDivElement;
|
||||||
|
let wavesurfer: WaveSurfer | null = $state(null);
|
||||||
|
let currentTime = $state('0:00');
|
||||||
|
let totalTime = $state('0:00');
|
||||||
|
|
||||||
|
function formatTime(seconds: number): string {
|
||||||
|
const m = Math.floor(seconds / 60);
|
||||||
|
const s = Math.floor(seconds % 60);
|
||||||
|
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
wavesurfer = WaveSurfer.create({
|
||||||
|
container,
|
||||||
|
waveColor: '#4a5568',
|
||||||
|
progressColor: '#e94560',
|
||||||
|
cursorColor: '#e94560',
|
||||||
|
height: 80,
|
||||||
|
barWidth: 2,
|
||||||
|
barGap: 1,
|
||||||
|
barRadius: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
wavesurfer.on('timeupdate', (time: number) => {
|
||||||
|
currentTimeMs.set(Math.round(time * 1000));
|
||||||
|
currentTime = formatTime(time);
|
||||||
|
});
|
||||||
|
|
||||||
|
wavesurfer.on('ready', () => {
|
||||||
|
const dur = wavesurfer!.getDuration();
|
||||||
|
durationMs.set(Math.round(dur * 1000));
|
||||||
|
totalTime = formatTime(dur);
|
||||||
|
});
|
||||||
|
|
||||||
|
wavesurfer.on('play', () => isPlaying.set(true));
|
||||||
|
wavesurfer.on('pause', () => isPlaying.set(false));
|
||||||
|
wavesurfer.on('finish', () => isPlaying.set(false));
|
||||||
|
|
||||||
|
if (audioUrl) {
|
||||||
|
wavesurfer.load(audioUrl);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
wavesurfer?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
function togglePlayPause() {
|
||||||
|
wavesurfer?.playPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipBack() {
|
||||||
|
if (wavesurfer) {
|
||||||
|
const time = Math.max(0, wavesurfer.getCurrentTime() - 5);
|
||||||
|
wavesurfer.setTime(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipForward() {
|
||||||
|
if (wavesurfer) {
|
||||||
|
const time = Math.min(wavesurfer.getDuration(), wavesurfer.getCurrentTime() + 5);
|
||||||
|
wavesurfer.setTime(time);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Seek to a specific time in milliseconds. Called from transcript click-to-seek. */
|
||||||
|
export function seekTo(timeMs: number) {
|
||||||
|
if (wavesurfer) {
|
||||||
|
wavesurfer.setTime(timeMs / 1000);
|
||||||
|
if (!wavesurfer.isPlaying()) {
|
||||||
|
wavesurfer.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Load a new audio file. */
|
||||||
|
export function loadAudio(url: string) {
|
||||||
|
wavesurfer?.load(url);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="waveform-player">
|
<div class="waveform-player">
|
||||||
<p>Waveform Player</p>
|
<div class="waveform-container" bind:this={container}></div>
|
||||||
<p class="placeholder">wavesurfer.js will be integrated here</p>
|
<div class="controls">
|
||||||
|
<button class="control-btn" onclick={skipBack} title="Back 5s">⏪</button>
|
||||||
|
<button class="control-btn play-btn" onclick={togglePlayPause} title="Play/Pause">
|
||||||
|
{#if $isPlaying}⏸{:else}▶{/if}
|
||||||
|
</button>
|
||||||
|
<button class="control-btn" onclick={skipForward} title="Forward 5s">⏩</button>
|
||||||
|
<span class="time">{currentTime} / {totalTime}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.waveform-player {
|
.waveform-player {
|
||||||
padding: 1rem;
|
|
||||||
background: #1a1a2e;
|
background: #1a1a2e;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #e0e0e0;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
.placeholder {
|
.waveform-container {
|
||||||
color: #666;
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.control-btn {
|
||||||
|
background: #0f3460;
|
||||||
|
border: none;
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.control-btn:hover {
|
||||||
|
background: #1a4a7a;
|
||||||
|
}
|
||||||
|
.play-btn {
|
||||||
|
padding: 0.4rem 1rem;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
color: #999;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
|
margin-left: auto;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,3 +12,29 @@ export async function getProject(id: string): Promise<Project | null> {
|
|||||||
export async function listProjects(): Promise<Project[]> {
|
export async function listProjects(): Promise<Project[]> {
|
||||||
return invoke('list_projects');
|
return invoke('list_projects');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TranscriptionResult {
|
||||||
|
segments: Array<{
|
||||||
|
text: string;
|
||||||
|
start_ms: number;
|
||||||
|
end_ms: number;
|
||||||
|
words: Array<{
|
||||||
|
word: string;
|
||||||
|
start_ms: number;
|
||||||
|
end_ms: number;
|
||||||
|
confidence: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
language: string;
|
||||||
|
language_probability: number;
|
||||||
|
duration_ms: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transcribeFile(
|
||||||
|
filePath: string,
|
||||||
|
model?: string,
|
||||||
|
device?: string,
|
||||||
|
language?: string,
|
||||||
|
): Promise<TranscriptionResult> {
|
||||||
|
return invoke('transcribe_file', { filePath, model, device, language });
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,104 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import WaveformPlayer from '$lib/components/WaveformPlayer.svelte';
|
import WaveformPlayer from '$lib/components/WaveformPlayer.svelte';
|
||||||
import TranscriptEditor from '$lib/components/TranscriptEditor.svelte';
|
import TranscriptEditor from '$lib/components/TranscriptEditor.svelte';
|
||||||
import SpeakerManager from '$lib/components/SpeakerManager.svelte';
|
import SpeakerManager from '$lib/components/SpeakerManager.svelte';
|
||||||
import AIChatPanel from '$lib/components/AIChatPanel.svelte';
|
import AIChatPanel from '$lib/components/AIChatPanel.svelte';
|
||||||
|
import ProgressOverlay from '$lib/components/ProgressOverlay.svelte';
|
||||||
|
import { segments, speakers } from '$lib/stores/transcript';
|
||||||
|
import type { Segment, Word } from '$lib/types/transcript';
|
||||||
|
|
||||||
|
let waveformPlayer: WaveformPlayer;
|
||||||
|
let audioUrl = $state('');
|
||||||
|
let isTranscribing = $state(false);
|
||||||
|
let transcriptionProgress = $state(0);
|
||||||
|
let transcriptionStage = $state('');
|
||||||
|
let transcriptionMessage = $state('');
|
||||||
|
|
||||||
|
function handleWordClick(timeMs: number) {
|
||||||
|
waveformPlayer?.seekTo(timeMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleFileImport() {
|
||||||
|
// For now, use a simple prompt — will be replaced with Tauri file dialog
|
||||||
|
const filePath = prompt('Enter path to audio/video file:');
|
||||||
|
if (!filePath) return;
|
||||||
|
|
||||||
|
// Convert file path to URL for wavesurfer
|
||||||
|
// In Tauri, we can use convertFileSrc or asset protocol
|
||||||
|
audioUrl = `asset://localhost/${encodeURIComponent(filePath)}`;
|
||||||
|
waveformPlayer?.loadAudio(audioUrl);
|
||||||
|
|
||||||
|
// Start transcription
|
||||||
|
isTranscribing = true;
|
||||||
|
transcriptionProgress = 0;
|
||||||
|
transcriptionStage = 'Starting...';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await invoke<{
|
||||||
|
segments: Array<{
|
||||||
|
text: string;
|
||||||
|
start_ms: number;
|
||||||
|
end_ms: number;
|
||||||
|
words: Array<{
|
||||||
|
word: string;
|
||||||
|
start_ms: number;
|
||||||
|
end_ms: number;
|
||||||
|
confidence: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
language: string;
|
||||||
|
duration_ms: number;
|
||||||
|
}>('transcribe_file', { filePath });
|
||||||
|
|
||||||
|
// Convert result to our store format
|
||||||
|
const newSegments: Segment[] = result.segments.map((seg, idx) => ({
|
||||||
|
id: `seg-${idx}`,
|
||||||
|
project_id: '',
|
||||||
|
media_file_id: '',
|
||||||
|
speaker_id: null,
|
||||||
|
start_ms: seg.start_ms,
|
||||||
|
end_ms: seg.end_ms,
|
||||||
|
text: seg.text,
|
||||||
|
original_text: null,
|
||||||
|
confidence: null,
|
||||||
|
is_edited: false,
|
||||||
|
edited_at: null,
|
||||||
|
segment_index: idx,
|
||||||
|
words: seg.words.map((w, widx) => ({
|
||||||
|
id: `word-${idx}-${widx}`,
|
||||||
|
segment_id: `seg-${idx}`,
|
||||||
|
word: w.word,
|
||||||
|
start_ms: w.start_ms,
|
||||||
|
end_ms: w.end_ms,
|
||||||
|
confidence: w.confidence,
|
||||||
|
word_index: widx,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
segments.set(newSegments);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Transcription failed:', err);
|
||||||
|
alert(`Transcription failed: ${err}`);
|
||||||
|
} finally {
|
||||||
|
isTranscribing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="app-header">
|
||||||
|
<h1>Voice to Notes</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<button class="import-btn" onclick={handleFileImport}>
|
||||||
|
Import Audio/Video
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="workspace">
|
<div class="workspace">
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
<WaveformPlayer />
|
<WaveformPlayer bind:this={waveformPlayer} {audioUrl} />
|
||||||
<TranscriptEditor />
|
<TranscriptEditor onWordClick={handleWordClick} />
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-right">
|
<div class="sidebar-right">
|
||||||
<SpeakerManager />
|
<SpeakerManager />
|
||||||
@@ -16,23 +106,58 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ProgressOverlay
|
||||||
|
visible={isTranscribing}
|
||||||
|
percent={transcriptionProgress}
|
||||||
|
stage={transcriptionStage}
|
||||||
|
message={transcriptionMessage}
|
||||||
|
/>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.app-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: #0f3460;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.import-btn {
|
||||||
|
background: #e94560;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.import-btn:hover {
|
||||||
|
background: #d63851;
|
||||||
|
}
|
||||||
.workspace {
|
.workspace {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
height: calc(100vh - 3rem);
|
height: calc(100vh - 3.5rem);
|
||||||
|
background: #0a0a23;
|
||||||
}
|
}
|
||||||
.main-content {
|
.main-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.sidebar-right {
|
.sidebar-right {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user